import { searchTool, queryTool, extractTool, DEFAULT_SYSTEM_MESSAGE, listFilesByLevel } from '@buger/probe';
import { exec, spawn } from 'child_process';
import { promisify } from 'util';
import { randomUUID } from 'crypto';
import { EventEmitter } from 'events';
import fs from 'fs';
import { promises as fsPromises } from 'fs';
import path from 'path';
import os from 'os';
import { glob } from 'glob';
export const toolCallEmitter = new EventEmitter();
const activeToolExecutions = new Map();
export function isSessionCancelled(sessionId) {
return activeToolExecutions.get(sessionId)?.cancelled || false;
}
export function cancelToolExecutions(sessionId) {
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
console.log(`Cancelling tool executions for session: ${sessionId}`);
}
const sessionData = activeToolExecutions.get(sessionId);
if (sessionData) {
sessionData.cancelled = true;
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
console.log(`Session ${sessionId} marked as cancelled`);
}
return true;
}
return false;
}
function registerToolExecution(sessionId) {
if (!sessionId) return;
if (!activeToolExecutions.has(sessionId)) {
activeToolExecutions.set(sessionId, { cancelled: false });
} else {
activeToolExecutions.get(sessionId).cancelled = false;
}
}
export function clearToolExecutionData(sessionId) {
if (!sessionId) return;
if (activeToolExecutions.has(sessionId)) {
activeToolExecutions.delete(sessionId);
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
console.log(`Cleared tool execution data for session: ${sessionId}`);
}
}
}
const defaultSessionId = randomUUID();
if (process.env.DEBUG_CHAT === '1') {
console.log(`Generated default session ID (probeTool.js): ${defaultSessionId}`);
}
const configOptions = {
sessionId: defaultSessionId,
debug: process.env.DEBUG_CHAT === '1'
};
const baseSearchTool = searchTool(configOptions);
const baseQueryTool = queryTool(configOptions);
const baseExtractTool = extractTool(configOptions);
const wrapToolWithEmitter = (tool, toolName, baseExecute) => {
return {
...tool, execute: async (params) => { const debug = process.env.DEBUG_CHAT === '1';
const toolSessionId = params.sessionId || defaultSessionId;
if (debug) {
console.log(`[DEBUG] probeTool: Executing ${toolName} for session ${toolSessionId}`);
console.log(`[DEBUG] probeTool: Received params:`, params);
}
registerToolExecution(toolSessionId);
if (isSessionCancelled(toolSessionId)) {
console.error(`Tool execution cancelled BEFORE starting for session ${toolSessionId}`);
throw new Error(`Tool execution cancelled for session ${toolSessionId}`);
}
console.error(`Executing ${toolName} for session ${toolSessionId}`);
const { sessionId, ...toolParams } = params;
try {
const toolCallStartData = {
timestamp: new Date().toISOString(),
name: toolName,
args: toolParams, status: 'started'
};
if (debug) {
console.log(`[DEBUG] probeTool: Emitting toolCallStart:${toolSessionId}`);
}
toolCallEmitter.emit(`toolCall:${toolSessionId}`, toolCallStartData);
let result = null;
let executionError = null;
const executionPromise = baseExecute(toolParams).catch(err => {
executionError = err; });
const checkInterval = 50; while (result === null && executionError === null) {
if (isSessionCancelled(toolSessionId)) {
console.error(`Tool execution cancelled DURING execution for session ${toolSessionId}`);
throw new Error(`Tool execution cancelled for session ${toolSessionId}`);
}
const status = await Promise.race([
executionPromise.then(() => 'resolved').catch(() => 'rejected'),
new Promise(resolve => setTimeout(() => resolve('pending'), checkInterval))
]);
if (status === 'resolved') {
result = await executionPromise; } else if (status === 'rejected') {
break;
}
}
if (executionError) {
throw executionError;
}
if (isSessionCancelled(toolSessionId)) {
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
console.log(`Tool execution finished but session was cancelled for ${toolSessionId}`);
}
throw new Error(`Tool execution cancelled for session ${toolSessionId}`);
}
const toolCallData = {
timestamp: new Date().toISOString(),
name: toolName,
args: toolParams,
resultPreview: typeof result === 'string'
? (result.length > 200 ? result.substring(0, 200) + '...' : result)
: (result ? JSON.stringify(result).substring(0, 200) + '...' : 'No Result'),
status: 'completed'
};
if (debug) {
console.log(`[DEBUG] probeTool: Emitting toolCall:${toolSessionId} (completed)`);
}
toolCallEmitter.emit(`toolCall:${toolSessionId}`, toolCallData);
return result;
} catch (error) {
if (error.message.includes('cancelled for session')) {
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
console.log(`Caught cancellation error for ${toolName} in session ${toolSessionId}`);
}
throw error;
}
if (debug) {
console.error(`[DEBUG] probeTool: Error executing ${toolName}:`, error);
}
const toolCallErrorData = {
timestamp: new Date().toISOString(),
name: toolName,
args: toolParams,
error: error.message || 'Unknown error',
status: 'error'
};
if (debug) {
console.log(`[DEBUG] probeTool: Emitting toolCall:${toolSessionId} (error)`);
}
toolCallEmitter.emit(`toolCall:${toolSessionId}`, toolCallErrorData);
throw error; }
}
};
};
const baseImplementTool = {
name: "implement",
description: 'Implement a feature or fix a bug using aider. Only available when --allow-edit is enabled.',
parameters: {
type: 'object',
properties: {
task: {
type: 'string',
description: 'The task description to pass to aider for implementation'
}
},
required: ['task']
},
execute: async ({ task, autoCommits = false, prompt, sessionId }) => {
const execPromise = promisify(exec); const debug = process.env.DEBUG_CHAT === '1';
const currentWorkingDir = process.cwd();
if (debug) {
console.log(`[DEBUG] Executing aider with task: ${task}`);
console.log(`[DEBUG] Auto-commits: ${autoCommits}`);
console.log(`[DEBUG] Working directory: ${currentWorkingDir}`);
if (prompt) console.log(`[DEBUG] Custom prompt: ${prompt}`);
}
const tempDir = os.tmpdir();
const tempFilePath = path.join(tempDir, `aider-task-${Date.now()}-${Math.random().toString(36).substring(2, 10)}.txt`);
try {
await fsPromises.writeFile(tempFilePath, task, 'utf8');
if (debug) {
console.log(`[DEBUG] Created temporary file for task: ${tempFilePath}`);
}
const autoCommitsFlag = '';
const aiderCommand = `aider --yes --no-check-update --no-auto-commits --no-analytics ${autoCommitsFlag} --message-file "${tempFilePath}"`;
console.error("Task:", task.substring(0, 100) + (task.length > 100 ? "..." : ""));
console.error("Working directory:", currentWorkingDir);
console.error("Temp file:", tempFilePath);
return new Promise((resolve, reject) => {
try {
const childProcess = spawn('sh', ['-c', aiderCommand], {
cwd: currentWorkingDir
});
let stdoutData = '';
let stderrData = '';
childProcess.stdout.on('data', (data) => {
const output = data.toString();
stdoutData += output;
process.stderr.write(output);
});
childProcess.stderr.on('data', (data) => {
const output = data.toString();
stderrData += output;
process.stderr.write(output);
});
childProcess.on('close', (code) => {
if (debug) {
console.log(`[DEBUG] aider process exited with code ${code}`);
console.log(`[DEBUG] Total stdout: ${stdoutData.length} chars`);
console.log(`[DEBUG] Total stderr: ${stderrData.length} chars`);
}
fsPromises.unlink(tempFilePath)
.then(() => {
if (debug) {
console.log(`[DEBUG] Removed temporary file: ${tempFilePath}`);
}
})
.catch(err => {
console.error(`Error removing temporary file ${tempFilePath}:`, err);
})
.finally(() => {
resolve({
success: code === 0,
output: stdoutData,
error: stderrData || (code !== 0 ? `Process exited with code ${code}` : null),
command: aiderCommand,
timestamp: new Date().toISOString(),
prompt: prompt || null
});
});
});
childProcess.on('error', (error) => {
console.error(`Error executing aider:`, error);
fsPromises.unlink(tempFilePath)
.then(() => {
if (debug) {
console.log(`[DEBUG] Removed temporary file after error: ${tempFilePath}`);
}
})
.catch(err => {
console.error(`Error removing temporary file ${tempFilePath}:`, err);
})
.finally(() => {
resolve({
success: false,
output: stdoutData,
error: error.message || 'Unknown error executing aider',
command: aiderCommand,
timestamp: new Date().toISOString(),
prompt: prompt || null
});
});
});
} catch (error) {
console.error(`Error spawning aider process:`, error);
fsPromises.unlink(tempFilePath)
.then(() => {
if (debug) {
console.log(`[DEBUG] Removed temporary file after spawn error: ${tempFilePath}`);
}
})
.catch(err => {
console.error(`Error removing temporary file ${tempFilePath}:`, err);
})
.finally(() => {
resolve({
success: false,
output: null,
error: error.message || 'Unknown error spawning aider process',
command: aiderCommand,
timestamp: new Date().toISOString(),
prompt: prompt || null
});
});
}
});
} catch (error) {
console.error(`Error creating temporary file:`, error);
return {
success: false,
output: null,
error: `Error creating temporary file: ${error.message}`,
command: null,
timestamp: new Date().toISOString(),
prompt: prompt || null
};
}
}
};
const baseListFilesTool = {
name: "listFiles",
description: 'List files in a specified directory',
parameters: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'The directory path to list files from. Defaults to current directory if not specified.'
}
},
required: []
},
execute: async ({ directory = '.', sessionId }) => {
const debug = process.env.DEBUG_CHAT === '1';
const currentWorkingDir = process.cwd();
const targetDir = path.resolve(currentWorkingDir, directory);
if (debug) {
console.log(`[DEBUG] Listing files in directory: ${targetDir}`);
}
try {
const files = await fs.promises.readdir(targetDir, { withFileTypes: true });
const result = files.map(file => {
const isDirectory = file.isDirectory();
return {
name: file.name,
type: isDirectory ? 'directory' : 'file',
path: path.join(directory, file.name)
};
});
if (debug) {
console.log(`[DEBUG] Found ${result.length} files/directories in ${targetDir}`);
}
return {
success: true,
directory: targetDir,
files: result,
timestamp: new Date().toISOString()
};
} catch (error) {
console.error(`Error listing files in ${targetDir}:`, error);
return {
success: false,
directory: targetDir,
error: error.message || 'Unknown error listing files',
timestamp: new Date().toISOString()
};
}
}
};
const baseSearchFilesTool = {
name: "searchFiles",
description: 'Search for files using a glob pattern, recursively by default',
parameters: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'The glob pattern to search for (e.g., "**/*.js", "*.md")'
},
directory: {
type: 'string',
description: 'The directory to search in. Defaults to current directory if not specified.'
},
recursive: {
type: 'boolean',
description: 'Whether to search recursively. Defaults to true.'
}
},
required: ['pattern']
},
execute: async ({ pattern, directory, recursive = true, sessionId }) => {
directory = directory || '.';
const debug = process.env.DEBUG_CHAT === '1';
const currentWorkingDir = process.cwd();
const targetDir = path.resolve(currentWorkingDir, directory);
console.error(`Executing searchFiles with params: pattern="${pattern}", directory="${directory}", recursive=${recursive}`);
console.error(`Resolved target directory: ${targetDir}`);
console.error(`Current working directory: ${currentWorkingDir}`);
if (debug) {
console.log(`[DEBUG] Searching for files with pattern: ${pattern}`);
console.log(`[DEBUG] In directory: ${targetDir}`);
console.log(`[DEBUG] Recursive: ${recursive}`);
}
if (pattern.includes('**/**') || pattern.split('*').length > 10) {
console.error(`Pattern too complex: ${pattern}`);
return {
success: false,
directory: targetDir,
pattern: pattern,
error: 'Pattern too complex. Please use a simpler glob pattern.',
timestamp: new Date().toISOString()
};
}
try {
const options = {
cwd: targetDir,
dot: true, nodir: true, absolute: false, timeout: 10000, maxDepth: recursive ? 10 : 1, };
const searchPattern = recursive ? pattern : pattern.replace(/^\*\*\//, '');
console.error(`Starting glob search with pattern: ${searchPattern} in ${targetDir}`);
console.error(`Glob options: ${JSON.stringify(options)}`);
let files = [];
if (pattern.includes('*') && !pattern.includes('**') && pattern.split('/').length <= 2) {
console.error(`Using direct file search for simple pattern: ${pattern}`);
try {
const parts = pattern.split('/');
let searchDir = targetDir;
let filePattern;
if (parts.length === 2) {
searchDir = path.join(targetDir, parts[0]);
filePattern = parts[1];
} else {
filePattern = parts[0];
}
console.error(`Searching in directory: ${searchDir} for files matching: ${filePattern}`);
try {
await fsPromises.access(searchDir);
} catch (err) {
console.error(`Directory does not exist: ${searchDir}`);
return {
success: true,
directory: targetDir,
pattern: pattern,
recursive: recursive,
files: [],
count: 0,
timestamp: new Date().toISOString()
};
}
const dirEntries = await fsPromises.readdir(searchDir, { withFileTypes: true });
const regexPattern = filePattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`);
files = dirEntries
.filter(entry => entry.isFile() && regex.test(entry.name))
.map(entry => {
const relativePath = parts.length === 2
? path.join(parts[0], entry.name)
: entry.name;
return relativePath;
});
console.error(`Direct search found ${files.length} files matching ${filePattern}`);
} catch (err) {
console.error(`Error in direct file search: ${err.message}`);
console.error(`Falling back to glob search`);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Search operation timed out after 10 seconds')), 10000);
});
files = await Promise.race([
glob(searchPattern, options),
timeoutPromise
]);
}
} else {
console.error(`Using glob for complex pattern: ${pattern}`);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Search operation timed out after 10 seconds')), 10000);
});
files = await Promise.race([
glob(searchPattern, options),
timeoutPromise
]);
}
console.error(`Search completed, found ${files.length} files in ${targetDir}`);
console.error(`Pattern: ${pattern}, Recursive: ${recursive}`);
if (debug) {
console.log(`[DEBUG] Found ${files.length} files matching pattern ${pattern}`);
}
const maxResults = 1000;
const limitedFiles = files.length > maxResults ? files.slice(0, maxResults) : files;
if (files.length > maxResults) {
console.warn(`Warning: Limited results to ${maxResults} files out of ${files.length} total matches`);
}
return {
success: true,
directory: targetDir,
pattern: pattern,
recursive: recursive,
files: limitedFiles.map(file => path.join(directory, file)),
count: limitedFiles.length,
totalMatches: files.length,
limited: files.length > maxResults,
timestamp: new Date().toISOString()
};
} catch (error) {
console.error(`Error searching files with pattern "${pattern}" in ${targetDir}:`, error);
console.error(`Search parameters: directory="${directory}", recursive=${recursive}, sessionId=${sessionId}`);
return {
success: false,
directory: targetDir,
pattern: pattern,
error: error.message || 'Unknown error searching files',
timestamp: new Date().toISOString()
};
}
}
};
export const searchToolInstance = wrapToolWithEmitter(baseSearchTool, 'search', baseSearchTool.execute);
export const queryToolInstance = wrapToolWithEmitter(baseQueryTool, 'query', baseQueryTool.execute);
export const extractToolInstance = wrapToolWithEmitter(baseExtractTool, 'extract', baseExtractTool.execute);
export const implementToolInstance = wrapToolWithEmitter(baseImplementTool, 'implement', baseImplementTool.execute);
export const listFilesToolInstance = wrapToolWithEmitter(baseListFilesTool, 'listFiles', baseListFilesTool.execute);
export const searchFilesToolInstance = wrapToolWithEmitter(baseSearchFilesTool, 'searchFiles', baseSearchFilesTool.execute);
export const probeTool = {
...searchToolInstance, name: "search", description: 'DEPRECATED: Use <search> tool instead. Search code using keywords.',
execute: async (params) => { const debug = process.env.DEBUG_CHAT === '1';
if (debug) {
console.log(`[DEBUG] probeTool (Compatibility Layer) executing for session ${params.sessionId}`);
}
const { keywords, folder, sessionId, ...rest } = params;
const mappedParams = {
query: keywords,
path: folder || '.', sessionId: sessionId, ...rest };
if (debug) {
console.log("[DEBUG] probeTool mapped params: ", mappedParams);
}
try {
const result = await searchToolInstance.execute(mappedParams);
const formattedResult = {
results: result, command: `probe search --query "${keywords}" --path "${folder || '.'}"`, timestamp: new Date().toISOString()
};
if (debug) {
console.log("[DEBUG] probeTool compatibility layer returning formatted result.");
}
return formattedResult;
} catch (error) {
if (debug) {
console.error(`[DEBUG] Error in probeTool compatibility layer:`, error);
}
throw error;
}
}
};
export { DEFAULT_SYSTEM_MESSAGE, listFilesByLevel };
export { searchTool, queryTool, extractTool };
export const toolCapabilities = {
search: "Search code using keywords and patterns",
query: "Query code with structured parameters for more precise results",
extract: "Extract code blocks and context from files",
implement: "Implement features or fix bugs using aider (requires --allow-edit)",
listFiles: "List files and directories in a specified location",
searchFiles: "Find files matching a glob pattern with recursive search capability"
};