What is ty-find?
ty-find (tyf) is an LSP adapter that lets AI coding agents query Python’s type system by symbol name. It wraps ty’s LSP server so that tyf show MyClass returns the definition location, type signature, and all references — without requiring file paths or line numbers. A background daemon keeps responses under 100ms.
Built for Claude Code, Codex, Cursor, Gemini CLI — and humans who want fast Python navigation from the terminal.
Why tyf?
vs grep/ripgrep
grep matches text. tyf understands Python’s type system.
When you grep for calculate_sum, you get hits in comments, docstrings, string literals, and variable names that happen to contain the substring. tyf returns only the actual symbol definition, its type signature, and where it’s referenced — because it uses ty’s type inference engine under the hood.
vs raw LSP (in editors)
LSP servers are the gold standard for code intelligence, but they require file positions (file.py:29:7) to answer queries. An LLM doesn’t know positions — it thinks in symbol names (MyClass). To use an LSP, it would first need to grep for the position, which is imprecise and adds a round-trip.
tyf breaks this cycle: symbol name in → structured LSP knowledge out. No file paths, no line numbers, no grep step needed.
Installation
ty-find requires ty to be installed and on PATH.
# Install ty (required)
uv add --dev ty
# Install ty-find
uv add --dev ty-find
Quick start
# Definition + signature (default)
$ tyf show list_animals
# Definition (func)
main.py:14:1
# Signature
def list_animals(animals: list[Animal]) -> None
# Refs: 2 across 1 file(s)
# Everything: docs + refs + test refs
$ tyf show list_animals --all
# See all commands
$ tyf --help
For AI agents
Machine-readable documentation is available at llms.txt and llms-full.txt, following the llms.txt convention.
Setup with Claude Code
The main use case for ty-find is giving AI coding agents precise Python symbol navigation. This page explains how to configure Claude Code to prefer tyf over grep for symbol lookups.
CLAUDE.md snippet
Add this to your project’s CLAUDE.md file:
### Python Symbol Navigation — `tyf`
This project has `tyf` — a type-aware code search that gives LSP-quality
results by symbol name. Use `tyf` instead of grep/ripgrep for Python symbol lookups.
- `tyf show my_function` — definition + signature (add `-d` docs, `-r` refs, `-t` test refs, or `--all`)
- `tyf find MyClass` — find definition location
- `tyf refs my_function` — all usages (before refactoring)
- `tyf members TheirClass` — class public API
- `tyf list file.py` — file outline
All commands accept multiple symbols — batch to save tool calls.
Run `tyf <cmd> --help` for options.
Use grep for: string literals, config values, TODOs, non-Python files.
Permissions
Claude Code will prompt you for permission the first time it tries to run tyf. To avoid repeated prompts, add a Bash permission rule in your project’s .claude/settings.json:
{
"permissions": {
"allow": [
"Bash(tyf:*)"
]
}
}
This allows Claude Code to run any tyf command without asking each time.
Why the strong language?
Claude Code’s system prompt tells it to use its built-in Grep tool for searching code. This is a sensible default — Grep works everywhere and requires no setup.
The problem goes deeper than precision. To use an LSP (the gold standard for code intelligence), an LLM first needs a file position — but it doesn’t know the position without searching. So it greps, gets imprecise results, and has to validate them — a circular round-trip that wastes tokens and time.
tyf breaks this cycle: the LLM passes a symbol name, and tyf resolves the position internally, returning structured LSP results directly. No grep step, no position guessing, no validation loop.
On top of that, grep is a text search tool — it returns false positives from comments, docstrings, and string literals. It can’t tell you a symbol’s type or find all references through the type system.
The CLAUDE.md snippet uses emphatic language (“Use tyf instead of grep”) because that’s what it takes to override a strong system-level preference. Softer phrasing like “consider using tyf” gets ignored in practice.
Priming a new session
In the first Claude Code session with a new project, you can prime Claude by asking it to run:
tyf --help
This helps Claude understand what commands are available and how to use them, making it more likely to reach for tyf over grep in subsequent interactions.
AGENTS.md for other tools
If you use Cursor, Codex, Gemini CLI, or another AI coding tool, the same instructions work — just put them in the file your tool reads:
| Tool | File |
|---|---|
| Claude Code | CLAUDE.md |
| Cursor | .cursorrules |
| Codex | AGENTS.md |
| Gemini CLI | GEMINI.md |
If you use multiple tools, you can maintain one file and symlink:
# Write instructions in CLAUDE.md, symlink for others
ln -s CLAUDE.md AGENTS.md
ln -s CLAUDE.md .cursorrules
How It Works
ty-find is built as a three-layer system: a thin CLI client, a persistent background daemon, and the ty LSP server that does the actual Python analysis. This page explains how they fit together and why.
Architecture overview
graph TB
subgraph Terminal
CLI["<b>tyf</b> CLI<br/><small>parse args · format output</small>"]
end
subgraph Daemon ["Daemon (background process)"]
Router["Request Router"]
subgraph Pool ["LSP Client Pool"]
CA["TyLspClient A"]
CB["TyLspClient B"]
end
end
subgraph LSP ["ty LSP servers"]
LA["ty lsp<br/><small>workspace A</small>"]
LB["ty lsp<br/><small>workspace B</small>"]
end
CLI -- "JSON-RPC 2.0<br/>Unix socket" --> Router
Router --> CA
Router --> CB
CA -- "LSP protocol<br/>stdin/stdout" --> LA
CB -- "LSP protocol<br/>stdin/stdout" --> LB
Each layer has a single responsibility:
| Layer | Responsibility |
|---|---|
CLI (tyf) | Parse arguments, connect to daemon, format output |
| Daemon | Keep LSP servers alive between calls, route requests |
| ty LSP | Python type analysis, symbol resolution, indexing |
Request lifecycle
Here’s what happens when you run tyf find calculate_sum:
sequenceDiagram
participant CLI as tyf CLI
participant D as Daemon
participant LSP as ty LSP server
CLI->>D: 1. Connect (Unix socket)
CLI->>D: 2. JSON-RPC "workspace_symbols" request
Note over D: 3. Look up workspace<br/>in client pool (hit → reuse)
D->>LSP: 4. workspace/symbol
LSP-->>D: 5. SymbolInformation[]
D-->>CLI: 6. JSON-RPC response
Note over CLI: 7. Format & print results
Note: The diagram above shows the default project-wide search path. When using
tyf find --file <path>, the CLI sends adefinitionrequest instead, and the daemon usestextDocument/definitionto resolve the symbol at a specific file position.
Steps 1–7 take 50–100 ms on a warm daemon. Without the daemon, every call would pay the full LSP startup cost (several seconds).
The daemon
The daemon is a long-running background process that listens on a Unix domain socket at /tmp/ty-find-{uid}.sock. It starts automatically on first use and shuts itself down after 5 minutes of inactivity.
Why a daemon?
Starting an LSP server is expensive. The ty LSP process needs to:
- Spawn and initialize
- Index the Python project (parse files, resolve imports, build type information)
- Reach a “ready” state where it can answer queries
This takes 1–5 seconds depending on project size. The daemon pays this cost once and keeps the server running for subsequent calls.
gantt
title Without daemon — cold start every time
dateFormat X
axisFormat %s
section tyf find foo
spawn ty lsp :a1, 0, 800
LSP initialize :a2, 800, 1000
index project :a3, 1000, 3000
LSP query :a4, 3000, 3050
format output :a5, 3050, 3060
section tyf find bar
spawn ty lsp :b1, 3060, 3860
LSP initialize :b2, 3860, 4060
index project :b3, 4060, 6060
LSP query :b4, 6060, 6110
format output :b5, 6110, 6120
gantt
title With daemon — warm after first call
dateFormat X
axisFormat %s
section tyf find foo
connect to daemon :a1, 0, 20
send request :a2, 20, 30
LSP query :a3, 30, 430
format output :a4, 430, 440
section tyf find bar
connect to daemon :b1, 440, 460
send request :b2, 460, 470
LSP query :b3, 470, 900
format output :b4, 900, 910
Auto-start and version checking
The CLI automatically manages the daemon lifecycle:
flowchart TD
A["tyf find foo"] --> B{"Is daemon<br/>running?"}
B -- No --> C["Spawn daemon"]
C --> D["Wait for ready"]
D --> G["Send request"]
B -- Yes --> E["Ping daemon"]
E --> F{"Version<br/>matches?"}
F -- Yes --> G
F -- No --> H["Stop old daemon"]
H --> C
When you upgrade ty-find, the CLI detects that the running daemon is from an older version and restarts it automatically.
Idle shutdown
The daemon tracks activity at two levels:
- Per-workspace: Each LSP client records its last access time. Clients idle for more than 5 minutes are cleaned up (the
ty lspprocess is terminated). - Daemon-wide: If all workspace clients are idle, the daemon shuts itself down.
timeline
title Daemon lifecycle example
0 sec : tyf find foo
: Daemon starts
: Workspace A client created
2 sec : tyf refs bar
: Workspace A client reused
30 sec : tyf find baz
: Workspace A client reused
: Idle timer reset
5 min 30 sec : No activity for 5 min
: Workspace A client removed
: No clients remain
: Daemon shuts down
LSP client pool
The daemon maintains a pool of LSP clients, one per workspace. When a request arrives, the daemon resolves it to a workspace root and looks up the corresponding client.
flowchart TD
R["Incoming request<br/><code>workspace: /home/user/project</code>"] --> L{"Lookup workspace<br/>in HashMap<br/><small>(lock held)</small>"}
L -- Hit --> RET["Return existing client"]
L -- Miss --> REL["Release lock"]
REL --> SPAWN["Spawn ty lsp<br/>Initialize LSP<br/><small>(async, no lock held)</small>"]
SPAWN --> RELOCK["Re-acquire lock"]
RELOCK --> CHECK{"Check again<br/><small>(another task may<br/>have created it)</small>"}
CHECK -- Already exists --> RET
CHECK -- Still missing --> INS["Insert new client"] --> RET
The pool uses a lock-free fast path pattern: the std::sync::Mutex is held only for the HashMap lookup (microseconds), then dropped before any async work. This avoids holding a lock across .await, which would block other tasks.
Communication protocols
CLI ↔ Daemon: JSON-RPC 2.0 over Unix socket
The CLI and daemon communicate using JSON-RPC 2.0 with LSP-style message framing:
Content-Length: 128\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"definition","params":{...}}
Available RPC methods:
| Method | Description |
|---|---|
ping | Health check (returns version and uptime) |
shutdown | Gracefully stop the daemon |
definition | Go to definition of a symbol at a position |
hover | Get type information for a symbol at a position |
references | Find all references to a symbol |
batch_references | Find references for multiple symbols in one call |
workspace_symbols | Search for symbols by name across the workspace |
document_symbols | List all symbols in a file |
inspect | Combined hover + references (definitions resolved client-side via workspace symbols) |
members | Public interface of a class |
diagnostics | Type errors in a file |
Daemon ↔ ty LSP: LSP protocol over stdin/stdout
The daemon communicates with each ty lsp process using the standard Language Server Protocol. Messages use the same Content-Length framing but carry standard LSP methods like textDocument/definition and textDocument/hover.
sequenceDiagram
participant D as Daemon
participant LSP as ty lsp (child process)
D->>LSP: stdin: LSP request (JSON-RPC 2.0)
LSP-->>D: stdout: LSP response
Response routing works through request IDs: each outgoing request gets a unique integer ID (from an AtomicU64). A background task reads responses from stdout and matches them to pending requests using a HashMap<u64, oneshot::Sender>.
sequenceDiagram
participant Caller as send_request()
participant Map as pending_requests
participant Stdin as stdin (to ty)
participant Handler as response_handler
participant Stdout as stdout (from ty)
Caller->>Map: store tx for id=42
Caller->>Stdin: write JSON-RPC {id: 42, ...}
Note over Caller: await rx...
Stdout-->>Handler: read {id: 42, result: ...}
Handler->>Map: remove(42) → tx
Handler->>Caller: tx.send(response)
Note over Caller: unblocked!
Concurrency model
All parallelism is handled by the daemon, not the CLI:
- The LSP protocol runs over a single stdin/stdout pipe per server, so requests are inherently sequential.
- Multi-symbol operations (like
tyf show A B C) are sent as a single batch RPC call. The daemon processes them sequentially on its LSP client and returns merged results. - The CLI never spawns multiple connections or concurrent requests. This keeps the architecture simple and avoids race conditions.
sequenceDiagram
participant CLI as tyf CLI
participant D as Daemon
participant LSP as ty LSP
CLI->>D: show [A, B, C]
D->>LSP: hover(A)
LSP-->>D: result A
D->>LSP: hover(B)
LSP-->>D: result B
D->>LSP: hover(C)
LSP-->>D: result C
D-->>CLI: [results for A, B, C]
Document tracking
The LSP protocol requires that a client sends textDocument/didOpen before querying a file, and only sends it once per file per session. The LSP client tracks opened documents in a HashSet<String>:
flowchart TD
Q["Query for src/main.py:10:5"] --> C{"URI in<br/>opened_documents?"}
C -- No --> O["Send didOpen"]
O --> ADD["Add URI to set"]
ADD --> DEF["Send textDocument/definition"]
C -- Yes --> DEF
Sending a duplicate didOpen would cause the LSP server to re-analyze the file, returning null results during the re-analysis window. The tracking set prevents this.
Warmup and retries
On a cold start, the LSP server may not be fully ready to answer queries even after initialization completes. The daemon handles this with automatic retries:
sequenceDiagram
participant D as Daemon
participant LSP as ty LSP
Note over D,LSP: First query after daemon start
D->>LSP: hover(symbol)
LSP-->>D: null (still indexing)
Note over D: wait 100ms
D->>LSP: hover(symbol)
LSP-->>D: null (still not ready)
Note over D: wait 200ms
D->>LSP: hover(symbol)
LSP-->>D: result ✓
Note over D: return result
Retries use exponential backoff (100ms, 200ms, 400ms, 800ms) and apply to all operations that can return empty or null results during warmup, including hover, workspace/symbol, definition, references, and documentSymbol.
Commands Overview
Type-aware Python code navigation (powered by ty)
Usage
tyf [OPTIONS] <COMMAND>
Global Options
--workspace- Project root (default: auto-detect)
-v, --verbose- Enable verbose output
--format- Output format: human (default), json, csv, or paths
--detail- Output detail level: condensed (token-efficient, default) or full (verbose)
--timeout- Timeout in seconds for daemon operations (default: 30)
--color- When to use colored output: auto (default), always, or never. Respects the
NO_COLORenvironment variable.
Commands
- show
- Definition, type signature, and usages of a symbol by name
- find
- Find where a symbol is defined by name (–fuzzy for partial matching)
- refs
- All usages of a symbol across the codebase (by name or file:line:col)
- members
- Public interface of a class: methods, properties, and class variables
- list
- All functions, classes, and variables defined in a file
- daemon
- Manage the background LSP server (auto-starts on first use)
show
Definition, type signature, and usages of a symbol — where it’s defined, its type signature, and optionally all usages. Searches the whole project by name, no file path needed.
Backward compatibility:
tyf inspectstill works as a hidden alias fortyf show.
Use Class.method dotted notation to narrow to a specific class member.
Examples: tyf show MyClass tyf show MyClass.get_data # narrow to a specific class method tyf show calculate_sum UserService # multiple symbols at once tyf show MyClass –references # also show all usages tyf show MyClass –doc # include docstring tyf show MyClass –all # show everything: doc + refs + test refs tyf show MyClass –file src/models.py # narrow to one file
Usage
tyf show <SYMBOLS> [OPTIONS]
Arguments
<symbols>(required)- Symbol name(s) to show. Use
Class.methodto narrow to a specific class.
Options
-f, --file- Narrow the search to a specific file (searches whole project if omitted)
-r, --references- Also find all references (can be slow on large codebases)
-d, --doc- Include the docstring in the output (off by default)
-a, --all- Show everything: docstring, references, and test references
Examples
# Show a single symbol
tyf show MyClass
# Show a specific class method (dotted notation)
tyf show MyClass.get_data
# Show multiple symbols at once
tyf show MyClass my_function
# Show a symbol in a specific file
tyf show MyClass --file src/module.py
# Include docstring
tyf show MyClass --doc
# Show everything (doc + refs + test refs)
tyf show MyClass --all
# Using the backward-compatible alias
tyf inspect MyClass
See also
find
Find where a function, class, or variable is defined. Searches the whole project by name — no need to know which file it’s in.
Use Class.method dotted notation to narrow to a specific class member.
Use --fuzzy for partial/prefix matching (returns richer symbol information including kind and container name).
Examples: tyf find calculate_sum tyf find Calculator.add # find a specific class method tyf find calculate_sum multiply divide # multiple symbols at once tyf find handler –file src/routes.py # narrow to one file tyf find handle_ –fuzzy # fuzzy/prefix match
Usage
tyf find <SYMBOLS> [OPTIONS]
Arguments
<symbols>(required)- Symbol name(s) to find. Use
Class.methodto narrow to a specific class.
Options
-f, --file- Narrow the search to a specific file (searches whole project if omitted)
--fuzzy- Use fuzzy/prefix matching via workspace symbols (richer output with kind + container)
Examples
# Find a single symbol
tyf find calculate_sum
# Find a specific class method (dotted notation)
tyf find Calculator.add
# Find multiple symbols at once
tyf find calculate_sum multiply divide
# Find a symbol in a specific file
tyf find my_function --file src/module.py
# Fuzzy/prefix match
tyf find handle_ --fuzzy
See also
refs
All usages of a symbol across the codebase. Useful before renaming or removing code to understand the impact.
Use Class.method dotted notation to narrow to a specific class member.
Usage
# Position mode (exact)
tyf refs -f <FILE> -l <LINE> -c <COLUMN>
# Symbol mode (parallel search)
tyf refs <QUERIES>... [-f <FILE>]
# Stdin mode (pipe positions or symbol names)
... | tyf refs --stdin
Arguments
| Argument | Description |
|---|---|
<QUERIES>... | Symbol names or file:line:col positions (auto-detected) |
Options
| Option | Description |
|---|---|
-f, --file | File path (required for position mode, optional for symbol mode) |
-l, --line | Line number (position mode, requires –file and –column) |
-c, --column | Column number (position mode, requires –file and –line) |
--stdin | Read queries from stdin (one per line) |
--include-declaration | Include the declaration in the results |
Examples
# Position mode: exact location
tyf refs -f main.py -l 10 -c 5
# Symbol mode: find references by name
tyf refs my_function
# Symbol mode: dotted notation for a specific method
tyf refs Calculator.add
# Symbol mode: multiple symbols searched in parallel
tyf refs my_function MyClass calculate_sum
# Auto-detected file:line:col positions (parallel)
tyf refs main.py:10:5 utils.py:20:3
# Mixed: positions and symbols together
tyf refs main.py:10:5 my_function
# Pipe from list
tyf list file.py --format csv \
| awk -F, 'NR>1{printf "file.py:%s:%s\n",$3,$4}' \
| tyf refs --stdin
# Pipe symbol names
tyf list file.py --format csv \
| tail -n+2 | cut -d, -f1 \
| tyf refs --stdin
See also
members
Public interface of a class – methods with signatures, properties, and class variables with types. Like ‘list’ scoped to a class, with type info included.
Excludes private (_prefixed) and dunder (dunder) members by default; use –all to include everything.
Note: only shows members defined directly on the class, not inherited members.
Examples: tyf members MyClass tyf members MyClass UserService # multiple classes tyf members MyClass –all # include init, repr, etc tyf members MyClass -f src/models.py # narrow to one file
Usage
tyf members <SYMBOLS> [OPTIONS]
Arguments
<symbols>(required)- Class name(s) to show (supports multiple classes)
Options
-f, --file- Narrow the search to a specific file (searches whole project if omitted)
--all- Include dunder methods and private members (excluded by default)
Examples
# Show public interface of a class
tyf members MyClass
# Multiple classes at once
tyf members MyClass UserService
# Include dunder methods and private members
tyf members MyClass --all
# Narrow to a specific file
tyf members MyClass --file src/models.py
Output format
The default text output groups members by category:
MyClass (src/models.py:15:1)
Methods:
calculate_total(self, items: list[Item]) -> Decimal :42:5
validate(self) -> bool :58:5
Properties:
name: str :20:5
is_active: bool :23:5
Class variables:
MAX_RETRIES: int = 3 :16:5
Line/col references on the right allow jumping to the source.
Limitations
- Only shows members defined directly on the class, not inherited members (MRO traversal is not yet supported by ty’s LSP)
- Type signatures come from hover, so they require ty to have analyzed the file
See also
- Commands Overview
- show – for definition, type, and usages of any symbol
- list – for all symbols in a file
list
All functions, classes, and variables defined in a file — like a table of contents for your code.
Examples: tyf list src/services/user.py
Usage
tyf list <FILE>
Arguments
Examples
# List all symbols in a file
tyf list main.py
See also
daemon
Manage the background LSP server (auto-starts on first use)
Usage
tyf daemon
Subcommands
start- Start the background LSP server
stop- Stop the background LSP server
restart- Stop and restart the background LSP server
status- Show the daemon’s running status
Examples
# Start the background daemon
tyf daemon start
# Restart the daemon (e.g. after upgrading tyf)
tyf daemon restart
# Check daemon status
tyf daemon status
# Stop the daemon
tyf daemon stop
See also
Performance
ty-find uses a background daemon to keep the ty LSP server running between calls. The first call starts the daemon and waits for LSP initialization, which is slower. Subsequent calls reuse the running server and respond quickly.
Benchmarks
Benchmarks are run as part of CI. See tests/bench_* for the test code.
| Operation | First call (cold daemon) | Subsequent calls (warm) |
|---|---|---|
| show | TBD | TBD |
| find | TBD | TBD |
| refs | TBD | TBD |
These numbers will be filled in with real measurements from the benchmark suite.
What affects performance
- Project size: Larger Python projects take longer for the initial LSP indexing.
- Daemon state: Cold starts include daemon spawn + LSP initialization. Warm calls skip both.
- Disk I/O: First call after a file change triggers re-indexing by the LSP server.
Troubleshooting
“ty: command not found”
ty-find requires ty to be installed and on PATH. Install it with:
uv add --dev ty
If ty is installed but not on PATH, ty-find will attempt to run it via uvx ty as a fallback. If neither works, you’ll see this error.
Daemon won’t start
Check the daemon status:
tyf daemon status
For more detail, enable debug logging:
RUST_LOG=ty_find=debug tyf daemon start
Common causes:
- Another process is using the daemon’s socket.
- ty is not installed (see above).
- Permissions issue on the socket file.
Wrong or stale results
If tyf returns outdated definitions or missing references, the LSP server may have stale state. Restart the daemon:
tyf daemon stop && tyf daemon start
Then retry your query.
Slow first call
The first call in a session is expected to be slower because it:
- Starts the background daemon process.
- Spawns the ty LSP server.
- Waits for LSP initialization and project indexing.
Subsequent calls reuse the running daemon and typically respond in 50–100ms. If every call is slow, check that the daemon is staying alive between calls with tyf daemon status.
No results for a symbol that exists
- Verify the symbol is in a
.pyfile within the workspace. - Check that ty can analyze the file:
ty check file.py. - Some dynamic constructs (e.g.,
getattr, runtime-generated classes) are not visible to static analysis.
Debug logging
For any issue, enable full debug output:
RUST_LOG=ty_find=debug tyf show MySymbol
This shows the LSP messages exchanged with ty, which helps diagnose protocol-level issues.