kernelbot 1.0.19 → 1.0.21
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.
- package/kernelbot/hello-world.md +21 -0
- package/kernelbot/newnew-1.md +11 -0
- package/package.json +3 -1
- package/src/bot.js +60 -4
- package/src/coder.js +81 -11
- package/src/prompts/system.js +10 -0
- package/src/security/confirm.js +1 -0
- package/src/tools/browser.js +624 -0
- package/src/tools/index.js +3 -0
- package/src/utils/display.js +14 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Hello World! 🌍
|
|
2
|
+
|
|
3
|
+
This is a **test file** created to verify that everything is working properly.
|
|
4
|
+
|
|
5
|
+
## About This File
|
|
6
|
+
|
|
7
|
+
This file demonstrates:
|
|
8
|
+
- *Basic markdown formatting*
|
|
9
|
+
- **Bold text**
|
|
10
|
+
- Simple lists
|
|
11
|
+
|
|
12
|
+
## Features Tested
|
|
13
|
+
|
|
14
|
+
- ✅ File creation
|
|
15
|
+
- ✅ Markdown formatting
|
|
16
|
+
- ✅ Emoji support 🚀
|
|
17
|
+
- ✅ Basic structure
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
*Created as a test for the KernelBot project!* 🤖
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kernelbot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "KernelBot — AI engineering agent with full OS control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
"chalk": "^5.4.1",
|
|
35
35
|
"commander": "^13.1.0",
|
|
36
36
|
"dotenv": "^16.4.7",
|
|
37
|
+
"gradient-string": "^3.0.0",
|
|
37
38
|
"js-yaml": "^4.1.0",
|
|
38
39
|
"node-telegram-bot-api": "^0.66.0",
|
|
39
40
|
"ora": "^8.1.1",
|
|
41
|
+
"puppeteer": "^24.37.3",
|
|
40
42
|
"simple-git": "^3.31.1",
|
|
41
43
|
"uuid": "^11.1.0",
|
|
42
44
|
"winston": "^3.17.0"
|
package/src/bot.js
CHANGED
|
@@ -47,7 +47,7 @@ export function startBot(config, agent, conversationManager) {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
let text = msg.text.trim();
|
|
51
51
|
|
|
52
52
|
// Handle commands
|
|
53
53
|
if (text === '/clean' || text === '/clear' || text === '/reset') {
|
|
@@ -69,6 +69,9 @@ export function startBot(config, agent, conversationManager) {
|
|
|
69
69
|
'',
|
|
70
70
|
'/clean — Clear conversation and start fresh',
|
|
71
71
|
'/history — Show message count in memory',
|
|
72
|
+
'/browse <url> — Browse a website and get a summary',
|
|
73
|
+
'/screenshot <url> — Take a screenshot of a website',
|
|
74
|
+
'/extract <url> <selector> — Extract content using CSS selector',
|
|
72
75
|
'/help — Show this help message',
|
|
73
76
|
'',
|
|
74
77
|
'Or just send any message to chat with the agent.',
|
|
@@ -76,6 +79,32 @@ export function startBot(config, agent, conversationManager) {
|
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// Web browsing shortcut commands — rewrite as natural language for the agent
|
|
83
|
+
if (text.startsWith('/browse ')) {
|
|
84
|
+
const browseUrl = text.slice('/browse '.length).trim();
|
|
85
|
+
if (!browseUrl) {
|
|
86
|
+
await bot.sendMessage(chatId, 'Usage: /browse <url>');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
text = `Browse this website and give me a summary: ${browseUrl}`;
|
|
90
|
+
} else if (text.startsWith('/screenshot ')) {
|
|
91
|
+
const screenshotUrl = text.slice('/screenshot '.length).trim();
|
|
92
|
+
if (!screenshotUrl) {
|
|
93
|
+
await bot.sendMessage(chatId, 'Usage: /screenshot <url>');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
text = `Take a screenshot of this website: ${screenshotUrl}`;
|
|
97
|
+
} else if (text.startsWith('/extract ')) {
|
|
98
|
+
const extractParts = text.slice('/extract '.length).trim().split(/\s+/);
|
|
99
|
+
if (extractParts.length < 2) {
|
|
100
|
+
await bot.sendMessage(chatId, 'Usage: /extract <url> <css-selector>');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const extractUrl = extractParts[0];
|
|
104
|
+
const extractSelector = extractParts.slice(1).join(' ');
|
|
105
|
+
text = `Extract content from ${extractUrl} using the CSS selector: ${extractSelector}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
logger.info(`Message from ${username} (${userId}): ${text.slice(0, 100)}`);
|
|
80
109
|
|
|
81
110
|
// Show typing and keep refreshing it
|
|
@@ -85,15 +114,42 @@ export function startBot(config, agent, conversationManager) {
|
|
|
85
114
|
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
86
115
|
|
|
87
116
|
try {
|
|
88
|
-
const onUpdate = async (update) => {
|
|
117
|
+
const onUpdate = async (update, opts = {}) => {
|
|
118
|
+
// Edit an existing message instead of sending a new one
|
|
119
|
+
if (opts.editMessageId) {
|
|
120
|
+
try {
|
|
121
|
+
const edited = await bot.editMessageText(update, {
|
|
122
|
+
chat_id: chatId,
|
|
123
|
+
message_id: opts.editMessageId,
|
|
124
|
+
parse_mode: 'Markdown',
|
|
125
|
+
});
|
|
126
|
+
return edited.message_id;
|
|
127
|
+
} catch {
|
|
128
|
+
try {
|
|
129
|
+
const edited = await bot.editMessageText(update, {
|
|
130
|
+
chat_id: chatId,
|
|
131
|
+
message_id: opts.editMessageId,
|
|
132
|
+
});
|
|
133
|
+
return edited.message_id;
|
|
134
|
+
} catch {
|
|
135
|
+
return opts.editMessageId;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Send new message(s)
|
|
89
141
|
const parts = splitMessage(update);
|
|
142
|
+
let lastMsgId = null;
|
|
90
143
|
for (const part of parts) {
|
|
91
144
|
try {
|
|
92
|
-
await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
145
|
+
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
146
|
+
lastMsgId = sent.message_id;
|
|
93
147
|
} catch {
|
|
94
|
-
await bot.sendMessage(chatId, part);
|
|
148
|
+
const sent = await bot.sendMessage(chatId, part);
|
|
149
|
+
lastMsgId = sent.message_id;
|
|
95
150
|
}
|
|
96
151
|
}
|
|
152
|
+
return lastMsgId;
|
|
97
153
|
};
|
|
98
154
|
|
|
99
155
|
const reply = await agent.processMessage(chatId, text, {
|
package/src/coder.js
CHANGED
|
@@ -103,7 +103,7 @@ function processEvent(line, onOutput, logger) {
|
|
|
103
103
|
const tool = extractToolUse(event);
|
|
104
104
|
if (tool) {
|
|
105
105
|
logger.info(`Claude Code tool: ${tool.name}: ${tool.summary}`);
|
|
106
|
-
if (onOutput) onOutput(`🔨
|
|
106
|
+
if (onOutput) onOutput(`🔨 ${tool.name}: ${tool.summary}`).catch(() => {});
|
|
107
107
|
}
|
|
108
108
|
return event;
|
|
109
109
|
}
|
|
@@ -113,7 +113,7 @@ function processEvent(line, onOutput, logger) {
|
|
|
113
113
|
const tool = extractToolUse(event);
|
|
114
114
|
if (tool) {
|
|
115
115
|
logger.info(`Claude Code tool: ${tool.name}: ${tool.summary}`);
|
|
116
|
-
if (onOutput) onOutput(`🔨
|
|
116
|
+
if (onOutput) onOutput(`🔨 ${tool.name}: ${tool.summary}`).catch(() => {});
|
|
117
117
|
}
|
|
118
118
|
return event;
|
|
119
119
|
}
|
|
@@ -150,6 +150,7 @@ export class ClaudeCodeSpawner {
|
|
|
150
150
|
'-p', prompt,
|
|
151
151
|
'--max-turns', String(turns),
|
|
152
152
|
'--output-format', 'stream-json',
|
|
153
|
+
'--verbose',
|
|
153
154
|
'--dangerously-skip-permissions',
|
|
154
155
|
];
|
|
155
156
|
if (this.model) {
|
|
@@ -159,7 +160,63 @@ export class ClaudeCodeSpawner {
|
|
|
159
160
|
const cmd = `claude ${args.map((a) => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
|
|
160
161
|
logger.info(`Spawning: ${cmd.slice(0, 300)}`);
|
|
161
162
|
logger.info(`CWD: ${workingDirectory}`);
|
|
162
|
-
|
|
163
|
+
|
|
164
|
+
// --- Smart output: consolidate tool activity into one editable message ---
|
|
165
|
+
let statusMsgId = null;
|
|
166
|
+
let activityLines = [];
|
|
167
|
+
let flushTimer = null;
|
|
168
|
+
const MAX_VISIBLE = 15;
|
|
169
|
+
|
|
170
|
+
const buildStatusText = (finalState = null) => {
|
|
171
|
+
const visible = activityLines.slice(-MAX_VISIBLE);
|
|
172
|
+
const countInfo = activityLines.length > MAX_VISIBLE
|
|
173
|
+
? `\n_... ${activityLines.length} operations total_\n`
|
|
174
|
+
: '';
|
|
175
|
+
if (finalState === 'done') {
|
|
176
|
+
return `✅ *Claude Code Done* — ${activityLines.length} ops\n${countInfo}\n${visible.join('\n')}`;
|
|
177
|
+
}
|
|
178
|
+
if (finalState === 'error') {
|
|
179
|
+
return `❌ *Claude Code Failed* — ${activityLines.length} ops\n${countInfo}\n${visible.join('\n')}`;
|
|
180
|
+
}
|
|
181
|
+
return `⚙️ *Claude Code Working...*\n${countInfo}\n${visible.join('\n')}`;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const flushStatus = async () => {
|
|
185
|
+
flushTimer = null;
|
|
186
|
+
if (!onOutput || activityLines.length === 0) return;
|
|
187
|
+
try {
|
|
188
|
+
if (statusMsgId) {
|
|
189
|
+
await onOutput(buildStatusText(), { editMessageId: statusMsgId });
|
|
190
|
+
} else {
|
|
191
|
+
statusMsgId = await onOutput(buildStatusText());
|
|
192
|
+
}
|
|
193
|
+
} catch {}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const addActivity = (line) => {
|
|
197
|
+
activityLines.push(line);
|
|
198
|
+
if (!statusMsgId && !flushTimer) {
|
|
199
|
+
// First activity — create the status message immediately
|
|
200
|
+
flushStatus();
|
|
201
|
+
} else if (!flushTimer) {
|
|
202
|
+
// Throttle subsequent edits to avoid Telegram rate limits
|
|
203
|
+
flushTimer = setTimeout(flushStatus, 1000);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const smartOutput = onOutput ? async (text) => {
|
|
208
|
+
// Tool calls, raw output, warnings, starting → accumulate in status message
|
|
209
|
+
if (text.startsWith('🔨') || text.startsWith('📟') || text.startsWith('⚠️') || text.startsWith('⏳')) {
|
|
210
|
+
addActivity(text);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Completion → handled by close handler, skip
|
|
214
|
+
if (text.startsWith('✅')) return;
|
|
215
|
+
// Everything else (💬 text, ❌ error, ⏰ timeout) → new message
|
|
216
|
+
await onOutput(text);
|
|
217
|
+
} : null;
|
|
218
|
+
|
|
219
|
+
if (smartOutput) smartOutput(`⏳ Starting Claude Code...`).catch(() => {});
|
|
163
220
|
|
|
164
221
|
return new Promise((resolve, reject) => {
|
|
165
222
|
const child = spawn('claude', args, {
|
|
@@ -192,7 +249,7 @@ export class ClaudeCodeSpawner {
|
|
|
192
249
|
}
|
|
193
250
|
} catch {}
|
|
194
251
|
|
|
195
|
-
processEvent(trimmed,
|
|
252
|
+
processEvent(trimmed, smartOutput, logger);
|
|
196
253
|
}
|
|
197
254
|
});
|
|
198
255
|
|
|
@@ -200,19 +257,18 @@ export class ClaudeCodeSpawner {
|
|
|
200
257
|
const chunk = data.toString().trim();
|
|
201
258
|
stderr += chunk + '\n';
|
|
202
259
|
logger.warn(`Claude Code stderr: ${chunk.slice(0, 300)}`);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
onOutput(`⚠️ Claude Code: ${chunk.slice(0, 400)}`).catch(() => {});
|
|
260
|
+
if (smartOutput && chunk) {
|
|
261
|
+
smartOutput(`⚠️ ${chunk.slice(0, 300)}`).catch(() => {});
|
|
206
262
|
}
|
|
207
263
|
});
|
|
208
264
|
|
|
209
265
|
const timer = setTimeout(() => {
|
|
210
266
|
child.kill('SIGTERM');
|
|
211
|
-
if (
|
|
267
|
+
if (smartOutput) smartOutput(`⏰ Claude Code timed out after ${this.timeout / 1000}s`).catch(() => {});
|
|
212
268
|
reject(new Error(`Claude Code timed out after ${this.timeout / 1000}s`));
|
|
213
269
|
}, this.timeout);
|
|
214
270
|
|
|
215
|
-
child.on('close', (code) => {
|
|
271
|
+
child.on('close', async (code) => {
|
|
216
272
|
clearTimeout(timer);
|
|
217
273
|
|
|
218
274
|
if (buffer.trim()) {
|
|
@@ -223,7 +279,22 @@ export class ClaudeCodeSpawner {
|
|
|
223
279
|
resultText = event.result || resultText;
|
|
224
280
|
}
|
|
225
281
|
} catch {}
|
|
226
|
-
processEvent(buffer.trim(),
|
|
282
|
+
processEvent(buffer.trim(), smartOutput, logger);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Flush any pending status edits
|
|
286
|
+
if (flushTimer) {
|
|
287
|
+
clearTimeout(flushTimer);
|
|
288
|
+
flushTimer = null;
|
|
289
|
+
}
|
|
290
|
+
await flushStatus();
|
|
291
|
+
|
|
292
|
+
// Final status message update — show done/failed state
|
|
293
|
+
if (statusMsgId && onOutput) {
|
|
294
|
+
const finalState = code === 0 ? 'done' : 'error';
|
|
295
|
+
try {
|
|
296
|
+
await onOutput(buildStatusText(finalState), { editMessageId: statusMsgId });
|
|
297
|
+
} catch {}
|
|
227
298
|
}
|
|
228
299
|
|
|
229
300
|
logger.info(`Claude Code exited with code ${code} | stdout: ${fullOutput.length} chars | stderr: ${stderr.length} chars`);
|
|
@@ -231,7 +302,6 @@ export class ClaudeCodeSpawner {
|
|
|
231
302
|
if (code !== 0) {
|
|
232
303
|
const errMsg = stderr.trim() || fullOutput.trim() || `exited with code ${code}`;
|
|
233
304
|
logger.error(`Claude Code failed: ${errMsg.slice(0, 500)}`);
|
|
234
|
-
if (onOutput) onOutput(`❌ Claude Code failed (exit ${code}):\n\`\`\`\n${errMsg.slice(0, 400)}\n\`\`\``).catch(() => {});
|
|
235
305
|
reject(new Error(`Claude Code exited with code ${code}: ${errMsg.slice(0, 500)}`));
|
|
236
306
|
} else {
|
|
237
307
|
resolve({
|
package/src/prompts/system.js
CHANGED
|
@@ -17,8 +17,18 @@ IMPORTANT: You MUST NOT write code yourself using read_file/write_file. ALWAYS d
|
|
|
17
17
|
4. Use GitHub tools to create the PR
|
|
18
18
|
5. Report back with the PR link
|
|
19
19
|
|
|
20
|
+
## Web Browsing Tasks (researching, scraping, reading documentation, taking screenshots)
|
|
21
|
+
- Use browse_website to read and summarize web pages
|
|
22
|
+
- Use screenshot_website to capture visual snapshots of pages
|
|
23
|
+
- Use extract_content to pull specific data from pages using CSS selectors
|
|
24
|
+
- Use interact_with_page for pages that need clicking, typing, or scrolling to reveal content
|
|
25
|
+
- When a user sends /browse <url>, use browse_website on that URL
|
|
26
|
+
- When a user sends /screenshot <url>, use screenshot_website on that URL
|
|
27
|
+
- When a user sends /extract <url> <selector>, use extract_content with that URL and selector
|
|
28
|
+
|
|
20
29
|
You are the orchestrator. Claude Code is the coder. Never use read_file + write_file to modify source code — that's Claude Code's job. You handle git, GitHub, and infrastructure. Claude Code handles all code changes.
|
|
21
30
|
|
|
31
|
+
|
|
22
32
|
## Non-Coding Tasks (monitoring, deploying, restarting services, checking status)
|
|
23
33
|
- Use OS, Docker, process, network, and monitoring tools directly
|
|
24
34
|
- No need to spawn Claude Code for these
|
package/src/security/confirm.js
CHANGED
|
@@ -6,6 +6,7 @@ const DANGEROUS_PATTERNS = [
|
|
|
6
6
|
{ tool: 'github_create_repo', pattern: null, label: 'create a GitHub repository' },
|
|
7
7
|
{ tool: 'docker_compose', param: 'action', value: 'down', label: 'take down containers' },
|
|
8
8
|
{ tool: 'git_push', param: 'force', value: true, label: 'force push' },
|
|
9
|
+
{ tool: 'interact_with_page', pattern: null, label: 'interact with a webpage (click, type, execute scripts)' },
|
|
9
10
|
];
|
|
10
11
|
|
|
11
12
|
export function requiresConfirmation(toolName, params, config) {
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const NAVIGATION_TIMEOUT = 30000;
|
|
9
|
+
const MAX_CONTENT_LENGTH = 15000;
|
|
10
|
+
const MAX_SCREENSHOT_WIDTH = 1920;
|
|
11
|
+
const MAX_SCREENSHOT_HEIGHT = 1080;
|
|
12
|
+
const SCREENSHOTS_DIR = join(homedir(), '.kernelbot', 'screenshots');
|
|
13
|
+
|
|
14
|
+
// Blocklist to prevent abuse — internal/private network ranges and sensitive targets
|
|
15
|
+
const BLOCKED_URL_PATTERNS = [
|
|
16
|
+
/^https?:\/\/localhost/i,
|
|
17
|
+
/^https?:\/\/127\./,
|
|
18
|
+
/^https?:\/\/0\./,
|
|
19
|
+
/^https?:\/\/10\./,
|
|
20
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
21
|
+
/^https?:\/\/192\.168\./,
|
|
22
|
+
/^https?:\/\/\[::1\]/,
|
|
23
|
+
/^https?:\/\/169\.254\./,
|
|
24
|
+
/^file:/i,
|
|
25
|
+
/^ftp:/i,
|
|
26
|
+
/^data:/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function validateUrl(url) {
|
|
32
|
+
if (!url || typeof url !== 'string') {
|
|
33
|
+
return { valid: false, error: 'URL is required' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Block non-http protocols before auto-prepending https
|
|
37
|
+
for (const pattern of BLOCKED_URL_PATTERNS) {
|
|
38
|
+
if (pattern.test(url)) {
|
|
39
|
+
return { valid: false, error: 'Access to internal/private network addresses or non-HTTP protocols is blocked' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add https:// if no protocol specified
|
|
44
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
45
|
+
url = 'https://' + url;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
new URL(url);
|
|
50
|
+
} catch {
|
|
51
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check again after normalization (e.g., localhost without protocol)
|
|
55
|
+
for (const pattern of BLOCKED_URL_PATTERNS) {
|
|
56
|
+
if (pattern.test(url)) {
|
|
57
|
+
return { valid: false, error: 'Access to internal/private network addresses is blocked' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { valid: true, url };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function truncate(text, maxLength = MAX_CONTENT_LENGTH) {
|
|
65
|
+
if (!text || text.length <= maxLength) return text;
|
|
66
|
+
return text.slice(0, maxLength) + `\n\n... [truncated, ${text.length - maxLength} chars omitted]`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ensureScreenshotsDir() {
|
|
70
|
+
await mkdir(SCREENSHOTS_DIR, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function withBrowser(fn) {
|
|
74
|
+
let browser;
|
|
75
|
+
try {
|
|
76
|
+
browser = await puppeteer.launch({
|
|
77
|
+
headless: true,
|
|
78
|
+
args: [
|
|
79
|
+
'--no-sandbox',
|
|
80
|
+
'--disable-setuid-sandbox',
|
|
81
|
+
'--disable-dev-shm-usage',
|
|
82
|
+
'--disable-gpu',
|
|
83
|
+
'--disable-extensions',
|
|
84
|
+
'--disable-background-networking',
|
|
85
|
+
'--disable-default-apps',
|
|
86
|
+
'--disable-sync',
|
|
87
|
+
'--no-first-run',
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
return await fn(browser);
|
|
91
|
+
} finally {
|
|
92
|
+
if (browser) {
|
|
93
|
+
await browser.close().catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function navigateTo(page, url, waitUntil = 'networkidle2') {
|
|
99
|
+
await page.setUserAgent(
|
|
100
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
101
|
+
);
|
|
102
|
+
await page.setViewport({ width: MAX_SCREENSHOT_WIDTH, height: MAX_SCREENSHOT_HEIGHT });
|
|
103
|
+
await page.goto(url, {
|
|
104
|
+
waitUntil,
|
|
105
|
+
timeout: NAVIGATION_TIMEOUT,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Tool Definitions ─────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export const definitions = [
|
|
112
|
+
{
|
|
113
|
+
name: 'browse_website',
|
|
114
|
+
description:
|
|
115
|
+
'Navigate to a website URL and extract its content including title, headings, text, links, and metadata. Returns a structured summary of the page. Handles JavaScript-rendered pages.',
|
|
116
|
+
input_schema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
url: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'The URL to browse (e.g., "https://example.com" or "example.com")',
|
|
122
|
+
},
|
|
123
|
+
wait_for_selector: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Optional CSS selector to wait for before extracting content (useful for JS-heavy pages)',
|
|
126
|
+
},
|
|
127
|
+
include_links: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Include links found on the page (default: false)',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ['url'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'screenshot_website',
|
|
137
|
+
description:
|
|
138
|
+
'Take a screenshot of a website and save it to disk. Returns the file path to the screenshot image. Supports full-page and viewport-only screenshots.',
|
|
139
|
+
input_schema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
url: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'The URL to screenshot',
|
|
145
|
+
},
|
|
146
|
+
full_page: {
|
|
147
|
+
type: 'boolean',
|
|
148
|
+
description: 'Capture the full scrollable page instead of just the viewport (default: false)',
|
|
149
|
+
},
|
|
150
|
+
selector: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Optional CSS selector to screenshot a specific element instead of the full page',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
required: ['url'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'extract_content',
|
|
160
|
+
description:
|
|
161
|
+
'Extract specific content from a webpage using CSS selectors. Returns the text or HTML content of matched elements. Useful for scraping structured data.',
|
|
162
|
+
input_schema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
url: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The URL to extract content from',
|
|
168
|
+
},
|
|
169
|
+
selector: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'CSS selector to match elements (e.g., "h1", ".article-body", "#main-content")',
|
|
172
|
+
},
|
|
173
|
+
attribute: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Extract a specific attribute instead of text content (e.g., "href", "src")',
|
|
176
|
+
},
|
|
177
|
+
include_html: {
|
|
178
|
+
type: 'boolean',
|
|
179
|
+
description: 'Include raw HTML of matched elements (default: false, returns text only)',
|
|
180
|
+
},
|
|
181
|
+
limit: {
|
|
182
|
+
type: 'number',
|
|
183
|
+
description: 'Maximum number of elements to return (default: 20)',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ['url', 'selector'],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'interact_with_page',
|
|
191
|
+
description:
|
|
192
|
+
'Interact with a webpage by clicking elements, typing into inputs, scrolling, or executing JavaScript. Returns the page state after interaction.',
|
|
193
|
+
input_schema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
url: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
description: 'The URL to interact with',
|
|
199
|
+
},
|
|
200
|
+
actions: {
|
|
201
|
+
type: 'array',
|
|
202
|
+
description:
|
|
203
|
+
'List of actions to perform in sequence. Each action is an object with a "type" field.',
|
|
204
|
+
items: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
type: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
enum: ['click', 'type', 'scroll', 'wait', 'evaluate'],
|
|
210
|
+
description: 'Action type',
|
|
211
|
+
},
|
|
212
|
+
selector: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'CSS selector for the target element (for click and type actions)',
|
|
215
|
+
},
|
|
216
|
+
text: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: 'Text to type (for type action)',
|
|
219
|
+
},
|
|
220
|
+
direction: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
enum: ['down', 'up'],
|
|
223
|
+
description: 'Scroll direction (for scroll action, default: down)',
|
|
224
|
+
},
|
|
225
|
+
pixels: {
|
|
226
|
+
type: 'number',
|
|
227
|
+
description: 'Number of pixels to scroll (default: 500)',
|
|
228
|
+
},
|
|
229
|
+
milliseconds: {
|
|
230
|
+
type: 'number',
|
|
231
|
+
description: 'Time to wait in ms (for wait action, default: 1000)',
|
|
232
|
+
},
|
|
233
|
+
script: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'JavaScript to execute in the page context (for evaluate action). Must be a single expression or IIFE.',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
required: ['type'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
extract_after: {
|
|
242
|
+
type: 'boolean',
|
|
243
|
+
description: 'Extract page content after performing actions (default: true)',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
required: ['url', 'actions'],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
async function handleBrowse(params) {
|
|
254
|
+
const validation = validateUrl(params.url);
|
|
255
|
+
if (!validation.valid) return { error: validation.error };
|
|
256
|
+
|
|
257
|
+
const url = validation.url;
|
|
258
|
+
|
|
259
|
+
return withBrowser(async (browser) => {
|
|
260
|
+
const page = await browser.newPage();
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await navigateTo(page, url);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err.message.includes('net::ERR_NAME_NOT_RESOLVED')) {
|
|
266
|
+
return { error: `Could not resolve hostname for: ${url}` };
|
|
267
|
+
}
|
|
268
|
+
if (err.message.includes('Timeout')) {
|
|
269
|
+
return { error: `Page load timed out after ${NAVIGATION_TIMEOUT / 1000}s: ${url}` };
|
|
270
|
+
}
|
|
271
|
+
return { error: `Navigation failed: ${err.message}` };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Wait for optional selector
|
|
275
|
+
if (params.wait_for_selector) {
|
|
276
|
+
try {
|
|
277
|
+
await page.waitForSelector(params.wait_for_selector, { timeout: 10000 });
|
|
278
|
+
} catch {
|
|
279
|
+
// Continue even if selector not found
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const content = await page.evaluate((includeLinks) => {
|
|
284
|
+
const title = document.title || '';
|
|
285
|
+
const metaDesc = document.querySelector('meta[name="description"]')?.content || '';
|
|
286
|
+
const canonicalUrl = document.querySelector('link[rel="canonical"]')?.href || window.location.href;
|
|
287
|
+
|
|
288
|
+
// Extract headings
|
|
289
|
+
const headings = [];
|
|
290
|
+
for (const tag of ['h1', 'h2', 'h3']) {
|
|
291
|
+
document.querySelectorAll(tag).forEach((el) => {
|
|
292
|
+
const text = el.textContent.trim();
|
|
293
|
+
if (text) headings.push({ level: tag, text });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract main text content
|
|
298
|
+
// Prefer common article/content containers
|
|
299
|
+
const contentSelectors = [
|
|
300
|
+
'article', 'main', '[role="main"]',
|
|
301
|
+
'.content', '.article', '.post',
|
|
302
|
+
'#content', '#main', '#article',
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
let mainText = '';
|
|
306
|
+
for (const sel of contentSelectors) {
|
|
307
|
+
const el = document.querySelector(sel);
|
|
308
|
+
if (el) {
|
|
309
|
+
mainText = el.innerText.trim();
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Fall back to body text if no content container found
|
|
315
|
+
if (!mainText) {
|
|
316
|
+
// Remove script, style, nav, footer, header noise
|
|
317
|
+
const clone = document.body.cloneNode(true);
|
|
318
|
+
for (const el of clone.querySelectorAll('script, style, nav, footer, header, aside, [role="navigation"]')) {
|
|
319
|
+
el.remove();
|
|
320
|
+
}
|
|
321
|
+
mainText = clone.innerText.trim();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Extract links if requested
|
|
325
|
+
let links = [];
|
|
326
|
+
if (includeLinks) {
|
|
327
|
+
document.querySelectorAll('a[href]').forEach((a) => {
|
|
328
|
+
const text = a.textContent.trim();
|
|
329
|
+
const href = a.href;
|
|
330
|
+
if (text && href && !href.startsWith('javascript:')) {
|
|
331
|
+
links.push({ text: text.slice(0, 100), href });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
links = links.slice(0, 50);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { title, metaDesc, canonicalUrl, headings, mainText, links };
|
|
338
|
+
}, params.include_links || false);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
url: page.url(),
|
|
343
|
+
title: content.title,
|
|
344
|
+
meta_description: content.metaDesc,
|
|
345
|
+
canonical_url: content.canonicalUrl,
|
|
346
|
+
headings: content.headings.slice(0, 30),
|
|
347
|
+
content: truncate(content.mainText),
|
|
348
|
+
links: content.links || [],
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function handleScreenshot(params) {
|
|
354
|
+
const validation = validateUrl(params.url);
|
|
355
|
+
if (!validation.valid) return { error: validation.error };
|
|
356
|
+
|
|
357
|
+
const url = validation.url;
|
|
358
|
+
await ensureScreenshotsDir();
|
|
359
|
+
|
|
360
|
+
return withBrowser(async (browser) => {
|
|
361
|
+
const page = await browser.newPage();
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
await navigateTo(page, url);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err.message.includes('net::ERR_NAME_NOT_RESOLVED')) {
|
|
367
|
+
return { error: `Could not resolve hostname for: ${url}` };
|
|
368
|
+
}
|
|
369
|
+
if (err.message.includes('Timeout')) {
|
|
370
|
+
return { error: `Page load timed out after ${NAVIGATION_TIMEOUT / 1000}s: ${url}` };
|
|
371
|
+
}
|
|
372
|
+
return { error: `Navigation failed: ${err.message}` };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const timestamp = Date.now();
|
|
376
|
+
const safeName = new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_');
|
|
377
|
+
const filename = `${safeName}_${timestamp}.png`;
|
|
378
|
+
const filepath = join(SCREENSHOTS_DIR, filename);
|
|
379
|
+
|
|
380
|
+
const screenshotOptions = {
|
|
381
|
+
path: filepath,
|
|
382
|
+
type: 'png',
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (params.selector) {
|
|
386
|
+
try {
|
|
387
|
+
const element = await page.$(params.selector);
|
|
388
|
+
if (!element) {
|
|
389
|
+
return { error: `Element not found for selector: ${params.selector}` };
|
|
390
|
+
}
|
|
391
|
+
await element.screenshot(screenshotOptions);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
return { error: `Failed to screenshot element: ${err.message}` };
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
screenshotOptions.fullPage = params.full_page || false;
|
|
397
|
+
await page.screenshot(screenshotOptions);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
url: page.url(),
|
|
403
|
+
title: await page.title(),
|
|
404
|
+
screenshot_path: filepath,
|
|
405
|
+
filename,
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function handleExtract(params) {
|
|
411
|
+
const validation = validateUrl(params.url);
|
|
412
|
+
if (!validation.valid) return { error: validation.error };
|
|
413
|
+
|
|
414
|
+
const url = validation.url;
|
|
415
|
+
const limit = Math.min(params.limit || 20, 100);
|
|
416
|
+
|
|
417
|
+
return withBrowser(async (browser) => {
|
|
418
|
+
const page = await browser.newPage();
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
await navigateTo(page, url);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (err.message.includes('net::ERR_NAME_NOT_RESOLVED')) {
|
|
424
|
+
return { error: `Could not resolve hostname for: ${url}` };
|
|
425
|
+
}
|
|
426
|
+
if (err.message.includes('Timeout')) {
|
|
427
|
+
return { error: `Page load timed out after ${NAVIGATION_TIMEOUT / 1000}s: ${url}` };
|
|
428
|
+
}
|
|
429
|
+
return { error: `Navigation failed: ${err.message}` };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const results = await page.evaluate(
|
|
433
|
+
(selector, attribute, includeHtml, maxItems) => {
|
|
434
|
+
const elements = document.querySelectorAll(selector);
|
|
435
|
+
if (elements.length === 0) return { found: 0, items: [] };
|
|
436
|
+
|
|
437
|
+
const items = [];
|
|
438
|
+
for (let i = 0; i < Math.min(elements.length, maxItems); i++) {
|
|
439
|
+
const el = elements[i];
|
|
440
|
+
const item = {};
|
|
441
|
+
|
|
442
|
+
if (attribute) {
|
|
443
|
+
item.value = el.getAttribute(attribute) || null;
|
|
444
|
+
} else {
|
|
445
|
+
item.text = el.innerText?.trim() || el.textContent?.trim() || '';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (includeHtml) {
|
|
449
|
+
item.html = el.outerHTML;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
item.tag = el.tagName.toLowerCase();
|
|
453
|
+
items.push(item);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { found: elements.length, items };
|
|
457
|
+
},
|
|
458
|
+
params.selector,
|
|
459
|
+
params.attribute || null,
|
|
460
|
+
params.include_html || false,
|
|
461
|
+
limit
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (results.found === 0) {
|
|
465
|
+
return {
|
|
466
|
+
success: true,
|
|
467
|
+
url: page.url(),
|
|
468
|
+
selector: params.selector,
|
|
469
|
+
found: 0,
|
|
470
|
+
items: [],
|
|
471
|
+
message: `No elements found matching selector: ${params.selector}`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Truncate individual items to prevent massive responses
|
|
476
|
+
for (const item of results.items) {
|
|
477
|
+
if (item.text) item.text = truncate(item.text, 2000);
|
|
478
|
+
if (item.html) item.html = truncate(item.html, 3000);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
success: true,
|
|
483
|
+
url: page.url(),
|
|
484
|
+
selector: params.selector,
|
|
485
|
+
found: results.found,
|
|
486
|
+
returned: results.items.length,
|
|
487
|
+
items: results.items,
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function handleInteract(params) {
|
|
493
|
+
const validation = validateUrl(params.url);
|
|
494
|
+
if (!validation.valid) return { error: validation.error };
|
|
495
|
+
|
|
496
|
+
const url = validation.url;
|
|
497
|
+
|
|
498
|
+
if (!params.actions || params.actions.length === 0) {
|
|
499
|
+
return { error: 'At least one action is required' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (params.actions.length > 10) {
|
|
503
|
+
return { error: 'Maximum 10 actions per request' };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Block dangerous evaluate scripts
|
|
507
|
+
for (const action of params.actions) {
|
|
508
|
+
if (action.type === 'evaluate' && action.script) {
|
|
509
|
+
const blocked = /fetch\s*\(|XMLHttpRequest|window\.location\s*=|document\.cookie|localStorage|sessionStorage/i;
|
|
510
|
+
if (blocked.test(action.script)) {
|
|
511
|
+
return { error: 'Script contains blocked patterns (network requests, cookie access, storage access, or redirects)' };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return withBrowser(async (browser) => {
|
|
517
|
+
const page = await browser.newPage();
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
await navigateTo(page, url);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
if (err.message.includes('net::ERR_NAME_NOT_RESOLVED')) {
|
|
523
|
+
return { error: `Could not resolve hostname for: ${url}` };
|
|
524
|
+
}
|
|
525
|
+
if (err.message.includes('Timeout')) {
|
|
526
|
+
return { error: `Page load timed out after ${NAVIGATION_TIMEOUT / 1000}s: ${url}` };
|
|
527
|
+
}
|
|
528
|
+
return { error: `Navigation failed: ${err.message}` };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const actionResults = [];
|
|
532
|
+
|
|
533
|
+
for (const action of params.actions) {
|
|
534
|
+
try {
|
|
535
|
+
switch (action.type) {
|
|
536
|
+
case 'click': {
|
|
537
|
+
if (!action.selector) {
|
|
538
|
+
actionResults.push({ action: 'click', error: 'selector is required' });
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
await page.waitForSelector(action.selector, { timeout: 5000 });
|
|
542
|
+
await page.click(action.selector);
|
|
543
|
+
// Brief wait for any navigation or rendering
|
|
544
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
545
|
+
actionResults.push({ action: 'click', selector: action.selector, success: true });
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
case 'type': {
|
|
550
|
+
if (!action.selector || !action.text) {
|
|
551
|
+
actionResults.push({ action: 'type', error: 'selector and text are required' });
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
await page.waitForSelector(action.selector, { timeout: 5000 });
|
|
555
|
+
await page.type(action.selector, action.text);
|
|
556
|
+
actionResults.push({ action: 'type', selector: action.selector, success: true });
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
case 'scroll': {
|
|
561
|
+
const direction = action.direction || 'down';
|
|
562
|
+
const pixels = Math.min(action.pixels || 500, 5000);
|
|
563
|
+
const scrollAmount = direction === 'up' ? -pixels : pixels;
|
|
564
|
+
await page.evaluate((amount) => window.scrollBy(0, amount), scrollAmount);
|
|
565
|
+
actionResults.push({ action: 'scroll', direction, pixels, success: true });
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
case 'wait': {
|
|
570
|
+
const ms = Math.min(action.milliseconds || 1000, 10000);
|
|
571
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
572
|
+
actionResults.push({ action: 'wait', milliseconds: ms, success: true });
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
case 'evaluate': {
|
|
577
|
+
if (!action.script) {
|
|
578
|
+
actionResults.push({ action: 'evaluate', error: 'script is required' });
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
const result = await page.evaluate(action.script);
|
|
582
|
+
actionResults.push({ action: 'evaluate', success: true, result: String(result).slice(0, 2000) });
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
default:
|
|
587
|
+
actionResults.push({ action: action.type, error: `Unknown action type: ${action.type}` });
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
actionResults.push({ action: action.type, error: err.message });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const response = {
|
|
595
|
+
success: true,
|
|
596
|
+
url: page.url(),
|
|
597
|
+
title: await page.title(),
|
|
598
|
+
actions: actionResults,
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// Extract content after interactions unless disabled
|
|
602
|
+
if (params.extract_after !== false) {
|
|
603
|
+
const text = await page.evaluate(() => {
|
|
604
|
+
const clone = document.body.cloneNode(true);
|
|
605
|
+
for (const el of clone.querySelectorAll('script, style, nav, footer, header')) {
|
|
606
|
+
el.remove();
|
|
607
|
+
}
|
|
608
|
+
return clone.innerText.trim();
|
|
609
|
+
});
|
|
610
|
+
response.content = truncate(text);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return response;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
618
|
+
|
|
619
|
+
export const handlers = {
|
|
620
|
+
browse_website: handleBrowse,
|
|
621
|
+
screenshot_website: handleScreenshot,
|
|
622
|
+
extract_content: handleExtract,
|
|
623
|
+
interact_with_page: handleInteract,
|
|
624
|
+
};
|
package/src/tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { definitions as networkDefinitions, handlers as networkHandlers } from '
|
|
|
6
6
|
import { definitions as gitDefinitions, handlers as gitHandlers } from './git.js';
|
|
7
7
|
import { definitions as githubDefinitions, handlers as githubHandlers } from './github.js';
|
|
8
8
|
import { definitions as codingDefinitions, handlers as codingHandlers } from './coding.js';
|
|
9
|
+
import { definitions as browserDefinitions, handlers as browserHandlers } from './browser.js';
|
|
9
10
|
import { logToolCall } from '../security/audit.js';
|
|
10
11
|
import { requiresConfirmation } from '../security/confirm.js';
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ export const toolDefinitions = [
|
|
|
18
19
|
...gitDefinitions,
|
|
19
20
|
...githubDefinitions,
|
|
20
21
|
...codingDefinitions,
|
|
22
|
+
...browserDefinitions,
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
const handlerMap = {
|
|
@@ -29,6 +31,7 @@ const handlerMap = {
|
|
|
29
31
|
...gitHandlers,
|
|
30
32
|
...githubHandlers,
|
|
31
33
|
...codingHandlers,
|
|
34
|
+
...browserHandlers,
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
export function checkConfirmation(name, params, config) {
|
package/src/utils/display.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import boxen from 'boxen';
|
|
7
|
+
import gradient from 'gradient-string';
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
@@ -25,8 +26,19 @@ const LOGO = `
|
|
|
25
26
|
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═╝
|
|
26
27
|
`;
|
|
27
28
|
|
|
29
|
+
// Create a vibrant rainbow gradient
|
|
30
|
+
const rainbowGradient = gradient([
|
|
31
|
+
'#FF0080', // Hot Pink
|
|
32
|
+
'#FF8C00', // Dark Orange
|
|
33
|
+
'#FFD700', // Gold
|
|
34
|
+
'#00FF00', // Lime Green
|
|
35
|
+
'#00CED1', // Dark Turquoise
|
|
36
|
+
'#1E90FF', // Dodger Blue
|
|
37
|
+
'#9370DB' // Medium Purple
|
|
38
|
+
]);
|
|
39
|
+
|
|
28
40
|
export function showLogo() {
|
|
29
|
-
console.log(
|
|
41
|
+
console.log(rainbowGradient.multiline(LOGO));
|
|
30
42
|
console.log(chalk.dim(` AI Engineering Agent — v${getVersion()}\n`));
|
|
31
43
|
console.log(
|
|
32
44
|
boxen(
|
|
@@ -93,4 +105,4 @@ export function showError(msg) {
|
|
|
93
105
|
|
|
94
106
|
export function createSpinner(text) {
|
|
95
107
|
return ora({ text, color: 'cyan' });
|
|
96
|
-
}
|
|
108
|
+
}
|