Files
fn_registry/docs/capabilities/terminal-capture.md
T
2026-06-04 23:44:39 +02:00

87 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# terminal-capture
Automatizar una CLI/TUI interactiva y capturar su texto, de forma headless, a través de un
pseudo-terminal (PTY). Cubre el ciclo completo: lanzar el proceso con un TTY real, inyectarle
input scripteado, esperar a que el render se estabilice, y convertir el stream crudo de bytes a
texto plano — bien reconstruyendo el layout 2D (TUIs con cursor absoluto), bien limpiando ANSI
de output secuencial.
Existe porque muchas CLIs (sobre todo la CLI `claude`) solo entran en su modo interactivo rico
cuando detectan un TTY; un pipe normal las degrada. El PTY es virtual, en memoria: **nunca abre
una ventana de terminal**.
## Funciones
| ID | Firma | Qué hace |
|---|---|---|
| `pty_capture_idle_go_infra` | `func PTYCaptureIdle(ctx, name string, args []string, warmup time.Duration, inputs []string, stepDelay, idle, maxDur time.Duration) (string, error)` | Lanza `name args` en un PTY (40×120), espera `warmup`, escribe cada `inputs` separado por `stepDelay`, y captura todos los bytes hasta que pasa `idle` sin output nuevo o se alcanza `maxDur`. Devuelve el stream **crudo** (ANSI intacto). One-shot. |
| `pty_capture_stream_go_infra` | `func PTYCaptureStream(ctx, name string, args []string, warmup time.Duration, inputs []string, stepDelay, snapshotInterval, idle, maxDur time.Duration) (<-chan string, error)` | Igual que `pty_capture_idle` pero emite **snapshots acumulativos** del buffer por un canal cada `snapshotInterval` — para hacer streaming de la TUI mientras renderiza. El consumidor renderiza/parsea cada snapshot. |
| `text_prefix_delta_go_core` | `func PrefixDelta(prev, curr string) string` | Devuelve la parte de `curr` que sigue al prefijo común con `prev` (delta de streaming por snapshots). Pura, compara por runas. Heurística ante reflow. |
| `vt_render_go_tui` | `func VTRender(raw string, rows, cols int) string` | Emula un terminal VT100 de `rows×cols`, alimenta `raw`, y devuelve el estado final de la pantalla como texto plano **con el layout reconstruido** (espacios reales donde el stream tenía movimientos de cursor). Pura. |
| `strip_ansi_go_core` | `func StripANSI(s string) string` | Elimina secuencias ANSI/VT100 y caracteres de control de un stream **secuencial** (logs), preservando `\n`, `\t`, `\r`. Pura. NO reconstruye layout 2D. |
| `parse_claude_tui_go_tui` | `func ParseClaudeTUI(screen string) ClaudeTUIParse` | Parsea la pantalla renderizada de la TUI de `claude` (salida de `vt_render`) y extrae los turnos (user/assistant/tool_use/tool_result) + la respuesta final (`Answer`), equivalente a lo que devolvería `claude -p`. Pura, heurística, específica de la TUI de claude. |
## Cuándo usar cada limpiador
El corazón del grupo es `pty_capture_idle` (la captura). Lo que cambia es cómo conviertes el raw a texto:
| Si la salida es… | Usa | Porque |
|---|---|---|
| Una TUI con posicionamiento absoluto (`claude`, `htop`, `dialog`) | `vt_render_go_tui` (modo screen) | Los "espacios" entre columnas eran movimientos de cursor; sin emular el grid las palabras se pegan (`2newMCPservers`). |
| Output secuencial línea a línea (logs, builds) | `strip_ansi_go_core` (modo stream) | No hay layout 2D que reconstruir; basta quitar los escape codes. |
| Quieres procesar los escape codes tú mismo | (ninguno — usa el raw) | El raw de `pty_capture_idle` ya los conserva. |
## Ejemplo canónico (end-to-end)
Capturar la respuesta de la CLI `claude` como texto con layout, en Go:
```go
import (
"context"
"time"
"fn-registry/functions/infra"
"fn-registry/functions/tui"
)
func main() {
ctx := context.Background()
// Teclear el prompt y pulsar Enter como pasos separados: un "\r" pegado al
// texto lo trata claude como newline literal, no como submit.
inputs := []string{"resume el README en 3 lineas", "\r"}
raw, _ := infra.PTYCaptureIdle(ctx, "claude", nil,
4*time.Second, // warmup: deja cargar la TUI
inputs, 600*time.Millisecond,
4*time.Second, // idle: corta tras 4s de silencio
60*time.Second) // maxDur: tope duro
screen := tui.VTRender(raw, 40, 120) // reconstruye el layout 2D
print(screen)
}
```
La app `claude_extract` (`apps/claude_extract`) empaqueta exactamente este flujo como CLI, con
modos `screen|stream|raw`, `--exec` para pipear a otro proceso, y `--cwd` para saltar el diálogo
de arranque de claude. Es el consumidor de referencia del grupo.
La app `claude_pipe` (`apps/claude_pipe`) va un paso más allá: añade `parse_claude_tui_go_tui`
al final del pipeline para devolver la respuesta de claude **como dato** con el mismo shape que
`claude -p --output-format json` (`--format json|text|turns`). Es la alternativa "parsea la TUI"
a `claude -p`, para cuando se quiere expresamente ir a través de la TUI en vez del stream-json.
## Fronteras
- **No es `claude -p`**: este grupo captura la TUI real (lo que se ve). Para interacción programática
limpia con la CLI `claude`, usa `claude_stream_go_core` (`claude -p --output-format stream-json`).
- **Linux/Unix only**: PTY POSIX (`creack/pty`). No Windows.
- **Sin color**: `vt_render` reconstruye texto y layout, no atributos de color.
- **Idle es heurístico**: TUIs con render periódico (spinners, relojes) no disparan el idle y caen
al `maxDur`. Para `claude` el spinner se detiene al terminar la respuesta, así que corta bien.
- **Dimensiones fijas 40×120**: el render debe usar el mismo tamaño que la captura o el wrapping no
cuadra.
## Notas
- Las dos funciones de limpieza son **puras**; solo `pty_capture_idle` es impura (lanza procesos).
Puras en los bordes, impura en el centro de la captura.
- `pty_capture_idle` no fija el cwd del hijo: para controlarlo, cambia el cwd del proceso que la
invoca antes de llamarla (lo que hace `claude_extract --cwd`).