arise-browser 0.1.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.
Files changed (148) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +247 -0
  3. package/deploy/neko/CONTEXT.md +37 -0
  4. package/deploy/neko/arise-browser.service +13 -0
  5. package/deploy/neko/neko.yaml +12 -0
  6. package/deploy/neko/openbox.xml +763 -0
  7. package/deploy/neko/policies.json +28 -0
  8. package/deploy/neko/pulseaudio.pa +16 -0
  9. package/deploy/neko/setup.sh +308 -0
  10. package/deploy/neko/xorg.conf +118 -0
  11. package/dist/bin/arise-browser.d.ts +26 -0
  12. package/dist/bin/arise-browser.d.ts.map +1 -0
  13. package/dist/bin/arise-browser.js +224 -0
  14. package/dist/bin/arise-browser.js.map +1 -0
  15. package/dist/src/browser/action-executor.d.ts +98 -0
  16. package/dist/src/browser/action-executor.d.ts.map +1 -0
  17. package/dist/src/browser/action-executor.js +2726 -0
  18. package/dist/src/browser/action-executor.js.map +1 -0
  19. package/dist/src/browser/behavior-recorder.d.ts +61 -0
  20. package/dist/src/browser/behavior-recorder.d.ts.map +1 -0
  21. package/dist/src/browser/behavior-recorder.js +442 -0
  22. package/dist/src/browser/behavior-recorder.js.map +1 -0
  23. package/dist/src/browser/browser-session.d.ts +202 -0
  24. package/dist/src/browser/browser-session.d.ts.map +1 -0
  25. package/dist/src/browser/browser-session.js +1647 -0
  26. package/dist/src/browser/browser-session.js.map +1 -0
  27. package/dist/src/browser/config.d.ts +43 -0
  28. package/dist/src/browser/config.d.ts.map +1 -0
  29. package/dist/src/browser/config.js +59 -0
  30. package/dist/src/browser/config.js.map +1 -0
  31. package/dist/src/browser/page-snapshot.d.ts +38 -0
  32. package/dist/src/browser/page-snapshot.d.ts.map +1 -0
  33. package/dist/src/browser/page-snapshot.js +241 -0
  34. package/dist/src/browser/page-snapshot.js.map +1 -0
  35. package/dist/src/browser/scripts/behavior_tracker.js +424 -0
  36. package/dist/src/browser/scripts/unified_analyzer.js +1576 -0
  37. package/dist/src/index.d.ts +15 -0
  38. package/dist/src/index.d.ts.map +1 -0
  39. package/dist/src/index.js +15 -0
  40. package/dist/src/index.js.map +1 -0
  41. package/dist/src/lock.d.ts +11 -0
  42. package/dist/src/lock.d.ts.map +1 -0
  43. package/dist/src/lock.js +47 -0
  44. package/dist/src/lock.js.map +1 -0
  45. package/dist/src/logger.d.ts +17 -0
  46. package/dist/src/logger.d.ts.map +1 -0
  47. package/dist/src/logger.js +29 -0
  48. package/dist/src/logger.js.map +1 -0
  49. package/dist/src/server/middleware/auth.d.ts +6 -0
  50. package/dist/src/server/middleware/auth.d.ts.map +1 -0
  51. package/dist/src/server/middleware/auth.js +24 -0
  52. package/dist/src/server/middleware/auth.js.map +1 -0
  53. package/dist/src/server/route-utils.d.ts +15 -0
  54. package/dist/src/server/route-utils.d.ts.map +1 -0
  55. package/dist/src/server/route-utils.js +33 -0
  56. package/dist/src/server/route-utils.js.map +1 -0
  57. package/dist/src/server/routes/action.d.ts +5 -0
  58. package/dist/src/server/routes/action.d.ts.map +1 -0
  59. package/dist/src/server/routes/action.js +69 -0
  60. package/dist/src/server/routes/action.js.map +1 -0
  61. package/dist/src/server/routes/actions.d.ts +3 -0
  62. package/dist/src/server/routes/actions.d.ts.map +1 -0
  63. package/dist/src/server/routes/actions.js +53 -0
  64. package/dist/src/server/routes/actions.js.map +1 -0
  65. package/dist/src/server/routes/cookies.d.ts +3 -0
  66. package/dist/src/server/routes/cookies.d.ts.map +1 -0
  67. package/dist/src/server/routes/cookies.js +27 -0
  68. package/dist/src/server/routes/cookies.js.map +1 -0
  69. package/dist/src/server/routes/download.d.ts +3 -0
  70. package/dist/src/server/routes/download.d.ts.map +1 -0
  71. package/dist/src/server/routes/download.js +35 -0
  72. package/dist/src/server/routes/download.js.map +1 -0
  73. package/dist/src/server/routes/evaluate.d.ts +3 -0
  74. package/dist/src/server/routes/evaluate.d.ts.map +1 -0
  75. package/dist/src/server/routes/evaluate.js +27 -0
  76. package/dist/src/server/routes/evaluate.js.map +1 -0
  77. package/dist/src/server/routes/health.d.ts +3 -0
  78. package/dist/src/server/routes/health.d.ts.map +1 -0
  79. package/dist/src/server/routes/health.js +11 -0
  80. package/dist/src/server/routes/health.js.map +1 -0
  81. package/dist/src/server/routes/navigate.d.ts +3 -0
  82. package/dist/src/server/routes/navigate.d.ts.map +1 -0
  83. package/dist/src/server/routes/navigate.js +36 -0
  84. package/dist/src/server/routes/navigate.js.map +1 -0
  85. package/dist/src/server/routes/page-model.d.ts +3 -0
  86. package/dist/src/server/routes/page-model.d.ts.map +1 -0
  87. package/dist/src/server/routes/page-model.js +22 -0
  88. package/dist/src/server/routes/page-model.js.map +1 -0
  89. package/dist/src/server/routes/pdf.d.ts +3 -0
  90. package/dist/src/server/routes/pdf.d.ts.map +1 -0
  91. package/dist/src/server/routes/pdf.js +20 -0
  92. package/dist/src/server/routes/pdf.js.map +1 -0
  93. package/dist/src/server/routes/recording.d.ts +5 -0
  94. package/dist/src/server/routes/recording.d.ts.map +1 -0
  95. package/dist/src/server/routes/recording.js +217 -0
  96. package/dist/src/server/routes/recording.js.map +1 -0
  97. package/dist/src/server/routes/screenshot.d.ts +3 -0
  98. package/dist/src/server/routes/screenshot.d.ts.map +1 -0
  99. package/dist/src/server/routes/screenshot.js +32 -0
  100. package/dist/src/server/routes/screenshot.js.map +1 -0
  101. package/dist/src/server/routes/snapshot.d.ts +3 -0
  102. package/dist/src/server/routes/snapshot.d.ts.map +1 -0
  103. package/dist/src/server/routes/snapshot.js +454 -0
  104. package/dist/src/server/routes/snapshot.js.map +1 -0
  105. package/dist/src/server/routes/tab-lock.d.ts +3 -0
  106. package/dist/src/server/routes/tab-lock.d.ts.map +1 -0
  107. package/dist/src/server/routes/tab-lock.js +30 -0
  108. package/dist/src/server/routes/tab-lock.js.map +1 -0
  109. package/dist/src/server/routes/tab.d.ts +3 -0
  110. package/dist/src/server/routes/tab.d.ts.map +1 -0
  111. package/dist/src/server/routes/tab.js +47 -0
  112. package/dist/src/server/routes/tab.js.map +1 -0
  113. package/dist/src/server/routes/tabs.d.ts +3 -0
  114. package/dist/src/server/routes/tabs.d.ts.map +1 -0
  115. package/dist/src/server/routes/tabs.js +13 -0
  116. package/dist/src/server/routes/tabs.js.map +1 -0
  117. package/dist/src/server/routes/text.d.ts +3 -0
  118. package/dist/src/server/routes/text.d.ts.map +1 -0
  119. package/dist/src/server/routes/text.js +20 -0
  120. package/dist/src/server/routes/text.js.map +1 -0
  121. package/dist/src/server/routes/upload.d.ts +3 -0
  122. package/dist/src/server/routes/upload.d.ts.map +1 -0
  123. package/dist/src/server/routes/upload.js +38 -0
  124. package/dist/src/server/routes/upload.js.map +1 -0
  125. package/dist/src/server/server.d.ts +7 -0
  126. package/dist/src/server/server.d.ts.map +1 -0
  127. package/dist/src/server/server.js +69 -0
  128. package/dist/src/server/server.js.map +1 -0
  129. package/dist/src/types/index.d.ts +125 -0
  130. package/dist/src/types/index.d.ts.map +1 -0
  131. package/dist/src/types/index.js +5 -0
  132. package/dist/src/types/index.js.map +1 -0
  133. package/dist/src/virtual-display/manager.d.ts +37 -0
  134. package/dist/src/virtual-display/manager.d.ts.map +1 -0
  135. package/dist/src/virtual-display/manager.js +229 -0
  136. package/dist/src/virtual-display/manager.js.map +1 -0
  137. package/dist/src/virtual-display/process-runner.d.ts +43 -0
  138. package/dist/src/virtual-display/process-runner.d.ts.map +1 -0
  139. package/dist/src/virtual-display/process-runner.js +174 -0
  140. package/dist/src/virtual-display/process-runner.js.map +1 -0
  141. package/dist/tsconfig.tsbuildinfo +1 -0
  142. package/package.json +57 -0
  143. package/plugin/openclaw.plugin.json +148 -0
  144. package/skill/arise-browser/SKILL.md +275 -0
  145. package/skill/arise-browser/TRUST.md +42 -0
  146. package/skill/arise-browser/references/api.md +198 -0
  147. package/src/browser/scripts/behavior_tracker.js +424 -0
  148. package/src/browser/scripts/unified_analyzer.js +1576 -0
@@ -0,0 +1,1647 @@
1
+ /**
2
+ * BrowserSession — Multi-mode browser automation engine.
3
+ *
4
+ * Connection modes:
5
+ * - 'standalone': Launch new Chromium instance
6
+ * - 'cdp': Connect to existing browser via CDP
7
+ * - 'managed': Persistent browser profile (launchPersistentContext)
8
+ *
9
+ * Key concepts:
10
+ * - Tab management (Map<tabId, Page>), Tab Groups
11
+ * - Singleton per session-id, factory method create()
12
+ * - Popup event listening, crash handling
13
+ * - getSnapshot(), execAction(), visit()
14
+ */
15
+ import { chromium } from "playwright";
16
+ import { BrowserConfig, getStealthContextOptions, getUserAgent, patchHeadlessUserAgent } from "./config.js";
17
+ import { PageSnapshot } from "./page-snapshot.js";
18
+ import { ActionExecutor } from "./action-executor.js";
19
+ import { createLogger } from "../logger.js";
20
+ const logger = createLogger("browser-session");
21
+ function renderListSummary(summary) {
22
+ const lines = ["Result list summary"];
23
+ if (summary.totalResults)
24
+ lines.push(`- results: ${summary.totalResults}`);
25
+ if (summary.querySummary)
26
+ lines.push(`- active query params: ${summary.querySummary}`);
27
+ lines.push(`- visible cards summarized: ${summary.cards.length}`);
28
+ lines.push("");
29
+ lines.push("Visible items:");
30
+ summary.cards.forEach((card, index) => {
31
+ const parts = [`${index + 1}. ${card.title}`];
32
+ if (card.price)
33
+ parts.push(`price=${card.price}`);
34
+ if (card.location)
35
+ parts.push(`location=${card.location}`);
36
+ if (card.meta)
37
+ parts.push(`meta=${card.meta}`);
38
+ if (card.url)
39
+ parts.push(`url=${card.url}`);
40
+ lines.push(parts.join(" | "));
41
+ });
42
+ if (summary.hiddenVisibleCards > 0) {
43
+ lines.push(`... ${summary.hiddenVisibleCards} more visible cards not expanded`);
44
+ }
45
+ return lines.join("\n");
46
+ }
47
+ function renderTableSummary(summary) {
48
+ const lines = ["Table summary"];
49
+ if (summary.caption)
50
+ lines.push(`- caption: ${summary.caption}`);
51
+ lines.push(`- columns: ${summary.columns.join(" | ")}`);
52
+ lines.push(`- visible rows summarized: ${Math.min(summary.visibleRows, summary.rows.length)}`);
53
+ lines.push("");
54
+ lines.push("Rows:");
55
+ summary.rows.forEach((row, index) => {
56
+ const pairs = row
57
+ .slice(0, summary.columns.length)
58
+ .map((cell, cellIndex) => `${summary.columns[cellIndex] || `col${cellIndex + 1}`}=${cell}`);
59
+ lines.push(`${index + 1}. ${pairs.join(" | ")}`);
60
+ });
61
+ if (summary.visibleRows > summary.rows.length) {
62
+ lines.push(`... ${summary.visibleRows - summary.rows.length} more visible rows not expanded`);
63
+ }
64
+ return lines.join("\n");
65
+ }
66
+ function renderArticleSummary(summary) {
67
+ const lines = ["Article summary"];
68
+ if (summary.title)
69
+ lines.push(`- title: ${summary.title}`);
70
+ lines.push("");
71
+ lines.push(...summary.paragraphs.slice(0, 8));
72
+ if (summary.paragraphs.length > 8) {
73
+ lines.push(`... ${summary.paragraphs.length - 8} more paragraphs not expanded`);
74
+ }
75
+ return lines.join("\n");
76
+ }
77
+ function renderTextFromPageModel(model, mode) {
78
+ const rawText = model.rawText || "";
79
+ if (mode === "raw")
80
+ return rawText;
81
+ if (mode === "list")
82
+ return model.listSummary ? renderListSummary(model.listSummary) : rawText;
83
+ if (mode === "table")
84
+ return model.tableSummary ? renderTableSummary(model.tableSummary) : rawText;
85
+ if (mode === "article") {
86
+ return model.articleSummary ? renderArticleSummary(model.articleSummary) : rawText;
87
+ }
88
+ if (model.primaryContent === "result_list" && model.listSummary) {
89
+ const summary = renderListSummary(model.listSummary);
90
+ if (model.auxiliarySections.includes("calendar_table")) {
91
+ return `${summary}\n\nAuxiliary sections:\n- calendar availability table detected but not treated as the main content`;
92
+ }
93
+ return summary;
94
+ }
95
+ if (model.primaryContent === "table" && model.tableSummary) {
96
+ return renderTableSummary(model.tableSummary);
97
+ }
98
+ if (model.primaryContent === "article" && model.articleSummary) {
99
+ return renderArticleSummary(model.articleSummary);
100
+ }
101
+ if (model.listSummary)
102
+ return renderListSummary(model.listSummary);
103
+ if (model.tableSummary)
104
+ return renderTableSummary(model.tableSummary);
105
+ if (model.articleSummary)
106
+ return renderArticleSummary(model.articleSummary);
107
+ return rawText;
108
+ }
109
+ function buildTextExtractionSource(mode) {
110
+ return `
111
+ (() => {
112
+ const requestedMode = ${JSON.stringify(mode)};
113
+ const normalize = function(value, max) {
114
+ const collapsed = String(value || '')
115
+ .replace(/\\u200b/g, ' ')
116
+ .replace(/\\s+/g, ' ')
117
+ .trim();
118
+ if (!max || collapsed.length <= max) return collapsed;
119
+ return collapsed.slice(0, Math.max(0, max - 3)).trimEnd() + '...';
120
+ };
121
+
122
+ const normalizeLines = function(value) {
123
+ return String(value || '')
124
+ .split(/\\n+/)
125
+ .map(function(line) { return normalize(line, 0); })
126
+ .filter(Boolean);
127
+ };
128
+
129
+ const isVisible = function(element) {
130
+ if (!(element instanceof HTMLElement)) return false;
131
+ const style = window.getComputedStyle(element);
132
+ if (style.visibility === 'hidden' || style.display === 'none') return false;
133
+ const rect = element.getBoundingClientRect();
134
+ return rect.width >= 1
135
+ && rect.height >= 1
136
+ && rect.bottom > 0
137
+ && rect.right > 0
138
+ && rect.top < (window.innerHeight || document.documentElement.clientHeight) * 1.5;
139
+ };
140
+
141
+ const isTimeLikeLine = function(line) {
142
+ return /^(?:<\\s*)?\\d+\\s*(?:m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|week|weeks|mo|month|months)\\s+ago$/i.test(line)
143
+ || /^(?:today|yesterday)$/i.test(line)
144
+ || /^\\d{1,2}:\\d{2}\\s*(?:AM|PM)$/i.test(line);
145
+ };
146
+
147
+ const bodyText = normalize(document.body.innerText || document.body.textContent || '', 0);
148
+
149
+ const buildQuerySummary = function() {
150
+ try {
151
+ const params = new URL(window.location.href).searchParams;
152
+ const entries = [];
153
+ for (const pair of params.entries()) {
154
+ const key = pair[0];
155
+ const value = pair[1];
156
+ if (!key || key === 'tabId') continue;
157
+ entries.push(value ? key + '=' + normalize(value, 48) : key);
158
+ if (entries.length >= 6) break;
159
+ }
160
+ return entries.length > 0 ? entries.join(', ') : null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ };
165
+
166
+ const buildTableSummary = function() {
167
+ const tables = Array.from(document.querySelectorAll('table')).filter(isVisible);
168
+ if (tables.length === 0) return null;
169
+
170
+ const candidates = tables
171
+ .map(function(table) {
172
+ const rows = Array.from(table.querySelectorAll('tr'))
173
+ .map(function(row) {
174
+ return Array.from(row.querySelectorAll('th, td'))
175
+ .map(function(cell) { return normalize(cell.innerText || cell.textContent || '', 80); })
176
+ .filter(Boolean);
177
+ })
178
+ .filter(function(row) { return row.length > 0; });
179
+ const header = rows[0] || [];
180
+ const dataRows = rows.slice(header.length > 0 ? 1 : 0);
181
+ return {
182
+ header,
183
+ dataRows,
184
+ score: (header.length * 2) + dataRows.length,
185
+ };
186
+ })
187
+ .filter(function(candidate) { return candidate.header.length >= 2 && candidate.dataRows.length >= 2; })
188
+ .sort(function(a, b) { return b.score - a.score; });
189
+
190
+ const best = candidates[0];
191
+ if (!best) return null;
192
+
193
+ const lines = [
194
+ 'Table summary',
195
+ '- columns: ' + best.header.join(' | '),
196
+ '- visible rows summarized: ' + String(Math.min(best.dataRows.length, 12)),
197
+ '',
198
+ 'Rows:',
199
+ ];
200
+
201
+ best.dataRows.slice(0, 12).forEach(function(row, index) {
202
+ const pairs = row
203
+ .slice(0, best.header.length)
204
+ .map(function(cell, cellIndex) {
205
+ return (best.header[cellIndex] || ('col' + String(cellIndex + 1))) + '=' + cell;
206
+ });
207
+ lines.push(String(index + 1) + '. ' + pairs.join(' | '));
208
+ });
209
+
210
+ if (best.dataRows.length > 12) {
211
+ lines.push('... ' + String(best.dataRows.length - 12) + ' more visible rows not expanded');
212
+ }
213
+
214
+ return lines.join('\\n');
215
+ };
216
+
217
+ const findCardRoot = function(anchor) {
218
+ let best = null;
219
+ let bestScore = Number.NEGATIVE_INFINITY;
220
+ let current = anchor;
221
+ let depth = 0;
222
+ while (current && current !== document.body && depth < 6) {
223
+ if (isVisible(current)) {
224
+ const text = normalize(current.innerText || current.textContent || '', 900);
225
+ const linkCount = Array.from(current.querySelectorAll('a[href]')).filter(isVisible).length;
226
+ const tag = current.tagName.toLowerCase();
227
+ const rect = current.getBoundingClientRect();
228
+ let score = 0;
229
+ if (text.length >= 24 && text.length <= 800) score += 4;
230
+ if (['article', 'li', 'section', 'tr'].includes(tag)) score += 3;
231
+ if (tag === 'div') score += 1;
232
+ if (linkCount >= 1 && linkCount <= 6) score += 2;
233
+ if (/\\$\\s?\\d[\\d,]*/.test(text)) score += 2;
234
+ if (/(?:^|\\b)(studio|\\d+\\s*br|\\d+\\s*ba|\\d+\\s*ft2|\\d+\\s*beds?|\\d+\\s*baths?)(?:\\b|$)/i.test(text)) {
235
+ score += 2;
236
+ }
237
+ if (rect.width >= 140 && rect.height >= 48) score += 2;
238
+
239
+ const parent = current.parentElement;
240
+ if (parent) {
241
+ const siblingCount = Array.from(parent.children).filter(function(child) {
242
+ if (!(child instanceof HTMLElement)) return false;
243
+ if (child.tagName !== current.tagName) return false;
244
+ return isVisible(child) && !!child.querySelector('a[href]');
245
+ }).length;
246
+ if (siblingCount >= 3) score += Math.min(5, siblingCount);
247
+ }
248
+
249
+ if (score > bestScore) {
250
+ best = current;
251
+ bestScore = score;
252
+ }
253
+ }
254
+ current = current.parentElement;
255
+ depth += 1;
256
+ }
257
+ return best || anchor;
258
+ };
259
+
260
+ const buildListSummary = function(force) {
261
+ const mainRoot =
262
+ document.querySelector("main, [role='main'], #main, .main")
263
+ || document.body;
264
+
265
+ const anchors = Array.from(mainRoot.querySelectorAll('a[href]'))
266
+ .filter(function(anchor) { return anchor instanceof HTMLAnchorElement; })
267
+ .filter(function(anchor) {
268
+ if (!isVisible(anchor)) return false;
269
+ const href = normalize(anchor.href, 0);
270
+ const text = normalize(anchor.innerText || anchor.textContent || '', 0);
271
+ if (!href || href.startsWith('javascript:') || href === window.location.href) return false;
272
+ return text.length >= 8;
273
+ });
274
+
275
+ const seenRoots = new Set();
276
+ const roots = anchors
277
+ .map(function(anchor) { return findCardRoot(anchor); })
278
+ .filter(function(root) {
279
+ if (seenRoots.has(root)) return false;
280
+ seenRoots.add(root);
281
+ return true;
282
+ })
283
+ .map(function(root) {
284
+ return {
285
+ root,
286
+ top: root.getBoundingClientRect().top + window.scrollY,
287
+ text: normalize(root.innerText || root.textContent || '', 1200),
288
+ };
289
+ })
290
+ .filter(function(entry) { return entry.text.length >= 24; })
291
+ .sort(function(a, b) { return a.top - b.top; });
292
+
293
+ const strongCards = roots.filter(function(entry) {
294
+ return /\\$\\s?\\d[\\d,]*/.test(entry.text)
295
+ || /(?:^|\\b)(studio|\\d+\\s*br|\\d+\\s*ba|\\d+\\s*ft2|\\d+\\s*beds?|\\d+\\s*baths?)(?:\\b|$)/i.test(entry.text);
296
+ });
297
+
298
+ if (!force && (roots.length < 5 || strongCards.length < Math.min(3, roots.length))) {
299
+ return null;
300
+ }
301
+ if (roots.length === 0) return null;
302
+
303
+ const totalResultsMatch = bodyText.match(/\\b\\d+\\s*-\\s*\\d+\\s+of\\s+\\d[\\d,]*\\b/i);
304
+ const querySummary = buildQuerySummary();
305
+
306
+ const lines = ['Result list summary'];
307
+ if (totalResultsMatch) lines.push('- results: ' + totalResultsMatch[0]);
308
+ if (querySummary) lines.push('- active query params: ' + querySummary);
309
+
310
+ const visibleCards = roots.slice(0, 15).map(function(entry, index) {
311
+ const root = entry.root;
312
+ const fullText = normalize(root.innerText || root.textContent || '', 1400);
313
+ const textLines = normalizeLines(root.innerText || root.textContent || '');
314
+ const visibleLinks = Array.from(root.querySelectorAll('a[href]'))
315
+ .filter(function(link) { return link instanceof HTMLAnchorElement; })
316
+ .filter(isVisible);
317
+
318
+ const primaryLink = visibleLinks
319
+ .map(function(link) {
320
+ const text = normalize(link.innerText || link.textContent || '', 180);
321
+ return {
322
+ link,
323
+ text,
324
+ score: Math.min(normalize(link.innerText || link.textContent || '', 0).length, 180),
325
+ };
326
+ })
327
+ .filter(function(candidate) { return candidate.text.length >= 8; })
328
+ .sort(function(a, b) { return b.score - a.score; })[0]?.link || visibleLinks[0] || null;
329
+
330
+ const heading = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6'))
331
+ .filter(isVisible)
332
+ .map(function(node) { return normalize(node.innerText || node.textContent || '', 180); })
333
+ .find(Boolean) || '';
334
+
335
+ const title = heading || normalize((primaryLink && (primaryLink.innerText || primaryLink.textContent)) || '', 180);
336
+ const priceMatch = fullText.match(/(?:US\\$|\\$)\\s?\\d[\\d,]*(?:\\.\\d{2})?/);
337
+ const price = priceMatch ? priceMatch[0] : '';
338
+ const meta = textLines.find(function(line) {
339
+ return /(?:^|\\b)(studio|\\d+\\s*br|\\d+\\s*ba|\\d+\\s*ft2|\\d+\\s*beds?|\\d+\\s*baths?)(?:\\b|$)/i.test(line);
340
+ }) || '';
341
+ const location = textLines.find(function(line) {
342
+ return line !== title
343
+ && line !== price
344
+ && line !== meta
345
+ && !isTimeLikeLine(line)
346
+ && !/show duplicates/i.test(line)
347
+ && !/^\\d+\\s*-\\s*\\d+\\s+of\\s+\\d+/i.test(line)
348
+ && !/^(?:US\\$|\\$)\\s?\\d[\\d,]*(?:\\.\\d{2})?$/i.test(line)
349
+ && line.length >= 3
350
+ && line.length <= 72;
351
+ }) || '';
352
+
353
+ const parts = [String(index + 1) + '. ' + (title || normalize(fullText, 120))];
354
+ if (price) parts.push('price=' + price);
355
+ if (location) parts.push('location=' + location);
356
+ if (meta) parts.push('meta=' + meta);
357
+ if (primaryLink && primaryLink.href) parts.push('url=' + normalize(primaryLink.href, 220));
358
+ return parts.join(' | ');
359
+ });
360
+
361
+ lines.push('- visible cards summarized: ' + String(visibleCards.length));
362
+ lines.push('');
363
+ lines.push('Visible items:');
364
+ lines.push.apply(lines, visibleCards);
365
+ if (roots.length > visibleCards.length) {
366
+ lines.push('... ' + String(roots.length - visibleCards.length) + ' more visible cards not expanded');
367
+ }
368
+
369
+ return lines.join('\\n');
370
+ };
371
+
372
+ const buildArticleSummary = function() {
373
+ const container = document.querySelector("article, main, [role='main']");
374
+ if (!container || !isVisible(container)) return null;
375
+
376
+ const paragraphs = Array.from(container.querySelectorAll('p'))
377
+ .filter(isVisible)
378
+ .map(function(node) { return normalize(node.innerText || node.textContent || '', 420); })
379
+ .filter(function(text) { return text.length >= 50; });
380
+ if (paragraphs.length < 3) return null;
381
+
382
+ const headingNode = container.querySelector('h1, h2, h3');
383
+ const heading = normalize((headingNode && (headingNode.innerText || headingNode.textContent)) || document.title || '', 180);
384
+ const lines = ['Article summary'];
385
+ if (heading) lines.push('- title: ' + heading);
386
+ lines.push('');
387
+ lines.push.apply(lines, paragraphs.slice(0, 8));
388
+ if (paragraphs.length > 8) {
389
+ lines.push('... ' + String(paragraphs.length - 8) + ' more paragraphs not expanded');
390
+ }
391
+ return lines.join('\\n');
392
+ };
393
+
394
+ if (requestedMode === 'raw') return bodyText;
395
+ if (requestedMode === 'table') return buildTableSummary() || bodyText;
396
+ if (requestedMode === 'list') return buildListSummary(true) || bodyText;
397
+ if (requestedMode === 'article') return buildArticleSummary() || bodyText;
398
+
399
+ const tableSummary = buildTableSummary();
400
+ if (tableSummary) return tableSummary;
401
+ const listSummary = buildListSummary(false);
402
+ if (listSummary) return listSummary;
403
+ return bodyText;
404
+ })()
405
+ `.trim();
406
+ }
407
+ function buildPageModelSource(options) {
408
+ const includeRawText = options?.includeRawText ?? false;
409
+ return `
410
+ (() => {
411
+ const includeRawText = ${includeRawText ? "true" : "false"};
412
+ const normalize = function(value, max) {
413
+ const collapsed = String(value || '')
414
+ .replace(/\\u200b/g, ' ')
415
+ .replace(/\\s+/g, ' ')
416
+ .trim();
417
+ if (!max || collapsed.length <= max) return collapsed;
418
+ return collapsed.slice(0, Math.max(0, max - 3)).trimEnd() + '...';
419
+ };
420
+ const normalizeLines = function(value) {
421
+ return String(value || '')
422
+ .split(/\\n+/)
423
+ .map(function(line) { return normalize(line, 0); })
424
+ .filter(Boolean);
425
+ };
426
+ const isVisible = function(element) {
427
+ if (!(element instanceof HTMLElement)) return false;
428
+ const style = window.getComputedStyle(element);
429
+ if (style.visibility === 'hidden' || style.display === 'none') return false;
430
+ const rect = element.getBoundingClientRect();
431
+ return rect.width >= 1
432
+ && rect.height >= 1
433
+ && rect.bottom > 0
434
+ && rect.right > 0
435
+ && rect.top < (window.innerHeight || document.documentElement.clientHeight) * 1.5;
436
+ };
437
+ const bodyText = normalize(document.body.innerText || document.body.textContent || '', 0);
438
+ const buildQueryData = function() {
439
+ const paramsObject = {};
440
+ const parts = [];
441
+ try {
442
+ const params = new URL(window.location.href).searchParams;
443
+ for (const pair of params.entries()) {
444
+ const key = pair[0];
445
+ const value = pair[1];
446
+ if (!key || key === 'tabId') continue;
447
+ paramsObject[key] = value;
448
+ if (parts.length < 6) {
449
+ parts.push(value ? key + '=' + normalize(value, 48) : key);
450
+ }
451
+ }
452
+ } catch {
453
+ return { summary: undefined, params: {} };
454
+ }
455
+ return { summary: parts.length > 0 ? parts.join(', ') : undefined, params: paramsObject };
456
+ };
457
+ const isTimeLikeLine = function(line) {
458
+ return /^(?:<\\s*)?\\d+\\s*(?:m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|week|weeks|mo|month|months)\\s+ago$/i.test(line)
459
+ || /^(?:today|yesterday)$/i.test(line)
460
+ || /^\\d{1,2}:\\d{2}\\s*(?:AM|PM)$/i.test(line);
461
+ };
462
+ const getAssociatedLabel = function(element) {
463
+ if (!(element instanceof HTMLElement)) return '';
464
+ const ariaLabel = normalize(element.getAttribute('aria-label') || '', 120);
465
+ if (ariaLabel) return ariaLabel;
466
+ const id = element.getAttribute('id');
467
+ if (id) {
468
+ const label = document.querySelector('label[for="' + CSS.escape(id) + '"]');
469
+ if (label instanceof HTMLElement) {
470
+ const text = normalize(label.innerText || label.textContent || '', 120);
471
+ if (text) return text;
472
+ }
473
+ }
474
+ const wrappingLabel = element.closest('label');
475
+ if (wrappingLabel instanceof HTMLElement) {
476
+ const text = normalize(wrappingLabel.innerText || wrappingLabel.textContent || '', 120);
477
+ if (text) return text;
478
+ }
479
+ return '';
480
+ };
481
+ const getContextText = function(element) {
482
+ if (!(element instanceof HTMLElement)) return '';
483
+ const seen = new Set();
484
+ const parts = [];
485
+ let current = element.parentElement;
486
+ let depth = 0;
487
+ while (current && current !== document.body && depth < 5) {
488
+ const directContext = current.querySelector(':scope > legend, :scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > .label, :scope > .title, :scope > button, :scope > strong');
489
+ if (directContext instanceof HTMLElement) {
490
+ const text = normalize(directContext.innerText || directContext.textContent || '', 120);
491
+ if (text && !seen.has(text)) {
492
+ seen.add(text);
493
+ parts.push(text);
494
+ }
495
+ }
496
+ const ownText = normalize(current.getAttribute('aria-label') || '', 120);
497
+ if (ownText && !seen.has(ownText)) {
498
+ seen.add(ownText);
499
+ parts.push(ownText);
500
+ }
501
+ current = current.parentElement;
502
+ depth += 1;
503
+ }
504
+ return parts.slice(0, 3).join(' | ');
505
+ };
506
+ const visibleInputs = Array.from(document.querySelectorAll('input, select, textarea'))
507
+ .filter(isVisible)
508
+ .map(function(element) {
509
+ if (!(element instanceof HTMLElement)) return null;
510
+ const result = {
511
+ type: normalize(element.getAttribute('type') || element.tagName || '', 24).toLowerCase(),
512
+ label: getAssociatedLabel(element),
513
+ placeholder: normalize(element.getAttribute('placeholder') || '', 80),
514
+ name: normalize(element.getAttribute('name') || element.getAttribute('id') || '', 80),
515
+ context: getContextText(element),
516
+ };
517
+ if ('value' in element && typeof element.value === 'string') {
518
+ const value = normalize(element.value || '', 80);
519
+ if (value) return { ...result, value };
520
+ }
521
+ return result;
522
+ })
523
+ .filter(Boolean);
524
+ const detectFiltersVisible = function() {
525
+ const bodyClass = normalize(document.body.className || '', 200);
526
+ if (/\\b(?:cl-show-filters|show-filters|filters-open|open-filters)\\b/i.test(bodyClass)) {
527
+ return true;
528
+ }
529
+ const containers = Array.from(document.querySelectorAll("aside, form, [role='complementary'], [class*='filter'], [id*='filter'], [data-testid*='filter'], [data-test*='filter']")).filter(isVisible);
530
+ const hasVisibleApply = Array.from(document.querySelectorAll('button, [role="button"]'))
531
+ .filter(isVisible)
532
+ .some(function(node) {
533
+ const text = normalize(node.innerText || node.textContent || '', 80);
534
+ return /^(apply|reset|update search|search)$/i.test(text);
535
+ });
536
+ const strongContainer = containers.some(function(container) {
537
+ const controls = Array.from(container.querySelectorAll('input, select, textarea, button, [role="button"], [role="checkbox"], [role="switch"]')).filter(isVisible);
538
+ const text = normalize(container.innerText || container.textContent || '', 500);
539
+ return controls.length >= 4 && (/\\b(filter|price|rent|beds?|baths?|sqft|square footage|housing type|laundry|parking|pets|amenities|neighborhood)\\b/i.test(text) || /\\b(apply|reset|update search)\\b/i.test(text));
540
+ });
541
+ return strongContainer || (visibleInputs.length >= 4 && hasVisibleApply);
542
+ };
543
+ const buildTableSummary = function() {
544
+ const tables = Array.from(document.querySelectorAll('table')).filter(isVisible);
545
+ if (tables.length === 0) return null;
546
+ const candidates = tables
547
+ .map(function(table) {
548
+ const rows = Array.from(table.querySelectorAll('tr'))
549
+ .map(function(row) {
550
+ return Array.from(row.querySelectorAll('th, td'))
551
+ .map(function(cell) { return normalize(cell.innerText || cell.textContent || '', 80); })
552
+ .filter(Boolean);
553
+ })
554
+ .filter(function(row) { return row.length > 0; });
555
+ const columns = rows[0] || [];
556
+ const dataRows = rows.slice(columns.length > 0 ? 1 : 0);
557
+ const captionNode = table.querySelector('caption');
558
+ const caption = normalize(table.getAttribute('aria-label') || (captionNode && (captionNode.innerText || captionNode.textContent)) || '', 120);
559
+ const weekdayHeaders = columns.filter(function(cell) { return /^(?:s|m|t|w|f|sa|su|sun|mon|tue|wed|thu|fri|sat)$/i.test(cell); }).length;
560
+ const calendarLike = columns.length === 7 && weekdayHeaders >= 6;
561
+ return {
562
+ caption,
563
+ columns,
564
+ rows: dataRows.slice(0, 12),
565
+ visibleRows: dataRows.length,
566
+ calendarLike,
567
+ score: (columns.length * 2) + dataRows.length + (caption ? 2 : 0) - (calendarLike ? 4 : 0),
568
+ };
569
+ })
570
+ .filter(function(candidate) { return candidate.columns.length >= 2 && candidate.visibleRows >= 2; })
571
+ .sort(function(a, b) { return b.score - a.score; });
572
+ return candidates[0] || null;
573
+ };
574
+ const findCardRoot = function(anchor) {
575
+ let best = null;
576
+ let bestScore = Number.NEGATIVE_INFINITY;
577
+ let current = anchor;
578
+ let depth = 0;
579
+ while (current && current !== document.body && depth < 6) {
580
+ if (isVisible(current)) {
581
+ const text = normalize(current.innerText || current.textContent || '', 900);
582
+ const linkCount = Array.from(current.querySelectorAll('a[href]')).filter(isVisible).length;
583
+ const tag = current.tagName.toLowerCase();
584
+ const rect = current.getBoundingClientRect();
585
+ let score = 0;
586
+ if (text.length >= 24 && text.length <= 900) score += 4;
587
+ if (['article', 'li', 'section', 'tr'].includes(tag)) score += 3;
588
+ if (tag === 'div') score += 1;
589
+ if (linkCount >= 1 && linkCount <= 6) score += 2;
590
+ if (/\\$\\s?\\d[\\d,]*/.test(text)) score += 2;
591
+ if (/(?:^|\\b)(studio|\\d+\\s*br|\\d+\\s*ba|\\d+\\s*ft2|\\d+\\s*beds?|\\d+\\s*baths?)(?:\\b|$)/i.test(text)) score += 2;
592
+ if (rect.width >= 140 && rect.height >= 48) score += 2;
593
+ const parent = current.parentElement;
594
+ if (parent) {
595
+ const siblingCount = Array.from(parent.children).filter(function(child) {
596
+ if (!(child instanceof HTMLElement)) return false;
597
+ if (child.tagName !== current.tagName) return false;
598
+ return isVisible(child) && !!child.querySelector('a[href]');
599
+ }).length;
600
+ if (siblingCount >= 3) score += Math.min(5, siblingCount);
601
+ }
602
+ if (score > bestScore) {
603
+ best = current;
604
+ bestScore = score;
605
+ }
606
+ }
607
+ current = current.parentElement;
608
+ depth += 1;
609
+ }
610
+ return best || anchor;
611
+ };
612
+ const buildListSummary = function(force) {
613
+ const mainRoot = document.querySelector("main, [role='main'], #main, .main") || document.body;
614
+ const anchors = Array.from(mainRoot.querySelectorAll('a[href]'))
615
+ .filter(function(anchor) { return anchor instanceof HTMLAnchorElement; })
616
+ .filter(function(anchor) {
617
+ if (!isVisible(anchor)) return false;
618
+ const href = normalize(anchor.href, 0);
619
+ const text = normalize(anchor.innerText || anchor.textContent || '', 0);
620
+ if (!href || href.startsWith('javascript:') || href === window.location.href) return false;
621
+ return text.length >= 8;
622
+ });
623
+ const seenRoots = new Set();
624
+ const roots = anchors
625
+ .map(function(anchor) { return findCardRoot(anchor); })
626
+ .filter(function(root) {
627
+ if (seenRoots.has(root)) return false;
628
+ seenRoots.add(root);
629
+ return true;
630
+ })
631
+ .map(function(root) {
632
+ return { root, top: root.getBoundingClientRect().top + window.scrollY, text: normalize(root.innerText || root.textContent || '', 1200) };
633
+ })
634
+ .filter(function(entry) { return entry.text.length >= 24; })
635
+ .sort(function(a, b) { return a.top - b.top; });
636
+ const strongCards = roots.filter(function(entry) {
637
+ return /\\$\\s?\\d[\\d,]*/.test(entry.text) || /(?:^|\\b)(studio|\\d+\\s*br|\\d+\\s*ba|\\d+\\s*ft2|\\d+\\s*beds?|\\d+\\s*baths?)(?:\\b|$)/i.test(entry.text);
638
+ });
639
+ if (!force && (roots.length < 5 || strongCards.length < Math.min(3, roots.length))) return null;
640
+ if (roots.length === 0) return null;
641
+ const totalResultsMatch = bodyText.match(/\\b\\d+\\s*-\\s*\\d+\\s+of\\s+\\d[\\d,]*\\b/i);
642
+ const queryData = buildQueryData();
643
+ const cards = roots.slice(0, 30).map(function(entry) {
644
+ const root = entry.root;
645
+ const fullText = normalize(root.innerText || root.textContent || '', 1400);
646
+ const textLines = normalizeLines(root.innerText || root.textContent || '');
647
+ const visibleLinks = Array.from(root.querySelectorAll('a[href]')).filter(function(link) { return link instanceof HTMLAnchorElement; }).filter(isVisible);
648
+ const primaryLink = visibleLinks
649
+ .map(function(link) {
650
+ const text = normalize(link.innerText || link.textContent || '', 180);
651
+ return { link, text, score: Math.min(normalize(link.innerText || link.textContent || '', 0).length, 180) };
652
+ })
653
+ .filter(function(candidate) { return candidate.text.length >= 8; })
654
+ .sort(function(a, b) { return b.score - a.score; })[0]?.link || visibleLinks[0] || null;
655
+ const heading = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6')).filter(isVisible).map(function(node) { return normalize(node.innerText || node.textContent || '', 180); }).find(Boolean) || '';
656
+ const title = heading || normalize((primaryLink && (primaryLink.innerText || primaryLink.textContent)) || '', 180);
657
+ const priceMatch = fullText.match(/(?:US\\$|\\$)\\s?\\d[\\d,]*(?:\\.\\d{2})?/);
658
+ const price = priceMatch ? priceMatch[0] : '';
659
+ const meta = textLines.find(function(line) { return /(?:^|\\b)(studio|\\d+\\s*br|\\d+\\s*ba|\\d+\\s*ft2|\\d+\\s*beds?|\\d+\\s*baths?)(?:\\b|$)/i.test(line); }) || '';
660
+ const location = textLines.find(function(line) {
661
+ return line !== title && line !== price && line !== meta && !isTimeLikeLine(line) && !/show duplicates/i.test(line) && !/^\\d+\\s*-\\s*\\d+\\s+of\\s+\\d+/i.test(line) && !/^(?:US\\$|\\$)\\s?\\d[\\d,]*(?:\\.\\d{2})?$/i.test(line) && line.length >= 3 && line.length <= 72;
662
+ }) || '';
663
+ return {
664
+ title: title || normalize(fullText, 120),
665
+ ...(price ? { price } : {}),
666
+ ...(location ? { location } : {}),
667
+ ...(meta ? { meta } : {}),
668
+ ...(primaryLink && primaryLink.href ? { url: normalize(primaryLink.href, 220) } : {}),
669
+ };
670
+ });
671
+ return {
672
+ totalResults: totalResultsMatch ? totalResultsMatch[0] : undefined,
673
+ querySummary: queryData.summary,
674
+ visibleCards: roots.length,
675
+ hiddenVisibleCards: Math.max(0, roots.length - Math.min(roots.length, 30)),
676
+ strongCardCount: strongCards.length,
677
+ score: roots.length + (strongCards.length * 2) + (totalResultsMatch ? 3 : 0),
678
+ cards,
679
+ };
680
+ };
681
+ const buildArticleSummary = function() {
682
+ const container = document.querySelector("article, main, [role='main']");
683
+ if (!container || !isVisible(container)) return null;
684
+ const paragraphs = Array.from(container.querySelectorAll('p')).filter(isVisible).map(function(node) { return normalize(node.innerText || node.textContent || '', 420); }).filter(function(text) { return text.length >= 50; });
685
+ if (paragraphs.length < 3) return null;
686
+ const headingNode = container.querySelector('h1, h2, h3');
687
+ const title = normalize((headingNode && (headingNode.innerText || headingNode.textContent)) || document.title || '', 180);
688
+ return { ...(title ? { title } : {}), paragraphs, score: paragraphs.length * 2 + (title ? 2 : 0) };
689
+ };
690
+ const queryData = buildQueryData();
691
+ const listSummary = buildListSummary(false);
692
+ const tableSummary = buildTableSummary();
693
+ const articleSummary = buildArticleSummary();
694
+ const filtersVisible = detectFiltersVisible();
695
+ const auxiliarySections = [];
696
+ let primaryContent = 'generic';
697
+ let confidence = 0.25;
698
+ if (listSummary && (!tableSummary || tableSummary.calendarLike || listSummary.score >= tableSummary.score + 1)) {
699
+ primaryContent = 'result_list';
700
+ confidence = Math.min(0.98, 0.55 + (listSummary.strongCardCount / Math.max(3, listSummary.cards.length)));
701
+ if (tableSummary) auxiliarySections.push(tableSummary.calendarLike ? 'calendar_table' : 'table');
702
+ } else if (tableSummary && tableSummary.score >= 7) {
703
+ primaryContent = 'table';
704
+ confidence = Math.min(0.95, 0.45 + (tableSummary.score / 30));
705
+ } else if (articleSummary && articleSummary.score >= 8) {
706
+ primaryContent = 'article';
707
+ confidence = Math.min(0.92, 0.4 + (articleSummary.score / 40));
708
+ } else if (filtersVisible && visibleInputs.length >= 3) {
709
+ primaryContent = 'form';
710
+ confidence = 0.6;
711
+ }
712
+ return {
713
+ primaryContent,
714
+ confidence,
715
+ ...(queryData.summary ? { querySummary: queryData.summary } : {}),
716
+ queryParams: queryData.params,
717
+ filtersVisible,
718
+ visibleInputs,
719
+ auxiliarySections,
720
+ ...(listSummary ? { listSummary } : {}),
721
+ ...(tableSummary ? { tableSummary } : {}),
722
+ ...(articleSummary ? { articleSummary } : {}),
723
+ ...(includeRawText ? { rawText: bodyText } : {}),
724
+ };
725
+ })()
726
+ `.trim();
727
+ }
728
+ function isLikelySingleExpression(source) {
729
+ const trimmed = source.trim().replace(/;$/, "");
730
+ return !trimmed.includes("\n")
731
+ && !trimmed.includes(";")
732
+ && !/\b(return|const|let|var|if|for|while|switch|try|class|function)\b/.test(trimmed);
733
+ }
734
+ function getEvaluationFallback(expression, error) {
735
+ const message = error instanceof Error ? error.message : String(error);
736
+ const source = expression.trim();
737
+ if (!source) {
738
+ return null;
739
+ }
740
+ if (message.includes("await is only valid")) {
741
+ if (isLikelySingleExpression(source)) {
742
+ return {
743
+ source: `(async () => (${source.replace(/;$/, "")}))()`,
744
+ mode: "async",
745
+ };
746
+ }
747
+ return {
748
+ source: `(async () => {\n${source}\n})()`,
749
+ mode: "async",
750
+ };
751
+ }
752
+ if (message.includes("Illegal return statement")) {
753
+ return {
754
+ source: `(() => {\n${source}\n})()`,
755
+ mode: "sync",
756
+ };
757
+ }
758
+ return null;
759
+ }
760
+ function buildCapturedEvaluationSource(expression) {
761
+ const source = expression.trim();
762
+ const setup = `
763
+ const __capture = [];
764
+ const __truncate = (value, max = 280) => value.length > max ? value.slice(0, max - 3) + "..." : value;
765
+ const __serialize = (value) => {
766
+ if (typeof value === "string") return __truncate(value);
767
+ if (value instanceof Error) return __truncate(value.stack || value.message || String(value));
768
+ try {
769
+ const json = JSON.stringify(value);
770
+ if (typeof json === "string") return __truncate(json);
771
+ } catch {}
772
+ return __truncate(String(value));
773
+ };
774
+ const __record = (type, args) => {
775
+ if (__capture.length >= 50) return;
776
+ __capture.push({
777
+ type,
778
+ text: __truncate(args.map((arg) => __serialize(arg)).join(" "), 500),
779
+ });
780
+ };
781
+ const __origConsole = {
782
+ log: console.log,
783
+ info: console.info,
784
+ warn: console.warn,
785
+ error: console.error,
786
+ debug: console.debug,
787
+ };
788
+ for (const __key of Object.keys(__origConsole)) {
789
+ console[__key] = (...args) => {
790
+ __record(__key, args);
791
+ };
792
+ }
793
+ `;
794
+ const teardown = `
795
+ for (const __key of Object.keys(__origConsole)) {
796
+ console[__key] = __origConsole[__key];
797
+ }
798
+ `;
799
+ if (isLikelySingleExpression(source)) {
800
+ const expr = source.replace(/;$/, "");
801
+ return `(async () => {
802
+ ${setup}
803
+ try {
804
+ const __result = (${expr});
805
+ return { result: await __result, console: __capture };
806
+ } finally {
807
+ ${teardown}
808
+ }
809
+ })()`;
810
+ }
811
+ return `(async () => {
812
+ ${setup}
813
+ try {
814
+ const __result = await (async () => {
815
+ ${source}
816
+ })();
817
+ return { result: __result, console: __capture };
818
+ } finally {
819
+ ${teardown}
820
+ }
821
+ })()`;
822
+ }
823
+ // ===== Tab Group =====
824
+ const TAB_GROUP_COLORS = ["blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
825
+ // ===== Tab ID Generator =====
826
+ let _tabCounter = 0;
827
+ function nextTabId() {
828
+ _tabCounter++;
829
+ return `tab-${String(_tabCounter).padStart(3, "0")}`;
830
+ }
831
+ // ===== BrowserSession =====
832
+ export class BrowserSession {
833
+ // Singleton registry
834
+ static _instances = new Map();
835
+ // Connection
836
+ _browser = null;
837
+ _context = null;
838
+ // Pages
839
+ _pages = new Map();
840
+ _page = null;
841
+ _currentTabId = null;
842
+ _snapshotCache = new WeakMap();
843
+ _executorCache = new WeakMap();
844
+ _pageIds = new WeakMap();
845
+ _pageListeners = new WeakSet();
846
+ _pageRegisteredListeners = new Set();
847
+ _contextListenersAttached = false;
848
+ /** Patched UA string for CDP-level override (covers Web Workers). */
849
+ _patchedUa = null;
850
+ // Tab Groups
851
+ _tabGroups = new Map();
852
+ _colorIndex = 0;
853
+ // Components
854
+ snapshot = null;
855
+ executor = null;
856
+ // Connection mutex
857
+ _connectPromise = null;
858
+ // Config
859
+ _sessionId;
860
+ _config;
861
+ constructor(sessionId, config) {
862
+ this._sessionId = sessionId;
863
+ this._config = config;
864
+ }
865
+ // ===== Public getters =====
866
+ get sessionId() {
867
+ return this._sessionId;
868
+ }
869
+ get isConnected() {
870
+ if (this._config.mode === "managed") {
871
+ return this._context !== null;
872
+ }
873
+ return this._browser?.isConnected() ?? false;
874
+ }
875
+ get currentPage() {
876
+ return this._page;
877
+ }
878
+ get currentTabId() {
879
+ return this._currentTabId;
880
+ }
881
+ /** Public getter for pages map (used by BehaviorRecorder). */
882
+ get pages() {
883
+ return this._pages;
884
+ }
885
+ onPageRegistered(listener) {
886
+ this._pageRegisteredListeners.add(listener);
887
+ return () => {
888
+ this._pageRegisteredListeners.delete(listener);
889
+ };
890
+ }
891
+ _getSnapshotForPage(page) {
892
+ let snapshot = this._snapshotCache.get(page);
893
+ if (!snapshot) {
894
+ snapshot = new PageSnapshot(page);
895
+ this._snapshotCache.set(page, snapshot);
896
+ }
897
+ return snapshot;
898
+ }
899
+ _getExecutorForPage(page) {
900
+ let executor = this._executorCache.get(page);
901
+ if (!executor) {
902
+ executor = new ActionExecutor(page, this);
903
+ this._executorCache.set(page, executor);
904
+ }
905
+ return executor;
906
+ }
907
+ _attachCurrentPage(tabId, page) {
908
+ this._currentTabId = tabId;
909
+ this._page = page;
910
+ this.snapshot = this._getSnapshotForPage(page);
911
+ this.executor = this._getExecutorForPage(page);
912
+ }
913
+ async _emitPageRegistered(tabId, page) {
914
+ for (const listener of this._pageRegisteredListeners) {
915
+ try {
916
+ await listener(tabId, page);
917
+ }
918
+ catch (e) {
919
+ logger.warn({ tabId, err: e }, "Page registered listener failed");
920
+ }
921
+ }
922
+ }
923
+ async _registerPage(page, options) {
924
+ const existingTabId = this._pageIds.get(page);
925
+ if (existingTabId) {
926
+ if (options?.group) {
927
+ options.group.tabs.set(existingTabId, page);
928
+ }
929
+ if (options?.makeCurrent) {
930
+ this._attachCurrentPage(existingTabId, page);
931
+ }
932
+ return { tabId: existingTabId, isNew: false };
933
+ }
934
+ const tabId = options?.tabId ?? nextTabId();
935
+ this._pages.set(tabId, page);
936
+ this._pageIds.set(page, tabId);
937
+ if (options?.group) {
938
+ options.group.tabs.set(tabId, page);
939
+ }
940
+ this._setupPageListeners(tabId, page);
941
+ if (options?.makeCurrent || !this._page) {
942
+ this._attachCurrentPage(tabId, page);
943
+ }
944
+ await this._emitPageRegistered(tabId, page);
945
+ return { tabId, isNew: true };
946
+ }
947
+ async _resolvePage(tabId, options) {
948
+ await this.ensureBrowser();
949
+ if (tabId) {
950
+ const page = this._pages.get(tabId);
951
+ if (!page || page.isClosed()) {
952
+ throw new Error(`Tab not found: ${tabId}`);
953
+ }
954
+ return { tabId, page };
955
+ }
956
+ if (this._page && !this._page.isClosed()) {
957
+ return { tabId: this._currentTabId, page: this._page };
958
+ }
959
+ if (options?.createIfMissing) {
960
+ const page = await this.getPage();
961
+ return { tabId: this._currentTabId, page };
962
+ }
963
+ return { tabId: null, page: null };
964
+ }
965
+ _normalizeNavigationTimeout(timeout) {
966
+ const parsed = Number(timeout);
967
+ if (!Number.isFinite(parsed) || parsed <= 0) {
968
+ return BrowserConfig.navigationTimeout;
969
+ }
970
+ return Math.floor(parsed);
971
+ }
972
+ _remainingTimeout(deadline) {
973
+ return Math.max(1, deadline - Date.now());
974
+ }
975
+ async _navigatePage(page, url, timeout) {
976
+ const deadline = Date.now() + this._normalizeNavigationTimeout(timeout);
977
+ await page.goto(url, {
978
+ timeout: this._remainingTimeout(deadline),
979
+ waitUntil: "domcontentloaded",
980
+ });
981
+ const idleTimeout = Math.min(BrowserConfig.networkIdleTimeout, this._remainingTimeout(deadline));
982
+ if (idleTimeout <= 0) {
983
+ return;
984
+ }
985
+ try {
986
+ await page.waitForLoadState("networkidle", {
987
+ timeout: idleTimeout,
988
+ });
989
+ }
990
+ catch {
991
+ // networkidle timeout is acceptable — DOM content already loaded
992
+ }
993
+ }
994
+ // ===== Factory / Singleton =====
995
+ static create(config, sessionId = "default") {
996
+ const existing = BrowserSession._instances.get(sessionId);
997
+ if (existing) {
998
+ logger.warn({ sessionId }, "Session already exists — returning existing instance (new config ignored)");
999
+ return existing;
1000
+ }
1001
+ const instance = new BrowserSession(sessionId, config);
1002
+ BrowserSession._instances.set(sessionId, instance);
1003
+ return instance;
1004
+ }
1005
+ static getInstance(sessionId) {
1006
+ return BrowserSession._instances.get(sessionId) ?? null;
1007
+ }
1008
+ // ===== Connection =====
1009
+ async ensureBrowser() {
1010
+ if (this._config.mode === "managed") {
1011
+ if (this._context)
1012
+ return;
1013
+ }
1014
+ else {
1015
+ if (this._browser?.isConnected())
1016
+ return;
1017
+ }
1018
+ // Mutex: deduplicate concurrent connection attempts
1019
+ if (this._connectPromise)
1020
+ return this._connectPromise;
1021
+ this._connectPromise = this._doConnect();
1022
+ try {
1023
+ await this._connectPromise;
1024
+ }
1025
+ finally {
1026
+ this._connectPromise = null;
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Try local Chrome first, then Playwright's bundled Chromium.
1031
+ * Avoids requiring `npx playwright install` when Chrome is already installed.
1032
+ */
1033
+ async _launchStandalone() {
1034
+ const headless = this._config.headless ?? true;
1035
+ const stealthOpts = {
1036
+ args: ['--disable-blink-features=AutomationControlled'],
1037
+ ignoreDefaultArgs: ['--enable-automation'],
1038
+ };
1039
+ // Try local Chrome / Edge first
1040
+ for (const channel of ["chrome", "msedge"]) {
1041
+ try {
1042
+ const browser = await chromium.launch({ channel, headless, ...stealthOpts });
1043
+ logger.info({ channel }, "Using local browser");
1044
+ return browser;
1045
+ }
1046
+ catch {
1047
+ // Not installed — try next
1048
+ }
1049
+ }
1050
+ // Fall back to Playwright's bundled Chromium
1051
+ logger.info("No local Chrome/Edge found — using Playwright Chromium");
1052
+ return chromium.launch({ headless, ...stealthOpts });
1053
+ }
1054
+ async _doConnect() {
1055
+ switch (this._config.mode) {
1056
+ case "cdp": {
1057
+ if (!this._config.cdpUrl) {
1058
+ throw new Error("cdpUrl required for 'cdp' mode");
1059
+ }
1060
+ logger.info({ cdpUrl: this._config.cdpUrl, sessionId: this._sessionId }, "Connecting via CDP");
1061
+ this._browser = await chromium.connectOverCDP(this._config.cdpUrl);
1062
+ const contexts = this._browser.contexts();
1063
+ if (contexts.length === 0) {
1064
+ throw new Error("No browser contexts found via CDP");
1065
+ }
1066
+ this._context = contexts[0];
1067
+ this._setupContextListeners();
1068
+ logger.info({ contexts: contexts.length, pages: this._context.pages().length }, "CDP connection established");
1069
+ // Register existing pages
1070
+ for (const page of this._context.pages()) {
1071
+ const url = page.url();
1072
+ if (url && url !== "about:blank" && !page.isClosed()) {
1073
+ await this._registerPage(page, { makeCurrent: !this._page });
1074
+ }
1075
+ }
1076
+ break;
1077
+ }
1078
+ case "standalone": {
1079
+ logger.info({ headless: this._config.headless ?? true, sessionId: this._sessionId }, "Launching standalone browser");
1080
+ const viewport = this._config.viewport || { width: BrowserConfig.viewportWidth, height: BrowserConfig.viewportHeight };
1081
+ this._browser = await this._launchStandalone();
1082
+ // Resolve User-Agent: explicit config > auto-patch headless UA > browser default
1083
+ let ua = this._config.userAgent || getUserAgent();
1084
+ if (!ua && (this._config.headless ?? true)) {
1085
+ // Headless Chrome reports "HeadlessChrome/..." in its default UA.
1086
+ // Open a throwaway context to grab the real UA string, then patch it.
1087
+ const probe = await this._browser.newContext();
1088
+ const probePage = await probe.newPage();
1089
+ const rawUa = await probePage.evaluate(() => navigator.userAgent);
1090
+ await probe.close();
1091
+ if (rawUa.includes("HeadlessChrome")) {
1092
+ ua = patchHeadlessUserAgent(rawUa);
1093
+ this._patchedUa = ua;
1094
+ logger.info({ patched: true }, "Patched headless User-Agent");
1095
+ }
1096
+ }
1097
+ const contextOpts = {
1098
+ viewport,
1099
+ };
1100
+ if (ua) {
1101
+ contextOpts.userAgent = ua;
1102
+ }
1103
+ if (this._config.stealthHeaders !== false) {
1104
+ Object.assign(contextOpts, getStealthContextOptions());
1105
+ }
1106
+ this._context = await this._browser.newContext(contextOpts);
1107
+ this._setupContextListeners();
1108
+ logger.info("Standalone browser launched");
1109
+ break;
1110
+ }
1111
+ case "managed": {
1112
+ if (!this._config.profileDir) {
1113
+ throw new Error("profileDir required for 'managed' mode");
1114
+ }
1115
+ logger.info({ profileDir: this._config.profileDir, sessionId: this._sessionId }, "Launching managed browser");
1116
+ const managedHeadless = this._config.headless ?? true;
1117
+ let managedUa = this._config.userAgent || getUserAgent();
1118
+ // Patch headless UA for managed mode: probe a temporary standalone browser
1119
+ if (!managedUa && managedHeadless) {
1120
+ try {
1121
+ const probeBrowser = await chromium.launch({ channel: "chrome", headless: true });
1122
+ const probeCtx = await probeBrowser.newContext();
1123
+ const probePage = await probeCtx.newPage();
1124
+ const rawUa = await probePage.evaluate(() => navigator.userAgent);
1125
+ await probeBrowser.close();
1126
+ if (rawUa.includes("HeadlessChrome")) {
1127
+ managedUa = patchHeadlessUserAgent(rawUa);
1128
+ this._patchedUa = managedUa;
1129
+ logger.info({ patched: true }, "Patched headless User-Agent (managed)");
1130
+ }
1131
+ }
1132
+ catch {
1133
+ // Probe failed — proceed without UA patch
1134
+ }
1135
+ }
1136
+ const managedViewport = this._config.viewport || { width: BrowserConfig.viewportWidth, height: BrowserConfig.viewportHeight };
1137
+ const contextOpts2 = {
1138
+ headless: managedHeadless,
1139
+ viewport: managedViewport,
1140
+ args: ['--disable-blink-features=AutomationControlled'],
1141
+ ignoreDefaultArgs: ['--enable-automation'],
1142
+ };
1143
+ if (managedUa) {
1144
+ contextOpts2.userAgent = managedUa;
1145
+ }
1146
+ if (this._config.stealthHeaders !== false) {
1147
+ Object.assign(contextOpts2, getStealthContextOptions());
1148
+ }
1149
+ this._context = await chromium.launchPersistentContext(this._config.profileDir, contextOpts2);
1150
+ this._setupContextListeners();
1151
+ // PersistentContext doesn't have a separate Browser object
1152
+ // but context.browser() may return the underlying browser
1153
+ this._browser = this._context.browser();
1154
+ logger.info("Managed browser launched");
1155
+ break;
1156
+ }
1157
+ }
1158
+ }
1159
+ // ===== Page Lifecycle =====
1160
+ /** Unified tab cleanup — removes from _pages, tab groups, and picks next tab if needed. */
1161
+ _cleanupTab(tabId) {
1162
+ this._pages.delete(tabId);
1163
+ // Remove from any tab group
1164
+ for (const group of this._tabGroups.values()) {
1165
+ group.tabs.delete(tabId);
1166
+ }
1167
+ // If this was the current tab, pick the next non-closed one
1168
+ if (this._currentTabId === tabId) {
1169
+ let found = false;
1170
+ for (const [nextId, nextPage] of this._pages) {
1171
+ if (!nextPage.isClosed()) {
1172
+ this._attachCurrentPage(nextId, nextPage);
1173
+ found = true;
1174
+ break;
1175
+ }
1176
+ }
1177
+ if (!found) {
1178
+ this._currentTabId = null;
1179
+ this._page = null;
1180
+ this.snapshot = null;
1181
+ this.executor = null;
1182
+ }
1183
+ }
1184
+ }
1185
+ _setupContextListeners() {
1186
+ if (!this._context || this._contextListenersAttached) {
1187
+ return;
1188
+ }
1189
+ this._context.addInitScript(() => {
1190
+ // --- navigator.webdriver ---
1191
+ // Playwright sets navigator.webdriver = true. We override it to false
1192
+ // and make it non-configurable so CDP cannot re-define it.
1193
+ Object.defineProperty(Navigator.prototype, 'webdriver', {
1194
+ get: () => false,
1195
+ configurable: false,
1196
+ enumerable: true,
1197
+ });
1198
+ });
1199
+ this._context.on("page", (page) => {
1200
+ this._handleNewPage(page).catch((e) => logger.warn({ err: e }, "Error handling context page"));
1201
+ });
1202
+ this._contextListenersAttached = true;
1203
+ }
1204
+ _setupPageListeners(tabId, page) {
1205
+ if (this._pageListeners.has(page)) {
1206
+ return;
1207
+ }
1208
+ this._pageListeners.add(page);
1209
+ page.on("popup", (popup) => this._handleNewPage(popup).catch((e) => logger.warn({ err: e }, "Error handling popup")));
1210
+ page.on("close", () => {
1211
+ this._cleanupTab(tabId);
1212
+ });
1213
+ page.on("crash", () => {
1214
+ logger.error({ tabId }, "Page crashed — removing from registry");
1215
+ this._cleanupTab(tabId);
1216
+ });
1217
+ }
1218
+ async _handleNewPage(page) {
1219
+ const { tabId, isNew } = await this._registerPage(page);
1220
+ if (isNew) {
1221
+ logger.info({ tabId, url: page.url() }, "New page auto-registered");
1222
+ }
1223
+ }
1224
+ // ===== Page Access =====
1225
+ async getPage() {
1226
+ await this.ensureBrowser();
1227
+ if (!this._page) {
1228
+ // Create a new page in standalone/managed mode
1229
+ if (!this._context) {
1230
+ throw new Error("No browser context available");
1231
+ }
1232
+ const page = await this._context.newPage();
1233
+ await this._registerPage(page, { makeCurrent: true });
1234
+ }
1235
+ return this._page;
1236
+ }
1237
+ // ===== Navigation =====
1238
+ async getPageForTab(tabId, options) {
1239
+ const resolved = await this._resolvePage(tabId, options);
1240
+ return resolved.page;
1241
+ }
1242
+ async getPageInfo(tabId) {
1243
+ const resolved = await this._resolvePage(tabId);
1244
+ const page = resolved.page;
1245
+ if (!page || page.isClosed()) {
1246
+ return {
1247
+ tabId: resolved.tabId,
1248
+ url: "",
1249
+ title: "",
1250
+ };
1251
+ }
1252
+ let title = "";
1253
+ try {
1254
+ title = await page.title();
1255
+ }
1256
+ catch {
1257
+ // ignore transient title failures
1258
+ }
1259
+ return {
1260
+ tabId: resolved.tabId,
1261
+ url: page.url(),
1262
+ title,
1263
+ };
1264
+ }
1265
+ async visit(url, options) {
1266
+ const resolved = await this._resolvePage(options?.tabId, {
1267
+ createIfMissing: !options?.tabId,
1268
+ });
1269
+ const page = resolved.page;
1270
+ if (!page) {
1271
+ throw new Error("No active page");
1272
+ }
1273
+ await this._navigatePage(page, url, options?.timeout);
1274
+ return `Navigated to ${page.url()}`;
1275
+ }
1276
+ // ===== Snapshot =====
1277
+ async getSnapshot(options) {
1278
+ const resolved = await this._resolvePage(options?.tabId);
1279
+ if (!resolved.page)
1280
+ return "<empty>";
1281
+ return this._getSnapshotForPage(resolved.page).capture(options);
1282
+ }
1283
+ async getSnapshotWithElements(options) {
1284
+ const resolved = await this._resolvePage(options?.tabId);
1285
+ if (!resolved.page) {
1286
+ return { snapshotText: "<empty>", elements: {} };
1287
+ }
1288
+ return this._getSnapshotForPage(resolved.page).getFullResult(options);
1289
+ }
1290
+ getLastElements(tabId) {
1291
+ if (tabId) {
1292
+ const page = this._pages.get(tabId);
1293
+ if (!page || page.isClosed()) {
1294
+ return {};
1295
+ }
1296
+ return this._getSnapshotForPage(page).getLastElements();
1297
+ }
1298
+ return this.snapshot?.getLastElements() ?? {};
1299
+ }
1300
+ // ===== Action Execution =====
1301
+ async execAction(action, tabId) {
1302
+ const resolved = await this._resolvePage(tabId);
1303
+ if (!resolved.page) {
1304
+ return { success: false, message: "No executor available", details: {} };
1305
+ }
1306
+ const executor = resolved.page === this._page && this.executor
1307
+ ? this.executor
1308
+ : this._getExecutorForPage(resolved.page);
1309
+ return executor.execute(action);
1310
+ }
1311
+ // ===== Tab Management =====
1312
+ async getTabInfo() {
1313
+ const tabs = [];
1314
+ for (const [tabId, page] of this._pages) {
1315
+ try {
1316
+ tabs.push({
1317
+ tab_id: tabId,
1318
+ url: page.isClosed() ? "(closed)" : page.url(),
1319
+ title: page.isClosed() ? "(closed)" : await page.title(),
1320
+ is_current: tabId === this._currentTabId,
1321
+ });
1322
+ }
1323
+ catch {
1324
+ tabs.push({
1325
+ tab_id: tabId,
1326
+ url: "(error)",
1327
+ title: "(error)",
1328
+ is_current: tabId === this._currentTabId,
1329
+ });
1330
+ }
1331
+ }
1332
+ return tabs;
1333
+ }
1334
+ async switchToTab(tabId) {
1335
+ const page = this._pages.get(tabId);
1336
+ if (!page || page.isClosed()) {
1337
+ return false;
1338
+ }
1339
+ this._attachCurrentPage(tabId, page);
1340
+ logger.debug({ tabId }, "Switched to tab");
1341
+ return true;
1342
+ }
1343
+ async closeTab(tabId) {
1344
+ const page = this._pages.get(tabId);
1345
+ if (!page)
1346
+ return false;
1347
+ try {
1348
+ if (!page.isClosed()) {
1349
+ await page.close();
1350
+ // close event triggers _cleanupTab automatically
1351
+ }
1352
+ else {
1353
+ // Already closed — clean up manually
1354
+ this._cleanupTab(tabId);
1355
+ }
1356
+ }
1357
+ catch (e) {
1358
+ logger.warn({ tabId, err: e }, "Error closing page");
1359
+ this._cleanupTab(tabId);
1360
+ }
1361
+ return true;
1362
+ }
1363
+ async createNewTab(url, options) {
1364
+ await this.ensureBrowser();
1365
+ if (!this._context) {
1366
+ throw new Error("No browser context available");
1367
+ }
1368
+ const page = await this._context.newPage();
1369
+ const { tabId } = await this._registerPage(page);
1370
+ if (url) {
1371
+ try {
1372
+ await this._navigatePage(page, url, options?.timeout);
1373
+ }
1374
+ catch (e) {
1375
+ logger.warn({ url, err: e }, "Navigation failed for new tab");
1376
+ await this.closeTab(tabId);
1377
+ throw e;
1378
+ }
1379
+ }
1380
+ return [tabId, page];
1381
+ }
1382
+ // ===== Tab Group Management =====
1383
+ async createTabGroup(taskId, title) {
1384
+ const existing = this._tabGroups.get(taskId);
1385
+ if (existing)
1386
+ return existing;
1387
+ const groupTitle = title ?? `task-${taskId.slice(0, 8)}`;
1388
+ const color = TAB_GROUP_COLORS[this._colorIndex % TAB_GROUP_COLORS.length];
1389
+ this._colorIndex++;
1390
+ const group = {
1391
+ taskId,
1392
+ title: groupTitle,
1393
+ color,
1394
+ tabs: new Map(),
1395
+ };
1396
+ this._tabGroups.set(taskId, group);
1397
+ logger.info({ taskId, title: groupTitle, color }, "Created Tab Group");
1398
+ return group;
1399
+ }
1400
+ async createTabInGroup(taskId, url, options) {
1401
+ await this.ensureBrowser();
1402
+ if (!this._context) {
1403
+ throw new Error("No browser context available");
1404
+ }
1405
+ let group = this._tabGroups.get(taskId);
1406
+ if (!group) {
1407
+ group = await this.createTabGroup(taskId);
1408
+ }
1409
+ const page = await this._context.newPage();
1410
+ const { tabId } = await this._registerPage(page, { group });
1411
+ if (url) {
1412
+ try {
1413
+ await this._navigatePage(page, url, options?.timeout);
1414
+ }
1415
+ catch (e) {
1416
+ logger.warn({ url, err: e }, "Navigation failed for new tab in group");
1417
+ await this.closeTab(tabId);
1418
+ throw e;
1419
+ }
1420
+ }
1421
+ logger.info({ tabId, taskId, groupTitle: group.title }, "Created tab in group");
1422
+ return [tabId, page];
1423
+ }
1424
+ async closeTabGroup(taskId) {
1425
+ const group = this._tabGroups.get(taskId);
1426
+ if (!group)
1427
+ return false;
1428
+ const tabIds = [...group.tabs.keys()];
1429
+ for (const tabId of tabIds) {
1430
+ await this.closeTab(tabId);
1431
+ }
1432
+ this._tabGroups.delete(taskId);
1433
+ logger.info({ taskId, title: group.title }, "Tab Group closed");
1434
+ return true;
1435
+ }
1436
+ getTabGroupsInfo() {
1437
+ const info = [];
1438
+ for (const [taskId, group] of this._tabGroups) {
1439
+ const tabs = [];
1440
+ for (const [tabId, page] of group.tabs) {
1441
+ try {
1442
+ tabs.push({
1443
+ tab_id: tabId,
1444
+ url: page.isClosed() ? "(closed)" : page.url(),
1445
+ is_current: tabId === group.currentTabId,
1446
+ });
1447
+ }
1448
+ catch {
1449
+ tabs.push({ tab_id: tabId, url: "(error)", is_current: false });
1450
+ }
1451
+ }
1452
+ info.push({
1453
+ task_id: taskId,
1454
+ title: group.title,
1455
+ color: group.color,
1456
+ tab_count: group.tabs.size,
1457
+ tabs,
1458
+ });
1459
+ }
1460
+ return info;
1461
+ }
1462
+ // ===== Screenshot =====
1463
+ async takeScreenshot(options) {
1464
+ const page = await this.getPageForTab(options?.tabId);
1465
+ if (!page || page.isClosed())
1466
+ return null;
1467
+ try {
1468
+ const imgType = options?.type ?? "jpeg";
1469
+ const screenshotOpts = {
1470
+ type: imgType,
1471
+ timeout: BrowserConfig.screenshotTimeout,
1472
+ };
1473
+ // quality is only valid for jpeg
1474
+ if (imgType === "jpeg") {
1475
+ screenshotOpts.quality = options?.quality ?? 75;
1476
+ }
1477
+ const buffer = await page.screenshot(screenshotOpts);
1478
+ return buffer;
1479
+ }
1480
+ catch (e) {
1481
+ logger.warn({ err: e }, "Screenshot failed");
1482
+ return null;
1483
+ }
1484
+ }
1485
+ // ===== PDF =====
1486
+ async exportPdf(tabId) {
1487
+ if (this._config.headless === false) {
1488
+ throw new Error("PDF export requires headless mode");
1489
+ }
1490
+ const page = await this.getPageForTab(tabId);
1491
+ if (!page || page.isClosed())
1492
+ return null;
1493
+ try {
1494
+ return await page.pdf();
1495
+ }
1496
+ catch (e) {
1497
+ logger.warn({ err: e }, "PDF export failed");
1498
+ return null;
1499
+ }
1500
+ }
1501
+ // ===== Text extraction =====
1502
+ async getPageModel(tabId, options) {
1503
+ const page = await this.getPageForTab(tabId);
1504
+ if (!page || page.isClosed()) {
1505
+ return {
1506
+ primaryContent: "generic",
1507
+ confidence: 0,
1508
+ queryParams: {},
1509
+ filtersVisible: false,
1510
+ visibleInputs: [],
1511
+ auxiliarySections: [],
1512
+ ...(options?.includeRawText ? { rawText: "" } : {}),
1513
+ };
1514
+ }
1515
+ try {
1516
+ return await page.evaluate(buildPageModelSource(options));
1517
+ }
1518
+ catch (e) {
1519
+ logger.warn({ err: e }, "Page model extraction failed");
1520
+ return {
1521
+ primaryContent: "generic",
1522
+ confidence: 0,
1523
+ queryParams: {},
1524
+ filtersVisible: false,
1525
+ visibleInputs: [],
1526
+ auxiliarySections: [],
1527
+ ...(options?.includeRawText ? { rawText: "" } : {}),
1528
+ };
1529
+ }
1530
+ }
1531
+ async getPageText(tabId, mode = "auto") {
1532
+ try {
1533
+ const resolvedMode = mode === "readability" ? "auto" : mode;
1534
+ const model = await this.getPageModel(tabId, {
1535
+ includeRawText: resolvedMode === "raw" || resolvedMode === "auto",
1536
+ });
1537
+ return renderTextFromPageModel(model, resolvedMode);
1538
+ }
1539
+ catch (e) {
1540
+ logger.warn({ err: e }, "Text extraction failed");
1541
+ return "";
1542
+ }
1543
+ }
1544
+ // ===== Evaluate =====
1545
+ async evaluate(expression, tabId) {
1546
+ const detailed = await this.evaluateDetailed(expression, tabId);
1547
+ return detailed.result;
1548
+ }
1549
+ async evaluateDetailed(expression, tabId, options) {
1550
+ const page = await this.getPageForTab(tabId);
1551
+ if (!page || page.isClosed()) {
1552
+ throw new Error("No active page");
1553
+ }
1554
+ if (options?.captureConsole) {
1555
+ const wrappedSource = buildCapturedEvaluationSource(expression);
1556
+ const result = await page.evaluate(wrappedSource);
1557
+ return result;
1558
+ }
1559
+ try {
1560
+ const result = await page.evaluate(expression);
1561
+ return { result, console: [] };
1562
+ }
1563
+ catch (error) {
1564
+ const fallback = getEvaluationFallback(expression, error);
1565
+ if (!fallback) {
1566
+ throw error;
1567
+ }
1568
+ logger.debug({ mode: fallback.mode }, "Retrying evaluation with wrapped function body");
1569
+ const result = await page.evaluate(fallback.source);
1570
+ return { result, console: [] };
1571
+ }
1572
+ }
1573
+ // ===== Cookies =====
1574
+ async getCookies(urls) {
1575
+ if (!this._context)
1576
+ return [];
1577
+ const cookies = await this._context.cookies(urls);
1578
+ return cookies;
1579
+ }
1580
+ async setCookies(cookies) {
1581
+ if (!this._context)
1582
+ throw new Error("No browser context");
1583
+ await this._context.addCookies(cookies);
1584
+ }
1585
+ // ===== Cleanup =====
1586
+ async close() {
1587
+ // Snapshot pages and clear map first so close event handlers are harmless no-ops
1588
+ const pagesToClose = [...this._pages.values()];
1589
+ this._pages.clear();
1590
+ this._tabGroups.clear();
1591
+ for (const page of pagesToClose) {
1592
+ try {
1593
+ if (!page.isClosed()) {
1594
+ await page.close();
1595
+ }
1596
+ }
1597
+ catch {
1598
+ // best effort
1599
+ }
1600
+ }
1601
+ this._page = null;
1602
+ this._currentTabId = null;
1603
+ this.snapshot = null;
1604
+ this.executor = null;
1605
+ // Close browser/context
1606
+ if (this._config.mode === "cdp") {
1607
+ // For CDP, just drop the reference (don't close the external browser)
1608
+ this._browser = null;
1609
+ this._context = null;
1610
+ }
1611
+ else {
1612
+ if (this._context) {
1613
+ try {
1614
+ await this._context.close();
1615
+ }
1616
+ catch {
1617
+ // best effort
1618
+ }
1619
+ this._context = null;
1620
+ }
1621
+ if (this._browser) {
1622
+ try {
1623
+ await this._browser.close();
1624
+ }
1625
+ catch {
1626
+ // best effort
1627
+ }
1628
+ this._browser = null;
1629
+ }
1630
+ }
1631
+ this._contextListenersAttached = false;
1632
+ BrowserSession._instances.delete(this._sessionId);
1633
+ }
1634
+ static async closeAllSessions() {
1635
+ const sessions = [...BrowserSession._instances.values()];
1636
+ BrowserSession._instances.clear();
1637
+ for (const session of sessions) {
1638
+ try {
1639
+ await session.close();
1640
+ }
1641
+ catch (e) {
1642
+ logger.error({ sessionId: session._sessionId, err: e }, "Error closing session");
1643
+ }
1644
+ }
1645
+ }
1646
+ }
1647
+ //# sourceMappingURL=browser-session.js.map