claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
249 lines (248 loc) • 8.64 kB
JavaScript
/**
* Tool Executor for CLI-Spawned Agents
*
* Executes tools requested by agents via Anthropic API tool_use blocks.
* Implements Read, Write, Edit, Bash, TodoWrite, Glob, Grep.
*/ import fs from 'fs/promises';
import { exec } from 'child_process';
import { promisify } from 'util';
import { glob } from 'glob';
const execAsync = promisify(exec);
/**
* Execute a single tool and return result
*/ export async function executeTool(toolUse) {
console.log(`[tool-executor] Executing tool: ${toolUse.name}`);
console.log(`[tool-executor] Tool use ID: ${toolUse.id}`);
try {
let result;
switch(toolUse.name){
case 'Read':
result = await executeRead(toolUse.input);
break;
case 'Write':
result = await executeWrite(toolUse.input);
break;
case 'Edit':
result = await executeEdit(toolUse.input);
break;
case 'Bash':
result = await executeBash(toolUse.input);
break;
case 'TodoWrite':
result = await executeTodoWrite(toolUse.input);
break;
case 'Glob':
result = await executeGlob(toolUse.input);
break;
case 'Grep':
result = await executeGrep(toolUse.input);
break;
default:
throw new Error(`Unknown tool: ${toolUse.name}`);
}
console.log(`[tool-executor] ✓ Tool executed successfully`);
return {
type: 'tool_result',
tool_use_id: toolUse.id,
content: result
};
} catch (error) {
console.error(`[tool-executor] ✗ Tool execution failed:`, error);
return {
type: 'tool_result',
tool_use_id: toolUse.id,
content: error instanceof Error ? error.message : String(error),
is_error: true
};
}
}
/**
* Execute Read tool
*/ async function executeRead(input) {
const { file_path, offset, limit } = input;
if (!file_path) {
throw new Error('file_path parameter is required');
}
const content = await fs.readFile(file_path, 'utf-8');
const lines = content.split('\n');
// Apply offset and limit if provided
const startLine = offset ? Number(offset) - 1 : 0;
const endLine = limit ? startLine + Number(limit) : lines.length;
const selectedLines = lines.slice(startLine, endLine);
// Format with line numbers (cat -n style)
const formatted = selectedLines.map((line, idx)=>`${String(startLine + idx + 1).padStart(6)}→${line}`).join('\n');
return formatted;
}
/**
* Execute Write tool
*/ async function executeWrite(input) {
const { file_path, content } = input;
if (!file_path) {
throw new Error('file_path parameter is required');
}
if (content === undefined) {
throw new Error('content parameter is required');
}
await fs.writeFile(file_path, content, 'utf-8');
return `File written successfully: ${file_path}`;
}
/**
* Execute Edit tool
*/ async function executeEdit(input) {
const { file_path, old_string, new_string, replace_all } = input;
if (!file_path) {
throw new Error('file_path parameter is required');
}
if (!old_string) {
throw new Error('old_string parameter is required');
}
if (new_string === undefined) {
throw new Error('new_string parameter is required');
}
// Read file
const content = await fs.readFile(file_path, 'utf-8');
// Perform replacement
let newContent;
if (replace_all) {
// Replace all occurrences
newContent = content.split(old_string).join(new_string);
} else {
// Replace first occurrence only (must be unique)
const occurrences = content.split(old_string).length - 1;
if (occurrences === 0) {
throw new Error('old_string not found in file');
}
if (occurrences > 1) {
throw new Error(`old_string appears ${occurrences} times. Must be unique or use replace_all: true`);
}
newContent = content.replace(old_string, new_string);
}
// Write back
await fs.writeFile(file_path, newContent, 'utf-8');
return `File edited successfully: ${file_path}`;
}
/**
* Execute Bash tool
*/ async function executeBash(input) {
const { command, timeout, run_in_background } = input;
if (!command) {
throw new Error('command parameter is required');
}
// DEBUG: Log bash command execution details
console.log(`[tool-executor] Bash command: ${command}`);
console.log(`[tool-executor] Timeout: ${timeout || 120000}ms`);
console.log(`[tool-executor] Background: ${run_in_background || false}`);
const timeoutMs = timeout ? Number(timeout) : 120000; // 2 minutes default
if (run_in_background) {
// Start background process and return immediately
exec(command);
return `Command started in background: ${command}`;
}
// Execute synchronously with timeout
// CRITICAL: Use /bin/bash instead of /bin/sh for [[ ]] support
const { stdout, stderr } = await execAsync(command, {
timeout: timeoutMs,
maxBuffer: 10 * 1024 * 1024,
shell: '/bin/bash' // REQUIRED: Coordinator uses [[ ]] conditionals
});
return stdout + stderr;
}
/**
* Execute TodoWrite tool
*/ async function executeTodoWrite(input) {
const { todos } = input;
if (!Array.isArray(todos)) {
throw new Error('todos parameter must be an array');
}
// Validate todo structure
for (const todo of todos){
if (!todo.content || !todo.status || !todo.activeForm) {
throw new Error('Each todo must have content, status, and activeForm');
}
if (![
'pending',
'in_progress',
'completed'
].includes(todo.status)) {
throw new Error('Invalid status. Must be pending, in_progress, or completed');
}
}
// Format todos for display
const formatted = todos.map((todo, idx)=>{
const status = todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '⚡' : '○';
return `${idx + 1}. [${status}] ${todo.content}`;
}).join('\n');
return `Todo list updated:\n${formatted}`;
}
/**
* Execute Glob tool
*/ async function executeGlob(input) {
const { pattern, path } = input;
if (!pattern) {
throw new Error('pattern parameter is required');
}
const cwd = path || process.cwd();
const files = await glob(pattern, {
cwd,
nodir: true
});
if (files.length === 0) {
return 'No files found';
}
return files.join('\n');
}
/**
* Execute Grep tool
*/ async function executeGrep(input) {
const { pattern, path, output_mode, glob: globPattern, type } = input;
if (!pattern) {
throw new Error('pattern parameter is required');
}
// Build ripgrep command
const args = [
'rg'
];
// Output mode
if (output_mode === 'files_with_matches' || !output_mode) {
args.push('-l'); // List files with matches
} else if (output_mode === 'count') {
args.push('-c'); // Count matches per file
}
// Default is content (no flag)
// Optional flags
if (input['-i']) args.push('-i'); // Case insensitive
if (input['-n']) args.push('-n'); // Line numbers
if (input['-A']) args.push(`-A${input['-A']}`); // After context
if (input['-B']) args.push(`-B${input['-B']}`); // Before context
if (input['-C']) args.push(`-C${input['-C']}`); // Context
// File filtering
if (globPattern) args.push('--glob', globPattern);
if (type) args.push('--type', type);
// Pattern and path
args.push(pattern);
if (path) args.push(path);
const command = args.join(' ');
try {
const { stdout } = await execAsync(command, {
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
});
return stdout || 'No matches found';
} catch (error) {
// ripgrep exits with code 1 if no matches found
if (error.code === 1) {
return 'No matches found';
}
throw error;
}
}
/**
* Execute multiple tools in sequence
*/ export async function executeTools(toolUses) {
const results = [];
for (const toolUse of toolUses){
const result = await executeTool(toolUse);
results.push(result);
}
return results;
}
//# sourceMappingURL=tool-executor.js.map