feat(browser): add native action commands

This commit is contained in:
Peter Steinberger committed 2025-12-20 00:53:56 +00:00
1 parent d67bec0740
commit a526d3c1f2
26 files changed
+2589 -1234

No files matched your search

@@ -15,7 +15,7 @@ var __export = (all, symbols) => {
};
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/css-tag.js
//#region node_modules/@lit/reactive-element/css-tag.js
/**
* @license
* Copyright 2019 Google LLC
@@ -60,7 +60,7 @@ const r$1 = (t$7) => new n$9("string" == typeof t$7 ? t$7 : t$7 + "", void 0, s$
})(t$7) : t$7;
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/reactive-element.js
//#region node_modules/@lit/reactive-element/reactive-element.js
/**
* @license
* Copyright 2017 Google LLC
@@ -291,7 +291,7 @@ var y = class extends HTMLElement {
y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[d$2("elementProperties")] = new Map(), y[d$2("finalized")] = new Map(), p$2?.({ ReactiveElement: y }), (a$1.reactiveElementVersions ??= []).push("2.1.1");
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/lit-html.js
//#region node_modules/lit-html/lit-html.js
/**
* @license
* Copyright 2017 Google LLC
@@ -559,7 +559,7 @@ const B = (t$7, i$10, s$9) => {
};
//#endregion
//#region node_modules/.pnpm/lit-element@4.2.1/node_modules/lit-element/lit-element.js
//#region node_modules/lit-element/lit-element.js
/**
* @license
* Copyright 2017 Google LLC
@@ -599,7 +599,7 @@ const n$7 = {
(s$6.litElementVersions ??= []).push("4.2.1");
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/is-server.js
//#region node_modules/lit-html/is-server.js
/**
* @license
* Copyright 2022 Google LLC
@@ -608,7 +608,7 @@ const n$7 = {
const o$9 = !1;
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directive.js
//#region node_modules/lit-html/directive.js
/**
* @license
* Copyright 2017 Google LLC
@@ -642,7 +642,7 @@ var i$3 = class {
};
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directive-helpers.js
//#region node_modules/lit-html/directive-helpers.js
/**
* @license
* Copyright 2020 Google LLC
@@ -678,7 +678,7 @@ var i$3 = class {
};
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/repeat.js
//#region node_modules/lit-html/directives/repeat.js
/**
* @license
* Copyright 2017 Google LLC
@@ -739,7 +739,7 @@ const u$1 = (e$14, s$9, t$7) => {
});
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/context-request-event.js
//#region node_modules/@lit/context/lib/context-request-event.js
/**
* @license
* Copyright 2021 Google LLC
@@ -755,7 +755,7 @@ var s$2 = class extends Event {
};
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/create-context.js
//#region node_modules/@lit/context/lib/create-context.js
/**
* @license
* Copyright 2021 Google LLC
@@ -766,7 +766,7 @@ function n$3(n$11) {
}
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/controllers/context-consumer.js
//#region node_modules/@lit/context/lib/controllers/context-consumer.js
/**
* @license
* Copyright 2021 Google LLC
@@ -793,7 +793,7 @@ function n$3(n$11) {
};
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/value-notifier.js
//#region node_modules/@lit/context/lib/value-notifier.js
/**
* @license
* Copyright 2021 Google LLC
@@ -832,7 +832,7 @@ var s$4 = class {
};
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/controllers/context-provider.js
//#region node_modules/@lit/context/lib/controllers/context-provider.js
/**
* @license
* Copyright 2021 Google LLC
@@ -868,7 +868,7 @@ var i$2 = class extends s$4 {
};
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/context-root.js
//#region node_modules/@lit/context/lib/context-root.js
/**
* @license
* Copyright 2021 Google LLC
@@ -908,7 +908,7 @@ var i$2 = class extends s$4 {
};
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/decorators/provide.js
//#region node_modules/@lit/context/lib/decorators/provide.js
/**
* @license
* Copyright 2017 Google LLC
@@ -963,7 +963,7 @@ var i$2 = class extends s$4 {
}
//#endregion
//#region node_modules/.pnpm/@lit+context@1.1.6/node_modules/@lit/context/lib/decorators/consume.js
//#region node_modules/@lit/context/lib/decorators/consume.js
/**
* @license
* Copyright 2022 Google LLC
@@ -2260,7 +2260,7 @@ var A2uiMessageProcessor = class A2uiMessageProcessor {
};
//#endregion
//#region node_modules/.pnpm/signal-polyfill@0.2.2/node_modules/signal-polyfill/dist/index.js
//#region node_modules/signal-polyfill/dist/index.js
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
enumerable: true,
@@ -2818,7 +2818,7 @@ var Signal;
})(Signal || (Signal = {}));
//#endregion
//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/-private/util.ts.js
//#region node_modules/signal-utils/dist/-private/util.ts.js
/**
* equality check here is always false so that we can dirty the storage
* via setting to _anything_
@@ -2845,7 +2845,7 @@ function fnCacheFor(context) {
}
//#endregion
//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/array.ts.js
//#region node_modules/signal-utils/dist/array.ts.js
const ARRAY_GETTER_METHODS = new Set([
Symbol.iterator,
"concat",
@@ -2978,7 +2978,7 @@ function signalArray(x$1) {
}
//#endregion
//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/map.ts.js
//#region node_modules/signal-utils/dist/map.ts.js
var SignalMap = class {
collection = createStorage();
storages = new Map();
@@ -3056,7 +3056,7 @@ var SignalMap = class {
Object.setPrototypeOf(SignalMap.prototype, Map.prototype);
//#endregion
//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/object.ts.js
//#region node_modules/signal-utils/dist/object.ts.js
/**
* Implementation based of tracked-built-ins' TrackedObject
* https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js
@@ -3145,7 +3145,7 @@ function signalObject(obj) {
}
//#endregion
//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/set.ts.js
//#region node_modules/signal-utils/dist/set.ts.js
var SignalSet = class {
collection = createStorage();
storages = new Map();
@@ -3968,7 +3968,7 @@ const Data = {
const Schemas = { A2UIClientEventMessage: server_to_client_with_standard_catalog_default };
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/custom-element.js
//#region node_modules/@lit/reactive-element/decorators/custom-element.js
/**
* @license
* Copyright 2017 Google LLC
@@ -3981,7 +3981,7 @@ const t = (t$7) => (e$14, o$14) => {
};
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/property.js
//#region node_modules/@lit/reactive-element/decorators/property.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4024,7 +4024,7 @@ function n(t$7) {
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/state.js
//#region node_modules/@lit/reactive-element/decorators/state.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4038,7 +4038,7 @@ function n(t$7) {
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/event-options.js
//#region node_modules/@lit/reactive-element/decorators/event-options.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4052,7 +4052,7 @@ function t$2(t$7) {
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/base.js
//#region node_modules/@lit/reactive-element/decorators/base.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4061,7 +4061,7 @@ function t$2(t$7) {
const e$6 = (e$14, t$7, c$7) => (c$7.configurable = !0, c$7.enumerable = !0, Reflect.decorate && "object" != typeof t$7 && Object.defineProperty(e$14, t$7, c$7), c$7);
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query.js
//#region node_modules/@lit/reactive-element/decorators/query.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4093,7 +4093,7 @@ const e$6 = (e$14, t$7, c$7) => (c$7.configurable = !0, c$7.enumerable = !0, Ref
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-all.js
//#region node_modules/@lit/reactive-element/decorators/query-all.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4107,7 +4107,7 @@ function r$6(r$11) {
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-async.js
//#region node_modules/@lit/reactive-element/decorators/query-async.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4120,7 +4120,7 @@ function r$5(r$11) {
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-assigned-elements.js
//#region node_modules/@lit/reactive-element/decorators/query-assigned-elements.js
/**
* @license
* Copyright 2021 Google LLC
@@ -4136,7 +4136,7 @@ function r$5(r$11) {
}
//#endregion
//#region node_modules/.pnpm/@lit+reactive-element@2.1.1/node_modules/@lit/reactive-element/decorators/query-assigned-nodes.js
//#region node_modules/@lit/reactive-element/decorators/query-assigned-nodes.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4152,7 +4152,7 @@ function r$5(r$11) {
}
//#endregion
//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/lib/signal-watcher.js
//#region node_modules/@lit-labs/signals/lib/signal-watcher.js
/**
* @license
* Copyright 2023 Google LLC
@@ -4215,7 +4215,7 @@ function e$5(e$14) {
}
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/async-directive.js
//#region node_modules/lit-html/async-directive.js
/**
* @license
* Copyright 2017 Google LLC
@@ -4273,7 +4273,7 @@ var f = class extends i$3 {
};
//#endregion
//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/lib/watch.js
//#region node_modules/@lit-labs/signals/lib/watch.js
/**
* @license
* Copyright 2023 Google LLC
@@ -4318,7 +4318,7 @@ var f = class extends i$3 {
const o$4 = e$1(h$1);
//#endregion
//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/lib/html-tag.js
//#region node_modules/@lit-labs/signals/lib/html-tag.js
/**
* @license
* Copyright 2023 Google LLC
@@ -4326,7 +4326,7 @@ const o$4 = e$1(h$1);
*/ const m = (o$14) => (t$7, ...m$3) => o$14(t$7, ...m$3.map(((o$15) => o$15 instanceof Signal.State || o$15 instanceof Signal.Computed ? o$4(o$15) : o$15))), l = m(x), r$2 = m(b);
//#endregion
//#region node_modules/.pnpm/@lit-labs+signals@0.1.3/node_modules/@lit-labs/signals/index.js
//#region node_modules/@lit-labs/signals/index.js
/**
* @license
* Copyright 2023 Google LLC
@@ -4334,7 +4334,7 @@ const o$4 = e$1(h$1);
*/ const l$1 = Signal.State, o$5 = Signal.Computed, r$3 = (l$5, o$14) => new Signal.State(l$5, o$14), i$5 = (l$5, o$14) => new Signal.Computed(l$5, o$14);
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/map.js
//#region node_modules/lit-html/directives/map.js
/**
* @license
* Copyright 2021 Google LLC
@@ -4348,7 +4348,7 @@ function* o$3(o$14, f$4) {
}
//#endregion
//#region node_modules/.pnpm/signal-utils@0.21.1_signal-polyfill@0.2.2/node_modules/signal-utils/dist/subtle/microtask-effect.ts.js
//#region node_modules/signal-utils/dist/subtle/microtask-effect.ts.js
let pending = false;
let watcher = new Signal.subtle.Watcher(() => {
if (!pending) {
@@ -5084,7 +5084,7 @@ let Root = (() => {
})();
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/class-map.js
//#region node_modules/lit-html/directives/class-map.js
/**
* @license
* Copyright 2018 Google LLC
@@ -5113,7 +5113,7 @@ let Root = (() => {
});
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/style-map.js
//#region node_modules/lit-html/directives/style-map.js
/**
* @license
* Copyright 2018 Google LLC
@@ -7160,7 +7160,7 @@ let MultipleChoice = (() => {
})();
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/ref.js
//#region node_modules/lit-html/directives/ref.js
/**
* @license
* Copyright 2020 Google LLC
@@ -8585,7 +8585,7 @@ let TextField = (() => {
})();
//#endregion
//#region node_modules/.pnpm/lit-html@3.3.1/node_modules/lit-html/directives/unsafe-html.js
//#region node_modules/lit-html/directives/unsafe-html.js
/**
* @license
* Copyright 2017 Google LLC
@@ -8612,7 +8612,7 @@ e$2.directiveName = "unsafeHTML", e$2.resultType = 1;
const o$1 = e$1(e$2);
//#endregion
//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/decode.mjs
//#region node_modules/mdurl/lib/decode.mjs
const decodeCache = {};
function getDecodeCache(exclude) {
let cache = decodeCache[exclude];
@@ -8696,7 +8696,7 @@ decode$2.componentChars = "";
var decode_default = decode$2;
//#endregion
//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/encode.mjs
//#region node_modules/mdurl/lib/encode.mjs
const encodeCache = {};
function getEncodeCache(exclude) {
let cache = encodeCache[exclude];
@@ -8761,7 +8761,7 @@ encode$2.componentChars = "-_.!~*'()";
var encode_default = encode$2;
//#endregion
//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/format.mjs
//#region node_modules/mdurl/lib/format.mjs
function format(url) {
let result = "";
result += url.protocol || "";
@@ -8781,7 +8781,7 @@ function format(url) {
;
//#endregion
//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/lib/parse.mjs
//#region node_modules/mdurl/lib/parse.mjs
function Url() {
this.protocol = null;
this.slashes = null;
@@ -8990,7 +8990,7 @@ Url.prototype.parseHost = function(host) {
var parse_default = urlParse;
//#endregion
//#region node_modules/.pnpm/mdurl@2.0.0/node_modules/mdurl/index.mjs
//#region node_modules/mdurl/index.mjs
var mdurl_exports = /* @__PURE__ */ __export({
decode: () => decode_default,
encode: () => encode_default,
@@ -8999,31 +8999,31 @@ var mdurl_exports = /* @__PURE__ */ __export({
});
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/properties/Any/regex.mjs
//#region node_modules/uc.micro/properties/Any/regex.mjs
var regex_default = /[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/;
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/Cc/regex.mjs
//#region node_modules/uc.micro/categories/Cc/regex.mjs
var regex_default$1 = /[\0-\x1F\x7F-\x9F]/;
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/Cf/regex.mjs
//#region node_modules/uc.micro/categories/Cf/regex.mjs
var regex_default$4 = /[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/;
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/P/regex.mjs
//#region node_modules/uc.micro/categories/P/regex.mjs
var regex_default$3 = /[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/;
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/S/regex.mjs
//#region node_modules/uc.micro/categories/S/regex.mjs
var regex_default$5 = /[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/;
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/categories/Z/regex.mjs
//#region node_modules/uc.micro/categories/Z/regex.mjs
var regex_default$2 = /[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/;
//#endregion
//#region node_modules/.pnpm/uc.micro@2.1.0/node_modules/uc.micro/index.mjs
//#region node_modules/uc.micro/index.mjs
var uc_exports = /* @__PURE__ */ __export({
Any: () => regex_default,
Cc: () => regex_default$1,
@@ -9034,15 +9034,15 @@ var uc_exports = /* @__PURE__ */ __export({
});
//#endregion
//#region node_modules/.pnpm/entities@4.5.0/node_modules/entities/lib/esm/generated/decode-data-html.js
//#region node_modules/entities/lib/esm/generated/decode-data-html.js
var decode_data_html_default = new Uint16Array("ᵁ<Õıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms„‹•˜¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀𝔄rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀𝔸plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀𝒜ign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀𝔅pf;쀀𝔹eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀𝒞pĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀𝔇Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀𝔻ƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegraìȹoɴ͹\0\0ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀𝒟rok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀𝔈rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀𝔼silon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀𝔉lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀𝔽All;戀riertrf;愱cò׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀𝔊;拙pf;쀀𝔾eater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀𝒢;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀𝕀a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻Ï䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀𝔍pf;쀀𝕁ǣ߇\0ߌr;쀀𝒥rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀𝔎pf;쀀𝕂cr;쀀𝒦րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀𝔏Ā;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀𝕃erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀𝔐nusPlus;戓pf;쀀𝕄cò੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘ë૙eryThiî૙tedĀGL૸ଆreaterGreateòٳessLesóੈLine;䀊r;쀀𝔑ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹveLine truncated
//#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, "&quot;"],
@@ -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 /^<a[>\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 /^<a[>\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,
+1 -1
View File
@@ -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`.
+83 -77
View File
@@ -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
-13
View File
@@ -1,13 +0,0 @@
---
summary: "Redirect: /clawd.md → /clawd"
permalink: /clawd.md
---
<!-- {% raw %} -->
<script>
window.location.replace("{{ \"/clawd\" | relative_url }}");
</script>
If you’re not redirected automatically, go to
<a href="{{ \"/clawd\" | relative_url }}">/clawd</a>.
<!-- {% endraw %} -->
-11
View File
@@ -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`.
+287
View File
@@ -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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<Record<string, unknown>>; targetId?: string },
): Promise<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<BrowserActionTabResult> {
return await fetchBrowserJson<BrowserActionTabResult>(`${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<ScreenshotResult & { filename?: string }> {
return await fetchBrowserJson<ScreenshotResult & { filename?: string }>(
`${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,
},
);
}
+207
View File
@@ -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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionPathResult> {
return await fetchBrowserJson<BrowserActionPathResult>(
`${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<BrowserActionPathResult> {
return await fetchBrowserJson<BrowserActionPathResult>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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<BrowserActionOk> {
return await fetchBrowserJson<BrowserActionOk>(`${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,
},
);
}
+15
View File
@@ -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;
};
+3
View File
@@ -0,0 +1,3 @@
export * from "./client-actions-core.js";
export * from "./client-actions-observe.js";
export * from "./client-actions-types.js";
+67
View File
@@ -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<T>(
url: string,
init?: RequestInit & { timeoutMs?: number },
): Promise<T> {
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;
}
+33 -109
View File
@@ -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<T>(
url: string,
init?: RequestInit & { timeoutMs?: number },
): Promise<T> {
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<BrowserStatus> {
return await fetchJson<BrowserStatus>(`${baseUrl}/`, { timeoutMs: 1500 });
return await fetchBrowserJson<BrowserStatus>(`${baseUrl}/`, {
timeoutMs: 1500,
});
}
export async function browserStart(baseUrl: string): Promise<void> {
await fetchJson(`${baseUrl}/start`, { method: "POST", timeoutMs: 15000 });
await fetchBrowserJson(`${baseUrl}/start`, {
method: "POST",
timeoutMs: 15000,
});
}
export async function browserStop(baseUrl: string): Promise<void> {
await fetchJson(`${baseUrl}/stop`, { method: "POST", timeoutMs: 15000 });
await fetchBrowserJson(`${baseUrl}/stop`, {
method: "POST",
timeoutMs: 15000,
});
}
export async function browserTabs(baseUrl: string): Promise<BrowserTab[]> {
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<BrowserTab> {
return await fetchJson<BrowserTab>(`${baseUrl}/tabs/open`, {
return await fetchBrowserJson<BrowserTab>(`${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<void> {
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<void> {
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<ScreenshotResult>(`${baseUrl}/screenshot${suffix}`, {
timeoutMs: 20000,
});
return await fetchBrowserJson<ScreenshotResult>(
`${baseUrl}/screenshot${suffix}`,
{
timeoutMs: 20000,
},
);
}
export async function browserEval(
@@ -270,7 +209,7 @@ export async function browserEval(
awaitPromise?: boolean;
},
): Promise<EvalResult> {
return await fetchJson<EvalResult>(`${baseUrl}/eval`, {
return await fetchBrowserJson<EvalResult>(`${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<QueryResult>(`${baseUrl}/query?${q.toString()}`, {
timeoutMs: 15000,
});
return await fetchBrowserJson<QueryResult>(
`${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<DomResult>(`${baseUrl}/dom?${q.toString()}`, {
return await fetchBrowserJson<DomResult>(`${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<SnapshotResult>(
return await fetchBrowserJson<SnapshotResult>(
`${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<string, unknown>;
targetId?: string;
},
): Promise<BrowserToolResponse> {
return await fetchJson<BrowserToolResponse>(`${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.
@@ -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<typeof handleBrowserActionCore>[0]["action"],
args: Record<string, unknown> = {},
ctxOverride?: Partial<BrowserRouteContext>,
) {
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" });
});
});
@@ -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<string, unknown>;
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<boolean> {
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<Record<string, unknown>>)
: 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;
@@ -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<string, unknown> = {}) {
async function callAction(
action: Parameters<typeof handleBrowserActionExtra>[0]["action"],
args: Record<string, unknown> = {},
) {
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",
@@ -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<string, unknown>;
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<boolean> {
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");
+249
View File
@@ -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<string, unknown> {
const body = req.body as Record<string, unknown> | 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<typeof handleBrowserActionCore>[0]["action"],
args: Record<string, unknown>,
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<typeof handleBrowserActionExtra>[0]["action"],
args: Record<string, unknown>,
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, "");
});
}
+2 -2
View File
@@ -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);
}
-23
View File
@@ -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));
}
});
}
-65
View File
@@ -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<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return value as Record<string, unknown>;
}
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));
}
});
}
+492
View File
@@ -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<string> {
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<string> {
const fs = await import("node:fs/promises");
return await fs.readFile(path, "utf8");
}
async function readCode(opts: {
code?: string;
codeFile?: string;
codeStdin?: boolean;
}): Promise<string> {
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<Array<Record<string, unknown>>> {
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<Record<string, unknown>>;
}
export function registerBrowserActionInputCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
) {
browser
.command("navigate")
.description("Navigate the current tab to a URL")
.argument("<url>", "URL to navigate to")
.option("--target-id <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 <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("<width>", "Viewport width", (v: string) => Number(v))
.argument("<height>", "Viewport height", (v: string) => Number(v))
.option("--target-id <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>", "Ref id from ai snapshot")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--double", "Double click", false)
.option("--button <left|right|middle>", "Mouse button to use")
.option("--modifiers <list>", "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>", "Ref id from ai snapshot")
.argument("<text>", "Text to type")
.option("--submit", "Press Enter after typing", false)
.option("--slowly", "Type slowly (human-like)", false)
.option("--target-id <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>", "Key to press (e.g. Enter)")
.option("--target-id <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>", "Ref id from ai snapshot")
.option("--target-id <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("<startRef>", "Start ref id")
.argument("<endRef>", "End ref id")
.option("--target-id <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>", "Ref id from ai snapshot")
.argument("<values...>", "Option values to select")
.option("--target-id <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("<paths...>", "File paths to upload")
.option("--target-id <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>", "JSON array of field objects")
.option("--fields-file <path>", "Read JSON array from a file")
.option("--target-id <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 <text>", "Prompt response text")
.option("--target-id <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 <ms>", "Wait for N milliseconds", (v: string) => Number(v))
.option("--text <value>", "Wait for text to appear")
.option("--text-gone <value>", "Wait for text to disappear")
.option("--target-id <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 <code>", "Function source, e.g. (el) => el.textContent")
.option("--ref <id>", "ARIA ref from ai snapshot")
.option("--target-id <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 <code>", "Function source, e.g. (page) => page.title()")
.option("--code-file <path>", "Read function source from a file")
.option("--code-stdin", "Read function source from stdin", false)
.option("--target-id <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);
}
});
}
+379
View File
@@ -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 <level>", "Filter by level (error, warn, info)")
.option("--target-id <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 <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 <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 <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 <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 <role>", "ARIA role")
.option("--name <text>", "Accessible name")
.option("--target-id <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>", "Text to find")
.option("--target-id <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>", "Ref id from ai snapshot")
.argument("<items...>", "List items to verify")
.option("--target-id <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>", "Ref id from ai snapshot")
.option("--type <type>", "Input type (textbox, checkbox, slider, etc)")
.option("--value <value>", "Expected value")
.option("--target-id <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 <n>", "X coordinate", (v: string) => Number(v))
.option("--y <n>", "Y coordinate", (v: string) => Number(v))
.option("--target-id <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 <n>", "X coordinate", (v: string) => Number(v))
.option("--y <n>", "Y coordinate", (v: string) => Number(v))
.option("--button <left|right|middle>", "Mouse button")
.option("--target-id <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 <n>", "Start X", (v: string) => Number(v))
.option("--start-y <n>", "Start Y", (v: string) => Number(v))
.option("--end-x <n>", "End X", (v: string) => Number(v))
.option("--end-y <n>", "End Y", (v: string) => Number(v))
.option("--target-id <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>", "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);
}
});
}
+48
View File
@@ -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",
];
+273
View File
@@ -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<string> {
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<string> {
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:<path>)")
.argument("[targetId]", "CDP target id (or unique prefix)")
.option("--full-page", "Capture full scrollable page", false)
.option("--ref <ref>", "ARIA ref from ai snapshot")
.option("--element <selector>", "CSS selector for element screenshot")
.option("--type <png|jpeg>", "Output type (default: png)", "png")
.option("--filename <name>", "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 <path>", "Read JavaScript from a file")
.option("--js-stdin", "Read JavaScript from stdin", false)
.option("--target-id <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("<selector>", "CSS selector")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--limit <n>", "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 <html|text>", "Output format (default: html)", "html")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--selector <css>", "Optional CSS selector to scope the dump")
.option(
"--max-chars <n>",
"Max characters (default: 200000)",
(v: string) => Number(v),
)
.option("--out <path>", "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 <aria|domSnapshot|ai>",
"Snapshot format (default: aria)",
"aria",
)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--limit <n>", "Max nodes (default: 500/800)", (v: string) =>
Number(v),
)
.option("--out <path>", "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);
}
});
}
+183
View File
@@ -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>", "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("<targetId>", "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);
}
});
}
+1
View File
@@ -0,0 +1 @@
export type BrowserParentOpts = { url?: string; json?: boolean };
+15 -455
View File
@@ -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:<path>
clawdis browser screenshot <targetId> --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>", "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("<targetId>", "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("<targetId>", "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:<path>)")
.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("<js>", "JavaScript expression")
.option("--target-id <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("<selector>", "CSS selector")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--limit <n>", "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 <html|text>", "Output format (default: html)", "html")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--selector <css>", "Optional CSS selector to scope the dump")
.option(
"--max-chars <n>",
"Max characters (default: 200000)",
(v: string) => Number(v),
)
.option("--out <path>", "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 <aria|domSnapshot|ai>",
"Snapshot format (default: aria)",
"aria",
)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--limit <n>", "Max nodes (default: 500/800)", (v: string) =>
Number(v),
)
.option("--out <path>", "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>", "Ref id from ai snapshot")
.option("--target-id <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("<name>", "Tool name (browser_*)")
.option("--args <json>", "JSON arguments for the tool")
.option("--target-id <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<string, unknown> = {};
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);
}