# AGENTS.md — flicker

Terminal text animations (typewriter, spinner, progress) designed for agents that already live in a terminal. The MCP server is the primary product; the CLI is a human convenience layer on the same engine.

If you're a human, read `README.md` instead.

## Tools

Five MCP tools, verb-first:

| Tool | Args | Returns |
|---|---|---|
| `play_typewriter` | `text`, `cps?` (1–1000, default 30), `mode?` | `Receipt` |
| `play_spinner` | `label`, `duration_ms?`, `kind?` (`dots` \| `bar` \| `pulse`), `mode?` | `Receipt` |
| `play_progress` | `label`, `total` (int or `null` for indeterminate), `steps?`, `mode?` | `Receipt` |
| `estimate` | `effect` (`typewriter` \| `spinner` \| `progress`), `args` | `CostEstimate` (no render) |
| `overview` | — | `ServerStatus` (uptime, mode, terminal caps, last 20 receipts, cumulative cost, per-effect counts) |

`mode` is `full` \| `reduced` \| `plain`. Omit it and the server picks the safe default.

### Receipt

```ts
{ effect, mode, fallback, duration_ms, frames_rendered, bytes_written, final_text, cost }
```

### CostEstimate

```ts
{ bytes_to_tty, bytes_stripped, tokens_stripped,
  dollars_stripped, dollars_worst_case,
  estimated, max_bytes_to_tty? }
```

`dollars_worst_case` is what it costs if every escape-bearing byte lands in your context. `dollars_stripped` is what it costs after `stripForAgent()` runs. The gap is the ANSI tax.

## The hard contract

1. **Animation never lands on MCP stdout.** That channel is JSON-RPC. The renderer writes to `/dev/tty` if openable, else a TTY-stderr, else nothing (mode silently downgrades to `plain` and the receipt records `fallback: "plain"`).
2. **Receipts are always JSON, even on error.** No partial writes, no half-encoded structs.
3. **Input text is allowlisted.** A CSI / OSC / DCS / BEL / bare-CR / 8-bit-CSI in `text` or `label` raises in full/reduced mode. Plain mode scrubs silently via `stripForAgent`.
4. **Hung spinners are impossible.** Indeterminate effects cap at `max_duration_ms` (default 60s), restore cleanly, and record `fallback: "duration_cap"`. They never throw.

## Sequencing rules

- **Unattended / non-interactive context:** call `estimate` first. Read `dollars_worst_case`. If it's above whatever budget you've been given, skip the animation. THEN call `play_*` only when you've decided to spend.
- **In an MCP context:** omit `mode`. The server's auto-detection looks at `/dev/tty`, `MCP_CLIENT`, `AGENT`, `CI`, `NO_COLOR` and picks the right tier without your help.
- **Inspecting live state:** call `overview()`. Returns cumulative cost so far, the last 20 receipts, current terminal capabilities, and per-effect counts. Same tool surface — no separate dashboard.
- **Validating a draft before sending:** `estimate` is pure planning; calling it any number of times costs nothing and reveals the exact bytes you'd emit.

## What flicker will NOT do

- Emit any CSI / OSC / DCS sequence that isn't in the v0.1 allowlist (no OSC 8 hyperlinks, no OSC 52 clipboard, no mouse modes, no Sixel).
- Animate to MCP stdout under any circumstance.
- Throw on a hung spinner. Cap-and-restore is the contract.
- Retry on TTY loss (v0.1 is best-effort; v0.2 will harden).
- Persist a cost log (v0.1 returns the struct only; consumer persists if needed).

## Where to look next

- `docs/plans/2026-05-17-flicker-design.md` — the design doc, including the full CSI allowlist and mode-detection cascade.
- `docs/research/2026-05-16-terminal-animation-research.md` — the research brief that motivates the design choices.
- `examples/` — three tiny scripts (`typewriter.ts`, `spinner.ts`, `progress.ts`) showing the library API directly.
- `README.md` — human-facing intro, CLI usage, and quickstart.
