@detiq/crawler 1.1.8 → 1.1.9

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.
Files changed (82) hide show
  1. package/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.js +266 -249
  22. package/dist/extractors/artifact-envelope.js +58 -59
  23. package/dist/failure-analysis-agent.js +33 -37
  24. package/dist/feature-flag-detector.js +66 -51
  25. package/dist/form-detector.js +75 -55
  26. package/dist/frontier.js +101 -76
  27. package/dist/gap-pass.js +89 -68
  28. package/dist/graphql-detector.js +72 -62
  29. package/dist/http-engine.js +330 -282
  30. package/dist/incremental-diff.js +87 -54
  31. package/dist/index.js +28 -67
  32. package/dist/input-fuzzer.js +97 -93
  33. package/dist/interactive-probe.js +827 -715
  34. package/dist/job-queue.js +2169 -2151
  35. package/dist/js-coverage.js +56 -52
  36. package/dist/js-router-detector.js +117 -86
  37. package/dist/language-detector.js +63 -51
  38. package/dist/link-classifier.js +50 -32
  39. package/dist/live-view.js +108 -91
  40. package/dist/locator-extractor.js +187 -151
  41. package/dist/logger.js +7 -9
  42. package/dist/markdown-extract.js +61 -48
  43. package/dist/native-crawler.js +946 -780
  44. package/dist/network-observer.js +156 -135
  45. package/dist/network-throttle.js +40 -29
  46. package/dist/openapi-generator.js +92 -77
  47. package/dist/pdf-extractor.js +90 -72
  48. package/dist/performance-metrics.js +102 -80
  49. package/dist/phash.js +144 -125
  50. package/dist/pii-scrubber.js +68 -65
  51. package/dist/playwright-recorder.js +58 -70
  52. package/dist/policies/crawl-safety.js +34 -31
  53. package/dist/profiles/domain-profile.js +125 -130
  54. package/dist/proxy.js +172 -146
  55. package/dist/rate-limiter.js +65 -58
  56. package/dist/redirect-tracker.js +64 -60
  57. package/dist/redis-frontier.js +136 -116
  58. package/dist/redis-queue.js +144 -157
  59. package/dist/response-cache.js +140 -127
  60. package/dist/responsive-detector.js +46 -42
  61. package/dist/retry-policy.js +50 -46
  62. package/dist/robots-txt.js +83 -69
  63. package/dist/screen-reader.js +70 -45
  64. package/dist/screenshot-masker.js +77 -59
  65. package/dist/seo-checker.js +107 -88
  66. package/dist/session-capture.js +212 -192
  67. package/dist/session-export.js +133 -107
  68. package/dist/sitemap-writer.js +41 -38
  69. package/dist/sitemap.js +202 -141
  70. package/dist/skyvern.js +148 -123
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +90 -74
  73. package/dist/stealth/human-behavior.js +173 -156
  74. package/dist/telemetry.js +82 -68
  75. package/dist/test-executor.js +335 -309
  76. package/dist/tls-fingerprint.js +88 -77
  77. package/dist/url-pattern-completer.js +90 -71
  78. package/dist/visual-diff-verdict.js +38 -23
  79. package/dist/webhook-notifier.js +69 -71
  80. package/dist/websocket-detector.js +68 -56
  81. package/dist/worker.js +38 -29
  82. package/package.json +1 -1
package/dist/live-view.js CHANGED
@@ -1,101 +1,118 @@
1
- import { chromium } from "playwright";
2
- import { randomBytes } from "node:crypto";
3
- import { logger } from "./logger.js";
4
- const sessions = /* @__PURE__ */ new Map();
5
- const SESSION_TTL_MS = 15 * 60 * 1e3;
1
+ import { chromium } from 'playwright';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { logger } from './logger.js';
4
+ const sessions = new Map();
5
+ const SESSION_TTL_MS = 15 * 60 * 1_000; // 15 min
6
6
  function generateId() {
7
- return randomBytes(16).toString("hex");
7
+ return randomBytes(16).toString('hex');
8
8
  }
9
- async function startLiveView(appUrl, storageState) {
10
- const id = generateId();
11
- const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.platform === "linux";
12
- const browser = await chromium.launch({
13
- headless: true,
14
- args: isDocker ? ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"] : []
15
- });
16
- const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
17
- if (storageState) {
18
- try {
19
- const state = JSON.parse(storageState);
20
- if (state.cookies?.length) await context.addCookies(state.cookies);
21
- } catch {
9
+ /**
10
+ * Start a live view session: launches a headless browser, navigates to appUrl,
11
+ * begins capturing frames. Returns the session ID.
12
+ */
13
+ export async function startLiveView(appUrl, storageState) {
14
+ const id = generateId();
15
+ const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.platform === 'linux';
16
+ const browser = await chromium.launch({
17
+ headless: true,
18
+ args: isDocker ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] : [],
19
+ });
20
+ const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
21
+ // Restore session if provided
22
+ if (storageState) {
23
+ try {
24
+ const state = JSON.parse(storageState);
25
+ if (state.cookies?.length)
26
+ await context.addCookies(state.cookies);
27
+ }
28
+ catch { /* ignore */ }
22
29
  }
23
- }
24
- const page = await context.newPage();
25
- await page.goto(appUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
26
- const session = {
27
- id,
28
- browser,
29
- context,
30
- page,
31
- appUrl,
32
- lastFrame: null,
33
- streaming: true,
34
- frameInterval: null,
35
- createdAt: Date.now()
36
- };
37
- session.frameInterval = setInterval(async () => {
38
- if (!session.streaming) return;
39
- try {
40
- const buf = await page.screenshot({ type: "jpeg", quality: 60, fullPage: false });
41
- session.lastFrame = buf.toString("base64");
42
- } catch {
43
- session.streaming = false;
44
- }
45
- }, 500);
46
- sessions.set(id, session);
47
- setTimeout(() => closeLiveView(id).catch(() => {
48
- }), SESSION_TTL_MS);
49
- logger.info({ id, appUrl }, "[live-view] Session started");
50
- return id;
30
+ const page = await context.newPage();
31
+ await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
32
+ const session = {
33
+ id, browser, context, page, appUrl,
34
+ lastFrame: null, streaming: true, frameInterval: null,
35
+ createdAt: Date.now(),
36
+ };
37
+ // Start capturing frames (every 500ms)
38
+ session.frameInterval = setInterval(async () => {
39
+ if (!session.streaming)
40
+ return;
41
+ try {
42
+ const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false });
43
+ session.lastFrame = buf.toString('base64');
44
+ }
45
+ catch {
46
+ session.streaming = false;
47
+ }
48
+ }, 500);
49
+ sessions.set(id, session);
50
+ // Auto-cleanup after TTL
51
+ setTimeout(() => closeLiveView(id).catch(() => { }), SESSION_TTL_MS);
52
+ logger.info({ id, appUrl }, '[live-view] Session started');
53
+ return id;
51
54
  }
52
- function getFrame(id) {
53
- return sessions.get(id)?.lastFrame ?? null;
55
+ /**
56
+ * Get the latest frame for a session (base64 JPEG).
57
+ */
58
+ export function getFrame(id) {
59
+ return sessions.get(id)?.lastFrame ?? null;
54
60
  }
55
- async function liveViewNavigate(id, url) {
56
- const session = sessions.get(id);
57
- if (!session) throw new Error(`Live view session ${id} not found`);
58
- await session.page.goto(url, { waitUntil: "domcontentloaded" });
61
+ /**
62
+ * Navigate the live view browser to a URL.
63
+ */
64
+ export async function liveViewNavigate(id, url) {
65
+ const session = sessions.get(id);
66
+ if (!session)
67
+ throw new Error(`Live view session ${id} not found`);
68
+ await session.page.goto(url, { waitUntil: 'domcontentloaded' });
59
69
  }
60
- async function completeLiveView(id) {
61
- const session = sessions.get(id);
62
- if (!session) throw new Error(`Live view session ${id} not found or expired`);
63
- const state = await session.context.storageState();
64
- const json = JSON.stringify(state);
65
- await closeLiveView(id);
66
- return json;
70
+ /**
71
+ * Capture storageState from the live view session, close the browser, return JSON string.
72
+ */
73
+ export async function completeLiveView(id) {
74
+ const session = sessions.get(id);
75
+ if (!session)
76
+ throw new Error(`Live view session ${id} not found or expired`);
77
+ const state = await session.context.storageState();
78
+ const json = JSON.stringify(state);
79
+ await closeLiveView(id);
80
+ return json;
67
81
  }
68
- async function sendInput(id, event) {
69
- const session = sessions.get(id);
70
- if (!session) throw new Error(`Live view session ${id} not found`);
71
- if (event.type === "click" && event.x !== void 0 && event.y !== void 0) {
72
- await session.page.mouse.click(event.x, event.y, { button: event.button ?? "left" });
73
- } else if (event.type === "type" && event.text) {
74
- await session.page.keyboard.type(event.text);
75
- } else if (event.type === "key" && event.key) {
76
- await session.page.keyboard.press(event.key);
77
- }
82
+ /**
83
+ * Send a mouse or keyboard input event to the live view browser.
84
+ */
85
+ export async function sendInput(id, event) {
86
+ const session = sessions.get(id);
87
+ if (!session)
88
+ throw new Error(`Live view session ${id} not found`);
89
+ if (event.type === 'click' && event.x !== undefined && event.y !== undefined) {
90
+ await session.page.mouse.click(event.x, event.y, { button: event.button ?? 'left' });
91
+ }
92
+ else if (event.type === 'type' && event.text) {
93
+ await session.page.keyboard.type(event.text);
94
+ }
95
+ else if (event.type === 'key' && event.key) {
96
+ await session.page.keyboard.press(event.key);
97
+ }
78
98
  }
79
- async function closeLiveView(id) {
80
- const session = sessions.get(id);
81
- if (!session) return;
82
- session.streaming = false;
83
- if (session.frameInterval) clearInterval(session.frameInterval);
84
- sessions.delete(id);
85
- await session.browser.close().catch(() => {
86
- });
87
- logger.info({ id }, "[live-view] Session closed");
99
+ /**
100
+ * Close a live view session without capturing.
101
+ */
102
+ export async function closeLiveView(id) {
103
+ const session = sessions.get(id);
104
+ if (!session)
105
+ return;
106
+ session.streaming = false;
107
+ if (session.frameInterval)
108
+ clearInterval(session.frameInterval);
109
+ sessions.delete(id);
110
+ await session.browser.close().catch(() => { });
111
+ logger.info({ id }, '[live-view] Session closed');
88
112
  }
89
- function listLiveViews() {
90
- return [...sessions.keys()];
113
+ /**
114
+ * List all active session IDs (for health checks / cleanup).
115
+ */
116
+ export function listLiveViews() {
117
+ return [...sessions.keys()];
91
118
  }
92
- export {
93
- closeLiveView,
94
- completeLiveView,
95
- getFrame,
96
- listLiveViews,
97
- liveViewNavigate,
98
- sendInput,
99
- startLiveView
100
- };
101
- //# sourceMappingURL=live-view.js.map
@@ -1,163 +1,199 @@
1
+ /**
2
+ * Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.
3
+ * Priority: data-testid → aria-label/placeholder (inputs) → role+name (buttons/links) → heading → text → id
4
+ */
5
+ /** Parse the notes JSON string stored on Element records. */
1
6
  function parseNotes(notes) {
2
- if (!notes) return {};
3
- try {
4
- const parsed = typeof notes === "string" ? JSON.parse(notes) : notes;
5
- if (parsed && typeof parsed === "object") return parsed;
6
- } catch {
7
- }
8
- return {};
7
+ if (!notes)
8
+ return {};
9
+ try {
10
+ const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;
11
+ if (parsed && typeof parsed === 'object')
12
+ return parsed;
13
+ }
14
+ catch { }
15
+ return {};
9
16
  }
17
+ /** Map crawler role strings to Playwright role values. */
10
18
  function mapRole(role) {
11
- if (!role) return null;
12
- const r = role.toLowerCase().trim();
13
- if (r === "button" || r === "submit" || r === "reset") return "button";
14
- if (r === "link" || r === "a" || r === "anchor") return "link";
15
- if (r === "textbox" || r === "input" || r === "text" || r === "email" || r === "password" || r === "tel" || r === "number" || r === "search") return "textbox";
16
- if (r === "combobox" || r === "select" || r === "dropdown") return "combobox";
17
- if (r === "checkbox") return "checkbox";
18
- if (r === "radio") return "radio";
19
- if (r === "heading" || r === "h1" || r === "h2" || r === "h3" || r === "h4" || r === "h5" || r === "h6") return "heading";
20
- if (r === "img" || r === "image") return "img";
21
- if (r === "nav" || r === "navigation") return "navigation";
22
- return r;
19
+ if (!role)
20
+ return null;
21
+ const r = role.toLowerCase().trim();
22
+ if (r === 'button' || r === 'submit' || r === 'reset')
23
+ return 'button';
24
+ if (r === 'link' || r === 'a' || r === 'anchor')
25
+ return 'link';
26
+ if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search')
27
+ return 'textbox';
28
+ if (r === 'combobox' || r === 'select' || r === 'dropdown')
29
+ return 'combobox';
30
+ if (r === 'checkbox')
31
+ return 'checkbox';
32
+ if (r === 'radio')
33
+ return 'radio';
34
+ if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6')
35
+ return 'heading';
36
+ if (r === 'img' || r === 'image')
37
+ return 'img';
38
+ if (r === 'nav' || r === 'navigation')
39
+ return 'navigation';
40
+ return r;
23
41
  }
42
+ /** Convert "Submit Form Button" → "submitFormButton" */
24
43
  function toKey(text) {
25
- return text.replace(/[^a-zA-Z0-9\s]/g, " ").replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase()).replace(/\s/g, "").slice(0, 40) || "element";
44
+ return text
45
+ .replace(/[^a-zA-Z0-9\s]/g, ' ')
46
+ .replace(/\s+(.)/g, (_, c) => c.toUpperCase())
47
+ .replace(/^(.)/, (_, c) => c.toLowerCase())
48
+ .replace(/\s/g, '')
49
+ .slice(0, 40)
50
+ || 'element';
26
51
  }
27
- function elementToPlaywrightLocator(element) {
28
- const attrs = parseNotes(element.notes);
29
- const testId = attrs["data-testid"];
30
- const ariaLabel = attrs["aria-label"] || element.meaning;
31
- const id = attrs["id"];
32
- const placeholder = attrs["placeholder"];
33
- const name = attrs["name"];
34
- const text = (element.text ?? "").trim();
35
- const meaning = (element.meaning ?? "").trim();
36
- const role = mapRole(element.role);
37
- const label = meaning || text || testId || ariaLabel || placeholder || id || name || "element";
38
- const key = toKey(label);
39
- if (testId) {
40
- return {
41
- key,
42
- expression: `page.getByTestId(${JSON.stringify(testId)})`,
43
- elementType: role ?? "element",
44
- confidence: "high",
45
- source: "testid",
46
- description: meaning || testId
47
- };
48
- }
49
- if (role === "textbox" || role === "combobox") {
50
- if (ariaLabel) {
51
- return {
52
- key,
53
- expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
54
- elementType: "input",
55
- confidence: "high",
56
- source: "label",
57
- description: ariaLabel
58
- };
52
+ /** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
53
+ export function elementToPlaywrightLocator(element) {
54
+ const attrs = parseNotes(element.notes);
55
+ const testId = attrs['data-testid'];
56
+ const ariaLabel = attrs['aria-label'] || element.meaning;
57
+ const id = attrs['id'];
58
+ const placeholder = attrs['placeholder'];
59
+ const name = attrs['name'];
60
+ const text = (element.text ?? '').trim();
61
+ const meaning = (element.meaning ?? '').trim();
62
+ const role = mapRole(element.role);
63
+ const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';
64
+ const key = toKey(label);
65
+ // Priority 1: data-testid
66
+ if (testId) {
67
+ return {
68
+ key,
69
+ expression: `page.getByTestId(${JSON.stringify(testId)})`,
70
+ elementType: role ?? 'element',
71
+ confidence: 'high',
72
+ source: 'testid',
73
+ description: meaning || testId,
74
+ };
75
+ }
76
+ // Priority 2: input/combobox/textarea with label → getByLabel; with placeholder → getByPlaceholder
77
+ if (role === 'textbox' || role === 'combobox') {
78
+ if (ariaLabel) {
79
+ return {
80
+ key,
81
+ expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
82
+ elementType: 'input',
83
+ confidence: 'high',
84
+ source: 'label',
85
+ description: ariaLabel,
86
+ };
87
+ }
88
+ if (placeholder) {
89
+ return {
90
+ key,
91
+ expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,
92
+ elementType: 'input',
93
+ confidence: 'high',
94
+ source: 'placeholder',
95
+ description: placeholder,
96
+ };
97
+ }
98
+ }
99
+ // Priority 3: checkbox/radio → getByLabel
100
+ if ((role === 'checkbox' || role === 'radio') && ariaLabel) {
101
+ return {
102
+ key,
103
+ expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
104
+ elementType: role,
105
+ confidence: 'high',
106
+ source: 'label',
107
+ description: ariaLabel,
108
+ };
109
+ }
110
+ // Priority 4: button/link with meaningful name → getByRole
111
+ if ((role === 'button' || role === 'link') && meaning) {
112
+ return {
113
+ key,
114
+ expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,
115
+ elementType: role,
116
+ confidence: 'high',
117
+ source: 'role-name',
118
+ description: meaning,
119
+ };
120
+ }
121
+ // Priority 5: heading → getByRole heading
122
+ if (role === 'heading' && (meaning || text)) {
123
+ const name2 = meaning || text;
124
+ return {
125
+ key,
126
+ expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,
127
+ elementType: 'heading',
128
+ confidence: 'medium',
129
+ source: 'heading',
130
+ description: name2,
131
+ };
132
+ }
133
+ // Priority 6: existing CSS selector from crawler
134
+ if (element.selector && element.selector.length > 0) {
135
+ return {
136
+ key,
137
+ expression: `page.locator(${JSON.stringify(element.selector)})`,
138
+ elementType: role ?? 'element',
139
+ confidence: 'medium',
140
+ source: 'id',
141
+ description: meaning || text || element.selector,
142
+ };
143
+ }
144
+ // Priority 7: id from attrs
145
+ if (id) {
146
+ return {
147
+ key,
148
+ expression: `page.locator(${JSON.stringify('#' + id)})`,
149
+ elementType: role ?? 'element',
150
+ confidence: 'medium',
151
+ source: 'id',
152
+ description: meaning || id,
153
+ };
59
154
  }
60
- if (placeholder) {
61
- return {
62
- key,
63
- expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,
64
- elementType: "input",
65
- confidence: "high",
66
- source: "placeholder",
67
- description: placeholder
68
- };
155
+ // Priority 8: fallback to text content
156
+ if (meaning || text) {
157
+ const t = meaning || text;
158
+ return {
159
+ key,
160
+ expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,
161
+ elementType: role ?? 'text',
162
+ confidence: 'low',
163
+ source: 'text',
164
+ description: t,
165
+ };
69
166
  }
70
- }
71
- if ((role === "checkbox" || role === "radio") && ariaLabel) {
72
- return {
73
- key,
74
- expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
75
- elementType: role,
76
- confidence: "high",
77
- source: "label",
78
- description: ariaLabel
79
- };
80
- }
81
- if ((role === "button" || role === "link") && meaning) {
82
- return {
83
- key,
84
- expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,
85
- elementType: role,
86
- confidence: "high",
87
- source: "role-name",
88
- description: meaning
89
- };
90
- }
91
- if (role === "heading" && (meaning || text)) {
92
- const name2 = meaning || text;
93
- return {
94
- key,
95
- expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,
96
- elementType: "heading",
97
- confidence: "medium",
98
- source: "heading",
99
- description: name2
100
- };
101
- }
102
- if (element.selector && element.selector.length > 0) {
103
- return {
104
- key,
105
- expression: `page.locator(${JSON.stringify(element.selector)})`,
106
- elementType: role ?? "element",
107
- confidence: "medium",
108
- source: "id",
109
- description: meaning || text || element.selector
110
- };
111
- }
112
- if (id) {
113
- return {
114
- key,
115
- expression: `page.locator(${JSON.stringify("#" + id)})`,
116
- elementType: role ?? "element",
117
- confidence: "medium",
118
- source: "id",
119
- description: meaning || id
120
- };
121
- }
122
- if (meaning || text) {
123
- const t = meaning || text;
124
- return {
125
- key,
126
- expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,
127
- elementType: role ?? "text",
128
- confidence: "low",
129
- source: "text",
130
- description: t
131
- };
132
- }
133
- return null;
167
+ return null;
134
168
  }
135
- function elementsToPlaywrightLocators(elements) {
136
- const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
137
- const byKey = /* @__PURE__ */ new Map();
138
- for (const el of elements) {
139
- const loc = elementToPlaywrightLocator(el);
140
- if (!loc) continue;
141
- const existing = byKey.get(loc.key);
142
- if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {
143
- byKey.set(loc.key, loc);
169
+ /**
170
+ * Convert an array of DB Element records to deduplicated Playwright locators.
171
+ * Deduplicates by key — higher confidence wins.
172
+ */
173
+ export function elementsToPlaywrightLocators(elements) {
174
+ const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
175
+ const byKey = new Map();
176
+ for (const el of elements) {
177
+ const loc = elementToPlaywrightLocator(el);
178
+ if (!loc)
179
+ continue;
180
+ const existing = byKey.get(loc.key);
181
+ if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {
182
+ byKey.set(loc.key, loc);
183
+ }
144
184
  }
145
- }
146
- return Array.from(byKey.values());
185
+ return Array.from(byKey.values());
147
186
  }
148
- function formatLocatorsForPrompt(locators) {
149
- if (locators.length === 0) return "";
150
- const lines = locators.map(
151
- (l) => ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`
152
- );
153
- return [
154
- "EXTRACTED PAGE LOCATORS (use these EXACTLY \u2014 do not invent other selectors; these were verified against the live page):",
155
- ...lines
156
- ].join("\n");
187
+ /**
188
+ * Format locators as a block for injection into LLM script generation prompts.
189
+ * Returns empty string if no locators.
190
+ */
191
+ export function formatLocatorsForPrompt(locators) {
192
+ if (locators.length === 0)
193
+ return '';
194
+ const lines = locators.map(l => ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`);
195
+ return [
196
+ 'EXTRACTED PAGE LOCATORS (use these EXACTLY — do not invent other selectors; these were verified against the live page):',
197
+ ...lines,
198
+ ].join('\n');
157
199
  }
158
- export {
159
- elementToPlaywrightLocator,
160
- elementsToPlaywrightLocators,
161
- formatLocatorsForPrompt
162
- };
163
- //# sourceMappingURL=locator-extractor.js.map
package/dist/logger.js CHANGED
@@ -1,10 +1,8 @@
1
- import pino from "pino";
2
- const logger = pino({
3
- level: process.env.LOG_LEVEL ?? "info",
4
- ...process.env.NODE_ENV === "development" ? { transport: { target: "pino-pretty", options: { colorize: true } } } : {},
5
- base: { service: "crawler" }
1
+ import pino from 'pino';
2
+ export const logger = pino({
3
+ level: process.env.LOG_LEVEL ?? 'info',
4
+ ...(process.env.NODE_ENV === 'development'
5
+ ? { transport: { target: 'pino-pretty', options: { colorize: true } } }
6
+ : {}),
7
+ base: { service: 'crawler' },
6
8
  });
7
- export {
8
- logger
9
- };
10
- //# sourceMappingURL=logger.js.map