tinyagent-ts
Version:
Modern TypeScript framework for building AI agents with pluggable tools and ReAct reasoning
1,580 lines (1,454 loc) • 1.03 MB
JavaScript
import { z } from 'zod';
import fs$2 from 'fs';
import { exec } from 'child_process';
import require$$0$6, { randomUUID } from 'crypto';
import readline from 'readline';
import execa from 'execa';
import path$3 from 'path';
import require$$1$1 from 'tty';
import require$$1$2 from 'util';
import require$$0$3 from 'os';
import require$$0$4 from 'buffer';
import require$$1$3 from 'string_decoder';
import require$$4$1 from 'node:zlib';
import require$$1$5 from 'node:events';
import require$$0$5 from 'url';
import require$$7$1 from 'node:path';
import require$$2$1 from 'node:fs';
import require$$2$2 from 'node:http';
import require$$6$1 from 'querystring';
import require$$1$4 from 'node:net';
import require$$13 from 'stream';
/**
* Model error types
*/
class ModelError extends Error {
statusCode;
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = 'ModelError';
}
}
class ModelAbortError extends ModelError {
constructor(message = 'Model request was aborted') {
super(message);
this.name = 'ModelAbortError';
}
}
/**
* OpenRouter model provider implementation
*/
class OpenRouterProvider {
baseUrl = 'https://openrouter.ai/api/v1/chat/completions';
getName() {
return 'openrouter';
}
async chat(messages, config, abortSignal) {
if (abortSignal?.aborted) {
throw new ModelAbortError();
}
try {
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/yourusername/tinyagent-ts',
'X-Title': 'TinyAgent-TS',
},
signal: abortSignal,
body: JSON.stringify({
model: config.model,
messages,
}),
});
if (!response.ok) {
let errorDetails = { message: 'Failed to parse error response' };
try {
errorDetails = await response.json();
}
catch (parseError) {
// Ignore parsing error, use default message
}
throw new ModelError(`OpenRouter API error: ${response.status} ${response.statusText}. Details: ${JSON.stringify(errorDetails)}`, response.status);
}
const data = await response.json();
const content = data.choices[0]?.message?.content?.trim() ?? '';
return {
content,
usage: data.usage ? {
promptTokens: data.usage.prompt_tokens,
completionTokens: data.usage.completion_tokens,
totalTokens: data.usage.total_tokens,
} : undefined,
};
}
catch (error) {
if (error instanceof ModelError) {
throw error;
}
if (error instanceof Error && error.name === 'AbortError') {
throw new ModelAbortError();
}
throw new ModelError(`Failed to communicate with OpenRouter: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/**
* Manages model providers and handles communication with LLMs
*/
class ModelManager {
providers = new Map();
config;
constructor(config = {}) {
this.config = {
defaultProvider: config.defaultProvider || 'openrouter',
defaultModel: config.defaultModel || 'openai/gpt-4o-mini',
apiKey: config.apiKey || process.env.OPENROUTER_API_KEY || '',
maxRetries: config.maxRetries || 3,
retryDelay: config.retryDelay || 1000,
};
if (!this.config.apiKey) {
throw new ModelError('API key is required. Set OPENROUTER_API_KEY environment variable or provide apiKey in config.');
}
// Register default providers
this.registerProvider('openrouter', new OpenRouterProvider());
}
/**
* Register a new model provider
*/
registerProvider(name, provider) {
this.providers.set(name, provider);
}
/**
* Get a registered provider
*/
getProvider(name) {
return this.providers.get(name);
}
/**
* Send messages to a model with retry logic
*/
async chat(messages, options = {}) {
const providerName = options.provider || this.config.defaultProvider;
const provider = this.getProvider(providerName);
if (!provider) {
throw new ModelError(`Unknown provider: ${providerName}`);
}
const modelConfig = {
model: options.model || this.config.defaultModel,
apiKey: options.apiKey || this.config.apiKey,
maxRetries: options.maxRetries || this.config.maxRetries,
};
const maxRetries = modelConfig.maxRetries || 0;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await provider.chat(messages, modelConfig, options.abortSignal);
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Don't retry on abort or certain error types
if (lastError.name === 'ModelAbortError' ||
lastError.name === 'AbortError' ||
(error instanceof ModelError && error.statusCode === 401)) {
throw lastError;
}
// If this is the last attempt, throw the error
if (attempt === maxRetries) {
break;
}
// Wait before retrying
if (this.config.retryDelay > 0) {
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay));
}
}
}
throw lastError;
}
/**
* Update configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
}
/**
* Implementation of ReAct state management
*/
class ReActStateManager {
task = '';
steps = [];
setTask(text) {
this.task = text;
}
getTask() {
return this.task;
}
clear() {
this.steps = [];
this.task = '';
}
addThought(text) {
this.steps.push({
type: 'thought',
text,
timestamp: new Date()
});
}
addAction(action) {
this.steps.push({
...action,
timestamp: new Date()
});
}
addObservation(text) {
this.steps.push({
type: 'observation',
text,
timestamp: new Date()
});
}
addReflexion(text) {
this.steps.push({
type: 'reflexion',
text,
timestamp: new Date()
});
}
getSteps() {
return [...this.steps];
}
/**
* Get the last argument value used for a specific key in any tool call.
*/
getLastArgValue(argKey) {
for (let i = this.steps.length - 1; i >= 0; i--) {
const step = this.steps[i];
if (step.type === 'action' &&
step.mode === 'json' &&
step.args &&
argKey in step.args) {
return step.args[argKey];
}
}
return undefined;
}
/**
* Convert state to LLM messages format
*/
toMessages(systemPrompt) {
const msgs = [];
if (systemPrompt) {
msgs.push({ role: 'system', content: systemPrompt });
}
if (this.task) {
msgs.push({ role: 'user', content: this.task });
}
for (const step of this.steps) {
switch (step.type) {
case 'thought':
msgs.push({ role: 'assistant', content: `Thought: ${step.text}` });
break;
case 'reflexion':
msgs.push({ role: 'assistant', content: `Reflexion: ${step.text}` });
break;
case 'action':
const actionStep = step;
if (actionStep.mode === 'code') {
msgs.push({
role: 'assistant',
content: `Action:\n\`\`\`ts\n${actionStep.text}\n\`\`\``,
});
}
else {
const json = JSON.stringify({ tool: actionStep.tool, args: actionStep.args });
msgs.push({ role: 'assistant', content: `Action: ${json}` });
}
break;
case 'observation':
msgs.push({
role: 'assistant',
content: `Observation: ${step.text}`,
});
break;
}
}
return msgs;
}
}
/**
* Parse a ReAct response text into structured components
*/
function parseReActResponse(text) {
const reflexMatch = text.match(/Reflect(?:ion|xion)?:([\s\S]*?)(?=\n(?:Thought|Action):|$)/i);
const reflexion = reflexMatch ? reflexMatch[1].trim() : undefined;
const thoughtMatch = text.match(/Thought:(.*?)(?:\nAction:|$)/s);
const thought = thoughtMatch ? thoughtMatch[1].trim() : '';
const actionPart = text.split(/\nAction:/s)[1] ?? '';
const trimmed = actionPart.trim();
// First, try to parse as JSON directly
try {
// Check if the trimmed text is a JSON object
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
const parsed = JSON.parse(trimmed);
if (typeof parsed.tool === 'string' && typeof parsed.args === 'object') {
const action = {
type: 'action',
mode: 'json',
tool: parsed.tool,
args: parsed.args,
text: trimmed,
};
return { thought, action, reflexion };
}
}
}
catch {
// If JSON parsing fails, continue to other formats
}
// Try to extract JSON from the text (may be embedded in other text)
const jsonMatch = trimmed.match(/{\s*"tool"\s*:\s*"([^"]+)".*}/);
if (jsonMatch) {
try {
const jsonStr = jsonMatch[0];
const parsed = JSON.parse(jsonStr);
if (typeof parsed.tool === 'string' && typeof parsed.args === 'object') {
const action = {
type: 'action',
mode: 'json',
tool: parsed.tool,
args: parsed.args,
text: jsonStr,
};
return { thought, action, reflexion };
}
}
catch {
// If JSON parsing fails, continue to other formats
}
}
// Check for code blocks
if (trimmed.startsWith('```')) {
const codeMatch = trimmed.match(/```(?:\w+)?\n([\s\S]*?)```/);
const code = codeMatch ? codeMatch[1].trim() : trimmed;
const action = {
type: 'action',
mode: 'code',
tool: 'code',
text: code
};
return { thought, action, reflexion };
}
// Final fallback: treat as code
if (trimmed) {
const action = {
type: 'action',
mode: 'code',
tool: 'code',
text: trimmed
};
return { thought, action, reflexion };
}
return { thought, reflexion };
}
/**
* Schema for validating final answer structure
*/
const FinalAnswerSchema$1 = z.object({
answer: z.unknown().refine((val) => val !== undefined && val !== null, { message: "Answer cannot be undefined or null" })
});
/**
* Validate and cast a raw final answer through the schema
*/
function validateFinalAnswer(rawAnswer) {
try {
return FinalAnswerSchema$1.parse(rawAnswer);
}
catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid final answer structure: ${error.errors.map(e => e.message).join(', ')}`);
}
throw error;
}
}
/**
* ReAct engine that orchestrates the reasoning and acting loop
*/
class ReActEngine {
modelManager;
state;
tools = new Map();
constructor(modelManager) {
this.modelManager = modelManager;
this.state = new ReActStateManager();
}
/**
* Register a tool for use in ReAct loops
*/
registerTool(tool) {
this.tools.set(tool.name, tool);
}
/**
* Unregister a tool
*/
unregisterTool(name) {
this.tools.delete(name);
}
/**
* Get all registered tools
*/
getTools() {
return Array.from(this.tools.values());
}
/**
* Execute a ReAct loop
*/
async execute(task, systemPrompt, config = {}, options = {}) {
const { maxSteps = 5, enableReflexion = true, enableTrace = false, onStep, onComplete, } = config;
this.state.clear();
this.state.setTask(task);
let finalAnswer = undefined;
let usedTool = false;
try {
for (let step = 0; step < maxSteps; step++) {
if (options.abortSignal?.aborted) {
throw new Error('ReAct execution was aborted');
}
// Get current state as messages
const messages = this.state.toMessages(systemPrompt);
// Get response from model
const response = await this.modelManager.chat(messages, {
model: options.model,
abortSignal: options.abortSignal,
});
// Parse the response
const parsed = parseReActResponse(response.content);
// Add thought if present
if (parsed.thought) {
this.state.addThought(parsed.thought);
if (enableTrace) {
console.log(`Thought: ${parsed.thought}`);
}
if (onStep) {
onStep({ type: 'thought', text: parsed.thought });
}
}
// Process action if present
let observation = '';
if (parsed.action) {
this.state.addAction(parsed.action);
if (enableTrace) {
console.log(`Action: ${parsed.action.tool}(${JSON.stringify(parsed.action.args)})`);
}
if (onStep) {
onStep(parsed.action);
}
try {
// Check for final answer
if (parsed.action.tool === 'final_answer') {
if (!usedTool) {
console.warn('final_answer called before any other tool');
}
finalAnswer = parsed.action.args;
observation = JSON.stringify(finalAnswer);
this.state.addObservation(observation);
if (enableTrace) {
console.log(`Observation: ${observation}`);
}
if (onStep) {
onStep({ type: 'observation', text: observation });
}
break;
}
// Execute tool
const tool = this.tools.get(parsed.action.tool);
if (!tool) {
observation = `Unknown tool: ${parsed.action.tool}`;
}
else {
// Auto-fill missing arguments from state if possible
let toolArgs = { ...parsed.action.args };
if (tool.schema && typeof tool.schema.shape === 'object') {
const argKeys = Object.keys(tool.schema.shape);
for (const key of argKeys) {
if (toolArgs[key] === undefined) {
const lastValue = this.state.getLastArgValue(key);
if (lastValue !== undefined) {
toolArgs[key] = lastValue;
}
}
}
}
const result = await tool.execute(toolArgs, options.abortSignal);
usedTool = true;
observation = JSON.stringify(result);
}
}
catch (error) {
observation = error instanceof Error ? error.message : String(error);
}
this.state.addObservation(observation);
if (enableTrace) {
console.log(`Observation: ${observation}`);
}
if (onStep) {
onStep({ type: 'observation', text: observation });
}
}
// Reflexion step if enabled
if (enableReflexion && parsed.action) {
const reflectMessages = this.state.toMessages(systemPrompt);
reflectMessages.push({ role: 'user', content: 'Reflect:' });
if (options.abortSignal?.aborted) {
throw new Error('ReAct execution was aborted');
}
const reflectResponse = await this.modelManager.chat(reflectMessages, {
model: options.model,
abortSignal: options.abortSignal,
});
const reflectParsed = parseReActResponse(reflectResponse.content);
if (reflectParsed.reflexion) {
this.state.addReflexion(reflectParsed.reflexion);
if (enableTrace) {
console.log(`Reflexion: ${reflectParsed.reflexion}`);
}
if (onStep) {
onStep({ type: 'reflexion', text: reflectParsed.reflexion });
}
}
// Handle fix action from reflexion
if (reflectParsed.action) {
if (reflectParsed.action.tool === 'final_answer') {
if (!usedTool) {
console.warn('final_answer called before any other tool');
}
finalAnswer = reflectParsed.action.args;
const obs = JSON.stringify(finalAnswer);
this.state.addObservation(obs);
if (enableTrace) {
console.log(`Observation: ${obs}`);
}
if (onStep) {
onStep({ type: 'observation', text: obs });
}
break;
}
// Execute fix action
const tool = this.tools.get(reflectParsed.action.tool);
let obs = '';
try {
if (!tool) {
obs = `Unknown tool: ${reflectParsed.action.tool}`;
}
else {
const result = await tool.execute(reflectParsed.action.args || {}, options.abortSignal);
usedTool = true;
obs = JSON.stringify(result);
}
}
catch (error) {
obs = error instanceof Error ? error.message : String(error);
}
if (reflectParsed.thought) {
this.state.addThought(reflectParsed.thought);
}
this.state.addAction(reflectParsed.action);
this.state.addObservation(obs);
if (enableTrace) {
if (reflectParsed.thought)
console.log(`Thought: ${reflectParsed.thought}`);
console.log(`Action: ${reflectParsed.action.tool}(${JSON.stringify(reflectParsed.action.args)})`);
console.log(`Observation: ${obs}`);
}
if (onStep) {
if (reflectParsed.thought)
onStep({ type: 'thought', text: reflectParsed.thought });
onStep(reflectParsed.action);
onStep({ type: 'observation', text: obs });
}
}
else if (reflectParsed.thought) {
this.state.addThought(reflectParsed.thought);
if (enableTrace) {
console.log(`Thought: ${reflectParsed.thought}`);
}
if (onStep) {
onStep({ type: 'thought', text: reflectParsed.thought });
}
}
}
}
// Enforce final answer if not provided
finalAnswer = await this.enforceFinalAnswer(finalAnswer, systemPrompt, options);
// Validate final answer structure
try {
finalAnswer = validateFinalAnswer(finalAnswer);
}
catch (error) {
throw new Error(`Final answer validation failed: ${error instanceof Error ? error.message : String(error)}`);
}
const result = {
success: true,
steps: this.state.getSteps(),
finalAnswer,
};
if (onComplete) {
onComplete(result);
}
return result;
}
catch (error) {
const result = {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
steps: this.state.getSteps(),
finalAnswer,
};
if (onComplete) {
onComplete(result);
}
return result;
}
}
/**
* Enforce final answer if not provided by the model
*/
async enforceFinalAnswer(currentFinalAnswer, systemPrompt, options) {
// If we already have a final answer, return it
if (currentFinalAnswer !== undefined) {
return currentFinalAnswer;
}
console.warn('ReAct loop completed without final_answer tool call. Forcing final answer generation.');
// Create a final answer request
const finalMessages = this.state.toMessages(systemPrompt);
finalMessages.push({
role: 'user',
content: 'You must provide a final answer now using the final_answer tool. Summarize your findings and provide a conclusive response.'
});
try {
const finalResponse = await this.modelManager.chat(finalMessages, {
model: options.model,
abortSignal: options.abortSignal,
});
const finalParsed = parseReActResponse(finalResponse.content);
if (finalParsed.action && finalParsed.action.tool === 'final_answer') {
const finalAnswer = finalParsed.action.args;
this.state.addThought(finalParsed.thought || 'Providing final answer');
this.state.addAction(finalParsed.action);
this.state.addObservation(JSON.stringify(finalAnswer));
return finalAnswer;
}
else {
// Fallback: create a final answer from the most relevant tool execution result
const steps = this.state.getSteps();
const observations = steps.filter(s => s.type === 'observation');
// Try to find the most recent meaningful tool execution result (not action text)
let answerContent = 'Task completed';
// Look for observations that contain actual results (not action descriptions)
for (let i = observations.length - 1; i >= 0; i--) {
const obs = observations[i];
if (obs.text && !obs.text.startsWith('Action:') && !obs.text.startsWith('{\"answer\":')) {
try {
// If observation is JSON, try to extract meaningful content
const parsed = JSON.parse(obs.text);
if (typeof parsed === 'string') {
// If it's a UUID or similar string result, format it nicely
if (parsed.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
answerContent = `The generated UUID is: ${parsed}`;
}
else {
answerContent = parsed;
}
break;
}
else if (parsed && typeof parsed === 'object') {
answerContent = JSON.stringify(parsed);
break;
}
}
catch {
// If not JSON, use the text directly if it looks meaningful
if (obs.text.length > 10 && !obs.text.includes('Unknown tool')) {
answerContent = obs.text;
break;
}
}
}
}
// If no meaningful observation found, use the final response
if (answerContent === 'Task completed' && finalResponse.content) {
answerContent = finalResponse.content;
}
const finalAnswer = { answer: answerContent };
this.state.addThought('Providing final answer based on previous results');
// Create proper ActionStep for final_answer
const finalAnswerAction = {
type: 'action',
mode: 'json',
tool: 'final_answer',
args: finalAnswer,
text: JSON.stringify({ tool: 'final_answer', args: finalAnswer })
};
this.state.addAction(finalAnswerAction);
this.state.addObservation(JSON.stringify(finalAnswer));
return finalAnswer;
}
}
catch (error) {
console.warn('Failed to force final answer:', error);
return { answer: 'Task completed but final answer generation failed' };
}
}
/**
* Get the current state
*/
getState() {
return this.state;
}
/**
* Reset the state
*/
reset() {
this.state.clear();
}
}
/**
* Base class for implementing tools
*/
class BaseTool {
/**
* Validate arguments against the tool's schema
*/
validateArgs(args) {
return this.schema.parse(args);
}
/**
* Create a successful result
*/
success(data, metadata) {
return {
success: true,
data,
metadata,
};
}
/**
* Create an error result
*/
error(message, metadata) {
return {
success: false,
error: message,
metadata,
};
}
}
/**
* Standard implementation of the tool registry
*/
class StandardToolRegistry {
tools = new Map();
metadata = new Map();
register(tool) {
if (this.tools.has(tool.name)) {
throw new Error(`Tool with name '${tool.name}' is already registered`);
}
this.tools.set(tool.name, tool);
this.metadata.set(tool.name, {
name: tool.name,
description: tool.description,
schema: tool.schema,
});
}
unregister(name) {
this.tools.delete(name);
this.metadata.delete(name);
}
get(name) {
return this.tools.get(name);
}
getAll() {
return Array.from(this.tools.values());
}
getByCategory(category) {
const result = [];
for (const [name, meta] of this.metadata.entries()) {
if (meta.category === category) {
const tool = this.tools.get(name);
if (tool) {
result.push(tool);
}
}
}
return result;
}
has(name) {
return this.tools.has(name);
}
/**
* Get metadata for a tool
*/
getMetadata(name) {
return this.metadata.get(name);
}
/**
* Get all tool metadata
*/
getAllMetadata() {
return Array.from(this.metadata.values());
}
/**
* Register a tool with additional metadata
*/
registerWithMetadata(tool, metadata) {
this.register(tool);
const fullMetadata = {
name: tool.name,
description: tool.description,
schema: tool.schema,
...metadata,
};
this.metadata.set(tool.name, fullMetadata);
}
/**
* Create a StandardToolRegistry from an object of tools
*/
static fromTools(toolsObj) {
const registry = new StandardToolRegistry();
for (const key in toolsObj) {
if (Object.prototype.hasOwnProperty.call(toolsObj, key)) {
registry.register(toolsObj[key]);
}
}
return registry;
}
/**
* Get tools catalog as string for LLM prompts
*/
getCatalog() {
return this.getAll()
.map(tool => `- ${tool.name}: ${tool.description}`)
.join('\n');
}
/**
* Clear all registered tools
*/
clear() {
this.tools.clear();
this.metadata.clear();
}
/**
* Get tool count
*/
size() {
return this.tools.size;
}
}
/**
* Schema for final answer arguments
*/
const FinalAnswerSchema = z.object({
answer: z.string().describe('The final answer to provide to the user'),
});
/**
* Tool for providing final answers in ReAct loops
*/
class FinalAnswerTool extends BaseTool {
name = 'final_answer';
description = 'Provide the final answer to the user\'s question or task';
schema = FinalAnswerSchema;
async execute(args) {
const validated = this.validateArgs(args);
return validated;
}
}
/**
* Schema for file operations
*/
const FileToolSchema = z.object({
action: z.enum(['read', 'write', 'append', 'delete']).describe('The file operation to perform'),
path: z.string().describe('The file path to operate on'),
content: z.string().optional().describe('Content to write/append (required for write/append actions)'),
});
/**
* File system tool supporting basic CRUD operations
*/
class FileTool extends BaseTool {
name = 'file';
description = 'Read, write, append or delete a file on disk';
schema = FileToolSchema;
async execute(args, abortSignal) {
if (abortSignal?.aborted) {
throw new Error('Operation was aborted');
}
const { action, path, content } = this.validateArgs(args);
try {
switch (action) {
case 'read':
if (abortSignal?.aborted)
throw new Error('Operation was aborted');
return fs$2.existsSync(path) ? fs$2.readFileSync(path, 'utf-8') : '';
case 'write':
if (!content && content !== '') {
throw new Error('Content is required for write action');
}
if (abortSignal?.aborted)
throw new Error('Operation was aborted');
fs$2.writeFileSync(path, content);
return `Successfully wrote to ${path}`;
case 'append':
if (!content && content !== '') {
throw new Error('Content is required for append action');
}
if (abortSignal?.aborted)
throw new Error('Operation was aborted');
fs$2.appendFileSync(path, content);
return `Successfully appended to ${path}`;
case 'delete':
if (abortSignal?.aborted)
throw new Error('Operation was aborted');
if (fs$2.existsSync(path)) {
fs$2.unlinkSync(path);
return `Successfully deleted ${path}`;
}
else {
return `File ${path} does not exist`;
}
default:
throw new Error(`Unknown action: ${action}`);
}
}
catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`File operation failed: ${String(error)}`);
}
}
}
/**
* Schema for grep operations
*/
const GrepToolSchema = z.object({
pattern: z.string().describe('The pattern to search for'),
file: z.string().describe('The file to search in'),
flags: z.string().optional().describe('Additional grep flags (e.g., -i for case insensitive)'),
});
/**
* Grep tool for searching patterns in files
*/
class GrepTool extends BaseTool {
name = 'grep';
description = 'Search for a pattern in a file using grep';
schema = GrepToolSchema;
async execute(args, abortSignal) {
if (abortSignal?.aborted) {
throw new Error('Operation was aborted');
}
const { pattern, file, flags = '' } = this.validateArgs(args);
return new Promise((resolve, reject) => {
// Escape the pattern to prevent shell injection
const escapedPattern = pattern.replace(/'/g, "\\'");
const command = `grep ${flags} -n '${escapedPattern}' '${file}'`;
const childProcess = exec(command, (error, stdout, stderr) => {
if (abortSignal?.aborted) {
childProcess.kill();
reject(new Error('Operation was aborted'));
return;
}
if (error) {
// Grep returns exit code 1 when no matches found, which is not an error
if (error.code === 1) {
resolve('No matches found');
return;
}
reject(new Error(`Grep failed: ${stderr.trim() || error.message}`));
return;
}
resolve(stdout.trim() || 'No matches found');
});
// Handle abort signal
if (abortSignal) {
abortSignal.addEventListener('abort', () => {
childProcess.kill();
reject(new Error('Operation was aborted'));
});
}
});
}
}
/**
* Schema for UUID generation (no arguments needed)
*/
const UuidToolSchema = z.object({
version: z.literal(4).optional().describe('UUID version (only v4 supported)'),
});
/**
* UUID generation tool
*/
class UuidTool extends BaseTool {
name = 'uuid';
description = 'Generate a random UUID v4';
schema = UuidToolSchema;
async execute(args, abortSignal) {
if (abortSignal?.aborted) {
throw new Error('Operation was aborted');
}
// Validate args even though they're optional
this.validateArgs(args);
return randomUUID();
}
}
/**
* Schema for human loop operations
*/
const HumanLoopToolSchema = z.object({
prompt: z.string().default('Need input:').describe('The prompt to show to the human operator'),
});
/**
* Human loop tool for getting input from human operators
*/
class HumanLoopTool extends BaseTool {
name = 'human_loop';
description = 'Pause and ask the human operator for guidance or input';
schema = HumanLoopToolSchema;
async execute(args, abortSignal) {
if (abortSignal?.aborted) {
throw new Error('Operation was aborted');
}
const { prompt } = this.validateArgs(args);
return new Promise((resolve, reject) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Handle abort signal
const cleanup = () => {
rl.close();
};
if (abortSignal) {
abortSignal.addEventListener('abort', () => {
cleanup();
reject(new Error('Operation was aborted'));
});
}
rl.question(`${prompt} `, (answer) => {
cleanup();
if (abortSignal?.aborted) {
reject(new Error('Operation was aborted'));
return;
}
resolve(answer.trim());
});
});
}
}
// ---------- schema ----------
const WebSearchSchema = z.object({
query: z.string()
.min(2, 'Query must be at least 2 characters')
.describe('The search term'),
count: z.number()
.int()
.min(1)
.max(20)
.default(10)
.describe('How many results to return (1-20)')
});
// ---------- tool ----------
class WebSearchTool extends BaseTool {
name = 'web-search';
description = 'Search the web and return a markdown list of results (title, URL, snippet).';
schema = WebSearchSchema;
endpoint = 'https://api.search.brave.com/res/v1/web/search';
apiKey = process.env.BRAVE_API_KEY ?? '';
async execute(args, abortSignal) {
// 1. validate + early cancel
const { query, count } = this.validateArgs(args);
if (abortSignal?.aborted)
throw new Error('Search was cancelled');
// 2. fetch results
const url = new URL(this.endpoint);
url.searchParams.set('q', query);
url.searchParams.set('count', String(count));
const res = await fetch(url.toString(), {
headers: { 'X-Subscription-Token': this.apiKey },
signal: abortSignal
});
if (!res.ok) {
throw new Error(`Brave API error ${res.status}`);
}
const data = await res.json();
// 3. extract
const list = data.web?.results?.map((r) => ({
title: r.title,
url: r.url,
desc: r.description ?? ''
})) ?? [];
// 4. format
if (!list.length)
return 'No results found.';
return ('## Search Results\n\n' +
list
.slice(0, count)
.map((r, i) => `${i + 1}. [${r.title}](${r.url})\n${r.desc?.trim() ?? ''}`)
.join('\n\n'));
}
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var express$2 = {exports: {}};
var bodyParser$1 = {exports: {}};
var httpErrors = {exports: {}};
/*!
* depd
* Copyright(c) 2014-2018 Douglas Christopher Wilson
* MIT Licensed
*/
/**
* Module dependencies.
*/
var relative = path$3.relative;
/**
* Module exports.
*/
var depd_1 = depd;
/**
* Get the path to base files on.
*/
var basePath = process.cwd();
/**
* Determine if namespace is contained in the string.
*/
function containsNamespace (str, namespace) {
var vals = str.split(/[ ,]+/);
var ns = String(namespace).toLowerCase();
for (var i = 0; i < vals.length; i++) {
var val = vals[i];
// namespace contained
if (val && (val === '*' || val.toLowerCase() === ns)) {
return true
}
}
return false
}
/**
* Convert a data descriptor to accessor descriptor.
*/
function convertDataDescriptorToAccessor (obj, prop, message) {
var descriptor = Object.getOwnPropertyDescriptor(obj, prop);
var value = descriptor.value;
descriptor.get = function getter () { return value };
if (descriptor.writable) {
descriptor.set = function setter (val) { return (value = val) };
}
delete descriptor.value;
delete descriptor.writable;
Object.defineProperty(obj, prop, descriptor);
return descriptor
}
/**
* Create arguments string to keep arity.
*/
function createArgumentsString (arity) {
var str = '';
for (var i = 0; i < arity; i++) {
str += ', arg' + i;
}
return str.substr(2)
}
/**
* Create stack string from stack.
*/
function createStackString (stack) {
var str = this.name + ': ' + this.namespace;
if (this.message) {
str += ' deprecated ' + this.message;
}
for (var i = 0; i < stack.length; i++) {
str += '\n at ' + stack[i].toString();
}
return str
}
/**
* Create deprecate for namespace in caller.
*/
function depd (namespace) {
if (!namespace) {
throw new TypeError('argument namespace is required')
}
var stack = getStack();
var site = callSiteLocation(stack[1]);
var file = site[0];
function deprecate (message) {
// call to self as log
log.call(deprecate, message);
}
deprecate._file = file;
deprecate._ignored = isignored(namespace);
deprecate._namespace = namespace;
deprecate._traced = istraced(namespace);
deprecate._warned = Object.create(null);
deprecate.function = wrapfunction;
deprecate.property = wrapproperty;
return deprecate
}
/**
* Determine if event emitter has listeners of a given type.
*
* The way to do this check is done three different ways in Node.js >= 0.8
* so this consolidates them into a minimal set using instance methods.
*
* @param {EventEmitter} emitter
* @param {string} type
* @returns {boolean}
* @private
*/
function eehaslisteners (emitter, type) {
var count = typeof emitter.listenerCount !== 'function'
? emitter.listeners(type).length
: emitter.listenerCount(type);
return count > 0
}
/**
* Determine if namespace is ignored.
*/
function isignored (namespace) {
if (process.noDeprecation) {
// --no-deprecation support
return true
}
var str = process.env.NO_DEPRECATION || '';
// namespace ignored
return containsNamespace(str, namespace)
}
/**
* Determine if namespace is traced.
*/
function istraced (namespace) {
if (process.traceDeprecation) {
// --trace-deprecation support
return true
}
var str = process.env.TRACE_DEPRECATION || '';
// namespace traced
return containsNamespace(str, namespace)
}
/**
* Display deprecation message.
*/
function log (message, site) {
var haslisteners = eehaslisteners(process, 'deprecation');
// abort early if no destination
if (!haslisteners && this._ignored) {
return
}
var caller;
var callFile;
var callSite;
var depSite;
var i = 0;
var seen = false;
var stack = getStack();
var file = this._file;
if (site) {
// provided site
depSite = site;
callSite = callSiteLocation(stack[1]);
callSite.name = depSite.name;
file = callSite[0];
} else {
// get call site
i = 2;
depSite = callSiteLocation(stack[i]);
callSite = depSite;
}
// get caller of deprecated thing in relation to file
for (; i < stack.length; i++) {
caller = callSiteLocation(stack[i]);
callFile = caller[0];
if (callFile === file) {
seen = true;
} else if (callFile === this._file) {
file = this._file;
} else if (seen) {
break
}
}
var key = caller
? depSite.join(':') + '__' + caller.join(':')
: undefined;
if (key !== undefined && key in this._warned) {
// already warned
return
}
this._warned[key] = true;
// generate automatic message from call site
var msg = message;
if (!msg) {
msg = callSite === depSite || !callSite.name
? defaultMessage(depSite)
: defaultMessage(callSite);
}
// emit deprecation if listeners exist
if (haslisteners) {
var err = DeprecationError(this._namespace, msg, stack.slice(i));
process.emit('deprecation', err);
return
}
// format and write message
var format = process.stderr.isTTY
? formatColor
: formatPlain;
var output = format.call(this, msg, caller, stack.slice(i));
process.stderr.write(output + '\n', 'utf8');
}
/**
* Get call site location as array.
*/
function callSiteLocation (callSite) {
var file = callSite.getFileName() || '<anonymous>';
var line = callSite.getLineNumber();
var colm = callSite.getColumnNumber();
if (callSite.isEval()) {
file = callSite.getEvalOrigin() + ', ' + file;
}
var site = [file, line, colm];
site.callSite = callSite;
site.name = callSite.getFunctionName();
return site
}
/**
* Generate a default message from the site.
*/
function defaultMessage (site) {
var callSite = site.callSite;
var funcName = site.name;
// make useful anonymous name
if (!funcName) {
funcName = '<anonymous@' + formatLocation(site) + '>';
}
var context = callSite.getThis();
var typeName = context && callSite.getTypeName();
// ignore useless type name
if (typeName === 'Object') {
typeName = undefined;
}
// make useful type name
if (typeName === 'Function') {
typeName = context.name || typeName;
}
return typeName && callSite.getMethodName()
? typeName + '.' + funcName
: funcName
}
/**
* Format deprecation message without color.
*/
function formatPlain (msg, caller, stack) {
var timestamp = new Date().toUTCString();
var formatted = timestamp +
' ' + this._namespace +
' deprecated ' + msg;
// add stack trace
if (this._traced) {
for (var i = 0; i < stack.length; i++) {
formatted += '\n at ' + stack[i].toString();
}
return formatted
}
if (caller) {
formatted += ' at ' + formatLocation(caller);
}
return formatted
}
/**
* Format deprecation message with color.
*/
function formatColor (msg, caller, stack) {
var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan
' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow
' \x1b[0m' + msg + '\x1b[39m'; // reset
// add stack trace
if (this._traced) {
for (var i = 0; i < stack.length; i++) {
formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m'; // cyan
}
return formatted
}
if (caller) {
formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m'; // cyan
}
return formatted
}
/**
* Format call site location.
*/
function formatLocation (callSite) {
return relative(basePath, callSite[0]) +
':' + callSite[1] +
':' + callSite[2]
}
/**
* Get the stack as array of call sites.
*/
function getStack () {
var limit = Error.stackTraceLimit;
var obj = {};
var prep = Error.prepareStackTrace;
Error.prepareStackTrace = prepareObjectStackTrace;
Error.stackTraceLimit = Math.max(10, limit);
// capture the stack
Error.captureStackTrace(obj);
// slice this function off the top
var stack = obj.stack.slice(1);
Error.prepareStackTrace = prep;
Error.stackTraceLimit = limit;
return stack
}
/**
* Capture call site stack from v8.
*/
function prepareObjectStackTrace (obj, stack) {
return stack
}
/**
* Return a wrapped function in a deprecation message.
*/
function wrapfunction (fn, message) {
if (typeof fn !== 'function') {
throw new TypeError('argument fn must be a function')
}
var args = createArgumentsString(fn.length);
var stack = getStack();
var site = callSiteLocation(stack[1]);
site.name = fn.name;
// eslint-disable-next-line no-new-func
var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site',
'"use strict"\n' +
'return function (' + args + ') {' +
'log.call(deprecate, message, site)\n' +
'return fn.apply(this, arguments)\n' +
'}')(fn, log, this, message, site);
return deprecatedfn
}
/**
* Wrap property in a deprecation message.
*/
function wrapproperty (obj, prop, message) {
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
throw new TypeError('argument obj must be object')
}
var descriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (!descriptor) {
throw new TypeError('must