Skip to main content
Search file contents using grep with pattern matching.

Parameters

pattern
string
required
Regex pattern to search for. Accepts alternative keys: query, regex.
path
string
Directory to search in (relative to workspace root). Defaults to workspace root. Accepts alternative keys: directory, dir.
include
string
File pattern to limit search (e.g., "*.ts", "*.{js,tsx}").

Returns

result
string
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

{
  "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

No matches found

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

pattern
string
default:"**/*"
Glob pattern to match files. Accepts alternative key: glob. Uses minimatch syntax.
path
string
Directory to search in (relative to workspace root). Defaults to workspace root. Accepts alternative keys: directory, dir.
include_hidden
boolean
default:false
Include dotfiles and directories in results.
include_directories
boolean
default:false
Include directories in results (default: files only).

Returns

result
string
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

{
  "pattern": "**/*.ts"
}

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"

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

query
string
required
Search query string. Accepts alternative keys: q, search, prompt.
num_results
number
default:10
Number of results to return (1-10). Accepts alternative key: numResults.
include_text
boolean
default:false
Include page content snippets in results. Accepts alternative key: includeText.
include_domains
string[]
Only search specific domains (e.g., ["github.com", "docs.python.org"]). Accepts alternative keys: includeDomains, domains.
exclude_domains
string[]
Exclude specific domains from results. Accepts alternative key: excludeDomains.

Returns

result
string
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

{
  "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" }
Use find_files to discover, then read_file to inspect:
// 1. Find config files
const files = await executeVoiceTool("find_files", 
  JSON.stringify({ pattern: "**/config.json" }), 
  workspace
);

// 2. Read each one
for (const file of files.split("\n").filter(f => !f.startsWith("["))) {
  const content = await executeVoiceTool("read_file",
    JSON.stringify({ path: file }),
    workspace
  );
  // ... process content ...
}
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"]);

Performance Tips

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

Build docs developers (and LLMs) love