osai-agent 4.0.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 (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
@@ -0,0 +1,434 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Browser Automation Module (Playwright + HTTP Fallback)
3
+ // =============================================================================
4
+ // Safe, stateless, per-request browser automation for the AI agent.
5
+ // Each call is fully isolated — launch → close in finally block.
6
+ // Falls back to HTTP fetch if Playwright/Chromium is unavailable.
7
+ // =============================================================================
8
+
9
+ import { logger } from '../utils/logger.js';
10
+ import { searchDDG, searchDDGHttp } from './search-providers.js';
11
+
12
+ let _chromium = null;
13
+ async function getChromium() {
14
+ if (_chromium) return _chromium;
15
+ try {
16
+ const mod = await import('playwright-core');
17
+ _chromium = mod.chromium;
18
+ return _chromium;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ // ─── CONSTANTS ────────────────────────────────────────────────────────────────
25
+
26
+ const NAVIGATION_TIMEOUT = 30000;
27
+ const MAX_CONTENT_LENGTH = 20000;
28
+ const MAX_LINKS = 50;
29
+ const MAX_CACHE_ENTRIES = 100;
30
+ const CACHE_TTL_MS = 5 * 60 * 1000;
31
+ const MAX_RETRIES = 2;
32
+ const UNSAFE_PROTOCOLS = ['file:', 'ftp:', 'javascript:', 'data:'];
33
+
34
+ const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';
35
+
36
+ // ─── CACHE (bounded, TTL-based, FIFO eviction) ───────────────────────────────
37
+
38
+ const _cache = new Map();
39
+ const _cacheKeys = [];
40
+
41
+ function _cacheGet(key) {
42
+ const entry = _cache.get(key);
43
+ if (!entry) return null;
44
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
45
+ _cache.delete(key);
46
+ const idx = _cacheKeys.indexOf(key);
47
+ if (idx >= 0) _cacheKeys.splice(idx, 1);
48
+ return null;
49
+ }
50
+ return entry.data;
51
+ }
52
+
53
+ function _cacheSet(key, data) {
54
+ if (_cacheKeys.length >= MAX_CACHE_ENTRIES) {
55
+ const oldest = _cacheKeys.shift();
56
+ _cache.delete(oldest);
57
+ }
58
+ _cache.set(key, { data, timestamp: Date.now() });
59
+ _cacheKeys.push(key);
60
+ }
61
+
62
+ // ─── URL VALIDATION ───────────────────────────────────────────────────────────
63
+
64
+ function validateUrl(url) {
65
+ if (!url || typeof url !== 'string') {
66
+ return { valid: false, error: 'URL is required and must be a string' };
67
+ }
68
+ const trimmed = url.trim();
69
+ if (!trimmed.startsWith('http://') && !trimmed.startsWith('https://')) {
70
+ return { valid: false, error: `Unsupported protocol. Only http:// and https:// URLs are allowed. Got: "${trimmed.slice(0, 30)}"` };
71
+ }
72
+ const protocol = trimmed.split(':')[0] + ':';
73
+ if (UNSAFE_PROTOCOLS.includes(protocol)) {
74
+ return { valid: false, error: `Blocked unsafe protocol: ${protocol}` };
75
+ }
76
+ try {
77
+ new URL(trimmed);
78
+ } catch {
79
+ return { valid: false, error: `Invalid URL format: "${trimmed.slice(0, 60)}"` };
80
+ }
81
+ return { valid: true, url: trimmed };
82
+ }
83
+
84
+ // ─── TEXT SANITIZATION ────────────────────────────────────────────────────────
85
+
86
+ function sanitizeText(text) {
87
+ if (!text) return '';
88
+ return text
89
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
90
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
91
+ .replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
92
+ .replace(/<[^>]+>/g, ' ')
93
+ .replace(/&amp;/g, '&')
94
+ .replace(/&lt;/g, '<')
95
+ .replace(/&gt;/g, '>')
96
+ .replace(/&quot;/g, '"')
97
+ .replace(/&#39;/g, "'")
98
+ .replace(/&#x27;/g, "'")
99
+ .replace(/&#x2F;/g, '/')
100
+ .replace(/\s{2,}/g, ' ')
101
+ .trim();
102
+ }
103
+
104
+ function truncateContent(text, maxLen = MAX_CONTENT_LENGTH) {
105
+ if (!text) return '';
106
+ if (text.length <= maxLen) return text;
107
+ return text.slice(0, maxLen) + `\n\n...[truncated: ${text.length - maxLen} more chars]`;
108
+ }
109
+
110
+ function dedupeUrls(urls) {
111
+ const seen = new Set();
112
+ const result = [];
113
+ for (const u of urls) {
114
+ if (seen.has(u)) continue;
115
+ seen.add(u);
116
+ if (result.length >= MAX_LINKS) break;
117
+ result.push(u);
118
+ }
119
+ return result;
120
+ }
121
+
122
+ // ─── BROWSER HELPER (stateless per-request) ───────────────────────────────────
123
+
124
+ async function withBrowser(fn) {
125
+ const pw = await getChromium();
126
+ if (!pw) throw new Error('playwright-core not available');
127
+ let browser = null;
128
+ try {
129
+ browser = await pw.launch({
130
+ headless: true,
131
+ args: [
132
+ '--no-sandbox',
133
+ '--disable-setuid-sandbox',
134
+ '--disable-dev-shm-usage',
135
+ '--disable-gpu',
136
+ '--disable-accelerated-2d-canvas',
137
+ '--no-first-run',
138
+ '--no-zygote',
139
+ '--single-process',
140
+ '--disable-background-networking',
141
+ ],
142
+ });
143
+ return await fn(browser);
144
+ } catch (err) {
145
+ throw err;
146
+ } finally {
147
+ if (browser) {
148
+ try { await browser.close(); } catch {}
149
+ }
150
+ }
151
+ }
152
+
153
+ // ─── BROWSER-BASED EXTRACTION ─────────────────────────────────────────────────
154
+
155
+ async function extractWithBrowser(url, timeout = NAVIGATION_TIMEOUT) {
156
+ return withBrowser(async (browser) => {
157
+ const context = await browser.newContext({
158
+ userAgent: USER_AGENT,
159
+ locale: 'en-US',
160
+ });
161
+ const page = await context.newPage();
162
+ page.setDefaultTimeout(timeout);
163
+
164
+ try {
165
+ const response = await page.goto(url, {
166
+ waitUntil: 'domcontentloaded',
167
+ timeout,
168
+ });
169
+
170
+ if (!response) {
171
+ return { success: false, error: 'Navigation returned no response' };
172
+ }
173
+
174
+ const status = response.status();
175
+ if (status >= 400) {
176
+ return { success: false, error: `HTTP ${status}: ${response.statusText()}` };
177
+ }
178
+
179
+ const title = await page.title().catch(() => '');
180
+ const content = await page.innerText('body').catch(() => '');
181
+ const rawLinks = await page.evaluate(() =>
182
+ Array.from(document.querySelectorAll('a[href]'))
183
+ .map(a => a.href)
184
+ .filter(h => h && (h.startsWith('http://') || h.startsWith('https://')))
185
+ ).catch(() => []);
186
+
187
+ return {
188
+ success: true,
189
+ source: 'browser',
190
+ url,
191
+ title: sanitizeText(title),
192
+ content: truncateContent(sanitizeText(content)),
193
+ links: dedupeUrls(rawLinks),
194
+ };
195
+ } finally {
196
+ try { await page.close(); } catch {}
197
+ try { await context.close(); } catch {}
198
+ }
199
+ });
200
+ }
201
+
202
+ // ─── HTTP FALLBACK EXTRACTION ─────────────────────────────────────────────────
203
+
204
+ async function extractWithHttp(url, timeout = NAVIGATION_TIMEOUT) {
205
+ try {
206
+ const controller = new AbortController();
207
+ const timer = setTimeout(() => controller.abort(), timeout);
208
+
209
+ const response = await fetch(url, {
210
+ signal: controller.signal,
211
+ headers: {
212
+ 'User-Agent': USER_AGENT,
213
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
214
+ 'Accept-Language': 'en-US,en;q=0.9',
215
+ },
216
+ redirect: 'follow',
217
+ });
218
+
219
+ clearTimeout(timer);
220
+
221
+ if (!response.ok) {
222
+ return { success: false, error: `HTTP ${response.status}: ${response.statusText()}` };
223
+ }
224
+
225
+ const html = await response.text();
226
+ const contentType = response.headers.get('content-type') || '';
227
+
228
+ // Extract title from HTML
229
+ const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
230
+ const title = titleMatch ? sanitizeText(titleMatch[1]) : '';
231
+
232
+ // Extract text content
233
+ const content = sanitizeText(html);
234
+
235
+ // Extract links
236
+ const linkRegex = /<a[^>]+href="([^"]+)"[^>]*>/gi;
237
+ const links = [];
238
+ let match;
239
+ while ((match = linkRegex.exec(html)) !== null) {
240
+ const href = match[1];
241
+ if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
242
+ links.push(href);
243
+ }
244
+ }
245
+
246
+ return {
247
+ success: true,
248
+ source: 'fallback',
249
+ url,
250
+ title,
251
+ content: truncateContent(content),
252
+ links: dedupeUrls(links),
253
+ };
254
+ } catch (err) {
255
+ return {
256
+ success: false,
257
+ error: `HTTP fallback failed: ${err.message}`,
258
+ };
259
+ }
260
+ }
261
+
262
+ // ─── SEARCH ENGINE PARSER (DuckDuckGo) ───────────────────────────────────────
263
+
264
+ async function searchDuckDuckGo(query, timeout = NAVIGATION_TIMEOUT) {
265
+ try {
266
+ return await searchDDG(query, 5);
267
+ } catch {
268
+ return searchDuckDuckGoHttp(query, timeout);
269
+ }
270
+ }
271
+
272
+ async function searchDuckDuckGoHttp(searchUrl, timeout) {
273
+ try {
274
+ const url = typeof searchUrl === 'string' && searchUrl.startsWith('http')
275
+ ? new URL(searchUrl).searchParams.get('q') || searchUrl
276
+ : searchUrl;
277
+ const q = typeof url === 'string' && url.startsWith('http') ? url : searchUrl;
278
+ if (q.startsWith('http')) {
279
+ return await searchDDGHttp(q, 5);
280
+ }
281
+ return await searchDDGHttp(q, 5);
282
+ } catch {
283
+ return [];
284
+ }
285
+ }
286
+
287
+ // ─── PUBLIC API ───────────────────────────────────────────────────────────────
288
+
289
+ /**
290
+ * Navigate to a URL and extract clean readable content.
291
+ * Uses Playwright Chromium by default, falls back to HTTP fetch on failure.
292
+ *
293
+ * @param {string} url - The URL to browse (http/https only)
294
+ * @returns {Promise<{success: boolean, source: string, url: string, title: string, content: string, links: string[]}>}
295
+ */
296
+ export async function browse(url) {
297
+ // 1. Validate URL before any action
298
+ const validation = validateUrl(url);
299
+ if (!validation.valid) {
300
+ return { success: false, source: 'none', url: url || '', title: '', content: '', links: [], error: validation.error };
301
+ }
302
+
303
+ const safeUrl = validation.url;
304
+
305
+ // 2. Check cache
306
+ const cacheKey = safeUrl;
307
+ const cached = _cacheGet(cacheKey);
308
+ if (cached) {
309
+ logger.debug(`Browser cache hit: ${safeUrl}`);
310
+ return { ...cached, cached: true };
311
+ }
312
+
313
+ // 3. Try Playwright with retry
314
+ let lastError = null;
315
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
316
+ try {
317
+ const result = await extractWithBrowser(safeUrl);
318
+ if (result.success) {
319
+ _cacheSet(cacheKey, result);
320
+ return result;
321
+ }
322
+ lastError = result.error;
323
+ } catch (err) {
324
+ lastError = err.message;
325
+ logger.warn(`Browser attempt ${attempt + 1}/${MAX_RETRIES} failed for ${safeUrl}: ${err.message}`);
326
+ }
327
+ }
328
+
329
+ // 4. Fallback to HTTP fetch
330
+ logger.debug(`Browser failed (${lastError}), falling back to HTTP for: ${safeUrl}`);
331
+ const fallbackResult = await extractWithHttp(safeUrl);
332
+ if (fallbackResult.success) {
333
+ _cacheSet(cacheKey, fallbackResult);
334
+ return fallbackResult;
335
+ }
336
+
337
+ return {
338
+ success: false,
339
+ source: 'none',
340
+ url: safeUrl,
341
+ title: '',
342
+ content: '',
343
+ links: [],
344
+ error: `All methods failed. Last error: ${lastError}`,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Search the web using DuckDuckGo, then open the first valid result.
350
+ *
351
+ * @param {string} query - Search query
352
+ * @returns {Promise<{success: boolean, source: string, url: string, title: string, content: string, links: string[], searchResults?: Array}>}
353
+ */
354
+ export async function searchAndBrowse(query) {
355
+ if (!query || typeof query !== 'string' || query.trim().length < 2) {
356
+ return { success: false, source: 'none', url: '', title: '', content: '', links: [], error: 'Search query too short (minimum 2 characters)' };
357
+ }
358
+
359
+ const searchResults = await searchDuckDuckGo(query);
360
+
361
+ if (!searchResults || searchResults.length === 0) {
362
+ return { success: false, source: 'none', url: '', title: '', content: '', links: [], error: `No search results found for "${query}"` };
363
+ }
364
+
365
+ // Pick the first valid result
366
+ const firstResult = searchResults[0];
367
+ const browseResult = await browse(firstResult.url);
368
+
369
+ return {
370
+ ...browseResult,
371
+ searchResults: searchResults.map(r => ({ title: r.title, url: r.url, snippet: r.snippet })),
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Extract structured data from a page using CSS selectors.
377
+ *
378
+ * @param {Object} selectors - Map of key → CSS selector (e.g. { heading: 'h1', price: '.price' })
379
+ * @param {string} url - The page URL to extract from
380
+ * @returns {Promise<{success: boolean, source: string, url: string, data: Object}>}
381
+ */
382
+ export async function extractStructuredData(selectors, url) {
383
+ if (!selectors || typeof selectors !== 'object' || Object.keys(selectors).length === 0) {
384
+ return { success: false, source: 'none', url: url || '', data: {}, error: 'Selectors map is required' };
385
+ }
386
+
387
+ const validation = validateUrl(url);
388
+ if (!validation.valid) {
389
+ return { success: false, source: 'none', url: url || '', data: {}, error: validation.error };
390
+ }
391
+
392
+ const safeUrl = validation.url;
393
+
394
+ try {
395
+ return withBrowser(async (browser) => {
396
+ const context = await browser.newContext({ userAgent: USER_AGENT });
397
+ const page = await context.newPage();
398
+ page.setDefaultTimeout(NAVIGATION_TIMEOUT);
399
+
400
+ try {
401
+ await page.goto(safeUrl, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT });
402
+
403
+ const data = {};
404
+ for (const [key, selector] of Object.entries(selectors)) {
405
+ try {
406
+ const element = page.locator(selector).first();
407
+ const text = await element.innerText();
408
+ data[key] = sanitizeText(text);
409
+ } catch {
410
+ data[key] = null;
411
+ }
412
+ }
413
+
414
+ return {
415
+ success: true,
416
+ source: 'browser',
417
+ url: safeUrl,
418
+ data,
419
+ };
420
+ } finally {
421
+ try { await page.close(); } catch {}
422
+ try { await context.close(); } catch {}
423
+ }
424
+ });
425
+ } catch (err) {
426
+ return {
427
+ success: false,
428
+ source: 'none',
429
+ url: safeUrl,
430
+ data: {},
431
+ error: `Structured extraction failed: ${err.message}`,
432
+ };
433
+ }
434
+ }