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୕ୖ୪௫ఄ಄ದൡඅ櫬Āoungruent;扢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;怳Ādpuct;戏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ǣጓ\0y;䐎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Ā;tbrk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯᝳ;䎲;愶een;扬r;쀀𝔟gcostuvwឍឝឳេ៕៛ƀ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Ā;elƀ;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\0aĀ;t䀬;䁀ƀ;fl戁îᅠeĀmxent»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ↄⅪ←ٖ↛ǰ↉\0proø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;䅓Ācrir;榿;쀀𝔬ͯ\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\0m;櫳;櫽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);
}
]