grep_search
Search file contents using grep with pattern matching.
Parameters
Regex pattern to search for. Accepts alternative keys: query, regex.
Directory to search in (relative to workspace root). Defaults to workspace root. Accepts alternative keys: directory, dir.
File pattern to limit search (e.g., "*.ts", "*.{js,tsx}").
Returns
Grep output showing matching lines with file paths and line numbers, or "No matches found". Output is truncated at 100 KB.
Behavior
Executes grep -rn pattern path in the workspace
Returns file paths relative to workspace root
Truncates output at 100 KB for performance
Returns "No matches found" when grep exits with code 1
Examples
Search All Files
Search Specific Directory
Filter by File Extension
{
"pattern" : "executeVoiceTool"
}
Success Response
src/daemon/request-handler.ts:102: async voiceToolCall(
src/daemon/request-handler.ts:575: const result = await executeVoiceTool(
src/daemon/voice-tools.ts:1011:export async function executeVoiceTool(
No Matches Response
Error Response
Error: grep failed with exit code 2: Invalid regex pattern
Implementation Details
From voice-tools.ts:516-577:
async function grepSearch (
args : Record < string , unknown >,
workspaceRoot : string
) : Promise < string > {
const pattern = extractStringArg ( args , [ "pattern" , "query" , "regex" ]);
if ( ! pattern ) {
return "Error: Missing required parameter 'pattern'" ;
}
let searchPath = workspaceRoot ;
const requestedPath = extractStringArg ( args , [ "path" , "directory" , "dir" ]);
if ( requestedPath ) {
const resolved = await resolvePath ( requestedPath , workspaceRoot );
if ( ! resolved ) {
return "Error: Path escapes workspace root" ;
}
searchPath = resolved ;
}
const grepArgs = [ "-rn" , pattern , searchPath ];
if ( typeof args . include === "string" ) {
grepArgs . unshift ( `--include= ${ args . include } ` );
}
return new Promise (( resolveP ) => {
const proc = spawn ( "grep" , grepArgs , { cwd: workspaceRoot });
const stdoutChunks : Buffer [] = [];
const stderrChunks : Buffer [] = [];
proc . stdout ?. on ( "data" , ( chunk : Buffer ) => stdoutChunks . push ( chunk ));
proc . stderr ?. on ( "data" , ( chunk : Buffer ) => stderrChunks . push ( chunk ));
proc . on ( "close" , ( code ) => {
if ( code === 1 ) {
return resolveP ( "No matches found" );
}
const stderr = Buffer . concat ( stderrChunks ). toString ( "utf8" ). trim ();
if ( code !== 0 ) {
return resolveP (
stderr
? `Error: grep failed with exit code ${ code } : ${ stderr } `
: `Error: grep failed with exit code ${ code } `
);
}
const output = Buffer . concat ( stdoutChunks ). toString ( "utf8" );
if ( ! output ) {
return resolveP ( "No matches found" );
}
resolveP ( truncateOutput ( output ));
});
proc . on ( "error" , ( err ) =>
resolveP ( `Error: Failed to launch grep: ${ err . message } ` )
);
});
}
find_files
Find files matching a glob pattern in the workspace.
Parameters
Glob pattern to match files. Accepts alternative key: glob. Uses minimatch syntax.
Directory to search in (relative to workspace root). Defaults to workspace root. Accepts alternative keys: directory, dir.
Include dotfiles and directories in results.
Include directories in results (default: files only).
Returns
Newline-separated list of matching file paths (relative to workspace root), sorted alphabetically. Limited to 200 results.
Behavior
Recursively walks directory tree from search root
Skips .git, .build, and node_modules directories
Matches pattern against basename, workspace-relative path, and search-relative path
Returns paths relative to workspace root (POSIX format)
Truncates results at 200 files
Reports warnings for unreadable paths (up to 5)
Glob Pattern Examples
All TypeScript Files
Specific Filename
Multiple Extensions
Test Files
Nested Path Pattern
Success Response
src/daemon/event-bus.ts
src/daemon/health.ts
src/daemon/main.ts
src/daemon/metadata-store.ts
src/daemon/pi-process-manager.ts
src/daemon/pi-process.ts
src/daemon/request-handler.ts
src/daemon/socket-server.ts
src/daemon/voice-tools.ts
No Matches Response
No files found matching '*.py'
Default Pattern Response
When no pattern is provided (searches all non-hidden files):
Workspace scan complete: no non-hidden files found. Try include_hidden=true to list dotfiles.
Truncated Results
file-001.ts
file-002.ts
...
file-200.ts
[Results truncated at 200 entries]
With Warnings
src/file1.ts
src/file2.ts
[Warning: Skipped 2 unreadable path(s)]
[Warning] src/private (Permission denied)
[Warning] src/symlink (ELOOP: too many symbolic links)
Implementation Details
From voice-tools.ts:582-746:
async function findFiles (
args : Record < string , unknown >,
workspaceRoot : string
) : Promise < string > {
const pattern = extractStringArg ( args , [ "pattern" , "glob" ]) ?? "**/*" ;
const includeHidden = args . include_hidden === true ;
const includeDirectories = args . include_directories === true ;
let searchRoot = workspaceRoot ;
const requestedPath = extractStringArg ( args , [ "path" , "directory" , "dir" ]);
if ( requestedPath ) {
const resolved = await resolvePath ( requestedPath , workspaceRoot );
if ( ! resolved ) {
return "Error: Path escapes workspace root" ;
}
searchRoot = resolved ;
}
if ( ! existsSync ( searchRoot )) {
return `Error: Search path not found: ' ${ requestedPath ?? "." } '` ;
}
// ... directory walking and pattern matching ...
const sortedResults = [ ... results ]. sort (( a , b ) => a . localeCompare ( b ));
const outputLines = sortedResults ;
if ( outputLines . length >= MAX_FIND_RESULTS ) {
outputLines . push ( `[Results truncated at ${ MAX_FIND_RESULTS } entries]` );
}
if ( warnings . size > 0 ) {
outputLines . push ( `[Warning: Skipped ${ warnings . size } unreadable path(s)]` );
for ( const warning of warnings ) {
outputLines . push ( `[Warning] ${ warning } ` );
}
}
if ( outputLines . length === 0 ) {
if ( pattern . trim () === "**/*" ) {
return "Workspace scan complete: no non-hidden files found. Try include_hidden=true to list dotfiles." ;
}
return `No files found matching ' ${ pattern } '` ;
}
return outputLines . join ( " \n " );
}
Pattern Matching Logic
From voice-tools.ts:582-612:
function matchesGlobPattern (
pattern : string ,
baseName : string ,
workspaceRelativePath : string ,
searchRelativePath : string ,
includeHidden : boolean
) : boolean {
const normalizedPattern = toPosixPath ( pattern . trim ());
if ( ! normalizedPattern ) {
return false ;
}
const options = {
dot: includeHidden ,
nocase: true , // Case-insensitive matching
matchBase: true , // Match basename without path
};
const candidates = [
baseName ,
workspaceRelativePath ,
searchRelativePath ,
`/ ${ workspaceRelativePath } ` ,
`/ ${ searchRelativePath } ` ,
];
return candidates . some (( candidate ) =>
minimatch ( candidate , normalizedPattern , options )
);
}
Patterns are matched against multiple path formats for flexibility:
Basename : "config.ts"
Workspace-relative : "src/config.ts"
Search-relative : "config.ts" (if searching in src/)
Leading slash variants : "/src/config.ts"
web_search
Search the web using the Exa API to find relevant information, documentation, and resources.
Requires EXA_API_KEY or RUBBER_DUCK_EXA_API_KEY environment variable to be set.
Parameters
Search query string. Accepts alternative keys: q, search, prompt.
Number of results to return (1-10). Accepts alternative key: numResults.
Include page content snippets in results. Accepts alternative key: includeText.
Only search specific domains (e.g., ["github.com", "docs.python.org"]). Accepts alternative keys: includeDomains, domains.
Exclude specific domains from results. Accepts alternative key: excludeDomains.
Returns
Formatted list of search results with titles, URLs, scores, published dates, and optional text snippets. Output is truncated at 100 KB.
Behavior
Sends search request to Exa API endpoint
30-second timeout per request
Results ranked by relevance score
Text snippets truncated at 400 characters
Supports domain filtering
Returns "No web results found" if no matches
Examples
Basic Search
With Text Snippets
Domain-Specific Search
Exclude Domains
{
"query" : "rust async programming tutorial"
}
Success Response
1. Rust Async Book
URL: https://rust-lang.github.io/async-book/
Published: 2023-11-15
Score: 0.892
2. Asynchronous Programming in Rust
URL: https://tokio.rs/tokio/tutorial
Score: 0.845
3. Rust Async/.await Primer
URL: https://rust-lang.org/async-await
Score: 0.801
With Text Snippets
1. TypeScript Decorators
URL: https://www.typescriptlang.org/docs/handbook/decorators.html
Score: 0.923
Snippet: Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript...
2. TC39 Decorator Proposal
URL: https://github.com/tc39/proposal-decorators
Score: 0.887
Snippet: This proposal adds decorators to JavaScript, which are functions that can be used to metaprogram and add functionality to a value. Decorators can be attached to classes, methods, accessors, properties, and more...
Error Responses
Error: EXA_API_KEY not configured. Set EXA_API_KEY (or RUBBER_DUCK_EXA_API_KEY) to enable web_search.
Error: Missing required parameter 'query'
Error: web_search request timed out after 30000ms
Error: web_search failed (401 Unauthorized): Invalid API key
Implementation Details
From voice-tools.ts:749-1001:
async function webSearch ( args : Record < string , unknown >) : Promise < string > {
const config = parseWebSearchConfig ( args );
if ( typeof config === "string" ) {
return config ;
}
const apiKey =
process . env . RUBBER_DUCK_EXA_API_KEY ?? process . env . EXA_API_KEY ?? "" ;
if ( ! apiKey ) {
return "Error: EXA_API_KEY not configured. Set EXA_API_KEY (or RUBBER_DUCK_EXA_API_KEY) to enable web_search." ;
}
const payload = await fetchWebSearchResults (
apiKey ,
buildWebSearchBody ( config )
);
if ( typeof payload === "string" ) {
return payload ;
}
if ( ! ( isRecord ( payload ) && Array . isArray ( payload . results ))) {
return "Error: web_search response missing expected 'results' array" ;
}
return formatWebSearchResults (
payload . results ,
config . query ,
config . includeText
);
}
API Configuration
From voice-tools.ts:34-36:
const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search" ;
const WEB_SEARCH_TIMEOUT_MS = 30_000 ;
const WEB_SEARCH_MAX_RESULTS = 10 ;
const WEB_SEARCH_DEFAULT_RESULTS = 10 ;
Environment Variables
Set one of the following to enable web search:
# Primary (preferred)
export EXA_API_KEY = "your-exa-api-key"
# Alternative
export RUBBER_DUCK_EXA_API_KEY = "your-exa-api-key"
Get an API key at exa.ai .
Comparison
grep_search Use for : Searching file contents
Find where a function is used
Locate TODO comments
Search for regex patterns in code
Get line numbers and context
find_files Use for : Finding files by name
List all test files
Find configuration files
Discover files by extension
Build file inventories
web_search Use for : Finding web resources
Lookup API documentation
Find tutorials and guides
Research libraries and tools
Discover relevant examples
Best Practices
Narrow your search scope to reduce noise: // Too broad
{ "pattern" : "error" }
// Better
{ "pattern" : "Error:" , "path" : "src" , "include" : "*.ts" }
Combine tools for workflows
Check for truncation markers: const result = await executeVoiceTool ( "grep_search" , args , workspace );
if ( result . includes ( "[Output truncated at 100KB]" )) {
console . warn ( "Results truncated. Use more specific pattern." );
}
if ( result . includes ( "[Results truncated at 200 entries]" )) {
console . warn ( "Too many files. Narrow your pattern." );
}
By default, dotfiles are excluded. Enable explicitly when needed: // Find all hidden config files
{
"pattern" : ".*rc" ,
"include_hidden" : true
}
Skipped Directories
Both tools automatically skip common large directories:
.git — Git repository metadata
.build — Swift build artifacts
node_modules — Node.js dependencies
From voice-tools.ts:38:
const SKIP_DIRS = new Set ([ ".git" , ".build" , "node_modules" ]);
Limit Scope Search subdirectories instead of entire workspace: {
"pattern" : "*.test.ts" ,
"path" : "src/daemon"
}
Use File Filters Filter by extension to reduce grep work: {
"pattern" : "interface" ,
"include" : "*.ts"
}
Specific Patterns Use anchored regex to reduce matches: // Slow: matches everywhere
{ "pattern" : "test" }
// Fast: word boundary
{ "pattern" : " \\ btest \\ b" }
Parse Results Filter out metadata lines: const files = result
. split ( " \n " )
. filter ( line => ! line . startsWith ( "[" ));
Bash Use bash for advanced grep/find with pipes
File Operations Read files discovered by search tools