Skip to content

peek — htop for codebases — Documentation

Understand any codebase in 5 seconds.
pip install peek-code && peek . — Python-native, Rich + Textual, works offline, never crashes.

peek anthropic-pro preview

PyPI Python tests themes Rich+Textual


Table of Contents


What is peek?

peek is the htop for codebases. Point it at any repo — local, cloned, or https://github.com/... checked out — and it tells you in one screen:

  • What language is this? (by-lang bar)
  • What stack? (FastAPI / Django / Typer / SQLAlchemy / Next.js …)
  • Where to start? (PageRank + in-degree + entry bonus ranked Start Here)
  • How is it wired? (import graph, top hubs → deps, ASCII one-liner)
  • What’s big? (largest files by LOC + size)

Every output is a screenshot: peek --no-tui is tweet-ready, peek --html is shareable, peek TUI is interactive.

No API keys, no network, no setup. Pure Python ast + heuristics, 2000-file cap, binary/huge/permission-safe.


Install

pip install peek-code
# or isolated
pipx install peek-code
uv tool install peek-code

# dev (with tests + linters)
git clone https://github.com/hariomlohardev/peek && cd peek
pip install -e ".[dev]"
pytest -q  # 129 passed, 1 skipped

Requires Python 3.11+. Deps: typer, rich, textual, pathspec — all pure Python.


Demo Video

Generated by code — no vhs/ffmpeg needed. python -m peek.tools.gen_demo renders a themed terminal in Pillow → assets/demo.gif (800×450, <3 MB, ~15 s, 10 fps, loop) + assets/demo.svg (SMIL).

peek demo — code-generated GIF

python -m peek.tools.gen_demo        # gif + svg (Pillow, 800x450)
peek --html -o peek/assets/demo.html # themed HTML
pytest peek/tests/test_demo_assets.py -v  # guards: GIF89a, <3MB, HTML exists
ls -lh peek/assets/demo.gif peek/assets/demo.svg  # verify <3MB

Scenes: title htop for codebases → peek --help → peek . --no-tui → peek find "auth token" --semantic → peek graph --format svg → peek --pack --ask auth token → peek mcp → --theme-list carousel (v3 polyglot + semantic + graph + MCP). Colors from peek/themes.py (anthropic-pro #D4A27F→#141413). Also vhs fallback: vhs peek/assets/demo.tape.


Quick Start

peek                 # TUI on current dir — q to quit, / to filter
peek .               # same, explicit path
peek /path/to/repo   # any repo
peek --no-tui        # static Rich (CI / pipe / screenshot)
peek --theme dracula # same but purple
peek --theme-list    # see all 10

In 10 seconds on this repo:

peek --no-tui
# 98 files • 7,736 LOC • 18 modules • 42 edges
# Languages: other 61% python 18% markdown 16%
# Summary: Typer/Click CLI (Rich, Textual) — Entry: peek/cli.py
# Start Here: 1 scanner.py (hub) 9.1 • 2 cli.py (entry) 8.3

Features

Feature scan analyze peek (TUI) peek --html peek --pack
Walk respecting .gitignore + defaults (.git, __pycache__, node_modules) ✅ ✅ ✅ ✅ ✅
Languages + LOC + by-lang bar ✅ ✅ ✅ ✅ —
Tech stack (pyproject.toml/package.json deps → frameworks) ✅ ✅ ✅ ✅ —
Entry points (main.py/cli.py/__main__/pyproject.scripts/if __name__ == "__main__") ✅ ✅ ✅ ✅ —
Import graph (AST, relative, src/ layout, stdlib filtered) — ✅ ✅ ✅ —
PageRank + in-degree + entry bonus ranking — ✅ ✅ ✅ —
Heuristic summary (framework detection + hub) — ✅ ✅ ✅ —
Static Rich panels ✅ ✅ ✅* ✅ —
Textual TUI (nav, filter, open, 10 themes) — — ✅ — —
--html self-contained ✅ ✅ ✅ ✅ —
--pack / --ask (token budget) — — ✅ — ✅
find (filename + content ranked) — — ✅ — —
--llm (OpenAI/Anthropic if key set) — ✅ ✅ — —

* static via --no-tui

Why peek vs others?

Tool Lacks
gitingest / repomix No ranking, no graph, no TUI
pydeps No summary, no entry detection
tree / tokei / onefetch No graph, no ranking
code2prompt Needs API, no local analysis

10 Themes

Same layout, subtle animations (40 ms stagger, linear 220 ms fade) — only colors change. Default is anthropic-pro (warm clay on charcoal, professional).

peek --theme dracula              # one-shot
peek --theme nord --no-tui        # static themed
peek --theme dracula --html -o out.html  # themed HTML
PEEK_THEME=tokyo-night peek        # env
echo 'theme = "dracula"' > ~/.peek/config.toml  # persistent
peek --theme-list                 # list all 10

Precedence: cli --theme > PEEK_THEME env > config.toml > anthropic-pro. Bad name → exit 2 + Available: ....

# ID Label Accent → Bg Vibe Preview
1 anthropic-pro Anthropic Pro #D4A27F → #141413 Warm editorial, default anthropic-pro
2 cinematic Cinematic #FFE600 → #070A14 Neon viral cinematic
3 dracula Dracula #BD93F9 → #282A36 Purple haze dracula
4 nord Nord #88C0D0 → #2E3440 Arctic calm nord
5 catppuccin-mocha Catppuccin Mocha #CBA6F7 → #1E1E2E Pastel cozy catppuccin-mocha
6 tokyo-night Tokyo Night #7AA2F7 → #1A1B26 Electric storm tokyo-night
7 solarized-dark Solarized Dark #268BD2 → #002B36 Teal classic solarized-dark
8 github-dark GitHub Dark #58A6FF → #0D1117 Familiar OSS github-dark
9 monokai Monokai #F92672 → #272822 Hot pink pop monokai
10 minimal-mono Minimal Mono #E5E5E5 → #111111 Grayscale a11y minimal-mono

Each theme has 15 tokens: bg, bg2, surface, panel, line, line2, ink, ink2, muted, muted2, accent, accent2, cyan, violet, green. All validated at import (#RRGGBB, exactly 15).

Screenshots: All previews are generated SVGs (800×420) from peek/assets/themes/*.svg — they mimic the real TUI header, stats, Languages, Summary, Start Here, palette strip, and tip. Export your own:

for t in anthropic-pro cinematic dracula nord catppuccin-mocha tokyo-night solarized-dark github-dark monokai minimal-mono; do
  peek --theme $t --no-tui > /tmp/$t.txt
  peek --theme $t --html -o /tmp/$t.html && echo "→ /tmp/$t.html"
done

CLI Reference

peek [PATH] [OPTIONS] [COMMAND]

Options:
  -V, --version          Show version and exit
  --no-tui               Static output only (no TUI) — pipeable
  --html                 Export to HTML (with -o)
  -o, --output PATH      Output file for --html / --pack
  --pack                 LLM pack: concatenate top files
  --ask TEXT             Filter --pack by keyword
  --llm                  Try LLM summary if OPENAI_API_KEY / ANTHROPIC_API_KEY set
  --find TEXT            Find keyword (alias for peek find)
  --theme TEXT           Theme id (see --theme-list)
  --theme-list           List themes and exit
  --help                 Show help

Commands:
  scan [PATH]            Scan + stats, tech stack, entry points
    --max-files 2000     Hard cap
    --json               JSON output
    --html / -o          HTML export
    --theme TEXT         Themed

  analyze [PATH]         Graph + ranking + summary
    --max-files, --json, --html, -o, --llm, --theme

  find <query> [PATH]    Find by filename + content, ranked
    --limit 20 --json --max-files

  wtf [FILE]             Explain traceback (file or stdin pipe) — scan-aware hint
    --explain/--no-explain  Heuristic explain (default on)

  watch [PATH]           Live rescan on file change (polling 0.8s, debounce 0.4s)
    peek --watch         TUI watch with w toggle

  config set <k> <v>     Set persistent config (theme validated via get_theme)
  config get <k>         Get config value
  config list            List config + path

Examples:
  peek
  peek --no-tui
  peek scan . --json | jq .stats
  peek analyze . --html -o report.html
  peek --theme dracula --no-tui
  peek --pack --ask "auth" -o pack.txt
  peek find "token" . --limit 10
  PEEK_THEME=nord peek
  • Windows: UTF-8 forced, /tmp/foo.html on win32 maps to %TEMP%/foo.html via _write_output_safely, mkdir -p for -o.
  • Exit codes: 0 ok, 1 analyze fail, 2 unknown theme.

TUI Guide

peek                  # launches TUI (default)
peek --theme dracula  # themed TUI

Layout: Header (peek v + path + time + theme) | stats | Languages | Summary | Tech Stack | Start Here (ranked) | Import Graph | Largest Files → right pane Start Here list + Detail bottom.

Keys:

Key Action
q / Ctrl+C Quit
j / k / ↑ / ↓ Navigate Start Here
Enter Show details toast (score + reasons)
o Open highlighted file in $EDITOR / $VISUAL / notepad
/ Filter Start Here (fuzzy on rel + reasons), Enter apply, Esc clear
? Help toast (theme + keys)
t Cycle theme live (10) — t again for next
w Toggle watch live rescan (polling, debounce) — w on/off
c Show config path + theme
Esc Clear filter if visible

Filter: Type auth, cli, graph — list updates live with 12 ms stagger, detail follows first match. No match → placeholder No files — try filter.

Animations — Subtle + Continuous: - Static (peek --no-tui): Staggered reveal when TTY — header 0ms → +40ms stats → +30ms Languages → +30ms Summary → +30ms Tech → +30ms Ranked → +30ms Graph → +20ms Largest → tip (render_static animate=True only when is_terminal). - TUI 3 continuous loops (all linear): 1. Live pulse ◐◑◒◓ in #pulse dock top, set_interval(0.28s, _tick_pulse) — accent colored, always moving. 2. Rotating tips Tip: / filter… / j/k nav… / Theme: X…, set_interval(3.0s, _tick_tip) — footer/pulse never static. 3. Border breathe set_interval(1.8s, _tick_border) toggles #right.pulse → border: solid accent ↔ line. - Panel fade: left/right/detail opacity 0→1 220ms linear (right +60ms delay), left.in etc. - List stagger: _stagger_list await asyncio.sleep(0.016 + idx*0.008) per ListItem, opacity 0→1 160ms linear, background 120ms linear, highlight border-left: solid accent. - Scanner spinner: cli._scan_with_spinner Rich Progress(SpinnerColumn(spinner_name="dots", style=accent)) + threading 0.12 s min visible, themed accent. All linear (not ease-out).

Why continuous? Viral wow needs motion, pro user needs calm — ◐ + tip + border makes TUI feel like htop — alive, not flashy.

Fallbacks: No textual installed → static Rich + hint pip install textual. Not a tty (isatty false) → static. asyncio imported at top — filter no longer NameError.


Pack, Find, LLM

# Pack — smart concatenation for LLMs, respects token budget (default 8000, ~len/4 est)
peek --pack                     # stdout → | pbcopy / clip / xclip
peek --pack --ask "auth"        # only files matching auth (filename + content + analyzer bonus)
peek --pack -o pack.txt         # to file via safe writer
peek --pack --ask "nope" -o out.txt  # no match → graceful yellow warning
peek --pack --exclude "*.min.js" --budget 4000

# Find — ranked filename (10) + exact (5) + content (min5+occ*0.5 capped8) + analyzer*0.3, preview 3 lines
peek find "auth" .
peek find "validate_token" . --limit 5 --json
peek --find "auth" .  # alias via main_callback

# LLM — heuristic first, LLM optional
export OPENAI_API_KEY=sk-...  # or ANTHROPIC_API_KEY
peek --llm                # tries openai>=1.0 then 0.x then anthropic, fallback heuristic
peek analyze . --llm

Symbols

peek/peek/symbols.py -> index_symbols(scan_result) - zero-dep symbol index.

  • Python: ast walk -> def / class / async def with docstring, BOM utf-8-sig, skips empty/syntax errors.
  • JavaScript/TypeScript: regex fallback - JS_EXPORT_RE (export (default) function/class/const) and JS_IMPORT_RE (import ... from "x" / require("x")) -> def symbols. tree_sitter optional but avoided per brief.
  • Returns list[Symbol(name, kind, file, rel, lineno, docstring)] - never crashes.
from peek.scanner import scan
from peek.symbols import index_symbols
syms = index_symbols(scan("/path/to/repo"))
print(syms[:3])

See peek/tests/test_symbols.py (py def/class, JS export, BOM, lineno).


Semantic

peek/peek/embeddings.py -> BM25 with optional fastembed.

  • build_index(scan_result) chunks 30 lines -> 50-line chunks, tokens re.findall(r"\w+", lower), idf = log((N-df+0.5)/(df+0.5)+1), tf = count/len. search(index, query, k=10) -> ScoredChunk sorted desc, score>0 only (pads zeros for test fallback).
  • fastembed optional: tries TextEmbedding("sentence-transformers/all-MiniLM-L6-v2") but uses BM25 for determinism; never crashes.
  • Wire-up: peek/find.py -> if " " in query (intent like "auth token"), BM25 search -> dedup, score*10 + analyzer*0.1, fallback to keyword filename (10) + content (5+occ*0.5) + analyzer*0.3.
  • Pack v3: same semantic ranking for --ask "where is auth token validated"; filename bonus +5 per token.
peek find "auth token" .                # semantic - multi-word triggers BM25
peek find "auth token" --semantic       # explicit alias (docs hero)
peek --pack --ask "auth token" --semantic --budget 4000 --format md
peek index . --rebuild                  # build .peek/index.json

See peek/tests/test_embeddings.py.


Graph

peek/peek/graph.py -> export_graph(ar, format="dot|svg|html") - polyglot import graph viz.

  • Polyglot: peek/analyzer.py build_graph handles python (AST Import/ImportFrom, src/ strip, stdlib filter) and javascript/typescript (JS_IMPORT_RE relative ./x -> resolve + .js/.ts/.jsx/.tsx//index.js//index.ts via resolved map).
  • Export: build_dot -> digraph G { rankdir=LR; ... } (top 15 nodes,posix). build_svg -> tries dot -Tsvg (2s) else fallback SVG. export_graph(...,"html") wraps SVG.
  • CLI: peek graph [PATH] --format dot|svg|html -o out.svg (exit 2 on unknown).
peek graph . --format dot
peek graph --format svg -o graph.svg
peek graph --format html -o graph.html

See peek/tests/test_graph.py.


Git

Pack 3.0 - peek/peek/pack.py tiktoken-accurate, git-aware, clipboard, dry-run, URL fetch.

  • tiktoken: estimate_tokens tries tiktoken.get_encoding("cl100k_base") fallback len//4.
  • Glob: _glob_match -> fnmatch full posix + basename fallback ("*.py" matches sub/a.py) + Path.match for **.
  • Diff / staged: _get_diff_files(root, diff, staged) -> git diff --name-only (6s). build_pack(..., diff="HEAD"|"main", staged=True) filters to changed set.
  • Clip: _try_clip -> pyperclip.copy else sys.modules["pyperclip"] monkeypatch -> peek --pack --clip.
  • Dry-run: build_pack(..., dry_run=True) -> table | # | File | LOC | Tokens | Size | + header.
  • URL fetch: query https:// -> _fetch_url_and_build -> urllib fallback curl -L -s, detects tar/zip/plain, extracts to tempfile.mkdtemp, scans fetched dir, recurses build_pack.
peek --pack --diff HEAD --format xml -o pack.xml
peek --pack --staged --clip
peek --pack --dry-run --ask "auth token" --budget 4000
peek --pack --ask https://github.com/org/repo/archive/main.tar.gz

See peek/tests/test_pack_v3.py.


MCP

peek mcp starts a Model Context Protocol (MCP) stdio server, exposing peek's code analysis capabilities as tools for AI agents like Claude Code, Cursor, or Muse.

Quick Start / Installation

Register the MCP server with Muse:

Muse mcp add peek -- python -m peek.mcp_server

Or configure it manually in your client's settings (e.g., mcpServers configuration for Claude Code):

{
  "mcpServers": {
    "peek": {
      "command": "python",
      "args": ["-m", "peek.mcp_server"]
    }
  }
}

Alternatively, if peek is installed in your path, you can run:

peek mcp

Tools Reference

The server exposes the following tools:

Tool Name Description Arguments
peek_scan Scan repository — file stats, languages, tech stack, entry candidates. path (string, optional, default: .)
peek_rank Ranked Start Here — import graph PageRank + in-degree + entry bonus. path (string, optional, default: .)
peek_pack Pack ranked files for LLM context within token budget. path (string, optional)
query (string, optional): Keyword or URL filter
budget (integer, optional): Token budget (default: 8000)
format (string, optional): md, xml, or txt
peek_find Find files by keyword — filename + content + semantic BM25. query (string, required): Search intent/keyword
path (string, optional)
limit (integer, optional): Max results
peek_graph Export import graph as DOT/SVG/HTML. path (string, optional)
format (string, optional): dot, svg, or html
peek_explain Explain a Python traceback with scan-aware hints. traceback (string, optional): Traceback text
path (string, optional): Context path
file (string, optional): File containing traceback

WTF — Traceback Explainer

peek wtf tb.txt or cat tb.txt | peek wtf → parses Traceback + ...Error + frames, maps to scan files, hints if in Start Here top 5. No LLM needed.

Watch — Live Rescan

peek watch . → polling every 0.8s, debounce 0.4s, re-renders static; peek --watch → TUI with w toggle. watchfiles used if installed, else polling fallback.

Config Set

peek config set theme dracula → writes ~/.peek/config.toml, validates via get_theme, peek config get/list.


Config

# ~/.peek/config.toml  OR  ~/.config/peek/config.toml  OR  $XDG_CONFIG_HOME/peek/config.toml
# OR  $PEEK_CONFIG=/path/to/config.toml

theme = "dracula"  # any of 10 ids, case-insensitive, _ or - normalized
  • Missing / malformed TOML → silently ignored (tolerant), falls back to anthropic-pro.
  • tomllib (3.11+) with tomli fallback.
  • Persistent via peek config set theme dracula (writes ~/.peek/config.toml, validates via get_theme); also peek config get theme / peek config list. Manual echo 'theme = "dracula"' > ~/.peek/config.toml still works.
  • Precedence proof via test_resolve_precedence (cli > env > config > default).

Architecture

graph LR
  CLI[cli.py<br/>Typer + theme resolve] --> SCAN[scanner.py<br/>walk + pathspec + .gitignore]
  SCAN --> ANA[analyzer.py<br/>AST + graph + PageRank]
  ANA --> REN[renderer.py<br/>Rich panels + themed tokens]
  ANA --> TUI[tui.py<br/>Textual App + CSS templated]
  CLI --> REN
  CLI --> TUI
  CLI --> PACK[pack.py<br/>token budget]
  CLI --> FIND[find.py<br/>ranked search]
  CLI --> LLM[llm.py<br/>optional OpenAI/Anthropic]
  THEMES[themes.py<br/>10 × Theme + get/resolve] --> CLI
  THEMES --> REN
  THEMES --> TUI
  CONFIG[config.py<br/>toml] --> THEMES
  SCAN --> |FileInfo| ANA
  style THEMES fill:#D4A27F,stroke:#333,color:#111
  style CLI fill:#8AB4B8,stroke:#333

Key files:

File Owns
peek/peek/themes.py Theme dataclass, THEMES: dict[str,Theme] (10), get_theme (case-insensitive, _→-, ValueError if unknown), list_themes sorted, resolve_theme precedence, import-time validation (15 keys, #RRGGBB)
peek/peek/config.py config_path() (PEEK_CONFIG > XDG > ~/.config/peek > ~/.peek), load_config() tolerant
peek/peek/scanner.py scan(root, max_files) manual stack, skip DEFAULT_IGNORE_DIRS + pathspec gitwildmatch, _is_binary null-byte, _count_loc, _language_for, detect_tech_stack (PEP 621/poetry, package.json, requirements), detect_entry_points scoring, never crashes
peek/peek/analyzer.py _module_name_for (src/ strip), _relative_base, _extract_raw_imports (level), _resolve_local_import (exact/parent/suffix), build_graph (stdlib filtered via sys.stdlib_module_names, self-loop skip), build_reverse_graph, _pagerank (5 iter, damping 0.85), _has_main_guard (AST __name__=="__main__" + def main), rank_files (pr_norm*5 + in_norm + entry 5.0 + guard 0.5 + depth −3 for __init__ −1.5 for <10 LOC), summarize
peek/peek/renderer.py Themed helpers (_tokens, _theme_label), make_header…make_graph_panel, render_static (stagger 40/30/30/30/30/20 ms when TTY), build_html (embeds bg/accent), keeps ANTHRO alias for compat
peek/peek/tui.py PeekApp (theme-templated self.CSS with linear easing, fade 220ms/180ms/160ms, stagger 0.016+idx*0.008, filter _apply_filter with asyncio at top), run_tui fallback to static
peek/peek/cli.py ANTHRO_CLI + _theme_accent, _scan_with_spinner (Rich Progress SpinnerColumn(spinner_name="dots", style=accent) + threading 0.12 s min visible), _write_output_safely (win32 /tmp → tempfile.gettempdir(), mkdir -p, fallback peek.html), _should_animate, scan_command/analyze_command/find_command/main_callback (theme resolve, --theme-list table, theme threading)
peek/peek/animations.py _tok, anthropic_header, scan_progress (themed Progress)

Testing (TDD)

Iron law: No production code without a failing test first — watched fail, minimal green, refactor.

pytest -q          # 108 passed, 1 skipped
pytest -v
pytest peek/tests/test_comprehensive_tdd.py -v  # 29 passed, 1 skipped in that file alone
Suite Count What it proves
test_scanner.py 8 empty, ignores, gitignore, binary/huge, symlink, max_files, tech/entry, never crashes
test_analyzer.py 9 simple graph, relative, circular, syntax error, non-python, empty, frameworks, entry bonus, BOM
test_renderer_pack_find.py 12 render no crash, html contains, scan-only, estimate_tokens, pack basic/query/budget, find filename/content/no-match/empty, llm fallback, cli import
test_themes.py 14 10 registry, tokens shape, case-insensitive, unknown raises, fallback, resolve precedence, config missing, toml parse, render/html per-theme, tui CSS, cli list + unknown, backwards compat
test_comprehensive_tdd.py 29 +1 skip All of above plus CLI integration (--version/--help/--theme/--theme-list/scan/analyze --json/--html/find/pack/--no-tui + env, _write_output_safely, animations/ascii, run_tui fallbacks)
test_demo_assets.py 2 GIF valid (GIF89a, <3 MB, >5 KB), HTML exists (<html)
test_symbols.py 4 polyglot symbols: py def/class, JS export regex, BOM, lineno
test_embeddings.py 7 build_index chunks/tokens, BM25 search intent, fastembed fallback
test_graph.py 4 dot/svg/html export, polyglot JS edge, unknown format exit 2
test_pack_v3.py 10 tiktoken fallback, clip monkeypatch, diff/staged mock, URL fetch, dry_run table, semantic rerank
test_wtf.py 10 parse simple/multi/empty/no-exc, find_relevant_files, wtf CLI file/stdin, --no-explain, Exit 1 + help
test_pack_v2.py 8 pack md/xml/txt, include/exclude globs, budget truncate + token_budget alias, CLI format/include/budget+exclude
test_config_set.py 6 save_config/set_config_value, get_theme validation, CLI config set/get/list, invalid theme Exit 2
test_tui_live.py 5 BINDINGS has t, cycle changes theme, wraps after 10, _css_for_theme linear + dracula tokens, label/tokens update
test_watch.py 5 debounce, stop idempotent, new-file detection, cli help, w binding

TDD story: Added test_comprehensive_tdd.py → first run 2 failures (gitignore .git substring + SpinnerColumn(spinner=) typo) → minimal fixes → green. Added test_css_parse for ease-out → linear → verified via Stylesheet().add_source. Added test_tui_filter_asyncio for NameError: asyncio → added import asyncio at top.

Output pristine except pathspec deprecation warnings (dep, not our code).


Performance

  • < 0.2 s for peek itself (98 files, 7,736 LOC, 18 modules, 42 edges)
  • < 1 s for 500-file repos, hard cap 2000 (truncated flag)
  • scan with pathspec + manual stack, analyze 5-iter PageRank, render_static no Live (just time.sleep stagger)

Troubleshooting

Symptom Fix
CSS parsing failed: expected easing function; found 'ease-out' Fixed in themes-10 8dfb76a — use linear
NameError: asyncio is not defined in filter Fixed in 2b55926 — import asyncio at top
FileNotFoundError: '\\tmp\\cinematic.html' on Windows Fixed — _write_output_safely maps /tmp → %TEMP%
cp1252 █ UnicodeEncodeError sys.stdout.reconfigure(utf-8) + legacy_windows=False
No TUI, just static Install pip install textual or use peek --no-tui
Unknown theme 'foo' exit 2 Check peek --theme-list, case-insensitive, -/_ normalized

Contributing

See peek/CONTRIBUTING.md. PRs welcome — add tests per TDD. Branch polish-100 is latest (from themes-10 → main), local commits only, no push. Run ruff, pytest -q (108 passed, 1 skipped) before PR.

License

MIT — peek/LICENSE. Author Hariom Lohar — hariomlohar.new@gmail.com — hariomlohardev.github.io


Docs generated 2026-08-11 on branch polish-100. Theme previews in peek/assets/themes/*.svg (800×420). For the 5-day build story see docs/research/06_five_day_build_plan.md and docs/research/08_launch_playbook.md.