Skip to content

Commit abd6836

Browse files
mikeckennedyclaude
andcommitted
Add --format markdown for raw Markdown output (closes #1)
Adds a third output format for AI agent and LLM integration. When --format markdown is used, raw Markdown is printed to stdout with no Rich formatting, and ?format=markdown is passed to the MCP server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3efd793 commit abd6836

File tree

8 files changed

+109
-19
lines changed

8 files changed

+109
-19
lines changed

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"Bash(./venv/bin/python -m talk_python_cli.app status:*)",
66
"Bash(./venv/bin/talkpython status:*)",
77
"Bash(./venv/bin/pip install:*)",
8-
"Bash(./venv/bin/talkpython:*)"
8+
"Bash(./venv/bin/talkpython:*)",
9+
"Bash(gh issue view:*)",
10+
"Bash(pyrefly check:*)"
911
]
1012
}
1113
}

README.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,41 +84,55 @@ talkpython courses list
8484

8585
## Output formats
8686

87-
The CLI auto-detects the best output format:
87+
The CLI supports three output formats via `--format`:
8888

89-
- **Interactive terminal** — Rich-formatted Markdown with styled panels and color.
90-
- **Piped / redirected** — Compact JSON, ready for processing.
91-
92-
Override the default with `--format`:
89+
- **`text`** (default) — Rich-formatted Markdown with styled panels and color for human reading.
90+
- **`json`** — Structured JSON, pretty-printed on a TTY or compact when piped.
91+
- **`markdown`** — Raw Markdown output with no Rich formatting. Ideal for piping into AI agents, LLMs, and automation tools that consume Markdown natively.
9392

9493
```bash
9594
# Force JSON output in the terminal
9695
talkpython --format json episodes search "async"
9796

97+
# Raw Markdown for AI agents and LLM pipelines
98+
talkpython --format markdown episodes get 535
99+
98100
# Force rich text output even when piping
99101
talkpython --format text episodes recent | less -R
100102
```
101103

104+
## Agentic AI and LLM integration
105+
106+
Use `--format markdown` when feeding output to AI agents, LLMs, or RAG pipelines. This gives you clean, raw Markdown without terminal styling — exactly what language models expect:
107+
108+
```bash
109+
# Feed an episode summary to an LLM
110+
talkpython --format markdown episodes get 535 | llm "Summarize this podcast episode"
111+
112+
# Grab a transcript for RAG ingestion
113+
talkpython --format markdown episodes transcript 535 | your-rag-pipeline ingest
114+
115+
# Pipe course details into an AI agent
116+
talkpython --format markdown courses get 57 | your-agent process
117+
```
118+
102119
## Piping JSON to other tools
103120

104-
Because the CLI outputs JSON automatically when piped, it integrates naturally with tools like `jq`, `llm`, or your own scripts:
121+
The `--format json` output integrates naturally with tools like `jq` or your own scripts:
105122

106123
```bash
107124
# Extract episode titles with jq
108-
talkpython episodes search "testing" | jq '.title'
109-
110-
# Feed episode data into an LLM
111-
talkpython episodes get 535 | llm "Summarize this podcast episode"
125+
talkpython --format json episodes search "testing" | jq '.title'
112126

113-
# Grab a transcript for RAG ingestion
114-
talkpython episodes transcript 535 | your-rag-pipeline ingest
127+
# Process structured data in a script
128+
talkpython --format json episodes recent | python process_episodes.py
115129
```
116130

117131
## Global options
118132

119133
| Option | Description |
120134
|--------|-------------|
121-
| `--format text\|json` | Force output format (auto-detected by default) |
135+
| `--format text\|json\|markdown` | Output format: `text` (rich), `json`, or `markdown` (raw) |
122136
| `--url <mcp-url>` | Override the MCP server URL (default: `https://talkpython.fm/api/mcp`) |
123137
| `--version`, `-V` | Show version |
124138

src/talk_python_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from importlib.metadata import version
44

5-
__version__ = version("talk-python-cli")
5+
__version__ = version('talk-python-cli')

src/talk_python_cli/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ def status() -> None:
8282
def launcher(
8383
*tokens: Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
8484
format: Annotated[
85-
Literal['text', 'json'],
85+
Literal['text', 'json', 'markdown'],
8686
cyclopts.Parameter(
8787
name='--format',
88-
help="Output format: 'text' (rich Markdown) or 'json'.",
88+
help="Output format: 'text' (rich Markdown), 'json', or 'markdown' (raw).",
8989
),
9090
] = 'text',
9191
url: Annotated[

src/talk_python_cli/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def _next_id(self) -> int:
4646
return self._msg_id
4747

4848
def _url(self) -> str:
49-
if self.output_format == 'json':
50-
return f'{self.base_url}?format=json'
49+
if self.output_format in ('json', 'markdown'):
50+
return f'{self.base_url}?format={self.output_format}'
5151
return self.base_url
5252

5353
def _post(self, payload: dict) -> httpx.Response:

src/talk_python_cli/formatting.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,17 @@ def display(content: str, output_format: str) -> None:
3939
"""Route content to the appropriate renderer."""
4040
if output_format == 'json':
4141
display_json(content)
42+
elif output_format == 'markdown':
43+
display_markdown_raw(content)
4244
else:
4345
display_markdown(content)
4446

4547

48+
def display_markdown_raw(content: str) -> None:
49+
"""Print raw Markdown content to stdout without any Rich formatting."""
50+
print(content)
51+
52+
4653
def display_markdown(content: str) -> None:
4754
"""Render Markdown content with Rich, wrapped in a styled panel."""
4855
md = Markdown(content, code_theme='monokai')

tests/test_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,35 @@ def test_json_format_adds_query_param(self, httpx_mock: HTTPXMock) -> None:
197197
assert 'format=json' in str(req.url)
198198
client.close()
199199

200+
def test_markdown_format_adds_query_param(self, httpx_mock: HTTPXMock) -> None:
201+
md_url = f'{DEFAULT_URL}?format=markdown'
202+
client = MCPClient(base_url=DEFAULT_URL, output_format='markdown')
203+
204+
httpx_mock.add_response(
205+
method='POST',
206+
url=md_url,
207+
json=jsonrpc_result(
208+
1,
209+
{
210+
'protocolVersion': '2025-03-26',
211+
'capabilities': {'tools': {}},
212+
'serverInfo': {'name': 'test', 'version': '0.1'},
213+
},
214+
),
215+
headers={'mcp-session-id': 's1'},
216+
)
217+
httpx_mock.add_response(method='POST', url=md_url, status_code=202, headers={'mcp-session-id': 's1'})
218+
httpx_mock.add_response(
219+
method='POST',
220+
url=md_url,
221+
json=tool_result(3, '# Episode 535\n\nSome markdown content'),
222+
)
223+
224+
client.call_tool('get_episodes')
225+
for req in httpx_mock.get_requests():
226+
assert 'format=markdown' in str(req.url)
227+
client.close()
228+
200229

201230
class TestContextManager:
202231
"""Verify MCPClient works as a context manager."""

tests/test_formatting.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Tests for output formatting."""
2+
3+
from __future__ import annotations
4+
5+
from talk_python_cli.formatting import display, display_markdown_raw
6+
7+
8+
class TestDisplayMarkdownRaw:
9+
def test_prints_raw_content(self, capsys) -> None:
10+
display_markdown_raw('# Hello\n\nSome **bold** text')
11+
12+
captured = capsys.readouterr()
13+
assert captured.out == '# Hello\n\nSome **bold** text\n'
14+
15+
def test_no_rich_markup_in_output(self, capsys) -> None:
16+
display_markdown_raw('## Heading\n- item 1\n- item 2')
17+
18+
captured = capsys.readouterr()
19+
# Raw markdown should appear verbatim — no Rich panel borders or styling
20+
assert '## Heading' in captured.out
21+
assert '- item 1' in captured.out
22+
assert '╭' not in captured.out # no Rich panel border
23+
24+
25+
class TestDisplayRouting:
26+
def test_markdown_format_routes_to_raw(self, capsys) -> None:
27+
display('# Raw markdown', 'markdown')
28+
29+
captured = capsys.readouterr()
30+
assert captured.out == '# Raw markdown\n'
31+
32+
def test_text_format_does_not_print_raw(self, capsys) -> None:
33+
display('# Rendered', 'text')
34+
35+
captured = capsys.readouterr()
36+
# Text mode uses Rich console, so raw '# Rendered' should NOT appear verbatim
37+
# (Rich renders it as a heading without the # prefix)
38+
assert '# Rendered' not in captured.out

0 commit comments

Comments
 (0)