AST-aware code intelligence for Hermes Agent β tree-sitter + ast-grep + LSP
Add semantic code understanding to Hermes without forking the core repo. This plugin gives the agent 5 tools that understand your code's structure, not just its text β making it dramatically more token-efficient and accurate when navigating, searching, and refactoring codebases.
Hermes ships with search_files (regex grep) and read_file (raw text). Those work, but they're blind to syntax β they match comments, strings, and formatting equally. This plugin adds:
- Symbol extraction β get all functions, classes, methods with signatures and line numbers (without reading the whole file)
- Structural search β find imports, decorators, function calls, try/catch blocks by AST node type, not regex
- Safe refactoring β rename patterns, wrap functions, add parameters across files. Dry-run by default β preview changes before applying
- Go-to-definition β LSP-powered jump to where a symbol is defined (falls back to AST if no LSP server)
- Find all references β LSP-powered cross-file usage search (falls back to AST)
The result: 10β50x fewer tokens for code navigation tasks and far fewer false-positive matches.
| Tool | What it does | Replaces |
|---|---|---|
code_symbols |
Extract functions, classes, methods, interfaces, enums, structs from any file. Returns signatures + line numbers. | Reading entire files just to see "what's in here?" |
code_search |
Tree-sitter query-based structural search. Find function calls, imports, decorators, return statements, assignments by their semantic meaning. | search_files / grep for code patterns |
code_refactor |
ast-grep structural search-and-replace. Matches by AST structure, not raw text. Supports meta-variables ($NAME, $$BODY). |
patch / sed for structural changes |
code_definition |
LSP go-to-definition. Falls back to tree-sitter AST analysis if no language server. | Manual grep for symbol definitions |
code_references |
LSP find-all-references. Falls back to tree-sitter AST analysis if no language server. | Manual grep for symbol usages |
The plugin automatically injects hints into the built-in tool descriptions, so the agent naturally prefers the AST tools:
read_fileβ "prefer code_symbols to understand what a file contains"search_filesβ "prefer code_search for structural code patterns"patchβ "prefer code_refactor for AST-aware structural replacement"
No prompt changes needed β it just works.
Make sure the plugin is enabled and the toolset is registered in your Hermes config:
# ~/.hermes/config.yaml
plugins:
enabled:
- code_intel
# code_intel is auto-injected into core toolsets on plugin load.
# Verify it's present for your platform:
platform_toolsets:
cli:
- ...existing...
- code_intel
discord:
- ...existing...
- code_intel
# Subagents inherit toolsets β ensure code_intel is in the delegation defaults:
delegation:
default_toolsets:
- terminal
- file
- code_intelhermes plugins install rewasa/hermes-code-intel-plugin
hermes plugins enable code_intel# Clone into your plugins directory
git clone https://github.com/rewasa/hermes-code-intel-plugin.git ~/.hermes/plugins/code_intel
# Enable in your config
hermes plugins enable code_intelOr add to ~/.hermes/config.yaml:
plugins:
enabled:
- code_intelThe plugin uses tree-sitter and ast-grep under the hood. Install them in your Hermes venv:
cd ~/.hermes/hermes-agent
source venv/bin/activate
pip install tree-sitter tree-sitter-languages ast-grep-pyLSP tools (
code_definition,code_references) work without additional setup β they fall back to AST analysis when no language server is available. For full LSP support, install your preferred language server:# Python (default β tried first) pip install pyright # TypeScript / JavaScript npm install -g typescript-language-server typescript # Rust rustup component add rust-analyzer # Go go install golang.org/x/tools/gopls@latestThe plugin auto-discovers servers via PATH, monorepo
node_modules/.bin, andnpxfallback. No additional configuration needed.
The plugin automatically detects monorepo roots by scanning for pnpm-workspace.yaml, nx.json, or lerna.json. When found:
- Workspace folders are parsed (e.g.
apps/*,packages/*,modules/*) and sent to the LSP server during initialization - This enables cross-workspace type resolution β e.g. resolving
@agentselly/loggerimports across package boundaries - Works out of the box with pnpm, Nx, and Lerna monorepos β no config needed
The workspace folder list is cached per project root and cleared on shutdown.
The TypeScript LSP integration has several smart behaviors for monorepo setups:
-
tsconfig root detection β Instead of using the monorepo root as
rootUri(which confuses TSServer with 60+ workspace folders), the plugin finds the nearesttsconfig.jsondirectory. This gives accurate cross-file resolution within a single app while keeping monorepo folders asworkspaceFolders. -
typeDefinition fallback β When
go-to-definitionon an import identifier returns the import binding itself (a TSServer quirk), the plugin automatically triestextDocument/typeDefinitionto jump to the actual class/interface definition. -
Initialization retry β TS language server sometimes returns empty results on the first request (still indexing). The plugin retries once after 500ms for TS/JS files.
| Language | Extensions | Tree-sitter | ast-grep | LSP |
|---|---|---|---|---|
| Python | .py, .pyi |
β | β | β (pyright) |
| JavaScript | .js, .jsx |
β | β | β |
| TypeScript | .ts |
β | β | β (tsls) |
| TSX | .tsx |
β | β | β (tsls) |
| Rust | .rs |
β | β | β (rust-analyzer) |
| Go | .go |
β | β | β (gopls) |
| Java | .java |
β | β | β (jdtls) |
| C | .c, .h |
β | β | β (clangd) |
| C++ | .cpp |
β | β | β (clangd) |
Once enabled, you get a /code-intel command in CLI and gateway sessions:
/code-intel status β Show AST symbol cache status
/code-intel clear β Clear the AST symbol cache (free memory)
/code-intel help β Show usage
code_intel.py β tree-sitter tools (code_symbols, code_search, code_refactor)
lsp_bridge.py β LSP tools (code_definition, code_references)
__init__.py β plugin registration, steering hints, hooks
LSP bridges are keyed by (language_id, workspace_root) and pooled with LRU eviction:
- Max 8 concurrent bridges β supports multi-language monorepos (Python + TypeScript + Go, etc.)
- Lazy creation β bridges start on first use, not on plugin load
- Auto-eviction β oldest idle bridge is shut down when the pool is full
- Server fallback chain β e.g.
pyright-langserverβpylspfor Python; first available server wins - All bridges are cleaned up on session end via the
on_session_endhook
For monorepo projects, the plugin detects root markers (pnpm-workspace.yaml, nx.json, lerna.json) separately from generic markers (.git, package.json). This prevents false stops at nested apps/*/package.json files. Discovered workspace folders are parsed and sent to the LSP server during initialization for full cross-workspace intelligence.
Parsed AST results are cached in memory (OrderedDict, max 2000 entries, LRU eviction). The cache is automatically cleared at session end via the on_session_end hook β no memory leaks during long-running gateway sessions.
On startup, the plugin dynamically injects into:
toolsets._HERMES_CORE_TOOLS(available on all platforms)toolsets.TOOLSETS["hermes-acp"](ACP / VS Code / JetBrains)toolsets.TOOLSETS["hermes-api-server"](API server mode)
cd ~/.hermes/plugins/code_intel
# Run tests (uses Hermes venv for tree-sitter dependencies)
PYTHONPATH=~/.hermes/plugins ~/.hermes/hermes-agent/venv/bin/python3 \
-m pytest tests/test_code_intel.py -v
# Run a single test
PYTHONPATH=~/.hermes/plugins ~/.hermes/hermes-agent/venv/bin/python3 \
-m pytest tests/test_code_intel.py::test_extract_symbols_python -vBefore (reading a 500-line file to find a function):
β read_file("src/service.py") β 500 lines, ~8000 tokens
After (using code_symbols):
β code_symbols("src/service.py")
β {"symbols": [
{"name": "processOrder", "kind": "function", "line": 42,
"signature": "def processOrder(order_id: str, priority: int = 0) -> dict"},
{"name": "OrderService", "kind": "class", "line": 120,
"signature": "class OrderService"},
{"name": "validate", "kind": "method", "line": 145,
"signature": "def validate(self, order: Order) -> bool"}
]}
β ~200 tokens (40x savings)
Benchmarks from a real pnpm monorepo (~60 workspace folders). Tests performed with typescript-language-server v5.1.3 on Apple Silicon.
| Tool | Scenario | Time | Output Tokens |
|---|---|---|---|
code_definition |
Import binding β typeDefinition fallback | ~1.5s (first request) | ~272 |
code_definition |
Cached request | ~0.65s | ~290 |
code_definition |
External module (NestFactory) | ~0.65s | ~288 |
code_references |
Small class (DealsController) | ~0.67s | ~1,362 |
code_references |
Medium class (PropertyStatsService) | ~0.66s | ~2,610 |
Key observations:
- First request penalty (~1.5s) only for import identifiers that trigger the typeDefinition fallback
- Cross-file references work correctly: 3 refs in 2 files, 6 refs in 3 files (verified against real NestJS monorepo)
- Token efficiency: definition results ~270-290 tokens, references scale with usage count
- No LSP startup delay: bridges are lazily created and pooled (max 8 concurrent)
Contributions welcome! This is a community plugin β PRs for new languages, better LSP fallbacks, or caching improvements are appreciated.
- Fork the repo
- Create a feature branch
- Add tests for your changes
- Open a PR
MIT β use it however you like.
- Hermes Agent by Nous Research β the plugin system this builds on
- tree-sitter β incremental parsing system
- ast-grep β pattern-based code search and replacement
- pyright β Python LSP server (fallback)
- typescript-language-server β TypeScript/JavaScript LSP server
- tsserver β TypeScript language service (used by typescript-language-server)