ultra-dex 3.2.0 → 3.3.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,373 @@
1
+ /**
2
+ * Browser Automation Module (Playwright-based)
3
+ * Enables Research agent to browse the web, take screenshots, and interact with pages
4
+ * This is what makes Ultra-Dex truly intelligent - it can LEARN from the web
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+
11
+ // ============================================================================
12
+ // BROWSER CONFIGURATION
13
+ // ============================================================================
14
+
15
+ const BROWSER_CONFIG = {
16
+ // Default browser settings
17
+ headless: true,
18
+ timeout: 30000,
19
+ viewport: { width: 1280, height: 720 },
20
+
21
+ // Screenshot settings
22
+ screenshotDir: '.ultra-dex/screenshots',
23
+
24
+ // User agent
25
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Ultra-Dex/3.2.0 Research Agent',
26
+
27
+ // Blocked resources (for faster loading)
28
+ blockedResources: ['image', 'stylesheet', 'font', 'media'],
29
+ };
30
+
31
+ // ============================================================================
32
+ // BROWSER AUTOMATION CLASS
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Browser automation for Research agent
37
+ * Uses Playwright for reliable cross-browser automation
38
+ */
39
+ export class BrowserAutomation {
40
+ constructor(options = {}) {
41
+ this.options = { ...BROWSER_CONFIG, ...options };
42
+ this.browser = null;
43
+ this.context = null;
44
+ this.page = null;
45
+ this.playwright = null;
46
+ }
47
+
48
+ /**
49
+ * Launch browser
50
+ */
51
+ async launch() {
52
+ try {
53
+ // Dynamic import of playwright
54
+ const { chromium } = await import('playwright');
55
+ this.playwright = { chromium };
56
+
57
+ this.browser = await chromium.launch({
58
+ headless: this.options.headless,
59
+ });
60
+
61
+ this.context = await this.browser.newContext({
62
+ userAgent: this.options.userAgent,
63
+ viewport: this.options.viewport,
64
+ });
65
+
66
+ // Block unnecessary resources for faster loading
67
+ if (this.options.blockedResources.length > 0) {
68
+ await this.context.route('**/*', (route) => {
69
+ if (this.options.blockedResources.includes(route.request().resourceType())) {
70
+ route.abort();
71
+ } else {
72
+ route.continue();
73
+ }
74
+ });
75
+ }
76
+
77
+ this.page = await this.context.newPage();
78
+ this.page.setDefaultTimeout(this.options.timeout);
79
+
80
+ return true;
81
+ } catch (err) {
82
+ console.log(chalk.yellow(`Browser launch failed: ${err.message}`));
83
+ console.log(chalk.gray('Install Playwright: npm install playwright'));
84
+ return false;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Close browser
90
+ */
91
+ async close() {
92
+ if (this.browser) {
93
+ await this.browser.close();
94
+ this.browser = null;
95
+ this.context = null;
96
+ this.page = null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Navigate to URL
102
+ */
103
+ async navigate(url) {
104
+ if (!this.page) {
105
+ await this.launch();
106
+ }
107
+
108
+ await this.page.goto(url, { waitUntil: 'domcontentloaded' });
109
+ return { url: this.page.url(), title: await this.page.title() };
110
+ }
111
+
112
+ /**
113
+ * Get page content (text extraction)
114
+ */
115
+ async getPageContent(url) {
116
+ await this.navigate(url);
117
+
118
+ // Extract text content
119
+ const content = await this.page.evaluate(() => {
120
+ // Remove scripts and styles
121
+ const scripts = document.querySelectorAll('script, style, noscript');
122
+ scripts.forEach(el => el.remove());
123
+
124
+ // Get main content areas
125
+ const mainContent = document.querySelector('main, article, .content, #content, .main');
126
+ const target = mainContent || document.body;
127
+
128
+ // Extract text
129
+ const text = target.innerText || target.textContent || '';
130
+
131
+ // Clean up whitespace
132
+ return text
133
+ .replace(/\s+/g, ' ')
134
+ .replace(/\n\s*\n/g, '\n')
135
+ .trim()
136
+ .substring(0, 50000); // Limit content size
137
+ });
138
+
139
+ // Extract links
140
+ const links = await this.page.evaluate(() => {
141
+ return Array.from(document.querySelectorAll('a[href]'))
142
+ .slice(0, 50)
143
+ .map(a => ({
144
+ text: a.innerText?.trim().substring(0, 100),
145
+ href: a.href,
146
+ }))
147
+ .filter(l => l.text && l.href.startsWith('http'));
148
+ });
149
+
150
+ // Extract headings
151
+ const headings = await this.page.evaluate(() => {
152
+ return Array.from(document.querySelectorAll('h1, h2, h3'))
153
+ .slice(0, 20)
154
+ .map(h => ({
155
+ level: h.tagName.toLowerCase(),
156
+ text: h.innerText?.trim().substring(0, 200),
157
+ }))
158
+ .filter(h => h.text);
159
+ });
160
+
161
+ return {
162
+ url: this.page.url(),
163
+ title: await this.page.title(),
164
+ content: content.substring(0, 20000), // Limit for context window
165
+ headings,
166
+ links: links.slice(0, 20),
167
+ wordCount: content.split(/\s+/).length,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Take screenshot
173
+ */
174
+ async screenshot(url, options = {}) {
175
+ await this.navigate(url);
176
+
177
+ const { fullPage = false, path: customPath } = options;
178
+
179
+ // Ensure screenshot directory exists
180
+ await fs.mkdir(this.options.screenshotDir, { recursive: true });
181
+
182
+ // Generate filename
183
+ const timestamp = Date.now();
184
+ const filename = customPath || path.join(
185
+ this.options.screenshotDir,
186
+ `screenshot-${timestamp}.png`
187
+ );
188
+
189
+ await this.page.screenshot({
190
+ path: filename,
191
+ fullPage,
192
+ });
193
+
194
+ return {
195
+ path: filename,
196
+ url: this.page.url(),
197
+ title: await this.page.title(),
198
+ timestamp: new Date().toISOString(),
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Click an element
204
+ */
205
+ async click(selector) {
206
+ await this.page.click(selector);
207
+ await this.page.waitForLoadState('domcontentloaded');
208
+
209
+ return {
210
+ clicked: selector,
211
+ url: this.page.url(),
212
+ title: await this.page.title(),
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Fill a form field
218
+ */
219
+ async fill(selector, value) {
220
+ await this.page.fill(selector, value);
221
+
222
+ return {
223
+ filled: selector,
224
+ value: value.substring(0, 50), // Don't expose full value
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Wait for selector
230
+ */
231
+ async waitFor(selector, options = {}) {
232
+ const { timeout = 10000, state = 'visible' } = options;
233
+
234
+ try {
235
+ await this.page.waitForSelector(selector, { timeout, state });
236
+ return { success: true, selector };
237
+ } catch {
238
+ return { success: false, selector, error: 'Timeout waiting for element' };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Execute JavaScript on page
244
+ */
245
+ async evaluate(script) {
246
+ const result = await this.page.evaluate(script);
247
+ return { result };
248
+ }
249
+
250
+ /**
251
+ * Get page metadata
252
+ */
253
+ async getMetadata(url) {
254
+ await this.navigate(url);
255
+
256
+ const metadata = await this.page.evaluate(() => {
257
+ const getMeta = (name) => {
258
+ const el = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
259
+ return el?.getAttribute('content') || null;
260
+ };
261
+
262
+ return {
263
+ title: document.title,
264
+ description: getMeta('description') || getMeta('og:description'),
265
+ keywords: getMeta('keywords'),
266
+ author: getMeta('author'),
267
+ ogImage: getMeta('og:image'),
268
+ ogTitle: getMeta('og:title'),
269
+ canonical: document.querySelector('link[rel="canonical"]')?.href,
270
+ };
271
+ });
272
+
273
+ return { url: this.page.url(), ...metadata };
274
+ }
275
+
276
+ /**
277
+ * Search Google and get results
278
+ */
279
+ async searchGoogle(query) {
280
+ const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
281
+ await this.navigate(searchUrl);
282
+
283
+ // Wait for results
284
+ await this.page.waitForSelector('#search', { timeout: 10000 }).catch(() => {});
285
+
286
+ const results = await this.page.evaluate(() => {
287
+ const items = document.querySelectorAll('#search .g');
288
+ return Array.from(items).slice(0, 10).map(item => {
289
+ const linkEl = item.querySelector('a');
290
+ const titleEl = item.querySelector('h3');
291
+ const snippetEl = item.querySelector('.VwiC3b');
292
+
293
+ return {
294
+ title: titleEl?.innerText || '',
295
+ url: linkEl?.href || '',
296
+ snippet: snippetEl?.innerText || '',
297
+ };
298
+ }).filter(r => r.url && r.title);
299
+ });
300
+
301
+ return { query, results };
302
+ }
303
+
304
+ /**
305
+ * Fetch documentation from common sources
306
+ */
307
+ async fetchDocs(library, topic = '') {
308
+ const docSources = {
309
+ react: 'https://react.dev/reference',
310
+ nextjs: 'https://nextjs.org/docs',
311
+ prisma: 'https://www.prisma.io/docs',
312
+ typescript: 'https://www.typescriptlang.org/docs',
313
+ tailwind: 'https://tailwindcss.com/docs',
314
+ node: 'https://nodejs.org/docs/latest/api',
315
+ };
316
+
317
+ const baseUrl = docSources[library.toLowerCase()];
318
+ if (!baseUrl) {
319
+ // Fall back to Google search
320
+ return this.searchGoogle(`${library} ${topic} documentation`);
321
+ }
322
+
323
+ const url = topic ? `${baseUrl}/${topic}` : baseUrl;
324
+ return this.getPageContent(url);
325
+ }
326
+ }
327
+
328
+ // ============================================================================
329
+ // LIGHTWEIGHT FETCH (No Browser)
330
+ // ============================================================================
331
+
332
+ /**
333
+ * Simple HTTP fetch for when browser is overkill
334
+ */
335
+ export async function simpleFetch(url) {
336
+ const response = await fetch(url, {
337
+ headers: {
338
+ 'User-Agent': BROWSER_CONFIG.userAgent,
339
+ },
340
+ });
341
+
342
+ const contentType = response.headers.get('content-type') || '';
343
+
344
+ if (contentType.includes('application/json')) {
345
+ return { type: 'json', data: await response.json() };
346
+ }
347
+
348
+ const text = await response.text();
349
+
350
+ // Simple HTML to text conversion
351
+ if (contentType.includes('text/html')) {
352
+ const cleaned = text
353
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
354
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
355
+ .replace(/<[^>]+>/g, ' ')
356
+ .replace(/\s+/g, ' ')
357
+ .trim();
358
+
359
+ return { type: 'html', data: cleaned.substring(0, 50000) };
360
+ }
361
+
362
+ return { type: 'text', data: text.substring(0, 50000) };
363
+ }
364
+
365
+ // ============================================================================
366
+ // EXPORTS
367
+ // ============================================================================
368
+
369
+ export default {
370
+ BrowserAutomation,
371
+ simpleFetch,
372
+ BROWSER_CONFIG,
373
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra-dex",
3
- "version": "3.2.0",
4
- "description": "AI Orchestration Meta-Layer for SaaS Development",
3
+ "version": "3.3.0",
4
+ "description": "AI Orchestration Meta-Layer for SaaS Development - The Headless CTO",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "orchestration",
@@ -12,7 +12,12 @@
12
12
  "openai",
13
13
  "gemini",
14
14
  "agents",
15
- "swarm"
15
+ "swarm",
16
+ "docker",
17
+ "sandbox",
18
+ "github",
19
+ "browser-automation",
20
+ "agent-sdk"
16
21
  ],
17
22
  "author": "Srujan Sai Karna",
18
23
  "license": "MIT",
@@ -60,7 +65,8 @@
60
65
  "@anthropic-ai/sdk": "^0.30.0",
61
66
  "@google/generative-ai": "^0.21.0",
62
67
  "dotenv": "^16.4.5",
63
- "openai": "^4.70.0"
68
+ "openai": "^4.70.0",
69
+ "playwright": "^1.41.0"
64
70
  },
65
71
  "devDependencies": {
66
72
  "eslint": "^8.57.0"