From a526d3c1f2ad091d60dfda598d0687db7c9d47bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Dec 2025 00:53:45 +0000 Subject: [PATCH] feat(browser): add native action commands --- .../Resources/CanvasA2UI/a2ui.bundle.js | 236 ++++----- docs/AGENTS.default.md | 2 +- docs/browser.md | 160 +++--- docs/clawd-md.md | 13 - docs/mac/browser.md | 11 - src/browser/client-actions-core.ts | 287 ++++++++++ src/browser/client-actions-observe.ts | 207 ++++++++ src/browser/client-actions-types.ts | 15 + src/browser/client-actions.ts | 3 + src/browser/client-fetch.ts | 67 +++ src/browser/client.ts | 142 ++--- ...tool-core.test.ts => actions-core.test.ts} | 199 ++----- .../routes/{tool-core.ts => actions-core.ts} | 198 ++----- ...ol-extra.test.ts => actions-extra.test.ts} | 43 +- .../{tool-extra.ts => actions-extra.ts} | 53 +- src/browser/routes/actions.ts | 249 +++++++++ src/browser/routes/index.ts | 4 +- src/browser/routes/inspect.ts | 23 - src/browser/routes/tool.ts | 65 --- src/cli/browser-cli-actions-input.ts | 492 ++++++++++++++++++ src/cli/browser-cli-actions-observe.ts | 379 ++++++++++++++ src/cli/browser-cli-examples.ts | 48 ++ src/cli/browser-cli-inspect.ts | 273 ++++++++++ src/cli/browser-cli-manage.ts | 183 +++++++ src/cli/browser-cli-shared.ts | 1 + src/cli/browser-cli.ts | 470 +---------------- 26 files changed, 2589 insertions(+), 1234 deletions(-) delete mode 100644 docs/clawd-md.md delete mode 100644 docs/mac/browser.md create mode 100644 src/browser/client-actions-core.ts create mode 100644 src/browser/client-actions-observe.ts create mode 100644 src/browser/client-actions-types.ts create mode 100644 src/browser/client-actions.ts create mode 100644 src/browser/client-fetch.ts rename src/browser/routes/{tool-core.test.ts => actions-core.test.ts} (59%) rename src/browser/routes/{tool-core.ts => actions-core.ts} (61%) rename src/browser/routes/{tool-extra.test.ts => actions-extra.test.ts} (86%) rename src/browser/routes/{tool-extra.ts => actions-extra.ts} (89%) create mode 100644 src/browser/routes/actions.ts delete mode 100644 src/browser/routes/tool.ts create mode 100644 src/cli/browser-cli-actions-input.ts create mode 100644 src/cli/browser-cli-actions-observe.ts create mode 100644 src/cli/browser-cli-examples.ts create mode 100644 src/cli/browser-cli-inspect.ts create mode 100644 src/cli/browser-cli-manage.ts create mode 100644 src/cli/browser-cli-shared.ts diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasA2UI/a2ui.bundle.js b/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasA2UI/a2ui.bundle.js index c090cfc37..215cd3f3c 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasA2UI/a2ui.bundle.js +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasA2UI/a2ui.bundle.js @@ -15,7 +15,7 @@ var __export = (all, symbols) => { }; //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/css-tag.js +//#region node_modules/@lit/reactive-element/css-tag.js /** * @license * Copyright 2019 Google LLC @@ -60,7 +60,7 @@ const r$1 = (t$7) => new n$9("string" == typeof t$7 ? t$7 : t$7 + "", void 0, s$ })(t$7) : t$7; //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/reactive-element.js +//#region node_modules/@lit/reactive-element/reactive-element.js /** * @license * Copyright 2017 Google LLC @@ -291,7 +291,7 @@ var y = class extends HTMLElement { y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[d$2("elementProperties")] = new Map(), y[d$2("finalized")] = new Map(), p$2?.({ ReactiveElement: y }), (a$1.reactiveElementVersions ??= []).push("2.1.1"); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/lit-html.js +//#region node_modules/lit-html/lit-html.js /** * @license * Copyright 2017 Google LLC @@ -559,7 +559,7 @@ const B = (t$7, i$10, s$9) => { }; //#endregion -//#region node_modules/.pnpm/lit-element@4.2.1/node_modules/lit-element/lit-element.js +//#region node_modules/lit-element/lit-element.js /** * @license * Copyright 2017 Google LLC @@ -599,7 +599,7 @@ const n$7 = { (s$6.litElementVersions ??= []).push("4.2.1"); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/is-server.js +//#region node_modules/lit-html/is-server.js /** * @license * Copyright 2022 Google LLC @@ -608,7 +608,7 @@ const n$7 = { const o$9 = !1; //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directive.js +//#region node_modules/lit-html/directive.js /** * @license * Copyright 2017 Google LLC @@ -642,7 +642,7 @@ var i$3 = class { }; //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directive-helpers.js +//#region node_modules/lit-html/directive-helpers.js /** * @license * Copyright 2020 Google LLC @@ -678,7 +678,7 @@ var i$3 = class { }; //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/repeat.js +//#region node_modules/lit-html/directives/repeat.js /** * @license * Copyright 2017 Google LLC @@ -739,7 +739,7 @@ const u$1 = (e$14, s$9, t$7) => { }); //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/context-request-event.js +//#region node_modules/@lit/context/lib/context-request-event.js /** * @license * Copyright 2021 Google LLC @@ -755,7 +755,7 @@ var s$2 = class extends Event { }; //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/create-context.js +//#region node_modules/@lit/context/lib/create-context.js /** * @license * Copyright 2021 Google LLC @@ -766,7 +766,7 @@ function n$3(n$11) { } //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/controllers/context-consumer.js +//#region node_modules/@lit/context/lib/controllers/context-consumer.js /** * @license * Copyright 2021 Google LLC @@ -793,7 +793,7 @@ function n$3(n$11) { }; //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/value-notifier.js +//#region node_modules/@lit/context/lib/value-notifier.js /** * @license * Copyright 2021 Google LLC @@ -832,7 +832,7 @@ var s$4 = class { }; //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/controllers/context-provider.js +//#region node_modules/@lit/context/lib/controllers/context-provider.js /** * @license * Copyright 2021 Google LLC @@ -868,7 +868,7 @@ var i$2 = class extends s$4 { }; //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/context-root.js +//#region node_modules/@lit/context/lib/context-root.js /** * @license * Copyright 2021 Google LLC @@ -908,7 +908,7 @@ var i$2 = class extends s$4 { }; //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/decorators/provide.js +//#region node_modules/@lit/context/lib/decorators/provide.js /** * @license * Copyright 2017 Google LLC @@ -963,7 +963,7 @@ var i$2 = class extends s$4 { } //#endregion -//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/decorators/consume.js +//#region node_modules/@lit/context/lib/decorators/consume.js /** * @license * Copyright 2022 Google LLC @@ -2260,7 +2260,7 @@ var A2uiMessageProcessor = class A2uiMessageProcessor { }; //#endregion -//#region node_modules/.pnpm/signal-polyfill@0.2.2/node_modules/signal-polyfill/dist/index.js +//#region node_modules/signal-polyfill/dist/index.js var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, @@ -2818,7 +2818,7 @@ var Signal; })(Signal || (Signal = {})); //#endregion -//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/-private/util.ts.js +//#region node_modules/signal-utils/dist/-private/util.ts.js /** * equality check here is always false so that we can dirty the storage * via setting to _anything_ @@ -2845,7 +2845,7 @@ function fnCacheFor(context) { } //#endregion -//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/array.ts.js +//#region node_modules/signal-utils/dist/array.ts.js const ARRAY_GETTER_METHODS = new Set([ Symbol.iterator, "concat", @@ -2978,7 +2978,7 @@ function signalArray(x$1) { } //#endregion -//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/map.ts.js +//#region node_modules/signal-utils/dist/map.ts.js var SignalMap = class { collection = createStorage(); storages = new Map(); @@ -3056,7 +3056,7 @@ var SignalMap = class { Object.setPrototypeOf(SignalMap.prototype, Map.prototype); //#endregion -//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/object.ts.js +//#region node_modules/signal-utils/dist/object.ts.js /** * Implementation based of tracked-built-ins' TrackedObject * https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js @@ -3145,7 +3145,7 @@ function signalObject(obj) { } //#endregion -//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/set.ts.js +//#region node_modules/signal-utils/dist/set.ts.js var SignalSet = class { collection = createStorage(); storages = new Map(); @@ -3968,7 +3968,7 @@ const Data = { const Schemas = { A2UIClientEventMessage: server_to_client_with_standard_catalog_default }; //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/custom-element.js +//#region node_modules/@lit/reactive-element/decorators/custom-element.js /** * @license * Copyright 2017 Google LLC @@ -3981,7 +3981,7 @@ const t = (t$7) => (e$14, o$14) => { }; //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/property.js +//#region node_modules/@lit/reactive-element/decorators/property.js /** * @license * Copyright 2017 Google LLC @@ -4024,7 +4024,7 @@ function n(t$7) { } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/state.js +//#region node_modules/@lit/reactive-element/decorators/state.js /** * @license * Copyright 2017 Google LLC @@ -4038,7 +4038,7 @@ function n(t$7) { } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/event-options.js +//#region node_modules/@lit/reactive-element/decorators/event-options.js /** * @license * Copyright 2017 Google LLC @@ -4052,7 +4052,7 @@ function t$2(t$7) { } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/base.js +//#region node_modules/@lit/reactive-element/decorators/base.js /** * @license * Copyright 2017 Google LLC @@ -4061,7 +4061,7 @@ function t$2(t$7) { const e$6 = (e$14, t$7, c$7) => (c$7.configurable = !0, c$7.enumerable = !0, Reflect.decorate && "object" != typeof t$7 && Object.defineProperty(e$14, t$7, c$7), c$7); //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query.js +//#region node_modules/@lit/reactive-element/decorators/query.js /** * @license * Copyright 2017 Google LLC @@ -4093,7 +4093,7 @@ const e$6 = (e$14, t$7, c$7) => (c$7.configurable = !0, c$7.enumerable = !0, Ref } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-all.js +//#region node_modules/@lit/reactive-element/decorators/query-all.js /** * @license * Copyright 2017 Google LLC @@ -4107,7 +4107,7 @@ function r$6(r$11) { } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-async.js +//#region node_modules/@lit/reactive-element/decorators/query-async.js /** * @license * Copyright 2017 Google LLC @@ -4120,7 +4120,7 @@ function r$5(r$11) { } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-assigned-elements.js +//#region node_modules/@lit/reactive-element/decorators/query-assigned-elements.js /** * @license * Copyright 2021 Google LLC @@ -4136,7 +4136,7 @@ function r$5(r$11) { } //#endregion -//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-assigned-nodes.js +//#region node_modules/@lit/reactive-element/decorators/query-assigned-nodes.js /** * @license * Copyright 2017 Google LLC @@ -4152,7 +4152,7 @@ function r$5(r$11) { } //#endregion -//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/lib/signal-watcher.js +//#region node_modules/@lit-labs/signals/lib/signal-watcher.js /** * @license * Copyright 2023 Google LLC @@ -4215,7 +4215,7 @@ function e$5(e$14) { } //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/async-directive.js +//#region node_modules/lit-html/async-directive.js /** * @license * Copyright 2017 Google LLC @@ -4273,7 +4273,7 @@ var f = class extends i$3 { }; //#endregion -//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/lib/watch.js +//#region node_modules/@lit-labs/signals/lib/watch.js /** * @license * Copyright 2023 Google LLC @@ -4318,7 +4318,7 @@ var f = class extends i$3 { const o$4 = e$1(h$1); //#endregion -//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/lib/html-tag.js +//#region node_modules/@lit-labs/signals/lib/html-tag.js /** * @license * Copyright 2023 Google LLC @@ -4326,7 +4326,7 @@ const o$4 = e$1(h$1); */ const m = (o$14) => (t$7, ...m$3) => o$14(t$7, ...m$3.map(((o$15) => o$15 instanceof Signal.State || o$15 instanceof Signal.Computed ? o$4(o$15) : o$15))), l = m(x), r$2 = m(b); //#endregion -//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/index.js +//#region node_modules/@lit-labs/signals/index.js /** * @license * Copyright 2023 Google LLC @@ -4334,7 +4334,7 @@ const o$4 = e$1(h$1); */ const l$1 = Signal.State, o$5 = Signal.Computed, r$3 = (l$5, o$14) => new Signal.State(l$5, o$14), i$5 = (l$5, o$14) => new Signal.Computed(l$5, o$14); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/map.js +//#region node_modules/lit-html/directives/map.js /** * @license * Copyright 2021 Google LLC @@ -4348,7 +4348,7 @@ function* o$3(o$14, f$4) { } //#endregion -//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/subtle/microtask-effect.ts.js +//#region node_modules/signal-utils/dist/subtle/microtask-effect.ts.js let pending = false; let watcher = new Signal.subtle.Watcher(() => { if (!pending) { @@ -5084,7 +5084,7 @@ let Root = (() => { })(); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/class-map.js +//#region node_modules/lit-html/directives/class-map.js /** * @license * Copyright 2018 Google LLC @@ -5113,7 +5113,7 @@ let Root = (() => { }); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/style-map.js +//#region node_modules/lit-html/directives/style-map.js /** * @license * Copyright 2018 Google LLC @@ -7160,7 +7160,7 @@ let MultipleChoice = (() => { })(); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/ref.js +//#region node_modules/lit-html/directives/ref.js /** * @license * Copyright 2020 Google LLC @@ -8585,7 +8585,7 @@ let TextField = (() => { })(); //#endregion -//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/unsafe-html.js +//#region node_modules/lit-html/directives/unsafe-html.js /** * @license * Copyright 2017 Google LLC @@ -8612,7 +8612,7 @@ e$2.directiveName = "unsafeHTML", e$2.resultType = 1; const o$1 = e$1(e$2); //#endregion -//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/decode.mjs +//#region node_modules/mdurl/lib/decode.mjs const decodeCache = {}; function getDecodeCache(exclude) { let cache = decodeCache[exclude]; @@ -8696,7 +8696,7 @@ decode$2.componentChars = ""; var decode_default = decode$2; //#endregion -//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/encode.mjs +//#region node_modules/mdurl/lib/encode.mjs const encodeCache = {}; function getEncodeCache(exclude) { let cache = encodeCache[exclude]; @@ -8761,7 +8761,7 @@ encode$2.componentChars = "-_.!~*'()"; var encode_default = encode$2; //#endregion -//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/format.mjs +//#region node_modules/mdurl/lib/format.mjs function format(url) { let result = ""; result += url.protocol || ""; @@ -8781,7 +8781,7 @@ function format(url) { ; //#endregion -//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/parse.mjs +//#region node_modules/mdurl/lib/parse.mjs function Url() { this.protocol = null; this.slashes = null; @@ -8990,7 +8990,7 @@ Url.prototype.parseHost = function(host) { var parse_default = urlParse; //#endregion -//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/index.mjs +//#region node_modules/mdurl/index.mjs var mdurl_exports = /* @__PURE__ */ __export({ decode: () => decode_default, encode: () => encode_default, @@ -8999,31 +8999,31 @@ var mdurl_exports = /* @__PURE__ */ __export({ }); //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/properties/Any/regex.mjs +//#region node_modules/uc.micro/properties/Any/regex.mjs var regex_default = /[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/Cc/regex.mjs +//#region node_modules/uc.micro/categories/Cc/regex.mjs var regex_default$1 = /[\0-\x1F\x7F-\x9F]/; //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/Cf/regex.mjs +//#region node_modules/uc.micro/categories/Cf/regex.mjs var regex_default$4 = /[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/; //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/P/regex.mjs +//#region node_modules/uc.micro/categories/P/regex.mjs var regex_default$3 = /[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/; //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/S/regex.mjs +//#region node_modules/uc.micro/categories/S/regex.mjs var regex_default$5 = /[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/; //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/Z/regex.mjs +//#region node_modules/uc.micro/categories/Z/regex.mjs var regex_default$2 = /[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/; //#endregion -//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/index.mjs +//#region node_modules/uc.micro/index.mjs var uc_exports = /* @__PURE__ */ __export({ Any: () => regex_default, Cc: () => regex_default$1, @@ -9034,15 +9034,15 @@ var uc_exports = /* @__PURE__ */ __export({ }); //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/generated/decode-data-html.js +//#region node_modules/entities/lib/esm/generated/decode-data-html.js var decode_data_html_default = new Uint16Array("ᵁ<Õıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms„‹•˜¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀𝔄rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀𝔸plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀𝒜ign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀𝔅pf;쀀𝔹eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀𝒞pĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀𝔇Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀𝔻ƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegraìȹoɴ͹\0\0ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀𝒟rok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀𝔈rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀𝔼silon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀𝔉lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀𝔽All;戀riertrf;愱cò׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀𝔊;拙pf;쀀𝔾eater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀𝒢;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀𝕀a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻Ï䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀𝔍pf;쀀𝕁ǣ߇\0ߌr;쀀𝒥rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀𝔎pf;쀀𝕂cr;쀀𝒦րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀𝔏Ā;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀𝕃erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀𝔐nusPlus;戓pf;쀀𝕄cò੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘ë૙eryThiî૙tedĀGL૸ଆreaterGreateòٳessLesóੈLine;䀊r;쀀𝔑ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ೒拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨೹setĀ;E೰ೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀𝒩ilde耻Ñ䃑;䎝܀Eacdfgmoprstuvලෂ෉෕ෛ෠෧෼ขภยา฿ไlig;䅒cute耻Ó䃓Āiy෎ීrc耻Ô䃔;䐞blac;䅐r;쀀𝔒rave耻Ò䃒ƀaei෮ෲ෶cr;䅌ga;䎩cron;䎟pf;쀀𝕆enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀𝒪ash耻Ø䃘iŬื฼de耻Õ䃕es;樷ml耻Ö䃖erĀBP๋๠Āar๐๓r;怾acĀek๚๜;揞et;掴arenthesis;揜Ҁacfhilors๿ງຊຏຒດຝະ໼rtialD;戂y;䐟r;쀀𝔓i;䎦;䎠usMinus;䂱Āipຢອncareplanåڝf;愙Ȁ;eio຺ູ໠໤檻cedesȀ;EST່້໏໚扺qual;檯lantEqual;扼ilde;找me;怳Ādp໩໮uct;戏ortionĀ;aȥ໹l;戝Āci༁༆r;쀀𝒫;䎨ȀUfos༑༖༛༟OT耻\"䀢r;쀀𝔔pf;愚cr;쀀𝒬؀BEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻®䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r»ཹo;䎡ghtЀACDFTUVa࿁࿫࿳ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL࿜࿝࿡憒ar;懥eftArrow;懄eiling;按oǵ࿹\0စbleBracket;柧nǔည\0နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀𝔖ortȀDLRUᄪᄴᄾᅉownArrow»ОeftArrow»࢚ightArrow»࿝pArrow;憑gma;䎣allCircle;战pf;쀀𝕊ɲᅭ\0\0ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀𝒮ar;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Tháྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et»ሓրHRSacfhiorsሾቄ቉ቕ቞ቱቶኟዂወዑORN耻Þ䃞ADE;愢ĀHc቎ቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀𝔗Āeiቻ኉Dzኀ\0ኇefore;戴a;䎘Ācn኎ኘkSpace;쀀  Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀𝕋ipleDot;惛Āctዖዛr;쀀𝒯rok;䅦ૡዷጎጚጦ\0ጬጱ\0\0\0\0\0ጸጽ፷ᎅ\0᏿ᐄᐊᐐĀcrዻጁute耻Ú䃚rĀ;oጇገ憟cir;楉rǣጓ\0጖y;䐎ve;䅬Āiyጞጣrc耻Û䃛;䐣blac;䅰r;쀀𝔘rave耻Ù䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻፿on;䅲f;쀀𝕌ЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥ownáϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀𝒰ilde;䅨ml耻Ü䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀𝔙pf;쀀𝕍cr;쀀𝒱dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀𝔚pf;쀀𝕎cr;쀀𝒲Ȁfiosᓋᓐᓒᓘr;쀀𝔛;䎞pf;쀀𝕏cr;쀀𝒳ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻Ý䃝Āiyᔉᔍrc;䅶;䐫r;쀀𝔜pf;쀀𝕐cr;쀀𝒴ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\0ᕛoWidtè૙a;䎖r;愨pf;愤cr;쀀𝒵௡ᖃᖊᖐ\0ᖰᖶᖿ\0\0\0\0ᗆᗛᗫᙟ᙭\0ᚕ᚛ᚲᚹ\0ᚾcute耻á䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻â䃢te肻´̆;䐰lig耻æ䃦Ā;r²ᖺ;쀀𝔞rave耻à䃠ĀepᗊᗖĀfpᗏᗔsym;愵èᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\0\0ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚΀;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e»ᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢»¹arr;捼Āgpᙣᙧon;䄅f;쀀𝕒΀;Eaeiop዁ᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;e዁ᚒñᚃing耻å䃥ƀctyᚡᚦᚨr;쀀𝒶;䀪mpĀ;e዁ᚯñʈilde耻ã䃣ml耻ä䃤Āciᛂᛈoninôɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰ᜼ᝃᝈ᝸᝽០៦ᠹᡐᜍ᤽᥈ᥰot;櫭Ācrᛶ᜞kȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e᜚᜛戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e»ᜭrkĀ;t፜᜷brk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓ᝛ᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯ᝱ᝳ;䎲;愶een;扬r;쀀𝔟g΀costuvwឍឝឳេ៕៛៞ƀaiuបពរðݠrc;旯p»፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\0\0ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄eåᑄåᒭarow;植ƀako៭ᠦᠵĀcn៲ᠣkƀlst៺֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘᠝斴own;斾eft;旂ight;斸k;搣Ʊᠫ\0ᠳƲᠯ\0ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀𝕓Ā;tᏋᡣom»Ꮜtie;拈؀DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬ᣿ᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教΀;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ᣷᣹᣻᣽;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ᤟;敛;敘;攘;攔΀;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģ᥂bar耻¦䂦Ȁceioᥑᥖᥚᥠr;쀀𝒷mi;恏mĀ;e᜚᜜lƀ;bhᥨᥩᥫ䁜;槅sub;柈Ŭᥴ᥾lĀ;e᥹᥺怢t»᥺pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\0᧨ᨑᨕᨲ\0ᨷᩐ\0\0᪴\0\0᫁\0\0ᬡᬮ᭍᭒\0᯽\0ᰌƀcpr᦭ᦲ᧝ute;䄇̀;abcdsᦿᧀᧄ᧊᧕᧙戩nd;橄rcup;橉Āau᧏᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁îړȀaeiu᧰᧻ᨁᨅǰ᧵\0᧸s;橍on;䄍dil耻ç䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻¸ƭptyv;榲t脀¢;eᨭᨮ䂢räƲr;쀀𝔠ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark»ᩈ;䏇r΀;Ecefms᩟᩠ᩢᩫ᪤᪪᪮旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\0\0᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖᪚᪟»ཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it»᪼ˬ᫇᫔᫺\0ᬊonĀ;eᫍᫎ䀺Ā;qÇÆɭ᫙\0\0᫢aĀ;t᫞᫟䀬;䁀ƀ;fl᫨᫩᫫戁îᅠeĀmx᫱᫶ent»᫩eóɍǧ᫾\0ᬇĀ;dኻᬂot;橭nôɆƀfryᬐᬔᬗ;쀀𝕔oäɔ脀©;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀𝒸Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯΀delprvw᭠᭬᭷ᮂᮬᯔ᯹arrĀlr᭨᭪;椸;椵ɰ᭲\0\0᭵r;拞c;拟arrĀ;p᭿ᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\0\0ᯒreã᭳uã᭵ee;拎edge;拏en耻¤䂤earrowĀlrᯮ᯳eft»ᮀight»ᮽeäᯝĀciᰁᰇoninôǷnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰸᰻᰿ᱝᱩᱵᲊᲞᲬᲷ᳻᳿ᴍᵻᶑᶫᶻ᷆᷍rò΁ar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸òᄳhĀ;vᱚᱛ怐»ऊūᱡᱧarow;椏aã̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻°䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀𝔡arĀlrᲳᲵ»ࣜ»သʀaegsv᳂͸᳖᳜᳠mƀ;oș᳊᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀÷;o᳧ᳰntimes;拇nø᳷cy;䑒cɯᴆ\0\0ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀𝕕ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedgåúnƀadhᄮᵝᵧownarrowóᲃarpoonĀlrᵲᵶefôᲴighôᲶŢᵿᶅkaro÷གɯᶊ\0\0ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀𝒹;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃ròЩaòྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄ὎὚ĀDoḆᴴoôᲉĀcsḎḔute耻é䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻ê䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀𝔢ƀ;rsṐṑṗ檚ave耻è䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et»ẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀𝕖ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on»ớ;䏵ȀcsuvỪỳἋἣĀioữḱrc»Ḯɩỹ\0\0ỻíՈantĀglἂἆtr»ṝess»Ṻƀaeiἒ἖Ἒls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯oô͒ĀahὉὋ;䎷耻ð䃰Āmrὓὗl耻ë䃫o;悬ƀcipὡὤὧl;䀡sôծĀeoὬὴctatioîՙnentialåչৡᾒ\0ᾞ\0ᾡᾧ\0\0ῆῌ\0ΐ\0ῦῪ \0 ⁚llingdotseñṄy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\0\0᾽g;耀ffig;耀ffl;쀀𝔣lig;耀filig;쀀fjƀaltῙ῜ῡt;晭ig;耀flns;斱of;䆒ǰ΅\0ῳf;쀀𝕗ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao‌⁕Ācs‑⁒ႉ‸⁅⁈\0⁐β•‥‧‪‬\0‮耻½䂽;慓耻¼䂼;慕;慙;慛Ƴ‴\0‶;慔;慖ʴ‾⁁\0\0⁃耻¾䂾;慗;慜5;慘ƶ⁌\0⁎;慚;慝8;慞l;恄wn;挢cr;쀀𝒻ࢀEabcdefgijlnorstv₂₉₟₥₰₴⃰⃵⃺⃿℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕ₝ute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽⃉ƀ;qsؾٌ⃄lanô٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀𝔤Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox»ℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀𝕘Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqr׮ⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\0↎proø₞r;楸qĀlqؿ↖lesó₈ií٫Āen↣↭rtneqq;쀀≩︀Å↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽ròΠȀilmr⇐⇔⇗⇛rsðᒄf»․ilôکĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it»∊lip;怦con;抹r;쀀𝔥sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀𝕙bar;怕ƀclt≯≴≸r;쀀𝒽asè⇴rok;䄧Ābp⊂⊇ull;恃hen»ᱛૡ⊣\0⊪\0⊸⋅⋎\0⋕⋳\0\0⋸⌢⍧⍢⍿\0⎆⎪⎴cute耻í䃭ƀ;iyݱ⊰⊵rc耻î䃮;䐸Ācx⊼⊿y;䐵cl耻¡䂡ĀfrΟ⋉;쀀𝔦rave耻ì䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓inåގarôܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝doô⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙eróᕣã⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀𝕚a;䎹uest耻¿䂿Āci⎊⎏r;쀀𝒾nʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\0⎼cy;䑖l耻ï䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀𝔧ath;䈷pf;쀀𝕛ǣ⏬\0⏱r;쀀𝒿rcy;䑘kcy;䑔Ѐacfghjos␋␖␢␧␭␱␵␻ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀𝔨reen;䄸cy;䑅cy;䑜pf;쀀𝕜cr;쀀𝓀஀ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼rò৆òΕail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\0⒪\0⒱\0\0\0\0\0⒵Ⓔ\0ⓆⓈⓍ\0⓹ute;䄺mptyv;榴raîࡌbda;䎻gƀ;dlࢎⓁⓃ;榑åࢎ;檅uo耻«䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝ë≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼ìࢰâ┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□aé⓶arpoonĀdu▯▴own»њp»०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoonó྘quigarro÷⇰hreetimes;拋ƀ;qs▋ও◺lanôবʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋pproøⓆot;拖qĀgq♃♅ôউgtò⒌ôছiíলƀilr♕࣡♚sht;楼;쀀𝔩Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖rò◁orneòᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che»⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox»⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽rëࣁgƀlmr⛿✍✔eftĀar০✇ightá৲apsto;柼ightá৽parrowĀlr✥✩efô⓭ight;憬ƀafl✶✹✽r;榅;쀀𝕝us;樭imes;樴š❋❏st;戗áፎƀ;ef❗❘᠀旊nge»❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇ròࢨorneòᶌarĀ;d྘➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀𝓁mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹reå◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ᠛旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀Å⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻¯䂯Āet⡗⡙;時Ā;e⡞⡟朠se»⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻owîҌefôएðᏑker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle»ᘦr;쀀𝔪o;愧ƀcdn⢯⢴⣉ro耻µ䂵Ȁ;acdᑤ⢽⣀⣄sôᚧir;櫰ot肻·Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛ò−ðઁĀdp⣩⣮els;抧f;쀀𝕞Āct⣸⣽r;쀀𝓂pos»ᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐௏쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la»˞ute;䅄g;쀀∠⃒ʀ;Eiop඄⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉roø඄urĀ;a⧓⧔普lĀ;s⧓ସdz⧟\0⧣p肻\xA0ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\0⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓΀;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸uiöୣĀei⩊⩎ar;椨í஘istĀ;s஠டr;쀀𝔫ȀEest௅⩦⩹⩼ƀ;qs஼⩭௡ƀ;qs஼௅⩴lanô௢ií௪Ā;rஶ⪁»ஷƀAap⪊⪍⪑rò⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚΀AEadest⪷⪺⪾⫂⫅⫶⫹rò⥦;쀀≦̸rr;憚r;急Ȁ;fqs఻⫎⫣⫯tĀar⫔⫙rro÷⫁ightarro÷⪐ƀ;qs఻⪺⫪lanôౕĀ;sౕ⫴»శiíౝĀ;rవ⫾iĀ;eచథiäඐĀpt⬌⬑f;쀀𝕟膀¬;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast୻⭕⭚⭟lleì୻l;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳uåಥĀ;cಘ⭸Ā;eಒ⭽ñಘȀAait⮈⮋⮝⮧rò⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow»⮕riĀ;eೋೖ΀chimpqu⮽⯍⯙⬄୸⯤⯯Ȁ;cerല⯆ഷ⯉uå൅;쀀𝓃ortɭ⬅\0\0⯖ará⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭å೸åഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗñസȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇìௗlde耻ñ䃱çృiangleĀlrⱒⱜeftĀ;eచⱚñదightĀ;eೋⱥñ೗Ā;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰⳴ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\0\0\0\0\0\0\0\0\0\0\0\0\0ⴭ\0ⴸⵈⵠⵥ⵲ⶄᬇ\0\0ⶍⶫ\0ⷈⷎ\0ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻ó䃳ĀiyⴼⵅrĀ;c᪞ⵂ耻ô䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācr⵩⵭ir;榿;쀀𝔬ͯ⵹\0\0⵼\0ⶂn;䋛ave耻ò䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕ⶘ⶥⶨrò᪀Āir⶝ⶠr;榾oss;榻nå๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀𝕠ƀaelⷔ⷗ǒr;榷rp;榹΀;adiosvⷪⷫⷮ⸈⸍⸐⸖戨rò᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f»ⷿ耻ª䂪耻º䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧ò⸁ash耻ø䃸l;折iŬⸯ⸴de耻õ䃵esĀ;aǛ⸺s;樶ml耻ö䃶bar;挽ૡ⹞\0⹽\0⺀⺝\0⺢⺹\0\0⻋ຜ\0⼓\0\0⼫⾼\0⿈rȀ;astЃ⹧⹲຅脀¶;l⹭⹮䂶leìЃɩ⹸\0\0⹻m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀𝔭ƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕maô੶ne;明ƀ;tv⺿⻀⻈䏀chfork»´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎ö⇴sҀ;abcdemst⻳⻴ᤈ⻹⻽⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻±ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀𝕡nd耻£䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷uå໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾pproø⽃urlyeñ໙ñ໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨iíໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺ð⽵ƀdfp໬⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t໻⾴ï໻rel;抰Āci⿀⿅r;쀀𝓅;䏈ncsp;怈̀fiopsu⿚⋢⿟⿥⿫⿱r;쀀𝔮pf;쀀𝕢rime;恗cr;쀀𝓆ƀaeo⿸〉〓tĀei⿾々rnionóڰnt;樖stĀ;e【】䀿ñἙô༔઀ABHabcdefhilmnoprstux぀けさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがròႳòϝail;検aròᱥar;楤΀cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕iãᅮmptyv;榳gȀ;del࿑らるろ;榒;榥å࿑uo耻»䂻rր;abcfhlpstw࿜ガクシスゼゾダッデナp;極Ā;f࿠ゴs;椠;椳s;椞ë≝ð✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶aló༞ƀabrョリヮrò៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes㄂㄄;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗ì࿲âヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜnåႻarôྩt;断ƀilrㅩဣㅮsht;楽;쀀𝔯ĀaoㅷㆆrĀduㅽㅿ»ѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘㇤㇮rrowĀ;t࿜ㆭaéトarpoonĀduㆻㆿowîㅾp»႒eftĀah㇊㇐rrowó࿪arpoonóՑightarrows;應quigarro÷ニhreetimes;拌g;䋚ingdotseñἲƀahm㈍㈐㈓rò࿪aòՑ;怏oustĀ;a㈞㈟掱che»㈟mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾rëဃƀafl㉇㉊㉎r;榆;쀀𝕣us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒arò㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀𝓇Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠reåㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\0㍺㎤\0\0㏬㏰\0㐨㑈㑚㒭㒱㓊㓱\0㘖\0\0㘳cute;䅛quï➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\0㋼;檸on;䅡uåᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓iíሄ;䑁otƀ;be㌴ᵇ㌵担;橦΀Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒ë∨Ā;oਸ਼਴t耻§䂧i;䀻war;椩mĀin㍩ðnuóñt;朶rĀ;o㍶⁕쀀𝔰Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\0\0㎜iäᑤaraì⹯耻­䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;q኱ኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲aròᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetmé㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀𝕤aĀdr㑍ЂesĀ;u㑔㑕晠it»㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍ñᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝ñᆮƀ;afᅻ㒦ְrť㒫ֱ»ᅼaròᅈȀcemt㒹㒾㓂㓅r;쀀𝓈tmîñiì㐕aræᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psiloîỠhé⺯s»⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦pproø㋺urlyeñᇾñᇳƀaes㖂㖈㌛pproø㌚qñ㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻¹䂹耻²䂲耻³䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨ë∮Ā;oਫ਩war;椪lig耻ß䃟௡㙑㙝㙠ዎ㙳㙹\0㙾㛂\0\0\0\0\0㛛㜃\0㜉㝬\0\0\0㞇ɲ㙖\0\0㙛get;挖;䏄rë๟ƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀𝔱Ȁeiko㚆㚝㚵㚼Dz㚋\0㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮pproø዁im»ኬsðኞĀas㚺㚮ð዁rn耻þ䃾Ǭ̟㛆⋧es膀×;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀á⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀𝕥rk;櫚á㍢rime;怴ƀaip㜏㜒㝤dåቈ΀adempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own»ᶻeftĀ;e⠀㜾ñम;扜ightĀ;e㊪㝋ñၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀𝓉;䑆cy;䑛rok;䅧Āio㞋㞎xô᝷headĀlr㞗㞠eftarro÷ࡏightarrow»ཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶ròϭar;楣Ācr㟜㟢ute耻ú䃺òᅐrǣ㟪\0㟭y;䑞ve;䅭Āiy㟵㟺rc耻û䃻;䑃ƀabh㠃㠆㠋ròᎭlac;䅱aòᏃĀir㠓㠘sht;楾;쀀𝔲rave耻ù䃹š㠧㠱rĀlr㠬㠮»ॗ»ႃlk;斀Āct㠹㡍ɯ㠿\0\0㡊rnĀ;e㡅㡆挜r»㡆op;挏ri;旸Āal㡖㡚cr;䅫肻¨͉Āgp㡢㡦on;䅳f;쀀𝕦̀adhlsuᅋ㡸㡽፲㢑㢠ownáᎳarpoonĀlr㢈㢌efô㠭ighô㠯iƀ;hl㢙㢚㢜䏅»ᏺon»㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\0\0㣁rnĀ;e㢼㢽挝r»㢽op;挎ng;䅯ri;旹cr;쀀𝓊ƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨»᠓Āam㣯㣲rò㢨l耻ü䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠ròϷarĀ;v㤦㤧櫨;櫩asèϡĀnr㤲㤷grt;榜΀eknprst㓣㥆㥋㥒㥝㥤㦖appá␕othinçẖƀhir㓫⻈㥙opô⾵Ā;hᎷ㥢ïㆍĀiu㥩㥭gmá㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟etá㚜iangleĀlr㦪㦯eft»थight»ၑy;䐲ash»ံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨaòᑩr;쀀𝔳tré㦮suĀbp㧯㧱»ജ»൙pf;쀀𝕧roð໻tré㦴Ācu㨆㨋r;쀀𝓋Ābp㨐㨘nĀEe㦀㨖»㥾nĀEe㦒㨞»㦐igzag;榚΀cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀𝔴pf;쀀𝕨Ā;eᑹ㩦atèᑹcr;쀀𝓌ૣណ㪇\0㪋\0㪐㪛\0\0㪝㪨㪫㪯\0\0㫃㫎\0㫘ៜ៟tré៑r;쀀𝔵ĀAa㪔㪗ròσrò৶;䎾ĀAa㪡㪤ròθrò৫að✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀𝕩imåឲĀAa㫇㫊ròώròਁĀcq㫒ីr;쀀𝓍Āpt៖㫜ré។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻ý䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻¥䂥r;쀀𝔶cy;䑗pf;쀀𝕪cr;쀀𝓎Ācm㬦㬩y;䑎l耻ÿ䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡træᕟa;䎶r;쀀𝔷cy;䐶grarr;懝pf;쀀𝕫cr;쀀𝓏Ājn㮅㮇;怍j;怌".split("").map((c$7) => c$7.charCodeAt(0))); //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/generated/decode-data-xml.js +//#region node_modules/entities/lib/esm/generated/decode-data-xml.js var decode_data_xml_default = new Uint16Array("Ȁaglq \x1Bɭ\0\0p;䀦os;䀧t;䀾t;䀼uot;䀢".split("").map((c$7) => c$7.charCodeAt(0))); //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/decode_codepoint.js +//#region node_modules/entities/lib/esm/decode_codepoint.js var _a; const decodeMap = new Map([ [0, 65533], @@ -9111,7 +9111,7 @@ function decodeCodePoint(codePoint) { } //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/decode.js +//#region node_modules/entities/lib/esm/decode.js var CharCodes; (function(CharCodes$1) { CharCodes$1[CharCodes$1["NUM"] = 35] = "NUM"; @@ -9545,7 +9545,7 @@ function decodeXML(str) { } //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/generated/encode-html.js +//#region node_modules/entities/lib/esm/generated/encode-html.js function restoreDiff(arr) { for (let i$10 = 1; i$10 < arr.length; i$10++) { arr[i$10][0] += arr[i$10 - 1][0] + 1; @@ -11254,7 +11254,7 @@ var encode_html_default = new Map(/* @__PURE__ */ restoreDiff([ ])); //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/escape.js +//#region node_modules/entities/lib/esm/escape.js const xmlReplacer = /["&'<>$\x80-\uFFFF]/g; const xmlCodeMap = new Map([ [34, """], @@ -11357,7 +11357,7 @@ const escapeText = getEscaper(/[&<>\u00A0]/g, new Map([ ])); //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/encode.js +//#region node_modules/entities/lib/esm/encode.js const htmlReplacer = /[\t\n!-,./:-@[-`\f{-}$\x80-\uFFFF]/g; /** * Encodes all characters in the input using HTML entities. This includes @@ -11418,7 +11418,7 @@ function encodeHTMLTrieRe(regExp, str) { } //#endregion -//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/index.js +//#region node_modules/entities/lib/esm/index.js /** The level of entities to support. */ var EntityLevel; (function(EntityLevel$1) { @@ -11504,7 +11504,7 @@ function encode$1(data, options = EntityLevel.XML) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/common/utils.mjs +//#region node_modules/markdown-it/lib/common/utils.mjs var utils_exports$1 = /* @__PURE__ */ __export({ arrayReplaceAt: () => arrayReplaceAt, assign: () => assign$1, @@ -11722,7 +11722,7 @@ const lib = { }; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/helpers/parse_link_label.mjs +//#region node_modules/markdown-it/lib/helpers/parse_link_label.mjs function parseLinkLabel(state, start, disableNested) { let level, found, marker, prevPos; const max = state.posMax; @@ -11758,7 +11758,7 @@ function parseLinkLabel(state, start, disableNested) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/helpers/parse_link_destination.mjs +//#region node_modules/markdown-it/lib/helpers/parse_link_destination.mjs function parseLinkDestination(str, start, max) { let code$1; let pos = start; @@ -11834,7 +11834,7 @@ function parseLinkDestination(str, start, max) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/helpers/parse_link_title.mjs +//#region node_modules/markdown-it/lib/helpers/parse_link_title.mjs function parseLinkTitle(str, start, max, prev_state) { let code$1; let pos = start; @@ -11883,7 +11883,7 @@ function parseLinkTitle(str, start, max, prev_state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/helpers/index.mjs +//#region node_modules/markdown-it/lib/helpers/index.mjs var helpers_exports = /* @__PURE__ */ __export({ parseLinkDestination: () => parseLinkDestination, parseLinkLabel: () => parseLinkLabel, @@ -11891,7 +11891,7 @@ var helpers_exports = /* @__PURE__ */ __export({ }); //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/renderer.mjs +//#region node_modules/markdown-it/lib/renderer.mjs /** * class Renderer * @@ -12134,7 +12134,7 @@ Renderer.prototype.render = function(tokens, options, env) { var renderer_default = Renderer; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/ruler.mjs +//#region node_modules/markdown-it/lib/ruler.mjs /** * class Ruler * @@ -12437,7 +12437,7 @@ Ruler.prototype.getRules = function(chainName) { var ruler_default = Ruler; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/token.mjs +//#region node_modules/markdown-it/lib/token.mjs /** * class Token **/ @@ -12610,7 +12610,7 @@ Token.prototype.attrJoin = function attrJoin(name, value) { var token_default = Token; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/state_core.mjs +//#region node_modules/markdown-it/lib/rules_core/state_core.mjs function StateCore(src, md, env) { this.src = src; this.env = env; @@ -12622,7 +12622,7 @@ StateCore.prototype.Token = token_default; var state_core_default = StateCore; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/normalize.mjs +//#region node_modules/markdown-it/lib/rules_core/normalize.mjs const NEWLINES_RE = /\r\n?|\n/g; const NULL_RE = /\0/g; function normalize(state) { @@ -12633,7 +12633,7 @@ function normalize(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/block.mjs +//#region node_modules/markdown-it/lib/rules_core/block.mjs function block(state) { let token; if (state.inlineMode) { @@ -12648,7 +12648,7 @@ function block(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/inline.mjs +//#region node_modules/markdown-it/lib/rules_core/inline.mjs function inline(state) { const tokens = state.tokens; for (let i$10 = 0, l$5 = tokens.length; i$10 < l$5; i$10++) { @@ -12660,7 +12660,7 @@ function inline(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/linkify.mjs +//#region node_modules/markdown-it/lib/rules_core/linkify.mjs function isLinkOpen$1(str) { return /^\s]/i.test(str); } @@ -12758,7 +12758,7 @@ function linkify$1(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/replacements.mjs +//#region node_modules/markdown-it/lib/rules_core/replacements.mjs const RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; const SCOPED_ABBR_TEST_RE = /\((c|tm|r)\)/i; const SCOPED_ABBR_RE = /\((c|tm|r)\)/gi; @@ -12821,7 +12821,7 @@ function replace(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/smartquotes.mjs +//#region node_modules/markdown-it/lib/rules_core/smartquotes.mjs const QUOTE_TEST_RE = /['"]/; const QUOTE_RE = /['"]/g; const APOSTROPHE = "’"; @@ -12967,7 +12967,7 @@ function smartquotes(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_core/text_join.mjs +//#region node_modules/markdown-it/lib/rules_core/text_join.mjs function text_join(state) { let curr, last; const blockTokens = state.tokens; @@ -12998,7 +12998,7 @@ function text_join(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/parser_core.mjs +//#region node_modules/markdown-it/lib/parser_core.mjs /** internal * class Core * @@ -13043,7 +13043,7 @@ Core.prototype.State = state_core_default; var parser_core_default = Core; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/state_block.mjs +//#region node_modules/markdown-it/lib/rules_block/state_block.mjs function StateBlock(src, md, env, tokens) { this.src = src; this.md = md; @@ -13201,7 +13201,7 @@ StateBlock.prototype.Token = token_default; var state_block_default = StateBlock; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/table.mjs +//#region node_modules/markdown-it/lib/rules_block/table.mjs const MAX_AUTOCOMPLETED_CELLS = 65536; function getLine(state, line) { const pos = state.bMarks[line] + state.tShift[line]; @@ -13392,7 +13392,7 @@ function table(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/code.mjs +//#region node_modules/markdown-it/lib/rules_block/code.mjs function code(state, startLine, endLine) { if (state.sCount[startLine] - state.blkIndent < 4) { return false; @@ -13419,7 +13419,7 @@ function code(state, startLine, endLine) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/fence.mjs +//#region node_modules/markdown-it/lib/rules_block/fence.mjs function fence(state, startLine, endLine, silent) { let pos = state.bMarks[startLine] + state.tShift[startLine]; let max = state.eMarks[startLine]; @@ -13489,7 +13489,7 @@ function fence(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/blockquote.mjs +//#region node_modules/markdown-it/lib/rules_block/blockquote.mjs function blockquote(state, startLine, endLine, silent) { let pos = state.bMarks[startLine] + state.tShift[startLine]; let max = state.eMarks[startLine]; @@ -13615,7 +13615,7 @@ function blockquote(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/hr.mjs +//#region node_modules/markdown-it/lib/rules_block/hr.mjs function hr(state, startLine, endLine, silent) { const max = state.eMarks[startLine]; if (state.sCount[startLine] - state.blkIndent >= 4) { @@ -13650,7 +13650,7 @@ function hr(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/list.mjs +//#region node_modules/markdown-it/lib/rules_block/list.mjs function skipBulletListMarker(state, startLine) { const max = state.eMarks[startLine]; let pos = state.bMarks[startLine] + state.tShift[startLine]; @@ -13875,7 +13875,7 @@ function list(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/reference.mjs +//#region node_modules/markdown-it/lib/rules_block/reference.mjs function reference(state, startLine, _endLine, silent) { let pos = state.bMarks[startLine] + state.tShift[startLine]; let max = state.eMarks[startLine]; @@ -14053,7 +14053,7 @@ function reference(state, startLine, _endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/common/html_blocks.mjs +//#region node_modules/markdown-it/lib/common/html_blocks.mjs var html_blocks_default = [ "address", "article", @@ -14120,7 +14120,7 @@ var html_blocks_default = [ ]; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/common/html_re.mjs +//#region node_modules/markdown-it/lib/common/html_re.mjs const attr_name = "[a-zA-Z_:][a-zA-Z0-9:._-]*"; const unquoted = "[^\"'=<>`\\x00-\\x20]+"; const single_quoted = "'[^']*'"; @@ -14137,7 +14137,7 @@ const HTML_TAG_RE = new RegExp("^(?:" + open_tag + "|" + close_tag + "|" + comme const HTML_OPEN_CLOSE_TAG_RE = new RegExp("^(?:" + open_tag + "|" + close_tag + ")"); //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/html_block.mjs +//#region node_modules/markdown-it/lib/rules_block/html_block.mjs const HTML_SEQUENCES = [ [ /^<(script|pre|style|textarea)(?=(\s|>|$))/i, @@ -14225,7 +14225,7 @@ function html_block(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/heading.mjs +//#region node_modules/markdown-it/lib/rules_block/heading.mjs function heading(state, startLine, endLine, silent) { let pos = state.bMarks[startLine] + state.tShift[startLine]; let max = state.eMarks[startLine]; @@ -14267,7 +14267,7 @@ function heading(state, startLine, endLine, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/lheading.mjs +//#region node_modules/markdown-it/lib/rules_block/lheading.mjs function lheading(state, startLine, endLine) { const terminatorRules = state.md.block.ruler.getRules("paragraph"); if (state.sCount[startLine] - state.blkIndent >= 4) { @@ -14330,7 +14330,7 @@ function lheading(state, startLine, endLine) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_block/paragraph.mjs +//#region node_modules/markdown-it/lib/rules_block/paragraph.mjs function paragraph(state, startLine, endLine) { const terminatorRules = state.md.block.ruler.getRules("paragraph"); const oldParentType = state.parentType; @@ -14368,7 +14368,7 @@ function paragraph(state, startLine, endLine) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/parser_block.mjs +//#region node_modules/markdown-it/lib/parser_block.mjs /** internal * class ParserBlock * @@ -14514,7 +14514,7 @@ ParserBlock.prototype.State = state_block_default; var parser_block_default = ParserBlock; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/state_inline.mjs +//#region node_modules/markdown-it/lib/rules_inline/state_inline.mjs function StateInline(src, md, env, outTokens) { this.src = src; this.env = env; @@ -14591,7 +14591,7 @@ StateInline.prototype.Token = token_default; var state_inline_default = StateInline; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/text.mjs +//#region node_modules/markdown-it/lib/rules_inline/text.mjs function isTerminatorChar(ch) { switch (ch) { case 10: @@ -14636,7 +14636,7 @@ function text(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/linkify.mjs +//#region node_modules/markdown-it/lib/rules_inline/linkify.mjs const SCHEME_RE = /(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$/i; function linkify(state, silent) { if (!state.md.options.linkify) return false; @@ -14674,7 +14674,7 @@ function linkify(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/newline.mjs +//#region node_modules/markdown-it/lib/rules_inline/newline.mjs function newline(state, silent) { let pos = state.pos; if (state.src.charCodeAt(pos) !== 10) { @@ -14706,7 +14706,7 @@ function newline(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/escape.mjs +//#region node_modules/markdown-it/lib/rules_inline/escape.mjs const ESCAPED = []; for (let i$10 = 0; i$10 < 256; i$10++) { ESCAPED.push(0); @@ -14758,7 +14758,7 @@ function escape(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/backticks.mjs +//#region node_modules/markdown-it/lib/rules_inline/backticks.mjs function backtick(state, silent) { let pos = state.pos; const ch = state.src.charCodeAt(pos); @@ -14804,7 +14804,7 @@ function backtick(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/strikethrough.mjs +//#region node_modules/markdown-it/lib/rules_inline/strikethrough.mjs function strikethrough_tokenize(state, silent) { const start = state.pos; const marker = state.src.charCodeAt(start); @@ -14900,7 +14900,7 @@ var strikethrough_default = { }; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/emphasis.mjs +//#region node_modules/markdown-it/lib/rules_inline/emphasis.mjs function emphasis_tokenize(state, silent) { const start = state.pos; const marker = state.src.charCodeAt(start); @@ -14974,7 +14974,7 @@ var emphasis_default = { }; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/link.mjs +//#region node_modules/markdown-it/lib/rules_inline/link.mjs function link(state, silent) { let code$1, label, res, ref; let href = ""; @@ -15083,7 +15083,7 @@ function link(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/image.mjs +//#region node_modules/markdown-it/lib/rules_inline/image.mjs function image(state, silent) { let code$1, content, label, pos, ref, res, title$1, start; let href = ""; @@ -15192,7 +15192,7 @@ function image(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/autolink.mjs +//#region node_modules/markdown-it/lib/rules_inline/autolink.mjs const EMAIL_RE = /^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/; const AUTOLINK_RE = /^([a-zA-Z][a-zA-Z0-9+.-]{1,31}):([^<>\x00-\x20]*)$/; function autolink(state, silent) { @@ -15251,7 +15251,7 @@ function autolink(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/html_inline.mjs +//#region node_modules/markdown-it/lib/rules_inline/html_inline.mjs function isLinkOpen(str) { return /^\s]/i.test(str); } @@ -15290,7 +15290,7 @@ function html_inline(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/entity.mjs +//#region node_modules/markdown-it/lib/rules_inline/entity.mjs const DIGITAL_RE = /^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i; const NAMED_RE = /^&([a-z][a-z0-9]{1,31});/i; function entity(state, silent) { @@ -15332,7 +15332,7 @@ function entity(state, silent) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/balance_pairs.mjs +//#region node_modules/markdown-it/lib/rules_inline/balance_pairs.mjs function processDelimiters(delimiters) { const openersBottom = {}; const max = delimiters.length; @@ -15404,7 +15404,7 @@ function link_pairs(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/rules_inline/fragments_join.mjs +//#region node_modules/markdown-it/lib/rules_inline/fragments_join.mjs function fragments_join(state) { let curr, last; let level = 0; @@ -15429,7 +15429,7 @@ function fragments_join(state) { } //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/parser_inline.mjs +//#region node_modules/markdown-it/lib/parser_inline.mjs /** internal * class ParserInline * @@ -15559,7 +15559,7 @@ ParserInline.prototype.State = state_inline_default; var parser_inline_default = ParserInline; //#endregion -//#region node_modules/.pnpm/linkify-it@5.0.0/node_modules/linkify-it/lib/re.mjs +//#region node_modules/linkify-it/lib/re.mjs function re_default(opts) { const re = {}; opts = opts || {}; @@ -15600,7 +15600,7 @@ function re_default(opts) { } //#endregion -//#region node_modules/.pnpm/linkify-it@5.0.0/node_modules/linkify-it/index.mjs +//#region node_modules/linkify-it/index.mjs function assign(obj) { const sources = Array.prototype.slice.call(arguments, 1); sources.forEach(function(source) { @@ -16092,7 +16092,7 @@ LinkifyIt.prototype.onCompile = function onCompile() {}; var linkify_it_default = LinkifyIt; //#endregion -//#region node_modules/.pnpm/punycode.js@2.3.1/node_modules/punycode.js/punycode.es6.js +//#region node_modules/punycode.js/punycode.es6.js /** Highest positive signed 32-bit float value */ const maxInt = 2147483647; /** Bootstring parameters */ @@ -16425,7 +16425,7 @@ const punycode = { var punycode_es6_default = punycode; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/presets/default.mjs +//#region node_modules/markdown-it/lib/presets/default.mjs var default_default = { options: { html: false, @@ -16446,7 +16446,7 @@ var default_default = { }; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/presets/zero.mjs +//#region node_modules/markdown-it/lib/presets/zero.mjs var zero_default = { options: { html: false, @@ -16475,7 +16475,7 @@ var zero_default = { }; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/presets/commonmark.mjs +//#region node_modules/markdown-it/lib/presets/commonmark.mjs var commonmark_default = { options: { html: true, @@ -16530,7 +16530,7 @@ var commonmark_default = { }; //#endregion -//#region node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/index.mjs +//#region node_modules/markdown-it/lib/index.mjs const config = { default: default_default, zero: zero_default, diff --git a/docs/AGENTS.default.md b/docs/AGENTS.default.md index 2c49c6edc..ffb1332e8 100644 --- a/docs/AGENTS.default.md +++ b/docs/AGENTS.default.md @@ -97,4 +97,4 @@ git commit -m "Add Clawd workspace" - Keep heartbeats enabled so the assistant can schedule reminders, monitor inboxes, and trigger camera captures. - For browser-driven verification, use `clawdis browser` (tabs/status/screenshot) with the clawd-managed Chrome profile. - For DOM inspection, use `clawdis browser eval|query|dom|snapshot` (and `--json`/`--out` when you need machine output). -- For advanced actions, use `clawdis browser tool browser_* --args '{...}'` (Playwright MCP parity). +- For interactions, use `clawdis browser click|type|hover|drag|select|upload|press|wait|navigate|back|evaluate|run`. diff --git a/docs/browser.md b/docs/browser.md index 959327c1f..75752bb79 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -1,5 +1,5 @@ --- -summary: "Spec: integrated browser control server + MCP tool dispatch" +summary: "Spec: integrated browser control server + action commands" read_when: - Adding agent-controlled browser automation - Debugging why clawd is interfering with your own Chrome @@ -98,45 +98,45 @@ Fallback behavior: the user set the profile color/name once via Chrome UI; it must persist because the `userDataDir` is persistent. -## Control server contract (proposed) +## Control server contract (current) Expose a small local HTTP API (and/or gateway RPC surface) so the agent can manage state without touching the user's Chrome. -Minimum endpoints/methods (names illustrative): +Basics: +- `GET /` status payload (enabled/running/pid/cdpPort/etc) +- `POST /start` start browser +- `POST /stop` stop browser +- `GET /tabs` list tabs +- `POST /tabs/open` open a new tab +- `POST /tabs/focus` focus a tab by id/prefix +- `DELETE /tabs/:targetId` close a tab by id/prefix +- `POST /close` close the current tab (optional targetId in body) -- `browser.status` - - returns: `{ enabled, url, running, pid?, version?, chosenBrowser?, userDataDir?, ports: { control, cdp } }` -- `browser.start` - - starts the browser-control server + browser (no-op if already running) -- `browser.stop` - - stops the server and closes the clawd browser (best-effort; graceful first, then force if needed) -- `browser.tabs.list` - - returns: array of `{ targetId, title, url, isActive, lastFocusedAt? }` -- `browser.tabs.open` - - params: `{ url, newTab?: true }` → returns `{ targetId }` -- `browser.tabs.focus` - - params: `{ targetId }` -- `browser.tabs.close` - - params: `{ targetId }` -- `browser.screenshot` - - params: `{ targetId?, fullPage?: false }` → returns a `MEDIA:` attachment URL (via the existing Clawdis media host) +Inspection: +- `GET /screenshot` (CDP screenshot) +- `POST /screenshot` (Playwright screenshot with ref/element) +- `POST /eval` (CDP evaluate) +- `GET /query` +- `GET /dom` +- `GET /snapshot` (`aria` | `domSnapshot` | `ai`) -DOM + inspection (v1): -- `browser.eval` - - params: `{ js, targetId?, await?: false }` → returns the CDP `Runtime.evaluate` result (best-effort `returnByValue`) -- `browser.query` - - params: `{ selector, targetId?, limit? }` → returns basic element summaries (tag/id/class/text/value/href/outerHTML) -- `browser.dom` - - params: `{ format: "html"|"text", targetId?, selector?, maxChars? }` → returns a truncated dump (`text` field) -- `browser.snapshot` - - params: `{ format: "aria"|"domSnapshot", targetId?, limit? }` - - `aria`: simplified Accessibility tree with `backendDOMNodeId` when available (future click/type hooks) - - `domSnapshot`: lightweight DOM walk snapshot (tree-ish, bounded by `limit`) - -Nice-to-have (later): -- `browser.click` / `browser.type` / `browser.waitFor` helpers built atop snapshot refs / backend node ids -- `browser.tool` dispatch that mirrors Playwright MCP tool names for quick feature parity +Actions: +- `POST /navigate`, `POST /back` +- `POST /resize` +- `POST /click`, `POST /type`, `POST /press`, `POST /hover`, `POST /drag`, `POST /select` +- `POST /upload` (file chooser modal must be open) +- `POST /fill` (JSON field descriptors) +- `POST /dialog` (alert/confirm/prompt) +- `POST /wait` (time/text/textGone) +- `POST /evaluate` (function + optional ref) +- `POST /run` (function(page) → result) +- `GET /console`, `GET /network` +- `POST /trace/start`, `POST /trace/stop` +- `POST /pdf` +- `POST /verify/element`, `POST /verify/text`, `POST /verify/list`, `POST /verify/value` +- `POST /mouse/move`, `POST /mouse/click`, `POST /mouse/drag` +- `POST /locator` (generate Playwright locator) ### "Is it open or closed?" @@ -163,54 +163,60 @@ The agent should not assume tabs are ephemeral. It should: - reuse an existing tab when appropriate (e.g. a persistent "main" tab) - avoid opening duplicate tabs unless asked -## Tool dispatch (Playwright MCP parity) +## CLI quick reference (one example each) -Clawdis exposes a generic tool dispatcher for Playwright MCP-style tools: +Basics: +- `clawdis browser status` +- `clawdis browser start` +- `clawdis browser stop` +- `clawdis browser tabs` +- `clawdis browser open https://example.com` +- `clawdis browser focus abcd1234` +- `clawdis browser close abcd1234` -`POST /tool` with JSON `{ name: "browser_*", args: { ... }, targetId?: "..." }` +Inspection: +- `clawdis browser screenshot` +- `clawdis browser screenshot --full-page` +- `clawdis browser screenshot --ref 12` +- `clawdis browser eval "document.title"` +- `clawdis browser query "a" --limit 5` +- `clawdis browser dom --format text --max-chars 5000` +- `clawdis browser snapshot --format aria --limit 200` +- `clawdis browser snapshot --format ai` -CLI helper: -`clawdis browser tool browser_* --args '{...}'` - -Supported tool names: -- `browser_close` -- `browser_resize` -- `browser_console_messages` -- `browser_network_requests` -- `browser_handle_dialog` -- `browser_evaluate` -- `browser_file_upload` -- `browser_fill_form` -- `browser_install` (no-op; uses system Chrome/Chromium) -- `browser_press_key` -- `browser_type` -- `browser_navigate` -- `browser_navigate_back` -- `browser_run_code` -- `browser_take_screenshot` -- `browser_snapshot` -- `browser_click` -- `browser_drag` -- `browser_hover` -- `browser_select_option` -- `browser_tabs` -- `browser_wait_for` -- `browser_pdf_save` -- `browser_start_tracing` -- `browser_stop_tracing` -- `browser_verify_element_visible` -- `browser_verify_text_visible` -- `browser_verify_list_visible` -- `browser_verify_value` -- `browser_mouse_move_xy` -- `browser_mouse_click_xy` -- `browser_mouse_drag_xy` -- `browser_generate_locator` +Actions: +- `clawdis browser navigate https://example.com` +- `clawdis browser back` +- `clawdis browser resize 1280 720` +- `clawdis browser click 12 --double` +- `clawdis browser type 23 "hello" --submit` +- `clawdis browser press Enter` +- `clawdis browser hover 44` +- `clawdis browser drag 10 11` +- `clawdis browser select 9 OptionA OptionB` +- `clawdis browser upload /tmp/file.pdf` +- `clawdis browser fill --fields '[{\"ref\":\"1\",\"value\":\"Ada\"}]'` +- `clawdis browser dialog --accept` +- `clawdis browser wait --text "Done"` +- `clawdis browser evaluate --fn '(el) => el.textContent' --ref 7` +- `clawdis browser run --code '(page) => page.title()'` +- `clawdis browser console --level error` +- `clawdis browser network --include-static` +- `clawdis browser trace-start` +- `clawdis browser trace-stop` +- `clawdis browser pdf` +- `clawdis browser verify-element --role button --name "Submit"` +- `clawdis browser verify-text "Welcome"` +- `clawdis browser verify-list 3 ItemA ItemB` +- `clawdis browser verify-value --ref 4 --type textbox --value hello` +- `clawdis browser mouse-move --x 120 --y 240` +- `clawdis browser mouse-click --x 120 --y 240` +- `clawdis browser mouse-drag --start-x 10 --start-y 20 --end-x 200 --end-y 300` +- `clawdis browser locator 77` Notes: -- `browser_file_upload` and `browser_handle_dialog` are modal-only; they only - work when a file chooser/dialog modal state is present. -- `browser_snapshot` returns a Playwright-for-AI snapshot (use for follow-up actions). +- `upload` and `dialog` only work when a file chooser or dialog is present. +- `snapshot --format ai` returns Playwright-for-AI markup used for ref-based actions. ## Security & privacy notes diff --git a/docs/clawd-md.md b/docs/clawd-md.md deleted file mode 100644 index b29b938c0..000000000 --- a/docs/clawd-md.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -summary: "Redirect: /clawd.md → /clawd" -permalink: /clawd.md ---- - - - - -If you’re not redirected automatically, go to -/clawd. - diff --git a/docs/mac/browser.md b/docs/mac/browser.md deleted file mode 100644 index 31608bb95..000000000 --- a/docs/mac/browser.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -summary: "Redirect: mac/browser.md → browser.md" -read_when: - - Adding agent-controlled browser automation - - Debugging why clawd is interfering with your own Chrome - - Implementing browser settings + lifecycle in the macOS app ---- - -# Browser (macOS app) — moved - -This doc moved to `docs/browser.md`. diff --git a/src/browser/client-actions-core.ts b/src/browser/client-actions-core.ts new file mode 100644 index 000000000..ed1b1bff0 --- /dev/null +++ b/src/browser/client-actions-core.ts @@ -0,0 +1,287 @@ +import type { ScreenshotResult } from "./client.js"; +import type { BrowserActionTabResult } from "./client-actions-types.js"; +import { fetchBrowserJson } from "./client-fetch.js"; + +export async function browserNavigate( + baseUrl: string, + opts: { url: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/navigate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: opts.url, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserBack( + baseUrl: string, + opts: { targetId?: string } = {}, +): Promise { + return await fetchBrowserJson(`${baseUrl}/back`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserResize( + baseUrl: string, + opts: { width: number; height: number; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/resize`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + width: opts.width, + height: opts.height, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserClosePage( + baseUrl: string, + opts: { targetId?: string } = {}, +): Promise { + return await fetchBrowserJson(`${baseUrl}/close`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserClick( + baseUrl: string, + opts: { + ref: string; + targetId?: string; + doubleClick?: boolean; + button?: string; + modifiers?: string[]; + }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/click`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ref: opts.ref, + targetId: opts.targetId, + doubleClick: opts.doubleClick, + button: opts.button, + modifiers: opts.modifiers, + }), + timeoutMs: 20000, + }); +} + +export async function browserType( + baseUrl: string, + opts: { + ref: string; + text: string; + targetId?: string; + submit?: boolean; + slowly?: boolean; + }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/type`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ref: opts.ref, + text: opts.text, + targetId: opts.targetId, + submit: opts.submit, + slowly: opts.slowly, + }), + timeoutMs: 20000, + }); +} + +export async function browserPressKey( + baseUrl: string, + opts: { key: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/press`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key: opts.key, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserHover( + baseUrl: string, + opts: { ref: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/hover`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ref: opts.ref, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserDrag( + baseUrl: string, + opts: { startRef: string; endRef: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/drag`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + startRef: opts.startRef, + endRef: opts.endRef, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserSelectOption( + baseUrl: string, + opts: { ref: string; values: string[]; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/select`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ref: opts.ref, + values: opts.values, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserUpload( + baseUrl: string, + opts: { paths?: string[]; targetId?: string } = {}, +): Promise { + return await fetchBrowserJson(`${baseUrl}/upload`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ paths: opts.paths, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserFillForm( + baseUrl: string, + opts: { fields: Array>; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/fill`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fields: opts.fields, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserHandleDialog( + baseUrl: string, + opts: { accept: boolean; promptText?: string; targetId?: string }, +): Promise<{ ok: true; message: string; type: string }> { + return await fetchBrowserJson<{ ok: true; message: string; type: string }>( + `${baseUrl}/dialog`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + accept: opts.accept, + promptText: opts.promptText, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }, + ); +} + +export async function browserWaitFor( + baseUrl: string, + opts: { + time?: number; + text?: string; + textGone?: string; + targetId?: string; + }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/wait`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + time: opts.time, + text: opts.text, + textGone: opts.textGone, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserEvaluate( + baseUrl: string, + opts: { fn: string; ref?: string; targetId?: string }, +): Promise<{ ok: true; result: unknown }> { + return await fetchBrowserJson<{ ok: true; result: unknown }>( + `${baseUrl}/evaluate`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + function: opts.fn, + ref: opts.ref, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }, + ); +} + +export async function browserRunCode( + baseUrl: string, + opts: { code: string; targetId?: string }, +): Promise<{ ok: true; result: unknown }> { + return await fetchBrowserJson<{ ok: true; result: unknown }>( + `${baseUrl}/run`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code: opts.code, targetId: opts.targetId }), + timeoutMs: 20000, + }, + ); +} + +export async function browserScreenshotAction( + baseUrl: string, + opts: { + targetId?: string; + fullPage?: boolean; + ref?: string; + element?: string; + type?: "png" | "jpeg"; + filename?: string; + }, +): Promise { + return await fetchBrowserJson( + `${baseUrl}/screenshot`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + fullPage: opts.fullPage, + ref: opts.ref, + element: opts.element, + type: opts.type, + filename: opts.filename, + }), + timeoutMs: 20000, + }, + ); +} diff --git a/src/browser/client-actions-observe.ts b/src/browser/client-actions-observe.ts new file mode 100644 index 000000000..b86d2ef1a --- /dev/null +++ b/src/browser/client-actions-observe.ts @@ -0,0 +1,207 @@ +import type { + BrowserActionOk, + BrowserActionPathResult, +} from "./client-actions-types.js"; +import { fetchBrowserJson } from "./client-fetch.js"; +import type { + BrowserConsoleMessage, + BrowserNetworkRequest, +} from "./pw-session.js"; + +export async function browserConsoleMessages( + baseUrl: string, + opts: { level?: string; targetId?: string } = {}, +): Promise<{ ok: true; messages: BrowserConsoleMessage[]; targetId: string }> { + const q = new URLSearchParams(); + if (opts.level) q.set("level", opts.level); + if (opts.targetId) q.set("targetId", opts.targetId); + const suffix = q.toString() ? `?${q.toString()}` : ""; + return await fetchBrowserJson<{ + ok: true; + messages: BrowserConsoleMessage[]; + targetId: string; + }>(`${baseUrl}/console${suffix}`, { timeoutMs: 20000 }); +} + +export async function browserNetworkRequests( + baseUrl: string, + opts: { includeStatic?: boolean; targetId?: string } = {}, +): Promise<{ ok: true; requests: BrowserNetworkRequest[]; targetId: string }> { + const q = new URLSearchParams(); + if (opts.includeStatic) q.set("includeStatic", "true"); + if (opts.targetId) q.set("targetId", opts.targetId); + const suffix = q.toString() ? `?${q.toString()}` : ""; + return await fetchBrowserJson<{ + ok: true; + requests: BrowserNetworkRequest[]; + targetId: string; + }>(`${baseUrl}/network${suffix}`, { timeoutMs: 20000 }); +} + +export async function browserStartTracing( + baseUrl: string, + opts: { targetId?: string } = {}, +): Promise { + return await fetchBrowserJson(`${baseUrl}/trace/start`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserStopTracing( + baseUrl: string, + opts: { targetId?: string } = {}, +): Promise { + return await fetchBrowserJson( + `${baseUrl}/trace/stop`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId }), + timeoutMs: 20000, + }, + ); +} + +export async function browserPdfSave( + baseUrl: string, + opts: { targetId?: string } = {}, +): Promise { + return await fetchBrowserJson(`${baseUrl}/pdf`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserVerifyElementVisible( + baseUrl: string, + opts: { role: string; accessibleName: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/verify/element`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + role: opts.role, + accessibleName: opts.accessibleName, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserVerifyTextVisible( + baseUrl: string, + opts: { text: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/verify/text`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: opts.text, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserVerifyListVisible( + baseUrl: string, + opts: { ref: string; items: string[]; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/verify/list`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ref: opts.ref, + items: opts.items, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserVerifyValue( + baseUrl: string, + opts: { ref: string; type: string; value?: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/verify/value`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ref: opts.ref, + type: opts.type, + value: opts.value, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserMouseMove( + baseUrl: string, + opts: { x: number; y: number; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/mouse/move`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ x: opts.x, y: opts.y, targetId: opts.targetId }), + timeoutMs: 20000, + }); +} + +export async function browserMouseClick( + baseUrl: string, + opts: { x: number; y: number; button?: string; targetId?: string }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/mouse/click`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + x: opts.x, + y: opts.y, + button: opts.button, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserMouseDrag( + baseUrl: string, + opts: { + startX: number; + startY: number; + endX: number; + endY: number; + targetId?: string; + }, +): Promise { + return await fetchBrowserJson(`${baseUrl}/mouse/drag`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + startX: opts.startX, + startY: opts.startY, + endX: opts.endX, + endY: opts.endY, + targetId: opts.targetId, + }), + timeoutMs: 20000, + }); +} + +export async function browserGenerateLocator( + baseUrl: string, + opts: { ref: string }, +): Promise<{ ok: true; locator: string }> { + return await fetchBrowserJson<{ ok: true; locator: string }>( + `${baseUrl}/locator`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ref: opts.ref }), + timeoutMs: 20000, + }, + ); +} diff --git a/src/browser/client-actions-types.ts b/src/browser/client-actions-types.ts new file mode 100644 index 000000000..648faff17 --- /dev/null +++ b/src/browser/client-actions-types.ts @@ -0,0 +1,15 @@ +export type BrowserActionOk = { ok: true }; + +export type BrowserActionTabResult = { + ok: true; + targetId: string; + url?: string; +}; + +export type BrowserActionPathResult = { + ok: true; + path: string; + targetId: string; + url?: string; + filename?: string; +}; diff --git a/src/browser/client-actions.ts b/src/browser/client-actions.ts new file mode 100644 index 000000000..24fd38be9 --- /dev/null +++ b/src/browser/client-actions.ts @@ -0,0 +1,3 @@ +export * from "./client-actions-core.js"; +export * from "./client-actions-observe.js"; +export * from "./client-actions-types.js"; diff --git a/src/browser/client-fetch.ts b/src/browser/client-fetch.ts new file mode 100644 index 000000000..6d484af98 --- /dev/null +++ b/src/browser/client-fetch.ts @@ -0,0 +1,67 @@ +function unwrapCause(err: unknown): unknown { + if (!err || typeof err !== "object") return null; + const cause = (err as { cause?: unknown }).cause; + return cause ?? null; +} + +function enhanceBrowserFetchError( + url: string, + err: unknown, + timeoutMs: number, +): Error { + const cause = unwrapCause(err); + const code = + (cause && typeof cause === "object" && "code" in cause + ? String((cause as { code?: unknown }).code ?? "") + : "") || + (err && typeof err === "object" && "code" in err + ? String((err as { code?: unknown }).code ?? "") + : ""); + + const hint = + "Start (or restart) the Clawdis gateway (Clawdis.app menubar, or `clawdis gateway`) and try again."; + + if (code === "ECONNREFUSED") { + return new Error( + `Can't reach the clawd browser control server at ${url} (connection refused). ${hint}`, + ); + } + if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") { + return new Error( + `Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`, + ); + } + + const msg = String(err); + if (msg.toLowerCase().includes("abort")) { + return new Error( + `Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`, + ); + } + + return new Error( + `Can't reach the clawd browser control server at ${url}. ${hint} (${msg})`, + ); +} + +export async function fetchBrowserJson( + url: string, + init?: RequestInit & { timeoutMs?: number }, +): Promise { + const timeoutMs = init?.timeoutMs ?? 5000; + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), timeoutMs); + let res: Response; + try { + res = await fetch(url, { ...init, signal: ctrl.signal } as RequestInit); + } catch (err) { + throw enhanceBrowserFetchError(url, err, timeoutMs); + } finally { + clearTimeout(t); + } + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text ? `${res.status}: ${text}` : `HTTP ${res.status}`); + } + return (await res.json()) as T; +} diff --git a/src/browser/client.ts b/src/browser/client.ts index 8d766d86a..4433d5a3c 100644 --- a/src/browser/client.ts +++ b/src/browser/client.ts @@ -1,4 +1,5 @@ import { loadConfig } from "../config/config.js"; +import { fetchBrowserJson } from "./client-fetch.js"; import { resolveBrowserConfig } from "./config.js"; export type BrowserStatus = { @@ -21,11 +22,6 @@ export type BrowserTab = { type?: string; }; -export type BrowserToolResponse = { - ok: true; - [key: string]: unknown; -}; - export type ScreenshotResult = { ok: true; path: string; @@ -117,74 +113,6 @@ export type SnapshotResult = snapshot: string; }; -function unwrapCause(err: unknown): unknown { - if (!err || typeof err !== "object") return null; - const cause = (err as { cause?: unknown }).cause; - return cause ?? null; -} - -function enhanceBrowserFetchError( - url: string, - err: unknown, - timeoutMs: number, -): Error { - const cause = unwrapCause(err); - const code = - (cause && typeof cause === "object" && "code" in cause - ? String((cause as { code?: unknown }).code ?? "") - : "") || - (err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code ?? "") - : ""); - - const hint = - "Start (or restart) the Clawdis gateway (Clawdis.app menubar, or `clawdis gateway`) and try again."; - - if (code === "ECONNREFUSED") { - return new Error( - `Can't reach the clawd browser control server at ${url} (connection refused). ${hint}`, - ); - } - if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") { - return new Error( - `Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`, - ); - } - - const msg = String(err); - if (msg.toLowerCase().includes("abort")) { - return new Error( - `Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`, - ); - } - - return new Error( - `Can't reach the clawd browser control server at ${url}. ${hint} (${msg})`, - ); -} - -async function fetchJson( - url: string, - init?: RequestInit & { timeoutMs?: number }, -): Promise { - const timeoutMs = init?.timeoutMs ?? 5000; - const ctrl = new AbortController(); - const t = setTimeout(() => ctrl.abort(), timeoutMs); - let res: Response; - try { - res = await fetch(url, { ...init, signal: ctrl.signal } as RequestInit); - } catch (err) { - throw enhanceBrowserFetchError(url, err, timeoutMs); - } finally { - clearTimeout(t); - } - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new Error(text ? `${res.status}: ${text}` : `HTTP ${res.status}`); - } - return (await res.json()) as T; -} - export function resolveBrowserControlUrl(overrideUrl?: string) { const cfg = loadConfig(); const resolved = resolveBrowserConfig(cfg.browser); @@ -193,19 +121,27 @@ export function resolveBrowserControlUrl(overrideUrl?: string) { } export async function browserStatus(baseUrl: string): Promise { - return await fetchJson(`${baseUrl}/`, { timeoutMs: 1500 }); + return await fetchBrowserJson(`${baseUrl}/`, { + timeoutMs: 1500, + }); } export async function browserStart(baseUrl: string): Promise { - await fetchJson(`${baseUrl}/start`, { method: "POST", timeoutMs: 15000 }); + await fetchBrowserJson(`${baseUrl}/start`, { + method: "POST", + timeoutMs: 15000, + }); } export async function browserStop(baseUrl: string): Promise { - await fetchJson(`${baseUrl}/stop`, { method: "POST", timeoutMs: 15000 }); + await fetchBrowserJson(`${baseUrl}/stop`, { + method: "POST", + timeoutMs: 15000, + }); } export async function browserTabs(baseUrl: string): Promise { - const res = await fetchJson<{ running: boolean; tabs: BrowserTab[] }>( + const res = await fetchBrowserJson<{ running: boolean; tabs: BrowserTab[] }>( `${baseUrl}/tabs`, { timeoutMs: 3000 }, ); @@ -216,7 +152,7 @@ export async function browserOpenTab( baseUrl: string, url: string, ): Promise { - return await fetchJson(`${baseUrl}/tabs/open`, { + return await fetchBrowserJson(`${baseUrl}/tabs/open`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), @@ -228,7 +164,7 @@ export async function browserFocusTab( baseUrl: string, targetId: string, ): Promise { - await fetchJson(`${baseUrl}/tabs/focus`, { + await fetchBrowserJson(`${baseUrl}/tabs/focus`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId }), @@ -240,7 +176,7 @@ export async function browserCloseTab( baseUrl: string, targetId: string, ): Promise { - await fetchJson(`${baseUrl}/tabs/${encodeURIComponent(targetId)}`, { + await fetchBrowserJson(`${baseUrl}/tabs/${encodeURIComponent(targetId)}`, { method: "DELETE", timeoutMs: 5000, }); @@ -257,9 +193,12 @@ export async function browserScreenshot( if (opts.targetId) q.set("targetId", opts.targetId); if (opts.fullPage) q.set("fullPage", "true"); const suffix = q.toString() ? `?${q.toString()}` : ""; - return await fetchJson(`${baseUrl}/screenshot${suffix}`, { - timeoutMs: 20000, - }); + return await fetchBrowserJson( + `${baseUrl}/screenshot${suffix}`, + { + timeoutMs: 20000, + }, + ); } export async function browserEval( @@ -270,7 +209,7 @@ export async function browserEval( awaitPromise?: boolean; }, ): Promise { - return await fetchJson(`${baseUrl}/eval`, { + return await fetchBrowserJson(`${baseUrl}/eval`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -294,9 +233,12 @@ export async function browserQuery( q.set("selector", opts.selector); if (opts.targetId) q.set("targetId", opts.targetId); if (typeof opts.limit === "number") q.set("limit", String(opts.limit)); - return await fetchJson(`${baseUrl}/query?${q.toString()}`, { - timeoutMs: 15000, - }); + return await fetchBrowserJson( + `${baseUrl}/query?${q.toString()}`, + { + timeoutMs: 15000, + }, + ); } export async function browserDom( @@ -314,7 +256,7 @@ export async function browserDom( if (typeof opts.maxChars === "number") q.set("maxChars", String(opts.maxChars)); if (opts.selector) q.set("selector", opts.selector); - return await fetchJson(`${baseUrl}/dom?${q.toString()}`, { + return await fetchBrowserJson(`${baseUrl}/dom?${q.toString()}`, { timeoutMs: 20000, }); } @@ -331,7 +273,7 @@ export async function browserSnapshot( q.set("format", opts.format); if (opts.targetId) q.set("targetId", opts.targetId); if (typeof opts.limit === "number") q.set("limit", String(opts.limit)); - return await fetchJson( + return await fetchBrowserJson( `${baseUrl}/snapshot?${q.toString()}`, { timeoutMs: 20000, @@ -346,7 +288,7 @@ export async function browserClickRef( targetId?: string; }, ): Promise<{ ok: true; targetId: string; url: string }> { - return await fetchJson<{ ok: true; targetId: string; url: string }>( + return await fetchBrowserJson<{ ok: true; targetId: string; url: string }>( `${baseUrl}/click`, { method: "POST", @@ -360,22 +302,4 @@ export async function browserClickRef( ); } -export async function browserTool( - baseUrl: string, - opts: { - name: string; - args?: Record; - targetId?: string; - }, -): Promise { - return await fetchJson(`${baseUrl}/tool`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: opts.name, - args: opts.args ?? {}, - targetId: opts.targetId, - }), - timeoutMs: 20000, - }); -} +// Actions beyond the basic read-only commands live in client-actions.ts. diff --git a/src/browser/routes/tool-core.test.ts b/src/browser/routes/actions-core.test.ts similarity index 59% rename from src/browser/routes/tool-core.test.ts rename to src/browser/routes/actions-core.test.ts index 3c676bacf..678f51186 100644 --- a/src/browser/routes/tool-core.test.ts +++ b/src/browser/routes/actions-core.test.ts @@ -21,37 +21,13 @@ const pw = vi.hoisted(() => ({ resizeViewportViaPlaywright: vi.fn().mockResolvedValue(undefined), runCodeViaPlaywright: vi.fn().mockResolvedValue("ok"), selectOptionViaPlaywright: vi.fn().mockResolvedValue(undefined), - snapshotAiViaPlaywright: vi - .fn() - .mockResolvedValue({ snapshot: "SNAP" }), - takeScreenshotViaPlaywright: vi - .fn() - .mockResolvedValue({ buffer: Buffer.from("png") }), typeViaPlaywright: vi.fn().mockResolvedValue(undefined), waitForViaPlaywright: vi.fn().mockResolvedValue(undefined), })); -const screenshot = vi.hoisted(() => ({ - DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES: 128, - DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE: 64, - normalizeBrowserScreenshot: vi - .fn() - .mockImplementation(async (buf: Buffer) => ({ - buffer: buf, - contentType: "image/png", - })), -})); - -const media = vi.hoisted(() => ({ - ensureMediaDir: vi.fn().mockResolvedValue(undefined), - saveMediaBuffer: vi.fn().mockResolvedValue({ path: "/tmp/fake.png" }), -})); - vi.mock("../pw-ai.js", () => pw); -vi.mock("../screenshot.js", () => screenshot); -vi.mock("../../media/store.js", () => media); -import { handleBrowserToolCore } from "./tool-core.js"; +import { handleBrowserActionCore } from "./actions-core.js"; const baseTab = { targetId: "tab1", @@ -84,10 +60,12 @@ function createCtx( ensureBrowserAvailable: vi.fn().mockResolvedValue(undefined), ensureTabAvailable: vi.fn().mockResolvedValue(baseTab), isReachable: vi.fn().mockResolvedValue(true), - listTabs: vi.fn().mockResolvedValue([ - baseTab, - { targetId: "tab2", title: "Two", url: "https://example.com/2" }, - ]), + listTabs: vi + .fn() + .mockResolvedValue([ + baseTab, + { targetId: "tab2", title: "Two", url: "https://example.com/2" }, + ]), openTab: vi.fn().mockResolvedValue({ targetId: "newtab", title: "", @@ -102,15 +80,15 @@ function createCtx( }; } -async function callTool( - name: string, +async function callAction( + action: Parameters[0]["action"], args: Record = {}, ctxOverride?: Partial, ) { const res = createRes(); const ctx = createCtx(ctxOverride); - const handled = await handleBrowserToolCore({ - name, + const handled = await handleBrowserActionCore({ + action, args, targetId: "", cdpPort: 18792, @@ -124,25 +102,30 @@ beforeEach(() => { vi.clearAllMocks(); }); -describe("handleBrowserToolCore", () => { - it("dispatches core Playwright tools", async () => { +describe("handleBrowserActionCore", () => { + it("dispatches core browser actions", async () => { const cases = [ { - name: "browser_close", + action: "close" as const, args: {}, fn: pw.closePageViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1" }, expectBody: { ok: true, targetId: "tab1", url: baseTab.url }, }, { - name: "browser_resize", + action: "resize" as const, args: { width: 800, height: 600 }, fn: pw.resizeViewportViaPlaywright, - expectArgs: { cdpPort: 18792, targetId: "tab1", width: 800, height: 600 }, + expectArgs: { + cdpPort: 18792, + targetId: "tab1", + width: 800, + height: 600, + }, expectBody: { ok: true, targetId: "tab1", url: baseTab.url }, }, { - name: "browser_handle_dialog", + action: "dialog" as const, args: { accept: true, promptText: "ok" }, fn: pw.handleDialogViaPlaywright, expectArgs: { @@ -154,7 +137,7 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, message: "ok", type: "alert" }, }, { - name: "browser_evaluate", + action: "evaluate" as const, args: { function: "() => 1", ref: "1" }, fn: pw.evaluateViaPlaywright, expectArgs: { @@ -166,7 +149,7 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, result: "result" }, }, { - name: "browser_file_upload", + action: "upload" as const, args: { paths: ["/tmp/file.txt"] }, fn: pw.fileUploadViaPlaywright, expectArgs: { @@ -177,7 +160,7 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_fill_form", + action: "fill" as const, args: { fields: [{ ref: "1", value: "x" }] }, fn: pw.fillFormViaPlaywright, expectArgs: { @@ -188,14 +171,14 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_press_key", + action: "press" as const, args: { key: "Enter" }, fn: pw.pressKeyViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1", key: "Enter" }, expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_type", + action: "type" as const, args: { ref: "2", text: "hi", submit: true, slowly: true }, fn: pw.typeViaPlaywright, expectArgs: { @@ -209,7 +192,7 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_navigate", + action: "navigate" as const, args: { url: "https://example.com" }, fn: pw.navigateViaPlaywright, expectArgs: { @@ -220,21 +203,21 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1", url: baseTab.url }, }, { - name: "browser_navigate_back", + action: "back" as const, args: {}, fn: pw.navigateBackViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1" }, expectBody: { ok: true, targetId: "tab1", url: "about:blank" }, }, { - name: "browser_run_code", + action: "run" as const, args: { code: "return 1" }, fn: pw.runCodeViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1", code: "return 1" }, expectBody: { ok: true, result: "ok" }, }, { - name: "browser_click", + action: "click" as const, args: { ref: "1", doubleClick: true, @@ -253,7 +236,7 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1", url: baseTab.url }, }, { - name: "browser_drag", + action: "drag" as const, args: { startRef: "1", endRef: "2" }, fn: pw.dragViaPlaywright, expectArgs: { @@ -265,14 +248,14 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_hover", + action: "hover" as const, args: { ref: "3" }, fn: pw.hoverViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1", ref: "3" }, expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_select_option", + action: "select" as const, args: { ref: "4", values: ["A"] }, fn: pw.selectOptionViaPlaywright, expectArgs: { @@ -284,7 +267,7 @@ describe("handleBrowserToolCore", () => { expectBody: { ok: true, targetId: "tab1" }, }, { - name: "browser_wait_for", + action: "wait" as const, args: { time: 500, text: "ok", textGone: "bye" }, fn: pw.waitForViaPlaywright, expectArgs: { @@ -299,120 +282,10 @@ describe("handleBrowserToolCore", () => { ]; for (const item of cases) { - const { res, handled } = await callTool(item.name, item.args); + const { res, handled } = await callAction(item.action, item.args); expect(handled).toBe(true); expect(item.fn).toHaveBeenCalledWith(item.expectArgs); expect(res.body).toEqual(item.expectBody); } }); - - it("handles screenshots via media storage", async () => { - const { res } = await callTool("browser_take_screenshot", { - type: "jpeg", - ref: "1", - fullPage: true, - element: "main", - filename: "shot.jpg", - }); - - expect(pw.takeScreenshotViaPlaywright).toHaveBeenCalledWith({ - cdpPort: 18792, - targetId: "tab1", - ref: "1", - element: "main", - fullPage: true, - type: "jpeg", - }); - expect(media.ensureMediaDir).toHaveBeenCalled(); - expect(media.saveMediaBuffer).toHaveBeenCalled(); - expect(res.body).toMatchObject({ - ok: true, - path: "/tmp/fake.png", - filename: "shot.jpg", - targetId: "tab1", - url: baseTab.url, - }); - }); - - it("handles snapshots with optional file output", async () => { - const { res } = await callTool("browser_snapshot", { - filename: "snapshot.txt", - }); - - expect(pw.snapshotAiViaPlaywright).toHaveBeenCalledWith({ - cdpPort: 18792, - targetId: "tab1", - }); - expect(media.ensureMediaDir).toHaveBeenCalled(); - expect(media.saveMediaBuffer).toHaveBeenCalledWith( - expect.any(Buffer), - "text/plain", - "browser", - ); - expect(res.body).toMatchObject({ - ok: true, - path: "/tmp/fake.png", - filename: "snapshot.txt", - targetId: "tab1", - url: baseTab.url, - }); - }); - - it("returns a message for browser_install", async () => { - const { res } = await callTool("browser_install"); - expect(res.body).toMatchObject({ ok: true }); - }); - - it("supports browser_tabs actions", async () => { - const ctx = createCtx(); - - const listRes = createRes(); - await handleBrowserToolCore({ - name: "browser_tabs", - args: { action: "list" }, - targetId: "", - cdpPort: 18792, - ctx, - res: listRes, - }); - expect(listRes.body).toMatchObject({ ok: true }); - expect(ctx.listTabs).toHaveBeenCalled(); - - const newRes = createRes(); - await handleBrowserToolCore({ - name: "browser_tabs", - args: { action: "new" }, - targetId: "", - cdpPort: 18792, - ctx, - res: newRes, - }); - expect(ctx.ensureBrowserAvailable).toHaveBeenCalled(); - expect(ctx.openTab).toHaveBeenCalled(); - expect(newRes.body).toMatchObject({ ok: true, tab: { targetId: "newtab" } }); - - const closeRes = createRes(); - await handleBrowserToolCore({ - name: "browser_tabs", - args: { action: "close", index: 1 }, - targetId: "", - cdpPort: 18792, - ctx, - res: closeRes, - }); - expect(ctx.closeTab).toHaveBeenCalledWith("tab2"); - expect(closeRes.body).toMatchObject({ ok: true, targetId: "tab2" }); - - const selectRes = createRes(); - await handleBrowserToolCore({ - name: "browser_tabs", - args: { action: "select", index: 0 }, - targetId: "", - cdpPort: 18792, - ctx, - res: selectRes, - }); - expect(ctx.focusTab).toHaveBeenCalledWith("tab1"); - expect(selectRes.body).toMatchObject({ ok: true, targetId: "tab1" }); - }); }); diff --git a/src/browser/routes/tool-core.ts b/src/browser/routes/actions-core.ts similarity index 61% rename from src/browser/routes/tool-core.ts rename to src/browser/routes/actions-core.ts index d47469496..0a3d84376 100644 --- a/src/browser/routes/tool-core.ts +++ b/src/browser/routes/actions-core.ts @@ -1,8 +1,5 @@ -import path from "node:path"; - import type express from "express"; -import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js"; import { clickViaPlaywright, closePageViaPlaywright, @@ -18,16 +15,9 @@ import { resizeViewportViaPlaywright, runCodeViaPlaywright, selectOptionViaPlaywright, - snapshotAiViaPlaywright, - takeScreenshotViaPlaywright, typeViaPlaywright, waitForViaPlaywright, } from "../pw-ai.js"; -import { - DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, - DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE, - normalizeBrowserScreenshot, -} from "../screenshot.js"; import type { BrowserRouteContext } from "../server-context.js"; import { jsonError, @@ -37,8 +27,26 @@ import { toStringOrEmpty, } from "./utils.js"; -type ToolCoreParams = { - name: string; +export type BrowserActionCore = + | "back" + | "click" + | "close" + | "dialog" + | "drag" + | "evaluate" + | "fill" + | "hover" + | "navigate" + | "press" + | "resize" + | "run" + | "select" + | "type" + | "upload" + | "wait"; + +type ActionCoreParams = { + action: BrowserActionCore; args: Record; targetId: string; cdpPort: number; @@ -46,20 +54,20 @@ type ToolCoreParams = { res: express.Response; }; -export async function handleBrowserToolCore( - params: ToolCoreParams, +export async function handleBrowserActionCore( + params: ActionCoreParams, ): Promise { - const { name, args, targetId, cdpPort, ctx, res } = params; + const { action, args, targetId, cdpPort, ctx, res } = params; const target = targetId || undefined; - switch (name) { - case "browser_close": { + switch (action) { + case "close": { const tab = await ctx.ensureTabAvailable(target); await closePageViaPlaywright({ cdpPort, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId, url: tab.url }); return true; } - case "browser_resize": { + case "resize": { const width = toNumber(args.width); const height = toNumber(args.height); if (!width || !height) { @@ -76,7 +84,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId, url: tab.url }); return true; } - case "browser_handle_dialog": { + case "dialog": { const accept = toBoolean(args.accept); if (accept === undefined) { jsonError(res, 400, "accept is required"); @@ -93,7 +101,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, ...result }); return true; } - case "browser_evaluate": { + case "evaluate": { const fn = toStringOrEmpty(args.function); if (!fn) { jsonError(res, 400, "function is required"); @@ -110,7 +118,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, result }); return true; } - case "browser_file_upload": { + case "upload": { const paths = toStringArray(args.paths) ?? []; const tab = await ctx.ensureTabAvailable(target); await fileUploadViaPlaywright({ @@ -121,7 +129,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_fill_form": { + case "fill": { const fields = Array.isArray(args.fields) ? (args.fields as Array>) : null; @@ -138,15 +146,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_install": { - res.json({ - ok: true, - message: - "clawd browser uses system Chrome/Chromium; no Playwright install needed.", - }); - return true; - } - case "browser_press_key": { + case "press": { const key = toStringOrEmpty(args.key); if (!key) { jsonError(res, 400, "key is required"); @@ -161,7 +161,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_type": { + case "type": { const ref = toStringOrEmpty(args.ref); const text = toStringOrEmpty(args.text); if (!ref || !text) { @@ -182,7 +182,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_navigate": { + case "navigate": { const url = toStringOrEmpty(args.url); if (!url) { jsonError(res, 400, "url is required"); @@ -197,7 +197,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId, ...result }); return true; } - case "browser_navigate_back": { + case "back": { const tab = await ctx.ensureTabAvailable(target); const result = await navigateBackViaPlaywright({ cdpPort, @@ -206,7 +206,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId, ...result }); return true; } - case "browser_run_code": { + case "run": { const code = toStringOrEmpty(args.code); if (!code) { jsonError(res, 400, "code is required"); @@ -221,73 +221,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, result }); return true; } - case "browser_take_screenshot": { - const type = args.type === "jpeg" ? "jpeg" : "png"; - const ref = toStringOrEmpty(args.ref) || undefined; - const fullPage = toBoolean(args.fullPage) ?? false; - const element = toStringOrEmpty(args.element) || undefined; - const filename = toStringOrEmpty(args.filename) || undefined; - const tab = await ctx.ensureTabAvailable(target); - const snap = await takeScreenshotViaPlaywright({ - cdpPort, - targetId: tab.targetId, - ref, - element, - fullPage, - type, - }); - const normalized = await normalizeBrowserScreenshot(snap.buffer, { - maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE, - maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, - }); - await ensureMediaDir(); - const saved = await saveMediaBuffer( - normalized.buffer, - normalized.contentType ?? `image/${type}`, - "browser", - DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, - ); - res.json({ - ok: true, - path: path.resolve(saved.path), - filename, - targetId: tab.targetId, - url: tab.url, - }); - return true; - } - case "browser_snapshot": { - const filename = toStringOrEmpty(args.filename) || undefined; - const tab = await ctx.ensureTabAvailable(target); - const snap = await snapshotAiViaPlaywright({ - cdpPort, - targetId: tab.targetId, - }); - if (filename) { - await ensureMediaDir(); - const saved = await saveMediaBuffer( - Buffer.from(snap.snapshot, "utf8"), - "text/plain", - "browser", - ); - res.json({ - ok: true, - path: path.resolve(saved.path), - filename, - targetId: tab.targetId, - url: tab.url, - }); - return true; - } - res.json({ - ok: true, - snapshot: snap.snapshot, - targetId: tab.targetId, - url: tab.url, - }); - return true; - } - case "browser_click": { + case "click": { const ref = toStringOrEmpty(args.ref); if (!ref) { jsonError(res, 400, "ref is required"); @@ -310,7 +244,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId, url: tab.url }); return true; } - case "browser_drag": { + case "drag": { const startRef = toStringOrEmpty(args.startRef); const endRef = toStringOrEmpty(args.endRef); if (!startRef || !endRef) { @@ -327,7 +261,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_hover": { + case "hover": { const ref = toStringOrEmpty(args.ref); if (!ref) { jsonError(res, 400, "ref is required"); @@ -342,7 +276,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_select_option": { + case "select": { const ref = toStringOrEmpty(args.ref); const values = toStringArray(args.values); if (!ref || !values?.length) { @@ -359,59 +293,7 @@ export async function handleBrowserToolCore( res.json({ ok: true, targetId: tab.targetId }); return true; } - case "browser_tabs": { - const action = toStringOrEmpty(args.action); - const index = toNumber(args.index); - if (!action) { - jsonError(res, 400, "action is required"); - return true; - } - if (action === "list") { - const reachable = await ctx.isReachable(300); - if (!reachable) { - res.json({ ok: true, tabs: [] }); - return true; - } - const tabs = await ctx.listTabs(); - res.json({ ok: true, tabs }); - return true; - } - if (action === "new") { - await ctx.ensureBrowserAvailable(); - const tab = await ctx.openTab("about:blank"); - res.json({ ok: true, tab }); - return true; - } - if (action === "close") { - const tabs = await ctx.listTabs(); - const targetTab = typeof index === "number" ? tabs[index] : tabs.at(0); - if (!targetTab) { - jsonError(res, 404, "tab not found"); - return true; - } - await ctx.closeTab(targetTab.targetId); - res.json({ ok: true, targetId: targetTab.targetId }); - return true; - } - if (action === "select") { - if (typeof index !== "number") { - jsonError(res, 400, "index is required"); - return true; - } - const tabs = await ctx.listTabs(); - const targetTab = tabs[index]; - if (!targetTab) { - jsonError(res, 404, "tab not found"); - return true; - } - await ctx.focusTab(targetTab.targetId); - res.json({ ok: true, targetId: targetTab.targetId }); - return true; - } - jsonError(res, 400, "unknown tab action"); - return true; - } - case "browser_wait_for": { + case "wait": { const time = toNumber(args.time); const text = toStringOrEmpty(args.text) || undefined; const textGone = toStringOrEmpty(args.textGone) || undefined; diff --git a/src/browser/routes/tool-extra.test.ts b/src/browser/routes/actions-extra.test.ts similarity index 86% rename from src/browser/routes/tool-extra.test.ts rename to src/browser/routes/actions-extra.test.ts index ef1f402d9..3131101bf 100644 --- a/src/browser/routes/tool-extra.test.ts +++ b/src/browser/routes/actions-extra.test.ts @@ -30,7 +30,7 @@ const media = vi.hoisted(() => ({ vi.mock("../pw-ai.js", () => pw); vi.mock("../../media/store.js", () => media); -import { handleBrowserToolExtra } from "./tool-extra.js"; +import { handleBrowserActionExtra } from "./actions-extra.js"; const baseTab = { targetId: "tab1", @@ -78,11 +78,14 @@ function createCtx( }; } -async function callTool(name: string, args: Record = {}) { +async function callAction( + action: Parameters[0]["action"], + args: Record = {}, +) { const res = createRes(); const ctx = createCtx(); - const handled = await handleBrowserToolExtra({ - name, + const handled = await handleBrowserActionExtra({ + action, args, targetId: "", cdpPort: 18792, @@ -96,11 +99,11 @@ beforeEach(() => { vi.clearAllMocks(); }); -describe("handleBrowserToolExtra", () => { - it("dispatches extra Playwright tools", async () => { +describe("handleBrowserActionExtra", () => { + it("dispatches extra browser actions", async () => { const cases = [ { - name: "browser_console_messages", + action: "console" as const, args: { level: "error" }, fn: pw.getConsoleMessagesViaPlaywright, expectArgs: { @@ -111,7 +114,7 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true, messages: [], targetId: "tab1" }, }, { - name: "browser_network_requests", + action: "network" as const, args: { includeStatic: true }, fn: pw.getNetworkRequestsViaPlaywright, expectArgs: { @@ -122,14 +125,14 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true, requests: [], targetId: "tab1" }, }, { - name: "browser_start_tracing", + action: "traceStart" as const, args: {}, fn: pw.startTracingViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1" }, expectBody: { ok: true }, }, { - name: "browser_verify_element_visible", + action: "verifyElement" as const, args: { role: "button", accessibleName: "Submit" }, fn: pw.verifyElementVisibleViaPlaywright, expectArgs: { @@ -141,14 +144,14 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true }, }, { - name: "browser_verify_text_visible", + action: "verifyText" as const, args: { text: "Hello" }, fn: pw.verifyTextVisibleViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1", text: "Hello" }, expectBody: { ok: true }, }, { - name: "browser_verify_list_visible", + action: "verifyList" as const, args: { ref: "1", items: ["a", "b"] }, fn: pw.verifyListVisibleViaPlaywright, expectArgs: { @@ -160,7 +163,7 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true }, }, { - name: "browser_verify_value", + action: "verifyValue" as const, args: { ref: "2", type: "textbox", value: "x" }, fn: pw.verifyValueViaPlaywright, expectArgs: { @@ -173,14 +176,14 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true }, }, { - name: "browser_mouse_move_xy", + action: "mouseMove" as const, args: { x: 10, y: 20 }, fn: pw.mouseMoveViaPlaywright, expectArgs: { cdpPort: 18792, targetId: "tab1", x: 10, y: 20 }, expectBody: { ok: true }, }, { - name: "browser_mouse_click_xy", + action: "mouseClick" as const, args: { x: 1, y: 2, button: "right" }, fn: pw.mouseClickViaPlaywright, expectArgs: { @@ -193,7 +196,7 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true }, }, { - name: "browser_mouse_drag_xy", + action: "mouseDrag" as const, args: { startX: 1, startY: 2, endX: 3, endY: 4 }, fn: pw.mouseDragViaPlaywright, expectArgs: { @@ -207,7 +210,7 @@ describe("handleBrowserToolExtra", () => { expectBody: { ok: true }, }, { - name: "browser_generate_locator", + action: "locator" as const, args: { ref: "99" }, fn: pw.generateLocatorForRef, expectArgs: "99", @@ -216,7 +219,7 @@ describe("handleBrowserToolExtra", () => { ]; for (const item of cases) { - const { res, handled } = await callTool(item.name, item.args); + const { res, handled } = await callAction(item.action, item.args); expect(handled).toBe(true); expect(item.fn).toHaveBeenCalledWith(item.expectArgs); expect(res.body).toEqual(item.expectBody); @@ -224,7 +227,7 @@ describe("handleBrowserToolExtra", () => { }); it("stores PDF and trace outputs", async () => { - const { res: pdfRes } = await callTool("browser_pdf_save"); + const { res: pdfRes } = await callAction("pdf"); expect(pw.pdfViaPlaywright).toHaveBeenCalledWith({ cdpPort: 18792, targetId: "tab1", @@ -239,7 +242,7 @@ describe("handleBrowserToolExtra", () => { }); media.saveMediaBuffer.mockResolvedValueOnce({ path: "/tmp/fake.zip" }); - const { res: traceRes } = await callTool("browser_stop_tracing"); + const { res: traceRes } = await callAction("traceStop"); expect(pw.stopTracingViaPlaywright).toHaveBeenCalledWith({ cdpPort: 18792, targetId: "tab1", diff --git a/src/browser/routes/tool-extra.ts b/src/browser/routes/actions-extra.ts similarity index 89% rename from src/browser/routes/tool-extra.ts rename to src/browser/routes/actions-extra.ts index fe353016c..ab8b1142e 100644 --- a/src/browser/routes/tool-extra.ts +++ b/src/browser/routes/actions-extra.ts @@ -27,8 +27,23 @@ import { toStringOrEmpty, } from "./utils.js"; -type ToolExtraParams = { - name: string; +export type BrowserActionExtra = + | "console" + | "locator" + | "mouseClick" + | "mouseDrag" + | "mouseMove" + | "network" + | "pdf" + | "traceStart" + | "traceStop" + | "verifyElement" + | "verifyList" + | "verifyText" + | "verifyValue"; + +type ActionExtraParams = { + action: BrowserActionExtra; args: Record; targetId: string; cdpPort: number; @@ -36,14 +51,14 @@ type ToolExtraParams = { res: express.Response; }; -export async function handleBrowserToolExtra( - params: ToolExtraParams, +export async function handleBrowserActionExtra( + params: ActionExtraParams, ): Promise { - const { name, args, targetId, cdpPort, ctx, res } = params; + const { action, args, targetId, cdpPort, ctx, res } = params; const target = targetId || undefined; - switch (name) { - case "browser_console_messages": { + switch (action) { + case "console": { const level = toStringOrEmpty(args.level) || undefined; const tab = await ctx.ensureTabAvailable(target); const messages = await getConsoleMessagesViaPlaywright({ @@ -54,7 +69,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true, messages, targetId: tab.targetId }); return true; } - case "browser_network_requests": { + case "network": { const includeStatic = toBoolean(args.includeStatic) ?? false; const tab = await ctx.ensureTabAvailable(target); const requests = await getNetworkRequestsViaPlaywright({ @@ -65,7 +80,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true, requests, targetId: tab.targetId }); return true; } - case "browser_pdf_save": { + case "pdf": { const tab = await ctx.ensureTabAvailable(target); const pdf = await pdfViaPlaywright({ cdpPort, @@ -86,7 +101,7 @@ export async function handleBrowserToolExtra( }); return true; } - case "browser_start_tracing": { + case "traceStart": { const tab = await ctx.ensureTabAvailable(target); await startTracingViaPlaywright({ cdpPort, @@ -95,7 +110,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_stop_tracing": { + case "traceStop": { const tab = await ctx.ensureTabAvailable(target); const trace = await stopTracingViaPlaywright({ cdpPort, @@ -116,7 +131,7 @@ export async function handleBrowserToolExtra( }); return true; } - case "browser_verify_element_visible": { + case "verifyElement": { const role = toStringOrEmpty(args.role); const accessibleName = toStringOrEmpty(args.accessibleName); if (!role || !accessibleName) { @@ -133,7 +148,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_verify_text_visible": { + case "verifyText": { const text = toStringOrEmpty(args.text); if (!text) { jsonError(res, 400, "text is required"); @@ -148,7 +163,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_verify_list_visible": { + case "verifyList": { const ref = toStringOrEmpty(args.ref); const items = toStringArray(args.items); if (!ref || !items?.length) { @@ -165,7 +180,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_verify_value": { + case "verifyValue": { const ref = toStringOrEmpty(args.ref); const type = toStringOrEmpty(args.type); const value = toStringOrEmpty(args.value); @@ -184,7 +199,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_mouse_move_xy": { + case "mouseMove": { const x = toNumber(args.x); const y = toNumber(args.y); if (x === undefined || y === undefined) { @@ -201,7 +216,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_mouse_click_xy": { + case "mouseClick": { const x = toNumber(args.x); const y = toNumber(args.y); if (x === undefined || y === undefined) { @@ -220,7 +235,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_mouse_drag_xy": { + case "mouseDrag": { const startX = toNumber(args.startX); const startY = toNumber(args.startY); const endX = toNumber(args.endX); @@ -246,7 +261,7 @@ export async function handleBrowserToolExtra( res.json({ ok: true }); return true; } - case "browser_generate_locator": { + case "locator": { const ref = toStringOrEmpty(args.ref); if (!ref) { jsonError(res, 400, "ref is required"); diff --git a/src/browser/routes/actions.ts b/src/browser/routes/actions.ts new file mode 100644 index 000000000..e4187a0b8 --- /dev/null +++ b/src/browser/routes/actions.ts @@ -0,0 +1,249 @@ +import type express from "express"; + +import type { BrowserRouteContext } from "../server-context.js"; +import { handleBrowserActionCore } from "./actions-core.js"; +import { handleBrowserActionExtra } from "./actions-extra.js"; +import { jsonError, toBoolean, toStringOrEmpty } from "./utils.js"; + +function readBody(req: express.Request): Record { + const body = req.body as Record | undefined; + if (!body || typeof body !== "object" || Array.isArray(body)) return {}; + return body; +} + +function readTargetId(value: unknown): string { + return toStringOrEmpty(value); +} + +function handleActionError( + ctx: BrowserRouteContext, + res: express.Response, + err: unknown, +) { + const mapped = ctx.mapTabError(err); + if (mapped) return jsonError(res, mapped.status, mapped.message); + jsonError(res, 500, String(err)); +} + +async function runCoreAction( + ctx: BrowserRouteContext, + res: express.Response, + action: Parameters[0]["action"], + args: Record, + targetId: string, +) { + try { + const cdpPort = ctx.state().cdpPort; + await handleBrowserActionCore({ + action, + args, + targetId, + cdpPort, + ctx, + res, + }); + } catch (err) { + handleActionError(ctx, res, err); + } +} + +async function runExtraAction( + ctx: BrowserRouteContext, + res: express.Response, + action: Parameters[0]["action"], + args: Record, + targetId: string, +) { + try { + const cdpPort = ctx.state().cdpPort; + await handleBrowserActionExtra({ + action, + args, + targetId, + cdpPort, + ctx, + res, + }); + } catch (err) { + handleActionError(ctx, res, err); + } +} + +export function registerBrowserActionRoutes( + app: express.Express, + ctx: BrowserRouteContext, +) { + app.post("/navigate", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "navigate", body, targetId); + }); + + app.post("/back", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "back", body, targetId); + }); + + app.post("/resize", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "resize", body, targetId); + }); + + app.post("/close", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "close", body, targetId); + }); + + app.post("/click", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "click", body, targetId); + }); + + app.post("/type", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "type", body, targetId); + }); + + app.post("/press", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "press", body, targetId); + }); + + app.post("/hover", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "hover", body, targetId); + }); + + app.post("/drag", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "drag", body, targetId); + }); + + app.post("/select", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "select", body, targetId); + }); + + app.post("/upload", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "upload", body, targetId); + }); + + app.post("/fill", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "fill", body, targetId); + }); + + app.post("/dialog", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "dialog", body, targetId); + }); + + app.post("/wait", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "wait", body, targetId); + }); + + app.post("/evaluate", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "evaluate", body, targetId); + }); + + app.post("/run", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runCoreAction(ctx, res, "run", body, targetId); + }); + + app.get("/console", async (req, res) => { + const targetId = readTargetId(req.query.targetId); + const level = toStringOrEmpty(req.query.level); + const args = level ? { level } : {}; + await runExtraAction(ctx, res, "console", args, targetId); + }); + + app.get("/network", async (req, res) => { + const targetId = readTargetId(req.query.targetId); + const includeStatic = toBoolean(req.query.includeStatic) ?? false; + await runExtraAction(ctx, res, "network", { includeStatic }, targetId); + }); + + app.post("/trace/start", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "traceStart", body, targetId); + }); + + app.post("/trace/stop", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "traceStop", body, targetId); + }); + + app.post("/pdf", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "pdf", body, targetId); + }); + + app.post("/verify/element", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "verifyElement", body, targetId); + }); + + app.post("/verify/text", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "verifyText", body, targetId); + }); + + app.post("/verify/list", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "verifyList", body, targetId); + }); + + app.post("/verify/value", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "verifyValue", body, targetId); + }); + + app.post("/mouse/move", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "mouseMove", body, targetId); + }); + + app.post("/mouse/click", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "mouseClick", body, targetId); + }); + + app.post("/mouse/drag", async (req, res) => { + const body = readBody(req); + const targetId = readTargetId(body.targetId); + await runExtraAction(ctx, res, "mouseDrag", body, targetId); + }); + + app.post("/locator", async (req, res) => { + const body = readBody(req); + await runExtraAction(ctx, res, "locator", body, ""); + }); +} diff --git a/src/browser/routes/index.ts b/src/browser/routes/index.ts index 4f1bf5dd2..5dbc2dfca 100644 --- a/src/browser/routes/index.ts +++ b/src/browser/routes/index.ts @@ -1,10 +1,10 @@ import type express from "express"; import type { BrowserRouteContext } from "../server-context.js"; +import { registerBrowserActionRoutes } from "./actions.js"; import { registerBrowserBasicRoutes } from "./basic.js"; import { registerBrowserInspectRoutes } from "./inspect.js"; import { registerBrowserTabRoutes } from "./tabs.js"; -import { registerBrowserToolRoutes } from "./tool.js"; export function registerBrowserRoutes( app: express.Express, @@ -13,5 +13,5 @@ export function registerBrowserRoutes( registerBrowserBasicRoutes(app, ctx); registerBrowserTabRoutes(app, ctx); registerBrowserInspectRoutes(app, ctx); - registerBrowserToolRoutes(app, ctx); + registerBrowserActionRoutes(app, ctx); } diff --git a/src/browser/routes/inspect.ts b/src/browser/routes/inspect.ts index f803e65dd..d5c750ecf 100644 --- a/src/browser/routes/inspect.ts +++ b/src/browser/routes/inspect.ts @@ -281,27 +281,4 @@ export function registerBrowserInspectRoutes( jsonError(res, 500, String(err)); } }); - - app.post("/click", async (req, res) => { - const ref = toStringOrEmpty((req.body as { ref?: unknown })?.ref); - const targetId = toStringOrEmpty( - (req.body as { targetId?: unknown })?.targetId, - ); - - if (!ref) return jsonError(res, 400, "ref is required"); - - try { - const tab = await ctx.ensureTabAvailable(targetId || undefined); - await clickViaPlaywright({ - cdpPort: ctx.state().cdpPort, - targetId: tab.targetId, - ref, - }); - res.json({ ok: true, targetId: tab.targetId, url: tab.url }); - } catch (err) { - const mapped = ctx.mapTabError(err); - if (mapped) return jsonError(res, mapped.status, mapped.message); - jsonError(res, 500, String(err)); - } - }); } diff --git a/src/browser/routes/tool.ts b/src/browser/routes/tool.ts deleted file mode 100644 index fb095ba4c..000000000 --- a/src/browser/routes/tool.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type express from "express"; - -import type { BrowserRouteContext } from "../server-context.js"; -import { handleBrowserToolCore } from "./tool-core.js"; -import { handleBrowserToolExtra } from "./tool-extra.js"; -import { jsonError, toStringOrEmpty } from "./utils.js"; - -type ToolRequestBody = { - name?: unknown; - args?: unknown; - targetId?: unknown; -}; - -function toolArgs(value: unknown): Record { - if (!value || typeof value !== "object" || Array.isArray(value)) return {}; - return value as Record; -} - -export function registerBrowserToolRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { - app.post("/tool", async (req, res) => { - const body = req.body as ToolRequestBody; - const name = toStringOrEmpty(body?.name); - if (!name) return jsonError(res, 400, "name is required"); - const args = toolArgs(body?.args); - const targetId = toStringOrEmpty(body?.targetId || args?.targetId); - - try { - let cdpPort: number; - try { - cdpPort = ctx.state().cdpPort; - } catch { - return jsonError(res, 503, "browser server not started"); - } - - const handledCore = await handleBrowserToolCore({ - name, - args, - targetId, - cdpPort, - ctx, - res, - }); - if (handledCore) return; - - const handledExtra = await handleBrowserToolExtra({ - name, - args, - targetId, - cdpPort, - ctx, - res, - }); - if (handledExtra) return; - - return jsonError(res, 400, "unknown tool name"); - } catch (err) { - const mapped = ctx.mapTabError(err); - if (mapped) return jsonError(res, mapped.status, mapped.message); - jsonError(res, 500, String(err)); - } - }); -} diff --git a/src/cli/browser-cli-actions-input.ts b/src/cli/browser-cli-actions-input.ts new file mode 100644 index 000000000..ce1051a11 --- /dev/null +++ b/src/cli/browser-cli-actions-input.ts @@ -0,0 +1,492 @@ +import type { Command } from "commander"; +import { resolveBrowserControlUrl } from "../browser/client.js"; +import { + browserBack, + browserClick, + browserDrag, + browserEvaluate, + browserFillForm, + browserHandleDialog, + browserHover, + browserNavigate, + browserPressKey, + browserResize, + browserRunCode, + browserSelectOption, + browserType, + browserUpload, + browserWaitFor, +} from "../browser/client-actions.js"; +import { danger } from "../globals.js"; +import { defaultRuntime } from "../runtime.js"; +import type { BrowserParentOpts } from "./browser-cli-shared.js"; + +async function readStdin(): Promise { + const chunks: string[] = []; + return await new Promise((resolve, reject) => { + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => chunks.push(chunk)); + process.stdin.on("end", () => resolve(chunks.join(""))); + process.stdin.on("error", reject); + }); +} + +async function readFile(path: string): Promise { + const fs = await import("node:fs/promises"); + return await fs.readFile(path, "utf8"); +} + +async function readCode(opts: { + code?: string; + codeFile?: string; + codeStdin?: boolean; +}): Promise { + if (opts.codeFile) return await readFile(opts.codeFile); + if (opts.codeStdin) return await readStdin(); + return opts.code ?? ""; +} + +async function readFields(opts: { + fields?: string; + fieldsFile?: string; +}): Promise>> { + const payload = opts.fieldsFile + ? await readFile(opts.fieldsFile) + : (opts.fields ?? ""); + if (!payload.trim()) throw new Error("fields are required"); + const parsed = JSON.parse(payload) as unknown; + if (!Array.isArray(parsed)) throw new Error("fields must be an array"); + return parsed as Array>; +} + +export function registerBrowserActionInputCommands( + browser: Command, + parentOpts: (cmd: Command) => BrowserParentOpts, +) { + browser + .command("navigate") + .description("Navigate the current tab to a URL") + .argument("", "URL to navigate to") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (url: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserNavigate(baseUrl, { + url, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`navigated to ${result.url ?? url}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("back") + .description("Navigate back in history") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserBack(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log( + `navigated back to ${result.url ?? "previous page"}`, + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("resize") + .description("Resize the viewport") + .argument("", "Viewport width", (v: string) => Number(v)) + .argument("", "Viewport height", (v: string) => Number(v)) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (width: number, height: number, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if (!Number.isFinite(width) || !Number.isFinite(height)) { + defaultRuntime.error(danger("width and height must be numbers")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserResize(baseUrl, { + width, + height, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`resized to ${width}x${height}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("click") + .description("Click an element by ref from an ai snapshot (e.g. 76)") + .argument("", "Ref id from ai snapshot") + .option("--target-id ", "CDP target id (or unique prefix)") + .option("--double", "Double click", false) + .option("--button ", "Mouse button to use") + .option("--modifiers ", "Comma-separated modifiers (Shift,Alt,Meta)") + .action(async (ref: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const modifiers = opts.modifiers + ? String(opts.modifiers) + .split(",") + .map((v: string) => v.trim()) + .filter(Boolean) + : undefined; + try { + const result = await browserClick(baseUrl, { + ref, + targetId: opts.targetId?.trim() || undefined, + doubleClick: Boolean(opts.double), + button: opts.button?.trim() || undefined, + modifiers, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + const suffix = result.url ? ` on ${result.url}` : ""; + defaultRuntime.log(`clicked ref ${ref}${suffix}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("type") + .description("Type into an element by ai ref") + .argument("", "Ref id from ai snapshot") + .argument("", "Text to type") + .option("--submit", "Press Enter after typing", false) + .option("--slowly", "Type slowly (human-like)", false) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (ref: string, text: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserType(baseUrl, { + ref, + text, + submit: Boolean(opts.submit), + slowly: Boolean(opts.slowly), + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`typed into ref ${ref}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("press") + .description("Press a key") + .argument("", "Key to press (e.g. Enter)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (key: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserPressKey(baseUrl, { + key, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`pressed ${key}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("hover") + .description("Hover an element by ai ref") + .argument("", "Ref id from ai snapshot") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (ref: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserHover(baseUrl, { + ref, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`hovered ref ${ref}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("drag") + .description("Drag from one ref to another") + .argument("", "Start ref id") + .argument("", "End ref id") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (startRef: string, endRef: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserDrag(baseUrl, { + startRef, + endRef, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`dragged ${startRef} → ${endRef}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("select") + .description("Select option(s) in a select element") + .argument("", "Ref id from ai snapshot") + .argument("", "Option values to select") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (ref: string, values: string[], opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserSelectOption(baseUrl, { + ref, + values, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`selected ${values.join(", ")}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("upload") + .description("Upload file(s) when a file chooser is open") + .argument("", "File paths to upload") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (paths: string[], opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserUpload(baseUrl, { + paths, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`uploaded ${paths.length} file(s)`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("fill") + .description("Fill a form with JSON field descriptors") + .option("--fields ", "JSON array of field objects") + .option("--fields-file ", "Read JSON array from a file") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const fields = await readFields({ + fields: opts.fields, + fieldsFile: opts.fieldsFile, + }); + const result = await browserFillForm(baseUrl, { + fields, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`filled ${fields.length} field(s)`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("dialog") + .description("Handle a modal dialog (alert/confirm/prompt)") + .option("--accept", "Accept the dialog", false) + .option("--dismiss", "Dismiss the dialog", false) + .option("--prompt ", "Prompt response text") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const accept = opts.accept ? true : opts.dismiss ? false : undefined; + if (accept === undefined) { + defaultRuntime.error(danger("Specify --accept or --dismiss")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserHandleDialog(baseUrl, { + accept, + promptText: opts.prompt?.trim() || undefined, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`dialog handled: ${result.type}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("wait") + .description("Wait for time or text conditions") + .option("--time ", "Wait for N milliseconds", (v: string) => Number(v)) + .option("--text ", "Wait for text to appear") + .option("--text-gone ", "Wait for text to disappear") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserWaitFor(baseUrl, { + time: Number.isFinite(opts.time) ? opts.time : undefined, + text: opts.text?.trim() || undefined, + textGone: opts.textGone?.trim() || undefined, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("wait complete"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("evaluate") + .description("Evaluate a function against the page or a ref") + .option("--fn ", "Function source, e.g. (el) => el.textContent") + .option("--ref ", "ARIA ref from ai snapshot") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if (!opts.fn) { + defaultRuntime.error(danger("Missing --fn")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserEvaluate(baseUrl, { + fn: opts.fn, + ref: opts.ref?.trim() || undefined, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.result, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("run") + .description("Run a Playwright code function (page => ...) ") + .option("--code ", "Function source, e.g. (page) => page.title()") + .option("--code-file ", "Read function source from a file") + .option("--code-stdin", "Read function source from stdin", false) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const code = await readCode({ + code: opts.code, + codeFile: opts.codeFile, + codeStdin: Boolean(opts.codeStdin), + }); + if (!code.trim()) { + defaultRuntime.error(danger("Missing --code (or file/stdin)")); + defaultRuntime.exit(1); + return; + } + const result = await browserRunCode(baseUrl, { + code, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.result, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); +} diff --git a/src/cli/browser-cli-actions-observe.ts b/src/cli/browser-cli-actions-observe.ts new file mode 100644 index 000000000..ffce3f271 --- /dev/null +++ b/src/cli/browser-cli-actions-observe.ts @@ -0,0 +1,379 @@ +import type { Command } from "commander"; +import { resolveBrowserControlUrl } from "../browser/client.js"; +import { + browserConsoleMessages, + browserGenerateLocator, + browserMouseClick, + browserMouseDrag, + browserMouseMove, + browserNetworkRequests, + browserPdfSave, + browserStartTracing, + browserStopTracing, + browserVerifyElementVisible, + browserVerifyListVisible, + browserVerifyTextVisible, + browserVerifyValue, +} from "../browser/client-actions.js"; +import { danger } from "../globals.js"; +import { defaultRuntime } from "../runtime.js"; +import type { BrowserParentOpts } from "./browser-cli-shared.js"; + +export function registerBrowserActionObserveCommands( + browser: Command, + parentOpts: (cmd: Command) => BrowserParentOpts, +) { + browser + .command("console") + .description("Get recent console messages") + .option("--level ", "Filter by level (error, warn, info)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserConsoleMessages(baseUrl, { + level: opts.level?.trim() || undefined, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.messages, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("network") + .description("Get recent network requests") + .option("--include-static", "Include static assets", false) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserNetworkRequests(baseUrl, { + includeStatic: Boolean(opts.includeStatic), + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.requests, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("trace-start") + .description("Start Playwright tracing") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserStartTracing(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("trace started"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("trace-stop") + .description("Stop tracing and save a trace.zip") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserStopTracing(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`trace: ${result.path}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("pdf") + .description("Save page as PDF") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserPdfSave(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`PDF: ${result.path}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("verify-element") + .description("Verify element visible by role + name") + .option("--role ", "ARIA role") + .option("--name ", "Accessible name") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if (!opts.role || !opts.name) { + defaultRuntime.error(danger("--role and --name are required")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserVerifyElementVisible(baseUrl, { + role: opts.role, + accessibleName: opts.name, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("element visible"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("verify-text") + .description("Verify text is visible") + .argument("", "Text to find") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (text: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserVerifyTextVisible(baseUrl, { + text, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("text visible"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("verify-list") + .description("Verify list items under a ref") + .argument("", "Ref id from ai snapshot") + .argument("", "List items to verify") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (ref: string, items: string[], opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserVerifyListVisible(baseUrl, { + ref, + items, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("list visible"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("verify-value") + .description("Verify a form control value") + .option("--ref ", "Ref id from ai snapshot") + .option("--type ", "Input type (textbox, checkbox, slider, etc)") + .option("--value ", "Expected value") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if (!opts.ref || !opts.type) { + defaultRuntime.error(danger("--ref and --type are required")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserVerifyValue(baseUrl, { + ref: opts.ref, + type: opts.type, + value: opts.value, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("value verified"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("mouse-move") + .description("Move mouse to viewport coordinates") + .option("--x ", "X coordinate", (v: string) => Number(v)) + .option("--y ", "Y coordinate", (v: string) => Number(v)) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if (!Number.isFinite(opts.x) || !Number.isFinite(opts.y)) { + defaultRuntime.error(danger("--x and --y are required")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserMouseMove(baseUrl, { + x: opts.x, + y: opts.y, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("mouse moved"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("mouse-click") + .description("Click at viewport coordinates") + .option("--x ", "X coordinate", (v: string) => Number(v)) + .option("--y ", "Y coordinate", (v: string) => Number(v)) + .option("--button ", "Mouse button") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if (!Number.isFinite(opts.x) || !Number.isFinite(opts.y)) { + defaultRuntime.error(danger("--x and --y are required")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserMouseClick(baseUrl, { + x: opts.x, + y: opts.y, + button: opts.button?.trim() || undefined, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("mouse clicked"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("mouse-drag") + .description("Drag by viewport coordinates") + .option("--start-x ", "Start X", (v: string) => Number(v)) + .option("--start-y ", "Start Y", (v: string) => Number(v)) + .option("--end-x ", "End X", (v: string) => Number(v)) + .option("--end-y ", "End Y", (v: string) => Number(v)) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + if ( + !Number.isFinite(opts.startX) || + !Number.isFinite(opts.startY) || + !Number.isFinite(opts.endX) || + !Number.isFinite(opts.endY) + ) { + defaultRuntime.error( + danger("--start-x, --start-y, --end-x, --end-y are required"), + ); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserMouseDrag(baseUrl, { + startX: opts.startX, + startY: opts.startY, + endX: opts.endX, + endY: opts.endY, + targetId: opts.targetId?.trim() || undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("mouse dragged"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("locator") + .description("Generate a Playwright locator for a ref") + .argument("", "Ref id from ai snapshot") + .action(async (ref: string, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserGenerateLocator(baseUrl, { ref }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(result.locator); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); +} diff --git a/src/cli/browser-cli-examples.ts b/src/cli/browser-cli-examples.ts new file mode 100644 index 000000000..945661fa5 --- /dev/null +++ b/src/cli/browser-cli-examples.ts @@ -0,0 +1,48 @@ +export const browserCoreExamples = [ + "clawdis browser status", + "clawdis browser start", + "clawdis browser stop", + "clawdis browser tabs", + "clawdis browser open https://example.com", + "clawdis browser focus abcd1234", + "clawdis browser close abcd1234", + "clawdis browser screenshot", + "clawdis browser screenshot --full-page", + "clawdis browser screenshot --ref 12", + 'clawdis browser eval "document.title"', + 'clawdis browser query "a" --limit 5', + "clawdis browser dom --format text --max-chars 5000", + "clawdis browser snapshot --format aria --limit 200", + "clawdis browser snapshot --format ai", +]; + +export const browserActionExamples = [ + "clawdis browser navigate https://example.com", + "clawdis browser back", + "clawdis browser resize 1280 720", + "clawdis browser click 12 --double", + 'clawdis browser type 23 "hello" --submit', + "clawdis browser press Enter", + "clawdis browser hover 44", + "clawdis browser drag 10 11", + "clawdis browser select 9 OptionA OptionB", + "clawdis browser upload /tmp/file.pdf", + 'clawdis browser fill --fields \'[{"ref":"1","value":"Ada"}]\'', + "clawdis browser dialog --accept", + 'clawdis browser wait --text "Done"', + "clawdis browser evaluate --fn '(el) => el.textContent' --ref 7", + "clawdis browser run --code '(page) => page.title()'", + "clawdis browser console --level error", + "clawdis browser network --include-static", + "clawdis browser trace-start", + "clawdis browser trace-stop", + "clawdis browser pdf", + 'clawdis browser verify-element --role button --name "Submit"', + 'clawdis browser verify-text "Welcome"', + "clawdis browser verify-list 3 ItemA ItemB", + "clawdis browser verify-value --ref 4 --type textbox --value hello", + "clawdis browser mouse-move --x 120 --y 240", + "clawdis browser mouse-click --x 120 --y 240", + "clawdis browser mouse-drag --start-x 10 --start-y 20 --end-x 200 --end-y 300", + "clawdis browser locator 77", +]; diff --git a/src/cli/browser-cli-inspect.ts b/src/cli/browser-cli-inspect.ts new file mode 100644 index 000000000..e934853bc --- /dev/null +++ b/src/cli/browser-cli-inspect.ts @@ -0,0 +1,273 @@ +import type { Command } from "commander"; + +import { + browserDom, + browserEval, + browserQuery, + browserScreenshot, + browserSnapshot, + resolveBrowserControlUrl, +} from "../browser/client.js"; +import { browserScreenshotAction } from "../browser/client-actions.js"; +import { danger } from "../globals.js"; +import { defaultRuntime } from "../runtime.js"; +import type { BrowserParentOpts } from "./browser-cli-shared.js"; + +async function readStdin(): Promise { + const chunks: string[] = []; + return await new Promise((resolve, reject) => { + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => chunks.push(chunk)); + process.stdin.on("end", () => resolve(chunks.join(""))); + process.stdin.on("error", reject); + }); +} + +async function readTextFromSource(opts: { + js?: string; + jsFile?: string; + jsStdin?: boolean; +}): Promise { + if (opts.jsFile) { + const fs = await import("node:fs/promises"); + return await fs.readFile(opts.jsFile, "utf8"); + } + if (opts.jsStdin) { + return await readStdin(); + } + return opts.js ?? ""; +} + +export function registerBrowserInspectCommands( + browser: Command, + parentOpts: (cmd: Command) => BrowserParentOpts, +) { + browser + .command("screenshot") + .description("Capture a screenshot (MEDIA:)") + .argument("[targetId]", "CDP target id (or unique prefix)") + .option("--full-page", "Capture full scrollable page", false) + .option("--ref ", "ARIA ref from ai snapshot") + .option("--element ", "CSS selector for element screenshot") + .option("--type ", "Output type (default: png)", "png") + .option("--filename ", "Preferred output filename") + .action(async (targetId: string | undefined, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const advanced = Boolean(opts.ref || opts.element || opts.filename); + const result = advanced + ? await browserScreenshotAction(baseUrl, { + targetId: targetId?.trim() || undefined, + fullPage: Boolean(opts.fullPage), + ref: opts.ref?.trim() || undefined, + element: opts.element?.trim() || undefined, + filename: opts.filename?.trim() || undefined, + type: opts.type === "jpeg" ? "jpeg" : "png", + }) + : await browserScreenshot(baseUrl, { + targetId: targetId?.trim() || undefined, + fullPage: Boolean(opts.fullPage), + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`MEDIA:${result.path}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("eval") + .description("Run JavaScript in the active tab") + .argument("[js]", "JavaScript expression") + .option("--js-file ", "Read JavaScript from a file") + .option("--js-stdin", "Read JavaScript from stdin", false) + .option("--target-id ", "CDP target id (or unique prefix)") + .option("--await", "Await promise result", false) + .action(async (js: string | undefined, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const source = await readTextFromSource({ + js, + jsFile: opts.jsFile, + jsStdin: Boolean(opts.jsStdin), + }); + if (!source.trim()) { + defaultRuntime.error(danger("Missing JavaScript input.")); + defaultRuntime.exit(1); + return; + } + const result = await browserEval(baseUrl, { + js: source, + targetId: opts.targetId?.trim() || undefined, + awaitPromise: Boolean(opts.await), + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.result, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("query") + .description("Query selector matches") + .argument("", "CSS selector") + .option("--target-id ", "CDP target id (or unique prefix)") + .option("--limit ", "Max matches (default: 20)", (v: string) => + Number(v), + ) + .action(async (selector: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const result = await browserQuery(baseUrl, { + selector, + targetId: opts.targetId?.trim() || undefined, + limit: Number.isFinite(opts.limit) ? opts.limit : undefined, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.matches, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("dom") + .description("Dump DOM (html or text) with truncation") + .option("--format ", "Output format (default: html)", "html") + .option("--target-id ", "CDP target id (or unique prefix)") + .option("--selector ", "Optional CSS selector to scope the dump") + .option( + "--max-chars ", + "Max characters (default: 200000)", + (v: string) => Number(v), + ) + .option("--out ", "Write output to a file") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const format = opts.format === "text" ? "text" : "html"; + try { + const result = await browserDom(baseUrl, { + format, + targetId: opts.targetId?.trim() || undefined, + maxChars: Number.isFinite(opts.maxChars) ? opts.maxChars : undefined, + selector: opts.selector?.trim() || undefined, + }); + if (opts.out) { + const fs = await import("node:fs/promises"); + await fs.writeFile(opts.out, result.text, "utf8"); + if (parent?.json) { + defaultRuntime.log( + JSON.stringify({ ok: true, out: opts.out }, null, 2), + ); + } else { + defaultRuntime.log(opts.out); + } + return; + } + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(result.text); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("snapshot") + .description("Capture an AI-friendly snapshot (aria, domSnapshot, or ai)") + .option( + "--format ", + "Snapshot format (default: aria)", + "aria", + ) + .option("--target-id ", "CDP target id (or unique prefix)") + .option("--limit ", "Max nodes (default: 500/800)", (v: string) => + Number(v), + ) + .option("--out ", "Write snapshot to a file") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const format = + opts.format === "domSnapshot" + ? "domSnapshot" + : opts.format === "ai" + ? "ai" + : "aria"; + try { + const result = await browserSnapshot(baseUrl, { + format, + targetId: opts.targetId?.trim() || undefined, + limit: Number.isFinite(opts.limit) ? opts.limit : undefined, + }); + + if (opts.out) { + const fs = await import("node:fs/promises"); + if (result.format === "ai") { + await fs.writeFile(opts.out, result.snapshot, "utf8"); + } else { + const payload = JSON.stringify(result, null, 2); + await fs.writeFile(opts.out, payload, "utf8"); + } + if (parent?.json) { + defaultRuntime.log( + JSON.stringify({ ok: true, out: opts.out }, null, 2), + ); + } else { + defaultRuntime.log(opts.out); + } + return; + } + + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + + if (result.format === "ai") { + defaultRuntime.log(result.snapshot); + return; + } + + if (result.format === "domSnapshot") { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + + const nodes = "nodes" in result ? result.nodes : []; + defaultRuntime.log( + nodes + .map((n) => { + const indent = " ".repeat(Math.min(20, n.depth)); + const name = n.name ? ` "${n.name}"` : ""; + const value = n.value ? ` = "${n.value}"` : ""; + return `${indent}- ${n.role}${name}${value}`; + }) + .join("\n"), + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); +} diff --git a/src/cli/browser-cli-manage.ts b/src/cli/browser-cli-manage.ts new file mode 100644 index 000000000..90950bb38 --- /dev/null +++ b/src/cli/browser-cli-manage.ts @@ -0,0 +1,183 @@ +import type { Command } from "commander"; + +import { + browserCloseTab, + browserFocusTab, + browserOpenTab, + browserStart, + browserStatus, + browserStop, + browserTabs, + resolveBrowserControlUrl, +} from "../browser/client.js"; +import { browserClosePage } from "../browser/client-actions.js"; +import { danger, info } from "../globals.js"; +import { defaultRuntime } from "../runtime.js"; +import type { BrowserParentOpts } from "./browser-cli-shared.js"; + +export function registerBrowserManageCommands( + browser: Command, + parentOpts: (cmd: Command) => BrowserParentOpts, +) { + browser + .command("status") + .description("Show browser status") + .action(async (_opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const status = await browserStatus(baseUrl); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(status, null, 2)); + return; + } + defaultRuntime.log( + [ + `enabled: ${status.enabled}`, + `running: ${status.running}`, + `controlUrl: ${status.controlUrl}`, + `cdpPort: ${status.cdpPort}`, + `browser: ${status.chosenBrowser ?? "unknown"}`, + `profileColor: ${status.color}`, + ].join("\n"), + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("start") + .description("Start the clawd browser (no-op if already running)") + .action(async (_opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + await browserStart(baseUrl); + const status = await browserStatus(baseUrl); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(status, null, 2)); + return; + } + defaultRuntime.log(info(`🦞 clawd browser running: ${status.running}`)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("stop") + .description("Stop the clawd browser (best-effort)") + .action(async (_opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + await browserStop(baseUrl); + const status = await browserStatus(baseUrl); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(status, null, 2)); + return; + } + defaultRuntime.log(info(`🦞 clawd browser running: ${status.running}`)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("tabs") + .description("List open tabs") + .action(async (_opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const tabs = await browserTabs(baseUrl); + if (parent?.json) { + defaultRuntime.log(JSON.stringify({ tabs }, null, 2)); + return; + } + if (tabs.length === 0) { + defaultRuntime.log("No tabs (browser closed or no targets)."); + return; + } + defaultRuntime.log( + tabs + .map( + (t, i) => + `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`, + ) + .join("\n"), + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("open") + .description("Open a URL in a new tab") + .argument("", "URL to open") + .action(async (url: string, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + const tab = await browserOpenTab(baseUrl, url); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(tab, null, 2)); + return; + } + defaultRuntime.log(`opened: ${tab.url}\nid: ${tab.targetId}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("focus") + .description("Focus a tab by target id (or unique prefix)") + .argument("", "Target id or unique prefix") + .action(async (targetId: string, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + await browserFocusTab(baseUrl, targetId); + if (parent?.json) { + defaultRuntime.log(JSON.stringify({ ok: true }, null, 2)); + return; + } + defaultRuntime.log(`focused tab ${targetId}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("close") + .description("Close a tab (target id optional)") + .argument("[targetId]", "Target id or unique prefix (optional)") + .action(async (targetId: string | undefined, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + try { + if (targetId?.trim()) { + await browserCloseTab(baseUrl, targetId.trim()); + } else { + await browserClosePage(baseUrl); + } + if (parent?.json) { + defaultRuntime.log(JSON.stringify({ ok: true }, null, 2)); + return; + } + defaultRuntime.log("closed tab"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); +} diff --git a/src/cli/browser-cli-shared.ts b/src/cli/browser-cli-shared.ts new file mode 100644 index 000000000..0c31b9558 --- /dev/null +++ b/src/cli/browser-cli-shared.ts @@ -0,0 +1 @@ +export type BrowserParentOpts = { url?: string; json?: boolean }; diff --git a/src/cli/browser-cli.ts b/src/cli/browser-cli.ts index 555b83206..144a81fd4 100644 --- a/src/cli/browser-cli.ts +++ b/src/cli/browser-cli.ts @@ -1,24 +1,15 @@ import type { Command } from "commander"; -import { - browserClickRef, - browserCloseTab, - browserDom, - browserEval, - browserFocusTab, - browserOpenTab, - browserQuery, - browserScreenshot, - browserSnapshot, - browserStart, - browserStatus, - browserStop, - browserTabs, - browserTool, - resolveBrowserControlUrl, -} from "../browser/client.js"; -import { danger, info } from "../globals.js"; +import { danger } from "../globals.js"; import { defaultRuntime } from "../runtime.js"; +import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js"; +import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js"; +import { + browserActionExamples, + browserCoreExamples, +} from "./browser-cli-examples.js"; +import { registerBrowserInspectCommands } from "./browser-cli-inspect.js"; +import { registerBrowserManageCommands } from "./browser-cli-manage.js"; export function registerBrowserCli(program: Command) { const browser = program @@ -31,24 +22,10 @@ export function registerBrowserCli(program: Command) { .option("--json", "Output machine-readable JSON", false) .addHelpText( "after", - ` -Examples: - clawdis browser status - clawdis browser start - clawdis browser tabs - clawdis browser open https://example.com - clawdis browser screenshot # emits MEDIA: - clawdis browser screenshot --full-page - clawdis browser eval "location.href" - clawdis browser query "a" --limit 5 - clawdis browser dom --format text --max-chars 5000 - clawdis browser snapshot --format aria --limit 200 - clawdis browser snapshot --format ai - clawdis browser click 76 - clawdis browser tool browser_file_upload --args '{"paths":["/tmp/file.txt"]}' -`, + `\nExamples:\n ${[...browserCoreExamples, ...browserActionExamples].join("\n ")}\n`, ) .action(() => { + browser.outputHelp(); defaultRuntime.error( danger('Missing subcommand. Try: "clawdis browser status"'), ); @@ -58,425 +35,8 @@ Examples: const parentOpts = (cmd: Command) => cmd.parent?.opts?.() as { url?: string; json?: boolean }; - browser - .command("status") - .description("Show browser status") - .action(async (_opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const status = await browserStatus(baseUrl); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(status, null, 2)); - return; - } - defaultRuntime.log( - [ - `enabled: ${status.enabled}`, - `running: ${status.running}`, - `controlUrl: ${status.controlUrl}`, - `cdpPort: ${status.cdpPort}`, - `browser: ${status.chosenBrowser ?? "unknown"}`, - `profileColor: ${status.color}`, - ].join("\n"), - ); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("start") - .description("Start the clawd browser (no-op if already running)") - .action(async (_opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - await browserStart(baseUrl); - const status = await browserStatus(baseUrl); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(status, null, 2)); - return; - } - defaultRuntime.log(info(`🦞 clawd browser running: ${status.running}`)); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("stop") - .description("Stop the clawd browser (best-effort)") - .action(async (_opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - await browserStop(baseUrl); - const status = await browserStatus(baseUrl); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(status, null, 2)); - return; - } - defaultRuntime.log(info(`🦞 clawd browser running: ${status.running}`)); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("tabs") - .description("List open tabs") - .action(async (_opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const tabs = await browserTabs(baseUrl); - if (parent?.json) { - defaultRuntime.log(JSON.stringify({ tabs }, null, 2)); - return; - } - if (tabs.length === 0) { - defaultRuntime.log("No tabs (browser closed or no targets)."); - return; - } - defaultRuntime.log( - tabs - .map( - (t, i) => - `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`, - ) - .join("\n"), - ); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("open") - .description("Open a URL in a new tab") - .argument("", "URL to open") - .action(async (url: string, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const tab = await browserOpenTab(baseUrl, url); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(tab, null, 2)); - return; - } - defaultRuntime.log(`opened: ${tab.url}\nid: ${tab.targetId}`); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("focus") - .description("Focus a tab by target id (or unique prefix)") - .argument("", "Target id or unique prefix") - .action(async (targetId: string, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - await browserFocusTab(baseUrl, targetId); - if (parent?.json) { - defaultRuntime.log(JSON.stringify({ ok: true }, null, 2)); - return; - } - defaultRuntime.log(`focused tab ${targetId}`); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("close") - .description("Close a tab by target id (or unique prefix)") - .argument("", "Target id or unique prefix") - .action(async (targetId: string, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - await browserCloseTab(baseUrl, targetId); - if (parent?.json) { - defaultRuntime.log(JSON.stringify({ ok: true }, null, 2)); - return; - } - defaultRuntime.log(`closed tab ${targetId}`); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("screenshot") - .description("Capture a screenshot (MEDIA:)") - .argument("[targetId]", "CDP target id (or unique prefix)") - .option("--full-page", "Capture full scrollable page", false) - .action(async (targetId: string | undefined, opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const result = await browserScreenshot(baseUrl, { - targetId: targetId?.trim() || undefined, - fullPage: Boolean(opts.fullPage), - }); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - defaultRuntime.log(`MEDIA:${result.path}`); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("eval") - .description("Run JavaScript in the active tab") - .argument("", "JavaScript expression") - .option("--target-id ", "CDP target id (or unique prefix)") - .option("--await", "Await promise result", false) - .action(async (js: string, opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const result = await browserEval(baseUrl, { - js, - targetId: opts.targetId?.trim() || undefined, - awaitPromise: Boolean(opts.await), - }); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - defaultRuntime.log(JSON.stringify(result.result, null, 2)); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("query") - .description("Query selector matches") - .argument("", "CSS selector") - .option("--target-id ", "CDP target id (or unique prefix)") - .option("--limit ", "Max matches (default: 20)", (v: string) => - Number(v), - ) - .action(async (selector: string, opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const result = await browserQuery(baseUrl, { - selector, - targetId: opts.targetId?.trim() || undefined, - limit: Number.isFinite(opts.limit) ? opts.limit : undefined, - }); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - defaultRuntime.log(JSON.stringify(result.matches, null, 2)); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("dom") - .description("Dump DOM (html or text) with truncation") - .option("--format ", "Output format (default: html)", "html") - .option("--target-id ", "CDP target id (or unique prefix)") - .option("--selector ", "Optional CSS selector to scope the dump") - .option( - "--max-chars ", - "Max characters (default: 200000)", - (v: string) => Number(v), - ) - .option("--out ", "Write output to a file") - .action(async (opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - const format = opts.format === "text" ? "text" : "html"; - try { - const result = await browserDom(baseUrl, { - format, - targetId: opts.targetId?.trim() || undefined, - maxChars: Number.isFinite(opts.maxChars) ? opts.maxChars : undefined, - selector: opts.selector?.trim() || undefined, - }); - if (opts.out) { - const fs = await import("node:fs/promises"); - await fs.writeFile(opts.out, result.text, "utf8"); - if (parent?.json) { - defaultRuntime.log( - JSON.stringify({ ok: true, out: opts.out }, null, 2), - ); - } else { - defaultRuntime.log(opts.out); - } - return; - } - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - defaultRuntime.log(result.text); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("snapshot") - .description("Capture an AI-friendly snapshot (aria, domSnapshot, or ai)") - .option( - "--format ", - "Snapshot format (default: aria)", - "aria", - ) - .option("--target-id ", "CDP target id (or unique prefix)") - .option("--limit ", "Max nodes (default: 500/800)", (v: string) => - Number(v), - ) - .option("--out ", "Write snapshot to a file") - .action(async (opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - const format = - opts.format === "domSnapshot" - ? "domSnapshot" - : opts.format === "ai" - ? "ai" - : "aria"; - try { - const result = await browserSnapshot(baseUrl, { - format, - targetId: opts.targetId?.trim() || undefined, - limit: Number.isFinite(opts.limit) ? opts.limit : undefined, - }); - - if (opts.out) { - const fs = await import("node:fs/promises"); - if (result.format === "ai") { - await fs.writeFile(opts.out, result.snapshot, "utf8"); - } else { - const payload = JSON.stringify(result, null, 2); - await fs.writeFile(opts.out, payload, "utf8"); - } - if (parent?.json) { - defaultRuntime.log( - JSON.stringify({ ok: true, out: opts.out }, null, 2), - ); - } else { - defaultRuntime.log(opts.out); - } - return; - } - - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - - if (result.format === "ai") { - defaultRuntime.log(result.snapshot); - return; - } - - if (result.format === "domSnapshot") { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - - const nodes = "nodes" in result ? result.nodes : []; - defaultRuntime.log( - nodes - .map((n) => { - const indent = " ".repeat(Math.min(20, n.depth)); - const name = n.name ? ` "${n.name}"` : ""; - const value = n.value ? ` = "${n.value}"` : ""; - return `${indent}- ${n.role}${name}${value}`; - }) - .join("\n"), - ); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("click") - .description("Click an element by ref from an ai snapshot (e.g. 76)") - .argument("", "Ref id from ai snapshot") - .option("--target-id ", "CDP target id (or unique prefix)") - .action(async (ref: string, opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - try { - const result = await browserClickRef(baseUrl, { - ref, - targetId: opts.targetId?.trim() || undefined, - }); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - defaultRuntime.log(`clicked ref ${ref} on ${result.url}`); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); - - browser - .command("tool") - .description("Call a Playwright MCP-style browser tool by name") - .argument("", "Tool name (browser_*)") - .option("--args ", "JSON arguments for the tool") - .option("--target-id ", "CDP target id (or unique prefix)") - .action(async (name: string, opts, cmd) => { - const parent = parentOpts(cmd); - const baseUrl = resolveBrowserControlUrl(parent?.url); - let args: Record = {}; - if (opts.args) { - try { - args = JSON.parse(String(opts.args)); - } catch (err) { - defaultRuntime.error( - danger(`Invalid JSON for --args: ${String(err)}`), - ); - defaultRuntime.exit(1); - } - } - try { - const result = await browserTool(baseUrl, { - name, - args, - targetId: opts.targetId?.trim() || undefined, - }); - if (parent?.json) { - defaultRuntime.log(JSON.stringify(result, null, 2)); - return; - } - defaultRuntime.log(JSON.stringify(result, null, 2)); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); + registerBrowserManageCommands(browser, parentOpts); + registerBrowserInspectCommands(browser, parentOpts); + registerBrowserActionInputCommands(browser, parentOpts); + registerBrowserActionObserveCommands(browser, parentOpts); }