feat(browser): add native action commands
This commit is contained in:
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୕ୖ୪௫ఄ಄ದൡඅ櫬Āoungruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater;EFGLSTஶஷ扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠Āeiಫಹ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, """],
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 %} -->
|
||||
@@ -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`.
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./client-actions-core.js";
|
||||
export * from "./client-actions-observe.js";
|
||||
export * from "./client-actions-types.js";
|
||||
@@ -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
@@ -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");
|
||||
@@ -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, "");
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type BrowserParentOpts = { url?: string; json?: boolean };
|
||||
+15
-455
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user