Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/mblode/rubber-duck/llms.txt

Use this file to discover all available pages before exploring further.

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