feat: scaffold claude_pipe — claude -p equivalente parseando la TUI

App que obtiene la respuesta de claude como dato parseando su TUI interactiva,
en lugar de usar claude -p. Compone tres funciones del registry:

- pty_capture_idle_go_infra: captura el render de la TUI via PTY headless.
- vt_render_go_tui: reconstruye el layout 2D como texto plano.
- parse_claude_tui_go_tui: extrae los turnos + la respuesta final.

Salida por defecto con el mismo shape que claude -p --output-format json.
Formatos: json, text, turns, screen. Validada end-to-end: el campo result
coincide exacto con claude -p nativo (PONG == PONG). e2e_checks deterministas
con un fake TUI (tests/fake_claude.sh) que no gasta llamadas reales.

Fase 1 (one-shot). El streaming incremental queda como fase 2.
This commit is contained in:
agent
2026-06-03 22:52:48 +02:00
commit 8d6078e99e
6 changed files with 433 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
claude_pipe
*.db-shm
*.db-wal
+127
View File
@@ -0,0 +1,127 @@
---
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
- vt_render_go_tui
- parse_claude_tui_go_tui
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
---
# 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. |
## 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**: esta version es one-shot (espera la respuesta completa y luego parsea). El
streaming incremental de la TUI esta planificado como fase 2 (requiere capturar snapshots
durante el render).
+44
View File
@@ -0,0 +1,44 @@
module claude_pipe
go 1.25.0
require fn-registry v0.0.0
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v1.0.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
nhooyr.io/websocket v1.8.17 // indirect
)
replace fn-registry => ../../
+74
View File
@@ -0,0 +1,74 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+163
View File
@@ -0,0 +1,163 @@
// Command claude_pipe is a drop-in-ish replacement for `claude -p` that works by
// driving the interactive `claude` TUI through a pseudo-terminal, capturing its
// rendered screen, and parsing it back into structured data — the assistant's
// answer plus the visible conversation turns.
//
// It exists for the (unusual) case where you want the result of an interactive
// claude session as data, going THROUGH the TUI rather than through
// `claude -p --output-format json`. For most programmatic use the stream-json path
// (claude_stream_go_core) is cleaner and more robust; claude_pipe is the TUI-parsing
// alternative, kept because the TUI exposes things `-p` does not.
//
// Pipeline (all registry functions):
//
// pty_capture_idle_go_infra -> capture the TUI render headlessly via PTY
// vt_render_go_tui -> reconstruct the 2D screen as plain text
// parse_claude_tui_go_tui -> extract turns + final answer
//
// Output formats:
//
// --format json {"type":"result","subtype":"success","is_error":false,"result":"<answer>"}
// (mirrors `claude -p --output-format json`)
// --format text just the answer text (mirrors plain `claude -p`)
// --format turns the full ClaudeTUIParse (every visible turn + answer) as JSON
// --format screen debug: the raw rendered screen before parsing
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"time"
"context"
"fn-registry/functions/infra"
"fn-registry/functions/tui"
)
// PTY grid size. Must match what pty_capture_idle_go_infra uses internally (40x120)
// so vt_render reconstructs the layout with the same wrapping.
const (
ptyRows = 40
ptyCols = 120
)
// claudePResult mirrors the shape of `claude -p --output-format json`.
type claudePResult struct {
Type string `json:"type"`
Subtype string `json:"subtype"`
IsError bool `json:"is_error"`
Result string `json:"result"`
}
func main() {
var (
prompt = flag.String("prompt", "", "prompt to send. If empty, taken from the positional arg, or from piped stdin")
format = flag.String("format", "json", "output format: json (like claude -p --output-format json), text (just the answer), turns (full parse), screen (debug: raw render)")
cwd = flag.String("cwd", "", "run claude in this directory (use a repo root whose MCP servers are approved, to skip the startup dialog)")
bin = flag.String("bin", "claude", "claude binary to launch")
warmup = flag.Duration("warmup", 4*time.Second, "wait before sending the prompt, so the TUI finishes loading")
stepDelay = flag.Duration("step-delay", 600*time.Millisecond, "delay between typing the prompt and pressing Enter")
idle = flag.Duration("idle", 4*time.Second, "stop capturing after this much silence (response finished rendering)")
maxDur = flag.Duration("max", 120*time.Second, "hard timeout for the whole capture")
)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `claude_pipe — get a claude answer as data by parsing its TUI (alternative to claude -p).
Usage:
claude_pipe [flags] [prompt]
Examples:
claude_pipe --cwd /home/enmanuel/fn_registry "responde solo PONG"
claude_pipe --format text --cwd /repo "resume el README en 3 lineas"
echo "explica este error" | claude_pipe --cwd /repo
claude_pipe --format turns --cwd /repo "lee main.go y resume" # incluye tool_use/tool_result visibles
Flags:
`)
flag.PrintDefaults()
}
flag.Parse()
if *cwd != "" {
if err := os.Chdir(*cwd); err != nil {
fmt.Fprintf(os.Stderr, "claude_pipe: --cwd: %v\n", err)
os.Exit(1)
}
}
promptText := *prompt
if promptText == "" && flag.NArg() > 0 {
promptText = strings.Join(flag.Args(), " ")
}
if promptText == "" && stdinIsPiped() {
if data, err := os.ReadFile("/dev/stdin"); err == nil {
promptText = strings.TrimRight(string(data), "\n")
}
}
if promptText == "" {
fmt.Fprintln(os.Stderr, "claude_pipe: no prompt (use --prompt, a positional arg, or pipe stdin)")
os.Exit(2)
}
// Type the prompt and press Enter as SEPARATE steps: a "\r" glued to the text is
// treated by claude as a literal newline in the input box, not a submit.
inputs := []string{promptText, "\r"}
ctx, cancel := context.WithTimeout(context.Background(), *maxDur+10*time.Second)
defer cancel()
raw, err := infra.PTYCaptureIdle(ctx, *bin, nil, *warmup, inputs, *stepDelay, *idle, *maxDur)
if err != nil {
fmt.Fprintf(os.Stderr, "claude_pipe: capture failed: %v\n", err)
os.Exit(1)
}
screen := tui.VTRender(raw, ptyRows, ptyCols)
if *format == "screen" {
fmt.Println(screen)
return
}
parsed := tui.ParseClaudeTUI(screen)
switch *format {
case "text":
fmt.Println(parsed.Answer)
case "turns":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(parsed); err != nil {
fmt.Fprintf(os.Stderr, "claude_pipe: encode: %v\n", err)
os.Exit(1)
}
case "json":
res := claudePResult{
Type: "result",
Subtype: "success",
IsError: parsed.Answer == "",
Result: parsed.Answer,
}
enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(res); err != nil {
fmt.Fprintf(os.Stderr, "claude_pipe: encode: %v\n", err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "claude_pipe: unknown --format %q (want json|text|turns|screen)\n", *format)
os.Exit(2)
}
}
// stdinIsPiped reports whether stdin is connected to a pipe/file rather than a terminal.
func stdinIsPiped() bool {
info, err := os.Stdin.Stat()
if err != nil {
return false
}
return (info.Mode() & os.ModeCharDevice) == 0
}
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Fake claude TUI used by claude_pipe's deterministic e2e checks.
#
# It does NOT talk to any model. It ignores stdin (the prompt claude_pipe types)
# and prints a minimal screen that mimics the markers parse_claude_tui_go_tui
# looks for: a " " user line and a "● " assistant line. After printing it idles
# briefly so the capture's idle-cutoff fires and the process exits cleanly.
#
# stty -echo is essential: claude_pipe types the prompt into the PTY, and a TTY
# echoes input by default. The real claude TUI captures keystrokes into its own
# widget instead of echoing them; this fake has no such widget, so without
# disabling echo the typed prompt would leak onto the screen and pollute the
# parsed answer. The real claude binary is unaffected by this.
#
# This lets us validate the full capture -> render -> parse pipeline without
# spending a real claude call or depending on network/model output.
stty -echo 2>/dev/null || true
printf '\xe2\x9d\xaf test prompt\n\n' # " test prompt"
printf '\xe2\x97\x8f RESPUESTA_FAKE_OK\n\n' # "● RESPUESTA_FAKE_OK"
sleep 2