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:
@@ -0,0 +1,3 @@
|
|||||||
|
claude_pipe
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
@@ -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).
|
||||||
@@ -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 => ../../
|
||||||
@@ -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=
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Executable
+22
@@ -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
|
||||||
Reference in New Issue
Block a user