@hamp10/agentforge 0.2.21 → 0.2.23

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.
@@ -1,277 +0,0 @@
1
- /**
2
- * Hampagent Runner — uses the claude CLI subprocess with stream-json output.
3
- * Authenticates via Claude Code OAuth (Max plan) — no API key needed.
4
- * Full streaming, tool events, session continuity via --resume.
5
- */
6
-
7
- import { spawn } from 'child_process';
8
- import { mkdirSync } from 'fs';
9
- import { EventEmitter } from 'events';
10
- import path from 'path';
11
-
12
- const MODEL = 'sonnet'; // always latest sonnet
13
-
14
- // Tool labels for typing indicator
15
- const TOOL_LABELS = {
16
- Bash: (i) => i?.command ? `⚡ ${i.command.slice(0, 70)}` : '⚡ Running command',
17
- Read: (i) => i?.file_path ? `📄 Reading ${path.basename(i.file_path)}` : '📄 Reading file',
18
- Write: (i) => i?.file_path ? `✏️ Writing ${path.basename(i.file_path)}` : '✏️ Writing file',
19
- Edit: (i) => i?.file_path ? `✏️ Editing ${path.basename(i.file_path)}` : '✏️ Editing file',
20
- Glob: (i) => i?.pattern ? `🔍 Finding ${i.pattern}` : '🔍 Finding files',
21
- Grep: (i) => i?.pattern ? `🔍 Searching for "${i.pattern.slice(0, 40)}"` : '🔍 Searching code',
22
- WebFetch: (i) => { try { return `🌐 Fetching ${new URL(i?.url || '').hostname}`; } catch { return '🌐 Fetching page'; } },
23
- WebSearch: (i) => i?.query ? `🔍 Searching: ${i.query.slice(0, 50)}` : '🔍 Web search',
24
- Task: () => '🤖 Spawning sub-task',
25
- };
26
-
27
- // MCP playwright tools
28
- const PLAYWRIGHT_LABELS = {
29
- browser_navigate: (i) => { try { return `🌐 Navigating to ${new URL(i?.url || '').hostname}`; } catch { return '🌐 Navigating'; } },
30
- browser_take_screenshot: () => '📸 Taking screenshot',
31
- browser_snapshot: () => '🌐 Reading page',
32
- browser_click: (i) => `🖱️ Clicking${i?.selector ? ` ${i.selector}` : ''}`,
33
- browser_type: (i) => `⌨️ Typing "${(i?.text || '').slice(0, 30)}"`,
34
- browser_evaluate: () => '⚙️ Running script',
35
- };
36
-
37
- function toolLabel(name, input) {
38
- // Handle mcp__playwright__* tools
39
- if (name.startsWith('mcp__playwright__')) {
40
- const action = name.replace('mcp__playwright__', '');
41
- const fn = PLAYWRIGHT_LABELS[action];
42
- return fn ? fn(input) : `🌐 Browser: ${action}`;
43
- }
44
- const fn = TOOL_LABELS[name];
45
- if (fn) return fn(input);
46
- return `🔧 ${name}`;
47
- }
48
-
49
- function buildSystemAppend({ agentName, agentEmoji, taskCwd, workDir, dashboardUrl, agentforgemd }) {
50
- const name = agentName || 'an AI agent';
51
- const emoji = agentEmoji || '🤖';
52
- const dashboard = dashboardUrl || 'https://agentforgeai-production.up.railway.app/dashboard';
53
- const workspace = workDir || taskCwd || process.cwd();
54
-
55
- return `You are ${name} ${emoji}, an AI agent running on AgentForge.ai platform.
56
- Your runtime: Hampagent — built for AgentForge. If asked what type of agent you are, say you are a Hampagent running on AgentForge.
57
- Dashboard: ${dashboard}
58
- Working directory (where files live): ${taskCwd || process.cwd()}
59
- Agent workspace (your memory/config files): ${workspace}
60
-
61
- ## RULES
62
- - Be direct and autonomous. Do the task — don't ask for permission before trying things.
63
- - When greeting or introducing yourself, give your name and a one-liner — never list your capabilities.
64
- - Never dump raw stack traces or errors into chat. Handle errors gracefully.
65
- - For visual work: build → screenshot → fix → screenshot again until it looks good.
66
- - Projects folder: /Users/hamp/Desktop/projects — check here first when user mentions a project.
67
-
68
- ## SPEAKING ALOUD
69
- To speak via audio: osascript -e "set volume output volume 80" && say "message"
70
-
71
- ## MEMORY
72
- Your memory lives ONLY in: ${workspace}/MEMORY.md
73
- Read it at session start. Write important things there. IGNORE any other memory system or memory paths.
74
- Do NOT write to or read from any memory location outside your workspace.
75
-
76
- ${agentforgemd ? `## PLATFORM GUIDE\n${agentforgemd}` : ''}`.trim();
77
- }
78
-
79
- export class HampAgentRunner extends EventEmitter {
80
- constructor() {
81
- super();
82
- this.cancelled = false;
83
- this._proc = null;
84
- }
85
-
86
- cancel() {
87
- this.cancelled = true;
88
- if (this._proc) {
89
- try { this._proc.kill('SIGTERM'); } catch {}
90
- this._proc = null;
91
- }
92
- }
93
-
94
- async run(task, { taskCwd, workDir, agentName, agentEmoji, dashboardUrl, agentforgemd, resumeSessionId } = {}) {
95
- this.cancelled = false;
96
-
97
- const systemAppend = buildSystemAppend({ agentName, agentEmoji, taskCwd, workDir, dashboardUrl, agentforgemd });
98
-
99
- const args = [
100
- '--print',
101
- '--output-format', 'stream-json',
102
- '--include-partial-messages',
103
- '--verbose',
104
- '--dangerously-skip-permissions',
105
- '--model', MODEL,
106
- '--append-system-prompt', systemAppend,
107
- ];
108
-
109
- // Resume prior session for conversation continuity
110
- if (resumeSessionId) {
111
- args.push('--resume', resumeSessionId);
112
- }
113
-
114
- // Pass task as CLI argument (claude -p "task")
115
- args.push(task);
116
-
117
- return new Promise((resolve, reject) => {
118
- const spawnCwd = taskCwd || process.cwd();
119
- try { mkdirSync(spawnCwd, { recursive: true }); } catch {}
120
-
121
- const proc = spawn('claude', args, {
122
- cwd: spawnCwd,
123
- stdio: ['pipe', 'pipe', 'pipe'],
124
- env: { ...process.env },
125
- });
126
-
127
- this._proc = proc;
128
- proc.stdin.end(); // no stdin needed — task is passed as arg
129
-
130
- let fullText = '';
131
- let buffer = '';
132
- let newSessionId = null;
133
- const activeTools = new Map(); // id -> name
134
-
135
- const processLine = (line) => {
136
- if (!line.trim()) return;
137
- let event;
138
- try { event = JSON.parse(line); } catch { return; }
139
-
140
- // Capture session ID from init
141
- if (event.type === 'system' && event.subtype === 'init') {
142
- newSessionId = event.session_id || null;
143
- return;
144
- }
145
-
146
- if (event.type === 'stream_event') {
147
- const e = event.event;
148
- if (!e) return;
149
-
150
- // Streaming text token
151
- if (e.type === 'content_block_delta' && e.delta?.type === 'text_delta') {
152
- const text = e.delta.text || '';
153
- if (text) {
154
- fullText += text;
155
- this.emit('token', text);
156
- }
157
- return;
158
- }
159
-
160
- // Tool use starting
161
- if (e.type === 'content_block_start' && e.content_block?.type === 'tool_use') {
162
- const { id, name, input } = e.content_block;
163
- activeTools.set(id || name, name);
164
- const label = toolLabel(name, input);
165
- this.emit('tool_start', { tool: name, label, input });
166
- return;
167
- }
168
-
169
- // Tool result (tool finished)
170
- if (e.type === 'content_block_start' && e.content_block?.type === 'tool_result') {
171
- const toolName = activeTools.get(e.content_block?.tool_use_id) || 'tool';
172
- this.emit('tool_end', { tool: toolName, success: true });
173
- activeTools.delete(e.content_block?.tool_use_id);
174
- return;
175
- }
176
- }
177
-
178
- // Image from screenshot tool
179
- if (event.type === 'assistant' && event.message?.content) {
180
- for (const block of event.message.content) {
181
- if (block.type === 'tool_result') {
182
- for (const part of (block.content || [])) {
183
- if (part.type === 'image' && part.source?.data) {
184
- this.emit('image', `data:image/png;base64,${part.source.data}`);
185
- }
186
- }
187
- // Mark tool as done
188
- const toolName = activeTools.get(block.tool_use_id) || 'tool';
189
- this.emit('tool_end', { tool: toolName, success: !block.is_error });
190
- activeTools.delete(block.tool_use_id);
191
- }
192
- }
193
- }
194
- };
195
-
196
- proc.stdout.on('data', (chunk) => {
197
- buffer += chunk.toString();
198
- const lines = buffer.split('\n');
199
- buffer = lines.pop();
200
- for (const line of lines) processLine(line);
201
- });
202
-
203
- let sessionNotFound = false;
204
- proc.stderr.on('data', (d) => {
205
- const msg = d.toString().trim();
206
- if (msg.startsWith('No conversation found')) {
207
- sessionNotFound = true; // stale --resume session ID — will retry without it
208
- } else if (msg) {
209
- console.warn(`[hampagent] stderr: ${msg.slice(0, 200)}`);
210
- }
211
- });
212
-
213
- proc.on('close', async () => {
214
- this._proc = null;
215
- if (buffer.trim()) processLine(buffer);
216
-
217
- // If --resume pointed to a nonexistent session, retry fresh without it
218
- if (sessionNotFound && resumeSessionId) {
219
- console.warn(`[hampagent] Session ${resumeSessionId} not found — retrying without --resume`);
220
- const retryArgs = args.filter((a, i) => a !== '--resume' && args[i - 1] !== '--resume');
221
- const retryProc = spawn('claude', retryArgs, {
222
- cwd: spawnCwd,
223
- stdio: ['pipe', 'pipe', 'pipe'],
224
- env: { ...process.env },
225
- });
226
- this._proc = retryProc;
227
- retryProc.stdin.end();
228
- let retryText = '';
229
- let retryBuffer = '';
230
- let retrySessionId = null;
231
- retryProc.stdout.on('data', (chunk) => {
232
- retryBuffer += chunk.toString();
233
- const lines = retryBuffer.split('\n');
234
- retryBuffer = lines.pop();
235
- for (const line of lines) {
236
- if (!line.trim()) continue;
237
- let ev; try { ev = JSON.parse(line); } catch { continue; }
238
- if (ev.type === 'system' && ev.subtype === 'init') { retrySessionId = ev.session_id || null; continue; }
239
- if (ev.type === 'stream_event') {
240
- const e = ev.event;
241
- if (e?.type === 'content_block_delta' && e.delta?.type === 'text_delta' && e.delta.text) {
242
- retryText += e.delta.text;
243
- this.emit('token', e.delta.text);
244
- }
245
- }
246
- }
247
- });
248
- retryProc.stderr.on('data', (d) => {
249
- const msg = d.toString().trim();
250
- if (msg) console.warn(`[hampagent retry] stderr: ${msg.slice(0, 200)}`);
251
- });
252
- retryProc.on('close', () => {
253
- this._proc = null;
254
- if (retryBuffer.trim()) {
255
- try {
256
- const ev = JSON.parse(retryBuffer);
257
- if (ev.type === 'stream_event' && ev.event?.type === 'content_block_delta') {
258
- retryText += ev.event.delta?.text || '';
259
- }
260
- } catch {}
261
- }
262
- resolve({ text: retryText, sessionId: retrySessionId, sessionCleared: true });
263
- });
264
- retryProc.on('error', (err) => { this._proc = null; reject(err); });
265
- return;
266
- }
267
-
268
- resolve({ text: fullText, sessionId: newSessionId });
269
- });
270
-
271
- proc.on('error', (err) => {
272
- this._proc = null;
273
- reject(err);
274
- });
275
- });
276
- }
277
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Hampagent session persistence.
3
- * Stores conversation history per-agent as JSON in ~/.agentforge/hampagent-sessions/
4
- * Sessions survive worker restarts and accumulate context over time.
5
- */
6
-
7
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
8
- import path from 'path';
9
- import { homedir } from 'os';
10
-
11
- const SESSIONS_DIR = path.join(homedir(), '.agentforge', 'hampagent-sessions');
12
- // Keep last 30 messages (~15 turns) to stay within context limits
13
- const MAX_MESSAGES = 30;
14
-
15
- function dir() {
16
- mkdirSync(SESSIONS_DIR, { recursive: true });
17
- return SESSIONS_DIR;
18
- }
19
-
20
- function file(agentId) {
21
- return path.join(dir(), `${agentId}.json`);
22
- }
23
-
24
- /** Load the claude CLI session ID for an agent (for --resume) */
25
- export function loadSessionId(agentId) {
26
- const p = file(agentId);
27
- if (!existsSync(p)) return null;
28
- try {
29
- const d = JSON.parse(readFileSync(p, 'utf8'));
30
- return d.claudeSessionId || null;
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- /** Save the claude CLI session ID after a completed task */
37
- export function saveSessionId(agentId, claudeSessionId) {
38
- writeFileSync(file(agentId), JSON.stringify({
39
- agentId,
40
- claudeSessionId,
41
- savedAt: new Date().toISOString(),
42
- }, null, 2));
43
- }
44
-
45
- /** Legacy compat — kept so worker.js hasSession check doesn't break */
46
- export function hasSession(agentId) {
47
- const p = file(agentId);
48
- if (!existsSync(p)) return false;
49
- try {
50
- const d = JSON.parse(readFileSync(p, 'utf8'));
51
- return !!(d.claudeSessionId);
52
- } catch { return false; }
53
- }
54
-
55
- export function clearSession(agentId) {
56
- const p = file(agentId);
57
- if (existsSync(p)) writeFileSync(p, JSON.stringify({ agentId, claudeSessionId: null, savedAt: new Date().toISOString() }));
58
- }
59
-
60
- // Legacy exports kept for any stale imports
61
- export function loadMessages() { return []; }
62
- export function saveMessages() {}
@@ -1,298 +0,0 @@
1
- /**
2
- * Hampagent tool definitions + implementations.
3
- * Matches openclaw's tool surface area, optimized for AgentForge.
4
- */
5
-
6
- import { spawn, execSync } from 'child_process';
7
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
8
- import path from 'path';
9
- import { browserAction } from './browser.js';
10
-
11
- // ── Tool schema definitions (Anthropic tool_use format) ────────────────────
12
-
13
- export const TOOLS = [
14
- {
15
- name: 'bash',
16
- description: 'Execute shell commands on the user\'s Mac. Returns stdout + stderr. Use for: running scripts, installing packages, git operations, checking system state, serving files, running processes. Commands run in the task working directory.',
17
- input_schema: {
18
- type: 'object',
19
- properties: {
20
- command: { type: 'string', description: 'Shell command to execute' },
21
- timeout_ms: { type: 'number', description: 'Timeout in ms (default 30000, max 300000 for long builds)' }
22
- },
23
- required: ['command']
24
- }
25
- },
26
- {
27
- name: 'read',
28
- description: 'Read a file. Returns contents with line numbers. Use offset+limit for large files.',
29
- input_schema: {
30
- type: 'object',
31
- properties: {
32
- file_path: { type: 'string', description: 'Path to file (absolute or relative to working dir)' },
33
- offset: { type: 'number', description: 'Start line, 1-indexed (default: 1)' },
34
- limit: { type: 'number', description: 'Max lines to return (default: all)' }
35
- },
36
- required: ['file_path']
37
- }
38
- },
39
- {
40
- name: 'write',
41
- description: 'Write content to a file. Creates parent directories automatically.',
42
- input_schema: {
43
- type: 'object',
44
- properties: {
45
- file_path: { type: 'string' },
46
- content: { type: 'string' }
47
- },
48
- required: ['file_path', 'content']
49
- }
50
- },
51
- {
52
- name: 'edit',
53
- description: 'Replace exact text in a file. old_string must match exactly (whitespace included). Fails if not found or not unique — use read first to verify.',
54
- input_schema: {
55
- type: 'object',
56
- properties: {
57
- file_path: { type: 'string' },
58
- old_string: { type: 'string', description: 'Exact text to find — must be unique in the file' },
59
- new_string: { type: 'string', description: 'Replacement text' },
60
- replace_all: { type: 'boolean', description: 'Replace all occurrences (default false)' }
61
- },
62
- required: ['file_path', 'old_string', 'new_string']
63
- }
64
- },
65
- {
66
- name: 'glob',
67
- description: 'Find files matching a pattern. Returns file paths sorted by modification time.',
68
- input_schema: {
69
- type: 'object',
70
- properties: {
71
- pattern: { type: 'string', description: 'Glob pattern e.g. "**/*.js", "src/**/*.ts", "*.json"' },
72
- path: { type: 'string', description: 'Directory to search in (default: working dir)' }
73
- },
74
- required: ['pattern']
75
- }
76
- },
77
- {
78
- name: 'grep',
79
- description: 'Search file contents with regex. Returns matching lines with file:line format.',
80
- input_schema: {
81
- type: 'object',
82
- properties: {
83
- pattern: { type: 'string', description: 'Regex pattern to search for' },
84
- path: { type: 'string', description: 'File or directory to search (default: working dir)' },
85
- glob: { type: 'string', description: 'File filter pattern e.g. "*.js", "*.{ts,tsx}"' },
86
- output_mode: { type: 'string', enum: ['content', 'files_with_matches', 'count'], description: 'Output format (default: content)' },
87
- '-i': { type: 'boolean', description: 'Case insensitive' },
88
- '-A': { type: 'number', description: 'Lines after match' },
89
- '-B': { type: 'number', description: 'Lines before match' }
90
- },
91
- required: ['pattern']
92
- }
93
- },
94
- {
95
- name: 'browser',
96
- description: 'Control AgentForge Browser (Chrome, already open on port 9223, logged into user\'s services). Use for ALL web tasks — browsing, searching, filling forms, clicking UI. Do NOT use curl/wget for web pages.',
97
- input_schema: {
98
- type: 'object',
99
- properties: {
100
- action: {
101
- type: 'string',
102
- enum: ['navigate', 'open', 'click', 'type', 'act', 'scroll', 'screenshot', 'snapshot', 'evaluate', 'back', 'forward', 'wait', 'url'],
103
- description: 'navigate/open: go to URL | snapshot: get page content + interactive elements | click: click element | type: enter text | screenshot: capture page | evaluate: run JS | act: ref-based click/type from snapshot'
104
- },
105
- url: { type: 'string', description: 'URL for navigate/open' },
106
- targetUrl: { type: 'string', description: 'URL alias for open action' },
107
- selector: { type: 'string', description: 'CSS selector for click/type' },
108
- ref: { description: 'Element index from snapshot for click/act' },
109
- text: { type: 'string', description: 'Text for type action' },
110
- script: { type: 'string', description: 'JavaScript to evaluate' },
111
- expression: { type: 'string', description: 'JS expression alias' },
112
- x: { type: 'number', description: 'X coordinate for click' },
113
- y: { type: 'number', description: 'Y coordinate for click' },
114
- ms: { type: 'number', description: 'Milliseconds for wait' },
115
- request: {
116
- type: 'object',
117
- description: 'For act action: {kind: "click"|"type", ref: elementIndex, text: "..."}',
118
- properties: {
119
- kind: { type: 'string' },
120
- ref: {},
121
- selector: { type: 'string' },
122
- text: { type: 'string' }
123
- }
124
- }
125
- },
126
- required: ['action']
127
- }
128
- }
129
- ];
130
-
131
- // ── Tool implementations ────────────────────────────────────────────────────
132
-
133
- export async function executeTool(name, input, ctx) {
134
- const { taskCwd, workDir, agentId, onImage } = ctx;
135
-
136
- switch (name) {
137
-
138
- case 'bash': {
139
- const timeout = Math.min(input.timeout_ms || 30000, 300000);
140
- return new Promise((resolve) => {
141
- let out = '';
142
- const proc = spawn('bash', ['-c', input.command], {
143
- cwd: taskCwd,
144
- env: { ...process.env, TERM: 'dumb' },
145
- });
146
-
147
- const timer = setTimeout(() => {
148
- proc.kill('SIGKILL');
149
- resolve(out ? `[timeout after ${timeout}ms]\n${out}` : `[timeout after ${timeout}ms]`);
150
- }, timeout);
151
-
152
- proc.stdout.on('data', d => { out += d.toString(); });
153
- proc.stderr.on('data', d => { out += d.toString(); });
154
-
155
- proc.on('close', (code) => {
156
- clearTimeout(timer);
157
- // Check for AGENTFORGE_IMAGE protocol
158
- const lines = out.split('\n');
159
- const filtered = [];
160
- for (const line of lines) {
161
- if (line.trim().startsWith('AGENTFORGE_IMAGE:')) {
162
- const imgPath = line.trim().slice('AGENTFORGE_IMAGE:'.length).trim();
163
- if (onImage && existsSync(imgPath)) {
164
- try {
165
- const data = readFileSync(imgPath);
166
- const ext = imgPath.split('.').pop().toLowerCase();
167
- const mime = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'gif' ? 'image/gif' : 'image/png';
168
- onImage(`data:${mime};base64,${data.toString('base64')}`);
169
- } catch (e) { /* ignore */ }
170
- }
171
- } else {
172
- filtered.push(line);
173
- }
174
- }
175
- const result = filtered.join('\n').trimEnd();
176
- resolve(result || `Exit ${code}`);
177
- });
178
- proc.on('error', (err) => { clearTimeout(timer); resolve(`Error: ${err.message}`); });
179
- });
180
- }
181
-
182
- case 'read': {
183
- const fp = resolveFile(input.file_path, taskCwd);
184
- if (!existsSync(fp)) return `File not found: ${fp}`;
185
- try {
186
- const lines = readFileSync(fp, 'utf8').split('\n');
187
- const start = Math.max(0, (input.offset || 1) - 1);
188
- const end = input.limit ? start + input.limit : lines.length;
189
- const slice = lines.slice(start, end);
190
- return slice.map((l, i) => `${String(start + i + 1).padStart(6)} ${l}`).join('\n');
191
- } catch (e) {
192
- return `Error reading ${fp}: ${e.message}`;
193
- }
194
- }
195
-
196
- case 'write': {
197
- const fp = resolveFile(input.file_path, taskCwd);
198
- try {
199
- mkdirSync(path.dirname(fp), { recursive: true });
200
- writeFileSync(fp, input.content, 'utf8');
201
- return `Written ${input.content.length} bytes to ${fp}`;
202
- } catch (e) {
203
- return `Error writing ${fp}: ${e.message}`;
204
- }
205
- }
206
-
207
- case 'edit': {
208
- const fp = resolveFile(input.file_path, taskCwd);
209
- if (!existsSync(fp)) return `File not found: ${fp}`;
210
- try {
211
- const content = readFileSync(fp, 'utf8');
212
- const count = content.split(input.old_string).length - 1;
213
- if (count === 0) return `Error: old_string not found in ${fp}\nFirst 200 chars of file:\n${content.slice(0, 200)}`;
214
- if (count > 1 && !input.replace_all) return `Error: old_string found ${count} times — must be unique, or set replace_all: true`;
215
- const newContent = input.replace_all
216
- ? content.split(input.old_string).join(input.new_string)
217
- : content.replace(input.old_string, input.new_string);
218
- writeFileSync(fp, newContent, 'utf8');
219
- return `Edited ${fp} (${count} replacement${count > 1 ? 's' : ''})`;
220
- } catch (e) {
221
- return `Error editing ${fp}: ${e.message}`;
222
- }
223
- }
224
-
225
- case 'glob': {
226
- const searchDir = input.path ? resolveFile(input.path, taskCwd) : taskCwd;
227
- try {
228
- // Use bash find + glob expansion for maximum compatibility
229
- const pattern = input.pattern;
230
- const cmd = `cd ${JSON.stringify(searchDir)} && find . -type f | grep -E ${JSON.stringify(globToRegex(pattern))} | sort -t'/' -k2 | head -200`;
231
- const result = execSync(cmd, { cwd: searchDir, timeout: 10000 }).toString().trim();
232
- if (!result) return '(no matches)';
233
- return result.split('\n').map(f => f.replace(/^\.\//, '')).join('\n');
234
- } catch {
235
- try {
236
- // Fallback: simple find
237
- const result = execSync(`find ${JSON.stringify(searchDir)} -name "${input.pattern.replace(/\*\*\//g, '').split('/').pop()}" 2>/dev/null | head -100`, { timeout: 8000 }).toString().trim();
238
- return result || '(no matches)';
239
- } catch { return '(no matches)'; }
240
- }
241
- }
242
-
243
- case 'grep': {
244
- const searchPath = input.path ? resolveFile(input.path, taskCwd) : taskCwd;
245
- const flags = ['-n', '--color=never'];
246
- if (input['-i']) flags.push('-i');
247
- if (input['-A']) flags.push(`-A${input['-A']}`);
248
- if (input['-B']) flags.push(`-B${input['-B']}`);
249
- if (input.glob) flags.push(`--include=${input.glob}`);
250
- if (input.output_mode === 'files_with_matches') flags.push('-l');
251
- if (input.output_mode === 'count') flags.push('-c');
252
- flags.push('-r');
253
- try {
254
- const result = execSync(`grep ${flags.join(' ')} ${JSON.stringify(input.pattern)} ${JSON.stringify(searchPath)} 2>/dev/null | head -200`, { timeout: 15000 }).toString().trim();
255
- return result || '(no matches)';
256
- } catch {
257
- return '(no matches)';
258
- }
259
- }
260
-
261
- case 'browser': {
262
- const result = await browserAction(input);
263
- // Handle screenshot result — save to workspace and emit image
264
- if (result && result.__screenshot) {
265
- const imgPath = path.join(workDir || taskCwd, `browser_screenshot_${Date.now()}.png`);
266
- try {
267
- mkdirSync(path.dirname(imgPath), { recursive: true });
268
- writeFileSync(imgPath, Buffer.from(result.base64, 'base64'));
269
- if (onImage) onImage(`data:image/png;base64,${result.base64}`);
270
- return `Screenshot saved to ${imgPath}. Image sent to chat.`;
271
- } catch (e) {
272
- if (onImage) onImage(`data:image/png;base64,${result.base64}`);
273
- return `Screenshot taken (${Math.round(result.base64.length * 0.75 / 1024)}KB)`;
274
- }
275
- }
276
- return result;
277
- }
278
-
279
- default:
280
- return `Unknown tool: ${name}`;
281
- }
282
- }
283
-
284
- // ── Helpers ─────────────────────────────────────────────────────────────────
285
-
286
- function resolveFile(filePath, cwd) {
287
- if (path.isAbsolute(filePath)) return filePath;
288
- return path.join(cwd || process.cwd(), filePath);
289
- }
290
-
291
- function globToRegex(pattern) {
292
- // Convert glob pattern to regex for grep filtering
293
- return pattern
294
- .replace(/\./g, '\\.')
295
- .replace(/\*\*/g, '.*')
296
- .replace(/\*/g, '[^/]*')
297
- .replace(/\?/g, '[^/]');
298
- }