tuna-agent 0.1.0 → 0.1.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 (68) hide show
  1. package/dist/agents/claude-code-adapter.d.ts +3 -1
  2. package/dist/agents/claude-code-adapter.js +28 -4
  3. package/dist/agents/factory.d.ts +2 -1
  4. package/dist/agents/factory.js +2 -2
  5. package/dist/browser/actions/download.d.ts +16 -0
  6. package/dist/browser/actions/download.js +39 -0
  7. package/dist/browser/actions/emulation.d.ts +53 -0
  8. package/dist/browser/actions/emulation.js +103 -0
  9. package/dist/browser/actions/evaluate.d.ts +29 -0
  10. package/dist/browser/actions/evaluate.js +92 -0
  11. package/dist/browser/actions/interaction.d.ts +79 -0
  12. package/dist/browser/actions/interaction.js +210 -0
  13. package/dist/browser/actions/keyboard.d.ts +6 -0
  14. package/dist/browser/actions/keyboard.js +9 -0
  15. package/dist/browser/actions/navigation.d.ts +40 -0
  16. package/dist/browser/actions/navigation.js +92 -0
  17. package/dist/browser/actions/wait.d.ts +12 -0
  18. package/dist/browser/actions/wait.js +33 -0
  19. package/dist/browser/browser.d.ts +722 -0
  20. package/dist/browser/browser.js +1066 -0
  21. package/dist/browser/capture/activity.d.ts +22 -0
  22. package/dist/browser/capture/activity.js +39 -0
  23. package/dist/browser/capture/pdf.d.ts +6 -0
  24. package/dist/browser/capture/pdf.js +6 -0
  25. package/dist/browser/capture/response.d.ts +8 -0
  26. package/dist/browser/capture/response.js +28 -0
  27. package/dist/browser/capture/screenshot.d.ts +30 -0
  28. package/dist/browser/capture/screenshot.js +72 -0
  29. package/dist/browser/capture/trace.d.ts +13 -0
  30. package/dist/browser/capture/trace.js +19 -0
  31. package/dist/browser/chrome-launcher.d.ts +8 -0
  32. package/dist/browser/chrome-launcher.js +543 -0
  33. package/dist/browser/connection.d.ts +42 -0
  34. package/dist/browser/connection.js +359 -0
  35. package/dist/browser/index.d.ts +6 -0
  36. package/dist/browser/index.js +3 -0
  37. package/dist/browser/security.d.ts +51 -0
  38. package/dist/browser/security.js +357 -0
  39. package/dist/browser/snapshot/ai-snapshot.d.ts +12 -0
  40. package/dist/browser/snapshot/ai-snapshot.js +47 -0
  41. package/dist/browser/snapshot/aria-snapshot.d.ts +26 -0
  42. package/dist/browser/snapshot/aria-snapshot.js +121 -0
  43. package/dist/browser/snapshot/ref-map.d.ts +31 -0
  44. package/dist/browser/snapshot/ref-map.js +250 -0
  45. package/dist/browser/storage/index.d.ts +36 -0
  46. package/dist/browser/storage/index.js +65 -0
  47. package/dist/browser/types.d.ts +429 -0
  48. package/dist/browser/types.js +2 -0
  49. package/dist/cli/commands/extension.d.ts +10 -0
  50. package/dist/cli/commands/extension.js +86 -0
  51. package/dist/cli/index.js +12 -0
  52. package/dist/daemon/extension-handlers.d.ts +63 -0
  53. package/dist/daemon/extension-handlers.js +630 -0
  54. package/dist/daemon/index.js +173 -44
  55. package/dist/daemon/ws-client.d.ts +28 -8
  56. package/dist/daemon/ws-client.js +68 -62
  57. package/dist/mcp/browser-server.d.ts +11 -0
  58. package/dist/mcp/browser-server.js +467 -0
  59. package/dist/mcp/knowledge-server.d.ts +11 -0
  60. package/dist/mcp/knowledge-server.js +263 -0
  61. package/dist/mcp/setup.d.ts +20 -0
  62. package/dist/mcp/setup.js +94 -0
  63. package/dist/types/index.d.ts +2 -0
  64. package/dist/utils/claude-cli.d.ts +2 -0
  65. package/dist/utils/claude-cli.js +29 -9
  66. package/dist/utils/message-schemas.d.ts +4 -1
  67. package/dist/utils/message-schemas.js +6 -1
  68. package/package.json +2 -1
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Browser MCP Server for Tuna Agent
4
+ *
5
+ * Stdio-based MCP server that exposes browser automation tools to Claude Code.
6
+ * Uses browserclaw (vendored from OpenClaw) for snapshot+ref browser control.
7
+ *
8
+ * Usage:
9
+ * node browser-server.js [--headless] [--user-data-dir /path/to/chrome/profile] [--cdp-port 9222]
10
+ */
11
+ import { BrowserClaw, setStealthEnabled } from '../browser/index.js';
12
+ function parseArgs() {
13
+ const args = process.argv.slice(2);
14
+ let headless = false; // Default: visible browser (not headless)
15
+ let userDataDir;
16
+ let profileDirectory;
17
+ let cdpPort;
18
+ let cdpUrl;
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] === '--headless')
21
+ headless = true;
22
+ else if (args[i] === '--user-data-dir' && args[i + 1])
23
+ userDataDir = args[++i];
24
+ else if (args[i] === '--profile-directory' && args[i + 1])
25
+ profileDirectory = args[++i];
26
+ else if (args[i] === '--cdp-port' && args[i + 1])
27
+ cdpPort = parseInt(args[++i], 10);
28
+ else if (args[i] === '--cdp-url' && args[i + 1])
29
+ cdpUrl = args[++i];
30
+ }
31
+ // Disable stealth injection for existing Chrome profiles (sites detect it and force logout)
32
+ if (userDataDir) {
33
+ setStealthEnabled(false);
34
+ }
35
+ return { headless, userDataDir, profileDirectory, cdpPort, cdpUrl };
36
+ }
37
+ // ===== Browser State =====
38
+ let browser = null;
39
+ const pages = new Map(); // targetId → CrawlPage
40
+ async function ensureBrowser(config) {
41
+ if (browser)
42
+ return browser;
43
+ if (config.cdpUrl) {
44
+ browser = await BrowserClaw.connect(config.cdpUrl);
45
+ }
46
+ else {
47
+ browser = await BrowserClaw.launch({
48
+ headless: config.headless,
49
+ userDataDir: config.userDataDir,
50
+ profileDirectory: config.profileDirectory,
51
+ cdpPort: config.cdpPort,
52
+ });
53
+ }
54
+ return browser;
55
+ }
56
+ function sendResponse(res) {
57
+ process.stdout.write(JSON.stringify(res) + '\n');
58
+ }
59
+ function sendResult(id, result) {
60
+ sendResponse({ jsonrpc: '2.0', id: id ?? null, result });
61
+ }
62
+ function sendError(id, code, message) {
63
+ sendResponse({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
64
+ }
65
+ // ===== Tool Definitions =====
66
+ const TOOLS = [
67
+ {
68
+ name: 'browser_open',
69
+ description: 'Open a URL in the browser and return an AI-readable snapshot of the page. Returns a text tree with numbered refs (e1, e2, ...) for interactive elements.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ url: { type: 'string', description: 'URL to navigate to (e.g. https://facebook.com)' },
74
+ },
75
+ required: ['url'],
76
+ },
77
+ },
78
+ {
79
+ name: 'browser_snapshot',
80
+ description: 'Take a snapshot of the current page. Returns an accessibility tree with numbered refs for all interactive elements. Use this after any navigation or page change to get fresh refs.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ page_id: { type: 'string', description: 'Target page ID (optional, uses last active page if omitted)' },
85
+ },
86
+ },
87
+ },
88
+ {
89
+ name: 'browser_click',
90
+ description: 'Click an element by its ref number from a snapshot. Run browser_snapshot first to get refs.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ ref: { type: 'string', description: 'Element ref from snapshot (e.g. "e1", "e5")' },
95
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
96
+ double_click: { type: 'boolean', description: 'Double-click instead of single click' },
97
+ },
98
+ required: ['ref'],
99
+ },
100
+ },
101
+ {
102
+ name: 'browser_type',
103
+ description: 'Type text into an element by its ref number. The element is clicked first, then text is typed.',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ ref: { type: 'string', description: 'Element ref from snapshot (e.g. "e3")' },
108
+ text: { type: 'string', description: 'Text to type' },
109
+ submit: { type: 'boolean', description: 'Press Enter after typing to submit' },
110
+ slowly: { type: 'boolean', description: 'Type slowly with keystroke delays (more human-like)' },
111
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
112
+ },
113
+ required: ['ref', 'text'],
114
+ },
115
+ },
116
+ {
117
+ name: 'browser_navigate',
118
+ description: 'Navigate to a different URL on the current page.',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ url: { type: 'string', description: 'URL to navigate to' },
123
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
124
+ },
125
+ required: ['url'],
126
+ },
127
+ },
128
+ {
129
+ name: 'browser_scroll',
130
+ description: 'Scroll the page or a specific element into view.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ ref: { type: 'string', description: 'Element ref to scroll into view (optional, scrolls page if omitted)' },
135
+ direction: { type: 'string', description: 'Scroll direction: "up" or "down" (default: "down")' },
136
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: 'browser_screenshot',
142
+ description: 'Take a screenshot of the current page. Returns base64-encoded PNG image.',
143
+ inputSchema: {
144
+ type: 'object',
145
+ properties: {
146
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
147
+ full_page: { type: 'boolean', description: 'Capture the full scrollable page (default: false, viewport only)' },
148
+ },
149
+ },
150
+ },
151
+ {
152
+ name: 'browser_tabs',
153
+ description: 'List all open browser tabs with their IDs, URLs, and titles.',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {},
157
+ },
158
+ },
159
+ {
160
+ name: 'browser_press',
161
+ description: 'Press a keyboard key (e.g. Enter, Tab, Escape, ArrowDown).',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ key: { type: 'string', description: 'Key to press (e.g. "Enter", "Tab", "Escape", "ArrowDown")' },
166
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
167
+ },
168
+ required: ['key'],
169
+ },
170
+ },
171
+ {
172
+ name: 'browser_select',
173
+ description: 'Select an option from a dropdown/select element.',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ ref: { type: 'string', description: 'Element ref of the select/dropdown' },
178
+ value: { type: 'string', description: 'Value or label to select' },
179
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
180
+ },
181
+ required: ['ref', 'value'],
182
+ },
183
+ },
184
+ {
185
+ name: 'browser_hover',
186
+ description: 'Hover over an element by its ref number.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ ref: { type: 'string', description: 'Element ref from snapshot' },
191
+ page_id: { type: 'string', description: 'Target page ID (optional)' },
192
+ },
193
+ required: ['ref'],
194
+ },
195
+ },
196
+ {
197
+ name: 'browser_close',
198
+ description: 'Close the browser and clean up resources.',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {},
202
+ },
203
+ },
204
+ ];
205
+ // ===== Request Handler =====
206
+ const serverConfig = parseArgs();
207
+ let activePageId = null;
208
+ async function handleRequest(req) {
209
+ try {
210
+ switch (req.method) {
211
+ case 'initialize':
212
+ sendResult(req.id, {
213
+ protocolVersion: '2024-11-05',
214
+ capabilities: { tools: {} },
215
+ serverInfo: { name: 'tuna-browser', version: '1.0.0' },
216
+ });
217
+ break;
218
+ case 'notifications/initialized':
219
+ break;
220
+ case 'tools/list':
221
+ sendResult(req.id, { tools: TOOLS });
222
+ break;
223
+ case 'tools/call': {
224
+ const toolName = req.params?.name ?? '';
225
+ const args = req.params?.arguments ?? {};
226
+ const result = await handleToolCall(toolName, args);
227
+ sendResult(req.id, result);
228
+ break;
229
+ }
230
+ case 'ping':
231
+ sendResult(req.id, {});
232
+ break;
233
+ default:
234
+ if (req.id !== undefined) {
235
+ sendError(req.id, -32601, `Method not found: ${req.method}`);
236
+ }
237
+ }
238
+ }
239
+ catch (err) {
240
+ const message = err instanceof Error ? err.message : String(err);
241
+ if (req.id !== undefined) {
242
+ sendError(req.id, -32603, message);
243
+ }
244
+ }
245
+ }
246
+ function getPageId(args) {
247
+ return args.page_id || activePageId || undefined;
248
+ }
249
+ async function getPage(args) {
250
+ const pageId = getPageId(args);
251
+ if (pageId && pages.has(pageId)) {
252
+ return pages.get(pageId);
253
+ }
254
+ // Return last active page or first available
255
+ if (activePageId && pages.has(activePageId)) {
256
+ return pages.get(activePageId);
257
+ }
258
+ // No pages open
259
+ throw new Error('No browser page is open. Use browser_open first to navigate to a URL.');
260
+ }
261
+ async function handleToolCall(toolName, args) {
262
+ try {
263
+ switch (toolName) {
264
+ case 'browser_open': {
265
+ const url = args.url;
266
+ if (!url)
267
+ return { content: [{ type: 'text', text: 'Error: url is required' }], isError: true };
268
+ const b = await ensureBrowser(serverConfig);
269
+ const page = await b.open(url);
270
+ const pageId = page.id;
271
+ pages.set(pageId, page);
272
+ activePageId = pageId;
273
+ // Auto-snapshot after opening
274
+ const result = await page.snapshot();
275
+ return {
276
+ content: [{
277
+ type: 'text',
278
+ text: `Page opened: ${url}\nPage ID: ${pageId}\n\n--- Snapshot ---\n${result.snapshot}\n\n--- Stats ---\nRefs: ${Object.keys(result.refs).length} interactive elements`,
279
+ }],
280
+ };
281
+ }
282
+ case 'browser_snapshot': {
283
+ const page = await getPage(args);
284
+ const result = await page.snapshot();
285
+ return {
286
+ content: [{
287
+ type: 'text',
288
+ text: `${result.snapshot}\n\n--- Stats ---\nRefs: ${Object.keys(result.refs).length} interactive elements`,
289
+ }],
290
+ };
291
+ }
292
+ case 'browser_click': {
293
+ const ref = args.ref;
294
+ if (!ref)
295
+ return { content: [{ type: 'text', text: 'Error: ref is required' }], isError: true };
296
+ const page = await getPage(args);
297
+ await page.click(ref, {
298
+ doubleClick: args.double_click,
299
+ });
300
+ // Auto-snapshot after click to show result
301
+ const result = await page.snapshot();
302
+ return {
303
+ content: [{
304
+ type: 'text',
305
+ text: `Clicked "${ref}".\n\n--- Updated Snapshot ---\n${result.snapshot}`,
306
+ }],
307
+ };
308
+ }
309
+ case 'browser_type': {
310
+ const ref = args.ref;
311
+ const text = args.text;
312
+ if (!ref || text === undefined)
313
+ return { content: [{ type: 'text', text: 'Error: ref and text are required' }], isError: true };
314
+ const page = await getPage(args);
315
+ await page.type(ref, text, {
316
+ submit: args.submit,
317
+ slowly: args.slowly,
318
+ });
319
+ return {
320
+ content: [{
321
+ type: 'text',
322
+ text: `Typed "${text}" into "${ref}"${args.submit ? ' and submitted' : ''}.`,
323
+ }],
324
+ };
325
+ }
326
+ case 'browser_navigate': {
327
+ const url = args.url;
328
+ if (!url)
329
+ return { content: [{ type: 'text', text: 'Error: url is required' }], isError: true };
330
+ const page = await getPage(args);
331
+ await page.goto(url);
332
+ const result = await page.snapshot();
333
+ return {
334
+ content: [{
335
+ type: 'text',
336
+ text: `Navigated to: ${url}\n\n--- Snapshot ---\n${result.snapshot}`,
337
+ }],
338
+ };
339
+ }
340
+ case 'browser_scroll': {
341
+ const page = await getPage(args);
342
+ if (args.ref) {
343
+ await page.scrollIntoView(args.ref);
344
+ }
345
+ else {
346
+ // Scroll page via keyboard
347
+ const key = args.direction === 'up' ? 'PageUp' : 'PageDown';
348
+ await page.press(key);
349
+ }
350
+ const result = await page.snapshot();
351
+ return {
352
+ content: [{
353
+ type: 'text',
354
+ text: `Scrolled ${args.direction || 'down'}.\n\n--- Updated Snapshot ---\n${result.snapshot}`,
355
+ }],
356
+ };
357
+ }
358
+ case 'browser_screenshot': {
359
+ const page = await getPage(args);
360
+ const buf = await page.screenshot({
361
+ fullPage: args.full_page,
362
+ });
363
+ return {
364
+ content: [{
365
+ type: 'image',
366
+ data: buf.toString('base64'),
367
+ mimeType: 'image/png',
368
+ }],
369
+ };
370
+ }
371
+ case 'browser_tabs': {
372
+ const b = await ensureBrowser(serverConfig);
373
+ const tabs = await b.tabs();
374
+ if (tabs.length === 0) {
375
+ return { content: [{ type: 'text', text: 'No tabs open.' }] };
376
+ }
377
+ const listing = tabs.map((t) => `- ${t.title || 'Untitled'} (ID: ${t.targetId})\n URL: ${t.url}`).join('\n');
378
+ return { content: [{ type: 'text', text: `Open tabs:\n${listing}` }] };
379
+ }
380
+ case 'browser_press': {
381
+ const key = args.key;
382
+ if (!key)
383
+ return { content: [{ type: 'text', text: 'Error: key is required' }], isError: true };
384
+ const page = await getPage(args);
385
+ await page.press(key);
386
+ return { content: [{ type: 'text', text: `Pressed "${key}".` }] };
387
+ }
388
+ case 'browser_select': {
389
+ const ref = args.ref;
390
+ const value = args.value;
391
+ if (!ref || !value)
392
+ return { content: [{ type: 'text', text: 'Error: ref and value are required' }], isError: true };
393
+ const page = await getPage(args);
394
+ await page.select(ref, value);
395
+ return { content: [{ type: 'text', text: `Selected "${value}" in "${ref}".` }] };
396
+ }
397
+ case 'browser_hover': {
398
+ const ref = args.ref;
399
+ if (!ref)
400
+ return { content: [{ type: 'text', text: 'Error: ref is required' }], isError: true };
401
+ const page = await getPage(args);
402
+ await page.hover(ref);
403
+ return { content: [{ type: 'text', text: `Hovered over "${ref}".` }] };
404
+ }
405
+ case 'browser_close': {
406
+ if (browser) {
407
+ await browser.stop();
408
+ browser = null;
409
+ pages.clear();
410
+ activePageId = null;
411
+ }
412
+ return { content: [{ type: 'text', text: 'Browser closed.' }] };
413
+ }
414
+ default:
415
+ return { content: [{ type: 'text', text: `Unknown tool: ${toolName}` }], isError: true };
416
+ }
417
+ }
418
+ catch (err) {
419
+ const message = err instanceof Error ? err.message : String(err);
420
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
421
+ }
422
+ }
423
+ // ===== Stdio Transport =====
424
+ function startServer() {
425
+ process.stderr.write(`[browser-mcp] Starting (headless: ${serverConfig.headless})\n`);
426
+ let buffer = '';
427
+ process.stdin.setEncoding('utf-8');
428
+ process.stdin.on('data', (chunk) => {
429
+ buffer += chunk;
430
+ const lines = buffer.split('\n');
431
+ buffer = lines.pop() ?? '';
432
+ for (const line of lines) {
433
+ const trimmed = line.trim();
434
+ if (!trimmed)
435
+ continue;
436
+ try {
437
+ const req = JSON.parse(trimmed);
438
+ handleRequest(req).catch((err) => {
439
+ process.stderr.write(`[browser-mcp] Error handling ${req.method}: ${err}\n`);
440
+ });
441
+ }
442
+ catch {
443
+ process.stderr.write(`[browser-mcp] Failed to parse JSON: ${trimmed.slice(0, 100)}\n`);
444
+ }
445
+ }
446
+ });
447
+ process.stdin.on('end', async () => {
448
+ process.stderr.write('[browser-mcp] stdin closed, cleaning up browser\n');
449
+ if (browser) {
450
+ await browser.stop().catch(() => { });
451
+ }
452
+ process.exit(0);
453
+ });
454
+ // Cleanup on process exit
455
+ process.on('SIGTERM', async () => {
456
+ if (browser)
457
+ await browser.stop().catch(() => { });
458
+ process.exit(0);
459
+ });
460
+ process.on('SIGINT', async () => {
461
+ if (browser)
462
+ await browser.stop().catch(() => { });
463
+ process.exit(0);
464
+ });
465
+ }
466
+ // ===== Main =====
467
+ startServer();
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knowledge MCP Server for Tuna Agent
4
+ *
5
+ * Stdio-based MCP server that exposes knowledge tools to Claude Code.
6
+ * Communicates with the Tuna API using agent token auth.
7
+ *
8
+ * Usage:
9
+ * node knowledge-server.js --api-url https://api.tuna.ai --token xxx --agent-id abc123
10
+ */
11
+ export {};