import { exec } from 'child_process';
import { promisify } from 'util';
import { getBinaryPath, buildCliArgs, escapeString } from './utils.js';
const execAsync = promisify(exec);
const SEARCH_FLAG_MAP = {
filesOnly: '--files-only',
ignore: '--ignore',
excludeFilenames: '--exclude-filenames',
reranker: '--reranker',
frequencySearch: '--frequency',
exact: '--exact',
maxResults: '--max-results',
maxBytes: '--max-bytes',
maxTokens: '--max-tokens',
allowTests: '--allow-tests',
noMerge: '--no-merge',
mergeThreshold: '--merge-threshold',
session: '--session',
timeout: '--timeout',
language: '--language'
};
export async function search(options) {
if (!options || !options.path) {
throw new Error('Path is required');
}
if (!options.query) {
throw new Error('Query is required');
}
const binaryPath = await getBinaryPath(options.binaryOptions || {});
const cliArgs = buildCliArgs(options, SEARCH_FLAG_MAP);
if (options.json) {
cliArgs.push('--format', 'json');
}
if (!options.maxTokens) {
options.maxTokens = 10000;
cliArgs.push('--max-tokens', '10000');
}
if (!options.timeout) {
options.timeout = 30;
cliArgs.push('--timeout', '30');
}
if (options.language) {
if (!cliArgs.includes('--language')) {
cliArgs.push('--language', options.language);
}
}
if (options.exact) {
if (!cliArgs.includes('--exact')) {
cliArgs.push('--exact');
}
}
if (!options.session && process.env.PROBE_SESSION_ID) {
options.session = process.env.PROBE_SESSION_ID;
}
const queries = Array.isArray(options.query) ? options.query : [options.query];
let logMessage = `\nSearch: query="${queries[0]}" path="${options.path}"`;
if (options.maxResults) logMessage += ` maxResults=${options.maxResults}`;
logMessage += ` maxTokens=${options.maxTokens}`;
logMessage += ` timeout=${options.timeout}`;
if (options.allowTests) logMessage += " allowTests=true";
if (options.language) logMessage += ` language=${options.language}`;
if (options.exact) logMessage += " exact=true";
if (options.session) logMessage += ` session=${options.session}`;
console.error(logMessage);
const positionalArgs = [];
if (queries.length > 0) {
positionalArgs.push(escapeString(queries[0]));
}
positionalArgs.push(escapeString(options.path));
const command = `${binaryPath} search ${cliArgs.join(' ')} ${positionalArgs.join(' ')}`;
try {
const { stdout, stderr } = await execAsync(command, {
shell: true,
timeout: options.timeout * 1000 });
if (stderr && process.env.DEBUG) {
console.error(`stderr: ${stderr}`);
}
let resultCount = 0;
let tokenCount = 0;
let bytesCount = 0;
const lines = stdout.split('\n');
for (const line of lines) {
if (line.startsWith('```') && !line.includes('```language')) {
resultCount++;
}
}
const totalBytesMatch = stdout.match(/Total bytes returned:\s*(\d+)/i);
if (totalBytesMatch && totalBytesMatch[1]) {
bytesCount = parseInt(totalBytesMatch[1], 10);
}
const totalTokensMatch = stdout.match(/Total tokens returned:\s*(\d+)/i);
if (totalTokensMatch && totalTokensMatch[1]) {
tokenCount = parseInt(totalTokensMatch[1], 10);
} else {
const tokenMatch = stdout.match(/Tokens:?\s*(\d+)/i) ||
stdout.match(/(\d+)\s*tokens/i) ||
stdout.match(/token count:?\s*(\d+)/i);
if (tokenMatch && tokenMatch[1]) {
tokenCount = parseInt(tokenMatch[1], 10);
} else {
tokenCount = options.maxTokens;
}
}
let resultsMessage = `\nSearch results: ${resultCount} matches, ${tokenCount} tokens`;
if (bytesCount > 0) {
resultsMessage += `, ${bytesCount} bytes`;
}
console.error(resultsMessage);
if (options.json) {
try {
return JSON.parse(stdout);
} catch (error) {
console.error('Error parsing JSON output:', error);
return stdout; }
}
return stdout;
} catch (error) {
if (error.code === 'ETIMEDOUT' || error.killed) {
const timeoutMessage = `Search operation timed out after ${options.timeout} seconds.\nCommand: ${command}`;
console.error(timeoutMessage);
throw new Error(timeoutMessage);
}
const errorMessage = `Error executing search command: ${error.message}\nCommand: ${command}`;
throw new Error(errorMessage);
}
}