@projectservan8n/cnapse 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,669 @@
1
+ /**
2
+ * Browser Service - Playwright-based web automation
3
+ *
4
+ * Provides reliable browser automation for:
5
+ * - Web searches
6
+ * - AI chat interactions (Perplexity, ChatGPT, Claude, etc.)
7
+ * - Email (Gmail, Outlook)
8
+ * - Google Sheets/Docs
9
+ * - General web browsing
10
+ */
11
+
12
+ import { chromium, Browser, Page, BrowserContext } from 'playwright';
13
+
14
+ // Singleton browser instance
15
+ let browser: Browser | null = null;
16
+ let context: BrowserContext | null = null;
17
+ let activePage: Page | null = null;
18
+
19
+ // Browser configuration
20
+ interface BrowserConfig {
21
+ headless: boolean;
22
+ slowMo: number;
23
+ viewport: { width: number; height: number };
24
+ }
25
+
26
+ const defaultConfig: BrowserConfig = {
27
+ headless: false, // Show browser so user can see what's happening
28
+ slowMo: 50, // Slight delay for visibility
29
+ viewport: { width: 1280, height: 800 }
30
+ };
31
+
32
+ /**
33
+ * Initialize browser if not already running
34
+ */
35
+ export async function initBrowser(config: Partial<BrowserConfig> = {}): Promise<Page> {
36
+ const cfg = { ...defaultConfig, ...config };
37
+
38
+ if (!browser) {
39
+ browser = await chromium.launch({
40
+ headless: cfg.headless,
41
+ slowMo: cfg.slowMo,
42
+ });
43
+ }
44
+
45
+ if (!context) {
46
+ context = await browser.newContext({
47
+ viewport: cfg.viewport,
48
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
49
+ });
50
+ }
51
+
52
+ if (!activePage) {
53
+ activePage = await context.newPage();
54
+ }
55
+
56
+ return activePage;
57
+ }
58
+
59
+ /**
60
+ * Get current page or create one
61
+ */
62
+ export async function getPage(): Promise<Page> {
63
+ if (!activePage) {
64
+ return initBrowser();
65
+ }
66
+ return activePage;
67
+ }
68
+
69
+ /**
70
+ * Close browser
71
+ */
72
+ export async function closeBrowser(): Promise<void> {
73
+ if (browser) {
74
+ await browser.close();
75
+ browser = null;
76
+ context = null;
77
+ activePage = null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Navigate to URL
83
+ */
84
+ export async function navigateTo(url: string): Promise<void> {
85
+ const page = await getPage();
86
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
87
+ }
88
+
89
+ /**
90
+ * Take screenshot and return as base64
91
+ */
92
+ export async function takeScreenshot(): Promise<string> {
93
+ const page = await getPage();
94
+ const buffer = await page.screenshot({ type: 'png' });
95
+ return buffer.toString('base64');
96
+ }
97
+
98
+ /**
99
+ * Take screenshot of specific element
100
+ */
101
+ export async function screenshotElement(selector: string): Promise<string | null> {
102
+ const page = await getPage();
103
+ try {
104
+ const element = await page.waitForSelector(selector, { timeout: 5000 });
105
+ if (element) {
106
+ const buffer = await element.screenshot({ type: 'png' });
107
+ return buffer.toString('base64');
108
+ }
109
+ } catch {
110
+ return null;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Wait for element and click
117
+ */
118
+ export async function clickElement(selector: string, timeout = 10000): Promise<boolean> {
119
+ const page = await getPage();
120
+ try {
121
+ await page.click(selector, { timeout });
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Type text into element
130
+ */
131
+ export async function typeInElement(selector: string, text: string, timeout = 10000): Promise<boolean> {
132
+ const page = await getPage();
133
+ try {
134
+ await page.fill(selector, text, { timeout });
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Type text character by character (for sites that need keypresses)
143
+ */
144
+ export async function typeSlowly(selector: string, text: string, delay = 50): Promise<boolean> {
145
+ const page = await getPage();
146
+ try {
147
+ await page.click(selector);
148
+ await page.type(selector, text, { delay });
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Press keyboard key
157
+ */
158
+ export async function pressKey(key: string): Promise<void> {
159
+ const page = await getPage();
160
+ await page.keyboard.press(key);
161
+ }
162
+
163
+ /**
164
+ * Scroll page
165
+ */
166
+ export async function scroll(direction: 'up' | 'down', amount = 500): Promise<void> {
167
+ const page = await getPage();
168
+ await page.mouse.wheel(0, direction === 'down' ? amount : -amount);
169
+ }
170
+
171
+ /**
172
+ * Wait for text to appear on page
173
+ */
174
+ export async function waitForText(text: string, timeout = 30000): Promise<boolean> {
175
+ const page = await getPage();
176
+ try {
177
+ await page.waitForFunction(
178
+ (searchText) => document.body.innerText.includes(searchText),
179
+ text,
180
+ { timeout }
181
+ );
182
+ return true;
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Get text content of element
190
+ */
191
+ export async function getTextContent(selector: string): Promise<string | null> {
192
+ const page = await getPage();
193
+ try {
194
+ return await page.textContent(selector);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get all text from page
202
+ */
203
+ export async function getPageText(): Promise<string> {
204
+ const page = await getPage();
205
+ return await page.evaluate(() => document.body.innerText);
206
+ }
207
+
208
+ /**
209
+ * Wait for navigation
210
+ */
211
+ export async function waitForNavigation(timeout = 30000): Promise<void> {
212
+ const page = await getPage();
213
+ await page.waitForLoadState('domcontentloaded', { timeout });
214
+ }
215
+
216
+ /**
217
+ * Check if element exists
218
+ */
219
+ export async function elementExists(selector: string): Promise<boolean> {
220
+ const page = await getPage();
221
+ try {
222
+ const element = await page.$(selector);
223
+ return element !== null;
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ // ========================================
230
+ // AI Chat Site Helpers
231
+ // ========================================
232
+
233
+ interface AIChatConfig {
234
+ url: string;
235
+ inputSelector: string;
236
+ submitSelector?: string;
237
+ submitKey?: string;
238
+ responseSelector: string;
239
+ waitForResponse: number;
240
+ }
241
+
242
+ const aiChatConfigs: Record<string, AIChatConfig> = {
243
+ perplexity: {
244
+ url: 'https://www.perplexity.ai',
245
+ inputSelector: 'textarea[placeholder*="Ask"]',
246
+ submitKey: 'Enter',
247
+ responseSelector: '.prose, [class*="answer"], [class*="response"]',
248
+ waitForResponse: 15000
249
+ },
250
+ chatgpt: {
251
+ url: 'https://chat.openai.com',
252
+ inputSelector: 'textarea[id="prompt-textarea"], textarea[data-id="root"]',
253
+ submitSelector: 'button[data-testid="send-button"]',
254
+ responseSelector: '[data-message-author-role="assistant"]',
255
+ waitForResponse: 20000
256
+ },
257
+ claude: {
258
+ url: 'https://claude.ai',
259
+ inputSelector: '[contenteditable="true"], textarea',
260
+ submitKey: 'Enter',
261
+ responseSelector: '[data-testid="message-content"]',
262
+ waitForResponse: 20000
263
+ },
264
+ copilot: {
265
+ url: 'https://copilot.microsoft.com',
266
+ inputSelector: 'textarea, [contenteditable="true"]',
267
+ submitKey: 'Enter',
268
+ responseSelector: '[class*="response"], [class*="message"]',
269
+ waitForResponse: 15000
270
+ },
271
+ google: {
272
+ url: 'https://www.google.com',
273
+ inputSelector: 'textarea[name="q"], input[name="q"]',
274
+ submitKey: 'Enter',
275
+ responseSelector: '#search',
276
+ waitForResponse: 5000
277
+ }
278
+ };
279
+
280
+ /**
281
+ * Ask AI chat and get response
282
+ */
283
+ export async function askAI(
284
+ site: keyof typeof aiChatConfigs,
285
+ question: string,
286
+ includeScreenshot = false
287
+ ): Promise<{ response: string; screenshot?: string }> {
288
+ const config = aiChatConfigs[site];
289
+ if (!config) {
290
+ throw new Error(`Unknown AI site: ${site}`);
291
+ }
292
+
293
+ const page = await getPage();
294
+
295
+ // Navigate to site
296
+ await page.goto(config.url, { waitUntil: 'domcontentloaded' });
297
+ await page.waitForTimeout(2000); // Let page fully load
298
+
299
+ // Find and fill input
300
+ try {
301
+ await page.waitForSelector(config.inputSelector, { timeout: 10000 });
302
+ await page.fill(config.inputSelector, question);
303
+ } catch {
304
+ // Try clicking first then typing
305
+ await page.click(config.inputSelector);
306
+ await page.type(config.inputSelector, question, { delay: 30 });
307
+ }
308
+
309
+ // Submit
310
+ if (config.submitSelector) {
311
+ await page.click(config.submitSelector);
312
+ } else if (config.submitKey) {
313
+ await page.keyboard.press(config.submitKey);
314
+ }
315
+
316
+ // Wait for response
317
+ await page.waitForTimeout(config.waitForResponse);
318
+
319
+ // Try to get response text
320
+ let response = '';
321
+ try {
322
+ const elements = await page.$$(config.responseSelector);
323
+ if (elements.length > 0) {
324
+ const lastElement = elements[elements.length - 1];
325
+ response = await lastElement.textContent() || '';
326
+ }
327
+ } catch {
328
+ // Fallback: get all page text
329
+ response = await getPageText();
330
+ }
331
+
332
+ // Optional screenshot
333
+ let screenshot: string | undefined;
334
+ if (includeScreenshot) {
335
+ screenshot = await takeScreenshot();
336
+ }
337
+
338
+ return { response: response.trim(), screenshot };
339
+ }
340
+
341
+ /**
342
+ * Scroll and capture full response (for long answers)
343
+ */
344
+ export async function getFullAIResponse(
345
+ site: keyof typeof aiChatConfigs,
346
+ maxScrolls = 5
347
+ ): Promise<string[]> {
348
+ const config = aiChatConfigs[site];
349
+ const page = await getPage();
350
+ const responseParts: string[] = [];
351
+
352
+ for (let i = 0; i < maxScrolls; i++) {
353
+ try {
354
+ const elements = await page.$$(config.responseSelector);
355
+ if (elements.length > 0) {
356
+ const lastElement = elements[elements.length - 1];
357
+ const text = await lastElement.textContent();
358
+ if (text) {
359
+ responseParts.push(text.trim());
360
+ }
361
+ }
362
+
363
+ // Scroll down
364
+ await page.mouse.wheel(0, 500);
365
+ await page.waitForTimeout(1000);
366
+
367
+ // Check if we've reached the bottom
368
+ const atBottom = await page.evaluate(() => {
369
+ return window.innerHeight + window.scrollY >= document.body.scrollHeight - 100;
370
+ });
371
+ if (atBottom) break;
372
+ } catch {
373
+ break;
374
+ }
375
+ }
376
+
377
+ return responseParts;
378
+ }
379
+
380
+ // ========================================
381
+ // Email Helpers
382
+ // ========================================
383
+
384
+ interface EmailData {
385
+ to: string;
386
+ subject: string;
387
+ body: string;
388
+ }
389
+
390
+ /**
391
+ * Send email via Gmail web interface
392
+ */
393
+ export async function sendGmail(email: EmailData): Promise<boolean> {
394
+ const page = await getPage();
395
+
396
+ try {
397
+ // Go to Gmail compose
398
+ await page.goto('https://mail.google.com/mail/u/0/#inbox?compose=new');
399
+ await page.waitForTimeout(3000);
400
+
401
+ // Wait for compose dialog
402
+ await page.waitForSelector('input[aria-label*="To"]', { timeout: 10000 });
403
+
404
+ // Fill To field
405
+ await page.fill('input[aria-label*="To"]', email.to);
406
+ await page.keyboard.press('Tab');
407
+
408
+ // Fill Subject
409
+ await page.fill('input[name="subjectbox"]', email.subject);
410
+ await page.keyboard.press('Tab');
411
+
412
+ // Fill Body
413
+ await page.fill('[aria-label*="Message Body"], [role="textbox"]', email.body);
414
+
415
+ // Click Send (Ctrl+Enter is faster)
416
+ await page.keyboard.press('Control+Enter');
417
+
418
+ await page.waitForTimeout(2000);
419
+ return true;
420
+ } catch {
421
+ return false;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Send email via Outlook web interface
427
+ */
428
+ export async function sendOutlook(email: EmailData): Promise<boolean> {
429
+ const page = await getPage();
430
+
431
+ try {
432
+ // Go to Outlook compose
433
+ await page.goto('https://outlook.office.com/mail/0/inbox');
434
+ await page.waitForTimeout(3000);
435
+
436
+ // Click New Message
437
+ await page.click('button[aria-label*="New mail"], button[title*="New mail"]');
438
+ await page.waitForTimeout(2000);
439
+
440
+ // Fill To
441
+ await page.fill('input[aria-label*="To"]', email.to);
442
+ await page.keyboard.press('Tab');
443
+
444
+ // Fill Subject
445
+ await page.fill('input[aria-label*="Subject"], input[placeholder*="Subject"]', email.subject);
446
+ await page.keyboard.press('Tab');
447
+
448
+ // Fill Body
449
+ await page.fill('[aria-label*="Message body"], [role="textbox"]', email.body);
450
+
451
+ // Click Send
452
+ await page.click('button[aria-label*="Send"], button[title*="Send"]');
453
+
454
+ await page.waitForTimeout(2000);
455
+ return true;
456
+ } catch {
457
+ return false;
458
+ }
459
+ }
460
+
461
+ // ========================================
462
+ // Google Apps Helpers
463
+ // ========================================
464
+
465
+ /**
466
+ * Create new Google Sheet and type in cells
467
+ */
468
+ export async function googleSheetsType(cellData: { cell: string; value: string }[]): Promise<boolean> {
469
+ const page = await getPage();
470
+
471
+ try {
472
+ // Go to Google Sheets
473
+ await page.goto('https://docs.google.com/spreadsheets/create');
474
+ await page.waitForTimeout(5000);
475
+
476
+ for (const { cell, value } of cellData) {
477
+ // Click on name box and type cell reference
478
+ await page.click('input#t-name-box');
479
+ await page.fill('input#t-name-box', cell);
480
+ await page.keyboard.press('Enter');
481
+ await page.waitForTimeout(500);
482
+
483
+ // Type value
484
+ await page.keyboard.type(value);
485
+ await page.keyboard.press('Enter');
486
+ await page.waitForTimeout(300);
487
+ }
488
+
489
+ return true;
490
+ } catch {
491
+ return false;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Create new Google Doc and type
497
+ */
498
+ export async function googleDocsType(text: string): Promise<boolean> {
499
+ const page = await getPage();
500
+
501
+ try {
502
+ // Go to Google Docs
503
+ await page.goto('https://docs.google.com/document/create');
504
+ await page.waitForTimeout(5000);
505
+
506
+ // Click on document body
507
+ await page.click('.kix-appview-editor');
508
+ await page.waitForTimeout(500);
509
+
510
+ // Type text
511
+ await page.keyboard.type(text, { delay: 20 });
512
+
513
+ return true;
514
+ } catch {
515
+ return false;
516
+ }
517
+ }
518
+
519
+ // ========================================
520
+ // Web Search
521
+ // ========================================
522
+
523
+ /**
524
+ * Perform web search and get results
525
+ */
526
+ export async function webSearch(query: string, engine: 'google' | 'bing' | 'duckduckgo' = 'google'): Promise<string[]> {
527
+ const page = await getPage();
528
+ const results: string[] = [];
529
+
530
+ const urls = {
531
+ google: 'https://www.google.com',
532
+ bing: 'https://www.bing.com',
533
+ duckduckgo: 'https://duckduckgo.com'
534
+ };
535
+
536
+ const selectors = {
537
+ google: { input: 'textarea[name="q"]', results: '#search .g h3' },
538
+ bing: { input: 'input[name="q"]', results: '#b_results h2 a' },
539
+ duckduckgo: { input: 'input[name="q"]', results: '[data-result] h2' }
540
+ };
541
+
542
+ try {
543
+ await page.goto(urls[engine]);
544
+ await page.waitForTimeout(2000);
545
+
546
+ // Search
547
+ await page.fill(selectors[engine].input, query);
548
+ await page.keyboard.press('Enter');
549
+ await page.waitForTimeout(3000);
550
+
551
+ // Get result titles
552
+ const elements = await page.$$(selectors[engine].results);
553
+ for (const el of elements.slice(0, 10)) {
554
+ const text = await el.textContent();
555
+ if (text) results.push(text);
556
+ }
557
+ } catch {
558
+ // Return empty on error
559
+ }
560
+
561
+ return results;
562
+ }
563
+
564
+ /**
565
+ * Click on search result by index
566
+ */
567
+ export async function clickSearchResult(index: number): Promise<boolean> {
568
+ const page = await getPage();
569
+
570
+ try {
571
+ const results = await page.$$('#search .g h3, #b_results h2 a, [data-result] h2 a');
572
+ if (results[index]) {
573
+ await results[index].click();
574
+ await page.waitForTimeout(2000);
575
+ return true;
576
+ }
577
+ } catch {}
578
+
579
+ return false;
580
+ }
581
+
582
+ // ========================================
583
+ // Research Helper (multi-step)
584
+ // ========================================
585
+
586
+ export interface ResearchResult {
587
+ query: string;
588
+ sources: { title: string; url: string; content: string }[];
589
+ summary: string;
590
+ }
591
+
592
+ /**
593
+ * Research a topic: search, visit results, gather info
594
+ */
595
+ export async function research(topic: string, maxSources = 3): Promise<ResearchResult> {
596
+ const page = await getPage();
597
+ const sources: { title: string; url: string; content: string }[] = [];
598
+
599
+ // Search
600
+ await webSearch(topic);
601
+ await page.waitForTimeout(2000);
602
+
603
+ // Visit top results
604
+ for (let i = 0; i < maxSources; i++) {
605
+ try {
606
+ const results = await page.$$('#search .g');
607
+ if (results[i]) {
608
+ // Get title and URL
609
+ const titleEl = await results[i].$('h3');
610
+ const linkEl = await results[i].$('a');
611
+
612
+ const title = await titleEl?.textContent() || 'Unknown';
613
+ const url = await linkEl?.getAttribute('href') || '';
614
+
615
+ // Click and get content
616
+ await titleEl?.click();
617
+ await page.waitForTimeout(3000);
618
+
619
+ // Get main content
620
+ const content = await page.evaluate(() => {
621
+ const article = document.querySelector('article, main, .content, #content');
622
+ return article?.textContent?.slice(0, 2000) || document.body.innerText.slice(0, 2000);
623
+ });
624
+
625
+ sources.push({ title, url, content: content.trim() });
626
+
627
+ // Go back
628
+ await page.goBack();
629
+ await page.waitForTimeout(1500);
630
+ }
631
+ } catch {
632
+ continue;
633
+ }
634
+ }
635
+
636
+ return {
637
+ query: topic,
638
+ sources,
639
+ summary: '' // To be filled by AI
640
+ };
641
+ }
642
+
643
+ export default {
644
+ initBrowser,
645
+ getPage,
646
+ closeBrowser,
647
+ navigateTo,
648
+ takeScreenshot,
649
+ screenshotElement,
650
+ clickElement,
651
+ typeInElement,
652
+ typeSlowly,
653
+ pressKey,
654
+ scroll,
655
+ waitForText,
656
+ getTextContent,
657
+ getPageText,
658
+ waitForNavigation,
659
+ elementExists,
660
+ askAI,
661
+ getFullAIResponse,
662
+ sendGmail,
663
+ sendOutlook,
664
+ googleSheetsType,
665
+ googleDocsType,
666
+ webSearch,
667
+ clickSearchResult,
668
+ research
669
+ };
@@ -8,7 +8,6 @@ export * from './clipboard.js';
8
8
  export * from './network.js';
9
9
  export * from './process.js';
10
10
  export * from './computer.js';
11
- export * from './vision.js';
12
11
 
13
12
  export interface ToolResult {
14
13
  success: boolean;