|
| 1 | +# CLAUDE.md — Agent Instructions for talk-python-cli |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +CLI client for the Talk Python to Me podcast and Talk Python Training courses. |
| 6 | +Wraps a remote MCP server (`https://talkpython.fm/api/mcp`) using JSON-RPC 2.0 |
| 7 | +over HTTP. The CLI is a **thin client** — all business logic lives on the server. |
| 8 | +The CLI handles argument parsing, HTTP transport, and output formatting. |
| 9 | + |
| 10 | +Published on PyPI as `talk-python-cli`. Entry point: `talkpython`. |
| 11 | + |
| 12 | +## Critical Rules |
| 13 | + |
| 14 | +- **Use `uv pip install`, never `pip install`.** |
| 15 | +- **Virtual environment is `./venv`, NOT `./.venv`.** |
| 16 | +- **After every code edit, run: `ruff format && ruff check --fix`** |
| 17 | +- **Use `pyrefly check` to validate type information after changes.** |
| 18 | +- Do not add unnecessary abstractions, comments, or docstrings to unchanged code. |
| 19 | + |
| 20 | +## Build & Run |
| 21 | + |
| 22 | +```bash |
| 23 | +# Activate venv |
| 24 | +source venv/bin/activate |
| 25 | + |
| 26 | +# Install in editable mode |
| 27 | +uv pip install -e ".[dev]" |
| 28 | + |
| 29 | +# Run CLI |
| 30 | +talkpython --help |
| 31 | +talkpython episodes search "fastapi" |
| 32 | +talkpython status |
| 33 | + |
| 34 | +# Run tests |
| 35 | +pytest |
| 36 | + |
| 37 | +# Lint & format (ALWAYS after edits) |
| 38 | +ruff format && ruff check --fix |
| 39 | + |
| 40 | +# Type check (ALWAYS after edits) |
| 41 | +pyrefly check |
| 42 | +``` |
| 43 | + |
| 44 | +## Project Structure |
| 45 | + |
| 46 | +``` |
| 47 | +src/talk_python_cli/ |
| 48 | + __init__.py # Version from importlib.metadata |
| 49 | + __main__.py # python -m entry point |
| 50 | + app.py # Root Cyclopts app, global options, meta-handler, status cmd |
| 51 | + client.py # MCPClient: httpx-based JSON-RPC 2.0 client |
| 52 | + formatting.py # Rich output: Markdown panels (text) or JSON |
| 53 | + episodes.py # Episode commands (search, get, list, recent, transcript) |
| 54 | + guests.py # Guest commands (search, get, list) |
| 55 | + courses.py # Course commands (search, get, list) |
| 56 | +
|
| 57 | +tests/ |
| 58 | + conftest.py # Shared fixtures, JSON-RPC response builders |
| 59 | + test_client.py # MCPClient tests |
| 60 | + test_episodes.py # Episode command tests |
| 61 | + test_guests.py # Guest command tests |
| 62 | + test_courses.py # Course command tests |
| 63 | +``` |
| 64 | + |
| 65 | +## Architecture & Key Patterns |
| 66 | + |
| 67 | +### CLI Framework: Cyclopts (not Click, not Typer) |
| 68 | + |
| 69 | +- Root app in `app.py` with sub-apps for episodes, guests, courses. |
| 70 | +- **Meta-app launcher** (`@app.meta.default`): intercepts all invocations to |
| 71 | + process global options (`--format`, `--url`) before dispatching to subcommands. |
| 72 | +- Parameters use `Annotated[type, cyclopts.Parameter(...)]` for docs/defaults. |
| 73 | +- Cyclopts auto-converts snake_case commands to kebab-case (e.g. `transcript_vtt` → `transcript-vtt`). |
| 74 | + |
| 75 | +### Client Pattern |
| 76 | + |
| 77 | +- `MCPClient` in `client.py` wraps httpx for JSON-RPC 2.0 over HTTP. |
| 78 | +- Lazy initialization: `_ensure_initialized()` runs MCP handshake on first call. |
| 79 | +- Session ID tracked via `Mcp-Session-Id` response header. |
| 80 | +- `call_tool(tool_name, arguments)` is the only public API for MCP tool calls. |
| 81 | +- Output format sent as URL query param: `?format=json` when JSON mode. |
| 82 | +- No authentication required (public API). |
| 83 | + |
| 84 | +### Lazy Client Access (avoids circular imports) |
| 85 | + |
| 86 | +Each command module retrieves the client via a local helper: |
| 87 | +```python |
| 88 | +def _client(): |
| 89 | + from talk_python_cli.app import get_client |
| 90 | + return get_client() |
| 91 | +``` |
| 92 | +This deferred import avoids circular dependency since `app.py` imports the command modules. |
| 93 | + |
| 94 | +### Output Formatting |
| 95 | + |
| 96 | +- `display(content, format)` in `formatting.py` routes to markdown or JSON renderer. |
| 97 | +- Text mode: Rich Markdown panel with "Talk Python" theme (cyan border, monokai code). |
| 98 | +- JSON mode on TTY: pretty-printed with syntax highlighting. |
| 99 | +- JSON mode piped: compact single-line JSON for scripting. |
| 100 | + |
| 101 | +### Adding a New Command |
| 102 | + |
| 103 | +1. Add function in the appropriate module (`episodes.py`, `guests.py`, `courses.py`). |
| 104 | +2. Decorate with `@sub_app.default` or just define as a regular function in the sub-app. |
| 105 | +3. Call `_client().call_tool('tool_name', {'arg': value})` to invoke the MCP tool. |
| 106 | +4. Pass result to `display(result, _client().output_format)`. |
| 107 | +5. Add tests in the corresponding test file using `pytest-httpx` mocks. |
| 108 | + |
| 109 | +### Adding a New Command Group |
| 110 | + |
| 111 | +1. Create `src/talk_python_cli/newgroup.py` with a `cyclopts.App(name='newgroup')`. |
| 112 | +2. Register in `app.py`: `app.command(newgroup.sub_app)`. |
| 113 | +3. Create `tests/test_newgroup.py`. |
| 114 | + |
| 115 | +## Testing |
| 116 | + |
| 117 | +- Framework: **pytest** with **pytest-httpx** for HTTP mocking. |
| 118 | +- `conftest.py` provides helpers: `jsonrpc_result()`, `tool_result()`, `add_init_responses()`. |
| 119 | +- Every test must call `add_init_responses(httpx_mock)` before making MCP client calls. |
| 120 | +- Tests verify JSON-RPC request structure, argument passing, and response handling. |
| 121 | + |
| 122 | +## Dependencies |
| 123 | + |
| 124 | +| Package | Purpose | |
| 125 | +|-------------|--------------------------------| |
| 126 | +| cyclopts | CLI framework (commands, args) | |
| 127 | +| httpx | HTTP client for MCP calls | |
| 128 | +| rich | Terminal output formatting | |
| 129 | +| pytest | Testing (dev) | |
| 130 | +| pytest-httpx| HTTP mocking in tests (dev) | |
| 131 | + |
| 132 | +Build system: **hatchling**. Package manager: **uv**. |
| 133 | + |
| 134 | +## Config Files |
| 135 | + |
| 136 | +- `pyproject.toml` — Package metadata, dependencies, entry points, build config |
| 137 | +- `ruff.toml` — Line length 120, single quotes, target Python 3.14 |
| 138 | +- `pyrefly.toml` — Type checker config, search path includes `src/` |
| 139 | +- `uv.lock` — Locked dependencies (committed) |
| 140 | + |
| 141 | +## Style Conventions |
| 142 | + |
| 143 | +- Line length: 120 |
| 144 | +- Quotes: single quotes |
| 145 | +- Modern Python type syntax: `dict | None` not `Optional[dict]` |
| 146 | +- `from __future__ import annotations` in all modules |
| 147 | +- Private helpers prefixed with `_` |
| 148 | +- Minimal docstrings: only on public functions/classes |
| 149 | +- Python target: 3.12+ (currently targeting 3.14 in tooling) |
0 commit comments