JSON When Piped
When you pipe tabstack to another process, the output format changes automatically. No --json flag required.
# On a TTY — pretty, human-readable
tabstack extract markdown https://example.com
# → Formatted markdown with colors and spacing
# Piped to jq — clean JSON
tabstack extract markdown https://example.com | jq -r .content
# → {"url":"...","content":"# Example Domain\n..."} How it works
The CLI checks process.stdout.isTTY at startup. If it’s a TTY (a real terminal), pretty mode activates. If it’s false (piped, redirected, or a non-interactive subprocess), JSON mode activates.
const outMode = process.stdout.isTTY ? 'pretty' : 'json'; This is the right default because it matches what each context actually needs. A human watching a terminal wants readable output. A script consuming the output wants parseable JSON. Making the user specify -o json every time they pipe is friction that compounds across dozens of pipeline invocations.
Forcing the mode
When the default detection doesn’t match your needs:
# Force JSON even on a TTY (e.g. for manual inspection of the JSON shape)
tabstack extract markdown https://example.com -o json
tabstack extract markdown https://example.com --json # shorthand
# Force pretty even when piped (rare, but valid for log files)
tabstack research "something" -o pretty > report.md The -o flag is --output in long form. It accepts pretty or json.
What JSON mode looks like per command
The output structure is consistent and documented:
| Command | JSON key |
|---|---|
extract markdown | {"url": "...", "content": "..."} |
extract json | the schema-shaped object directly |
generate json | the schema-shaped object directly |
research (NDJSON) | {"event": "...", "data": {...}} per line |
automate (NDJSON) | {"event": "...", "data": {...}} per line |
tabstack usage | {"estimated": 4750, "learned": {...}} |
tabstack recipes --json | [{n, heat, verbs, blurb, command}, ...] |
Streaming commands and NDJSON
research and automate are streaming — the response arrives in multiple events over time. In JSON mode, each event is one line of NDJSON (newline-delimited JSON). This is the natural fit: NDJSON is just JSON lines, which jq handles natively with no flags.
tabstack research "competitor pricing"
| jq -r 'select(.event=="complete") | .data.metadata.citedPages[]' In pretty mode, streaming commands print human-readable progress to stderr and the final result to stdout. Switching to JSON mode gives you the full event stream, which is useful when you need intermediate events (citation counts, iteration progress) not just the final answer.
See Streaming & NDJSON for the full event reference.
Why not --json as the only mechanism?
Because flags are for things that are genuinely optional. JSON output when piped isn’t optional — it’s the correct behavior. Making it the default when piped means the tool works correctly in pipelines written by people who didn’t read the docs. That’s the bar.
The alternative — requiring --json every time — means every broken pipeline is the user’s fault for forgetting a flag. That’s a bad design philosophy. Sensible defaults mean failures are rare and surprising; bad defaults mean failures are common and expected.