4574f08a22
Fase 2. Anade modo --stream que emite la respuesta de claude como NDJSON (eventos text_delta + result final), re-parseando snapshots del render. Compone dos funciones nuevas del registry: - pty_capture_stream_go_infra: captura snapshots acumulativos del PTY por canal. - text_prefix_delta_go_core: delta por prefijo comun entre snapshots sucesivos. Por cada snapshot: vt_render -> parse_claude_tui -> delta del Answer. Solo emite text_delta cuando el answer extiende limpiamente al anterior (HasPrefix); los frames no monotonos se reconcilian en el result final. Heuristico y documentado. e2e_check smoke_fake_stream verifica el flujo con el fake TUI (sin gastar claude).
158 lines
7.9 KiB
Markdown
158 lines
7.9 KiB
Markdown
---
|
|
name: claude_pipe
|
|
lang: go
|
|
domain: infra
|
|
version: 0.1.0
|
|
description: "Alternativa a 'claude -p' que obtiene la respuesta de claude como dato PARSEANDO su TUI interactiva: captura el render via PTY, reconstruye el layout y extrae los turnos + la respuesta final. Emite JSON con el mismo shape que 'claude -p --output-format json'. Para interaccion programatica robusta prefiere el camino stream-json (claude_stream_go_core); esta app es la variante que va a traves de la TUI."
|
|
tags: [cli, claude, terminal, pty, tui, parser]
|
|
uses_functions:
|
|
- pty_capture_idle_go_infra
|
|
- pty_capture_stream_go_infra
|
|
- vt_render_go_tui
|
|
- parse_claude_tui_go_tui
|
|
- text_prefix_delta_go_core
|
|
uses_types:
|
|
- claude_tui_parse_go_tui
|
|
- claude_turn_go_tui
|
|
framework: ""
|
|
entry_point: "main.go"
|
|
dir_path: "apps/claude_pipe"
|
|
icon:
|
|
phosphor: "faders"
|
|
accent: "#0ea5e9"
|
|
e2e_checks:
|
|
- id: build
|
|
cmd: "CGO_ENABLED=1 go build -tags fts5 -o claude_pipe ."
|
|
timeout_s: 120
|
|
- id: smoke_fake_text
|
|
cmd: "./claude_pipe --bin ./tests/fake_claude.sh --warmup 300ms --step-delay 100ms --idle 700ms --max 5s --format text test"
|
|
expect_stdout_contains: "RESPUESTA_FAKE_OK"
|
|
timeout_s: 15
|
|
- id: smoke_fake_json
|
|
cmd: "./claude_pipe --bin ./tests/fake_claude.sh --warmup 300ms --step-delay 100ms --idle 700ms --max 5s --format json test"
|
|
expect_stdout_contains: "\"result\":\"RESPUESTA_FAKE_OK\""
|
|
timeout_s: 15
|
|
- id: smoke_fake_stream
|
|
cmd: "./claude_pipe --stream --bin ./tests/fake_claude.sh --warmup 300ms --step-delay 100ms --snapshot-interval 120ms --idle 700ms --max 5s test"
|
|
expect_stdout_contains: "\"type\":\"text_delta\""
|
|
timeout_s: 15
|
|
---
|
|
|
|
# claude_pipe
|
|
|
|
## Que hace
|
|
|
|
Devuelve la respuesta de `claude` como dato **parseando su TUI interactiva**, en lugar de usar
|
|
`claude -p`. Internamente lanza el `claude` interactivo dentro de un pseudo-terminal, captura el
|
|
render de la pantalla, reconstruye el layout 2D, y extrae los turnos de la conversacion mas la
|
|
respuesta final del asistente. La salida por defecto imita `claude -p --output-format json`.
|
|
|
|
Es la respuesta a "quiero parsear la TUI y que devuelva lo mismo que `claude -p`". Para la mayoria
|
|
de los casos programaticos el camino limpio es `stream-json` (ver `claude_stream_go_core`), que no
|
|
toca la TUI; `claude_pipe` existe para cuando se quiere expresamente ir a traves de la TUI.
|
|
|
|
## Arquitectura (composicion de funciones del registry)
|
|
|
|
```
|
|
claude (TUI) ─pty_capture_idle_go_infra─▶ bytes crudos del terminal
|
|
─vt_render_go_tui──────────▶ pantalla como texto plano (layout 2D)
|
|
─parse_claude_tui_go_tui───▶ ClaudeTUIParse {Turns, Answer}
|
|
─(esta app)────────────────▶ JSON estilo claude -p / texto / turns
|
|
```
|
|
|
|
La app no aporta logica reutilizable: solo orquesta las tres funciones y elige el formato de salida.
|
|
|
|
## Formatos de salida (`--format`)
|
|
|
|
| Formato | Salida | Equivale a |
|
|
|---|---|---|
|
|
| `json` (default) | `{"type":"result","subtype":"success","is_error":false,"result":"<respuesta>"}` | `claude -p --output-format json` (campos esenciales) |
|
|
| `text` | solo el texto de la respuesta | `claude -p` plano |
|
|
| `turns` | el `ClaudeTUIParse` completo: cada turno visible (user/assistant/tool_use/tool_result) + answer | — (mas rico que `claude -p`) |
|
|
| `screen` | el render de la pantalla sin parsear | debug |
|
|
|
|
## Ejemplo
|
|
|
|
```bash
|
|
cd apps/claude_pipe
|
|
CGO_ENABLED=1 go build -tags fts5 -o claude_pipe .
|
|
|
|
# Respuesta como JSON (mismo shape que claude -p --output-format json)
|
|
./claude_pipe --cwd /home/enmanuel/fn_registry "responde unicamente con la palabra PONG"
|
|
# {"type":"result","subtype":"success","is_error":false,"result":"PONG"}
|
|
|
|
# Solo el texto
|
|
./claude_pipe --format text --cwd /home/enmanuel/fn_registry "resume el README en 3 lineas"
|
|
|
|
# Todos los turnos visibles (incluye tool_use/tool_result que la TUI muestra)
|
|
./claude_pipe --format turns --cwd /home/enmanuel/fn_registry "lee main.go y resumelo"
|
|
|
|
# Prompt por stdin
|
|
echo "explica este error" | ./claude_pipe --cwd /home/enmanuel/fn_registry
|
|
```
|
|
|
|
## Flags
|
|
|
|
| Flag | Default | Que hace |
|
|
|---|---|---|
|
|
| `--prompt` | — | Prompt a enviar. Si vacio, se toma del arg posicional o de stdin. |
|
|
| `--format` | `json` | `json` \| `text` \| `turns` \| `screen`. |
|
|
| `--cwd` | — | Directorio donde se lanza claude (usa un repo con los MCP aprobados, para saltar el dialogo de arranque). |
|
|
| `--bin` | `claude` | Binario claude a lanzar (o un fake para tests). |
|
|
| `--warmup` | `4s` | Espera antes de enviar el prompt, para que la TUI cargue. |
|
|
| `--step-delay` | `600ms` | Espera entre teclear el prompt y pulsar Enter. |
|
|
| `--idle` | `4s` | Corta la captura tras este silencio (respuesta terminada de renderizar). |
|
|
| `--max` | `120s` | Timeout duro de toda la captura. |
|
|
| `--stream` | false | Emite la respuesta incrementalmente como NDJSON (`text_delta` por snapshot) y un `result` final. |
|
|
| `--snapshot-interval` | `150ms` | En `--stream`, cada cuánto se captura y re-parsea la TUI. |
|
|
|
|
## Streaming (`--stream`)
|
|
|
|
Con `--stream`, la app usa `pty_capture_stream_go_infra` para tomar snapshots del render cada
|
|
`--snapshot-interval`, re-parsea cada snapshot con `parse_claude_tui_go_tui`, y emite el delta del
|
|
`Answer` (via `text_prefix_delta_go_core`) como NDJSON, terminando con un `result`:
|
|
|
|
```bash
|
|
./claude_pipe --stream --cwd /home/enmanuel/fn_registry "explica Go en 3 frases"
|
|
# {"type":"text_delta","text":"Go es un lenguaje..."}
|
|
# {"type":"text_delta","text":" compilado y concurrente..."}
|
|
# {"type":"result","subtype":"success","result":"Go es un lenguaje... compilado y concurrente..."}
|
|
```
|
|
|
|
Un programa externo Go (u otro) lo lanza como subprocess y lee líneas. **Es heurístico** (ver
|
|
Gotchas): la TUI re-renderiza el frame entero, así que solo se emite `text_delta` cuando el nuevo
|
|
`Answer` extiende limpiamente al anterior; los frames no monótonos (reflow) se reconcilian en el
|
|
`result` final, cuyo `result` lleva siempre la respuesta completa. Para streaming limpio y
|
|
monótono nativo sin tocar la TUI, `claude_stream_go_core` (stream-json) es superior.
|
|
|
|
## Cuando usarla
|
|
|
|
- Cuando quieras la respuesta de una sesion `claude` como dato yendo **a traves de la TUI**
|
|
(no via `claude -p`).
|
|
- Para auditar/scriptear lo que la TUI muestra y obtener ademas los `tool_use`/`tool_result`
|
|
visibles (`--format turns`).
|
|
|
|
Si no necesitas pasar por la TUI, usa `claude_stream_go_core` (stream-json) — es mas robusto.
|
|
|
|
## Gotchas
|
|
|
|
- **No reemplaza 1:1 a `claude -p`**: el campo `result` (la respuesta) coincide, pero NO se
|
|
extraen los metadatos (`cost`, `tokens`, `session_id`, `duration_ms`) porque la TUI no los
|
|
expone de forma fiable.
|
|
- **Truncacion por scroll**: la captura solo ve el grid visible (40x120). Una respuesta mas larga
|
|
que la pantalla se trunca por arriba (el render no guarda scrollback). Respuestas cortas/medias
|
|
van bien.
|
|
- **Heuristico y version-dependiente**: el parser asume el layout actual de la TUI de claude. Si
|
|
claude cambia su UI, hay que ajustar `parse_claude_tui_go_tui`.
|
|
- **Dialogo de arranque**: en un cwd cuyos MCP no estan aprobados, claude bloquea con "new MCP
|
|
servers found". Usa `--cwd <repo-raiz-aprobado>`.
|
|
- **Latencia**: anade `warmup` + `idle` (por defecto ~8s de overhead) sobre el tiempo de respuesta
|
|
de claude. `claude -p` no tiene ese overhead. Es el precio de ir por la TUI.
|
|
- **Linux/Unix only**: hereda el PTY POSIX de `pty_capture_idle_go_infra`.
|
|
- **Streaming heurístico**: `--stream` emite deltas re-parseando snapshots del render. Como la TUI
|
|
re-renderiza el frame entero, el `Answer` parseado puede no ser monótono (reflow al crecer la
|
|
respuesta, spinner). Solo se emite `text_delta` cuando el nuevo answer extiende limpiamente al
|
|
anterior; el `result` final siempre lleva la respuesta completa. Posibles artefactos: deltas que
|
|
reaparecen, o fragmentos perdidos en un frame intermedio. Si necesitas streaming exacto, usa
|
|
`claude_stream_go_core` (stream-json).
|