libretto 0.5.0 → 0.5.2
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 +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
filterSemanticClasses,
|
|
3
|
+
INTERACTIVE_ROLE_NAMES,
|
|
4
|
+
INTERACTIVE_TAG_NAMES,
|
|
5
|
+
isObfuscatedClass,
|
|
6
|
+
TEST_ATTRIBUTE_NAMES,
|
|
7
|
+
TRUSTED_ATTRIBUTE_NAMES
|
|
8
|
+
} from "../../shared/dom-semantics.js";
|
|
1
9
|
async function installSessionTelemetry(options) {
|
|
2
10
|
const STATIC_EXT_RE = /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
|
|
3
11
|
const { context, initialPage, logAction, logNetwork } = options;
|
|
@@ -12,7 +20,9 @@ async function installSessionTelemetry(options) {
|
|
|
12
20
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
13
21
|
const targetId = targetInfo?.targetInfo?.targetId;
|
|
14
22
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
15
|
-
throw new Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Could not resolve target id for page at URL "${page.url()}".`
|
|
25
|
+
);
|
|
16
26
|
}
|
|
17
27
|
pageIdCache.set(page, targetId);
|
|
18
28
|
return targetId;
|
|
@@ -150,6 +160,426 @@ async function installSessionTelemetry(options) {
|
|
|
150
160
|
}
|
|
151
161
|
return locator;
|
|
152
162
|
};
|
|
163
|
+
const createUserDomTrackingInitScript = () => {
|
|
164
|
+
const selectorAttributeNames = [
|
|
165
|
+
"id",
|
|
166
|
+
...TEST_ATTRIBUTE_NAMES,
|
|
167
|
+
"name",
|
|
168
|
+
"for",
|
|
169
|
+
"role",
|
|
170
|
+
"aria-label",
|
|
171
|
+
"title",
|
|
172
|
+
"placeholder",
|
|
173
|
+
"alt",
|
|
174
|
+
"href",
|
|
175
|
+
"type"
|
|
176
|
+
];
|
|
177
|
+
return `
|
|
178
|
+
(() => {
|
|
179
|
+
if (window.__btDomListenersInstalled) return;
|
|
180
|
+
window.__btDomListenersInstalled = true;
|
|
181
|
+
|
|
182
|
+
const TEST_ATTRS = new Set(${JSON.stringify([...TEST_ATTRIBUTE_NAMES])});
|
|
183
|
+
const TRUSTED_ATTRS = new Set(${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])});
|
|
184
|
+
const INTERACTIVE_TAGS = new Set(${JSON.stringify([...INTERACTIVE_TAG_NAMES])});
|
|
185
|
+
const INTERACTIVE_ROLES = new Set(${JSON.stringify([...INTERACTIVE_ROLE_NAMES])});
|
|
186
|
+
const SELECTOR_ATTRS = ${JSON.stringify(selectorAttributeNames)};
|
|
187
|
+
|
|
188
|
+
${filterSemanticClasses.toString()}
|
|
189
|
+
${isObfuscatedClass.toString()}
|
|
190
|
+
|
|
191
|
+
const normalizeWhitespace = (value) =>
|
|
192
|
+
String(value || "").replace(/\\s+/g, " ").trim();
|
|
193
|
+
|
|
194
|
+
const clipText = (value, limit = 120) => {
|
|
195
|
+
const normalized = normalizeWhitespace(value);
|
|
196
|
+
if (!normalized) return "";
|
|
197
|
+
return normalized.length > limit
|
|
198
|
+
? \`\${normalized.slice(0, limit - 1)}\u2026\`
|
|
199
|
+
: normalized;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const cssEscape = (value) => {
|
|
203
|
+
if (globalThis.CSS && typeof globalThis.CSS.escape === "function") {
|
|
204
|
+
return globalThis.CSS.escape(String(value));
|
|
205
|
+
}
|
|
206
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => {
|
|
207
|
+
const hex = char.charCodeAt(0).toString(16);
|
|
208
|
+
return \`\\\\\${hex} \`;
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const quoteAttrValue = (value) =>
|
|
213
|
+
String(value).replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
|
|
214
|
+
|
|
215
|
+
const isElementNode = (value) => value instanceof Element;
|
|
216
|
+
|
|
217
|
+
const isInteractiveElement = (el) => {
|
|
218
|
+
if (!el || !el.tagName) return false;
|
|
219
|
+
const tagName = el.tagName.toLowerCase();
|
|
220
|
+
if (INTERACTIVE_TAGS.has(tagName)) return true;
|
|
221
|
+
if (el.hasAttribute("tabindex") || el.hasAttribute("contenteditable")) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
225
|
+
return Boolean(role) && INTERACTIVE_ROLES.has(role);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const getFilteredClassSelector = (el) => {
|
|
229
|
+
const className =
|
|
230
|
+
typeof el.className === "string"
|
|
231
|
+
? filterSemanticClasses(el.className)
|
|
232
|
+
: "";
|
|
233
|
+
const classes = className
|
|
234
|
+
.split(/\\s+/)
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.slice(0, 2);
|
|
237
|
+
if (classes.length === 0) return "";
|
|
238
|
+
return classes.map((cls) => \`.\${cssEscape(cls)}\`).join("");
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const getMeaningfulAttrValue = (el, attrName) => {
|
|
242
|
+
if (!el.hasAttribute(attrName)) return "";
|
|
243
|
+
const value = clipText(el.getAttribute(attrName) || "", 80);
|
|
244
|
+
if (!value) return "";
|
|
245
|
+
if (attrName === "href" && value.startsWith("javascript:")) return "";
|
|
246
|
+
return value;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const buildSelectorForElement = (el) => {
|
|
250
|
+
if (!isElementNode(el)) return "";
|
|
251
|
+
const tagName = el.tagName.toLowerCase();
|
|
252
|
+
const id = clipText(el.id || "", 80);
|
|
253
|
+
if (id) {
|
|
254
|
+
return \`\${tagName}#\${cssEscape(id)}\`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
258
|
+
if (attrName === "id") continue;
|
|
259
|
+
const value = getMeaningfulAttrValue(el, attrName);
|
|
260
|
+
if (!value) continue;
|
|
261
|
+
if (attrName.startsWith("data-")) {
|
|
262
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
263
|
+
}
|
|
264
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const classSelector = getFilteredClassSelector(el);
|
|
268
|
+
if (classSelector) {
|
|
269
|
+
return \`\${tagName}\${classSelector}\`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return tagName;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const getElementSummary = (el) => {
|
|
276
|
+
if (!isElementNode(el)) return "";
|
|
277
|
+
const selector = buildSelectorForElement(el);
|
|
278
|
+
const role = clipText(el.getAttribute("role") || "", 40);
|
|
279
|
+
const text = getElementText(el);
|
|
280
|
+
const suffix = [];
|
|
281
|
+
if (role) suffix.push(\`role=\${role}\`);
|
|
282
|
+
if (text) suffix.push(\`text="\${text}"\`);
|
|
283
|
+
return suffix.length > 0 ? \`\${selector} [\${suffix.join(", ")}]\` : selector;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const getElementText = (el) => {
|
|
287
|
+
if (!isElementNode(el)) return "";
|
|
288
|
+
const tagName = el.tagName.toLowerCase();
|
|
289
|
+
if (tagName === "input") {
|
|
290
|
+
const inputType = normalizeWhitespace(el.getAttribute("type")).toLowerCase();
|
|
291
|
+
if (inputType === "password") return "";
|
|
292
|
+
const value = clipText(el.value || el.getAttribute("value") || "", 80);
|
|
293
|
+
if (value) return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const attrCandidates = [
|
|
297
|
+
el.getAttribute("aria-label"),
|
|
298
|
+
el.getAttribute("title"),
|
|
299
|
+
el.getAttribute("placeholder"),
|
|
300
|
+
el.getAttribute("alt"),
|
|
301
|
+
el.textContent,
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const candidate of attrCandidates) {
|
|
305
|
+
const text = clipText(candidate || "", 120);
|
|
306
|
+
if (text.length >= 2) return text;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return "";
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const isMeaningfulAncestor = (el) => {
|
|
313
|
+
if (!isElementNode(el)) return false;
|
|
314
|
+
const tagName = el.tagName.toLowerCase();
|
|
315
|
+
if (isInteractiveElement(el)) return true;
|
|
316
|
+
if (
|
|
317
|
+
[
|
|
318
|
+
"tr",
|
|
319
|
+
"li",
|
|
320
|
+
"td",
|
|
321
|
+
"th",
|
|
322
|
+
"label",
|
|
323
|
+
"section",
|
|
324
|
+
"article",
|
|
325
|
+
"dialog",
|
|
326
|
+
"fieldset",
|
|
327
|
+
"summary",
|
|
328
|
+
].includes(tagName)
|
|
329
|
+
) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (el.id) return true;
|
|
333
|
+
for (const attrName of TEST_ATTRS) {
|
|
334
|
+
if (el.hasAttribute(attrName)) return true;
|
|
335
|
+
}
|
|
336
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
337
|
+
return Boolean(role);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const hasStrongSelectorSignal = (el, selector) => {
|
|
341
|
+
if (!isElementNode(el)) return false;
|
|
342
|
+
if (!selector) return false;
|
|
343
|
+
const tagName = el.tagName.toLowerCase();
|
|
344
|
+
if (el.id) return true;
|
|
345
|
+
if (isInteractiveElement(el)) return true;
|
|
346
|
+
if (getFilteredClassSelector(el)) return true;
|
|
347
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
348
|
+
if (attrName === "id") continue;
|
|
349
|
+
if (getMeaningfulAttrValue(el, attrName)) return true;
|
|
350
|
+
}
|
|
351
|
+
return selector !== tagName;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const getAncestorSelectors = (target) => {
|
|
355
|
+
const selectors = [];
|
|
356
|
+
let current = isElementNode(target) ? target : null;
|
|
357
|
+
let depth = 0;
|
|
358
|
+
while (current && depth < 7) {
|
|
359
|
+
const selector = buildSelectorForElement(current);
|
|
360
|
+
if (
|
|
361
|
+
selector &&
|
|
362
|
+
!selectors.includes(selector) &&
|
|
363
|
+
(
|
|
364
|
+
(depth === 0 && hasStrongSelectorSignal(current, selector)) ||
|
|
365
|
+
isMeaningfulAncestor(current)
|
|
366
|
+
)
|
|
367
|
+
) {
|
|
368
|
+
selectors.push(selector);
|
|
369
|
+
}
|
|
370
|
+
current = current.parentElement;
|
|
371
|
+
depth += 1;
|
|
372
|
+
}
|
|
373
|
+
if (selectors.length === 0 && isElementNode(target)) {
|
|
374
|
+
selectors.push(buildSelectorForElement(target));
|
|
375
|
+
}
|
|
376
|
+
return selectors;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const getNearbyText = (target) => {
|
|
380
|
+
let current = isElementNode(target) ? target : null;
|
|
381
|
+
let depth = 0;
|
|
382
|
+
while (current && depth < 6) {
|
|
383
|
+
const text = getElementText(current);
|
|
384
|
+
if (text.length >= 2) return text;
|
|
385
|
+
current = current.parentElement;
|
|
386
|
+
depth += 1;
|
|
387
|
+
}
|
|
388
|
+
return "";
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const getComposedPathSummary = (event) => {
|
|
392
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
393
|
+
return path
|
|
394
|
+
.filter((entry) => isElementNode(entry))
|
|
395
|
+
.slice(0, 6)
|
|
396
|
+
.map((entry) => getElementSummary(entry));
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const getCoordinates = (event) => {
|
|
400
|
+
if (!(event instanceof MouseEvent)) return undefined;
|
|
401
|
+
return {
|
|
402
|
+
x: Math.round(event.clientX),
|
|
403
|
+
y: Math.round(event.clientY),
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const buildActionPayload = (event, action, extra = {}) => {
|
|
408
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
409
|
+
const ancestorSelectors = getAncestorSelectors(target);
|
|
410
|
+
const bestSemanticSelector = ancestorSelectors[0] || "";
|
|
411
|
+
const targetSelector = target ? buildSelectorForElement(target) : "";
|
|
412
|
+
return {
|
|
413
|
+
action,
|
|
414
|
+
bestSemanticSelector: bestSemanticSelector || undefined,
|
|
415
|
+
targetSelector: targetSelector || undefined,
|
|
416
|
+
ancestorSelectors: ancestorSelectors.length > 0 ? ancestorSelectors : undefined,
|
|
417
|
+
nearbyText: getNearbyText(target) || undefined,
|
|
418
|
+
composedPath: getComposedPathSummary(event),
|
|
419
|
+
coordinates: getCoordinates(event),
|
|
420
|
+
success: true,
|
|
421
|
+
...extra,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
let clickTimer = null;
|
|
426
|
+
let pendingClick = null;
|
|
427
|
+
|
|
428
|
+
document.addEventListener(
|
|
429
|
+
"click",
|
|
430
|
+
(event) => {
|
|
431
|
+
if (window.__btApiActionInProgress) return;
|
|
432
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
433
|
+
if (target?.type === "checkbox") {
|
|
434
|
+
window.__btActionLog(
|
|
435
|
+
JSON.stringify(
|
|
436
|
+
buildActionPayload(event, target.checked ? "check" : "uncheck"),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
pendingClick = buildActionPayload(event, "click");
|
|
443
|
+
if (clickTimer) clearTimeout(clickTimer);
|
|
444
|
+
clickTimer = setTimeout(() => {
|
|
445
|
+
if (pendingClick) {
|
|
446
|
+
window.__btActionLog(JSON.stringify(pendingClick));
|
|
447
|
+
}
|
|
448
|
+
pendingClick = null;
|
|
449
|
+
clickTimer = null;
|
|
450
|
+
}, 200);
|
|
451
|
+
},
|
|
452
|
+
true,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
document.addEventListener(
|
|
456
|
+
"dblclick",
|
|
457
|
+
(event) => {
|
|
458
|
+
if (window.__btApiActionInProgress) return;
|
|
459
|
+
if (clickTimer) {
|
|
460
|
+
clearTimeout(clickTimer);
|
|
461
|
+
clickTimer = null;
|
|
462
|
+
pendingClick = null;
|
|
463
|
+
}
|
|
464
|
+
window.__btActionLog(
|
|
465
|
+
JSON.stringify(buildActionPayload(event, "dblclick")),
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
true,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const inputTimers = new WeakMap();
|
|
472
|
+
document.addEventListener(
|
|
473
|
+
"input",
|
|
474
|
+
(event) => {
|
|
475
|
+
if (window.__btApiActionInProgress) return;
|
|
476
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
477
|
+
if (!target) return;
|
|
478
|
+
|
|
479
|
+
if (target.tagName === "SELECT") {
|
|
480
|
+
window.__btActionLog(
|
|
481
|
+
JSON.stringify(
|
|
482
|
+
buildActionPayload(event, "selectOption", {
|
|
483
|
+
value: target.value,
|
|
484
|
+
}),
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const existing = inputTimers.get(target);
|
|
491
|
+
if (existing) clearTimeout(existing);
|
|
492
|
+
inputTimers.set(
|
|
493
|
+
target,
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
inputTimers.delete(target);
|
|
496
|
+
window.__btActionLog(
|
|
497
|
+
JSON.stringify(
|
|
498
|
+
buildActionPayload(event, "fill", {
|
|
499
|
+
value: String(target.value || "").slice(0, 100),
|
|
500
|
+
}),
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
}, 500),
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
true,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const specialKeys = new Set([
|
|
510
|
+
"Enter",
|
|
511
|
+
"Escape",
|
|
512
|
+
"Tab",
|
|
513
|
+
"Backspace",
|
|
514
|
+
"Delete",
|
|
515
|
+
"ArrowUp",
|
|
516
|
+
"ArrowDown",
|
|
517
|
+
"ArrowLeft",
|
|
518
|
+
"ArrowRight",
|
|
519
|
+
"Home",
|
|
520
|
+
"End",
|
|
521
|
+
"PageUp",
|
|
522
|
+
"PageDown",
|
|
523
|
+
"F1",
|
|
524
|
+
"F2",
|
|
525
|
+
"F3",
|
|
526
|
+
"F4",
|
|
527
|
+
"F5",
|
|
528
|
+
"F6",
|
|
529
|
+
"F7",
|
|
530
|
+
"F8",
|
|
531
|
+
"F9",
|
|
532
|
+
"F10",
|
|
533
|
+
"F11",
|
|
534
|
+
"F12",
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
document.addEventListener(
|
|
538
|
+
"keydown",
|
|
539
|
+
(event) => {
|
|
540
|
+
if (window.__btApiActionInProgress) return;
|
|
541
|
+
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
542
|
+
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
543
|
+
const keyDesc =
|
|
544
|
+
(event.ctrlKey ? "Ctrl+" : "") +
|
|
545
|
+
(event.metaKey ? "Meta+" : "") +
|
|
546
|
+
(event.altKey ? "Alt+" : "") +
|
|
547
|
+
(event.shiftKey ? "Shift+" : "") +
|
|
548
|
+
event.key;
|
|
549
|
+
window.__btActionLog(
|
|
550
|
+
JSON.stringify(
|
|
551
|
+
buildActionPayload(event, "press", {
|
|
552
|
+
value: keyDesc,
|
|
553
|
+
}),
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
},
|
|
557
|
+
true,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
let scrollTimer = null;
|
|
561
|
+
document.addEventListener(
|
|
562
|
+
"scroll",
|
|
563
|
+
() => {
|
|
564
|
+
if (window.__btApiActionInProgress) return;
|
|
565
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
566
|
+
scrollTimer = setTimeout(() => {
|
|
567
|
+
scrollTimer = null;
|
|
568
|
+
window.__btActionLog(
|
|
569
|
+
JSON.stringify({
|
|
570
|
+
action: "scroll",
|
|
571
|
+
bestSemanticSelector: "document",
|
|
572
|
+
success: true,
|
|
573
|
+
value: \`y=\${window.scrollY}\`,
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
}, 300);
|
|
577
|
+
},
|
|
578
|
+
true,
|
|
579
|
+
);
|
|
580
|
+
})();
|
|
581
|
+
`;
|
|
582
|
+
};
|
|
153
583
|
const installUserDomTracking = async (page, pageId) => {
|
|
154
584
|
if (exposedPages.has(page)) return;
|
|
155
585
|
exposedPages.add(page);
|
|
@@ -161,178 +591,7 @@ async function installSessionTelemetry(options) {
|
|
|
161
591
|
...parsed
|
|
162
592
|
});
|
|
163
593
|
});
|
|
164
|
-
await page.addInitScript(()
|
|
165
|
-
if (window.__btDomListenersInstalled) return;
|
|
166
|
-
window.__btDomListenersInstalled = true;
|
|
167
|
-
const identify = (el) => {
|
|
168
|
-
if (!el || !el.tagName) return "";
|
|
169
|
-
const testId = el.getAttribute("data-testid");
|
|
170
|
-
if (testId) return `[data-testid="${testId}"]`;
|
|
171
|
-
const role = el.getAttribute("role") || "";
|
|
172
|
-
const id = el.id;
|
|
173
|
-
if (role && id) return `${role}#${id}`;
|
|
174
|
-
const label = el.getAttribute("aria-label") || (el.textContent || "").trim().slice(0, 30) || "";
|
|
175
|
-
if (role && label) return `${role} "${label}"`;
|
|
176
|
-
const tag = el.tagName.toLowerCase();
|
|
177
|
-
const cls = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
|
|
178
|
-
return `${tag}${cls}`;
|
|
179
|
-
};
|
|
180
|
-
let clickTimer = null;
|
|
181
|
-
let pendingClick = null;
|
|
182
|
-
document.addEventListener(
|
|
183
|
-
"click",
|
|
184
|
-
(event) => {
|
|
185
|
-
if (window.__btApiActionInProgress) return;
|
|
186
|
-
const target = event.target;
|
|
187
|
-
const selector = identify(target);
|
|
188
|
-
if (target?.type === "checkbox") {
|
|
189
|
-
window.__btActionLog(
|
|
190
|
-
JSON.stringify({
|
|
191
|
-
action: target.checked ? "check" : "uncheck",
|
|
192
|
-
selector,
|
|
193
|
-
success: true
|
|
194
|
-
})
|
|
195
|
-
);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
pendingClick = { selector };
|
|
199
|
-
if (clickTimer) clearTimeout(clickTimer);
|
|
200
|
-
clickTimer = setTimeout(() => {
|
|
201
|
-
if (pendingClick) {
|
|
202
|
-
window.__btActionLog(
|
|
203
|
-
JSON.stringify({
|
|
204
|
-
action: "click",
|
|
205
|
-
selector: pendingClick.selector,
|
|
206
|
-
success: true
|
|
207
|
-
})
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
pendingClick = null;
|
|
211
|
-
clickTimer = null;
|
|
212
|
-
}, 200);
|
|
213
|
-
},
|
|
214
|
-
true
|
|
215
|
-
);
|
|
216
|
-
document.addEventListener(
|
|
217
|
-
"dblclick",
|
|
218
|
-
(event) => {
|
|
219
|
-
if (window.__btApiActionInProgress) return;
|
|
220
|
-
if (clickTimer) {
|
|
221
|
-
clearTimeout(clickTimer);
|
|
222
|
-
clickTimer = null;
|
|
223
|
-
pendingClick = null;
|
|
224
|
-
}
|
|
225
|
-
const selector = identify(event.target);
|
|
226
|
-
window.__btActionLog(
|
|
227
|
-
JSON.stringify({ action: "dblclick", selector, success: true })
|
|
228
|
-
);
|
|
229
|
-
},
|
|
230
|
-
true
|
|
231
|
-
);
|
|
232
|
-
const inputTimers = /* @__PURE__ */ new WeakMap();
|
|
233
|
-
document.addEventListener(
|
|
234
|
-
"input",
|
|
235
|
-
(event) => {
|
|
236
|
-
if (window.__btApiActionInProgress) return;
|
|
237
|
-
const target = event.target;
|
|
238
|
-
const selector = identify(target);
|
|
239
|
-
if (target.tagName === "SELECT") {
|
|
240
|
-
window.__btActionLog(
|
|
241
|
-
JSON.stringify({
|
|
242
|
-
action: "selectOption",
|
|
243
|
-
selector,
|
|
244
|
-
value: target.value,
|
|
245
|
-
success: true
|
|
246
|
-
})
|
|
247
|
-
);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const existing = inputTimers.get(target);
|
|
251
|
-
if (existing) clearTimeout(existing);
|
|
252
|
-
inputTimers.set(
|
|
253
|
-
target,
|
|
254
|
-
setTimeout(() => {
|
|
255
|
-
inputTimers.delete(target);
|
|
256
|
-
window.__btActionLog(
|
|
257
|
-
JSON.stringify({
|
|
258
|
-
action: "fill",
|
|
259
|
-
selector,
|
|
260
|
-
value: (target.value || "").slice(0, 100),
|
|
261
|
-
success: true
|
|
262
|
-
})
|
|
263
|
-
);
|
|
264
|
-
}, 500)
|
|
265
|
-
);
|
|
266
|
-
},
|
|
267
|
-
true
|
|
268
|
-
);
|
|
269
|
-
const specialKeys = /* @__PURE__ */ new Set([
|
|
270
|
-
"Enter",
|
|
271
|
-
"Escape",
|
|
272
|
-
"Tab",
|
|
273
|
-
"Backspace",
|
|
274
|
-
"Delete",
|
|
275
|
-
"ArrowUp",
|
|
276
|
-
"ArrowDown",
|
|
277
|
-
"ArrowLeft",
|
|
278
|
-
"ArrowRight",
|
|
279
|
-
"Home",
|
|
280
|
-
"End",
|
|
281
|
-
"PageUp",
|
|
282
|
-
"PageDown",
|
|
283
|
-
"F1",
|
|
284
|
-
"F2",
|
|
285
|
-
"F3",
|
|
286
|
-
"F4",
|
|
287
|
-
"F5",
|
|
288
|
-
"F6",
|
|
289
|
-
"F7",
|
|
290
|
-
"F8",
|
|
291
|
-
"F9",
|
|
292
|
-
"F10",
|
|
293
|
-
"F11",
|
|
294
|
-
"F12"
|
|
295
|
-
]);
|
|
296
|
-
document.addEventListener(
|
|
297
|
-
"keydown",
|
|
298
|
-
(event) => {
|
|
299
|
-
if (window.__btApiActionInProgress) return;
|
|
300
|
-
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
301
|
-
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
302
|
-
const selector = identify(event.target);
|
|
303
|
-
const keyDesc = (event.ctrlKey ? "Ctrl+" : "") + (event.metaKey ? "Meta+" : "") + (event.altKey ? "Alt+" : "") + (event.shiftKey ? "Shift+" : "") + event.key;
|
|
304
|
-
window.__btActionLog(
|
|
305
|
-
JSON.stringify({
|
|
306
|
-
action: "press",
|
|
307
|
-
selector,
|
|
308
|
-
value: keyDesc,
|
|
309
|
-
success: true
|
|
310
|
-
})
|
|
311
|
-
);
|
|
312
|
-
},
|
|
313
|
-
true
|
|
314
|
-
);
|
|
315
|
-
let scrollTimer = null;
|
|
316
|
-
document.addEventListener(
|
|
317
|
-
"scroll",
|
|
318
|
-
() => {
|
|
319
|
-
if (window.__btApiActionInProgress) return;
|
|
320
|
-
if (scrollTimer) clearTimeout(scrollTimer);
|
|
321
|
-
scrollTimer = setTimeout(() => {
|
|
322
|
-
scrollTimer = null;
|
|
323
|
-
window.__btActionLog(
|
|
324
|
-
JSON.stringify({
|
|
325
|
-
action: "scroll",
|
|
326
|
-
selector: "document",
|
|
327
|
-
value: `y=${window.scrollY}`,
|
|
328
|
-
success: true
|
|
329
|
-
})
|
|
330
|
-
);
|
|
331
|
-
}, 300);
|
|
332
|
-
},
|
|
333
|
-
true
|
|
334
|
-
);
|
|
335
|
-
});
|
|
594
|
+
await page.addInitScript({ content: createUserDomTrackingInitScript() });
|
|
336
595
|
};
|
|
337
596
|
const wrapPageActions = (page, pageId) => {
|
|
338
597
|
if (wrappedPages.has(page)) return;
|
|
@@ -439,7 +698,8 @@ async function installSessionTelemetry(options) {
|
|
|
439
698
|
page.on("response", async (response) => {
|
|
440
699
|
const request = response.request();
|
|
441
700
|
const url = request.url();
|
|
442
|
-
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
701
|
+
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
702
|
+
return;
|
|
443
703
|
emitNetwork({
|
|
444
704
|
pageId,
|
|
445
705
|
method: request.method(),
|
package/dist/cli/core/session.js
CHANGED
|
@@ -18,9 +18,16 @@ import {
|
|
|
18
18
|
serializeSessionState
|
|
19
19
|
} from "../../shared/state/index.js";
|
|
20
20
|
const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
21
|
-
const SESSION_DEFAULT = "default";
|
|
22
21
|
const SESSION_DEV_SERVER = "dev-server";
|
|
23
22
|
const SESSION_BROWSER_AGENT = "browser-agent";
|
|
23
|
+
function generateSessionName() {
|
|
24
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
25
|
+
let id = "";
|
|
26
|
+
for (let i = 0; i < 4; i++) {
|
|
27
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
28
|
+
}
|
|
29
|
+
return `ses-${id}`;
|
|
30
|
+
}
|
|
24
31
|
function logFileForSession(session) {
|
|
25
32
|
validateSessionName(session);
|
|
26
33
|
const dir = getSessionDir(session);
|
|
@@ -108,7 +115,10 @@ function readSessionStateOrThrow(session) {
|
|
|
108
115
|
throwSessionNotFoundError(session);
|
|
109
116
|
}
|
|
110
117
|
try {
|
|
111
|
-
return parseSessionStateContent(
|
|
118
|
+
return parseSessionStateContent(
|
|
119
|
+
readFileSync(stateFile, "utf-8"),
|
|
120
|
+
stateFile
|
|
121
|
+
);
|
|
112
122
|
} catch (err) {
|
|
113
123
|
throw new Error(
|
|
114
124
|
`Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -147,15 +157,18 @@ function setSessionStatus(session, status, logger) {
|
|
|
147
157
|
const state = readSessionState(session, logger);
|
|
148
158
|
if (!state) return;
|
|
149
159
|
if (state.status === status) return;
|
|
150
|
-
writeSessionState(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
writeSessionState(
|
|
161
|
+
{
|
|
162
|
+
...state,
|
|
163
|
+
status
|
|
164
|
+
},
|
|
165
|
+
logger
|
|
166
|
+
);
|
|
154
167
|
}
|
|
155
168
|
function assertSessionAvailableForStart(session, logger) {
|
|
156
169
|
const existingState = readSessionState(session, logger);
|
|
157
170
|
if (!existingState) return;
|
|
158
|
-
if (!isPidRunning(existingState.pid)) {
|
|
171
|
+
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
159
172
|
setSessionStatus(session, "exited", logger);
|
|
160
173
|
return;
|
|
161
174
|
}
|
|
@@ -166,12 +179,12 @@ function assertSessionAvailableForStart(session, logger) {
|
|
|
166
179
|
}
|
|
167
180
|
export {
|
|
168
181
|
SESSION_BROWSER_AGENT,
|
|
169
|
-
SESSION_DEFAULT,
|
|
170
182
|
SESSION_DEV_SERVER,
|
|
171
183
|
SESSION_STATE_VERSION,
|
|
172
184
|
assertSessionAvailableForStart,
|
|
173
185
|
assertSessionStateExistsOrThrow,
|
|
174
186
|
clearSessionState,
|
|
187
|
+
generateSessionName,
|
|
175
188
|
getStateFilePath,
|
|
176
189
|
listSessionsWithStateFile,
|
|
177
190
|
logFileForSession,
|