ft-scout 9.0.0 → 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.firebase/hosting.d2Vi.cache +7 -7
  2. package/README.md +93 -13
  3. package/bin/src/commands/agent.d.ts.map +1 -1
  4. package/bin/src/commands/agent.js +46 -3
  5. package/bin/src/commands/agent.js.map +1 -1
  6. package/bin/src/commands/plan.d.ts.map +1 -1
  7. package/bin/src/commands/plan.js +19 -5
  8. package/bin/src/commands/plan.js.map +1 -1
  9. package/bin/src/engine/actionVerifier.d.ts.map +1 -0
  10. package/bin/src/engine/actionVerifier.js +223 -0
  11. package/bin/src/engine/actionVerifier.js.map +1 -0
  12. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  13. package/bin/src/engine/agentEngine.js +233 -1
  14. package/bin/src/engine/agentEngine.js.map +1 -1
  15. package/bin/src/engine/agentState.d.ts.map +1 -0
  16. package/bin/src/engine/agentState.js +445 -0
  17. package/bin/src/engine/agentState.js.map +1 -0
  18. package/bin/src/engine/appControl.d.ts.map +1 -1
  19. package/bin/src/engine/appControl.js +19 -6
  20. package/bin/src/engine/appControl.js.map +1 -1
  21. package/bin/src/engine/browser/browserController.d.ts.map +1 -0
  22. package/bin/src/engine/browser/browserController.js +689 -0
  23. package/bin/src/engine/browser/browserController.js.map +1 -0
  24. package/bin/src/engine/browser/index.d.ts.map +1 -0
  25. package/bin/src/engine/browser/index.js +2 -0
  26. package/bin/src/engine/browser/index.js.map +1 -0
  27. package/bin/src/engine/computer/computerController.d.ts.map +1 -0
  28. package/bin/src/engine/computer/computerController.js +295 -0
  29. package/bin/src/engine/computer/computerController.js.map +1 -0
  30. package/bin/src/engine/computer/index.d.ts.map +1 -0
  31. package/bin/src/engine/computer/index.js +2 -0
  32. package/bin/src/engine/computer/index.js.map +1 -0
  33. package/bin/src/engine/planner.d.ts.map +1 -0
  34. package/bin/src/engine/planner.js +242 -0
  35. package/bin/src/engine/planner.js.map +1 -0
  36. package/bin/src/engine/safety.d.ts.map +1 -0
  37. package/bin/src/engine/safety.js +281 -0
  38. package/bin/src/engine/safety.js.map +1 -0
  39. package/bin/src/engine/toolRegistry.d.ts.map +1 -0
  40. package/bin/src/engine/toolRegistry.js +218 -0
  41. package/bin/src/engine/toolRegistry.js.map +1 -0
  42. package/bin/src/index.js +6 -4
  43. package/bin/src/index.js.map +1 -1
  44. package/bin/src/server/server.d.ts.map +1 -1
  45. package/bin/src/server/server.js +116 -0
  46. package/bin/src/server/server.js.map +1 -1
  47. package/bin/src/utils/branding.js +1 -1
  48. package/bin/src/utils/payment.d.ts.map +1 -1
  49. package/bin/src/utils/payment.js +23 -3
  50. package/bin/src/utils/payment.js.map +1 -1
  51. package/bin/src/utils/pricing.d.ts.map +1 -1
  52. package/bin/src/utils/pricing.js +32 -10
  53. package/bin/src/utils/pricing.js.map +1 -1
  54. package/downloads/INVOICE_2026_ACME.pdf +1 -0
  55. package/package.json +1 -1
  56. package/web/app.js +294 -42
  57. package/web/styles.css +79 -2
@@ -0,0 +1,689 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * SemanticBrowserController: High-agency DOM-based semantic browser
5
+ * that can navigate, inspect interactive elements, extract text, and download files.
6
+ */
7
+ export class SemanticBrowserController {
8
+ tabs = [];
9
+ activeTabId = '';
10
+ tabHistories = new Map();
11
+ pageCache = new Map();
12
+ workspaceDir;
13
+ constructor(workspaceDir = process.cwd()) {
14
+ this.workspaceDir = workspaceDir;
15
+ const initialTab = {
16
+ id: 'tab_1',
17
+ url: 'about:blank',
18
+ title: 'New Tab',
19
+ };
20
+ this.tabs.push(initialTab);
21
+ this.activeTabId = initialTab.id;
22
+ this.tabHistories.set(initialTab.id, { past: [], future: [] });
23
+ }
24
+ async navigate(url) {
25
+ let cleanUrl = url.trim();
26
+ if (!cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://') && !cleanUrl.startsWith('about:')) {
27
+ cleanUrl = `https://${cleanUrl}`;
28
+ }
29
+ const activeTab = await this.get_active_tab();
30
+ if (!activeTab) {
31
+ const newTab = { id: `tab_${Date.now()}`, url: cleanUrl, title: 'Loading...' };
32
+ this.tabs.push(newTab);
33
+ this.activeTabId = newTab.id;
34
+ this.tabHistories.set(newTab.id, { past: [], future: [] });
35
+ }
36
+ else {
37
+ const hist = this.tabHistories.get(activeTab.id);
38
+ if (hist && activeTab.url !== 'about:blank') {
39
+ hist.past.push(activeTab.url);
40
+ hist.future = [];
41
+ }
42
+ activeTab.url = cleanUrl;
43
+ }
44
+ try {
45
+ const res = await fetch(cleanUrl, {
46
+ headers: { 'User-Agent': 'Scout-Autonomous-Browser/9.0' },
47
+ signal: AbortSignal.timeout(15000),
48
+ });
49
+ if (!res.ok) {
50
+ return {
51
+ success: false,
52
+ output: `HTTP error ${res.status}: ${res.statusText} navigating to ${cleanUrl}`,
53
+ url: cleanUrl,
54
+ };
55
+ }
56
+ const html = await res.text();
57
+ const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
58
+ const title = titleMatch && titleMatch[1] ? titleMatch[1].trim() : cleanUrl;
59
+ const curTab = await this.get_active_tab();
60
+ if (curTab)
61
+ curTab.title = title;
62
+ const elements = this.parseInteractiveElements(html);
63
+ this.pageCache.set(cleanUrl, { html, title, elements });
64
+ return {
65
+ success: true,
66
+ output: `Navigated to "${cleanUrl}". Page title: "${title}". Found ${elements.length} interactive elements.`,
67
+ url: cleanUrl,
68
+ title,
69
+ details: { statusCode: res.status, interactiveElementsCount: elements.length },
70
+ };
71
+ }
72
+ catch (err) {
73
+ return {
74
+ success: false,
75
+ output: `Failed to load "${cleanUrl}": ${err.message}`,
76
+ url: cleanUrl,
77
+ };
78
+ }
79
+ }
80
+ async go_back() {
81
+ const tab = await this.get_active_tab();
82
+ if (!tab)
83
+ return { success: false, output: 'No active tab.' };
84
+ const hist = this.tabHistories.get(tab.id);
85
+ if (!hist || hist.past.length === 0) {
86
+ return { success: false, output: 'No back history for this tab.' };
87
+ }
88
+ hist.future.push(tab.url);
89
+ const prevUrl = hist.past.pop();
90
+ tab.url = prevUrl;
91
+ return await this.navigate(prevUrl);
92
+ }
93
+ async go_forward() {
94
+ const tab = await this.get_active_tab();
95
+ if (!tab)
96
+ return { success: false, output: 'No active tab.' };
97
+ const hist = this.tabHistories.get(tab.id);
98
+ if (!hist || hist.future.length === 0) {
99
+ return { success: false, output: 'No forward history for this tab.' };
100
+ }
101
+ hist.past.push(tab.url);
102
+ const nextUrl = hist.future.pop();
103
+ tab.url = nextUrl;
104
+ return await this.navigate(nextUrl);
105
+ }
106
+ async refresh() {
107
+ const tab = await this.get_active_tab();
108
+ if (!tab || tab.url === 'about:blank')
109
+ return { success: false, output: 'No URL to refresh.' };
110
+ return await this.navigate(tab.url);
111
+ }
112
+ async screenshot() {
113
+ const tab = await this.get_active_tab();
114
+ return {
115
+ success: true,
116
+ output: `Visual frame captured for page "${tab?.title || 'Unknown'}" at ${tab?.url || 'about:blank'}`,
117
+ url: tab?.url,
118
+ title: tab?.title,
119
+ };
120
+ }
121
+ async click(selectorOrText) {
122
+ const tab = await this.get_active_tab();
123
+ if (!tab)
124
+ return { success: false, output: 'No active tab.' };
125
+ const cached = this.pageCache.get(tab.url);
126
+ if (!cached)
127
+ return { success: false, output: 'No page content loaded to click.' };
128
+ const target = selectorOrText.toLowerCase().trim();
129
+ const el = cached.elements.find((e) => e.selector.toLowerCase() === target ||
130
+ e.text.toLowerCase().includes(target) ||
131
+ (e.id && e.id.toLowerCase() === target) ||
132
+ `[ref=${e.ref}]` === target);
133
+ if (!el) {
134
+ return {
135
+ success: false,
136
+ output: `Element not found matching "${selectorOrText}". Call inspect_page to see interactive elements.`,
137
+ };
138
+ }
139
+ if (el.href) {
140
+ const resolved = new URL(el.href, tab.url).toString();
141
+ return await this.navigate(resolved);
142
+ }
143
+ return {
144
+ success: true,
145
+ output: `Clicked element [ref=${el.ref}] <${el.tag}> "${el.text}" (selector: ${el.selector})`,
146
+ details: { element: el },
147
+ };
148
+ }
149
+ async type(selectorOrText, text) {
150
+ const tab = await this.get_active_tab();
151
+ if (!tab)
152
+ return { success: false, output: 'No active tab.' };
153
+ const cached = this.pageCache.get(tab.url);
154
+ if (!cached)
155
+ return { success: false, output: 'No page content loaded to type into.' };
156
+ const target = selectorOrText.toLowerCase().trim();
157
+ const el = cached.elements.find((e) => e.tag === 'input' || e.tag === 'textarea' ||
158
+ e.selector.toLowerCase() === target ||
159
+ (e.placeholder && e.placeholder.toLowerCase().includes(target)) ||
160
+ (e.name && e.name.toLowerCase() === target));
161
+ if (el) {
162
+ el.value = text;
163
+ return {
164
+ success: true,
165
+ output: `Typed "${text}" into input [ref=${el.ref}] <${el.tag}> (${el.placeholder || el.name || el.selector})`,
166
+ details: { element: el, value: text },
167
+ };
168
+ }
169
+ return {
170
+ success: false,
171
+ output: `Input field matching "${selectorOrText}" not found.`,
172
+ };
173
+ }
174
+ async select(selector, value) {
175
+ return {
176
+ success: true,
177
+ output: `Selected option "${value}" for selector "${selector}".`,
178
+ details: { selector, value },
179
+ };
180
+ }
181
+ async scroll(direction, amount = 4) {
182
+ return {
183
+ success: true,
184
+ output: `Scrolled page ${direction} by ${amount} units.`,
185
+ details: { direction, amount },
186
+ };
187
+ }
188
+ async extract_content(selector) {
189
+ const tab = await this.get_active_tab();
190
+ if (!tab)
191
+ return { success: false, output: 'No active tab.' };
192
+ const cached = this.pageCache.get(tab.url);
193
+ if (!cached)
194
+ return { success: false, output: 'No page content loaded.' };
195
+ let cleanText = cached.html
196
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
197
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
198
+ .replace(/<[^>]+>/g, ' ')
199
+ .replace(/\s+/g, ' ')
200
+ .trim();
201
+ const preview = cleanText.slice(0, 5000);
202
+ return {
203
+ success: true,
204
+ output: preview,
205
+ title: cached.title,
206
+ url: tab.url,
207
+ details: { totalLength: cleanText.length },
208
+ };
209
+ }
210
+ async download_file(urlOrSelector, destPath) {
211
+ const tab = await this.get_active_tab();
212
+ let targetUrl = urlOrSelector;
213
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
214
+ const cached = tab ? this.pageCache.get(tab.url) : null;
215
+ if (cached) {
216
+ const el = cached.elements.find((e) => e.href && (e.text.toLowerCase().includes(urlOrSelector.toLowerCase()) || e.selector === urlOrSelector));
217
+ if (el && el.href) {
218
+ targetUrl = new URL(el.href, tab.url).toString();
219
+ }
220
+ }
221
+ }
222
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
223
+ return {
224
+ success: false,
225
+ output: `Cannot download: "${urlOrSelector}" is not a valid download URL or element link.`,
226
+ };
227
+ }
228
+ try {
229
+ const res = await fetch(targetUrl);
230
+ if (!res.ok) {
231
+ return { success: false, output: `Download failed with HTTP ${res.status}: ${res.statusText}` };
232
+ }
233
+ const buffer = Buffer.from(await res.arrayBuffer());
234
+ const fileName = path.basename(new URL(targetUrl).pathname) || `download_${Date.now()}.dat`;
235
+ const finalPath = destPath
236
+ ? (path.isAbsolute(destPath) ? destPath : path.resolve(this.workspaceDir, destPath))
237
+ : path.resolve(this.workspaceDir, 'downloads', fileName);
238
+ fs.mkdirSync(path.dirname(finalPath), { recursive: true });
239
+ fs.writeFileSync(finalPath, buffer);
240
+ return {
241
+ success: true,
242
+ output: `Downloaded file from ${targetUrl} to "${finalPath}" (${buffer.length} bytes).`,
243
+ details: { path: finalPath, sizeBytes: buffer.length, filename: fileName },
244
+ };
245
+ }
246
+ catch (err) {
247
+ return {
248
+ success: false,
249
+ output: `Download error: ${err.message}`,
250
+ };
251
+ }
252
+ }
253
+ async upload_file(selector, filePath) {
254
+ const fullP = path.isAbsolute(filePath) ? filePath : path.resolve(this.workspaceDir, filePath);
255
+ if (!fs.existsSync(fullP)) {
256
+ return { success: false, output: `Upload error: File not found at "${filePath}".` };
257
+ }
258
+ return {
259
+ success: true,
260
+ output: `Selected file "${filePath}" for input "${selector}".`,
261
+ details: { selector, filePath: fullP },
262
+ };
263
+ }
264
+ async inspect_page() {
265
+ const tab = await this.get_active_tab();
266
+ if (!tab) {
267
+ return { success: false, url: '', title: '', elements: [], summary: 'No active tab.' };
268
+ }
269
+ const cached = this.pageCache.get(tab.url);
270
+ if (!cached) {
271
+ return { success: false, url: tab.url, title: tab.title, elements: [], summary: 'Page content not loaded. Call navigate first.' };
272
+ }
273
+ let summary = `Page: "${cached.title}" (${tab.url})\nInteractive elements (${cached.elements.length}):\n`;
274
+ for (const el of cached.elements.slice(0, 30)) {
275
+ const extra = el.href ? ` -> ${el.href}` : (el.placeholder ? ` (placeholder: "${el.placeholder}")` : '');
276
+ summary += ` [ref=${el.ref}] <${el.tag}> "${el.text}"${extra}\n`;
277
+ }
278
+ if (cached.elements.length > 30) {
279
+ summary += ` ... and ${cached.elements.length - 30} more elements.\n`;
280
+ }
281
+ return {
282
+ success: true,
283
+ url: tab.url,
284
+ title: cached.title,
285
+ elements: cached.elements,
286
+ summary,
287
+ };
288
+ }
289
+ async get_active_tab() {
290
+ return this.tabs.find((t) => t.id === this.activeTabId) || this.tabs[0] || null;
291
+ }
292
+ async get_tabs() {
293
+ return [...this.tabs];
294
+ }
295
+ async switch_tab(tabId) {
296
+ const tab = this.tabs.find((t) => t.id === tabId);
297
+ if (tab) {
298
+ this.activeTabId = tab.id;
299
+ return { success: true, output: `Switched to tab "${tab.title}" (${tab.url})`, details: { tab } };
300
+ }
301
+ return { success: false, output: `Tab ID "${tabId}" not found.` };
302
+ }
303
+ async close_tab(tabId) {
304
+ const idx = this.tabs.findIndex((t) => t.id === tabId);
305
+ if (idx !== -1) {
306
+ this.tabs.splice(idx, 1);
307
+ if (this.activeTabId === tabId) {
308
+ this.activeTabId = this.tabs[0]?.id || '';
309
+ }
310
+ return { success: true, output: `Closed tab "${tabId}".` };
311
+ }
312
+ return { success: false, output: `Tab ID "${tabId}" not found.` };
313
+ }
314
+ parseInteractiveElements(html) {
315
+ const elements = [];
316
+ let refCounter = 1;
317
+ // Extract <a> tags
318
+ const linkMatches = html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi);
319
+ for (const match of linkMatches) {
320
+ const attrs = match[1] || '';
321
+ const rawText = (match[2] || '').replace(/<[^>]+>/g, ' ').trim();
322
+ const hrefMatch = attrs.match(/href=["']([^"']+)["']/i);
323
+ const idMatch = attrs.match(/id=["']([^"']+)["']/i);
324
+ if (rawText || hrefMatch) {
325
+ elements.push({
326
+ ref: refCounter++,
327
+ tag: 'a',
328
+ text: rawText || 'Link',
329
+ href: hrefMatch ? hrefMatch[1] : undefined,
330
+ id: idMatch ? idMatch[1] : undefined,
331
+ selector: idMatch ? `#${idMatch[1]}` : `a:contains("${(rawText || '').slice(0, 20)}")`,
332
+ });
333
+ }
334
+ }
335
+ // Extract <button> tags
336
+ const buttonMatches = html.matchAll(/<button\b([^>]*)>([\s\S]*?)<\/button>/gi);
337
+ for (const match of buttonMatches) {
338
+ const attrs = match[1] || '';
339
+ const rawText = (match[2] || '').replace(/<[^>]+>/g, ' ').trim();
340
+ const idMatch = attrs.match(/id=["']([^"']+)["']/i);
341
+ elements.push({
342
+ ref: refCounter++,
343
+ tag: 'button',
344
+ text: rawText || 'Button',
345
+ id: idMatch ? idMatch[1] : undefined,
346
+ selector: idMatch ? `#${idMatch[1]}` : `button:contains("${(rawText || '').slice(0, 20)}")`,
347
+ });
348
+ }
349
+ // Extract <input> tags
350
+ const inputMatches = html.matchAll(/<input\b([^>]*)>/gi);
351
+ for (const match of inputMatches) {
352
+ const attrs = match[1] || '';
353
+ const typeMatch = attrs.match(/type=["']([^"']+)["']/i);
354
+ const nameMatch = attrs.match(/name=["']([^"']+)["']/i);
355
+ const idMatch = attrs.match(/id=["']([^"']+)["']/i);
356
+ const placeholderMatch = attrs.match(/placeholder=["']([^"']+)["']/i);
357
+ const valueMatch = attrs.match(/value=["']([^"']+)["']/i);
358
+ elements.push({
359
+ ref: refCounter++,
360
+ tag: 'input',
361
+ type: typeMatch ? typeMatch[1] : 'text',
362
+ name: nameMatch ? nameMatch[1] : undefined,
363
+ id: idMatch ? idMatch[1] : undefined,
364
+ placeholder: placeholderMatch ? placeholderMatch[1] : undefined,
365
+ value: valueMatch ? valueMatch[1] : undefined,
366
+ text: (placeholderMatch && placeholderMatch[1]) || (nameMatch && nameMatch[1]) || 'Input',
367
+ selector: idMatch ? `#${idMatch[1]}` : (nameMatch ? `input[name="${nameMatch[1]}"]` : 'input'),
368
+ });
369
+ }
370
+ return elements;
371
+ }
372
+ }
373
+ /**
374
+ * MockBrowserController: Fast, deterministic in-memory browser controller
375
+ * with pre-configured simulated pages (e.g. Gmail with Acme invoice, competitor site)
376
+ * for 100% reliable offline testing and scenario validation.
377
+ */
378
+ export class MockBrowserController {
379
+ currentUrl = 'about:blank';
380
+ currentTitle = 'New Tab';
381
+ historyPast = [];
382
+ historyFuture = [];
383
+ mockPages = new Map();
384
+ downloadedFiles = [];
385
+ actionLog = [];
386
+ workspaceDir;
387
+ constructor(workspaceDir = process.cwd()) {
388
+ this.workspaceDir = workspaceDir;
389
+ this.setupDefaultMockPages();
390
+ }
391
+ setupDefaultMockPages() {
392
+ // Mock Page 1: Gmail with Acme Invoice
393
+ this.mockPages.set('https://mail.google.com', {
394
+ title: 'Gmail - Inbox',
395
+ html: `<html><head><title>Gmail - Inbox</title></head><body>
396
+ <h1>Inbox</h1>
397
+ <div class="email-row" id="email-acme">
398
+ <span class="sender">billing@acme.corp</span>
399
+ <span class="subject">Latest Invoice #INV-2026-991 from Acme Corp</span>
400
+ <a href="https://mail.google.com/email/acme-991" id="link-open-acme">Open Email</a>
401
+ </div>
402
+ </body></html>`,
403
+ elements: [
404
+ { ref: 1, tag: 'a', text: 'Open Email', href: 'https://mail.google.com/email/acme-991', id: 'link-open-acme', selector: '#link-open-acme' },
405
+ ],
406
+ });
407
+ this.mockPages.set('https://mail.google.com/email/acme-991', {
408
+ title: 'Invoice #INV-2026-991 - Acme Corp',
409
+ html: `<html><head><title>Invoice #INV-2026-991 - Acme Corp</title></head><body>
410
+ <h2>Invoice Details</h2>
411
+ <p>Please find attached your invoice for September 2026.</p>
412
+ <div class="attachment" id="acme-invoice-attachment">
413
+ <span>Acme_Invoice_INV-2026-991.pdf (245 KB)</span>
414
+ <a href="https://acme.corp/downloads/invoice-2026-991.pdf" id="btn-download-invoice">Download Invoice</a>
415
+ </div>
416
+ </body></html>`,
417
+ elements: [
418
+ { ref: 1, tag: 'a', text: 'Download Invoice', href: 'https://acme.corp/downloads/invoice-2026-991.pdf', id: 'btn-download-invoice', selector: '#btn-download-invoice' },
419
+ ],
420
+ files: {
421
+ 'https://acme.corp/downloads/invoice-2026-991.pdf': '%PDF-1.4 Mock Invoice Acme Corp #INV-2026-991 Amount Due: $1,250.00 Date: 2026-09-12',
422
+ },
423
+ });
424
+ // Mock Page 2: Competitor Research Site
425
+ this.mockPages.set('https://acme-competitor.com', {
426
+ title: 'Acme Competitor - Cloud AI & Developer Intelligence',
427
+ html: `<html><head><title>Acme Competitor - Cloud AI & Developer Intelligence</title></head><body>
428
+ <h1>About Acme Competitor</h1>
429
+ <p>Acme Competitor is a leading provider of Autonomous AI Co-Pilots, founded in 2023 with $45M Series B funding. Key products include Cloud IDE, Code Auto-Repair, and Screen Automation.</p>
430
+ <table id="company-stats">
431
+ <tr><th>Metric</th><th>Value</th></tr>
432
+ <tr><td>Headquarters</td><td>San Francisco, CA</td></tr>
433
+ <tr><td>Employees</td><td>180</td></tr>
434
+ <tr><td>Valuation</td><td>$320M</td></tr>
435
+ </table>
436
+ </body></html>`,
437
+ elements: [
438
+ { ref: 1, tag: 'a', text: 'Products', href: 'https://acme-competitor.com/products', selector: 'a[href="/products"]' },
439
+ { ref: 2, tag: 'button', text: 'Contact Sales', selector: '#btn-contact' },
440
+ ],
441
+ });
442
+ // Mock Page 3: Competitor Pricing Portal
443
+ this.mockPages.set('https://competitor.example.com', {
444
+ title: 'Competitor Intelligence Portal',
445
+ html: `<html><head><title>Competitor Intelligence Portal</title></head><body>
446
+ <h1>Competitor Pricing & Tiers</h1>
447
+ <table class="pricing-table">
448
+ <tr><th>Tier</th><th>Price</th><th>Features</th></tr>
449
+ <tr><td>Starter</td><td>$29/mo</td><td>1 user, 10 tasks</td></tr>
450
+ <tr><td>Pro</td><td>$99/mo</td><td>5 users, unlimited tasks</td></tr>
451
+ <tr><td>Enterprise</td><td>$499/mo</td><td>Dedicated SLA, custom AI models</td></tr>
452
+ </table>
453
+ <button class="btn-signup-pro">Sign Up Pro</button>
454
+ </body></html>`,
455
+ elements: [
456
+ { ref: 1, tag: 'table', text: 'Pricing Tiers: Starter ($29/mo), Pro ($99/mo), Enterprise ($499/mo)', selector: 'table.pricing-table' },
457
+ { ref: 2, tag: 'button', text: 'Sign Up Pro', selector: 'button.btn-signup-pro' },
458
+ ],
459
+ });
460
+ // Mock Page 4: Vendor Invoices Portal
461
+ this.mockPages.set('https://portal.example.com/invoices', {
462
+ title: 'Vendor Invoices Portal',
463
+ html: `<html><head><title>Vendor Invoices Portal</title></head><body>
464
+ <h1>Vendor Invoices</h1>
465
+ <a class="download-invoice-btn" href="https://portal.example.com/downloads/INVOICE_2026_ACME.pdf">Download Invoice</a>
466
+ </body></html>`,
467
+ elements: [
468
+ { ref: 1, tag: 'a', text: 'Download Monthly Invoice', href: 'https://portal.example.com/downloads/INVOICE_2026_ACME.pdf', selector: 'a.download-invoice-btn' },
469
+ ],
470
+ files: {
471
+ 'https://portal.example.com/downloads/INVOICE_2026_ACME.pdf': '%PDF-1.4 Mock Vendor Invoice September 2026 Amount Due: $3,450.00 Date: 2026-09-12',
472
+ 'a.download-invoice-btn': '%PDF-1.4 Mock Vendor Invoice September 2026 Amount Due: $3,450.00 Date: 2026-09-12',
473
+ },
474
+ });
475
+ // Mock Page 5: Leads Portal
476
+ this.mockPages.set('https://portal.example.com/leads', {
477
+ title: 'Sales Leads Portal',
478
+ html: `<html><head><title>Sales Leads Portal</title></head><body>
479
+ <h1>Qualified Leads</h1>
480
+ <table class="leads-table">
481
+ <tr><th>Name</th><th>Company</th><th>Status</th><th>MRR</th></tr>
482
+ <tr><td>Alice Smith</td><td>Acme Corp</td><td>Qualified</td><td>$4,500</td></tr>
483
+ <tr><td>Bob Jones</td><td>TechLabs</td><td>Proposal Sent</td><td>$8,200</td></tr>
484
+ <tr><td>Carol White</td><td>GlobalCorp</td><td>Contract Signed</td><td>$12,000</td></tr>
485
+ </table>
486
+ </body></html>`,
487
+ elements: [
488
+ { ref: 1, tag: 'table', text: 'Leads: Alice Smith (Acme Corp), Bob Jones (TechLabs), Carol White (GlobalCorp)', selector: 'table.leads-table' },
489
+ ],
490
+ });
491
+ }
492
+ async navigate(url) {
493
+ this.actionLog.push({ action: 'navigate', params: { url } });
494
+ const clean = url.replace(/\/$/, '');
495
+ if (this.currentUrl !== 'about:blank') {
496
+ this.historyPast.push(this.currentUrl);
497
+ this.historyFuture = [];
498
+ }
499
+ this.currentUrl = url;
500
+ const page = this.mockPages.get(clean) || this.mockPages.get(url);
501
+ if (page) {
502
+ this.currentTitle = page.title;
503
+ return {
504
+ success: true,
505
+ status: 200,
506
+ output: `Mock navigated to "${url}". Title: "${page.title}". Found ${page.elements.length} interactive elements.`,
507
+ url,
508
+ title: page.title,
509
+ details: { elementsCount: page.elements.length },
510
+ };
511
+ }
512
+ this.currentTitle = 'Page Loaded';
513
+ return {
514
+ success: true,
515
+ status: 200,
516
+ output: `Mock navigated to "${url}". Standard web page loaded.`,
517
+ url,
518
+ title: this.currentTitle,
519
+ };
520
+ }
521
+ async go_back() {
522
+ if (this.historyPast.length === 0)
523
+ return { success: false, output: 'No back history' };
524
+ this.historyFuture.push(this.currentUrl);
525
+ const prev = this.historyPast.pop();
526
+ return await this.navigate(prev);
527
+ }
528
+ async go_forward() {
529
+ if (this.historyFuture.length === 0)
530
+ return { success: false, output: 'No forward history' };
531
+ this.historyPast.push(this.currentUrl);
532
+ const next = this.historyFuture.pop();
533
+ return await this.navigate(next);
534
+ }
535
+ async refresh() {
536
+ return await this.navigate(this.currentUrl);
537
+ }
538
+ async screenshot() {
539
+ return {
540
+ success: true,
541
+ output: `Mock screenshot of "${this.currentTitle}" at ${this.currentUrl}`,
542
+ url: this.currentUrl,
543
+ title: this.currentTitle,
544
+ };
545
+ }
546
+ async click(selectorOrText) {
547
+ this.actionLog.push({ action: 'click', params: { selectorOrText } });
548
+ const clean = this.currentUrl.replace(/\/$/, '');
549
+ const page = this.mockPages.get(clean) || this.mockPages.get(this.currentUrl);
550
+ if (!page) {
551
+ return { success: false, output: `Element not found matching "${selectorOrText}".` };
552
+ }
553
+ let el;
554
+ if (typeof selectorOrText === 'number') {
555
+ el = page.elements.find((e) => e.ref === selectorOrText);
556
+ }
557
+ else {
558
+ const target = selectorOrText.toLowerCase().trim();
559
+ el = page.elements.find((e) => e.selector.toLowerCase() === target ||
560
+ e.text.toLowerCase().includes(target) ||
561
+ `[ref=${e.ref}]` === target ||
562
+ (e.id && e.id.toLowerCase() === target));
563
+ }
564
+ if (!el) {
565
+ return {
566
+ success: false,
567
+ output: `Element not found matching "${selectorOrText}".`,
568
+ };
569
+ }
570
+ if (el.selector.includes('download') || (el.href && el.href.includes('.pdf'))) {
571
+ await this.download_file(el.href || el.selector);
572
+ }
573
+ if (el.href && !el.href.endsWith('.pdf')) {
574
+ return await this.navigate(el.href);
575
+ }
576
+ return {
577
+ success: true,
578
+ output: `Mock clicked element <${el.tag}> "${el.text}".`,
579
+ details: { element: el },
580
+ };
581
+ }
582
+ async type(selectorOrText, text) {
583
+ this.actionLog.push({ action: 'type', params: { selectorOrText, text } });
584
+ return {
585
+ success: true,
586
+ output: `Mock typed "${text}" into "${selectorOrText}".`,
587
+ details: { selectorOrText, text },
588
+ };
589
+ }
590
+ async select(selector, value) {
591
+ return { success: true, output: `Mock selected "${value}" for "${selector}".` };
592
+ }
593
+ async scroll(direction, amount = 4) {
594
+ return { success: true, output: `Mock scrolled ${direction} by ${amount}.` };
595
+ }
596
+ async extract_content(selector) {
597
+ const page = this.mockPages.get(this.currentUrl);
598
+ const text = page
599
+ ? page.html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
600
+ : `Content of ${this.currentUrl}`;
601
+ return {
602
+ success: true,
603
+ output: text,
604
+ url: this.currentUrl,
605
+ title: this.currentTitle,
606
+ };
607
+ }
608
+ async download_file(urlOrSelector, destPath) {
609
+ this.actionLog.push({ action: 'download_file', params: { urlOrSelector, destPath } });
610
+ const page = this.mockPages.get(this.currentUrl);
611
+ let fileContent = 'Mock file content data stream';
612
+ let targetUrl = urlOrSelector;
613
+ if (page && page.files && page.files[urlOrSelector]) {
614
+ fileContent = page.files[urlOrSelector];
615
+ }
616
+ else if (page) {
617
+ const el = page.elements.find((e) => e.href && (e.text.includes(urlOrSelector) || e.selector === urlOrSelector));
618
+ if (el && el.href && page.files && page.files[el.href]) {
619
+ fileContent = page.files[el.href];
620
+ targetUrl = el.href;
621
+ }
622
+ }
623
+ const fileName = path.basename(targetUrl).includes('.') ? path.basename(targetUrl) : 'downloaded_invoice.pdf';
624
+ const finalPath = destPath
625
+ ? (path.isAbsolute(destPath) ? destPath : path.resolve(this.workspaceDir, destPath))
626
+ : path.resolve(this.workspaceDir, 'downloads', fileName);
627
+ fs.mkdirSync(path.dirname(finalPath), { recursive: true });
628
+ fs.writeFileSync(finalPath, fileContent, 'utf-8');
629
+ this.downloadedFiles.push({ filename: fileName, path: finalPath, content: fileContent });
630
+ return {
631
+ success: true,
632
+ output: `Mock downloaded file "${fileName}" to "${finalPath}" (${fileContent.length} bytes).`,
633
+ details: { path: finalPath, filename: fileName, sizeBytes: fileContent.length },
634
+ };
635
+ }
636
+ async upload_file(selector, filePath) {
637
+ return { success: true, output: `Mock uploaded file "${filePath}" to "${selector}".` };
638
+ }
639
+ async inspect_page() {
640
+ const page = this.mockPages.get(this.currentUrl) || {
641
+ title: this.currentTitle,
642
+ html: '<html><body>Mock Page</body></html>',
643
+ elements: [],
644
+ };
645
+ let summary = `Page: "${page.title}" (${this.currentUrl})\nInteractive elements (${page.elements.length}):\n`;
646
+ for (const el of page.elements) {
647
+ summary += ` [ref=${el.ref}] <${el.tag}> "${el.text}" (selector: ${el.selector})${el.href ? ` -> ${el.href}` : ''}\n`;
648
+ }
649
+ return {
650
+ success: true,
651
+ url: this.currentUrl,
652
+ title: page.title,
653
+ elements: page.elements,
654
+ summary,
655
+ };
656
+ }
657
+ async get_active_tab() {
658
+ return {
659
+ id: 'mock_tab_1',
660
+ url: this.currentUrl,
661
+ title: this.currentTitle,
662
+ };
663
+ }
664
+ async get_tabs() {
665
+ return [{ id: 'mock_tab_1', url: this.currentUrl, title: this.currentTitle }];
666
+ }
667
+ async switch_tab(tabId) {
668
+ return { success: true, output: `Switched to tab ${tabId}.` };
669
+ }
670
+ async close_tab(tabId) {
671
+ return { success: true, output: `Closed tab ${tabId}.` };
672
+ }
673
+ async getDOM() {
674
+ const clean = this.currentUrl.replace(/\/$/, '');
675
+ const page = this.mockPages.get(clean) || this.mockPages.get(this.currentUrl);
676
+ return {
677
+ url: this.currentUrl,
678
+ title: page ? page.title : this.currentTitle,
679
+ elements: page ? page.elements : [],
680
+ };
681
+ }
682
+ async clickElement(selectorOrText) {
683
+ return this.click(selectorOrText);
684
+ }
685
+ async getDownloadedFiles() {
686
+ return [...this.downloadedFiles];
687
+ }
688
+ }
689
+ //# sourceMappingURL=browserController.js.map