Files
2026-06-04 23:44:39 +02:00

5.7 KiB
Raw Permalink Blame History

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:

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).