Skip to content

Code Search

Navigate your codebase like an IDE—find function definitions, locate usages, and search code semantically using AI-powered tools.

Introduction

Ever wished you could just ask "Where is the calculate_total function?" instead of manually grepping through files? Consoul's code search tools understand code structure, not just text—finding definitions, usages, and references across your entire project.

Quick example:

$ consoul ask "Find all usages of deprecated_function"

The AI uses semantic code analysis to locate every call, import, and reference—not just text matches.

Related Tools:

Table of Contents

Overview

Consoul provides three complementary code search tools that work together to help you understand and navigate codebases:

  1. grep_search - Fast text-based pattern matching
  2. code_search - AST-based semantic symbol search
  3. find_references - Symbol usage finder

These tools are designed to work like an IDE's navigation features, but accessible through AI chat or programmatic SDK usage.

Key Benefits

  • Semantic Understanding: AST-based parsing understands code structure, not just text
  • Multi-Language: Support for Python, JavaScript/TypeScript, Go, Rust, Java, C/C++
  • Fast with Caching: Intelligent mtime-based caching provides 5-10x speedup
  • Safe Operations: All search tools are read-only (RiskLevel.SAFE)
  • AI-Friendly: Designed for tool calling with clear, structured JSON output

Available Tools

Purpose: Fast text pattern matching across files

Use when:

  • Searching for simple text strings or regex patterns
  • Finding TODO/FIXME comments
  • Searching across any file type (not just code)
  • Need fastest possible search
  • Pattern doesn't require understanding code structure

Technology: ripgrep (with grep fallback)

Speed: Very fast (<1s for typical projects)

Example:

from consoul.ai.tools import grep_search

# Find all TODO comments in Python files
result = grep_search.invoke({
    "pattern": "TODO",
    "glob_pattern": "*.py",
    "case_sensitive": False
})

Purpose: Find symbol definitions (functions, classes, methods)

Use when:

  • Finding where a function/class is defined
  • Locating symbol definitions before reading code
  • Building code inventory or documentation
  • Need to understand code structure

Technology: tree-sitter AST parsing

Speed: Fast with cache (~1-2s first run, <1s cached)

Example:

from consoul.ai.tools import code_search

# Find the ToolRegistry class definition
result = code_search.invoke({
    "query": "ToolRegistry",
    "symbol_type": "class"
})

find_references

Purpose: Find all usages/references of a symbol

Use when:

  • Planning to refactor/rename a function
  • Understanding how a symbol is used
  • Finding dead code (zero references)
  • Impact analysis before changes
  • Understanding dependencies

Technology: tree-sitter AST parsing (with cache)

Speed: Medium (~2-3s first run, <1s cached)

Example:

from consoul.ai.tools import find_references

# Find all usages of bash_execute
result = find_references.invoke({
    "symbol": "bash_execute",
    "scope": "project"
})

When to Use Which Tool

Decision Matrix

Question Tool Why
"Where is function X defined?" code_search Finds definitions semantically
"Where is function X used?" find_references Finds all usages
"Find all TODO comments" grep_search Text pattern in comments
"What functions are in file.py?" code_search Lists all symbols
"Is this function still used?" find_references Check for usages
"Find all error messages" grep_search String literal search
"Find calls to deprecated_fn" find_references Finds function calls
"Find class MyClass" code_search Find class definition
"Search across all file types" grep_search Works on any text
"Find imports of module X" find_references Finds import statements

Tool Comparison

grep_search code_search find_references
What it finds Text patterns Symbol definitions Symbol usages
Understanding Text-based Structure-aware Structure-aware
Speed Very fast Fast (cached) Medium (cached)
File types Any text Source code Source code
Comments ✅ Finds in comments ❌ Ignores comments ❌ Ignores comments
Strings ✅ Finds in strings ❌ Ignores strings ❌ Ignores strings
Context Line context Symbol context Usage context
Cache N/A ✅ Cached ✅ Cached

Common Workflows

Refactoring a Function:

1. code_search → Find definition
2. find_references → Find all usages
3. Analyze impact
4. Make changes
5. find_references → Verify no broken references

Understanding a Feature:

1. grep_search → Find relevant comments/docs
2. code_search → Find main classes/functions
3. find_references → Understand dependencies

Code Review:

1. grep_search → Find TODOs, FIXMEs, console.logs
2. code_search → Check new symbols added
3. find_references → Verify proper usage

Language Support

Support Matrix

Language Extension grep_search code_search find_references Notes
Python .py ✅ ✅ ✅ Full support
JavaScript .js ✅ ✅ ✅ Full support
TypeScript .ts, .tsx ✅ ✅ ✅ Full support
JSX .jsx ✅ ✅ ✅ Full support
Go .go ✅ ✅ ✅ Full support
Kotlin .kt ✅ ✅ ✅ Full support
Rust .rs ✅ ✅ ❌ Use grep_search for references
Java .java ✅ ✅ ❌ Use grep_search for references
C .c, .h ✅ ✅ ❌ Use grep_search for references
C++ .cpp, .hpp ✅ ✅ ❌ Use grep_search for references

Legend:

  • ✅ Full support - All features work correctly
  • ❌ No support - Tool doesn't work for this language

Note: find_references currently implements reference detection for Python, JavaScript/TypeScript, Go, Kotlin, and Java. For Rust and C/C++, use grep_search for text-based reference finding.

Detected Node Types by Language

Python

  • Definitions: function_definition, class_definition, method
  • References: call, import_from_statement, attribute

JavaScript/TypeScript

  • Definitions: function_declaration, class_declaration, method_definition
  • References: call_expression, import_specifier, member_expression

Go

  • Definitions: function_declaration, type_declaration
  • References: call_expression, selector_expression

Common Use Cases

1. Find All TODO Comments

Goal: Locate all TODO items across the project

Tool: grep_search

result = grep_search.invoke({
    "pattern": r"TODO|FIXME|XXX|HACK",
    "glob_pattern": "*.{py,js,ts}",
    "context_lines": 2
})

Why grep_search? TODOs are in comments, which AST tools ignore.

2. Find Function Definition

Goal: Locate where calculate_total is defined

Tool: code_search

result = code_search.invoke({
    "query": "calculate_total",
    "symbol_type": "function"
})

Why code_search? Finds exact definition location semantically.

3. Find All Function Usages

Goal: Find every call to deprecated_function before removing it

Tool: find_references

result = find_references.invoke({
    "symbol": "deprecated_function",
    "scope": "project",
    "include_definition": True  # Shows definition + usages
})

Why find_references? Distinguishes function calls from definition.

4. Locate Dead Code

Goal: Find functions with zero references

Workflow:

# 1. Get all functions
functions = code_search.invoke({"symbol_type": "function"})

# 2. Check each for references
for func in parse_results(functions):
    refs = find_references.invoke({"symbol": func["name"]})
    if len(parse_results(refs)) == 0:
        print(f"Dead code: {func['name']} at {func['file']}:{func['line']}")

5. Understand Class Dependencies

Goal: See how ShoppingCart class is used

Workflow:

# 1. Find class definition
definition = code_search.invoke({
    "query": "ShoppingCart",
    "symbol_type": "class"
})

# 2. Find all usages
usages = find_references.invoke({
    "symbol": "ShoppingCart",
    "scope": "project"
})

# 3. Analyze instantiations vs imports

6. Search Across Documentation

Goal: Find documentation for a specific API

Tool: grep_search

result = grep_search.invoke({
    "pattern": "authenticate user",
    "path": "docs/",
    "case_sensitive": False
})

Why grep_search? Markdown/docs aren't source code.

7. Find All Methods of a Class

Goal: List all methods in ToolRegistry class

Tool: code_search

result = code_search.invoke({
    "query": ".*",  # Match all
    "symbol_type": "method",
    "path": "src/consoul/ai/tools/registry.py"
})

8. Pre-Refactoring Analysis

Goal: Safely rename old_name to new_name

Complete Workflow:

# 1. Find definition
definition = code_search.invoke({"query": "old_name"})
print(f"Defined at: {definition}")

# 2. Find all usages
references = find_references.invoke({"symbol": "old_name"})
usage_count = len(parse_results(references))
print(f"Found {usage_count} usages")

# 3. Review each usage before refactoring
# 4. Make changes
# 5. Verify with find_references again

Performance

Speed Benchmarks

Typical project (1000 files, ~100K LOC):

Tool First Run Cached Cache Benefit
grep_search 0.5s 0.5s N/A (always fast)
code_search 2.5s 0.3s 8x faster
find_references 3.0s 0.4s 7x faster

Large project (5000 files, ~500K LOC):

Tool First Run Cached Cache Benefit
grep_search 1.2s 1.2s N/A
code_search 8.5s 0.8s 10x faster
find_references 10.0s 1.0s 10x faster

Cache Behavior

How caching works: 1. First search: Parses AST and caches results with file mtime 2. Subsequent searches: Checks mtime, returns cached data if unchanged 3. File modified: Detects mtime change, reparses file 4. Shared cache: code_search and find_references share cache instance

Cache keys:

  • code_search: tags:{file_path}
  • find_references: refs:{file_path}

Cache location:

  • Default: ~/.consoul/cache/code-search.v1/
  • Configurable via CodeSearchCache initialization

Optimization Tips

1. Use narrowest scope possible

# Slow - searches entire project
find_references.invoke({"symbol": "foo", "scope": "project"})

# Fast - searches one directory
find_references.invoke({"symbol": "foo", "scope": "directory", "path": "src/"})

# Fastest - searches one file
find_references.invoke({"symbol": "foo", "scope": "file", "path": "src/main.py"})

2. Use ripgrep for text search

# Install ripgrep for 5-10x faster grep_search
brew install ripgrep  # macOS
apt-get install ripgrep  # Ubuntu

3. Warm up cache for common operations

# Run code_search once to populate cache
code_search.invoke({"query": ".*", "path": "src/"})

# Future searches will be much faster

4. Filter by symbol type

# Faster - parses less
code_search.invoke({"query": "Foo", "symbol_type": "class"})

# Slower - searches all symbol types
code_search.invoke({"query": "Foo"})

5. Use case-insensitive search wisely

# Slower - must check more variations
find_references.invoke({"symbol": "foo", "case_sensitive": False})

# Faster - exact match only
find_references.invoke({"symbol": "Foo", "case_sensitive": True})

Integration Patterns

Pattern 1: Multi-Step Discovery

from consoul import Consoul

console = Consoul(tools=True)

# Step 1: Find potential matches with grep
grep_results = console.chat("Search for 'calculate' in Python files")

# Step 2: Find exact definitions
code_results = console.chat("Find the calculate_total function definition")

# Step 3: Find all usages
ref_results = console.chat("Find all usages of calculate_total")

Pattern 2: Programmatic Search Workflow

from consoul.ai.tools import grep_search, code_search, find_references
import json

def analyze_function(function_name: str):
    """Complete analysis of a function."""

    # 1. Find definition
    definition = code_search.invoke({
        "query": function_name,
        "symbol_type": "function"
    })
    def_data = json.loads(definition)

    if not def_data:
        print(f"Function {function_name} not found")
        return

    # 2. Find references
    references = find_references.invoke({
        "symbol": function_name,
        "scope": "project"
    })
    ref_data = json.loads(references)

    # 3. Report
    print(f"Function: {function_name}")
    print(f"Defined at: {def_data[0]['file']}:{def_data[0]['line']}")
    print(f"Used {len(ref_data)} times")

    return {
        "definition": def_data[0],
        "references": ref_data,
        "usage_count": len(ref_data)
    }

# Use it
analyze_function("bash_execute")
from consoul.ai.tools import ToolRegistry, RiskLevel
from consoul.ai.tools.providers import CliApprovalProvider
from consoul.ai.tools import grep_search, code_search, find_references

# All search tools are SAFE - can auto-approve
registry = ToolRegistry(
    config=config.tools,
    approval_provider=CliApprovalProvider()
)

# Register search tools (all SAFE)
registry.register(grep_search, risk_level=RiskLevel.SAFE)
registry.register(code_search, risk_level=RiskLevel.SAFE)
registry.register(find_references, risk_level=RiskLevel.SAFE)

# Bind to model for AI usage
model = registry.bind_to_model(model)

Pattern 4: Batch Analysis

def find_unused_functions(directory: str):
    """Find functions with zero references."""

    # Get all functions
    all_functions = code_search.invoke({
        "symbol_type": "function",
        "path": directory
    })

    functions = json.loads(all_functions)
    unused = []

    for func in functions:
        # Check for usages
        refs = find_references.invoke({
            "symbol": func["name"],
            "scope": "directory",
            "path": directory
        })

        ref_count = len(json.loads(refs))
        if ref_count == 0:
            unused.append(func)

    return unused

# Find unused functions in src/
unused = find_unused_functions("src/")
print(f"Found {len(unused)} unused functions")

Best Practices

1. Choose the Right Tool

✅ DO:

  • Use grep_search for comments, strings, and documentation
  • Use code_search to find definitions
  • Use find_references to find usages
  • Combine tools for comprehensive analysis

❌ DON'T:

  • Use grep_search for finding function calls (use find_references)
  • Use code_search for text in comments (use grep_search)
  • Search entire project when you know the directory

2. Leverage Caching

✅ DO:

  • Run searches on same files multiple times to benefit from cache
  • Use code_search before find_references (shares cache)
  • Keep searches in same session for cache benefit

❌ DON'T:

  • Clear cache unnecessarily
  • Run searches in separate processes (cache not shared)

3. Scope Appropriately

✅ DO:

  • Use file scope when you know the file
  • Use directory scope for package/module searches
  • Use project scope only when necessary

❌ DON'T:

  • Default to project scope for everything
  • Search entire codebase for file-specific questions

4. Handle Results Properly

✅ DO:

  • Parse JSON results before using
  • Check for empty results
  • Handle errors gracefully
  • Validate file paths exist

❌ DON'T:

  • Assume results are always valid
  • Ignore empty result sets
  • Skip error handling

5. Performance Optimization

✅ DO:

  • Install ripgrep for faster grep_search
  • Use specific symbol_type filters
  • Warm cache for frequently searched directories
  • Use case-sensitive search when possible

❌ DON'T:

  • Search with overly broad patterns
  • Disable caching
  • Search large binary files
  • Use case-insensitive unless needed

Troubleshooting

Common issues and solutions for Consoul's code search tools.

Tool Not Found Errors

Error: "Tool 'grep_search' not found in registry"

Cause: Tool not registered with ToolRegistry

Solution:

from consoul.ai.tools import ToolRegistry, grep_search, RiskLevel

registry = ToolRegistry(config=config.tools)
registry.register(grep_search, risk_level=RiskLevel.SAFE)

In TUI: Ensure tools are enabled in config:

profiles:
  default:
    tools:
      enabled: true

Error: "ToolExecutionError: ripgrep not found"

Cause: grep_search prefers ripgrep but can't find it

Solution: Install ripgrep or use grep fallback:

# macOS
brew install ripgrep

# Ubuntu/Debian
apt-get install ripgrep

# Windows (Scoop)
scoop install ripgrep

Fallback: grep_search automatically falls back to standard grep if ripgrep not found.

Language Not Supported

Error: "Unsupported language for file: *.xyz"

Cause: File extension not recognized by tree-sitter

Supported Extensions:

  • Python: .py
  • JavaScript: .js, .jsx
  • TypeScript: .ts, .tsx
  • Go: .go
  • Rust: .rs
  • Java: .java
  • C/C++: .c, .cpp, .h, .hpp

Solution: Only use code_search and find_references on supported file types. Use grep_search for other files.

How to Check Language Support

from grep_ast import filename_to_lang

# Check if file is supported
lang = filename_to_lang("myfile.py")
if lang:
    print(f"Supported: {lang}")
else:
    print("Not supported - use grep_search instead")

Cache Issues

Cache Not Working - Still Slow on Repeat Searches

Symptoms: code_search and find_references always take full parse time

Diagnosis:

from consoul.ai.tools.cache import CodeSearchCache

cache = CodeSearchCache()
stats = cache.get_stats()
print(f"Hit rate: {stats.hit_rate:.1%}")  # Should be >50% after warmup
print(f"Hits: {stats.hits}, Misses: {stats.misses}")

Causes & Solutions:

1. Cache directory not writable

from pathlib import Path

cache_dir = Path.home() / ".consoul" / "cache" / "code-search.v1"
print(cache_dir.exists())  # Should be True
print(os.access(cache_dir, os.W_OK))  # Should be True

# Fix permissions
cache_dir.mkdir(parents=True, exist_ok=True)

2. Files changing between searches

# Check file modification times
stat src/file.py  # mtime should be stable

3. Running in different processes

  • Cache is per-process
  • Solution: Keep searches in same Python session

4. Cache manually cleared

# Don't do this unless necessary
cache.invalidate_cache()

Cache Growing Too Large

Check cache size:

cache = CodeSearchCache()
stats = cache.get_stats()
print(f"Cache size: {stats.size_bytes / 1024 / 1024:.1f} MB")

Solution: Configure smaller size limit:

cache = CodeSearchCache(size_limit_mb=50)  # Default is 100MB

Manual cleanup:

cache.invalidate_cache()  # Clears all entries

Performance Problems

Searches Taking Too Long (>10s)

Diagnosis: 1. Check project size:

find . -name "*.py" | wc -l  # How many files?
du -sh .  # Total size

  1. Check if cache is being used:
    stats = cache.get_stats()
    print(f"Hit rate: {stats.hit_rate:.1%}")  # Should be >0% on repeat
    

Solutions:

1. Use narrower scope

# Slow - searches whole project
find_references.invoke({"symbol": "foo", "scope": "project"})

# Faster - search one directory
find_references.invoke({"symbol": "foo", "scope": "directory", "path": "src/"})

# Fastest - search one file
find_references.invoke({"symbol": "foo", "scope": "file", "path": "src/main.py"})

2. Increase file size limit

tools:
  code_search:
    max_file_size_kb: 2048  # Default is 1024 (1MB)

3. Skip large generated files Add to .gitignore or filter:

# Skip build directories
code_search.invoke({"query": "Foo", "path": "src/"})  # Not build/

4. Use grep_search for simple patterns

# Fast text search instead of AST
grep_search.invoke({"pattern": "MyClass"})

grep_search Slower Than Expected

Diagnosis:

which rg  # Should find ripgrep
rg --version  # Check version

Solution: Install ripgrep for 5-10x speedup:

brew install ripgrep  # macOS
apt-get install ripgrep  # Ubuntu

Fallback performance: Standard grep is slower but still functional.

Parsing Errors

Error: "Failed to parse AST"

Symptoms:

  • code_search returns empty results
  • find_references shows warning: "Failed to parse file.py"

Causes:

1. Syntax errors in source file

# File has invalid Python syntax
def foo(  # Missing closing paren
    return 42

Solution: Fix syntax errors in source files. tree-sitter requires valid syntax.

2. Tree-sitter language pack issue

# Check if tree-sitter-language-pack is installed
python -c "import tree_sitter_language_pack; print('OK')"

Solution: Reinstall dependency:

pip install --force-reinstall tree-sitter-language-pack>=0.11.0

3. Encoding issues

# File isn't UTF-8
result = file_path.read_text(encoding="utf-8", errors="ignore")

Solution: Convert file to UTF-8 or let tools ignore decode errors (automatic).

Error: "Failed to get parser for language"

Cause: Language not supported by tree-sitter

Solution: Check supported languages and use grep_search for unsupported files.

Empty Results

code_search Returns No Results

Diagnosis Checklist:

  1. Verify file exists:

    ls -la path/to/file.py
    

  2. Check file isn't too large:

    # Default limit is 1MB
    file_size_kb = Path("file.py").stat().st_size / 1024
    print(f"Size: {file_size_kb:.1f} KB")  # Should be <1024
    

  3. Verify symbol exists:

    # Use grep to confirm symbol is in file
    grep_search.invoke({"pattern": "MyClass", "path": "file.py"})
    

  4. Check symbol type:

    # Maybe it's a method, not a function
    code_search.invoke({"query": "MyClass", "symbol_type": "method"})
    

  5. Try case-insensitive:

    code_search.invoke({"query": "myclass", "case_sensitive": False})
    

Common Issues:

Symbol is in comment/string:

  • code_search ignores comments and strings
  • Solution: Use grep_search instead

Symbol is imported, not defined:

  • code_search finds definitions, not imports
  • Solution: Use find_references to find imports

Symbol is in unsupported file:

  • Check language support matrix
  • Solution: Use grep_search for unsupported languages

find_references Returns No Results

Diagnosis:

  1. Symbol might not be used:

    # Check if symbol is defined but unused (dead code)
    code_search.invoke({"query": "MyClass"})  # Finds definition
    find_references.invoke({"symbol": "MyClass"})  # Finds usages
    

  2. Symbol name might be different:

    # Case-sensitive by default
    find_references.invoke({"symbol": "myclass", "case_sensitive": False})
    

  3. Symbol only used in comments:

  4. find_references ignores comments

  5. Solution: Use grep_search

  6. Scope too narrow:

    # Maybe references are in other directories
    find_references.invoke({"symbol": "Foo", "scope": "project"})  # Wider
    

Permission Errors

Error: "Permission denied"

Cause: No read access to file or directory

Solution:

# Check permissions
ls -la path/to/file

# Fix permissions
chmod +r path/to/file

Error: "Failed to access cache directory"

Cause: Cache directory not writable

Solution:

# Create cache directory with correct permissions
mkdir -p ~/.consoul/cache/code-search.v1
chmod 755 ~/.consoul/cache/code-search.v1

Debug Mode

Enable debug logging to troubleshoot issues:

import logging

# Enable debug logging
logging.basicConfig(level=logging.DEBUG)

# Or configure Consoul logging
from consoul.ai.tools.implementations import code_search

# Run with debug output
result = code_search.invoke({"query": "Foo"})
# Will show detailed parsing info

Getting Help

If issues persist:

  1. Check version:

    import consoul
    print(consoul.__version__)
    

  2. Verify dependencies:

    pip list | grep -E "tree-sitter|grep-ast|ripgrep"
    

  3. Minimal reproduction:

    from consoul.ai.tools import code_search
    
    # Simplest possible test
    result = code_search.invoke({"query": ".*", "path": "."})
    print(result)
    

  4. Report issue:

  5. GitHub: https://github.com/goatbytes/consoul/issues

  6. Include: Python version, OS, error message, minimal reproduction

Quick Reference

Issue Quick Fix
Tool not found Enable tools in config.yaml
Language not supported Use grep_search instead
Cache not working Check ~/.consoul/cache permissions
Too slow Use narrower scope, install ripgrep
Parsing errors Check file syntax, verify UTF-8
Empty results Check file size, try case-insensitive
Permission denied Fix file/directory permissions

See Also

Other Tools:

SDK & API:

Configuration: