lobster-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -269
- package/dist/browser/chrome-attach.js +102 -0
- package/dist/browser/chrome-attach.js.map +1 -0
- package/dist/browser/dom/compact-snapshot.js +162 -0
- package/dist/browser/dom/compact-snapshot.js.map +1 -0
- package/dist/browser/dom/index.js +160 -0
- package/dist/browser/dom/index.js.map +1 -1
- package/dist/browser/index.js +907 -70
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/manager.js +443 -11
- package/dist/browser/manager.js.map +1 -1
- package/dist/browser/page-adapter.js +370 -1
- package/dist/browser/page-adapter.js.map +1 -1
- package/dist/browser/profiles.js +238 -0
- package/dist/browser/profiles.js.map +1 -0
- package/dist/browser/semantic-find.js +152 -0
- package/dist/browser/semantic-find.js.map +1 -0
- package/dist/browser/stealth.js +187 -0
- package/dist/browser/stealth.js.map +1 -0
- package/dist/config/index.js +8 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.js +8 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/domain-guard.js +103 -0
- package/dist/domain-guard.js.map +1 -0
- package/dist/index.js +851 -48
- package/dist/index.js.map +1 -1
- package/dist/lib.js +1141 -244
- package/dist/lib.js.map +1 -1
- package/dist/router/index.js +862 -61
- package/dist/router/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/browser/chrome-attach.ts","../../src/utils/logger.ts"],"sourcesContent":["/**\n * Chrome Attach — discover and connect to a running Chrome instance.\n *\n * Probes common debug ports for Chrome's /json/version endpoint\n * and returns the WebSocket debugger URL for Puppeteer.connect().\n *\n * Inspired by PinchTab's attach mode, built from scratch.\n */\n\nimport http from 'node:http';\nimport { log } from '../utils/logger.js';\n\nexport interface ChromeDiscoveryResult {\n wsEndpoint: string;\n port: number;\n version: string;\n browser: string;\n}\n\nconst DEFAULT_PORTS = [9222, 9229, 9333, 9515];\nconst PROBE_TIMEOUT = 1500; // ms\n\n/**\n * Probe a single port for Chrome's DevTools endpoint.\n */\nfunction probePort(port: number): Promise<ChromeDiscoveryResult | null> {\n return new Promise((resolve) => {\n const req = http.get(`http://127.0.0.1:${port}/json/version`, {\n timeout: PROBE_TIMEOUT,\n }, (res) => {\n let data = '';\n res.on('data', (chunk: string) => { data += chunk; });\n res.on('end', () => {\n try {\n const info = JSON.parse(data);\n if (info.webSocketDebuggerUrl) {\n resolve({\n wsEndpoint: info.webSocketDebuggerUrl,\n port,\n version: info['Protocol-Version'] || '',\n browser: info.Browser || '',\n });\n } else {\n resolve(null);\n }\n } catch {\n resolve(null);\n }\n });\n });\n\n req.on('error', () => resolve(null));\n req.on('timeout', () => { req.destroy(); resolve(null); });\n });\n}\n\n/**\n * Discover a running Chrome instance by probing common debug ports.\n * Returns the first responding instance, or null if none found.\n */\nexport async function discoverChrome(ports?: number[]): Promise<ChromeDiscoveryResult | null> {\n const portsToCheck = ports || DEFAULT_PORTS;\n log.debug(`Scanning ports for Chrome: ${portsToCheck.join(', ')}`);\n\n // Probe all ports in parallel for speed\n const results = await Promise.all(portsToCheck.map(probePort));\n const found = results.find(Boolean) || null;\n\n if (found) {\n log.info(`Found Chrome on port ${found.port}: ${found.browser}`);\n } else {\n log.debug('No running Chrome instance found on debug ports.');\n }\n\n return found;\n}\n\n/**\n * Get WebSocket debugger URL from a specific port.\n */\nexport async function getWebSocketDebuggerUrl(port: number): Promise<string | null> {\n const result = await probePort(port);\n return result?.wsEndpoint || null;\n}\n\n/**\n * Parse an attach target — could be:\n * - \"true\" / true → auto-discover\n * - \"ws://...\" → explicit WebSocket URL\n * - \"9222\" → specific port number\n */\nexport async function resolveAttachTarget(target: boolean | string): Promise<string> {\n if (target === true || target === 'true') {\n const result = await discoverChrome();\n if (!result) {\n throw new Error(\n 'No running Chrome found. Start Chrome with:\\n' +\n ' google-chrome --remote-debugging-port=9222\\n' +\n ' # or on Mac:\\n' +\n ' /Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --remote-debugging-port=9222'\n );\n }\n return result.wsEndpoint;\n }\n\n if (typeof target === 'string') {\n // Explicit WebSocket URL\n if (target.startsWith('ws://') || target.startsWith('wss://')) {\n return target;\n }\n\n // Port number\n const port = parseInt(target, 10);\n if (!isNaN(port) && port > 0 && port < 65536) {\n const url = await getWebSocketDebuggerUrl(port);\n if (!url) {\n throw new Error(`No Chrome found on port ${port}. Make sure Chrome is running with --remote-debugging-port=${port}`);\n }\n return url;\n }\n\n throw new Error(`Invalid attach target: \"${target}\". Use \"true\" for auto-discover, a port number, or a ws:// URL.`);\n }\n\n throw new Error('Invalid attach target.');\n}\n","import chalk from 'chalk';\n\nexport const log = {\n info: (msg: string) => console.log(chalk.blue('ℹ'), msg),\n success: (msg: string) => console.log(chalk.green('✓'), msg),\n warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),\n error: (msg: string) => console.error(chalk.red('✗'), msg),\n debug: (msg: string) => {\n if (process.env.LOBSTER_DEBUG) console.log(chalk.gray('⋯'), msg);\n },\n step: (n: number, msg: string) => console.log(chalk.cyan(`[${n}]`), msg),\n dim: (msg: string) => console.log(chalk.dim(msg)),\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/browser/dom/compact-snapshot.ts
|
|
2
|
+
var COMPACT_SNAPSHOT_SCRIPT = `
|
|
3
|
+
(() => {
|
|
4
|
+
const TOKEN_BUDGET = 800;
|
|
5
|
+
const CHARS_PER_TOKEN = 4;
|
|
6
|
+
|
|
7
|
+
const INTERACTIVE_TAGS = new Set([
|
|
8
|
+
'a','button','input','select','textarea','details','summary','label',
|
|
9
|
+
]);
|
|
10
|
+
const INTERACTIVE_ROLES = new Set([
|
|
11
|
+
'button','link','textbox','checkbox','radio','combobox','listbox',
|
|
12
|
+
'menu','menuitem','tab','switch','slider','searchbox','spinbutton',
|
|
13
|
+
'option','menuitemcheckbox','menuitemradio','treeitem',
|
|
14
|
+
]);
|
|
15
|
+
const LANDMARK_TAGS = new Map([
|
|
16
|
+
['nav', 'Navigation'],
|
|
17
|
+
['main', 'Main Content'],
|
|
18
|
+
['header', 'Header'],
|
|
19
|
+
['footer', 'Footer'],
|
|
20
|
+
['aside', 'Sidebar'],
|
|
21
|
+
['form', 'Form'],
|
|
22
|
+
]);
|
|
23
|
+
const LANDMARK_ROLES = new Map([
|
|
24
|
+
['navigation', 'Navigation'],
|
|
25
|
+
['main', 'Main Content'],
|
|
26
|
+
['banner', 'Header'],
|
|
27
|
+
['contentinfo', 'Footer'],
|
|
28
|
+
['complementary', 'Sidebar'],
|
|
29
|
+
['search', 'Search'],
|
|
30
|
+
['dialog', 'Dialog'],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function isVisible(el) {
|
|
34
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;
|
|
35
|
+
const s = getComputedStyle(el);
|
|
36
|
+
return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isInteractive(el) {
|
|
40
|
+
const tag = el.tagName.toLowerCase();
|
|
41
|
+
if (INTERACTIVE_TAGS.has(tag)) {
|
|
42
|
+
if (el.disabled) return false;
|
|
43
|
+
if (tag === 'input' && el.type === 'hidden') return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const role = el.getAttribute('role');
|
|
47
|
+
if (role && INTERACTIVE_ROLES.has(role)) return true;
|
|
48
|
+
if (el.contentEditable === 'true') return true;
|
|
49
|
+
if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getRole(el) {
|
|
54
|
+
const role = el.getAttribute('role');
|
|
55
|
+
if (role) return role;
|
|
56
|
+
const tag = el.tagName.toLowerCase();
|
|
57
|
+
if (tag === 'a') return 'link';
|
|
58
|
+
if (tag === 'button' || tag === 'summary') return 'button';
|
|
59
|
+
if (tag === 'input') return el.type || 'text';
|
|
60
|
+
if (tag === 'select') return 'select';
|
|
61
|
+
if (tag === 'textarea') return 'textarea';
|
|
62
|
+
if (tag === 'label') return 'label';
|
|
63
|
+
return tag;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getName(el) {
|
|
67
|
+
return (
|
|
68
|
+
el.getAttribute('aria-label') ||
|
|
69
|
+
el.getAttribute('alt') ||
|
|
70
|
+
el.getAttribute('title') ||
|
|
71
|
+
el.getAttribute('placeholder') ||
|
|
72
|
+
(el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button') ? el.value : '') ||
|
|
73
|
+
(el.id ? document.querySelector('label[for="' + el.id + '"]')?.textContent?.trim() : '') ||
|
|
74
|
+
(el.children.length <= 2 ? el.textContent?.trim() : '') ||
|
|
75
|
+
''
|
|
76
|
+
).slice(0, 60);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getValue(el) {
|
|
80
|
+
const tag = el.tagName.toLowerCase();
|
|
81
|
+
if (tag === 'input') {
|
|
82
|
+
const type = el.type || 'text';
|
|
83
|
+
if (type === 'checkbox' || type === 'radio') return el.checked ? 'checked' : 'unchecked';
|
|
84
|
+
if (type === 'password') return el.value ? '****' : '';
|
|
85
|
+
return el.value ? el.value.slice(0, 30) : '';
|
|
86
|
+
}
|
|
87
|
+
if (tag === 'textarea') return el.value ? el.value.slice(0, 30) : '';
|
|
88
|
+
if (tag === 'select' && el.selectedOptions?.length) return el.selectedOptions[0].text.slice(0, 30);
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Collect elements
|
|
93
|
+
let idx = 0;
|
|
94
|
+
let charsUsed = 0;
|
|
95
|
+
const lines = [];
|
|
96
|
+
let lastLandmark = '';
|
|
97
|
+
|
|
98
|
+
// Page header
|
|
99
|
+
const scrollY = window.scrollY;
|
|
100
|
+
const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
|
|
101
|
+
const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;
|
|
102
|
+
const header = 'url: ' + location.href + ' | scroll: ' + scrollPct + '%';
|
|
103
|
+
lines.push(header);
|
|
104
|
+
charsUsed += header.length;
|
|
105
|
+
|
|
106
|
+
// Walk DOM
|
|
107
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
108
|
+
let node;
|
|
109
|
+
while ((node = walker.nextNode())) {
|
|
110
|
+
if (!isVisible(node)) continue;
|
|
111
|
+
|
|
112
|
+
const tag = node.tagName.toLowerCase();
|
|
113
|
+
if (['script','style','noscript','svg','path','meta','link','head','template'].includes(tag)) continue;
|
|
114
|
+
|
|
115
|
+
// Check for landmark
|
|
116
|
+
const role = node.getAttribute('role');
|
|
117
|
+
const landmark = LANDMARK_TAGS.get(tag) || (role ? LANDMARK_ROLES.get(role) : null);
|
|
118
|
+
if (landmark && landmark !== lastLandmark) {
|
|
119
|
+
const sectionLine = '--- ' + landmark + ' ---';
|
|
120
|
+
if (charsUsed + sectionLine.length > TOKEN_BUDGET * CHARS_PER_TOKEN) break;
|
|
121
|
+
lines.push(sectionLine);
|
|
122
|
+
charsUsed += sectionLine.length;
|
|
123
|
+
lastLandmark = landmark;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Only emit interactive elements
|
|
127
|
+
if (!isInteractive(node)) continue;
|
|
128
|
+
|
|
129
|
+
const elRole = getRole(node);
|
|
130
|
+
const name = getName(node);
|
|
131
|
+
const value = getValue(node);
|
|
132
|
+
|
|
133
|
+
// Build compact line
|
|
134
|
+
let line = '[' + idx + '] ' + elRole;
|
|
135
|
+
if (name) line += ' "' + name.replace(/"/g, "'") + '"';
|
|
136
|
+
if (value) line += ' val="' + value.replace(/"/g, "'") + '"';
|
|
137
|
+
|
|
138
|
+
// Check token budget
|
|
139
|
+
if (charsUsed + line.length > TOKEN_BUDGET * CHARS_PER_TOKEN) {
|
|
140
|
+
lines.push('... (' + (document.querySelectorAll('a,button,input,select,textarea,[role]').length - idx) + ' more elements)');
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Annotate element with ref for clicking
|
|
145
|
+
try { node.dataset.ref = String(idx); } catch {}
|
|
146
|
+
|
|
147
|
+
lines.push(line);
|
|
148
|
+
charsUsed += line.length;
|
|
149
|
+
idx++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return lines.join('\\n');
|
|
153
|
+
})()
|
|
154
|
+
`;
|
|
155
|
+
function buildCompactSnapshotScript(tokenBudget = 800) {
|
|
156
|
+
return COMPACT_SNAPSHOT_SCRIPT.replace("const TOKEN_BUDGET = 800;", `const TOKEN_BUDGET = ${tokenBudget};`);
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
COMPACT_SNAPSHOT_SCRIPT,
|
|
160
|
+
buildCompactSnapshotScript
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=compact-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/browser/dom/compact-snapshot.ts"],"sourcesContent":["/**\n * Compact Snapshot — token-efficient DOM snapshot (~800 tokens).\n *\n * Only emits interactive elements + landmark section headers.\n * Format: [0] button \"Sign In\" (one line per element)\n *\n * Inspired by PinchTab's token-counting approach, built from scratch.\n */\n\nexport const COMPACT_SNAPSHOT_SCRIPT = `\n(() => {\n const TOKEN_BUDGET = 800;\n const CHARS_PER_TOKEN = 4;\n\n const INTERACTIVE_TAGS = new Set([\n 'a','button','input','select','textarea','details','summary','label',\n ]);\n const INTERACTIVE_ROLES = new Set([\n 'button','link','textbox','checkbox','radio','combobox','listbox',\n 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',\n 'option','menuitemcheckbox','menuitemradio','treeitem',\n ]);\n const LANDMARK_TAGS = new Map([\n ['nav', 'Navigation'],\n ['main', 'Main Content'],\n ['header', 'Header'],\n ['footer', 'Footer'],\n ['aside', 'Sidebar'],\n ['form', 'Form'],\n ]);\n const LANDMARK_ROLES = new Map([\n ['navigation', 'Navigation'],\n ['main', 'Main Content'],\n ['banner', 'Header'],\n ['contentinfo', 'Footer'],\n ['complementary', 'Sidebar'],\n ['search', 'Search'],\n ['dialog', 'Dialog'],\n ]);\n\n function isVisible(el) {\n if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;\n const s = getComputedStyle(el);\n return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';\n }\n\n function isInteractive(el) {\n const tag = el.tagName.toLowerCase();\n if (INTERACTIVE_TAGS.has(tag)) {\n if (el.disabled) return false;\n if (tag === 'input' && el.type === 'hidden') return false;\n return true;\n }\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) return true;\n if (el.contentEditable === 'true') return true;\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;\n return false;\n }\n\n function getRole(el) {\n const role = el.getAttribute('role');\n if (role) return role;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a') return 'link';\n if (tag === 'button' || tag === 'summary') return 'button';\n if (tag === 'input') return el.type || 'text';\n if (tag === 'select') return 'select';\n if (tag === 'textarea') return 'textarea';\n if (tag === 'label') return 'label';\n return tag;\n }\n\n function getName(el) {\n return (\n el.getAttribute('aria-label') ||\n el.getAttribute('alt') ||\n el.getAttribute('title') ||\n el.getAttribute('placeholder') ||\n (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button') ? el.value : '') ||\n (el.id ? document.querySelector('label[for=\"' + el.id + '\"]')?.textContent?.trim() : '') ||\n (el.children.length <= 2 ? el.textContent?.trim() : '') ||\n ''\n ).slice(0, 60);\n }\n\n function getValue(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === 'input') {\n const type = el.type || 'text';\n if (type === 'checkbox' || type === 'radio') return el.checked ? 'checked' : 'unchecked';\n if (type === 'password') return el.value ? '****' : '';\n return el.value ? el.value.slice(0, 30) : '';\n }\n if (tag === 'textarea') return el.value ? el.value.slice(0, 30) : '';\n if (tag === 'select' && el.selectedOptions?.length) return el.selectedOptions[0].text.slice(0, 30);\n return '';\n }\n\n // Collect elements\n let idx = 0;\n let charsUsed = 0;\n const lines = [];\n let lastLandmark = '';\n\n // Page header\n const scrollY = window.scrollY;\n const scrollMax = document.documentElement.scrollHeight - window.innerHeight;\n const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;\n const header = 'url: ' + location.href + ' | scroll: ' + scrollPct + '%';\n lines.push(header);\n charsUsed += header.length;\n\n // Walk DOM\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);\n let node;\n while ((node = walker.nextNode())) {\n if (!isVisible(node)) continue;\n\n const tag = node.tagName.toLowerCase();\n if (['script','style','noscript','svg','path','meta','link','head','template'].includes(tag)) continue;\n\n // Check for landmark\n const role = node.getAttribute('role');\n const landmark = LANDMARK_TAGS.get(tag) || (role ? LANDMARK_ROLES.get(role) : null);\n if (landmark && landmark !== lastLandmark) {\n const sectionLine = '--- ' + landmark + ' ---';\n if (charsUsed + sectionLine.length > TOKEN_BUDGET * CHARS_PER_TOKEN) break;\n lines.push(sectionLine);\n charsUsed += sectionLine.length;\n lastLandmark = landmark;\n }\n\n // Only emit interactive elements\n if (!isInteractive(node)) continue;\n\n const elRole = getRole(node);\n const name = getName(node);\n const value = getValue(node);\n\n // Build compact line\n let line = '[' + idx + '] ' + elRole;\n if (name) line += ' \"' + name.replace(/\"/g, \"'\") + '\"';\n if (value) line += ' val=\"' + value.replace(/\"/g, \"'\") + '\"';\n\n // Check token budget\n if (charsUsed + line.length > TOKEN_BUDGET * CHARS_PER_TOKEN) {\n lines.push('... (' + (document.querySelectorAll('a,button,input,select,textarea,[role]').length - idx) + ' more elements)');\n break;\n }\n\n // Annotate element with ref for clicking\n try { node.dataset.ref = String(idx); } catch {}\n\n lines.push(line);\n charsUsed += line.length;\n idx++;\n }\n\n return lines.join('\\\\n');\n})()\n`;\n\n/**\n * Build compact snapshot script with custom token budget.\n */\nexport function buildCompactSnapshotScript(tokenBudget: number = 800): string {\n return COMPACT_SNAPSHOT_SCRIPT.replace('const TOKEN_BUDGET = 800;', `const TOKEN_BUDGET = ${tokenBudget};`);\n}\n"],"mappings":";AASO,IAAM,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6JhC,SAAS,2BAA2B,cAAsB,KAAa;AAC5E,SAAO,wBAAwB,QAAQ,6BAA6B,wBAAwB,WAAW,GAAG;AAC5G;","names":[]}
|
|
@@ -1083,13 +1083,173 @@ var FORM_STATE_SCRIPT = `
|
|
|
1083
1083
|
return result;
|
|
1084
1084
|
})()
|
|
1085
1085
|
`;
|
|
1086
|
+
|
|
1087
|
+
// src/browser/dom/compact-snapshot.ts
|
|
1088
|
+
var COMPACT_SNAPSHOT_SCRIPT = `
|
|
1089
|
+
(() => {
|
|
1090
|
+
const TOKEN_BUDGET = 800;
|
|
1091
|
+
const CHARS_PER_TOKEN = 4;
|
|
1092
|
+
|
|
1093
|
+
const INTERACTIVE_TAGS = new Set([
|
|
1094
|
+
'a','button','input','select','textarea','details','summary','label',
|
|
1095
|
+
]);
|
|
1096
|
+
const INTERACTIVE_ROLES = new Set([
|
|
1097
|
+
'button','link','textbox','checkbox','radio','combobox','listbox',
|
|
1098
|
+
'menu','menuitem','tab','switch','slider','searchbox','spinbutton',
|
|
1099
|
+
'option','menuitemcheckbox','menuitemradio','treeitem',
|
|
1100
|
+
]);
|
|
1101
|
+
const LANDMARK_TAGS = new Map([
|
|
1102
|
+
['nav', 'Navigation'],
|
|
1103
|
+
['main', 'Main Content'],
|
|
1104
|
+
['header', 'Header'],
|
|
1105
|
+
['footer', 'Footer'],
|
|
1106
|
+
['aside', 'Sidebar'],
|
|
1107
|
+
['form', 'Form'],
|
|
1108
|
+
]);
|
|
1109
|
+
const LANDMARK_ROLES = new Map([
|
|
1110
|
+
['navigation', 'Navigation'],
|
|
1111
|
+
['main', 'Main Content'],
|
|
1112
|
+
['banner', 'Header'],
|
|
1113
|
+
['contentinfo', 'Footer'],
|
|
1114
|
+
['complementary', 'Sidebar'],
|
|
1115
|
+
['search', 'Search'],
|
|
1116
|
+
['dialog', 'Dialog'],
|
|
1117
|
+
]);
|
|
1118
|
+
|
|
1119
|
+
function isVisible(el) {
|
|
1120
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;
|
|
1121
|
+
const s = getComputedStyle(el);
|
|
1122
|
+
return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function isInteractive(el) {
|
|
1126
|
+
const tag = el.tagName.toLowerCase();
|
|
1127
|
+
if (INTERACTIVE_TAGS.has(tag)) {
|
|
1128
|
+
if (el.disabled) return false;
|
|
1129
|
+
if (tag === 'input' && el.type === 'hidden') return false;
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
const role = el.getAttribute('role');
|
|
1133
|
+
if (role && INTERACTIVE_ROLES.has(role)) return true;
|
|
1134
|
+
if (el.contentEditable === 'true') return true;
|
|
1135
|
+
if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function getRole(el) {
|
|
1140
|
+
const role = el.getAttribute('role');
|
|
1141
|
+
if (role) return role;
|
|
1142
|
+
const tag = el.tagName.toLowerCase();
|
|
1143
|
+
if (tag === 'a') return 'link';
|
|
1144
|
+
if (tag === 'button' || tag === 'summary') return 'button';
|
|
1145
|
+
if (tag === 'input') return el.type || 'text';
|
|
1146
|
+
if (tag === 'select') return 'select';
|
|
1147
|
+
if (tag === 'textarea') return 'textarea';
|
|
1148
|
+
if (tag === 'label') return 'label';
|
|
1149
|
+
return tag;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function getName(el) {
|
|
1153
|
+
return (
|
|
1154
|
+
el.getAttribute('aria-label') ||
|
|
1155
|
+
el.getAttribute('alt') ||
|
|
1156
|
+
el.getAttribute('title') ||
|
|
1157
|
+
el.getAttribute('placeholder') ||
|
|
1158
|
+
(el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button') ? el.value : '') ||
|
|
1159
|
+
(el.id ? document.querySelector('label[for="' + el.id + '"]')?.textContent?.trim() : '') ||
|
|
1160
|
+
(el.children.length <= 2 ? el.textContent?.trim() : '') ||
|
|
1161
|
+
''
|
|
1162
|
+
).slice(0, 60);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function getValue(el) {
|
|
1166
|
+
const tag = el.tagName.toLowerCase();
|
|
1167
|
+
if (tag === 'input') {
|
|
1168
|
+
const type = el.type || 'text';
|
|
1169
|
+
if (type === 'checkbox' || type === 'radio') return el.checked ? 'checked' : 'unchecked';
|
|
1170
|
+
if (type === 'password') return el.value ? '****' : '';
|
|
1171
|
+
return el.value ? el.value.slice(0, 30) : '';
|
|
1172
|
+
}
|
|
1173
|
+
if (tag === 'textarea') return el.value ? el.value.slice(0, 30) : '';
|
|
1174
|
+
if (tag === 'select' && el.selectedOptions?.length) return el.selectedOptions[0].text.slice(0, 30);
|
|
1175
|
+
return '';
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Collect elements
|
|
1179
|
+
let idx = 0;
|
|
1180
|
+
let charsUsed = 0;
|
|
1181
|
+
const lines = [];
|
|
1182
|
+
let lastLandmark = '';
|
|
1183
|
+
|
|
1184
|
+
// Page header
|
|
1185
|
+
const scrollY = window.scrollY;
|
|
1186
|
+
const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
|
|
1187
|
+
const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;
|
|
1188
|
+
const header = 'url: ' + location.href + ' | scroll: ' + scrollPct + '%';
|
|
1189
|
+
lines.push(header);
|
|
1190
|
+
charsUsed += header.length;
|
|
1191
|
+
|
|
1192
|
+
// Walk DOM
|
|
1193
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
1194
|
+
let node;
|
|
1195
|
+
while ((node = walker.nextNode())) {
|
|
1196
|
+
if (!isVisible(node)) continue;
|
|
1197
|
+
|
|
1198
|
+
const tag = node.tagName.toLowerCase();
|
|
1199
|
+
if (['script','style','noscript','svg','path','meta','link','head','template'].includes(tag)) continue;
|
|
1200
|
+
|
|
1201
|
+
// Check for landmark
|
|
1202
|
+
const role = node.getAttribute('role');
|
|
1203
|
+
const landmark = LANDMARK_TAGS.get(tag) || (role ? LANDMARK_ROLES.get(role) : null);
|
|
1204
|
+
if (landmark && landmark !== lastLandmark) {
|
|
1205
|
+
const sectionLine = '--- ' + landmark + ' ---';
|
|
1206
|
+
if (charsUsed + sectionLine.length > TOKEN_BUDGET * CHARS_PER_TOKEN) break;
|
|
1207
|
+
lines.push(sectionLine);
|
|
1208
|
+
charsUsed += sectionLine.length;
|
|
1209
|
+
lastLandmark = landmark;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Only emit interactive elements
|
|
1213
|
+
if (!isInteractive(node)) continue;
|
|
1214
|
+
|
|
1215
|
+
const elRole = getRole(node);
|
|
1216
|
+
const name = getName(node);
|
|
1217
|
+
const value = getValue(node);
|
|
1218
|
+
|
|
1219
|
+
// Build compact line
|
|
1220
|
+
let line = '[' + idx + '] ' + elRole;
|
|
1221
|
+
if (name) line += ' "' + name.replace(/"/g, "'") + '"';
|
|
1222
|
+
if (value) line += ' val="' + value.replace(/"/g, "'") + '"';
|
|
1223
|
+
|
|
1224
|
+
// Check token budget
|
|
1225
|
+
if (charsUsed + line.length > TOKEN_BUDGET * CHARS_PER_TOKEN) {
|
|
1226
|
+
lines.push('... (' + (document.querySelectorAll('a,button,input,select,textarea,[role]').length - idx) + ' more elements)');
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Annotate element with ref for clicking
|
|
1231
|
+
try { node.dataset.ref = String(idx); } catch {}
|
|
1232
|
+
|
|
1233
|
+
lines.push(line);
|
|
1234
|
+
charsUsed += line.length;
|
|
1235
|
+
idx++;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return lines.join('\\n');
|
|
1239
|
+
})()
|
|
1240
|
+
`;
|
|
1241
|
+
function buildCompactSnapshotScript(tokenBudget = 800) {
|
|
1242
|
+
return COMPACT_SNAPSHOT_SCRIPT.replace("const TOKEN_BUDGET = 800;", `const TOKEN_BUDGET = ${tokenBudget};`);
|
|
1243
|
+
}
|
|
1086
1244
|
export {
|
|
1245
|
+
COMPACT_SNAPSHOT_SCRIPT,
|
|
1087
1246
|
FLAT_TREE_SCRIPT,
|
|
1088
1247
|
FORM_STATE_SCRIPT,
|
|
1089
1248
|
INTERACTIVE_ELEMENTS_SCRIPT,
|
|
1090
1249
|
MARKDOWN_SCRIPT,
|
|
1091
1250
|
SEMANTIC_TREE_SCRIPT,
|
|
1092
1251
|
SNAPSHOT_SCRIPT,
|
|
1252
|
+
buildCompactSnapshotScript,
|
|
1093
1253
|
buildSnapshotScript,
|
|
1094
1254
|
flatTreeToString
|
|
1095
1255
|
};
|