@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.
- package/bin/agentforge.js +909 -115
- package/package.json +2 -1
- package/scripts/check-task-semantics.js +911 -0
- package/scripts/postinstall.js +20 -5
- package/src/OllamaAgent.js +1178 -246
- package/src/OpenClawCLI.js +5897 -748
- package/src/browser.js +392 -0
- package/src/default-task-guides.js +95 -0
- package/src/resolveOpenclaw.js +38 -7
- package/src/selfUpdate.js +31 -3
- package/src/supervisor.js +88 -20
- package/src/taskSemantics.js +141 -0
- package/src/worker.js +4257 -230
- package/templates/agent/AGENTFORGE.md +151 -53
- package/templates/hooks/agentforge-platform/handler.js +322 -0
- package/src/HampAgentCLI.js +0 -125
- package/src/hampagent/browser.js +0 -321
- package/src/hampagent/runner.js +0 -277
- package/src/hampagent/sessions.js +0 -62
- package/src/hampagent/tools.js +0 -298
package/src/hampagent/runner.js
DELETED
|
@@ -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() {}
|
package/src/hampagent/tools.js
DELETED
|
@@ -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
|
-
}
|