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.
@@ -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
  };