@silbercue/chrome 0.2.0
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/LICENSE +21 -0
- package/README.md +229 -0
- package/build/cache/a11y-tree.d.ts +252 -0
- package/build/cache/a11y-tree.js +1956 -0
- package/build/cache/index.d.ts +8 -0
- package/build/cache/index.js +4 -0
- package/build/cache/selector-cache.d.ts +47 -0
- package/build/cache/selector-cache.js +119 -0
- package/build/cache/session-defaults.d.ts +27 -0
- package/build/cache/session-defaults.js +130 -0
- package/build/cache/tab-state-cache.d.ts +39 -0
- package/build/cache/tab-state-cache.js +171 -0
- package/build/cdp/cdp-client.d.ts +25 -0
- package/build/cdp/cdp-client.js +146 -0
- package/build/cdp/chrome-launcher.d.ts +85 -0
- package/build/cdp/chrome-launcher.js +502 -0
- package/build/cdp/console-collector.d.ts +53 -0
- package/build/cdp/console-collector.js +147 -0
- package/build/cdp/debug.d.ts +1 -0
- package/build/cdp/debug.js +6 -0
- package/build/cdp/dialog-handler.d.ts +54 -0
- package/build/cdp/dialog-handler.js +129 -0
- package/build/cdp/dom-watcher.d.ts +45 -0
- package/build/cdp/dom-watcher.js +195 -0
- package/build/cdp/emulation.d.ts +12 -0
- package/build/cdp/emulation.js +17 -0
- package/build/cdp/index.d.ts +11 -0
- package/build/cdp/index.js +6 -0
- package/build/cdp/network-collector.d.ts +77 -0
- package/build/cdp/network-collector.js +257 -0
- package/build/cdp/protocol.d.ts +20 -0
- package/build/cdp/protocol.js +1 -0
- package/build/cdp/session-manager.d.ts +62 -0
- package/build/cdp/session-manager.js +205 -0
- package/build/cdp/settle.d.ts +16 -0
- package/build/cdp/settle.js +71 -0
- package/build/cli/license-commands.d.ts +19 -0
- package/build/cli/license-commands.js +199 -0
- package/build/cli/top-level-commands.d.ts +49 -0
- package/build/cli/top-level-commands.js +222 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.js +1 -0
- package/build/hooks/pro-hooks.d.ts +126 -0
- package/build/hooks/pro-hooks.js +17 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +86 -0
- package/build/license/free-tier-config.d.ts +14 -0
- package/build/license/free-tier-config.js +18 -0
- package/build/license/index.d.ts +4 -0
- package/build/license/index.js +2 -0
- package/build/license/license-status.d.ts +15 -0
- package/build/license/license-status.js +9 -0
- package/build/overlay/session-overlay.d.ts +22 -0
- package/build/overlay/session-overlay.js +372 -0
- package/build/plan/index.d.ts +7 -0
- package/build/plan/index.js +4 -0
- package/build/plan/plan-conditions.d.ts +12 -0
- package/build/plan/plan-conditions.js +242 -0
- package/build/plan/plan-executor.d.ts +49 -0
- package/build/plan/plan-executor.js +259 -0
- package/build/plan/plan-state-store.d.ts +24 -0
- package/build/plan/plan-state-store.js +43 -0
- package/build/plan/plan-variables.d.ts +16 -0
- package/build/plan/plan-variables.js +71 -0
- package/build/registry.d.ts +124 -0
- package/build/registry.js +884 -0
- package/build/server.d.ts +1 -0
- package/build/server.js +245 -0
- package/build/tools/click.d.ts +34 -0
- package/build/tools/click.js +293 -0
- package/build/tools/configure-session.d.ts +15 -0
- package/build/tools/configure-session.js +45 -0
- package/build/tools/console-logs.d.ts +18 -0
- package/build/tools/console-logs.js +44 -0
- package/build/tools/dom-snapshot.d.ts +13 -0
- package/build/tools/dom-snapshot.js +259 -0
- package/build/tools/element-utils.d.ts +23 -0
- package/build/tools/element-utils.js +133 -0
- package/build/tools/error-utils.d.ts +8 -0
- package/build/tools/error-utils.js +27 -0
- package/build/tools/evaluate.d.ts +34 -0
- package/build/tools/evaluate.js +217 -0
- package/build/tools/file-upload.d.ts +20 -0
- package/build/tools/file-upload.js +174 -0
- package/build/tools/fill-form.d.ts +39 -0
- package/build/tools/fill-form.js +256 -0
- package/build/tools/handle-dialog.d.ts +15 -0
- package/build/tools/handle-dialog.js +48 -0
- package/build/tools/index.d.ts +35 -0
- package/build/tools/index.js +18 -0
- package/build/tools/navigate.d.ts +18 -0
- package/build/tools/navigate.js +111 -0
- package/build/tools/network-monitor.d.ts +18 -0
- package/build/tools/network-monitor.js +66 -0
- package/build/tools/observe.d.ts +44 -0
- package/build/tools/observe.js +339 -0
- package/build/tools/press-key.d.ts +33 -0
- package/build/tools/press-key.js +155 -0
- package/build/tools/read-page.d.ts +22 -0
- package/build/tools/read-page.js +100 -0
- package/build/tools/run-plan.d.ts +205 -0
- package/build/tools/run-plan.js +215 -0
- package/build/tools/screenshot.d.ts +16 -0
- package/build/tools/screenshot.js +283 -0
- package/build/tools/scroll.d.ts +28 -0
- package/build/tools/scroll.js +143 -0
- package/build/tools/switch-tab.d.ts +26 -0
- package/build/tools/switch-tab.js +355 -0
- package/build/tools/tab-status.d.ts +7 -0
- package/build/tools/tab-status.js +50 -0
- package/build/tools/type.d.ts +31 -0
- package/build/tools/type.js +247 -0
- package/build/tools/virtual-desk.d.ts +7 -0
- package/build/tools/virtual-desk.js +108 -0
- package/build/tools/visual-constants.d.ts +3 -0
- package/build/tools/visual-constants.js +10 -0
- package/build/tools/wait-for.d.ts +26 -0
- package/build/tools/wait-for.js +323 -0
- package/build/transport/index.d.ts +3 -0
- package/build/transport/index.js +2 -0
- package/build/transport/pipe-transport.d.ts +18 -0
- package/build/transport/pipe-transport.js +63 -0
- package/build/transport/transport.d.ts +8 -0
- package/build/transport/transport.js +1 -0
- package/build/transport/websocket-transport.d.ts +22 -0
- package/build/transport/websocket-transport.js +200 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveElement } from "./element-utils.js";
|
|
3
|
+
import { RefNotFoundError } from "../cache/a11y-tree.js";
|
|
4
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
5
|
+
// --- Schema ---
|
|
6
|
+
export const observeSchema = z.object({
|
|
7
|
+
selector: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("CSS selector or element ref (e.g. 'e5') of the element to observe"),
|
|
10
|
+
duration: z
|
|
11
|
+
.number()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Collect all changes for this many ms, then return them. Mutually exclusive with 'until'. Default: 5000"),
|
|
14
|
+
until: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("JS expression evaluated on each change — stops when it returns true. Variable 'el' is the observed element. Example: el.textContent === '8'"),
|
|
18
|
+
then_click: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("CSS selector to click immediately when 'until' condition is met (for timing-critical actions). Only used with 'until'."),
|
|
22
|
+
click_first: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("CSS selector to click AFTER the observer is set up but BEFORE collection starts. Use to trigger the changes you want to observe (e.g. 'Start Mutations' button)."),
|
|
26
|
+
collect: z
|
|
27
|
+
.enum(["text", "attributes", "all"])
|
|
28
|
+
.optional()
|
|
29
|
+
.default("text")
|
|
30
|
+
.describe("What to collect: 'text' for textContent changes, 'attributes' for attribute changes, 'all' for both (default: 'text')"),
|
|
31
|
+
interval: z
|
|
32
|
+
.number()
|
|
33
|
+
.optional()
|
|
34
|
+
.default(100)
|
|
35
|
+
.describe("Polling interval in ms for change detection fallback (default: 100)"),
|
|
36
|
+
timeout: z
|
|
37
|
+
.number()
|
|
38
|
+
.optional()
|
|
39
|
+
.default(10000)
|
|
40
|
+
.describe("Maximum observation time in ms (default: 10000, max: 25000)"),
|
|
41
|
+
});
|
|
42
|
+
// --- Max timeout to stay under CDP 30s limit ---
|
|
43
|
+
const MAX_TIMEOUT_MS = 25000;
|
|
44
|
+
// --- JS function builders ---
|
|
45
|
+
/**
|
|
46
|
+
* Build the observer function for "collect" mode.
|
|
47
|
+
* Runs for `duration` ms, collects all text/attribute changes.
|
|
48
|
+
*/
|
|
49
|
+
export function buildCollectFunction(duration, interval, collect, clickFirstSelector) {
|
|
50
|
+
const observerConfig = buildMutationObserverConfig(collect);
|
|
51
|
+
const clickFirstCode = clickFirstSelector
|
|
52
|
+
? `var cf = document.querySelector(${JSON.stringify(clickFirstSelector)}); if (cf) cf.click();`
|
|
53
|
+
: "";
|
|
54
|
+
return `function() {
|
|
55
|
+
var el = this;
|
|
56
|
+
var changes = [];
|
|
57
|
+
var lastText = el.textContent;
|
|
58
|
+
var lastAttrs = {};
|
|
59
|
+
var attrs = el.attributes;
|
|
60
|
+
for (var i = 0; i < attrs.length; i++) lastAttrs[attrs[i].name] = attrs[i].value;
|
|
61
|
+
|
|
62
|
+
function checkText() {
|
|
63
|
+
var text = el.textContent;
|
|
64
|
+
if (text !== lastText) {
|
|
65
|
+
changes.push({ type: "text", value: text });
|
|
66
|
+
lastText = text;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function checkAttrs() {
|
|
70
|
+
var a = el.attributes;
|
|
71
|
+
for (var i = 0; i < a.length; i++) {
|
|
72
|
+
if (lastAttrs[a[i].name] !== a[i].value) {
|
|
73
|
+
changes.push({ type: "attribute", name: a[i].name, value: a[i].value, old: lastAttrs[a[i].name] || null });
|
|
74
|
+
lastAttrs[a[i].name] = a[i].value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function check() {
|
|
79
|
+
${collect === "text" ? "checkText();" : collect === "attributes" ? "checkAttrs();" : "checkText(); checkAttrs();"}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return new Promise(function(resolve) {
|
|
83
|
+
var observer = new MutationObserver(check);
|
|
84
|
+
observer.observe(el, ${JSON.stringify(observerConfig)});
|
|
85
|
+
var poll = setInterval(check, ${interval});
|
|
86
|
+
${clickFirstCode}
|
|
87
|
+
setTimeout(function() {
|
|
88
|
+
observer.disconnect();
|
|
89
|
+
clearInterval(poll);
|
|
90
|
+
check();
|
|
91
|
+
resolve({ changes: changes, count: changes.length });
|
|
92
|
+
}, ${duration});
|
|
93
|
+
});
|
|
94
|
+
}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the observer function for "until" mode.
|
|
98
|
+
* Waits until a JS condition is met, optionally clicks a target element.
|
|
99
|
+
*/
|
|
100
|
+
export function buildUntilFunction(untilExpression, timeout, interval, collect, thenClickSelector, clickFirstSelector) {
|
|
101
|
+
const observerConfig = buildMutationObserverConfig(collect);
|
|
102
|
+
const clickCode = thenClickSelector
|
|
103
|
+
? `var clickTarget = document.querySelector(${JSON.stringify(thenClickSelector)});
|
|
104
|
+
if (clickTarget) clickTarget.click();`
|
|
105
|
+
: "";
|
|
106
|
+
const clickFirstCode = clickFirstSelector
|
|
107
|
+
? `var cf = document.querySelector(${JSON.stringify(clickFirstSelector)}); if (cf) cf.click();`
|
|
108
|
+
: "";
|
|
109
|
+
return `function() {
|
|
110
|
+
var el = this;
|
|
111
|
+
var changes = [];
|
|
112
|
+
var lastText = el.textContent;
|
|
113
|
+
var lastAttrs = {};
|
|
114
|
+
var attrs = el.attributes;
|
|
115
|
+
for (var i = 0; i < attrs.length; i++) lastAttrs[attrs[i].name] = attrs[i].value;
|
|
116
|
+
|
|
117
|
+
function checkText() {
|
|
118
|
+
var text = el.textContent;
|
|
119
|
+
if (text !== lastText) {
|
|
120
|
+
changes.push({ type: "text", value: text });
|
|
121
|
+
lastText = text;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function checkAttrs() {
|
|
125
|
+
var a = el.attributes;
|
|
126
|
+
for (var i = 0; i < a.length; i++) {
|
|
127
|
+
if (lastAttrs[a[i].name] !== a[i].value) {
|
|
128
|
+
changes.push({ type: "attribute", name: a[i].name, value: a[i].value, old: lastAttrs[a[i].name] || null });
|
|
129
|
+
lastAttrs[a[i].name] = a[i].value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function checkChanges() {
|
|
134
|
+
${collect === "text" ? "checkText();" : collect === "attributes" ? "checkAttrs();" : "checkText(); checkAttrs();"}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new Promise(function(resolve) {
|
|
138
|
+
var done = false;
|
|
139
|
+
function check() {
|
|
140
|
+
if (done) return;
|
|
141
|
+
checkChanges();
|
|
142
|
+
if (${untilExpression}) {
|
|
143
|
+
done = true;
|
|
144
|
+
observer.disconnect();
|
|
145
|
+
clearInterval(poll);
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
${clickCode}
|
|
148
|
+
resolve({ met: true, value: el.textContent, changes: changes, clicked: ${thenClickSelector ? "!!clickTarget" : "false"} });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var observer = new MutationObserver(check);
|
|
153
|
+
observer.observe(el, ${JSON.stringify(observerConfig)});
|
|
154
|
+
var poll = setInterval(check, ${interval});
|
|
155
|
+
${clickFirstCode}
|
|
156
|
+
check();
|
|
157
|
+
var timer = setTimeout(function() {
|
|
158
|
+
if (done) return;
|
|
159
|
+
done = true;
|
|
160
|
+
observer.disconnect();
|
|
161
|
+
clearInterval(poll);
|
|
162
|
+
resolve({ met: false, value: el.textContent, changes: changes, clicked: false });
|
|
163
|
+
}, ${timeout});
|
|
164
|
+
});
|
|
165
|
+
}`;
|
|
166
|
+
}
|
|
167
|
+
function buildMutationObserverConfig(collect) {
|
|
168
|
+
const config = { subtree: true };
|
|
169
|
+
if (collect === "text" || collect === "all") {
|
|
170
|
+
config.childList = true;
|
|
171
|
+
config.characterData = true;
|
|
172
|
+
}
|
|
173
|
+
if (collect === "attributes" || collect === "all") {
|
|
174
|
+
config.attributes = true;
|
|
175
|
+
}
|
|
176
|
+
return config;
|
|
177
|
+
}
|
|
178
|
+
function formatCollectResponse(result, elapsedMs) {
|
|
179
|
+
const textChanges = result.changes.filter((c) => c.type === "text").map((c) => c.value);
|
|
180
|
+
const attrChanges = result.changes.filter((c) => c.type === "attribute");
|
|
181
|
+
const lines = [];
|
|
182
|
+
if (textChanges.length > 0) {
|
|
183
|
+
lines.push(`Text changes (${textChanges.length}): ${textChanges.join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
if (attrChanges.length > 0) {
|
|
186
|
+
lines.push(`Attribute changes (${attrChanges.length}): ${attrChanges.map((c) => `${c.name}: ${c.old} → ${c.value}`).join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
if (lines.length === 0) {
|
|
189
|
+
lines.push("No changes detected during observation period.");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
193
|
+
_meta: {
|
|
194
|
+
elapsedMs,
|
|
195
|
+
method: "observe",
|
|
196
|
+
mode: "collect",
|
|
197
|
+
changeCount: result.count,
|
|
198
|
+
textChanges,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function formatUntilResponse(result, elapsedMs, thenClickSelector) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
if (result.met) {
|
|
205
|
+
lines.push(`Condition met after ${elapsedMs}ms — value: "${result.value}"`);
|
|
206
|
+
if (result.clicked && thenClickSelector) {
|
|
207
|
+
lines.push(`Clicked ${thenClickSelector}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
lines.push(`Timeout — condition not met. Current value: "${result.value}"`);
|
|
212
|
+
}
|
|
213
|
+
const textChanges = result.changes.filter((c) => c.type === "text").map((c) => c.value);
|
|
214
|
+
if (textChanges.length > 0) {
|
|
215
|
+
lines.push(`Changes observed (${textChanges.length}): ${textChanges.join(", ")}`);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
219
|
+
isError: !result.met ? true : undefined,
|
|
220
|
+
_meta: {
|
|
221
|
+
elapsedMs,
|
|
222
|
+
method: "observe",
|
|
223
|
+
mode: "until",
|
|
224
|
+
conditionMet: result.met,
|
|
225
|
+
clicked: result.clicked,
|
|
226
|
+
textChanges,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// --- Main handler ---
|
|
231
|
+
export async function observeHandler(params, cdpClient, sessionId, sessionManager) {
|
|
232
|
+
const start = performance.now();
|
|
233
|
+
// Validation: need either duration or until
|
|
234
|
+
if (params.duration === undefined && !params.until) {
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: "observe requires either 'duration' (collect mode) or 'until' (condition mode)",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
isError: true,
|
|
243
|
+
_meta: { elapsedMs: 0, method: "observe" },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (params.duration !== undefined && params.until) {
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: "observe: 'duration' and 'until' are mutually exclusive — use one or the other",
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
isError: true,
|
|
255
|
+
_meta: { elapsedMs: 0, method: "observe" },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (params.then_click && !params.until) {
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: "observe: 'then_click' requires 'until' — it clicks when the condition is met",
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
isError: true,
|
|
267
|
+
_meta: { elapsedMs: 0, method: "observe" },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// Cap timeout at MAX_TIMEOUT_MS to stay under CDP 30s limit
|
|
271
|
+
const effectiveTimeout = Math.min(params.duration ?? params.timeout, MAX_TIMEOUT_MS);
|
|
272
|
+
// Resolve the target element
|
|
273
|
+
let objectId;
|
|
274
|
+
let resolvedSessionId;
|
|
275
|
+
try {
|
|
276
|
+
const resolved = await resolveElement(cdpClient, sessionId, {
|
|
277
|
+
ref: /^e\d+$/.test(params.selector) ? params.selector : undefined,
|
|
278
|
+
selector: /^e\d+$/.test(params.selector) ? undefined : params.selector,
|
|
279
|
+
}, sessionManager);
|
|
280
|
+
objectId = resolved.objectId;
|
|
281
|
+
resolvedSessionId = resolved.resolvedSessionId;
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
285
|
+
if (err instanceof RefNotFoundError) {
|
|
286
|
+
return {
|
|
287
|
+
content: [{ type: "text", text: `observe: ${err.message}` }],
|
|
288
|
+
isError: true,
|
|
289
|
+
_meta: { elapsedMs, method: "observe" },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: "text", text: wrapCdpError(err, "observe") }],
|
|
294
|
+
isError: true,
|
|
295
|
+
_meta: { elapsedMs, method: "observe" },
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Build and execute the observer function
|
|
299
|
+
try {
|
|
300
|
+
let functionDeclaration;
|
|
301
|
+
if (params.until) {
|
|
302
|
+
functionDeclaration = buildUntilFunction(params.until, effectiveTimeout, params.interval, params.collect, params.then_click, params.click_first);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
functionDeclaration = buildCollectFunction(effectiveTimeout, params.interval, params.collect, params.click_first);
|
|
306
|
+
}
|
|
307
|
+
const cdpResult = await cdpClient.send("Runtime.callFunctionOn", {
|
|
308
|
+
objectId,
|
|
309
|
+
functionDeclaration,
|
|
310
|
+
returnByValue: true,
|
|
311
|
+
awaitPromise: true,
|
|
312
|
+
}, resolvedSessionId);
|
|
313
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
314
|
+
// Check for JS exception
|
|
315
|
+
if (cdpResult.exceptionDetails) {
|
|
316
|
+
const desc = cdpResult.exceptionDetails.exception?.description ??
|
|
317
|
+
cdpResult.exceptionDetails.text ??
|
|
318
|
+
"Unknown JS error";
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: "text", text: `observe JS error: ${desc}` }],
|
|
321
|
+
isError: true,
|
|
322
|
+
_meta: { elapsedMs, method: "observe" },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const result = cdpResult.result.value;
|
|
326
|
+
if (params.until) {
|
|
327
|
+
return formatUntilResponse(result, elapsedMs, params.then_click);
|
|
328
|
+
}
|
|
329
|
+
return formatCollectResponse(result, elapsedMs);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: wrapCdpError(err, "observe") }],
|
|
335
|
+
isError: true,
|
|
336
|
+
_meta: { elapsedMs, method: "observe" },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { SessionManager } from "../cdp/session-manager.js";
|
|
4
|
+
import type { ToolResponse } from "../types.js";
|
|
5
|
+
export declare const pressKeySchema: z.ZodObject<{
|
|
6
|
+
key: z.ZodString;
|
|
7
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
8
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
9
|
+
modifiers: z.ZodOptional<z.ZodArray<z.ZodEnum<["ctrl", "shift", "alt", "meta"]>, "many">>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
key: string;
|
|
12
|
+
ref?: string | undefined;
|
|
13
|
+
selector?: string | undefined;
|
|
14
|
+
modifiers?: ("shift" | "ctrl" | "alt" | "meta")[] | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
key: string;
|
|
17
|
+
ref?: string | undefined;
|
|
18
|
+
selector?: string | undefined;
|
|
19
|
+
modifiers?: ("shift" | "ctrl" | "alt" | "meta")[] | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type PressKeyParams = z.infer<typeof pressKeySchema>;
|
|
22
|
+
interface KeyDef {
|
|
23
|
+
code: string;
|
|
24
|
+
keyCode: number;
|
|
25
|
+
text?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Resolve a key string to its CDP key definition */
|
|
28
|
+
export declare function resolveKey(key: string): {
|
|
29
|
+
key: string;
|
|
30
|
+
def: KeyDef;
|
|
31
|
+
};
|
|
32
|
+
export declare function pressKeyHandler(params: PressKeyParams, cdpClient: CdpClient, sessionId?: string, sessionManager?: SessionManager): Promise<ToolResponse>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveElement, buildRefNotFoundError, RefNotFoundError } from "./element-utils.js";
|
|
3
|
+
// --- Schema ---
|
|
4
|
+
export const pressKeySchema = z.object({
|
|
5
|
+
key: z
|
|
6
|
+
.string()
|
|
7
|
+
.describe("Key to press — e.g. 'Enter', 'Escape', 'Tab', 'a', 'ArrowDown', 'F1'. For printable characters use the character itself."),
|
|
8
|
+
ref: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Element ref to focus before pressing key (e.g. 'e5')"),
|
|
12
|
+
selector: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("CSS selector to focus before pressing key (e.g. '#search-input')"),
|
|
16
|
+
modifiers: z
|
|
17
|
+
.array(z.enum(["ctrl", "shift", "alt", "meta"]))
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Modifier keys to hold during key press (e.g. ['ctrl', 'shift'] for Ctrl+Shift+key)"),
|
|
20
|
+
});
|
|
21
|
+
const SPECIAL_KEYS = {
|
|
22
|
+
Enter: { code: "Enter", keyCode: 13, text: "\r" },
|
|
23
|
+
Tab: { code: "Tab", keyCode: 9 },
|
|
24
|
+
Escape: { code: "Escape", keyCode: 27 },
|
|
25
|
+
Backspace: { code: "Backspace", keyCode: 8 },
|
|
26
|
+
Delete: { code: "Delete", keyCode: 46 },
|
|
27
|
+
Space: { code: "Space", keyCode: 32, text: " " },
|
|
28
|
+
" ": { code: "Space", keyCode: 32, text: " " },
|
|
29
|
+
ArrowUp: { code: "ArrowUp", keyCode: 38 },
|
|
30
|
+
ArrowDown: { code: "ArrowDown", keyCode: 40 },
|
|
31
|
+
ArrowLeft: { code: "ArrowLeft", keyCode: 37 },
|
|
32
|
+
ArrowRight: { code: "ArrowRight", keyCode: 39 },
|
|
33
|
+
Home: { code: "Home", keyCode: 36 },
|
|
34
|
+
End: { code: "End", keyCode: 35 },
|
|
35
|
+
PageUp: { code: "PageUp", keyCode: 33 },
|
|
36
|
+
PageDown: { code: "PageDown", keyCode: 34 },
|
|
37
|
+
F1: { code: "F1", keyCode: 112 },
|
|
38
|
+
F2: { code: "F2", keyCode: 113 },
|
|
39
|
+
F3: { code: "F3", keyCode: 114 },
|
|
40
|
+
F4: { code: "F4", keyCode: 115 },
|
|
41
|
+
F5: { code: "F5", keyCode: 116 },
|
|
42
|
+
F6: { code: "F6", keyCode: 117 },
|
|
43
|
+
F7: { code: "F7", keyCode: 118 },
|
|
44
|
+
F8: { code: "F8", keyCode: 119 },
|
|
45
|
+
F9: { code: "F9", keyCode: 120 },
|
|
46
|
+
F10: { code: "F10", keyCode: 121 },
|
|
47
|
+
F11: { code: "F11", keyCode: 122 },
|
|
48
|
+
F12: { code: "F12", keyCode: 123 },
|
|
49
|
+
};
|
|
50
|
+
const MODIFIER_BITS = {
|
|
51
|
+
alt: 1,
|
|
52
|
+
ctrl: 2,
|
|
53
|
+
meta: 4,
|
|
54
|
+
shift: 8,
|
|
55
|
+
};
|
|
56
|
+
/** Resolve a key string to its CDP key definition */
|
|
57
|
+
export function resolveKey(key) {
|
|
58
|
+
// Special key (Enter, Escape, etc.)
|
|
59
|
+
if (SPECIAL_KEYS[key]) {
|
|
60
|
+
return { key, def: SPECIAL_KEYS[key] };
|
|
61
|
+
}
|
|
62
|
+
// Single character
|
|
63
|
+
if (key.length === 1) {
|
|
64
|
+
const upper = key.toUpperCase();
|
|
65
|
+
const code = upper.charCodeAt(0);
|
|
66
|
+
// a-z / A-Z
|
|
67
|
+
if (code >= 65 && code <= 90) {
|
|
68
|
+
return {
|
|
69
|
+
key,
|
|
70
|
+
def: { code: `Key${upper}`, keyCode: code, text: key },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// 0-9
|
|
74
|
+
if (code >= 48 && code <= 57) {
|
|
75
|
+
return {
|
|
76
|
+
key,
|
|
77
|
+
def: { code: `Digit${key}`, keyCode: code, text: key },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Other printable characters
|
|
81
|
+
return {
|
|
82
|
+
key,
|
|
83
|
+
def: { code: "", keyCode: key.charCodeAt(0), text: key },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Unknown key — pass through as-is
|
|
87
|
+
return { key, def: { code: key, keyCode: 0 } };
|
|
88
|
+
}
|
|
89
|
+
// --- Handler ---
|
|
90
|
+
export async function pressKeyHandler(params, cdpClient, sessionId, sessionManager) {
|
|
91
|
+
const start = performance.now();
|
|
92
|
+
// Focus target element if ref or selector provided
|
|
93
|
+
let effectiveSessionId = sessionId;
|
|
94
|
+
if (params.ref || params.selector) {
|
|
95
|
+
try {
|
|
96
|
+
const target = params.ref ? { ref: params.ref } : { selector: params.selector };
|
|
97
|
+
const element = await resolveElement(cdpClient, sessionId, target, sessionManager);
|
|
98
|
+
effectiveSessionId = element.resolvedSessionId;
|
|
99
|
+
await cdpClient.send("Runtime.callFunctionOn", {
|
|
100
|
+
functionDeclaration: "function() { this.focus(); }",
|
|
101
|
+
objectId: element.objectId,
|
|
102
|
+
returnByValue: false,
|
|
103
|
+
}, element.resolvedSessionId);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err instanceof RefNotFoundError && params.ref) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: buildRefNotFoundError(params.ref) }],
|
|
109
|
+
isError: true,
|
|
110
|
+
_meta: { elapsedMs: Math.round(performance.now() - start), method: "press_key" },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const { key, def } = resolveKey(params.key);
|
|
117
|
+
const modBits = (params.modifiers ?? []).reduce((acc, m) => acc | MODIFIER_BITS[m], 0);
|
|
118
|
+
// Suppress text output when modifier keys are held (Ctrl+K should not type "k")
|
|
119
|
+
const hasModifier = modBits > 0;
|
|
120
|
+
const text = hasModifier ? undefined : def.text;
|
|
121
|
+
// keyDown
|
|
122
|
+
await cdpClient.send("Input.dispatchKeyEvent", {
|
|
123
|
+
type: text ? "keyDown" : "rawKeyDown",
|
|
124
|
+
modifiers: modBits,
|
|
125
|
+
key,
|
|
126
|
+
code: def.code,
|
|
127
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
128
|
+
...(text ? { text } : {}),
|
|
129
|
+
}, effectiveSessionId);
|
|
130
|
+
// char event for printable characters (without modifiers)
|
|
131
|
+
if (text) {
|
|
132
|
+
await cdpClient.send("Input.dispatchKeyEvent", {
|
|
133
|
+
type: "char",
|
|
134
|
+
modifiers: modBits,
|
|
135
|
+
key,
|
|
136
|
+
code: def.code,
|
|
137
|
+
text,
|
|
138
|
+
}, effectiveSessionId);
|
|
139
|
+
}
|
|
140
|
+
// keyUp
|
|
141
|
+
await cdpClient.send("Input.dispatchKeyEvent", {
|
|
142
|
+
type: "keyUp",
|
|
143
|
+
modifiers: modBits,
|
|
144
|
+
key,
|
|
145
|
+
code: def.code,
|
|
146
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
147
|
+
}, effectiveSessionId);
|
|
148
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
149
|
+
const modStr = params.modifiers?.length ? params.modifiers.join("+") + "+" : "";
|
|
150
|
+
const targetStr = params.ref ? ` on ${params.ref}` : params.selector ? ` on ${params.selector}` : "";
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: `Pressed ${modStr}${params.key}${targetStr}` }],
|
|
153
|
+
_meta: { elapsedMs, method: "press_key" },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { SessionManager } from "../cdp/session-manager.js";
|
|
4
|
+
import type { ToolResponse } from "../types.js";
|
|
5
|
+
export declare const readPageSchema: z.ZodObject<{
|
|
6
|
+
depth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
7
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
8
|
+
filter: z.ZodDefault<z.ZodOptional<z.ZodEnum<["interactive", "all", "landmark", "visual"]>>>;
|
|
9
|
+
max_tokens: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, number | undefined>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
filter: "interactive" | "all" | "landmark" | "visual";
|
|
12
|
+
depth: number;
|
|
13
|
+
ref?: string | undefined;
|
|
14
|
+
max_tokens?: number | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
filter?: "interactive" | "all" | "landmark" | "visual" | undefined;
|
|
17
|
+
depth?: number | undefined;
|
|
18
|
+
ref?: string | undefined;
|
|
19
|
+
max_tokens?: number | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type ReadPageParams = z.infer<typeof readPageSchema>;
|
|
22
|
+
export declare function readPageHandler(params: ReadPageParams, cdpClient: CdpClient, sessionId?: string, sessionManager?: SessionManager): Promise<ToolResponse>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { a11yTree, RefNotFoundError } from "../cache/a11y-tree.js";
|
|
3
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
4
|
+
export const readPageSchema = z.object({
|
|
5
|
+
depth: z.number().optional().default(3).describe("Nesting depth — how many tree levels to display (default: 3). Controls indentation, not visibility. Hidden sections (display: none) require clicking tabs/buttons to reveal."),
|
|
6
|
+
ref: z.string().optional().describe("Element ref (e.g. 'e5') to get subtree for"),
|
|
7
|
+
filter: z
|
|
8
|
+
.enum(["interactive", "all", "landmark", "visual"])
|
|
9
|
+
.optional()
|
|
10
|
+
.default("interactive")
|
|
11
|
+
.describe("Filter mode: interactive (default), all, landmark, or visual (adds bounds/click/visibility)"),
|
|
12
|
+
max_tokens: z.number().int().optional().transform(v => v !== undefined && v < 500 ? 500 : v).describe("Token budget — page content is automatically downsampled to fit. Omit for full output."),
|
|
13
|
+
});
|
|
14
|
+
export async function readPageHandler(params, cdpClient, sessionId, sessionManager) {
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
const method = "read_page";
|
|
17
|
+
try {
|
|
18
|
+
const result = await a11yTree.getTree(cdpClient, sessionId, {
|
|
19
|
+
depth: params.depth,
|
|
20
|
+
ref: params.ref,
|
|
21
|
+
filter: params.filter,
|
|
22
|
+
max_tokens: params.max_tokens,
|
|
23
|
+
fresh: true, // Story 13a.2 fix: always fetch fresh data — precomputed cache may be stale after SPA navigation
|
|
24
|
+
}, sessionManager);
|
|
25
|
+
let responseText = result.text;
|
|
26
|
+
// FR-016: Warn when a subtree request returns a single leaf node — likely stale ref
|
|
27
|
+
if (params.ref && result.refCount <= 1) {
|
|
28
|
+
const trimmed = result.text.trim();
|
|
29
|
+
const isLeaf = /^(\[e\d+\]\s+)?(StaticText|img|separator|none)\b/.test(trimmed) ||
|
|
30
|
+
trimmed.split("\n").length <= 2;
|
|
31
|
+
if (isLeaf) {
|
|
32
|
+
responseText += `\n\n⚠ This ref points to a single leaf node — the DOM may have changed since read_page was last called. Consider calling read_page without ref for a fresh view.`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// FR-03: Token metadata as structured footer — prevents LLM from needing extra calls
|
|
36
|
+
const metaParts = [`~${result.tokenCount} tokens`, `${result.refCount} refs`];
|
|
37
|
+
if (result.downsampled) {
|
|
38
|
+
metaParts.push(`downsampled from ~${result.originalTokens}`);
|
|
39
|
+
}
|
|
40
|
+
responseText += `\n\n[${metaParts.join(" | ")}]`;
|
|
41
|
+
// Truncation warning — when downsampled, hint that overlays may be hidden
|
|
42
|
+
if (result.downsampled && params.max_tokens) {
|
|
43
|
+
responseText += `\n⚠ Truncated to ~${params.max_tokens} tokens. Overlays/modals are prioritized but some elements may be hidden — retry without max_tokens or use screenshot to check for modals.`;
|
|
44
|
+
}
|
|
45
|
+
// FR-H6: Detect hidden interactive elements — hint when page has hidden sections
|
|
46
|
+
if (params.filter === "interactive" && result.refCount > 0) {
|
|
47
|
+
try {
|
|
48
|
+
const hiddenResult = await cdpClient.send("Runtime.evaluate", {
|
|
49
|
+
expression: `(() => { let h = 0; for (const el of document.querySelectorAll('button,a[href],input:not([type="hidden"]),select,textarea,[role="button"],[role="tab"],[role="link"]')) { if (el.offsetParent === null) { const p = getComputedStyle(el).position; if (p !== "fixed" && p !== "sticky") h++; } } return h; })()`,
|
|
50
|
+
returnByValue: true,
|
|
51
|
+
}, sessionId);
|
|
52
|
+
const hiddenCount = hiddenResult?.result?.value;
|
|
53
|
+
if (typeof hiddenCount === "number" && hiddenCount >= 5) {
|
|
54
|
+
responseText += `\n\nNote: ${hiddenCount} interactive elements are hidden (display: none). Click tabs/buttons to reveal hidden sections.`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort — ignore errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// FR-022: Hint that visible text content (table cells, codes, labels) is filtered out by 'interactive'.
|
|
62
|
+
// Prevents the LLM from reaching for evaluate/querySelector to read visible text.
|
|
63
|
+
if (params.filter === "interactive" && (result.hiddenContentCount ?? 0) >= 5) {
|
|
64
|
+
responseText += `\n\nNote: ${result.hiddenContentCount} text/content nodes (table cells, paragraphs, static text) are not shown by filter:"interactive". If you need to read visible text content, call read_page(ref: "eN", filter: "all") on the subtree — don't fall back to evaluate/querySelector.`;
|
|
65
|
+
}
|
|
66
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: responseText }],
|
|
69
|
+
_meta: {
|
|
70
|
+
elapsedMs,
|
|
71
|
+
method,
|
|
72
|
+
refCount: result.refCount,
|
|
73
|
+
depth: result.depth,
|
|
74
|
+
tokenCount: result.tokenCount,
|
|
75
|
+
pageUrl: result.pageUrl,
|
|
76
|
+
...(result.hasVisualData !== undefined ? { hasVisualData: result.hasVisualData } : {}),
|
|
77
|
+
...(result.downsampled ? {
|
|
78
|
+
downsampled: true,
|
|
79
|
+
originalTokens: result.originalTokens,
|
|
80
|
+
downsampleLevel: result.downsampleLevel,
|
|
81
|
+
} : {}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
87
|
+
if (err instanceof RefNotFoundError) {
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: err.message }],
|
|
90
|
+
isError: true,
|
|
91
|
+
_meta: { elapsedMs, method },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: wrapCdpError(err, "read_page") }],
|
|
96
|
+
isError: true,
|
|
97
|
+
_meta: { elapsedMs, method },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|