# tyf — Full Documentation # 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](https://github.com/astral-sh/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](https://github.com/astral-sh/ty) to be installed and on PATH. ```bash # 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`](llms.txt) and [`llms-full.txt`](llms-full.txt), following the [llms.txt](https://llmstxt.org) 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: ```markdown ### 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 --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`: ```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: ```bash # 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 ```mermaid graph TB subgraph Terminal CLI["tyf CLI
parse args · format output"] 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
workspace A"] LB["ty lsp
workspace B"] end CLI -- "JSON-RPC 2.0
Unix socket" --> Router Router --> CA Router --> CB CA -- "LSP protocol
stdin/stdout" --> LA CB -- "LSP protocol
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`: ```mermaid 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
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 `, the CLI sends a `definition` request instead, and the daemon uses `textDocument/definition` to 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: 1. Spawn and initialize 2. Index the Python project (parse files, resolve imports, build type information) 3. 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. ```mermaid 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 ``` ```mermaid 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: ```mermaid flowchart TD A["tyf find foo"] --> B{"Is daemon
running?"} B -- No --> C["Spawn daemon"] C --> D["Wait for ready"] D --> G["Send request"] B -- Yes --> E["Ping daemon"] E --> F{"Version
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 lsp` process is terminated). - **Daemon-wide**: If all workspace clients are idle, the daemon shuts itself down. ```mermaid 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. ```mermaid flowchart TD R["Incoming request
workspace: /home/user/project"] --> L{"Lookup workspace
in HashMap
(lock held)"} L -- Hit --> RET["Return existing client"] L -- Miss --> REL["Release lock"] REL --> SPAWN["Spawn ty lsp
Initialize LSP
(async, no lock held)"] SPAWN --> RELOCK["Re-acquire lock"] RELOCK --> CHECK{"Check again
(another task may
have created it)
"} 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](https://microsoft.github.io/language-server-protocol/). Messages use the same `Content-Length` framing but carry standard LSP methods like `textDocument/definition` and `textDocument/hover`. ```mermaid 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`. ```mermaid 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. ```mermaid 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`: ```mermaid flowchart TD Q["Query for src/main.py:10:5"] --> C{"URI in
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: ```mermaid 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] ``` ## 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_COLOR` environment variable. ## Commands **[show](show.md)** : Definition, type signature, and usages of a symbol by name **[find](find.md)** : Find where a symbol is defined by name (--fuzzy for partial matching) **[refs](refs.md)** : All usages of a symbol across the codebase (by name or file:line:col) **[members](members.md)** : Public interface of a class: methods, properties, and class variables **[list](list.md)** : All functions, classes, and variables defined in a file **[daemon](daemon.md)** : 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 inspect` still works as a hidden alias for `tyf 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 [OPTIONS] ``` ## Arguments **``** *(required)* : Symbol name(s) to show. Use `Class.method` to 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 ```bash # 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 - [Commands Overview](overview.md) --- # 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 [OPTIONS] ``` ## Arguments **``** *(required)* : Symbol name(s) to find. Use `Class.method` to 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 ```bash # 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 - [Commands Overview](overview.md) --- # 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 -l -c # Symbol mode (parallel search) tyf refs ... [-f ] # Stdin mode (pipe positions or symbol names) ... | tyf refs --stdin ``` ## Arguments | Argument | Description | |----------|-------------| | `...` | 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 ```bash # 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 - [Commands Overview](overview.md) --- # 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 [OPTIONS] ``` ## Arguments **``** *(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 ```bash # 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](overview.md) - [show](show.md) -- for definition, type, and usages of any symbol - [list](list.md) -- 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 ``` ## Arguments **``** *(required)* : ## Examples ```bash # List all symbols in a file tyf list main.py ``` ## See also - [Commands Overview](overview.md) --- # 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 ```bash # 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 - [Commands Overview](overview.md) --- # 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](https://github.com/astral-sh/ty) to be installed and on PATH. Install it with: ```bash 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: ```bash tyf daemon status ``` For more detail, enable debug logging: ```bash 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: ```bash 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: 1. Starts the background daemon process. 2. Spawns the ty LSP server. 3. 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 `.py` file 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: ```bash RUST_LOG=ty_find=debug tyf show MySymbol ``` This shows the LSP messages exchanged with ty, which helps diagnose protocol-level issues. ---