@wenathlan/extension 1.1.30 → 1.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/dist/cli.js +23 -2
- package/dist/index.js +124 -15
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +11 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +7 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +7 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +44 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +330 -38
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +7 -1
- package/extension/dist/pagebridge.js +422 -7
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +36 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +75 -16
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +1 -0
- package/extension/manifest.json +7 -1
- package/package.json +1 -1
|
@@ -1,14 +1,339 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// policy.ts
|
|
4
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile"]);
|
|
5
|
+
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset"]);
|
|
6
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot"]);
|
|
7
|
+
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
8
|
+
function parseoptions(step) {
|
|
9
|
+
if (step.options === void 0) return {};
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = JSON.parse(step.options);
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error("Step options must be a JSON object.");
|
|
15
|
+
}
|
|
16
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// extension/pageactions.ts
|
|
21
|
+
function events(target) {
|
|
22
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
23
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
24
|
+
}
|
|
25
|
+
function modifiers(options) {
|
|
26
|
+
return Array.isArray(options.modifiers) ? options.modifiers.filter((item) => typeof item === "string") : [];
|
|
27
|
+
}
|
|
28
|
+
function keyevent(type, key, mods) {
|
|
29
|
+
const code = key.length === 1 ? `Key${key.toUpperCase()}` : key;
|
|
30
|
+
return new KeyboardEvent(type, { key, code, bubbles: true, cancelable: true, composed: true, ctrlKey: mods.includes("ctrl"), shiftKey: mods.includes("shift"), altKey: mods.includes("alt"), metaKey: mods.includes("meta") });
|
|
31
|
+
}
|
|
32
|
+
function fieldlike(target) {
|
|
33
|
+
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement ? target : null;
|
|
34
|
+
}
|
|
35
|
+
function stringify(value) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(JSON.stringify(value)) ?? null;
|
|
38
|
+
} catch {
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function runpageaction(step, target) {
|
|
43
|
+
const options = (() => {
|
|
44
|
+
try {
|
|
45
|
+
return parseoptions(step);
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
switch (step.kind) {
|
|
51
|
+
case "presskey": {
|
|
52
|
+
const receiver = target instanceof HTMLElement ? target : document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
|
|
53
|
+
const key = step.value ?? "";
|
|
54
|
+
const mods = modifiers(options);
|
|
55
|
+
receiver.dispatchEvent(keyevent("keydown", key, mods));
|
|
56
|
+
receiver.dispatchEvent(keyevent("keypress", key, mods));
|
|
57
|
+
receiver.dispatchEvent(keyevent("keyup", key, mods));
|
|
58
|
+
return { ok: true, summary: `Key ${key} delivered with ${mods.length} modifier${mods.length === 1 ? "" : "s"}.` };
|
|
59
|
+
}
|
|
60
|
+
case "clickdeep": {
|
|
61
|
+
if (!(target instanceof HTMLElement)) return { ok: false, summary: "Action target is no longer available." };
|
|
62
|
+
target.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true, composed: true }));
|
|
63
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
|
|
64
|
+
target.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, cancelable: true, composed: true }));
|
|
65
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
|
|
66
|
+
target.click();
|
|
67
|
+
return { ok: true, summary: "Full pointer click sequence delivered." };
|
|
68
|
+
}
|
|
69
|
+
case "rightclick": {
|
|
70
|
+
if (!(target instanceof HTMLElement)) return { ok: false, summary: "Action target is no longer available." };
|
|
71
|
+
const init = { bubbles: true, cancelable: true, button: 2, buttons: 2 };
|
|
72
|
+
target.dispatchEvent(new PointerEvent("pointerdown", { ...init, composed: true }));
|
|
73
|
+
target.dispatchEvent(new MouseEvent("mousedown", init));
|
|
74
|
+
target.dispatchEvent(new MouseEvent("contextmenu", init));
|
|
75
|
+
return { ok: true, summary: "Context menu events delivered." };
|
|
76
|
+
}
|
|
77
|
+
case "doubleclick": {
|
|
78
|
+
if (!(target instanceof HTMLElement)) return { ok: false, summary: "Action target is no longer available." };
|
|
79
|
+
target.click();
|
|
80
|
+
target.click();
|
|
81
|
+
target.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true, detail: 2 }));
|
|
82
|
+
return { ok: true, summary: "Double click sequence delivered." };
|
|
83
|
+
}
|
|
84
|
+
case "drag": {
|
|
85
|
+
if (!(target instanceof HTMLElement)) return { ok: false, summary: "Drag source is no longer available." };
|
|
86
|
+
const destination = document.querySelector(step.value ?? "");
|
|
87
|
+
if (!destination) return { ok: false, summary: "Drag destination is no longer available." };
|
|
88
|
+
const transfer = new DataTransfer();
|
|
89
|
+
if (typeof options.data === "string") transfer.setData("text/plain", options.data);
|
|
90
|
+
target.dispatchEvent(new DragEvent("dragstart", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
91
|
+
destination.dispatchEvent(new DragEvent("dragenter", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
92
|
+
destination.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
93
|
+
destination.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
94
|
+
target.dispatchEvent(new DragEvent("dragend", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
95
|
+
return { ok: true, summary: "Drag and drop sequence delivered." };
|
|
96
|
+
}
|
|
97
|
+
case "drop": {
|
|
98
|
+
if (!(target instanceof HTMLElement)) return { ok: false, summary: "Drop zone is no longer available." };
|
|
99
|
+
const transfer = new DataTransfer();
|
|
100
|
+
transfer.setData("text/plain", step.value ?? "");
|
|
101
|
+
target.dispatchEvent(new DragEvent("dragenter", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
102
|
+
target.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
103
|
+
target.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
104
|
+
return { ok: true, summary: "Drop payload delivered." };
|
|
105
|
+
}
|
|
106
|
+
case "upload": {
|
|
107
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "file") return { ok: false, summary: "Target is not a file input." };
|
|
108
|
+
const transfer = new DataTransfer();
|
|
109
|
+
transfer.items.add(new File([typeof options.content === "string" ? options.content : ""], step.value ?? "upload", { type: typeof options.type === "string" ? options.type : "text/plain" }));
|
|
110
|
+
target.files = transfer.files;
|
|
111
|
+
events(target);
|
|
112
|
+
return { ok: true, summary: `Uploaded ${step.value ?? "file"} into the reviewed input.` };
|
|
113
|
+
}
|
|
114
|
+
case "clear": {
|
|
115
|
+
const field = fieldlike(target);
|
|
116
|
+
if (!field) return { ok: false, summary: "Target cannot hold a value." };
|
|
117
|
+
field.value = "";
|
|
118
|
+
events(field);
|
|
119
|
+
return { ok: true, summary: "Field cleared." };
|
|
120
|
+
}
|
|
121
|
+
case "check":
|
|
122
|
+
case "uncheck":
|
|
123
|
+
case "toggle": {
|
|
124
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "checkbox" && target.type !== "radio") return { ok: false, summary: "Target is not a checkbox or radio control." };
|
|
125
|
+
if (step.kind === "uncheck" && target.type === "radio") return { ok: false, summary: "A radio control cannot be unchecked." };
|
|
126
|
+
target.checked = step.kind === "toggle" ? !target.checked : step.kind === "check";
|
|
127
|
+
events(target);
|
|
128
|
+
return { ok: true, summary: `Control is now ${target.checked ? "checked" : "unchecked"}.` };
|
|
129
|
+
}
|
|
130
|
+
case "submit": {
|
|
131
|
+
const form = target instanceof HTMLFormElement ? target : target instanceof HTMLElement ? target.closest("form") : null;
|
|
132
|
+
if (!form) return { ok: false, summary: "No form owns the reviewed target." };
|
|
133
|
+
try {
|
|
134
|
+
form.requestSubmit(target instanceof HTMLFormElement ? void 0 : target);
|
|
135
|
+
} catch {
|
|
136
|
+
form.submit();
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, summary: "Form submission requested." };
|
|
139
|
+
}
|
|
140
|
+
case "setattribute": {
|
|
141
|
+
if (!target) return { ok: false, summary: "Action target is no longer available." };
|
|
142
|
+
const name = typeof options.name === "string" ? options.name : "";
|
|
143
|
+
target.setAttribute(name, typeof options.value === "string" ? options.value : "");
|
|
144
|
+
return { ok: true, summary: `Attribute ${name} set.` };
|
|
145
|
+
}
|
|
146
|
+
case "removeattribute": {
|
|
147
|
+
if (!target) return { ok: false, summary: "Action target is no longer available." };
|
|
148
|
+
const name = step.value ?? "";
|
|
149
|
+
target.removeAttribute(name);
|
|
150
|
+
return { ok: true, summary: `Attribute ${name} removed.` };
|
|
151
|
+
}
|
|
152
|
+
case "writestorage": {
|
|
153
|
+
try {
|
|
154
|
+
localStorage.setItem(typeof options.key === "string" ? options.key : "", typeof options.value === "string" ? options.value : "");
|
|
155
|
+
return { ok: true, summary: `Local storage entry ${String(options.key)} written.` };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return { ok: false, summary: `Local storage refused the write: ${error instanceof Error ? error.message : String(error)}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
case "evaluate": {
|
|
161
|
+
try {
|
|
162
|
+
let outcome;
|
|
163
|
+
try {
|
|
164
|
+
outcome = new Function(`"use strict"; return (${step.value ?? "undefined"});`)();
|
|
165
|
+
} catch {
|
|
166
|
+
outcome = new Function(`"use strict"; ${step.value ?? ""}`)();
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, summary: `Reviewed expression returned ${outcome === void 0 ? "no value" : "a value"}.`, details: { result: stringify(outcome) } };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { ok: false, summary: `Reviewed expression failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
case "fullscreen": {
|
|
174
|
+
const element = target instanceof HTMLElement ? target : document.documentElement;
|
|
175
|
+
return document.fullscreenElement === element ? document.exitFullscreen().then(() => ({ ok: true, summary: "Fullscreen state cleared." })) : element.requestFullscreen().then(() => ({ ok: true, summary: "Fullscreen state entered." })).catch((error) => ({ ok: false, summary: `Fullscreen was refused: ${error instanceof Error ? error.message : String(error)}` }));
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return { ok: false, summary: "Unsupported page action." };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// extension/pagereads.ts
|
|
183
|
+
var highlightid = "devthinkactionhighlight";
|
|
184
|
+
function clearhighlight() {
|
|
185
|
+
document.getElementById(highlightid)?.remove();
|
|
186
|
+
}
|
|
187
|
+
function highlighttarget(target) {
|
|
188
|
+
clearhighlight();
|
|
189
|
+
const rect = target.getBoundingClientRect();
|
|
190
|
+
const overlay = document.createElement("div");
|
|
191
|
+
overlay.id = highlightid;
|
|
192
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
193
|
+
Object.assign(overlay.style, { position: "fixed", left: `${Math.max(0, rect.left - 3)}px`, top: `${Math.max(0, rect.top - 3)}px`, width: `${rect.width + 6}px`, height: `${rect.height + 6}px`, border: "3px solid #2f9e44", borderRadius: "6px", pointerEvents: "none", zIndex: "2147483647", boxSizing: "border-box" });
|
|
194
|
+
document.documentElement.append(overlay);
|
|
195
|
+
window.setTimeout(clearhighlight, 5e3);
|
|
196
|
+
return { ok: true, summary: "Target outlined for five seconds." };
|
|
197
|
+
}
|
|
198
|
+
function poll(predicate, description, timeout) {
|
|
199
|
+
return new Promise((resolve) => {
|
|
200
|
+
const started = Date.now();
|
|
201
|
+
const check = () => {
|
|
202
|
+
if (predicate()) {
|
|
203
|
+
resolve({ ok: true, summary: `${description} is now present on the page.` });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (timeout > 0 && Date.now() - started >= timeout) {
|
|
207
|
+
resolve({ ok: false, summary: `${description} did not appear within ${timeout} milliseconds.` });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
window.setTimeout(check, 100);
|
|
211
|
+
};
|
|
212
|
+
check();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function formstate() {
|
|
216
|
+
return [...document.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
217
|
+
type: element.getAttribute("type") ?? element.tagName.toLowerCase(),
|
|
218
|
+
name: element.getAttribute("name") ?? "",
|
|
219
|
+
value: element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ? element.value : "",
|
|
220
|
+
...element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio") ? { checked: element.checked } : {}
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
function runpageread(step, target) {
|
|
224
|
+
const options = (() => {
|
|
225
|
+
try {
|
|
226
|
+
return parseoptions(step);
|
|
227
|
+
} catch {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
switch (step.kind) {
|
|
232
|
+
case "highlight": {
|
|
233
|
+
if (!target) return { ok: false, summary: "Highlight target is no longer available." };
|
|
234
|
+
return highlighttarget(target);
|
|
235
|
+
}
|
|
236
|
+
case "readattribute": {
|
|
237
|
+
if (!target) return { ok: false, summary: "Read target is no longer available." };
|
|
238
|
+
const value = target.getAttribute(step.value ?? "");
|
|
239
|
+
return value === null ? { ok: false, summary: `Attribute ${step.value} is absent.` } : { ok: true, summary: `Attribute ${step.value} read.`, details: { value } };
|
|
240
|
+
}
|
|
241
|
+
case "readstyle": {
|
|
242
|
+
if (!target) return { ok: false, summary: "Read target is no longer available." };
|
|
243
|
+
const computed = getComputedStyle(target);
|
|
244
|
+
const styles = {};
|
|
245
|
+
for (let index = 0; index < computed.length; index += 1) {
|
|
246
|
+
const property = computed.item(index);
|
|
247
|
+
styles[property] = computed.getPropertyValue(property);
|
|
248
|
+
}
|
|
249
|
+
return { ok: true, summary: `Read ${computed.length} computed style properties.`, details: { styles } };
|
|
250
|
+
}
|
|
251
|
+
case "readgeometry": {
|
|
252
|
+
if (!target) return { ok: false, summary: "Read target is no longer available." };
|
|
253
|
+
const rect = target.getBoundingClientRect();
|
|
254
|
+
const geometry = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left };
|
|
255
|
+
return { ok: true, summary: "Target geometry read.", details: { geometry } };
|
|
256
|
+
}
|
|
257
|
+
case "readvalue": {
|
|
258
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) return { ok: false, summary: "Target does not hold a form value." };
|
|
259
|
+
return { ok: true, summary: "Form value read.", details: { value: target.value } };
|
|
260
|
+
}
|
|
261
|
+
case "readtext": {
|
|
262
|
+
if (!target) return { ok: false, summary: "Read target is no longer available." };
|
|
263
|
+
const text = target.textContent ?? "";
|
|
264
|
+
return { ok: true, summary: `Read ${text.length} characters of text.`, details: { text } };
|
|
265
|
+
}
|
|
266
|
+
case "readhtml": {
|
|
267
|
+
if (!target) return { ok: false, summary: "Read target is no longer available." };
|
|
268
|
+
return { ok: true, summary: "Target markup read.", details: { html: target.outerHTML } };
|
|
269
|
+
}
|
|
270
|
+
case "countelements": {
|
|
271
|
+
const count = document.querySelectorAll(step.target ?? "").length;
|
|
272
|
+
return { ok: true, summary: `Selector matches ${count} element${count === 1 ? "" : "s"}.`, details: { count } };
|
|
273
|
+
}
|
|
274
|
+
case "readtable": {
|
|
275
|
+
if (!(target instanceof HTMLTableElement)) return { ok: false, summary: "Read target is not a table element." };
|
|
276
|
+
const rows = [...target.querySelectorAll("tr")].map((row) => [...row.querySelectorAll("th, td")].map((cell) => cell.textContent?.trim() ?? ""));
|
|
277
|
+
const headers = rows[0] ?? [];
|
|
278
|
+
const body = rows.slice(1);
|
|
279
|
+
return { ok: true, summary: `Read table with ${headers.length} column${headers.length === 1 ? "" : "s"} and ${body.length} row${body.length === 1 ? "" : "s"}.`, details: { headers, rows: body } };
|
|
280
|
+
}
|
|
281
|
+
case "readlinks": {
|
|
282
|
+
const links = [...document.querySelectorAll("a[href]")].map((element) => ({ text: element.textContent?.trim() ?? "", href: element.getAttribute("href") ?? "" }));
|
|
283
|
+
return { ok: true, summary: `Read ${links.length} link${links.length === 1 ? "" : "s"}.`, details: { links } };
|
|
284
|
+
}
|
|
285
|
+
case "readimages": {
|
|
286
|
+
const images = [...document.querySelectorAll("img")].map((element) => ({ src: element.getAttribute("src") ?? "", alt: element.getAttribute("alt") ?? "" }));
|
|
287
|
+
return { ok: true, summary: `Read ${images.length} image${images.length === 1 ? "" : "s"}.`, details: { images } };
|
|
288
|
+
}
|
|
289
|
+
case "readmeta": {
|
|
290
|
+
const meta = [...document.querySelectorAll("meta")].map((element) => ({ name: element.getAttribute("name") ?? "", property: element.getAttribute("property") ?? "", content: element.getAttribute("content") ?? "" }));
|
|
291
|
+
return { ok: true, summary: `Read ${meta.length} meta entr${meta.length === 1 ? "y" : "ies"}.`, details: { meta } };
|
|
292
|
+
}
|
|
293
|
+
case "readforms": {
|
|
294
|
+
const forms = formstate();
|
|
295
|
+
return { ok: true, summary: `Read ${forms.length} form control${forms.length === 1 ? "" : "s"}.`, details: { forms } };
|
|
296
|
+
}
|
|
297
|
+
case "readstorage": {
|
|
298
|
+
try {
|
|
299
|
+
if (step.value) {
|
|
300
|
+
const value = localStorage.getItem(step.value);
|
|
301
|
+
return { ok: true, summary: `Read local storage entry ${step.value}.`, details: { value } };
|
|
302
|
+
}
|
|
303
|
+
const entries = {};
|
|
304
|
+
for (let index = 0; index < localStorage.length; index += 1) {
|
|
305
|
+
const key = localStorage.key(index);
|
|
306
|
+
if (key !== null) entries[key] = localStorage.getItem(key);
|
|
307
|
+
}
|
|
308
|
+
return { ok: true, summary: `Read ${Object.keys(entries).length} local storage entr${Object.keys(entries).length === 1 ? "y" : "ies"}.`, details: { entries } };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return { ok: false, summary: `Local storage refused the read: ${error instanceof Error ? error.message : String(error)}` };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
case "waitfor": {
|
|
314
|
+
const selector2 = step.target ?? "";
|
|
315
|
+
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
316
|
+
return poll(() => Boolean(document.querySelector(selector2)), `Selector ${selector2}`, timeout);
|
|
317
|
+
}
|
|
318
|
+
case "waittext": {
|
|
319
|
+
const text = step.value ?? "";
|
|
320
|
+
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
321
|
+
return poll(() => (document.body?.innerText ?? "").includes(text), `Text ${text}`, timeout);
|
|
322
|
+
}
|
|
323
|
+
default:
|
|
324
|
+
return { ok: false, summary: "Unsupported page read." };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
3
328
|
// extension/pagebridge.ts
|
|
4
|
-
function
|
|
5
|
-
return value.replace(/\s+/g, " ").trim()
|
|
329
|
+
function clean(value) {
|
|
330
|
+
return value.replace(/\s+/g, " ").trim();
|
|
6
331
|
}
|
|
7
332
|
function label(element) {
|
|
8
333
|
const aria = element.getAttribute("aria-label");
|
|
9
334
|
const labelledby = element.getAttribute("aria-labelledby");
|
|
10
335
|
const linked = labelledby ? document.getElementById(labelledby)?.textContent : "";
|
|
11
|
-
return
|
|
336
|
+
return clean(aria || linked || element.getAttribute("title") || element.textContent || "");
|
|
12
337
|
}
|
|
13
338
|
function selector(element) {
|
|
14
339
|
if (element.id) return `#${CSS.escape(element.id)}`;
|
|
@@ -22,6 +347,15 @@
|
|
|
22
347
|
const peers = [...parent.children].filter((node) => node.tagName === element.tagName);
|
|
23
348
|
return `${tag}:nth-of-type(${peers.indexOf(element) + 1})`;
|
|
24
349
|
}
|
|
350
|
+
function stepoptions(step) {
|
|
351
|
+
if (!step.options) return {};
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(step.options);
|
|
354
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
355
|
+
} catch {
|
|
356
|
+
return {};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
25
359
|
var previewid = "devthinktargetpreview";
|
|
26
360
|
function clearpreview() {
|
|
27
361
|
document.getElementById(previewid)?.remove();
|
|
@@ -42,23 +376,101 @@
|
|
|
42
376
|
return { ok: true, summary: `Previewing ${label(target) || target.tagName.toLowerCase()} for five seconds.` };
|
|
43
377
|
}
|
|
44
378
|
function capturesnapshot() {
|
|
45
|
-
const candidates = [...document.querySelectorAll("a[href], button, input, textarea, select, [role=button], [role=link]
|
|
379
|
+
const candidates = [...document.querySelectorAll("a[href], button, input, textarea, select, [role=button], [role=link], [role=combobox], [role=option], [role=checkbox], [role=radio], [role=switch], [role=tab], details, summary")];
|
|
46
380
|
const interactive = candidates.map((element) => ({ selector: selector(element), role: element.getAttribute("role") || element.tagName.toLowerCase(), label: label(element) })).filter((item) => item.label || item.role);
|
|
47
|
-
const forms = [...document.querySelectorAll("input, textarea, select")].
|
|
48
|
-
|
|
49
|
-
|
|
381
|
+
const forms = [...document.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
382
|
+
label: label(element),
|
|
383
|
+
type: element.getAttribute("type") || element.tagName.toLowerCase(),
|
|
384
|
+
name: element.getAttribute("name") || "",
|
|
385
|
+
...element instanceof HTMLSelectElement ? { options: [...element.options].map((option) => clean(option.textContent || option.value)) } : {}
|
|
386
|
+
}));
|
|
387
|
+
const text = clean(document.body?.innerText || "");
|
|
388
|
+
return { schemaversion: 2, url: location.href, title: clean(document.title), textpreview: text, textlength: document.body?.innerText.length ?? 0, forms, interactive, capturedat: Date.now() };
|
|
389
|
+
}
|
|
390
|
+
function extractcontent(targetselector) {
|
|
391
|
+
if (!targetselector) {
|
|
392
|
+
const links = [...document.querySelectorAll("a[href]")].map((element) => {
|
|
393
|
+
const href = element instanceof HTMLAnchorElement ? element.getAttribute("href") ?? "" : "";
|
|
394
|
+
return { text: element.textContent?.trim() ?? "", href };
|
|
395
|
+
});
|
|
396
|
+
return { ok: true, summary: `Extracted ${links.length} link entries.`, details: { links } };
|
|
397
|
+
}
|
|
398
|
+
const target = document.querySelector(targetselector);
|
|
399
|
+
if (!target) return { ok: false, summary: "Extraction target is no longer available." };
|
|
400
|
+
const text = target.textContent ?? "";
|
|
401
|
+
return { ok: true, summary: `Extracted ${text.length} characters of content.`, details: { text } };
|
|
402
|
+
}
|
|
403
|
+
function scrolltarget(target) {
|
|
404
|
+
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
|
|
405
|
+
return { ok: true, summary: `Scrolled ${label(target) || target.tagName.toLowerCase()} into view.` };
|
|
406
|
+
}
|
|
407
|
+
function hovertarget(target) {
|
|
408
|
+
for (const type of ["pointerover", "mouseover", "pointerenter"]) {
|
|
409
|
+
target.dispatchEvent(new PointerEvent(type, { bubbles: type !== "pointerenter", cancelable: true, composed: true }));
|
|
410
|
+
}
|
|
411
|
+
target.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false, cancelable: true }));
|
|
412
|
+
return { ok: true, summary: `Hover events delivered to ${label(target) || target.tagName.toLowerCase()}.` };
|
|
50
413
|
}
|
|
414
|
+
function selectoption(target, value) {
|
|
415
|
+
if (!(target instanceof HTMLSelectElement)) return { ok: false, summary: "Target is not a select element." };
|
|
416
|
+
const option = [...target.options].find((candidate) => candidate.value === value || candidate.textContent?.trim() === value);
|
|
417
|
+
if (!option) return { ok: false, summary: "Reviewed option is not part of the select element." };
|
|
418
|
+
target.value = option.value;
|
|
419
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
420
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
421
|
+
return { ok: true, summary: `Selected ${clean(option.textContent || option.value)}.` };
|
|
422
|
+
}
|
|
423
|
+
var readkinds = /* @__PURE__ */ new Set(["readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "waitfor", "waittext", "highlight"]);
|
|
424
|
+
var mutatingkinds = /* @__PURE__ */ new Set(["presskey", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "setattribute", "removeattribute", "writestorage", "evaluate", "fullscreen"]);
|
|
51
425
|
function performstep(step, expectedorigin) {
|
|
52
426
|
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
|
|
53
427
|
if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
|
|
428
|
+
if (step.kind === "wait") {
|
|
429
|
+
const requested = step.value ? Number.parseInt(step.value, 10) : 250;
|
|
430
|
+
const duration = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
431
|
+
return new Promise((resolve) => window.setTimeout(() => resolve({ ok: true, summary: `Reviewed wait of ${duration} milliseconds completed.` }), duration));
|
|
432
|
+
}
|
|
433
|
+
if (step.kind === "extract") return extractcontent(step.target);
|
|
54
434
|
if (step.kind === "navigate") {
|
|
55
435
|
if (!step.value || new URL(step.value).origin !== expectedorigin) return { ok: false, summary: "Navigation target is outside the approved origin." };
|
|
56
436
|
location.assign(step.value);
|
|
57
437
|
return { ok: true, summary: "Navigation request sent." };
|
|
58
438
|
}
|
|
439
|
+
if (step.kind === "reload") {
|
|
440
|
+
location.reload();
|
|
441
|
+
return { ok: true, summary: "Page reload requested." };
|
|
442
|
+
}
|
|
443
|
+
if (step.kind === "back") {
|
|
444
|
+
history.back();
|
|
445
|
+
return { ok: true, summary: "History back requested." };
|
|
446
|
+
}
|
|
447
|
+
if (step.kind === "forward") {
|
|
448
|
+
history.forward();
|
|
449
|
+
return { ok: true, summary: "History forward requested." };
|
|
450
|
+
}
|
|
451
|
+
if (step.kind === "scrollpage") {
|
|
452
|
+
const options = stepoptions(step);
|
|
453
|
+
window.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
|
|
454
|
+
return { ok: true, summary: "Window scrolled by the reviewed amounts." };
|
|
455
|
+
}
|
|
456
|
+
if (step.kind === "scrollend") {
|
|
457
|
+
window.scrollTo(0, document.documentElement.scrollHeight);
|
|
458
|
+
return { ok: true, summary: "Window scrolled to the page end." };
|
|
459
|
+
}
|
|
460
|
+
if (step.kind === "scrolltop") {
|
|
461
|
+
window.scrollTo(0, 0);
|
|
462
|
+
return { ok: true, summary: "Window scrolled to the page top." };
|
|
463
|
+
}
|
|
464
|
+
if (readkinds.has(step.kind)) return runpageread(step, step.target ? document.querySelector(step.target) : null);
|
|
465
|
+
if (mutatingkinds.has(step.kind)) return runpageaction(step, step.target ? document.querySelector(step.target) : null);
|
|
59
466
|
if (!step.target) return { ok: false, summary: "Action target is absent." };
|
|
60
467
|
const target = document.querySelector(step.target);
|
|
61
468
|
if (!(target instanceof HTMLElement)) return { ok: false, summary: "Action target is no longer available." };
|
|
469
|
+
if (step.kind === "scrollby") {
|
|
470
|
+
const options = stepoptions(step);
|
|
471
|
+
target.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
|
|
472
|
+
return { ok: true, summary: "Container scrolled by the reviewed amounts." };
|
|
473
|
+
}
|
|
62
474
|
if (step.kind === "focus") {
|
|
63
475
|
target.focus();
|
|
64
476
|
return { ok: true, summary: "Target focused." };
|
|
@@ -68,6 +480,9 @@
|
|
|
68
480
|
target.click();
|
|
69
481
|
return { ok: true, summary: "Reviewed click completed." };
|
|
70
482
|
}
|
|
483
|
+
if (step.kind === "scroll") return scrolltarget(target);
|
|
484
|
+
if (step.kind === "hover") return hovertarget(target);
|
|
485
|
+
if (step.kind === "select") return selectoption(target, step.value ?? "");
|
|
71
486
|
if (step.kind === "type") {
|
|
72
487
|
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return { ok: false, summary: "Target cannot receive text." };
|
|
73
488
|
if (typeof step.value !== "string") return { ok: false, summary: "Approved text is absent." };
|