Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ToolFile
Claude CodeCLAUDE.md
Cursor.cursorrules
CodexAGENTS.md
Gemini CLIGEMINI.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:

LayerResponsibility
CLI (tyf)Parse arguments, connect to daemon, format output
DaemonKeep LSP servers alive between calls, route requests
ty LSPPython 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 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.

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 lsp process 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:

MethodDescription
pingHealth check (returns version and uptime)
shutdownGracefully stop the daemon
definitionGo to definition of a symbol at a position
hoverGet type information for a symbol at a position
referencesFind all references to a symbol
batch_referencesFind references for multiple symbols in one call
workspace_symbolsSearch for symbols by name across the workspace
document_symbolsList all symbols in a file
inspectCombined hover + references (definitions resolved client-side via workspace symbols)
membersPublic interface of a class
diagnosticsType 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_COLOR environment 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 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 <SYMBOLS> [OPTIONS]

Arguments

<symbols> (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

# 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.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

# 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

ArgumentDescription
<QUERIES>...Symbol names or file:line:col positions (auto-detected)

Options

OptionDescription
-f, --fileFile path (required for position mode, optional for symbol mode)
-l, --lineLine number (position mode, requires –file and –column)
-c, --columnColumn number (position mode, requires –file and –line)
--stdinRead queries from stdin (one per line)
--include-declarationInclude 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

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

<file> (required)

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.

OperationFirst call (cold daemon)Subsequent calls (warm)
showTBDTBD
findTBDTBD
refsTBDTBD

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:

  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:

RUST_LOG=ty_find=debug tyf show MySymbol

This shows the LSP messages exchanged with ty, which helps diagnose protocol-level issues.