agentic-browser 2.0.1 → 2.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/README.md +7 -3
- package/dist/cli/index.mjs +16 -25
- package/dist/index.mjs +2 -3
- package/dist/mcp/index.mjs +56 -15
- package/dist/{runtime-DUSoQl5x.mjs → runtime-CRkUsvhV.mjs} +1190 -326
- package/dist/{setup-COK-KCfv.mjs → setup-BP0YyerC.mjs} +1 -3
- package/package.json +8 -8
|
@@ -5,10 +5,9 @@ import net from "node:net";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import WebSocket, { WebSocketServer } from "ws";
|
|
8
|
-
import { URL as URL$1, fileURLToPath } from "node:url";
|
|
9
8
|
import crypto from "node:crypto";
|
|
9
|
+
import { URL as URL$1 } from "node:url";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
|
|
12
11
|
//#region src/session/chrome-launcher.ts
|
|
13
12
|
const CANDIDATES = [
|
|
14
13
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -22,33 +21,93 @@ function discoverChrome(explicitPath) {
|
|
|
22
21
|
if (!found) throw new Error("No supported Chrome installation found.");
|
|
23
22
|
return found;
|
|
24
23
|
}
|
|
25
|
-
|
|
26
|
-
//#endregion
|
|
27
|
-
//#region src/session/extension-loader.ts
|
|
28
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
function loadControlExtension() {
|
|
30
|
-
const packageRoot = path.resolve(__dirname, "..");
|
|
31
|
-
return {
|
|
32
|
-
extensionPath: path.resolve(packageRoot, "extension"),
|
|
33
|
-
loadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
24
|
//#endregion
|
|
38
25
|
//#region src/session/browser-controller.ts
|
|
39
26
|
var CdpConnection = class CdpConnection {
|
|
40
27
|
nextId = 0;
|
|
28
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
29
|
+
pendingEvents = /* @__PURE__ */ new Map();
|
|
30
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
31
|
+
closed = false;
|
|
41
32
|
constructor(ws) {
|
|
42
33
|
this.ws = ws;
|
|
34
|
+
this.ensureListening();
|
|
43
35
|
}
|
|
44
36
|
static async connect(targetWsUrl) {
|
|
45
37
|
const ws = new WebSocket(targetWsUrl);
|
|
46
38
|
await new Promise((resolve, reject) => {
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
const onOpen = () => {
|
|
40
|
+
ws.off("error", onError);
|
|
41
|
+
resolve();
|
|
42
|
+
};
|
|
43
|
+
const onError = (err) => {
|
|
44
|
+
ws.off("open", onOpen);
|
|
45
|
+
reject(err);
|
|
46
|
+
};
|
|
47
|
+
ws.once("open", onOpen);
|
|
48
|
+
ws.once("error", onError);
|
|
49
49
|
});
|
|
50
50
|
return new CdpConnection(ws);
|
|
51
51
|
}
|
|
52
|
+
ensureListening() {
|
|
53
|
+
this.ws.on("message", this.handleMessage);
|
|
54
|
+
this.ws.once("close", this.handleClose);
|
|
55
|
+
this.ws.on("error", this.handleError);
|
|
56
|
+
}
|
|
57
|
+
handleMessage = (raw) => {
|
|
58
|
+
let message;
|
|
59
|
+
try {
|
|
60
|
+
message = JSON.parse(raw.toString("utf8"));
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof message.id === "number") {
|
|
65
|
+
const pending = this.pendingRequests.get(message.id);
|
|
66
|
+
if (!pending) return;
|
|
67
|
+
this.pendingRequests.delete(message.id);
|
|
68
|
+
clearTimeout(pending.timeout);
|
|
69
|
+
if (message.error) {
|
|
70
|
+
pending.reject(new Error(message.error.message));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
pending.resolve(message.result ?? {});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!message.method) return;
|
|
77
|
+
const params = message.params ?? {};
|
|
78
|
+
const handlers = this.eventHandlers.get(message.method);
|
|
79
|
+
if (handlers) for (const handler of handlers) handler(params);
|
|
80
|
+
const waiters = this.pendingEvents.get(message.method);
|
|
81
|
+
if (!waiters?.length) return;
|
|
82
|
+
this.pendingEvents.delete(message.method);
|
|
83
|
+
for (const waiter of waiters) {
|
|
84
|
+
clearTimeout(waiter.timeout);
|
|
85
|
+
waiter.resolve(params);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
handleError = (err) => {
|
|
89
|
+
this.failAllPending(err);
|
|
90
|
+
};
|
|
91
|
+
handleClose = () => {
|
|
92
|
+
this.failAllPending(/* @__PURE__ */ new Error("CDP connection closed"));
|
|
93
|
+
};
|
|
94
|
+
failAllPending(error) {
|
|
95
|
+
if (this.closed) return;
|
|
96
|
+
this.closed = true;
|
|
97
|
+
this.ws.off("message", this.handleMessage);
|
|
98
|
+
this.ws.off("error", this.handleError);
|
|
99
|
+
for (const pending of this.pendingRequests.values()) {
|
|
100
|
+
clearTimeout(pending.timeout);
|
|
101
|
+
pending.reject(error);
|
|
102
|
+
}
|
|
103
|
+
this.pendingRequests.clear();
|
|
104
|
+
for (const waiters of this.pendingEvents.values()) for (const waiter of waiters) {
|
|
105
|
+
clearTimeout(waiter.timeout);
|
|
106
|
+
waiter.reject(error);
|
|
107
|
+
}
|
|
108
|
+
this.pendingEvents.clear();
|
|
109
|
+
this.eventHandlers.clear();
|
|
110
|
+
}
|
|
52
111
|
async send(method, params, timeoutMs = 15e3) {
|
|
53
112
|
if (this.ws.readyState !== WebSocket.OPEN) throw new Error(`WebSocket not open (state=${this.ws.readyState}), cannot send '${method}'`);
|
|
54
113
|
const id = ++this.nextId;
|
|
@@ -59,51 +118,55 @@ var CdpConnection = class CdpConnection {
|
|
|
59
118
|
};
|
|
60
119
|
return await new Promise((resolve, reject) => {
|
|
61
120
|
const timeout = setTimeout(() => {
|
|
62
|
-
this.
|
|
121
|
+
this.pendingRequests.delete(id);
|
|
63
122
|
reject(/* @__PURE__ */ new Error(`CDP call '${method}' timed out after ${timeoutMs}ms`));
|
|
64
123
|
}, timeoutMs);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (message.error) {
|
|
71
|
-
reject(new Error(message.error.message));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
resolve(message.result ?? {});
|
|
75
|
-
};
|
|
76
|
-
this.ws.on("message", onMessage);
|
|
124
|
+
this.pendingRequests.set(id, {
|
|
125
|
+
resolve: (value) => resolve(value),
|
|
126
|
+
reject,
|
|
127
|
+
timeout
|
|
128
|
+
});
|
|
77
129
|
this.ws.send(JSON.stringify(payload));
|
|
78
130
|
});
|
|
79
131
|
}
|
|
80
132
|
waitForEvent(method, timeoutMs = 5e3) {
|
|
81
133
|
return new Promise((resolve, reject) => {
|
|
82
134
|
const timeout = setTimeout(() => {
|
|
83
|
-
this.
|
|
135
|
+
const waiters = this.pendingEvents.get(method);
|
|
136
|
+
if (!waiters) {
|
|
137
|
+
reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const next = waiters.filter((waiter) => waiter.timeout !== timeout);
|
|
141
|
+
if (next.length > 0) this.pendingEvents.set(method, next);
|
|
142
|
+
else this.pendingEvents.delete(method);
|
|
84
143
|
reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
|
|
85
144
|
}, timeoutMs);
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.ws.on("message", onMessage);
|
|
145
|
+
const waiters = this.pendingEvents.get(method) ?? [];
|
|
146
|
+
waiters.push({
|
|
147
|
+
resolve: (value) => resolve(value),
|
|
148
|
+
reject,
|
|
149
|
+
timeout
|
|
150
|
+
});
|
|
151
|
+
this.pendingEvents.set(method, waiters);
|
|
94
152
|
});
|
|
95
153
|
}
|
|
96
154
|
onEvent(method, handler) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
155
|
+
const handlers = this.eventHandlers.get(method) ?? /* @__PURE__ */ new Set();
|
|
156
|
+
const wrapped = (params) => {
|
|
157
|
+
handler(params);
|
|
100
158
|
};
|
|
101
|
-
|
|
159
|
+
handlers.add(wrapped);
|
|
160
|
+
this.eventHandlers.set(method, handlers);
|
|
102
161
|
return () => {
|
|
103
|
-
this.
|
|
162
|
+
const current = this.eventHandlers.get(method);
|
|
163
|
+
if (!current) return;
|
|
164
|
+
current.delete(wrapped);
|
|
165
|
+
if (current.size === 0) this.eventHandlers.delete(method);
|
|
104
166
|
};
|
|
105
167
|
}
|
|
106
168
|
close() {
|
|
169
|
+
this.failAllPending(/* @__PURE__ */ new Error("CDP connection closed"));
|
|
107
170
|
this.ws.close();
|
|
108
171
|
}
|
|
109
172
|
};
|
|
@@ -201,6 +264,377 @@ function resolveDefaultProfileDir() {
|
|
|
201
264
|
if (platform === "win32") return path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data");
|
|
202
265
|
return path.join(home, ".config", "google-chrome");
|
|
203
266
|
}
|
|
267
|
+
const CONTENT_TRUNCATION_HINT = "Use a CSS selector to scope the content, or use mode=\"summary\" for a lower-token overview.";
|
|
268
|
+
const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
269
|
+
const LOCATOR_SEPARATOR = ' >>> ';
|
|
270
|
+
|
|
271
|
+
function splitLocator(locator) {
|
|
272
|
+
return String(locator ?? '')
|
|
273
|
+
.split(/\s*>>>\s*/g)
|
|
274
|
+
.map((part) => part.trim())
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getFrameElementForDocument(doc) {
|
|
279
|
+
try {
|
|
280
|
+
return doc && doc.defaultView && doc.defaultView.frameElement ? doc.defaultView.frameElement : null;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createComposedChain(node) {
|
|
287
|
+
const chain = [];
|
|
288
|
+
let current = node;
|
|
289
|
+
while (current) {
|
|
290
|
+
chain.push(current);
|
|
291
|
+
if (current.parentNode) {
|
|
292
|
+
current = current.parentNode;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (current.host) {
|
|
296
|
+
current = current.host;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (current.nodeType === Node.DOCUMENT_NODE) {
|
|
300
|
+
current = getFrameElementForDocument(current);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
current = null;
|
|
304
|
+
}
|
|
305
|
+
return chain;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isSameComposedTarget(left, right) {
|
|
309
|
+
if (!left || !right) return false;
|
|
310
|
+
const seen = new Set(createComposedChain(left));
|
|
311
|
+
for (const current of createComposedChain(right)) {
|
|
312
|
+
if (seen.has(current)) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isWithinComposedSubtree(root, candidate) {
|
|
320
|
+
if (!root || !candidate) return false;
|
|
321
|
+
for (const current of createComposedChain(candidate)) {
|
|
322
|
+
if (current === root) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resolveLocator(locator) {
|
|
330
|
+
const parts = splitLocator(locator);
|
|
331
|
+
if (parts.length === 0) return null;
|
|
332
|
+
|
|
333
|
+
let root = document;
|
|
334
|
+
let element = null;
|
|
335
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
336
|
+
element = root.querySelector(parts[index]);
|
|
337
|
+
if (!element) return null;
|
|
338
|
+
|
|
339
|
+
if (index === parts.length - 1) {
|
|
340
|
+
return element;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (element.tagName && element.tagName.toLowerCase() === 'iframe') {
|
|
344
|
+
let nextDocument = null;
|
|
345
|
+
try {
|
|
346
|
+
nextDocument = element.contentDocument;
|
|
347
|
+
} catch {
|
|
348
|
+
nextDocument = null;
|
|
349
|
+
}
|
|
350
|
+
if (!nextDocument) {
|
|
351
|
+
throw new Error('Unable to access iframe content for locator segment: ' + parts[index]);
|
|
352
|
+
}
|
|
353
|
+
root = nextDocument;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (element.shadowRoot) {
|
|
358
|
+
root = element.shadowRoot;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
throw new Error('Locator segment does not enter a shadow root or iframe: ' + parts[index]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return element;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolveWithFallbacks(selector, fallbacks) {
|
|
369
|
+
let lastError = null;
|
|
370
|
+
const locators = [];
|
|
371
|
+
if (selector) locators.push(selector);
|
|
372
|
+
if (Array.isArray(fallbacks)) {
|
|
373
|
+
for (const fallback of fallbacks) {
|
|
374
|
+
locators.push(fallback);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const locator of locators) {
|
|
379
|
+
try {
|
|
380
|
+
const element = resolveLocator(locator);
|
|
381
|
+
if (element) {
|
|
382
|
+
return { element, locator };
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
lastError = error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (lastError) {
|
|
390
|
+
throw lastError;
|
|
391
|
+
}
|
|
392
|
+
throw new Error('Selector not found');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ensureInView(element) {
|
|
396
|
+
if (element && typeof element.scrollIntoView === 'function') {
|
|
397
|
+
element.scrollIntoView({ block: 'center', inline: 'center' });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function toMainFramePoint(element) {
|
|
402
|
+
const rect = element.getBoundingClientRect();
|
|
403
|
+
let x = rect.left + rect.width / 2;
|
|
404
|
+
let y = rect.top + rect.height / 2;
|
|
405
|
+
let doc = element.ownerDocument;
|
|
406
|
+
|
|
407
|
+
while (doc) {
|
|
408
|
+
const frameElement = getFrameElementForDocument(doc);
|
|
409
|
+
if (!frameElement) {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const frameRect = frameElement.getBoundingClientRect();
|
|
413
|
+
x += frameRect.left;
|
|
414
|
+
y += frameRect.top;
|
|
415
|
+
doc = frameElement.ownerDocument;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { x, y, width: rect.width, height: rect.height };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getTopFrameElement(element) {
|
|
422
|
+
let topFrame = null;
|
|
423
|
+
let doc = element.ownerDocument;
|
|
424
|
+
while (doc) {
|
|
425
|
+
const frameElement = getFrameElementForDocument(doc);
|
|
426
|
+
if (!frameElement) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
topFrame = frameElement;
|
|
430
|
+
doc = frameElement.ownerDocument;
|
|
431
|
+
}
|
|
432
|
+
return topFrame;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function describeElement(element) {
|
|
436
|
+
if (!element || !element.tagName) return 'unknown';
|
|
437
|
+
const tag = element.tagName.toLowerCase();
|
|
438
|
+
const id = element.id ? '#' + element.id : '';
|
|
439
|
+
const className =
|
|
440
|
+
element.className && typeof element.className === 'string'
|
|
441
|
+
? '.' + element.className.trim().split(/\s+/).filter(Boolean).join('.')
|
|
442
|
+
: '';
|
|
443
|
+
return tag + id + className;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function deepActiveElement() {
|
|
447
|
+
let current = document.activeElement;
|
|
448
|
+
while (current) {
|
|
449
|
+
if (current.shadowRoot && current.shadowRoot.activeElement) {
|
|
450
|
+
current = current.shadowRoot.activeElement;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
454
|
+
try {
|
|
455
|
+
const frameActive = current.contentDocument && current.contentDocument.activeElement;
|
|
456
|
+
if (frameActive) {
|
|
457
|
+
current = frameActive;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
// ignore inaccessible frames
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
return current;
|
|
467
|
+
}
|
|
468
|
+
`;
|
|
469
|
+
function buildLocatorRuntimeExpression(inputName, inputValue, body) {
|
|
470
|
+
return `(async () => {
|
|
471
|
+
${LOCATOR_RUNTIME_HELPERS}
|
|
472
|
+
const ${inputName} = ${JSON.stringify(inputValue)};
|
|
473
|
+
${body}
|
|
474
|
+
})()`;
|
|
475
|
+
}
|
|
476
|
+
const SPECIAL_KEY_PAYLOADS = new Map([
|
|
477
|
+
["Enter", {
|
|
478
|
+
key: "Enter",
|
|
479
|
+
code: "Enter",
|
|
480
|
+
windowsVirtualKeyCode: 13,
|
|
481
|
+
nativeVirtualKeyCode: 13,
|
|
482
|
+
text: "\r",
|
|
483
|
+
unmodifiedText: "\r"
|
|
484
|
+
}],
|
|
485
|
+
["Tab", {
|
|
486
|
+
key: "Tab",
|
|
487
|
+
code: "Tab",
|
|
488
|
+
windowsVirtualKeyCode: 9,
|
|
489
|
+
nativeVirtualKeyCode: 9
|
|
490
|
+
}],
|
|
491
|
+
["Escape", {
|
|
492
|
+
key: "Escape",
|
|
493
|
+
code: "Escape",
|
|
494
|
+
windowsVirtualKeyCode: 27,
|
|
495
|
+
nativeVirtualKeyCode: 27
|
|
496
|
+
}],
|
|
497
|
+
["Backspace", {
|
|
498
|
+
key: "Backspace",
|
|
499
|
+
code: "Backspace",
|
|
500
|
+
windowsVirtualKeyCode: 8,
|
|
501
|
+
nativeVirtualKeyCode: 8
|
|
502
|
+
}],
|
|
503
|
+
["Delete", {
|
|
504
|
+
key: "Delete",
|
|
505
|
+
code: "Delete",
|
|
506
|
+
windowsVirtualKeyCode: 46,
|
|
507
|
+
nativeVirtualKeyCode: 46
|
|
508
|
+
}],
|
|
509
|
+
["Space", {
|
|
510
|
+
key: " ",
|
|
511
|
+
code: "Space",
|
|
512
|
+
windowsVirtualKeyCode: 32,
|
|
513
|
+
nativeVirtualKeyCode: 32,
|
|
514
|
+
text: " ",
|
|
515
|
+
unmodifiedText: " "
|
|
516
|
+
}],
|
|
517
|
+
[" ", {
|
|
518
|
+
key: " ",
|
|
519
|
+
code: "Space",
|
|
520
|
+
windowsVirtualKeyCode: 32,
|
|
521
|
+
nativeVirtualKeyCode: 32,
|
|
522
|
+
text: " ",
|
|
523
|
+
unmodifiedText: " "
|
|
524
|
+
}],
|
|
525
|
+
["ArrowUp", {
|
|
526
|
+
key: "ArrowUp",
|
|
527
|
+
code: "ArrowUp",
|
|
528
|
+
windowsVirtualKeyCode: 38,
|
|
529
|
+
nativeVirtualKeyCode: 38
|
|
530
|
+
}],
|
|
531
|
+
["ArrowDown", {
|
|
532
|
+
key: "ArrowDown",
|
|
533
|
+
code: "ArrowDown",
|
|
534
|
+
windowsVirtualKeyCode: 40,
|
|
535
|
+
nativeVirtualKeyCode: 40
|
|
536
|
+
}],
|
|
537
|
+
["ArrowLeft", {
|
|
538
|
+
key: "ArrowLeft",
|
|
539
|
+
code: "ArrowLeft",
|
|
540
|
+
windowsVirtualKeyCode: 37,
|
|
541
|
+
nativeVirtualKeyCode: 37
|
|
542
|
+
}],
|
|
543
|
+
["ArrowRight", {
|
|
544
|
+
key: "ArrowRight",
|
|
545
|
+
code: "ArrowRight",
|
|
546
|
+
windowsVirtualKeyCode: 39,
|
|
547
|
+
nativeVirtualKeyCode: 39
|
|
548
|
+
}],
|
|
549
|
+
["Home", {
|
|
550
|
+
key: "Home",
|
|
551
|
+
code: "Home",
|
|
552
|
+
windowsVirtualKeyCode: 36,
|
|
553
|
+
nativeVirtualKeyCode: 36
|
|
554
|
+
}],
|
|
555
|
+
["End", {
|
|
556
|
+
key: "End",
|
|
557
|
+
code: "End",
|
|
558
|
+
windowsVirtualKeyCode: 35,
|
|
559
|
+
nativeVirtualKeyCode: 35
|
|
560
|
+
}],
|
|
561
|
+
["PageUp", {
|
|
562
|
+
key: "PageUp",
|
|
563
|
+
code: "PageUp",
|
|
564
|
+
windowsVirtualKeyCode: 33,
|
|
565
|
+
nativeVirtualKeyCode: 33
|
|
566
|
+
}],
|
|
567
|
+
["PageDown", {
|
|
568
|
+
key: "PageDown",
|
|
569
|
+
code: "PageDown",
|
|
570
|
+
windowsVirtualKeyCode: 34,
|
|
571
|
+
nativeVirtualKeyCode: 34
|
|
572
|
+
}]
|
|
573
|
+
]);
|
|
574
|
+
function appendTruncationNotice(content, limit, originalLength) {
|
|
575
|
+
return content + `\n\n[Truncated - showing first ${limit} of ${originalLength} characters. ${CONTENT_TRUNCATION_HINT}]`;
|
|
576
|
+
}
|
|
577
|
+
function applyContentLimit(content, limit, originalLength = content.length) {
|
|
578
|
+
if (!limit || originalLength <= limit) return {
|
|
579
|
+
content,
|
|
580
|
+
truncated: false
|
|
581
|
+
};
|
|
582
|
+
return {
|
|
583
|
+
content: appendTruncationNotice(content.slice(0, limit), limit, originalLength),
|
|
584
|
+
truncated: true,
|
|
585
|
+
originalLength
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function normalizeRuntimeContentPayload(value, limit) {
|
|
589
|
+
if (typeof value === "string") {
|
|
590
|
+
const limited = applyContentLimit(value, limit);
|
|
591
|
+
return {
|
|
592
|
+
content: limited.content,
|
|
593
|
+
truncated: limited.truncated || void 0,
|
|
594
|
+
originalLength: limited.originalLength
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const content = value?.content ?? "";
|
|
598
|
+
if (value?.truncated) {
|
|
599
|
+
const originalLength = value.originalLength ?? content.length;
|
|
600
|
+
const effectiveLimit = limit && limit > 0 ? limit : content.length;
|
|
601
|
+
return {
|
|
602
|
+
content: content.includes("[Truncated - showing first") || effectiveLimit === 0 ? content : appendTruncationNotice(content, effectiveLimit, originalLength),
|
|
603
|
+
truncated: true,
|
|
604
|
+
originalLength
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const limited = applyContentLimit(content, limit, value?.originalLength ?? content.length);
|
|
608
|
+
return {
|
|
609
|
+
content: limited.content,
|
|
610
|
+
truncated: limited.truncated || void 0,
|
|
611
|
+
originalLength: limited.originalLength
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function buildKeyDispatchPayload(inputKey) {
|
|
615
|
+
const key = inputKey || "Enter";
|
|
616
|
+
const special = SPECIAL_KEY_PAYLOADS.get(key);
|
|
617
|
+
if (special) return { ...special };
|
|
618
|
+
if (key.length === 1) {
|
|
619
|
+
const upper = key.toUpperCase();
|
|
620
|
+
const code = key >= "a" && key <= "z" ? `Key${upper}` : key >= "A" && key <= "Z" ? `Key${key}` : key >= "0" && key <= "9" ? `Digit${key}` : "Unidentified";
|
|
621
|
+
const keyCode = upper.charCodeAt(0);
|
|
622
|
+
return {
|
|
623
|
+
key,
|
|
624
|
+
code,
|
|
625
|
+
windowsVirtualKeyCode: keyCode,
|
|
626
|
+
nativeVirtualKeyCode: keyCode,
|
|
627
|
+
text: key,
|
|
628
|
+
unmodifiedText: key
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
key,
|
|
633
|
+
code: key,
|
|
634
|
+
windowsVirtualKeyCode: 0,
|
|
635
|
+
nativeVirtualKeyCode: 0
|
|
636
|
+
};
|
|
637
|
+
}
|
|
204
638
|
var ChromeCdpBrowserController = class {
|
|
205
639
|
connections = /* @__PURE__ */ new Map();
|
|
206
640
|
cleanupInterval;
|
|
@@ -283,7 +717,6 @@ var ChromeCdpBrowserController = class {
|
|
|
283
717
|
async launch(sessionId, options) {
|
|
284
718
|
const { executablePath: explicitPath, userProfileDir, headless, userAgent } = options ?? {};
|
|
285
719
|
const executablePath = discoverChrome(explicitPath);
|
|
286
|
-
const extension = loadControlExtension();
|
|
287
720
|
let profileDir;
|
|
288
721
|
if (userProfileDir === "default") profileDir = resolveDefaultProfileDir();
|
|
289
722
|
else if (userProfileDir) profileDir = userProfileDir;
|
|
@@ -291,23 +724,7 @@ var ChromeCdpBrowserController = class {
|
|
|
291
724
|
fs.mkdirSync(profileDir, { recursive: true });
|
|
292
725
|
const lockFile = path.join(profileDir, "SingletonLock");
|
|
293
726
|
if (fs.existsSync(lockFile)) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
|
|
294
|
-
const launchAttempts = headless ? [{
|
|
295
|
-
withExtension: false,
|
|
296
|
-
headless: true
|
|
297
|
-
}] : [
|
|
298
|
-
{
|
|
299
|
-
withExtension: true,
|
|
300
|
-
headless: false
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
withExtension: false,
|
|
304
|
-
headless: false
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
withExtension: false,
|
|
308
|
-
headless: true
|
|
309
|
-
}
|
|
310
|
-
];
|
|
727
|
+
const launchAttempts = headless ? [{ headless: true }] : [{ headless: false }, { headless: true }];
|
|
311
728
|
let lastError;
|
|
312
729
|
for (const attempt of launchAttempts) {
|
|
313
730
|
const port = await getFreePort();
|
|
@@ -317,7 +734,6 @@ var ChromeCdpBrowserController = class {
|
|
|
317
734
|
"--no-first-run",
|
|
318
735
|
"--no-default-browser-check"
|
|
319
736
|
];
|
|
320
|
-
if (attempt.withExtension) args.push(`--disable-extensions-except=${extension.extensionPath}`, `--load-extension=${extension.extensionPath}`);
|
|
321
737
|
if (attempt.headless) args.push("--headless=new");
|
|
322
738
|
args.push("about:blank");
|
|
323
739
|
const child = spawn(executablePath, args, {
|
|
@@ -392,6 +808,151 @@ var ChromeCdpBrowserController = class {
|
|
|
392
808
|
return `${result}: ${dialog.message}`;
|
|
393
809
|
});
|
|
394
810
|
}
|
|
811
|
+
async preparePointerTarget(conn, payload) {
|
|
812
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
813
|
+
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
814
|
+
const el = resolved.element;
|
|
815
|
+
ensureInView(el);
|
|
816
|
+
|
|
817
|
+
const point = toMainFramePoint(el);
|
|
818
|
+
if (point.width === 0 && point.height === 0) {
|
|
819
|
+
throw new Error('Element has zero size - it may be hidden or not rendered');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const topEl = document.elementFromPoint(point.x, point.y);
|
|
823
|
+
const topFrame = getTopFrameElement(el);
|
|
824
|
+
if (topEl && !isSameComposedTarget(topEl, el) && (!topFrame || topEl !== topFrame)) {
|
|
825
|
+
throw new Error('Element is covered by another element: ' + describeElement(topEl));
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return { x: point.x, y: point.y };
|
|
829
|
+
`);
|
|
830
|
+
const point = (await conn.send("Runtime.evaluate", {
|
|
831
|
+
expression,
|
|
832
|
+
returnByValue: true,
|
|
833
|
+
awaitPromise: true
|
|
834
|
+
})).result.value;
|
|
835
|
+
if (!point || typeof point.x !== "number" || typeof point.y !== "number") throw new Error("Unable to resolve pointer target");
|
|
836
|
+
return {
|
|
837
|
+
x: point.x,
|
|
838
|
+
y: point.y
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async prepareTypeTarget(conn, payload) {
|
|
842
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
843
|
+
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
844
|
+
const el = resolved.element;
|
|
845
|
+
ensureInView(el);
|
|
846
|
+
|
|
847
|
+
const point = toMainFramePoint(el);
|
|
848
|
+
if (point.width === 0 && point.height === 0) {
|
|
849
|
+
throw new Error('Element has zero size - it may be hidden or not rendered');
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const tag = el.tagName.toLowerCase();
|
|
853
|
+
const inputType = tag === 'input' ? (el.type || 'text').toLowerCase() : '';
|
|
854
|
+
const editable =
|
|
855
|
+
el.isContentEditable ||
|
|
856
|
+
tag === 'textarea' ||
|
|
857
|
+
(tag === 'input' &&
|
|
858
|
+
!['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'hidden'].includes(inputType));
|
|
859
|
+
|
|
860
|
+
if (!editable) {
|
|
861
|
+
throw new Error('Element is not text-editable');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (typeof el.focus === 'function') {
|
|
865
|
+
el.focus();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if ((payload.text ?? '') === '') {
|
|
869
|
+
if (el.isContentEditable) {
|
|
870
|
+
el.textContent = '';
|
|
871
|
+
} else if ('value' in el) {
|
|
872
|
+
el.value = '';
|
|
873
|
+
}
|
|
874
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
875
|
+
return 'cleared';
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (el.isContentEditable) {
|
|
879
|
+
const selection = el.ownerDocument.getSelection ? el.ownerDocument.getSelection() : null;
|
|
880
|
+
if (selection) {
|
|
881
|
+
const range = el.ownerDocument.createRange();
|
|
882
|
+
range.selectNodeContents(el);
|
|
883
|
+
selection.removeAllRanges();
|
|
884
|
+
selection.addRange(range);
|
|
885
|
+
}
|
|
886
|
+
} else if (typeof el.select === 'function') {
|
|
887
|
+
el.select();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return 'ready';
|
|
891
|
+
`);
|
|
892
|
+
return (await conn.send("Runtime.evaluate", {
|
|
893
|
+
expression,
|
|
894
|
+
returnByValue: true,
|
|
895
|
+
awaitPromise: true
|
|
896
|
+
})).result.value ?? "ready";
|
|
897
|
+
}
|
|
898
|
+
async dispatchMouseClick(conn, x, y) {
|
|
899
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
900
|
+
type: "mouseMoved",
|
|
901
|
+
x,
|
|
902
|
+
y,
|
|
903
|
+
button: "none"
|
|
904
|
+
});
|
|
905
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
906
|
+
type: "mousePressed",
|
|
907
|
+
x,
|
|
908
|
+
y,
|
|
909
|
+
button: "left",
|
|
910
|
+
clickCount: 1
|
|
911
|
+
});
|
|
912
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
913
|
+
type: "mouseReleased",
|
|
914
|
+
x,
|
|
915
|
+
y,
|
|
916
|
+
button: "left",
|
|
917
|
+
clickCount: 1
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
async focusTarget(conn, payload) {
|
|
921
|
+
if (!payload.selector) return;
|
|
922
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
923
|
+
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
924
|
+
const el = resolved.element;
|
|
925
|
+
ensureInView(el);
|
|
926
|
+
if (typeof el.focus === 'function') {
|
|
927
|
+
el.focus();
|
|
928
|
+
}
|
|
929
|
+
return true;
|
|
930
|
+
`);
|
|
931
|
+
await conn.send("Runtime.evaluate", {
|
|
932
|
+
expression,
|
|
933
|
+
returnByValue: true,
|
|
934
|
+
awaitPromise: true
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async dispatchKeyPress(conn, keyInput) {
|
|
938
|
+
const payload = buildKeyDispatchPayload(keyInput ?? "Enter");
|
|
939
|
+
await conn.send("Input.dispatchKeyEvent", {
|
|
940
|
+
type: payload.text ? "keyDown" : "rawKeyDown",
|
|
941
|
+
key: payload.key,
|
|
942
|
+
code: payload.code,
|
|
943
|
+
windowsVirtualKeyCode: payload.windowsVirtualKeyCode,
|
|
944
|
+
nativeVirtualKeyCode: payload.nativeVirtualKeyCode,
|
|
945
|
+
text: payload.text,
|
|
946
|
+
unmodifiedText: payload.unmodifiedText
|
|
947
|
+
});
|
|
948
|
+
await conn.send("Input.dispatchKeyEvent", {
|
|
949
|
+
type: "keyUp",
|
|
950
|
+
key: payload.key,
|
|
951
|
+
code: payload.code,
|
|
952
|
+
windowsVirtualKeyCode: payload.windowsVirtualKeyCode,
|
|
953
|
+
nativeVirtualKeyCode: payload.nativeVirtualKeyCode
|
|
954
|
+
});
|
|
955
|
+
}
|
|
395
956
|
async interact(targetWsUrl, payload) {
|
|
396
957
|
if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
397
958
|
await this.ensureEnabled(targetWsUrl);
|
|
@@ -430,59 +991,43 @@ var ChromeCdpBrowserController = class {
|
|
|
430
991
|
return "page refreshed";
|
|
431
992
|
});
|
|
432
993
|
if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
if (payload.action === 'type') {
|
|
467
|
-
const el = resolveElement(payload.selector, payload.fallbackSelectors);
|
|
468
|
-
el.focus();
|
|
469
|
-
el.value = payload.text ?? '';
|
|
470
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
471
|
-
return 'typed';
|
|
472
|
-
}
|
|
473
|
-
if (payload.action === 'press') {
|
|
474
|
-
const target = document.activeElement;
|
|
475
|
-
if (!target) throw new Error('No active element to press key on');
|
|
476
|
-
const key = payload.key ?? 'Enter';
|
|
477
|
-
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
478
|
-
target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
|
|
479
|
-
return 'pressed';
|
|
480
|
-
}
|
|
994
|
+
if (payload.action === "click" || payload.action === "hover" || payload.action === "type" || payload.action === "press") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
995
|
+
await this.ensureEnabled(targetWsUrl);
|
|
996
|
+
if (payload.action === "click") {
|
|
997
|
+
const point = await this.preparePointerTarget(conn, payload);
|
|
998
|
+
await this.dispatchMouseClick(conn, point.x, point.y);
|
|
999
|
+
try {
|
|
1000
|
+
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
1001
|
+
try {
|
|
1002
|
+
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
1003
|
+
} catch {}
|
|
1004
|
+
} catch {}
|
|
1005
|
+
return "clicked";
|
|
1006
|
+
}
|
|
1007
|
+
if (payload.action === "hover") {
|
|
1008
|
+
const point = await this.preparePointerTarget(conn, payload);
|
|
1009
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
1010
|
+
type: "mouseMoved",
|
|
1011
|
+
x: point.x,
|
|
1012
|
+
y: point.y,
|
|
1013
|
+
button: "none"
|
|
1014
|
+
});
|
|
1015
|
+
return "hovered";
|
|
1016
|
+
}
|
|
1017
|
+
if (payload.action === "press") {
|
|
1018
|
+
await this.focusTarget(conn, payload);
|
|
1019
|
+
await this.dispatchKeyPress(conn, payload.key);
|
|
1020
|
+
return "pressed";
|
|
1021
|
+
}
|
|
1022
|
+
if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
|
|
1023
|
+
return "typed";
|
|
1024
|
+
});
|
|
1025
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
481
1026
|
if (payload.action === 'waitFor') {
|
|
482
1027
|
const timeout = payload.timeoutMs ?? 2000;
|
|
483
1028
|
const started = Date.now();
|
|
484
1029
|
while (Date.now() - started < timeout) {
|
|
485
|
-
if (
|
|
1030
|
+
if (resolveLocator(payload.selector)) {
|
|
486
1031
|
return 'found';
|
|
487
1032
|
}
|
|
488
1033
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -499,25 +1044,15 @@ var ChromeCdpBrowserController = class {
|
|
|
499
1044
|
}
|
|
500
1045
|
if (payload.action === 'scroll') {
|
|
501
1046
|
if (payload.selector) {
|
|
502
|
-
const el =
|
|
503
|
-
el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0
|
|
1047
|
+
const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
|
|
1048
|
+
el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0 });
|
|
504
1049
|
return 'scrolled element';
|
|
505
1050
|
}
|
|
506
|
-
window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0
|
|
1051
|
+
window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0 });
|
|
507
1052
|
return 'scrolled page';
|
|
508
1053
|
}
|
|
509
|
-
if (payload.action === 'hover') {
|
|
510
|
-
const el = resolveElement(payload.selector, payload.fallbackSelectors);
|
|
511
|
-
const rect = el.getBoundingClientRect();
|
|
512
|
-
const cx = rect.left + rect.width / 2;
|
|
513
|
-
const cy = rect.top + rect.height / 2;
|
|
514
|
-
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, clientX: cx, clientY: cy }));
|
|
515
|
-
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: cx, clientY: cy }));
|
|
516
|
-
el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: cx, clientY: cy }));
|
|
517
|
-
return 'hovered';
|
|
518
|
-
}
|
|
519
1054
|
if (payload.action === 'select') {
|
|
520
|
-
const el =
|
|
1055
|
+
const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
|
|
521
1056
|
if (el.tagName.toLowerCase() !== 'select') throw new Error('Element is not a <select>');
|
|
522
1057
|
el.value = payload.value ?? '';
|
|
523
1058
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
@@ -525,120 +1060,333 @@ var ChromeCdpBrowserController = class {
|
|
|
525
1060
|
return 'selected ' + el.value;
|
|
526
1061
|
}
|
|
527
1062
|
if (payload.action === 'toggle') {
|
|
528
|
-
const el =
|
|
1063
|
+
const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
|
|
529
1064
|
el.click();
|
|
530
1065
|
const checked = el.checked !== undefined ? el.checked : el.getAttribute('aria-checked') === 'true';
|
|
531
1066
|
return 'toggled to ' + (checked ? 'checked' : 'unchecked');
|
|
532
1067
|
}
|
|
533
1068
|
throw new Error('Unsupported interact action');
|
|
534
|
-
|
|
1069
|
+
`);
|
|
535
1070
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
536
1071
|
await this.ensureEnabled(targetWsUrl);
|
|
537
|
-
|
|
1072
|
+
return (await conn.send("Runtime.evaluate", {
|
|
538
1073
|
expression,
|
|
539
1074
|
returnByValue: true,
|
|
540
1075
|
awaitPromise: true
|
|
541
1076
|
})).result.value ?? "";
|
|
542
|
-
if (payload.action === "click" && value === "clicked") try {
|
|
543
|
-
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
544
|
-
try {
|
|
545
|
-
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
546
|
-
} catch {}
|
|
547
|
-
} catch {}
|
|
548
|
-
return value;
|
|
549
1077
|
});
|
|
550
1078
|
}
|
|
551
1079
|
async getContent(targetWsUrl, options) {
|
|
552
|
-
if (options.mode === "
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1080
|
+
if (options.mode === "summary") return await this.getSummaryContent(targetWsUrl, options);
|
|
1081
|
+
if (options.mode === "a11y") {
|
|
1082
|
+
const limited = normalizeRuntimeContentPayload(await this.getAccessibilityTree(targetWsUrl, options.selector), options.maxChars);
|
|
1083
|
+
return {
|
|
1084
|
+
mode: "a11y",
|
|
1085
|
+
content: limited.content,
|
|
1086
|
+
truncated: limited.truncated,
|
|
1087
|
+
originalLength: limited.originalLength
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
const expression = buildLocatorRuntimeExpression("options", options, `
|
|
1091
|
+
const limit =
|
|
1092
|
+
typeof options.maxChars === 'number' && Number.isFinite(options.maxChars) && options.maxChars > 0
|
|
1093
|
+
? Math.floor(options.maxChars)
|
|
1094
|
+
: null;
|
|
1095
|
+
|
|
1096
|
+
function serializeContent(content) {
|
|
1097
|
+
const normalized = typeof content === 'string' ? content : String(content ?? '');
|
|
1098
|
+
const originalLength = normalized.length;
|
|
1099
|
+
if (limit !== null && originalLength > limit) {
|
|
1100
|
+
return { content: normalized.slice(0, limit), truncated: true, originalLength };
|
|
1101
|
+
}
|
|
1102
|
+
return { content: normalized, truncated: false, originalLength };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (options.mode === 'title') return serializeContent(document.title ?? '');
|
|
559
1106
|
if (options.mode === 'html') {
|
|
560
1107
|
if (options.selector) {
|
|
561
|
-
const el =
|
|
562
|
-
return el ? el.outerHTML : '';
|
|
1108
|
+
const el = resolveLocator(options.selector);
|
|
1109
|
+
return serializeContent(el ? el.outerHTML : '');
|
|
563
1110
|
}
|
|
564
|
-
return document.documentElement?.outerHTML ?? '';
|
|
1111
|
+
return serializeContent(document.documentElement?.outerHTML ?? '');
|
|
565
1112
|
}
|
|
566
1113
|
if (options.selector) {
|
|
567
|
-
const el =
|
|
568
|
-
return el ? el.innerText ?? '' : '';
|
|
1114
|
+
const el = resolveLocator(options.selector);
|
|
1115
|
+
return serializeContent(el ? el.innerText ?? '' : '');
|
|
569
1116
|
}
|
|
570
|
-
return document.body?.innerText ?? '';
|
|
571
|
-
|
|
1117
|
+
return serializeContent(document.body?.innerText ?? '');
|
|
1118
|
+
`);
|
|
572
1119
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
573
1120
|
await this.ensureEnabled(targetWsUrl);
|
|
574
|
-
const
|
|
1121
|
+
const limited = normalizeRuntimeContentPayload((await conn.send("Runtime.evaluate", {
|
|
575
1122
|
expression,
|
|
576
|
-
returnByValue: true
|
|
577
|
-
|
|
1123
|
+
returnByValue: true,
|
|
1124
|
+
awaitPromise: true
|
|
1125
|
+
})).result.value, options.maxChars);
|
|
578
1126
|
return {
|
|
579
1127
|
mode: options.mode,
|
|
580
|
-
content
|
|
1128
|
+
content: limited.content,
|
|
1129
|
+
truncated: limited.truncated,
|
|
1130
|
+
originalLength: limited.originalLength
|
|
581
1131
|
};
|
|
582
1132
|
});
|
|
583
1133
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1134
|
+
summarizeInteractiveElements(result) {
|
|
1135
|
+
const primaryActions = [];
|
|
1136
|
+
const inputs = [];
|
|
1137
|
+
for (const element of result.elements) {
|
|
1138
|
+
if (primaryActions.length < 8 && (element.role === "button" || element.role === "link" || element.role === "custom")) primaryActions.push({
|
|
1139
|
+
role: element.role,
|
|
1140
|
+
text: element.text,
|
|
1141
|
+
selector: element.selector,
|
|
1142
|
+
fallbackSelectors: element.fallbackSelectors,
|
|
1143
|
+
href: element.href
|
|
1144
|
+
});
|
|
1145
|
+
if (inputs.length < 8 && (element.role === "input" || element.role === "textarea" || element.role === "select" || element.role === "checkbox" || element.role === "radio" || element.role === "contenteditable")) inputs.push({
|
|
1146
|
+
role: element.role,
|
|
1147
|
+
text: element.text,
|
|
1148
|
+
selector: element.selector,
|
|
1149
|
+
fallbackSelectors: element.fallbackSelectors,
|
|
1150
|
+
inputType: element.inputType,
|
|
1151
|
+
placeholder: element.placeholder,
|
|
1152
|
+
ariaLabel: element.ariaLabel
|
|
1153
|
+
});
|
|
1154
|
+
if (primaryActions.length >= 8 && inputs.length >= 8) break;
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
primaryActions,
|
|
1158
|
+
inputs
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
async getSummaryContent(targetWsUrl, options) {
|
|
1162
|
+
const expression = buildLocatorRuntimeExpression("options", options, `
|
|
1163
|
+
const scopeSelector = options.selector;
|
|
1164
|
+
let scopeRoot = document.body ?? document.documentElement;
|
|
1165
|
+
if (scopeSelector) {
|
|
1166
|
+
try {
|
|
1167
|
+
scopeRoot = resolveLocator(scopeSelector) ?? scopeRoot;
|
|
1168
|
+
} catch {
|
|
1169
|
+
scopeRoot = scopeRoot ?? document.documentElement;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function escapeAttr(value) {
|
|
1174
|
+
return String(value ?? '').replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function uniqueValues(values, limit) {
|
|
1178
|
+
const seen = new Set();
|
|
1179
|
+
const results = [];
|
|
1180
|
+
for (const value of values) {
|
|
1181
|
+
const normalized = String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
1182
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
1183
|
+
seen.add(normalized);
|
|
1184
|
+
results.push(normalized.slice(0, 160));
|
|
1185
|
+
if (results.length >= limit) break;
|
|
1186
|
+
}
|
|
1187
|
+
return results;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function textOf(el) {
|
|
1191
|
+
if (!el) return '';
|
|
1192
|
+
const ariaLabel = el.getAttribute && el.getAttribute('aria-label');
|
|
1193
|
+
if (ariaLabel) return ariaLabel;
|
|
1194
|
+
const text = el.innerText || el.textContent || '';
|
|
1195
|
+
return text.replace(/\\s+/g, ' ').trim();
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function frameSelector(frame) {
|
|
1199
|
+
if (frame.id) return '#' + CSS.escape(frame.id);
|
|
1200
|
+
const name = frame.getAttribute('name');
|
|
1201
|
+
if (name) return 'iframe[name="' + escapeAttr(name) + '"]';
|
|
1202
|
+
const title = frame.getAttribute('title');
|
|
1203
|
+
if (title) return 'iframe[title="' + escapeAttr(title) + '"]';
|
|
1204
|
+
const src = frame.getAttribute('src');
|
|
1205
|
+
if (src) return 'iframe[src="' + escapeAttr(src) + '"]';
|
|
1206
|
+
const parent = frame.parentElement;
|
|
1207
|
+
if (!parent) return 'iframe';
|
|
1208
|
+
const siblings = Array.from(parent.children).filter((candidate) => candidate.tagName === frame.tagName);
|
|
1209
|
+
if (siblings.length === 1) return 'iframe';
|
|
1210
|
+
return 'iframe:nth-of-type(' + (siblings.indexOf(frame) + 1) + ')';
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const headingSelectors = 'h1,h2,h3,h4,h5,h6,[role="heading"]';
|
|
1214
|
+
const landmarkSelectors = [
|
|
1215
|
+
'main',
|
|
1216
|
+
'nav',
|
|
1217
|
+
'aside',
|
|
1218
|
+
'header',
|
|
1219
|
+
'footer',
|
|
1220
|
+
'[role="main"]',
|
|
1221
|
+
'[role="navigation"]',
|
|
1222
|
+
'[role="banner"]',
|
|
1223
|
+
'[role="complementary"]',
|
|
1224
|
+
'[role="contentinfo"]',
|
|
1225
|
+
'[role="search"]'
|
|
1226
|
+
].join(',');
|
|
1227
|
+
const alertSelectors = '[role="alert"], [aria-live="assertive"], dialog[open], [aria-modal="true"]';
|
|
1228
|
+
|
|
1229
|
+
const headings = uniqueValues(
|
|
1230
|
+
Array.from(scopeRoot.querySelectorAll(headingSelectors)).map((el) => textOf(el)),
|
|
1231
|
+
8
|
|
1232
|
+
);
|
|
1233
|
+
const landmarks = uniqueValues(
|
|
1234
|
+
Array.from(scopeRoot.querySelectorAll(landmarkSelectors)).map((el) => {
|
|
1235
|
+
const role = el.getAttribute('role') || el.tagName.toLowerCase();
|
|
1236
|
+
const label = textOf(el);
|
|
1237
|
+
return label ? role + ' "' + label.slice(0, 80) + '"' : role;
|
|
1238
|
+
}),
|
|
1239
|
+
8
|
|
1240
|
+
);
|
|
1241
|
+
const alerts = uniqueValues(
|
|
1242
|
+
Array.from(scopeRoot.querySelectorAll(alertSelectors)).map((el) => textOf(el)),
|
|
1243
|
+
6
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
const allFrames = Array.from(scopeRoot.querySelectorAll('iframe'));
|
|
1247
|
+
const frames = allFrames.slice(0, 8).map((frame) => {
|
|
1248
|
+
let sameOrigin = false;
|
|
1249
|
+
try {
|
|
1250
|
+
sameOrigin = !!frame.contentDocument;
|
|
1251
|
+
} catch {
|
|
1252
|
+
sameOrigin = false;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return {
|
|
1256
|
+
selector: frameSelector(frame),
|
|
1257
|
+
name: frame.getAttribute('name') || undefined,
|
|
1258
|
+
title: frame.getAttribute('title') || undefined,
|
|
1259
|
+
src: frame.getAttribute('src') || frame.src || undefined,
|
|
1260
|
+
sameOrigin,
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
return {
|
|
1265
|
+
url: window.location.href,
|
|
1266
|
+
title: document.title ?? '',
|
|
1267
|
+
headings,
|
|
1268
|
+
landmarks,
|
|
1269
|
+
alerts,
|
|
1270
|
+
frames,
|
|
1271
|
+
crossOriginFrameCount: allFrames.filter((frame) => {
|
|
1272
|
+
try {
|
|
1273
|
+
return !frame.contentDocument;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
}).length,
|
|
1278
|
+
hasMoreFrames: allFrames.length > frames.length,
|
|
1279
|
+
scopedToSelector: scopeSelector || undefined,
|
|
1280
|
+
};
|
|
1281
|
+
`);
|
|
1282
|
+
const pageSummary = await this.withRetry(targetWsUrl, async (conn) => {
|
|
1283
|
+
await this.ensureEnabled(targetWsUrl);
|
|
1284
|
+
return (await conn.send("Runtime.evaluate", {
|
|
1285
|
+
expression,
|
|
1286
|
+
returnByValue: true,
|
|
1287
|
+
awaitPromise: true
|
|
1288
|
+
})).result.value ?? {};
|
|
1289
|
+
});
|
|
1290
|
+
const elements = await this.getInteractiveElements(targetWsUrl, {
|
|
1291
|
+
selector: options.selector,
|
|
1292
|
+
visibleOnly: true,
|
|
1293
|
+
limit: 24
|
|
1294
|
+
});
|
|
1295
|
+
const elementSummary = this.summarizeInteractiveElements(elements);
|
|
1296
|
+
return {
|
|
1297
|
+
mode: "summary",
|
|
1298
|
+
content: "",
|
|
1299
|
+
structuredContent: {
|
|
1300
|
+
mode: "summary",
|
|
1301
|
+
...pageSummary,
|
|
1302
|
+
...elementSummary,
|
|
1303
|
+
totalInteractiveElements: elements.totalFound,
|
|
1304
|
+
truncatedInteractiveElements: elements.truncated
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
formatAccessibilityTree(nodes) {
|
|
1309
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
1310
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (const node of nodes) {
|
|
1312
|
+
nodeMap.set(node.nodeId, node);
|
|
1313
|
+
if (node.parentId) {
|
|
1314
|
+
const siblings = childrenMap.get(node.parentId);
|
|
1315
|
+
if (siblings) siblings.push(node.nodeId);
|
|
1316
|
+
else childrenMap.set(node.parentId, [node.nodeId]);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const lines = [];
|
|
1320
|
+
const formatNode = (nodeId, depth) => {
|
|
1321
|
+
const node = nodeMap.get(nodeId);
|
|
1322
|
+
if (!node) return;
|
|
1323
|
+
const role = node.role?.value ?? "unknown";
|
|
1324
|
+
const name = node.name?.value ?? "";
|
|
1325
|
+
const value = node.value?.value ?? "";
|
|
1326
|
+
if (node.ignored) {
|
|
1327
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
1328
|
+
for (const childId of children) formatNode(childId, depth);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const skip = !name && !value && (role === "generic" || role === "none" || role === "GenericContainer");
|
|
1332
|
+
if (!skip) {
|
|
1333
|
+
let line = `${" ".repeat(depth)}${role}`;
|
|
1334
|
+
if (name) line += ` "${name}"`;
|
|
1335
|
+
if (value) line += ` value="${value}"`;
|
|
1336
|
+
if (node.properties) {
|
|
1337
|
+
for (const prop of node.properties) if (prop.value.value === true) line += ` [${prop.name}]`;
|
|
1338
|
+
else if (prop.name === "checked" && prop.value.value === "mixed") line += ` [indeterminate]`;
|
|
596
1339
|
}
|
|
1340
|
+
lines.push(line);
|
|
597
1341
|
}
|
|
598
|
-
const
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1342
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
1343
|
+
for (const childId of children) formatNode(childId, skip ? depth : depth + 1);
|
|
1344
|
+
};
|
|
1345
|
+
const roots = nodes.filter((node) => !node.parentId || !nodeMap.has(node.parentId));
|
|
1346
|
+
for (const root of roots) formatNode(root.nodeId, 0);
|
|
1347
|
+
return lines.join("\n");
|
|
1348
|
+
}
|
|
1349
|
+
async getAccessibilityTree(targetWsUrl, selector) {
|
|
1350
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1351
|
+
await this.ensureEnabled(targetWsUrl);
|
|
1352
|
+
await conn.send("Accessibility.enable");
|
|
1353
|
+
let nodes;
|
|
1354
|
+
if (selector) {
|
|
1355
|
+
const objectGroup = "agentic-browser-a11y";
|
|
1356
|
+
const selected = await conn.send("Runtime.evaluate", {
|
|
1357
|
+
expression: buildLocatorRuntimeExpression("selector", selector, `
|
|
1358
|
+
return resolveLocator(selector);
|
|
1359
|
+
`),
|
|
1360
|
+
objectGroup
|
|
1361
|
+
});
|
|
1362
|
+
const objectId = selected.result.objectId;
|
|
1363
|
+
if (!objectId || selected.result.subtype === "null") {
|
|
1364
|
+
try {
|
|
1365
|
+
await conn.send("Runtime.releaseObjectGroup", { objectGroup });
|
|
1366
|
+
} catch {}
|
|
1367
|
+
return "";
|
|
609
1368
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
lines.push(line);
|
|
1369
|
+
try {
|
|
1370
|
+
nodes = (await conn.send("Accessibility.getPartialAXTree", {
|
|
1371
|
+
objectId,
|
|
1372
|
+
fetchRelatives: false
|
|
1373
|
+
})).nodes;
|
|
1374
|
+
} finally {
|
|
1375
|
+
try {
|
|
1376
|
+
await conn.send("Runtime.releaseObjectGroup", { objectGroup });
|
|
1377
|
+
} catch {}
|
|
620
1378
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
};
|
|
624
|
-
const roots = nodes.filter((n) => !n.parentId);
|
|
625
|
-
for (const root of roots) formatNode(root.nodeId, 0);
|
|
626
|
-
return lines.join("\n");
|
|
1379
|
+
} else nodes = (await conn.send("Accessibility.getFullAXTree")).nodes;
|
|
1380
|
+
return this.formatAccessibilityTree(nodes);
|
|
627
1381
|
});
|
|
628
1382
|
}
|
|
629
1383
|
async getInteractiveElements(targetWsUrl, options) {
|
|
630
|
-
const expression =
|
|
631
|
-
const options = ${JSON.stringify(options)};
|
|
1384
|
+
const expression = buildLocatorRuntimeExpression("options", options, `
|
|
632
1385
|
const visibleOnly = options.visibleOnly !== false;
|
|
633
1386
|
const limit = options.limit ?? 50;
|
|
634
1387
|
const scopeSelector = options.selector;
|
|
635
1388
|
const roleFilter = options.roles ? new Set(options.roles) : null;
|
|
636
|
-
|
|
637
|
-
const root = scopeSelector
|
|
638
|
-
? document.querySelector(scopeSelector) ?? document.body
|
|
639
|
-
: document.body;
|
|
640
|
-
|
|
641
|
-
const candidates = root.querySelectorAll([
|
|
1389
|
+
const candidateSelector = [
|
|
642
1390
|
'a[href]',
|
|
643
1391
|
'button',
|
|
644
1392
|
'input:not([type="hidden"])',
|
|
@@ -654,43 +1402,15 @@ var ChromeCdpBrowserController = class {
|
|
|
654
1402
|
'[onclick]',
|
|
655
1403
|
'[tabindex]',
|
|
656
1404
|
'[contenteditable="true"]',
|
|
657
|
-
'[contenteditable=""]'
|
|
658
|
-
].join(',')
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (tag === 'button' || ariaRole === 'button') return 'button';
|
|
667
|
-
if (tag === 'input') {
|
|
668
|
-
const t = (el.type || 'text').toLowerCase();
|
|
669
|
-
if (t === 'checkbox') return 'checkbox';
|
|
670
|
-
if (t === 'radio') return 'radio';
|
|
671
|
-
return 'input';
|
|
672
|
-
}
|
|
673
|
-
if (tag === 'select') return 'select';
|
|
674
|
-
if (tag === 'textarea') return 'textarea';
|
|
675
|
-
if (el.isContentEditable) return 'contenteditable';
|
|
676
|
-
if (ariaRole === 'link') return 'link';
|
|
677
|
-
if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
|
|
678
|
-
if (ariaRole === 'radio') return 'radio';
|
|
679
|
-
return 'custom';
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function getActions(role, el) {
|
|
683
|
-
switch (role) {
|
|
684
|
-
case 'link': case 'button': case 'custom': return ['click'];
|
|
685
|
-
case 'input': {
|
|
686
|
-
const t = (el.type || 'text').toLowerCase();
|
|
687
|
-
if (t === 'submit' || t === 'reset' || t === 'button' || t === 'file') return ['click'];
|
|
688
|
-
return ['click', 'type', 'press'];
|
|
689
|
-
}
|
|
690
|
-
case 'textarea': case 'contenteditable': return ['click', 'type', 'press'];
|
|
691
|
-
case 'select': return ['click', 'select'];
|
|
692
|
-
case 'checkbox': case 'radio': return ['click', 'toggle'];
|
|
693
|
-
default: return ['click'];
|
|
1405
|
+
'[contenteditable=""]'
|
|
1406
|
+
].join(',');
|
|
1407
|
+
|
|
1408
|
+
let scopeElement = null;
|
|
1409
|
+
if (scopeSelector) {
|
|
1410
|
+
try {
|
|
1411
|
+
scopeElement = resolveLocator(scopeSelector) ?? document.body;
|
|
1412
|
+
} catch {
|
|
1413
|
+
scopeElement = document.body;
|
|
694
1414
|
}
|
|
695
1415
|
}
|
|
696
1416
|
|
|
@@ -698,79 +1418,90 @@ var ChromeCdpBrowserController = class {
|
|
|
698
1418
|
return value.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
|
|
699
1419
|
}
|
|
700
1420
|
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
1421
|
+
function composeLocator(path, selector) {
|
|
1422
|
+
if (!path.length) return selector;
|
|
1423
|
+
return path.join(LOCATOR_SEPARATOR) + LOCATOR_SEPARATOR + selector;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function tryUniqueSelector(root, selector) {
|
|
1427
|
+
try {
|
|
1428
|
+
return root.querySelectorAll(selector).length === 1 ? selector : null;
|
|
1429
|
+
} catch {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
704
1432
|
}
|
|
705
1433
|
|
|
706
|
-
function buildSelector(el) {
|
|
1434
|
+
function buildSelector(root, el) {
|
|
707
1435
|
if (el.id) return '#' + CSS.escape(el.id);
|
|
708
1436
|
|
|
709
1437
|
const name = el.getAttribute('name');
|
|
710
1438
|
if (name) {
|
|
711
1439
|
const tag = el.tagName.toLowerCase();
|
|
712
|
-
const
|
|
713
|
-
if (
|
|
1440
|
+
const selector = tryUniqueSelector(root, tag + '[name="' + escapeAttr(name) + '"]');
|
|
1441
|
+
if (selector) return selector;
|
|
714
1442
|
}
|
|
715
1443
|
|
|
716
1444
|
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
|
|
717
1445
|
if (testId) {
|
|
718
1446
|
const attr = el.hasAttribute('data-testid') ? 'data-testid' : 'data-test-id';
|
|
719
|
-
const
|
|
720
|
-
if (
|
|
1447
|
+
const selector = tryUniqueSelector(root, '[' + attr + '="' + escapeAttr(testId) + '"]');
|
|
1448
|
+
if (selector) return selector;
|
|
721
1449
|
}
|
|
722
1450
|
|
|
723
1451
|
const ariaLabel = el.getAttribute('aria-label');
|
|
724
1452
|
if (ariaLabel) {
|
|
725
1453
|
const tag = el.tagName.toLowerCase();
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
1454
|
+
const selector = tryUniqueSelector(root, tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]');
|
|
1455
|
+
if (selector) return selector;
|
|
728
1456
|
}
|
|
729
1457
|
|
|
730
1458
|
const dataCy = el.getAttribute('data-cy');
|
|
731
1459
|
if (dataCy) {
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
1460
|
+
const selector = tryUniqueSelector(root, '[data-cy="' + escapeAttr(dataCy) + '"]');
|
|
1461
|
+
if (selector) return selector;
|
|
734
1462
|
}
|
|
735
1463
|
|
|
736
1464
|
const dataTest = el.getAttribute('data-test');
|
|
737
1465
|
if (dataTest) {
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
1466
|
+
const selector = tryUniqueSelector(root, '[data-test="' + escapeAttr(dataTest) + '"]');
|
|
1467
|
+
if (selector) return selector;
|
|
740
1468
|
}
|
|
741
1469
|
|
|
742
1470
|
const role = el.getAttribute('role');
|
|
743
1471
|
if (role && ariaLabel) {
|
|
744
|
-
const
|
|
745
|
-
|
|
1472
|
+
const selector = tryUniqueSelector(
|
|
1473
|
+
root,
|
|
1474
|
+
'[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]'
|
|
1475
|
+
);
|
|
1476
|
+
if (selector) return selector;
|
|
746
1477
|
}
|
|
747
1478
|
|
|
748
|
-
// Path fallback — anchor at nearest ancestor with an id for shorter selectors
|
|
749
1479
|
const parts = [];
|
|
750
1480
|
let current = el;
|
|
751
|
-
while (current && current
|
|
1481
|
+
while (current && current.nodeType === 1) {
|
|
752
1482
|
const tag = current.tagName.toLowerCase();
|
|
753
1483
|
if (current !== el && current.id) {
|
|
754
1484
|
parts.unshift('#' + CSS.escape(current.id));
|
|
755
1485
|
break;
|
|
756
1486
|
}
|
|
757
1487
|
const parent = current.parentElement;
|
|
758
|
-
if (!parent) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1488
|
+
if (!parent) {
|
|
1489
|
+
parts.unshift(tag);
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
const siblings = Array.from(parent.children).filter((candidate) => candidate.tagName === current.tagName);
|
|
762
1493
|
if (siblings.length === 1) {
|
|
763
1494
|
parts.unshift(tag);
|
|
764
1495
|
} else {
|
|
765
|
-
const
|
|
766
|
-
parts.unshift(tag + ':nth-of-type(' +
|
|
1496
|
+
const index = siblings.indexOf(current) + 1;
|
|
1497
|
+
parts.unshift(tag + ':nth-of-type(' + index + ')');
|
|
767
1498
|
}
|
|
768
1499
|
current = parent;
|
|
769
1500
|
}
|
|
770
1501
|
return parts.join(' > ');
|
|
771
1502
|
}
|
|
772
1503
|
|
|
773
|
-
function buildFallbackSelectors(el, primarySelector) {
|
|
1504
|
+
function buildFallbackSelectors(root, path, el, primarySelector) {
|
|
774
1505
|
const fallbacks = [];
|
|
775
1506
|
const candidates = [];
|
|
776
1507
|
|
|
@@ -779,8 +1510,7 @@ var ChromeCdpBrowserController = class {
|
|
|
779
1510
|
const name = el.getAttribute('name');
|
|
780
1511
|
if (name) {
|
|
781
1512
|
const tag = el.tagName.toLowerCase();
|
|
782
|
-
|
|
783
|
-
candidates.push(sel);
|
|
1513
|
+
candidates.push(tag + '[name="' + escapeAttr(name) + '"]');
|
|
784
1514
|
}
|
|
785
1515
|
|
|
786
1516
|
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
|
|
@@ -806,16 +1536,68 @@ var ChromeCdpBrowserController = class {
|
|
|
806
1536
|
candidates.push('[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]');
|
|
807
1537
|
}
|
|
808
1538
|
|
|
809
|
-
for (const
|
|
810
|
-
if (
|
|
811
|
-
|
|
1539
|
+
for (const candidate of candidates) {
|
|
1540
|
+
if (!tryUniqueSelector(root, candidate)) continue;
|
|
1541
|
+
const locator = composeLocator(path, candidate);
|
|
1542
|
+
if (locator === primarySelector) continue;
|
|
1543
|
+
fallbacks.push(locator);
|
|
812
1544
|
if (fallbacks.length >= 3) break;
|
|
813
1545
|
}
|
|
814
1546
|
return fallbacks;
|
|
815
1547
|
}
|
|
816
1548
|
|
|
1549
|
+
function classifyRole(el) {
|
|
1550
|
+
const tag = el.tagName.toLowerCase();
|
|
1551
|
+
const ariaRole = el.getAttribute('role');
|
|
1552
|
+
if (tag === 'a') return 'link';
|
|
1553
|
+
if (tag === 'button' || ariaRole === 'button') return 'button';
|
|
1554
|
+
if (tag === 'input') {
|
|
1555
|
+
const type = (el.type || 'text').toLowerCase();
|
|
1556
|
+
if (type === 'checkbox') return 'checkbox';
|
|
1557
|
+
if (type === 'radio') return 'radio';
|
|
1558
|
+
return 'input';
|
|
1559
|
+
}
|
|
1560
|
+
if (tag === 'select') return 'select';
|
|
1561
|
+
if (tag === 'textarea') return 'textarea';
|
|
1562
|
+
if (el.isContentEditable) return 'contenteditable';
|
|
1563
|
+
if (ariaRole === 'link') return 'link';
|
|
1564
|
+
if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
|
|
1565
|
+
if (ariaRole === 'radio') return 'radio';
|
|
1566
|
+
return 'custom';
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function getActions(role, el) {
|
|
1570
|
+
switch (role) {
|
|
1571
|
+
case 'link':
|
|
1572
|
+
case 'button':
|
|
1573
|
+
case 'custom':
|
|
1574
|
+
return ['click'];
|
|
1575
|
+
case 'input': {
|
|
1576
|
+
const type = (el.type || 'text').toLowerCase();
|
|
1577
|
+
if (type === 'submit' || type === 'reset' || type === 'button' || type === 'file') {
|
|
1578
|
+
return ['click'];
|
|
1579
|
+
}
|
|
1580
|
+
return ['click', 'type', 'press'];
|
|
1581
|
+
}
|
|
1582
|
+
case 'textarea':
|
|
1583
|
+
case 'contenteditable':
|
|
1584
|
+
return ['click', 'type', 'press'];
|
|
1585
|
+
case 'select':
|
|
1586
|
+
return ['click', 'select'];
|
|
1587
|
+
case 'checkbox':
|
|
1588
|
+
case 'radio':
|
|
1589
|
+
return ['click', 'toggle'];
|
|
1590
|
+
default:
|
|
1591
|
+
return ['click'];
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function viewForElement(el) {
|
|
1596
|
+
return el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : window;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
817
1599
|
function isVisible(el) {
|
|
818
|
-
const style =
|
|
1600
|
+
const style = viewForElement(el).getComputedStyle(el);
|
|
819
1601
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
820
1602
|
return false;
|
|
821
1603
|
}
|
|
@@ -829,16 +1611,16 @@ var ChromeCdpBrowserController = class {
|
|
|
829
1611
|
|
|
830
1612
|
const tag = el.tagName.toLowerCase();
|
|
831
1613
|
if (tag === 'input') {
|
|
832
|
-
const
|
|
833
|
-
if (
|
|
834
|
-
const
|
|
835
|
-
if (
|
|
1614
|
+
const value = el.value;
|
|
1615
|
+
if (value) return value.slice(0, 80);
|
|
1616
|
+
const placeholder = el.getAttribute('placeholder');
|
|
1617
|
+
if (placeholder) return placeholder.slice(0, 80);
|
|
836
1618
|
return el.type || 'text';
|
|
837
1619
|
}
|
|
838
1620
|
|
|
839
1621
|
const directText = Array.from(el.childNodes)
|
|
840
|
-
.filter(
|
|
841
|
-
.map(
|
|
1622
|
+
.filter((node) => node.nodeType === 3)
|
|
1623
|
+
.map((node) => node.textContent.trim())
|
|
842
1624
|
.filter(Boolean)
|
|
843
1625
|
.join(' ');
|
|
844
1626
|
if (directText) return directText.slice(0, 80);
|
|
@@ -861,48 +1643,95 @@ var ChromeCdpBrowserController = class {
|
|
|
861
1643
|
return ariaDisabled !== 'true';
|
|
862
1644
|
}
|
|
863
1645
|
|
|
1646
|
+
function rootEntries(root) {
|
|
1647
|
+
if (!root) return [];
|
|
1648
|
+
if (root.nodeType === Node.DOCUMENT_NODE) {
|
|
1649
|
+
if (root.body) return [root.body];
|
|
1650
|
+
if (root.documentElement) return [root.documentElement];
|
|
1651
|
+
return [];
|
|
1652
|
+
}
|
|
1653
|
+
if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
1654
|
+
return Array.from(root.children || []);
|
|
1655
|
+
}
|
|
1656
|
+
return [root];
|
|
1657
|
+
}
|
|
1658
|
+
|
|
864
1659
|
const results = [];
|
|
865
1660
|
let totalFound = 0;
|
|
866
1661
|
|
|
867
|
-
|
|
868
|
-
if (
|
|
869
|
-
|
|
1662
|
+
function maybeRecordElement(root, path, el) {
|
|
1663
|
+
if (scopeElement && !isWithinComposedSubtree(scopeElement, el)) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (!el.matches(candidateSelector)) {
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
870
1670
|
|
|
871
1671
|
const role = classifyRole(el);
|
|
872
|
-
if (roleFilter && !roleFilter.has(role))
|
|
1672
|
+
if (roleFilter && !roleFilter.has(role)) return;
|
|
873
1673
|
|
|
874
|
-
const
|
|
875
|
-
if (visibleOnly && !
|
|
1674
|
+
const visible = isVisible(el);
|
|
1675
|
+
if (visibleOnly && !visible) return;
|
|
876
1676
|
|
|
877
|
-
totalFound
|
|
878
|
-
if (results.length >= limit)
|
|
1677
|
+
totalFound += 1;
|
|
1678
|
+
if (results.length >= limit) return;
|
|
879
1679
|
|
|
880
|
-
const
|
|
1680
|
+
const selector = composeLocator(path, buildSelector(root, el));
|
|
881
1681
|
const entry = {
|
|
882
|
-
selector
|
|
1682
|
+
selector,
|
|
883
1683
|
role,
|
|
884
1684
|
tagName: el.tagName.toLowerCase(),
|
|
885
1685
|
text: getText(el),
|
|
886
1686
|
actions: getActions(role, el),
|
|
887
|
-
visible
|
|
1687
|
+
visible,
|
|
888
1688
|
enabled: isEnabled(el),
|
|
889
1689
|
};
|
|
890
1690
|
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
1691
|
+
const fallbacks = buildFallbackSelectors(root, path, el, selector);
|
|
1692
|
+
if (fallbacks.length) entry.fallbackSelectors = fallbacks;
|
|
893
1693
|
|
|
894
1694
|
if (role === 'link' && el.href) entry.href = el.href;
|
|
895
1695
|
if (role === 'input') entry.inputType = (el.type || 'text').toLowerCase();
|
|
896
|
-
const
|
|
897
|
-
if (
|
|
898
|
-
const
|
|
899
|
-
if (
|
|
1696
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
1697
|
+
if (ariaLabel) entry.ariaLabel = ariaLabel;
|
|
1698
|
+
const placeholder = el.getAttribute('placeholder');
|
|
1699
|
+
if (placeholder) entry.placeholder = placeholder;
|
|
900
1700
|
|
|
901
1701
|
results.push(entry);
|
|
902
1702
|
}
|
|
903
1703
|
|
|
1704
|
+
function walkRoot(root, path) {
|
|
1705
|
+
const queue = rootEntries(root).slice();
|
|
1706
|
+
while (queue.length) {
|
|
1707
|
+
const current = queue.shift();
|
|
1708
|
+
if (!current || current.nodeType !== 1) continue;
|
|
1709
|
+
|
|
1710
|
+
maybeRecordElement(root, path, current);
|
|
1711
|
+
|
|
1712
|
+
if (current.shadowRoot) {
|
|
1713
|
+
walkRoot(current.shadowRoot, path.concat(buildSelector(root, current)));
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
1717
|
+
try {
|
|
1718
|
+
if (current.contentDocument) {
|
|
1719
|
+
walkRoot(current.contentDocument, path.concat(buildSelector(root, current)));
|
|
1720
|
+
}
|
|
1721
|
+
} catch {
|
|
1722
|
+
// ignore inaccessible frames
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
for (const child of Array.from(current.children)) {
|
|
1727
|
+
queue.push(child);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
walkRoot(document, []);
|
|
904
1733
|
return { elements: results, totalFound, truncated: totalFound > results.length };
|
|
905
|
-
|
|
1734
|
+
`);
|
|
906
1735
|
const emptyResult = {
|
|
907
1736
|
elements: [],
|
|
908
1737
|
totalFound: 0,
|
|
@@ -1152,17 +1981,29 @@ var MockBrowserController = class {
|
|
|
1152
1981
|
async getContent(cdpUrl, options) {
|
|
1153
1982
|
const page = this.pages.get(cdpUrl);
|
|
1154
1983
|
if (!page) throw new Error("mock page missing");
|
|
1155
|
-
if (options.mode === "
|
|
1156
|
-
mode: "
|
|
1157
|
-
content:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1984
|
+
if (options.mode === "summary") return {
|
|
1985
|
+
mode: "summary",
|
|
1986
|
+
content: "",
|
|
1987
|
+
structuredContent: {
|
|
1988
|
+
mode: "summary",
|
|
1989
|
+
url: page.url,
|
|
1990
|
+
title: page.title,
|
|
1991
|
+
headings: [page.title],
|
|
1992
|
+
landmarks: [],
|
|
1993
|
+
alerts: [],
|
|
1994
|
+
frames: [],
|
|
1995
|
+
primaryActions: [],
|
|
1996
|
+
inputs: [],
|
|
1997
|
+
totalInteractiveElements: 0,
|
|
1998
|
+
truncatedInteractiveElements: false
|
|
1999
|
+
}
|
|
1162
2000
|
};
|
|
2001
|
+
const limited = applyContentLimit(options.mode === "title" ? page.title : options.mode === "html" ? page.html : page.text, options.maxChars);
|
|
1163
2002
|
return {
|
|
1164
|
-
mode:
|
|
1165
|
-
content:
|
|
2003
|
+
mode: options.mode,
|
|
2004
|
+
content: limited.content,
|
|
2005
|
+
truncated: limited.truncated || void 0,
|
|
2006
|
+
originalLength: limited.originalLength
|
|
1166
2007
|
};
|
|
1167
2008
|
}
|
|
1168
2009
|
async getInteractiveElements(_targetWsUrl, _options) {
|
|
@@ -1178,7 +2019,6 @@ var MockBrowserController = class {
|
|
|
1178
2019
|
terminate(_pid) {}
|
|
1179
2020
|
closeConnection(_targetWsUrl) {}
|
|
1180
2021
|
};
|
|
1181
|
-
|
|
1182
2022
|
//#endregion
|
|
1183
2023
|
//#region src/session/session-store.ts
|
|
1184
2024
|
var SessionStore = class {
|
|
@@ -1267,7 +2107,6 @@ var SessionStore = class {
|
|
|
1267
2107
|
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
|
|
1268
2108
|
}
|
|
1269
2109
|
};
|
|
1270
|
-
|
|
1271
2110
|
//#endregion
|
|
1272
2111
|
//#region src/session/session-manager.ts
|
|
1273
2112
|
var SessionManager = class {
|
|
@@ -1640,7 +2479,6 @@ var SessionManager = class {
|
|
|
1640
2479
|
}
|
|
1641
2480
|
}
|
|
1642
2481
|
};
|
|
1643
|
-
|
|
1644
2482
|
//#endregion
|
|
1645
2483
|
//#region src/transport/control-api.ts
|
|
1646
2484
|
var ControlApi = class {
|
|
@@ -1694,7 +2532,6 @@ var ControlApi = class {
|
|
|
1694
2532
|
return this.sessions.cleanupSessions(input);
|
|
1695
2533
|
}
|
|
1696
2534
|
};
|
|
1697
|
-
|
|
1698
2535
|
//#endregion
|
|
1699
2536
|
//#region src/lib/config.ts
|
|
1700
2537
|
const DEFAULT_PORT = 43111;
|
|
@@ -1720,7 +2557,6 @@ function loadConfig(env = process.env) {
|
|
|
1720
2557
|
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
|
|
1721
2558
|
};
|
|
1722
2559
|
}
|
|
1723
|
-
|
|
1724
2560
|
//#endregion
|
|
1725
2561
|
//#region src/observability/logger.ts
|
|
1726
2562
|
var Logger = class {
|
|
@@ -1752,23 +2588,32 @@ var Logger = class {
|
|
|
1752
2588
|
this.emit("error", message, context);
|
|
1753
2589
|
}
|
|
1754
2590
|
};
|
|
1755
|
-
|
|
1756
2591
|
//#endregion
|
|
1757
2592
|
//#region src/observability/event-store.ts
|
|
1758
2593
|
const MAX_EVENTS_PER_SESSION = 500;
|
|
1759
2594
|
var EventStore = class {
|
|
1760
2595
|
events = /* @__PURE__ */ new Map();
|
|
1761
2596
|
filePath;
|
|
2597
|
+
bufferedLines = [];
|
|
2598
|
+
flushTimer;
|
|
2599
|
+
flushInFlight;
|
|
1762
2600
|
constructor(baseDir) {
|
|
1763
2601
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
1764
2602
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
2603
|
+
process.once("beforeExit", this.flushSync);
|
|
2604
|
+
process.once("exit", this.flushSync);
|
|
1765
2605
|
}
|
|
1766
2606
|
append(event) {
|
|
1767
2607
|
let existing = this.events.get(event.sessionId) ?? [];
|
|
1768
2608
|
existing.push(event);
|
|
1769
2609
|
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
|
|
1770
2610
|
this.events.set(event.sessionId, existing);
|
|
1771
|
-
|
|
2611
|
+
this.bufferedLines.push(`${JSON.stringify(event)}\n`);
|
|
2612
|
+
if (this.bufferedLines.length >= 20) {
|
|
2613
|
+
this.flushSoon(0);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
this.flushSoon(25);
|
|
1772
2617
|
}
|
|
1773
2618
|
list(sessionId, limit = 100) {
|
|
1774
2619
|
const entries = this.events.get(sessionId) ?? [];
|
|
@@ -1777,8 +2622,37 @@ var EventStore = class {
|
|
|
1777
2622
|
clear(sessionId) {
|
|
1778
2623
|
this.events.delete(sessionId);
|
|
1779
2624
|
}
|
|
2625
|
+
flushSoon(delayMs) {
|
|
2626
|
+
if (this.flushInFlight) return this.flushInFlight;
|
|
2627
|
+
if (this.flushTimer) return Promise.resolve();
|
|
2628
|
+
this.flushTimer = setTimeout(() => {
|
|
2629
|
+
this.flushTimer = void 0;
|
|
2630
|
+
this.flushAsync();
|
|
2631
|
+
}, delayMs);
|
|
2632
|
+
this.flushTimer.unref();
|
|
2633
|
+
return Promise.resolve();
|
|
2634
|
+
}
|
|
2635
|
+
async flushAsync() {
|
|
2636
|
+
if (this.flushInFlight || this.bufferedLines.length === 0) return;
|
|
2637
|
+
const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
|
|
2638
|
+
this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, "utf8").catch(() => {}).finally(() => {
|
|
2639
|
+
this.flushInFlight = void 0;
|
|
2640
|
+
if (this.bufferedLines.length > 0) this.flushSoon(0);
|
|
2641
|
+
});
|
|
2642
|
+
await this.flushInFlight;
|
|
2643
|
+
}
|
|
2644
|
+
flushSync = () => {
|
|
2645
|
+
if (this.flushTimer) {
|
|
2646
|
+
clearTimeout(this.flushTimer);
|
|
2647
|
+
this.flushTimer = void 0;
|
|
2648
|
+
}
|
|
2649
|
+
if (this.bufferedLines.length === 0) return;
|
|
2650
|
+
const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
|
|
2651
|
+
try {
|
|
2652
|
+
fs.appendFileSync(this.filePath, chunk, "utf8");
|
|
2653
|
+
} catch {}
|
|
2654
|
+
};
|
|
1780
2655
|
};
|
|
1781
|
-
|
|
1782
2656
|
//#endregion
|
|
1783
2657
|
//#region src/auth/session-token.ts
|
|
1784
2658
|
var SessionTokenService = class {
|
|
@@ -1806,7 +2680,6 @@ var SessionTokenService = class {
|
|
|
1806
2680
|
this.tokens.set(sessionId, token);
|
|
1807
2681
|
}
|
|
1808
2682
|
};
|
|
1809
|
-
|
|
1810
2683
|
//#endregion
|
|
1811
2684
|
//#region src/transport/ws-server.ts
|
|
1812
2685
|
var AuthenticatedWsServer = class {
|
|
@@ -1841,7 +2714,6 @@ var AuthenticatedWsServer = class {
|
|
|
1841
2714
|
this.server = void 0;
|
|
1842
2715
|
}
|
|
1843
2716
|
};
|
|
1844
|
-
|
|
1845
2717
|
//#endregion
|
|
1846
2718
|
//#region src/memory/memory-index.ts
|
|
1847
2719
|
function normalize(value) {
|
|
@@ -1902,11 +2774,10 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
|
|
|
1902
2774
|
var MemoryIndex = class {
|
|
1903
2775
|
/** Domain → insights index, rebuilt lazily when insight list changes. */
|
|
1904
2776
|
domainIndex = /* @__PURE__ */ new Map();
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
/** Rebuild the domain index when the underlying array or its size changes. */
|
|
2777
|
+
indexedRefs = [];
|
|
2778
|
+
/** Rebuild the domain index when the insight object set changes. */
|
|
1908
2779
|
ensureIndex(insights) {
|
|
1909
|
-
if (this.
|
|
2780
|
+
if (this.indexedRefs.length === insights.length && insights.every((insight, index) => this.indexedRefs[index] === insight)) return;
|
|
1910
2781
|
this.domainIndex.clear();
|
|
1911
2782
|
for (const insight of insights) {
|
|
1912
2783
|
const domain = normalize(insight.siteDomain);
|
|
@@ -1917,8 +2788,7 @@ var MemoryIndex = class {
|
|
|
1917
2788
|
}
|
|
1918
2789
|
bucket.push(insight);
|
|
1919
2790
|
}
|
|
1920
|
-
this.
|
|
1921
|
-
this.indexedLength = insights.length;
|
|
2791
|
+
this.indexedRefs = insights.slice();
|
|
1922
2792
|
}
|
|
1923
2793
|
search(insights, input) {
|
|
1924
2794
|
const normalizedIntent = normalize(input.taskIntent);
|
|
@@ -1940,7 +2810,6 @@ var MemoryIndex = class {
|
|
|
1940
2810
|
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1941
2811
|
}
|
|
1942
2812
|
};
|
|
1943
|
-
|
|
1944
2813
|
//#endregion
|
|
1945
2814
|
//#region src/memory/staleness-detector.ts
|
|
1946
2815
|
const MAX_SUSPECT_STRIKES = 2;
|
|
@@ -1983,7 +2852,6 @@ function applySuccess(insight) {
|
|
|
1983
2852
|
updatedAt: now
|
|
1984
2853
|
};
|
|
1985
2854
|
}
|
|
1986
|
-
|
|
1987
2855
|
//#endregion
|
|
1988
2856
|
//#region src/memory/memory-schemas.ts
|
|
1989
2857
|
const InsightFreshnessSchema = z.enum([
|
|
@@ -2036,7 +2904,6 @@ const TaskInsightSchema = z.object({
|
|
|
2036
2904
|
selectorAliases: z.array(SelectorAliasSchema).default([])
|
|
2037
2905
|
});
|
|
2038
2906
|
const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
|
|
2039
|
-
|
|
2040
2907
|
//#endregion
|
|
2041
2908
|
//#region src/memory/task-insight-store.ts
|
|
2042
2909
|
const EMPTY_STATE = { insights: [] };
|
|
@@ -2160,7 +3027,6 @@ var TaskInsightStore = class {
|
|
|
2160
3027
|
});
|
|
2161
3028
|
}
|
|
2162
3029
|
};
|
|
2163
|
-
|
|
2164
3030
|
//#endregion
|
|
2165
3031
|
//#region src/memory/memory-service.ts
|
|
2166
3032
|
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
@@ -2380,7 +3246,6 @@ var MemoryService = class {
|
|
|
2380
3246
|
return [...recipe, step].slice(-8);
|
|
2381
3247
|
}
|
|
2382
3248
|
};
|
|
2383
|
-
|
|
2384
3249
|
//#endregion
|
|
2385
3250
|
//#region src/cli/app.ts
|
|
2386
3251
|
function createAppContext(env = process.env) {
|
|
@@ -2401,7 +3266,6 @@ function createAppContext(env = process.env) {
|
|
|
2401
3266
|
memoryService: new MemoryService(config.logDir)
|
|
2402
3267
|
};
|
|
2403
3268
|
}
|
|
2404
|
-
|
|
2405
3269
|
//#endregion
|
|
2406
3270
|
//#region src/cli/runtime.ts
|
|
2407
3271
|
var AgenticBrowserCore = class {
|
|
@@ -2429,7 +3293,8 @@ var AgenticBrowserCore = class {
|
|
|
2429
3293
|
async getPageContent(input) {
|
|
2430
3294
|
return await this.api.getContent(input.sessionId, {
|
|
2431
3295
|
mode: input.mode,
|
|
2432
|
-
selector: input.selector
|
|
3296
|
+
selector: input.selector,
|
|
3297
|
+
maxChars: input.maxChars
|
|
2433
3298
|
});
|
|
2434
3299
|
}
|
|
2435
3300
|
async getInteractiveElements(input) {
|
|
@@ -2475,6 +3340,5 @@ function createMockAgenticBrowserCore(env) {
|
|
|
2475
3340
|
function createCliRuntime() {
|
|
2476
3341
|
return createAgenticBrowserCore();
|
|
2477
3342
|
}
|
|
2478
|
-
|
|
2479
3343
|
//#endregion
|
|
2480
|
-
export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
|
|
3344
|
+
export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
|