← back to Cli Printing Press
feat(vision): add Phase 0 Visionary Research - the press thinks before it prints
e25a1b4501f2a531d977d714d8f5e91e4e056830 · 2026-03-25 19:09:30 -0700 · Matt Van Horn
The printing press now asks "What would a thoughtful developer build?"
before generating any CLI. Inspired by steipete's discrawl, which proved
that a Discord CLI should be an archive/search tool, not just 307 CRUD commands.
New Phase 0 in SKILL.md:
- 0a: API identity and data profile analysis
- 0b: Usage pattern discovery with evidence scoring
- 0c: Tool landscape discovery (the "discrawl finder")
- 0d: Workflow analysis for compound commands
- 0e: Architecture planning (SQLite/FTS5/WebSocket decisions)
- 0f: Feature ideation with 16-point scoring rubric
New Go packages:
- internal/vision/ - types and report writer for visionary research
- internal/generator/vision_templates.go - template selector by data profile
7 new domain-aware templates:
- export.go.tmpl (JSONL export with pagination)
- import.go.tmpl (JSONL import from file/stdin)
- store.go.tmpl (SQLite + FTS5 persistence)
- search.go.tmpl (full-text search command)
- sync.go.tmpl (resumable API-to-SQLite sync)
- tail.go.tmpl (polling-based live event streaming)
- analytics.go.tmpl (count/group-by on local data)
Scorecard expanded: 10th dimension "Vision" (0-10), new max 100/100.
Pure API wrappers now correctly score 90/100 with Vision: 0/10.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
A docs/plans/2026-03-25-feat-discord-cli-research.mdA docs/plans/2026-03-25-feat-discord-cli-vs-discrawl-competitive-plan.mdA docs/plans/2026-03-25-feat-visionary-research-phase-plan.mdA docs/plans/2026-03-25-fix-discord-cli-audit.mdM internal/cli/root.goA internal/cli/scorecard.goA internal/cli/vision.goA internal/generator/templates/analytics.go.tmplA internal/generator/templates/export.go.tmplA internal/generator/templates/import.go.tmplA internal/generator/templates/search.go.tmplA internal/generator/templates/store.go.tmplA internal/generator/templates/sync.go.tmplA internal/generator/templates/tail.go.tmplA internal/generator/vision_templates.goM internal/pipeline/scorecard.goA internal/vision/report.goA internal/vision/vision.goM skills/printing-press/SKILL.md
Diff
commit e25a1b4501f2a531d977d714d8f5e91e4e056830
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Wed Mar 25 19:09:30 2026 -0700
feat(vision): add Phase 0 Visionary Research - the press thinks before it prints
The printing press now asks "What would a thoughtful developer build?"
before generating any CLI. Inspired by steipete's discrawl, which proved
that a Discord CLI should be an archive/search tool, not just 307 CRUD commands.
New Phase 0 in SKILL.md:
- 0a: API identity and data profile analysis
- 0b: Usage pattern discovery with evidence scoring
- 0c: Tool landscape discovery (the "discrawl finder")
- 0d: Workflow analysis for compound commands
- 0e: Architecture planning (SQLite/FTS5/WebSocket decisions)
- 0f: Feature ideation with 16-point scoring rubric
New Go packages:
- internal/vision/ - types and report writer for visionary research
- internal/generator/vision_templates.go - template selector by data profile
7 new domain-aware templates:
- export.go.tmpl (JSONL export with pagination)
- import.go.tmpl (JSONL import from file/stdin)
- store.go.tmpl (SQLite + FTS5 persistence)
- search.go.tmpl (full-text search command)
- sync.go.tmpl (resumable API-to-SQLite sync)
- tail.go.tmpl (polling-based live event streaming)
- analytics.go.tmpl (count/group-by on local data)
Scorecard expanded: 10th dimension "Vision" (0-10), new max 100/100.
Pure API wrappers now correctly score 90/100 with Vision: 0/10.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
docs/plans/2026-03-25-feat-discord-cli-research.md | 56 ++
...eat-discord-cli-vs-discrawl-competitive-plan.md | 443 ++++++++++++++
...026-03-25-feat-visionary-research-phase-plan.md | 652 +++++++++++++++++++++
docs/plans/2026-03-25-fix-discord-cli-audit.md | 73 +++
internal/cli/root.go | 2 +
internal/cli/scorecard.go | 72 +++
internal/cli/vision.go | 86 +++
internal/generator/templates/analytics.go.tmpl | 136 +++++
internal/generator/templates/export.go.tmpl | 106 ++++
internal/generator/templates/import.go.tmpl | 100 ++++
internal/generator/templates/search.go.tmpl | 77 +++
internal/generator/templates/store.go.tmpl | 227 +++++++
internal/generator/templates/sync.go.tmpl | 128 ++++
internal/generator/templates/tail.go.tmpl | 117 ++++
internal/generator/vision_templates.go | 122 ++++
internal/pipeline/scorecard.go | 55 +-
internal/vision/report.go | 126 ++++
internal/vision/vision.go | 113 ++++
skills/printing-press/SKILL.md | 265 ++++++++-
19 files changed, 2928 insertions(+), 28 deletions(-)
diff --git a/docs/plans/2026-03-25-feat-discord-cli-research.md b/docs/plans/2026-03-25-feat-discord-cli-research.md
new file mode 100644
index 00000000..f896f995
--- /dev/null
+++ b/docs/plans/2026-03-25-feat-discord-cli-research.md
@@ -0,0 +1,56 @@
+---
+title: "Research: Discord CLI"
+type: feat
+status: active
+date: 2026-03-25
+---
+
+# Research: Discord CLI
+
+## Spec Discovery
+- Official OpenAPI spec: https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json
+- Source: Known-specs registry (discord/discord-api-spec GitHub repo)
+- Format: JSON, OpenAPI 3.1.0
+- Endpoint count: 200+ (Discord's full REST API)
+
+## Competitors (Deep Analysis)
+
+### discli (ibbybuilds/discli) - 6 stars
+- Repo: https://github.com/ibbybuilds/discli
+- Language: TypeScript/JavaScript (Node.js)
+- Commands: ~51 across 9 resource groups (server, invites, channels, roles, members, permissions, messages, emojis, audit)
+- Last commit: Recent (2026)
+- Open issues: 0
+- Maintained: Yes (new project)
+- Notable features: YAML output by default (token-efficient for AI), --confirm for destructive ops, SOUL.md personality customization, no WebSocket needed
+- Weaknesses: Only 6 stars, limited to subset of Discord API (no webhooks, slash commands, scheduled events, threads, auto-mod, stickers, stage instances, voice regions), TypeScript (requires Node.js runtime)
+
+### discordo (ayn2op/discordo) - 5,400 stars
+- Repo: https://github.com/ayn2op/discordo
+- Language: Go
+- Commands: N/A (TUI chat client, not API management CLI)
+- Last commit: Active
+- Open issues: 45
+- Maintained: Yes, 40 contributors
+- Notable features: Terminal Discord chat client, QR code login, TOML config
+- Weaknesses: Different category entirely - interactive chat TUI, not an API management CLI. Violates Discord ToS (automated user accounts). Not comparable.
+
+## User Pain Points
+> "Using external clients were against Discord's wish and terms of service" - discord-cli README (highlights the need for bot-token-based API CLIs that are ToS-compliant)
+> "Official plugin support and third party client" discussion on discord-api-docs shows demand for programmatic Discord access beyond the web UI - Discussion #3857
+
+## Auth Method
+- Type: Bot token (Bearer) via Authorization header
+- Env var convention: DISCORD_BOT_TOKEN (discli uses this)
+
+## Demand Signals
+- discord/discord-api-docs Discussion #3857: users requesting official third-party client support
+- discli's "for AI agents and humans" framing shows the agent-native CLI demand is recognized but underserved (6 stars = early, not widely adopted)
+
+## Strategic Justification
+**Why this CLI should exist:** discli is the only API management CLI and has just 6 stars with ~51 commands covering a fraction of Discord's 200+ endpoint API. It requires Node.js runtime. Our CLI will be a single Go binary covering the full Discord API spec, with --json/--select/--dry-run/--stdin for agent workflows, and proper doctor/cache/auth patterns. We'll beat discli on breadth (full API coverage vs partial) and distribution (single binary vs npm install).
+
+## Target
+- Command count: 80+ (match full API spec coverage, beat discli's 51)
+- Key differentiator: Full API coverage in a single Go binary with agent-native features (--json, --select, --dry-run, --stdin, doctor, cache)
+- Quality bar: Steinberger Grade A (72+/90)
diff --git a/docs/plans/2026-03-25-feat-discord-cli-vs-discrawl-competitive-plan.md b/docs/plans/2026-03-25-feat-discord-cli-vs-discrawl-competitive-plan.md
new file mode 100644
index 00000000..5043e792
--- /dev/null
+++ b/docs/plans/2026-03-25-feat-discord-cli-vs-discrawl-competitive-plan.md
@@ -0,0 +1,443 @@
+---
+title: "Discord CLI vs Discrawl: Deep Competitive Analysis & Gap Closure Plan"
+type: feat
+status: active
+date: 2026-03-25
+---
+
+# Discord CLI vs Discrawl: Deep Competitive Analysis & Gap Closure Plan
+
+## Overview
+
+Line-by-line comparison of two Go-based Discord CLI tools, followed by a gap analysis and phased implementation plan to close the gaps.
+
+- **discord-cli** (ours): REST API management CLI generated by CLI Printing Press. 307 commands covering the full Discord v10 API. Stateless, agent-native.
+- **discrawl** (steipete): Discord archive/search CLI by Peter Steinberger. 11 commands focused on mirroring Discord data to local SQLite with FTS5 search. Stateful, data-oriented.
+
+---
+
+## Part 1: Line-by-Line Command Comparison
+
+### discrawl Commands vs discord-cli Equivalents
+
+#### 1. `discrawl init`
+
+**What it does:** Creates `~/.discrawl/config.toml`, discovers all guilds the bot can access, writes guild IDs to config, auto-selects default guild if only one exists.
+
+**Flags:** `--from-openclaw <path>`, `--guild <id>`, `--db <path>`, `--with-embeddings`
+
+**discord-cli equivalent:**
+- `discord-cli auth login` - sets up token (partial overlap)
+- `discord-cli doctor` - validates auth (partial overlap)
+- **GAP:** No guild discovery. No config bootstrapping that auto-discovers accessible guilds and lets you pick a default. No database setup.
+
+**What discrawl does better:** Single command goes from zero to configured. Discovers guilds, writes config, sets defaults. discord-cli requires manual setup: set env var, run doctor, figure out guild IDs yourself.
+
+---
+
+#### 2. `discrawl sync`
+
+**What it does:** Backfills guild data (channels, threads, members, messages) into SQLite. Parallel channel workers (8-32, auto-sized). Resumable via `sync_state` checkpoints. Auto-batches incomplete channels in groups of 25.
+
+**Flags:** `--full`, `--guild <id>`, `--guilds <csv>`, `--channels <csv>`, `--concurrency <n>`, `--since <RFC3339>`, `--with-embeddings`
+
+**discord-cli equivalent:** **NONE.** discord-cli has no local storage, no sync, no backfill capability. Every command is a stateless API call.
+
+**What discrawl does better:** Enables offline analysis. Rate-limit-aware parallel crawling with resumable checkpoints. discord-cli can only fetch data one API call at a time with no persistence.
+
+---
+
+#### 3. `discrawl tail`
+
+**What it does:** Runs live Gateway WebSocket event streaming plus periodic repair cycles. Two concurrent processes: real-time event handler + repair ticker (default 6h). Handles MessageCreate, MessageUpdate, MessageDelete, ChannelUpsert, MemberUpsert, MemberDelete. 4-16 worker pool with 30s handler timeout and panic recovery. Graceful SIGTERM/SIGINT shutdown.
+
+**Flags:** `--guild <id>`, `--repair-every <duration>`
+
+**discord-cli equivalent:** **NONE.** discord-cli is REST-only. No WebSocket, no Gateway, no real-time events, no long-running processes.
+
+**What discrawl does better:** Live data pipeline. Self-healing with repair cycles. discord-cli can only poll via repeated REST calls.
+
+---
+
+#### 4. `discrawl search`
+
+**What it does:** FTS5 full-text search over archived messages. Searches message text, attachment text, attachment filenames, embeds, and replies. Falls back to LIKE-based matching when FTS fails. Query normalization wraps terms in quotes.
+
+**Flags:** `--guild <id>`, `--channel <name>`, `--author <name>`, `--limit <n>`, `--include-empty`, `--guilds <csv>`, `--mode`, `--json`
+
+**discord-cli equivalent:**
+- `discord-cli guilds messages guild-search <guild_id>` - searches messages via Discord's API
+- **PARTIAL:** API search is rate-limited, slow for large servers, and only searches current messages (no history if messages are deleted)
+
+**What discrawl does better:** Instant local FTS5 search. No rate limits. Searches attachment text and filenames. Works offline. Persists deleted message content. discord-cli's API search is limited to Discord's server-side search capabilities.
+
+---
+
+#### 5. `discrawl messages`
+
+**What it does:** Lists exact message slices from local SQLite. Requires at least one filter (channel, author, time range). Supports `--sync` flag to run a blocking pre-query sync before reading.
+
+**Flags:** `--channel <id|name|#name>`, `--author <name>`, `--guild <id>`, `--guilds <csv>`, `--hours <n>`, `--days <n>`, `--since <RFC3339>`, `--before <RFC3339>`, `--limit <n>`, `--last <n>`, `--all`, `--sync`, `--include-empty`, `--json`
+
+**discord-cli equivalent:**
+- `discord-cli channels messages list <channel_id>` - lists messages from a channel via API
+- `discord-cli channels messages get <channel_id> <message_id>` - gets a single message
+
+**What discrawl does better:** Rich time-range filters (`--hours`, `--days`, `--since`, `--before`). Author and channel name matching (not just IDs). `--last N` for newest messages. `--sync` for fresh data before query. Cross-channel queries. discord-cli can only list messages from one channel at a time using the API, with no time filters beyond Discord's API parameters.
+
+---
+
+#### 6. `discrawl mentions`
+
+**What it does:** Queries structured mention events from `mention_events` table. Joins across 4 tables (mention_events, messages, channels, members). Supports user and role mention types. Cascading author name resolution (display_name -> nickname -> global_name -> username -> JSON-extracted fields).
+
+**Flags:** `--channel <name>`, `--target <id|name>`, `--type <user|role>`, `--limit <n>`, `--days <n>`, `--hours <n>`, `--guild <id>`, `--guilds <csv>`, `--json`
+
+**discord-cli equivalent:** **NONE.** discord-cli has no mention tracking. The closest is `channels messages list` with manual content parsing.
+
+**What discrawl does better:** Structured mention data as first-class queryable entities. Tracks who was mentioned, where, when, by whom. discord-cli treats message content as opaque strings.
+
+---
+
+#### 7. `discrawl sql`
+
+**What it does:** Runs arbitrary read-only SQL against the local SQLite database. Supports stdin piping. Safety gate: write queries require both `--unsafe` and `--confirm`. 50,000 row cap, 15-second timeout.
+
+**Flags:** `--unsafe`, `--confirm`, `--json`
+
+**discord-cli equivalent:** **NONE.** discord-cli has no local database.
+
+**What discrawl does better:** Ultimate flexibility - any analysis the user can express in SQL. Power-user escape hatch. discord-cli has no equivalent.
+
+---
+
+#### 8. `discrawl members`
+
+**What it does:** Offline member directory with 3 subcommands: `list` (up to 500 members), `show <id|query>` (detailed profile + recent messages), `search <query>` (FTS across names, bios, social handles, URLs). Recursive JSON walking extracts bios, X/Twitter handles, GitHub handles, websites, pronouns. Filters out sensitive data (tokens, emails).
+
+**Flags (show):** `--messages <n>`
+
+**discord-cli equivalent:**
+- `discord-cli guilds members list-guild <guild_id>` - lists members via API
+- `discord-cli guilds members get-guild <guild_id> <user_id>` - gets a member
+- `discord-cli guilds members search-guild <guild_id>` - searches members by nickname/username
+
+**What discrawl does better:** Profile intelligence - extracts social handles, bios, URLs from raw JSON. FTS member search across all profile fields. Recent message history per member. discord-cli returns raw API responses with no profile enrichment.
+
+---
+
+#### 9. `discrawl channels`
+
+**What it does:** Lists or shows channel metadata from local SQLite. Shows NSFW flags, archive status, position ordering.
+
+**Subcommands:** `list`, `show <id>`
+
+**discord-cli equivalent:**
+- `discord-cli guilds channels list-guild <guild_id>` - lists channels via API
+- `discord-cli channels get <channel_id>` - gets a channel
+
+**Parity:** Roughly equivalent for basic listing. discrawl adds archive status and position ordering from its normalized schema.
+
+---
+
+#### 10. `discrawl status`
+
+**What it does:** Shows local archive completeness for the default guild. How many channels synced, message counts, last sync time.
+
+**discord-cli equivalent:** **NONE.** discord-cli has no local state to report on.
+
+---
+
+#### 11. `discrawl doctor`
+
+**What it does:** 6-component health check: config file integrity, Discord token resolution (reports source not contents), bot authentication success, guild accessibility count, database connectivity, FTS5 wiring verification.
+
+**discord-cli equivalent:**
+- `discord-cli doctor` - checks config, auth, API connectivity, version
+
+**Parity:** Both have doctor commands. discrawl's checks database and FTS health. discord-cli's checks API connectivity. Both validate auth without exposing tokens.
+
+---
+
+### discord-cli Commands with No discrawl Equivalent
+
+discrawl is read-only (archive/search). It cannot:
+
+| discord-cli capability | Commands |
+|---|---|
+| **Send messages** | `channels messages create` |
+| **Manage channels** | `channels delete`, `channels update`, `guilds channels create-guild` |
+| **Manage roles** | `guilds roles create/delete/update-guild` |
+| **Manage bans** | `guilds bans user-from-guild`, `guilds bans unban-user-from-guild` |
+| **Manage emojis** | `guilds emojis create/delete/update-guild` |
+| **Register slash commands** | `applications commands create-application` |
+| **Execute webhooks** | `webhooks execute` |
+| **Manage stage instances** | `stage-instances create/update/delete` |
+| **Manage scheduled events** | `guilds scheduled-events create/update/delete-guild` |
+| **Manage auto-moderation** | `guilds auto-moderation create/update/delete-rule` |
+| **Manage invites** | `invites resolve/revoke`, `channels invites create-channel` |
+| **Manage threads** | `channels threads create`, `channels thread-members add/delete` |
+| **Manage reactions** | `channels messages add-my-reaction`, `delete-all-reactions` |
+| **Manage pins** | `channels messages create-pin`, `delete-pin` |
+| **OAuth2 flows** | `oauth2 get-my-application`, `auth login` |
+| **Manage entitlements** | `applications entitlements create/consume/delete` |
+| **Manage lobbies** | `lobbies create-lobby`, `lobbies members add-lobby` |
+| **Manage soundboard** | `guilds soundboard-sounds create/update/delete-guild` |
+| **Manage stickers** | `guilds stickers create/update/delete-guild` |
+| **Manage welcome screen** | `guilds welcome-screen update-guild` |
+| **Manage onboarding** | `guilds onboarding put-guilds` |
+
+**Total:** ~280 write/management commands that discrawl cannot do.
+
+---
+
+## Part 2: Design Decision Comparison
+
+### Architecture
+
+| Dimension | discord-cli | discrawl |
+|---|---|---|
+| **Data model** | Stateless REST | Local SQLite with FTS5 |
+| **Persistence** | 5-min GET cache only | Full archive database |
+| **Protocol** | REST API only | REST + Gateway WebSocket |
+| **Concurrency** | Single request per command | 8-32 parallel workers |
+| **Rate limiting** | Client-side retry with backoff | discordgo built-in + context timeouts |
+| **Auth** | Bot token, OAuth2, custom header | Bot token only (+ OpenClaw integration) |
+| **Config format** | TOML at `~/.config/discord-cli/` | TOML at `~/.discrawl/` |
+| **Output** | JSON, CSV, plain, table, quiet | JSON, text tables |
+| **Field selection** | `--select id,name` | None (use `sql` or `jq`) |
+| **Dry-run** | `--dry-run` shows request | None (read-only anyway) |
+| **Agent-native** | Full (--json, --select, --stdin, --yes, typed exits) | Partial (--json, --quiet) |
+| **CGO** | Standard Go | Pure Go SQLite (modernc.org/sqlite) |
+| **Cross-compilation** | Standard | 6 platforms via GoReleaser |
+
+### Error Handling
+
+| Aspect | discord-cli | discrawl |
+|---|---|---|
+| **Exit codes** | 7 typed codes (0, 2, 3, 4, 5, 7, 10) | Standard Go (0/1) |
+| **Idempotency** | 409=success, 404 on delete=success | N/A (read-only) |
+| **Retry** | 3 retries, exponential backoff for 5xx, Retry-After for 429 | discordgo built-in rate limiting |
+| **Error hints** | "Run doctor" suggestions | Descriptive error messages |
+
+### Search Capabilities
+
+| Aspect | discord-cli | discrawl |
+|---|---|---|
+| **Method** | Discord API search (server-side) | Local FTS5 (instant) |
+| **Speed** | Network-bound, rate-limited | Milliseconds (local) |
+| **Scope** | Current messages only | Full history + deleted messages |
+| **Attachment search** | Not supported | Text attachments indexed |
+| **Offline** | No | Yes |
+| **Embeddings** | No | Optional OpenAI embeddings |
+| **Mention tracking** | No | Structured mention_events table |
+
+### Member Intelligence
+
+| Aspect | discord-cli | discrawl |
+|---|---|---|
+| **Profile data** | Raw API response | Extracted bios, social handles, URLs |
+| **Social handles** | Not parsed | X/Twitter, GitHub extracted |
+| **Member search** | API name/nickname search | FTS across all profile fields |
+| **Activity** | Not tracked | Message count, first/last message |
+
+---
+
+## Part 3: Gap Analysis
+
+### Critical Gaps (discrawl has, discord-cli lacks entirely)
+
+1. **Local data persistence** - No SQLite, no FTS, no offline capability
+2. **Gateway WebSocket** - No real-time event streaming
+3. **Sync/backfill** - No way to archive message history
+4. **Full-text search** - Only API search, no local search
+5. **Mention tracking** - No structured mention data
+6. **Member profile intelligence** - No social handle extraction
+7. **SQL escape hatch** - No arbitrary query capability
+8. **Guild auto-discovery** - `init` doesn't discover accessible guilds
+9. **Pre-query sync** - No `--sync` flag to refresh before reading
+10. **Attachment text indexing** - No text extraction from attachments
+
+### Moderate Gaps (discrawl does better)
+
+11. **Time-range filters** - No `--hours`, `--days`, `--since`, `--before` on message listing
+12. **Channel name matching** - Must use IDs, not names or `#channel` syntax
+13. **Author name filtering** - Must use user IDs, not display names
+14. **Archive status reporting** - No `status` command showing completeness
+15. **Repair cycles** - No self-healing data pipeline
+
+### Minor Gaps (polish differences)
+
+16. **Cross-compilation** - No GoReleaser, no multi-platform binaries
+17. **Verbose logging** - No `--verbose`/`-v` flag
+18. **Config env override** - No `DISCORD_CONFIG` env var for config path
+19. **OpenClaw integration** - No token import from OpenClaw
+
+### discord-cli Advantages (discrawl lacks)
+
+1. **Full API coverage** - 307 commands vs 11
+2. **Write operations** - Send messages, manage roles, create channels, etc.
+3. **Agent-native design** - `--select`, `--stdin`, `--dry-run`, `--yes`, typed exit codes
+4. **OAuth2** - Full OAuth2 flow with token refresh
+5. **Field selection** - `--select id,name` for output filtering
+6. **Dry-run** - Preview requests without sending
+7. **CSV output** - Additional output format
+8. **Pagination** - Cursor-based with NDJSON progress events
+
+---
+
+## Part 4: Implementation Plan
+
+### Strategic Direction
+
+discord-cli and discrawl are **complementary tools** with different missions:
+- discord-cli = **API management** (CRUD operations, agent workflows)
+- discrawl = **data intelligence** (archive, search, analyze)
+
+The gap closure plan focuses on cherry-picking discrawl's best ideas that enhance discord-cli's core mission (API management + agent workflows) without turning it into a different tool.
+
+### Phase 1: Quick Wins (1-2 hours)
+
+Low-effort improvements that make discord-cli more useful for common workflows.
+
+#### 1.1 Guild Discovery Command
+Add `discord-cli guilds list-my` that calls GET `/users/@me/guilds` and lists all accessible guilds. This is the equivalent of discrawl's init-time guild discovery.
+
+**File:** `internal/cli/guilds_list-my.go`
+**Effort:** New command, follows existing patterns
+
+#### 1.2 Time-Range Flags for Message Listing
+Add `--hours <n>`, `--days <n>`, `--since <RFC3339>`, `--before <RFC3339>` to `channels messages list`.
+
+**File:** `internal/cli/channels_messages_list.go`
+**Effort:** 4 new flags, convert to Discord API `before`/`after` snowflake parameters
+
+#### 1.3 Verbose Flag
+Add `--verbose`/`-v` global flag for debug logging (request/response details to stderr).
+
+**File:** `internal/cli/root.go`, `internal/client/client.go`
+**Effort:** Flag + conditional logging in HTTP client
+
+#### 1.4 Config Path Env Var
+Support `DISCORD_CLI_CONFIG` env var as alternative to `--config` flag.
+
+**File:** `internal/config/config.go`
+**Effort:** Single env var check
+
+### Phase 2: Search & Intelligence (3-5 hours)
+
+Features that bring discrawl-like intelligence to API-based workflows.
+
+#### 2.1 Message Search with Rich Filters
+Enhance `guilds messages guild-search` with Discord API search parameters:
+- `--author <name>` (resolve name to user ID, then use `author_id` API param)
+- `--channel <name>` (resolve name to channel ID, then use `channel_id` API param)
+- `--has <attachment|embed|link>` (Discord API's `has:` filter)
+- `--mentions <user>` (Discord API's `mentions:` filter)
+
+**Files:** `internal/cli/guilds_messages_guild-search.go`
+**Effort:** Flag additions + name-to-ID resolution helper
+
+#### 2.2 Member Profile Enrichment
+Add a `guilds members show` command that fetches a member and enriches the output:
+- Parse `bio` field for URLs, social handles
+- Show role names (resolve role IDs)
+- Show join date, account creation date (from snowflake)
+
+**File:** New `internal/cli/guilds_members_show.go`
+**Effort:** New command + profile parsing logic
+
+#### 2.3 Channel Name Resolution
+Add a `--channel-name` flag (or accept `#channel-name` as positional arg) that resolves channel names to IDs by listing guild channels first.
+
+**File:** `internal/cli/helpers.go` (new `resolveChannelName` helper)
+**Effort:** Helper function + integration into message commands
+
+### Phase 3: Local Cache Enhancement (5-8 hours)
+
+Extend the existing 5-minute GET cache into something more useful for data analysis workflows.
+
+#### 3.1 Persistent Cache Mode
+Add `--cache-persist` flag that saves GET responses to `~/.cache/discord-cli/` with no TTL expiration. Useful for building up a local dataset over repeated queries.
+
+**File:** `internal/client/client.go`
+**Effort:** New cache mode alongside existing TTL cache
+
+#### 3.2 Cache Search
+Add `discord-cli cache search <query>` that greps cached responses. Quick local search without FTS overhead.
+
+**File:** New `internal/cli/cache_search.go`
+**Effort:** New command, simple file grep over JSON cache
+
+#### 3.3 Cache Export
+Add `discord-cli cache export --format sqlite|jsonl` that exports cached data to SQLite or JSONL for external analysis.
+
+**File:** New `internal/cli/cache_export.go`
+**Effort:** New command + SQLite writer (or just JSONL for simplicity)
+
+### Phase 4: Workflow Conveniences (2-3 hours)
+
+#### 4.1 `discord-cli init` Command
+One-command setup: prompts for token (or reads from env), runs doctor, discovers guilds, shows available guilds, writes config with defaults.
+
+**File:** New `internal/cli/init.go`
+**Effort:** New command combining auth + doctor + guild list
+
+#### 4.2 Mention Lookup
+Add `discord-cli guilds messages search --mentions <user>` that uses Discord's API search to find messages mentioning a user.
+
+**File:** Enhancement to `guilds_messages_guild-search.go`
+**Effort:** Flag addition + API parameter
+
+#### 4.3 Bulk Message Export
+Add `discord-cli channels messages export <channel_id> --format jsonl --since <date>` that paginates through all messages and exports to JSONL. Agent-friendly bulk data extraction.
+
+**File:** New `internal/cli/channels_messages_export.go`
+**Effort:** Pagination loop + JSONL writer
+
+### Phase 5: Future Consideration (Not Recommended Now)
+
+These are discrawl's core differentiators that would fundamentally change discord-cli's architecture. Listed for completeness but **not recommended** - use discrawl for these instead.
+
+- **SQLite persistence** - Would require major architectural change. Use discrawl.
+- **Gateway WebSocket streaming** - Different runtime model (long-running process). Use discrawl.
+- **FTS5 search** - Requires local persistence. Use discrawl.
+- **Repair cycles** - Requires tail + sync infrastructure. Use discrawl.
+- **Embeddings** - Requires local persistence + OpenAI integration. Use discrawl.
+
+**Recommended approach:** Document discrawl as a companion tool in the README. "Use discord-cli for API management, discrawl for archive and search."
+
+---
+
+## Part 5: Acceptance Criteria
+
+### Phase 1
+- [ ] `discord-cli guilds list-my --json` returns all accessible guilds
+- [ ] `discord-cli channels messages list <id> --hours 24` returns last 24h of messages
+- [ ] `discord-cli channels messages list <id> --since 2026-03-01T00:00:00Z` works
+- [ ] `discord-cli --verbose doctor` shows HTTP request details on stderr
+- [ ] `DISCORD_CLI_CONFIG=~/custom.toml discord-cli doctor` uses custom config
+
+### Phase 2
+- [ ] `discord-cli guilds messages guild-search <id> --author steipete` resolves name to user ID
+- [ ] `discord-cli guilds messages guild-search <id> --has attachment` filters by attachment
+- [ ] `discord-cli guilds members show <guild_id> <user_id>` shows enriched profile
+- [ ] Channel name resolution works with `#channel-name` syntax
+
+### Phase 3
+- [ ] `discord-cli channels messages list <id> --cache-persist` saves responses permanently
+- [ ] `discord-cli cache search "error"` finds cached responses containing "error"
+- [ ] `discord-cli cache export --format jsonl > messages.jsonl` exports cache
+
+### Phase 4
+- [ ] `discord-cli init` does full setup in one command
+- [ ] `discord-cli channels messages export <id> --format jsonl --since 2026-03-01T00:00:00Z` exports all messages
+
+---
+
+## Sources
+
+- discrawl repo: https://github.com/steipete/discrawl (539 stars, Go, MIT)
+- discli repo: https://github.com/ibbybuilds/discli (6 stars, TypeScript)
+- Discord API spec: https://github.com/discord/discord-api-spec
+- discord-cli generated code: `/Users/mvanhorn/cli-printing-press/discord-cli/`
diff --git a/docs/plans/2026-03-25-feat-visionary-research-phase-plan.md b/docs/plans/2026-03-25-feat-visionary-research-phase-plan.md
new file mode 100644
index 00000000..092ab914
--- /dev/null
+++ b/docs/plans/2026-03-25-feat-visionary-research-phase-plan.md
@@ -0,0 +1,652 @@
+---
+title: "feat: Visionary Research Phase - Make the Press Think Like Steinberger"
+type: feat
+status: completed
+date: 2026-03-25
+---
+
+# Visionary Research Phase - Make the Press Think Like Steinberger
+
+## The Humiliation
+
+Peter Steinberger looked at Discord's API and didn't think "I should generate 307 Cobra commands from the OpenAPI spec." He thought: "Developers need to search their Discord history. The API rate limits make that painful. I'll mirror everything into SQLite with FTS5 and add a WebSocket tail for live updates."
+
+The printing press looked at the same API and generated an API printer. 307 commands. 90/90 Steinberger scorecard. And completely pointless compared to discrawl. A perfect score on a test that measures the wrong thing.
+
+**The scorecard measures whether a CLI looks like gogcli. It doesn't measure whether a CLI is worth using.**
+
+The press has two existing plans for getting smarter (Intelligence Engine + LLM Brain Before Generation). Both still produce API wrappers - just smarter API wrappers. A smarter API wrapper is still an API wrapper.
+
+This plan adds a phase that asks the question Peter asked: **"What would a thoughtful developer actually build for this API?"**
+
+---
+
+## Problem Statement
+
+### What the Press Does Today
+
+```
+Phase 1: Find OpenAPI spec, find competitors, find issues
+Phase 2: Generate Cobra commands from spec
+Phase 3: Score against Steinberger (pattern matching)
+Phase 4: Fix scored dimensions
+Phase 5: Re-score, report
+```
+
+Every API gets the same treatment. Discord gets 307 commands. Stripe gets 200 commands. Notion gets 50 commands. They all look the same: `<api>-cli <resource> <verb> [flags]`. They're all interchangeable. None of them are discrawl.
+
+### What's Missing
+
+The press never asks:
+
+1. **What do people actually DO with this API?** (Discord: archive servers, search history, moderate, monitor)
+2. **What tools exist that AREN'T just API wrappers?** (discrawl: SQLite archive with FTS5 search)
+3. **What workflows need LOCAL STATE beyond a 5-minute cache?** (transaction sync, message archive, event streaming)
+4. **What would make this CLI worth installing over `curl | jq`?** (intelligence, not just convenience)
+5. **What are the next 5 features this CLI could have that would be good for the world?**
+
+### The Discrawl Test
+
+For every API, there's a potential discrawl - a tool that a thoughtful human would build that goes beyond the API surface. The press should discover it.
+
+| API | API Wrapper | Discrawl Equivalent |
+|---|---|---|
+| Discord | 307 CRUD commands | Archive server to SQLite, FTS5 search, WebSocket tail, mention tracking |
+| Stripe | Payment CRUD commands | Local transaction ledger, webhook test server, fixture chains, reconciliation engine |
+| Notion | Page/database CRUD | Local Markdown sync, offline editing, full-text search, backlinks graph |
+| GitHub | Issue/PR CRUD | Codebase analytics, contributor graphs, review queue dashboard, notification triage |
+| Slack | Channel/message CRUD | Channel archive, search across workspaces, conversation export, bot management |
+| Linear | Issue CRUD | Sprint analytics, team velocity dashboard, issue dependency graph, standup generator |
+
+The press should discover these ideas BEFORE generating anything.
+
+---
+
+## Proposed Solution: Phase 0 - Visionary Research
+
+Add a new phase BEFORE the current Phase 1 that discovers what a thoughtful CLI would look like. This phase uses the osc-newfeature methodology adapted for CLI generation.
+
+### New Pipeline
+
+```
+PHASE 0: VISIONARY RESEARCH (new - 8-12 min)
+ 0a. API Identity & Domain Understanding
+ 0b. Usage Pattern Discovery (what people DO with this API)
+ 0c. Tool Landscape Discovery (what exists BEYOND API wrappers)
+ 0d. Workflow Analysis (what multi-step sequences matter)
+ 0e. Architecture Planning (what local state, persistence, protocols)
+ 0f. Feature Ideation (top 5 features scored by evidence)
+
+PHASE 1: DEEP RESEARCH (existing - enhanced)
+ Now informed by Phase 0's visionary plan
+ Competitor search now includes non-wrapper tools like discrawl
+
+PHASE 2: GENERATE (existing - enhanced)
+ Now generates BOTH API wrapper commands AND visionary commands
+ Visionary commands may be stubs with TODO markers
+
+PHASE 3-5: AUDIT, FIX, REPORT (existing)
+ Scorecard expanded with "Vision" dimension
+```
+
+---
+
+## Technical Approach
+
+### Phase 0a: API Identity & Domain Understanding
+
+**Goal:** Understand what this API IS, not just what endpoints it has.
+
+**Method:** LLM reads the API's homepage, developer docs landing page, and "Getting Started" guide. Extracts:
+
+- **Domain category:** messaging, payments, productivity, infrastructure, analytics
+- **Primary users:** Who uses this API? (developers building bots, businesses processing payments, teams managing projects)
+- **Core entities:** What are the main objects? (Discord: guilds, channels, messages, members. Stripe: customers, charges, subscriptions)
+- **Data characteristics:** Is data write-heavy or read-heavy? Append-only or mutable? Real-time or batch? Large or small?
+
+**Implementation:**
+
+```go
+// internal/vision/identity.go
+type APIIdentity struct {
+ Domain string // "messaging", "payments", "productivity"
+ PrimaryUsers []string // "bot developers", "server admins"
+ CoreEntities []string // "guilds", "channels", "messages"
+ DataProfile DataProfile
+}
+
+type DataProfile struct {
+ WritePattern string // "append-only", "mutable", "event-sourced"
+ Volume string // "high" (millions of records), "medium", "low"
+ Realtime bool // does the API have webhooks/websockets?
+ SearchNeed string // "high" (users need to find things), "low"
+}
+```
+
+**Sources:**
+- WebFetch the API docs landing page
+- WebFetch the "Getting Started" or "Introduction" page
+- WebSearch `"<API name>" developer documentation overview`
+
+---
+
+### Phase 0b: Usage Pattern Discovery
+
+**Goal:** Discover what people ACTUALLY DO with this API. Not what the spec says - what humans need.
+
+**Method:** Adapted from osc-newfeature's demand signal methodology.
+
+**Step 1: Community Research (3-4 parallel searches)**
+
+```
+WebSearch: "<API name>" CLI workflow site:reddit.com
+WebSearch: "<API name>" automation script site:github.com
+WebSearch: "<API name>" "I built" OR "I made" OR "my tool" site:reddit.com OR site:news.ycombinator.com
+WebSearch: "<API name>" tutorial automation workflow 2025 2026
+```
+
+**Step 2: GitHub Non-Wrapper Tool Discovery**
+
+This is the critical search that would have found discrawl:
+
+```
+WebSearch: "<API name>" tool NOT "api wrapper" NOT "sdk" site:github.com
+WebSearch: "<API name>" sync OR archive OR export OR backup site:github.com
+WebSearch: "<API name>" search OR analytics OR dashboard site:github.com
+WebSearch: "<API name>" monitor OR watch OR tail site:github.com
+```
+
+For each tool found:
+- Is it an API wrapper or something MORE?
+- What local functionality does it add? (database, cache, search, streaming)
+- What workflow does it enable that raw API calls don't?
+- How many stars? Is it maintained?
+
+**Step 3: Stack Overflow / Forum Pain Points**
+
+```
+WebSearch: "<API name>" API "pain point" OR "limitation" OR "workaround" OR "wish"
+WebSearch: site:stackoverflow.com "<API name>" API rate limit OR pagination OR bulk
+```
+
+Extract the top 5 pain points people have with the raw API.
+
+**Step 4: Usage Pattern Synthesis**
+
+From all research, identify the **top 5 usage patterns** ranked by evidence:
+
+```markdown
+## Usage Patterns (Evidence-Backed)
+
+### 1. Archive & Search (Evidence: 8/10)
+- discrawl (539 stars) mirrors Discord to SQLite
+- Reddit: 12 posts asking "how to search old Discord messages"
+- Pain point: Discord's built-in search is slow and limited
+- **What it needs:** Local database, FTS, resumable sync
+
+### 2. Moderation Automation (Evidence: 6/10)
+- discli (6 stars) focuses on server management
+- GitHub: multiple moderation bots reference the API
+- Pain point: managing bans/roles across channels is tedious
+- **What it needs:** Bulk operations, templated actions
+
+### 3. Deploy Notifications (Evidence: 5/10)
+- Many CI/CD pipelines post to Discord webhooks
+- Reddit: common "how to send deploy notification" questions
+- **What it needs:** Webhook execute with templates, --stdin
+```
+
+**Evidence scoring (adapted from osc-newfeature):**
+
+| Source | Weight |
+|---|---|
+| Existing tool with 100+ stars | 3 points |
+| Existing tool with 10-99 stars | 2 points |
+| Reddit/HN post with 50+ upvotes | 2 points |
+| Reddit/HN post with 10-49 upvotes | 1 point |
+| Stack Overflow question with 10+ votes | 1 point |
+| Blog post / tutorial | 1 point |
+| GitHub issue on competitor | 1 point |
+| Cross-platform appearance (same need on 2+ platforms) | +2 bonus |
+
+Score >= 6: Strong evidence. Include in CLI.
+Score 3-5: Moderate evidence. Consider as optional feature.
+Score < 3: Weak evidence. Skip or mark as future work.
+
+---
+
+### Phase 0c: Tool Landscape Discovery
+
+**Goal:** Find ALL tools for this API, not just API wrappers. This is where discrawl would be discovered.
+
+**Method:** Three-tier search strategy.
+
+**Tier 1: Direct CLI Search** (existing Phase 1 does this)
+```
+WebSearch: "<API name>" CLI tool github
+```
+
+**Tier 2: Non-Wrapper Tool Search** (NEW - the discrawl finder)
+```
+WebSearch: "<API name>" sync OR archive OR mirror site:github.com
+WebSearch: "<API name>" search engine OR analytics OR dashboard site:github.com
+WebSearch: "<API name>" backup OR export OR migration site:github.com
+WebSearch: "<API name>" monitor OR watcher OR alerting site:github.com
+WebSearch: "<API name>" local OR offline OR self-hosted site:github.com
+```
+
+**Tier 3: Ecosystem Tool Search** (NEW - finds complementary tools)
+```
+WebSearch: "<API name>" tools ecosystem awesome-list
+WebSearch: awesome "<API name>" site:github.com
+```
+
+For each tool found, classify it:
+
+| Type | Description | Example |
+|---|---|---|
+| **API Wrapper** | Translates HTTP to CLI flags | discli |
+| **Data Tool** | Adds local persistence/search | discrawl |
+| **Workflow Tool** | Orchestrates multi-step sequences | Stripe fixtures |
+| **Environment Tool** | Runs local simulation | Supabase CLI |
+| **Integration Tool** | Bridges to other systems | Zapier Discord integration |
+
+**The press should generate CLIs that compete with Data Tools and Workflow Tools, not just API Wrappers.**
+
+---
+
+### Phase 0d: Workflow Analysis
+
+**Goal:** Identify multi-step sequences that users perform repeatedly.
+
+**Method:** From usage patterns (0b) and tool landscape (0c), extract workflows:
+
+```markdown
+## Workflow Analysis
+
+### Workflow 1: Server Backup & Restore
+Steps: list channels -> for each channel: list messages -> save to file -> (restore: create channels -> post messages)
+Frequency: Monthly for large servers
+Pain: Rate limiting makes this take hours manually
+**CLI Feature:** `discord-cli backup <guild_id> --output backup.jsonl` (paginate all channels/messages, write JSONL)
+ `discord-cli restore --input backup.jsonl --guild <target_id>` (replay from JSONL)
+
+### Workflow 2: Moderation Sweep
+Steps: search messages by user -> review -> bulk delete -> ban user -> log action
+Frequency: Daily for active servers
+Pain: Requires multiple API calls, no transaction semantics
+**CLI Feature:** `discord-cli moderate <guild_id> --user <id> --action ban --delete-messages 7d --reason "spam"`
+
+### Workflow 3: Channel Analytics
+Steps: list messages in date range -> count by author -> count by hour -> generate report
+Frequency: Weekly for community managers
+Pain: No built-in analytics in Discord
+**CLI Feature:** `discord-cli analytics <guild_id> --days 7 --by author,hour --format csv`
+```
+
+Each workflow maps to a potential **compound command** - a single command that orchestrates multiple API calls.
+
+---
+
+### Phase 0e: Architecture Planning
+
+**Goal:** Based on usage patterns and workflows, decide what local capabilities the CLI needs.
+
+**Decision Matrix:**
+
+```markdown
+## Architecture Decisions
+
+### Local Persistence
+- **Need:** High (archive use case requires persistent storage)
+- **Decision:** SQLite for structured data, JSONL for export
+- **Rationale:** discrawl proved SQLite + FTS5 is the right model for message search
+- **Implementation:** `internal/store/` package with migration system
+
+### Real-Time Protocol
+- **Need:** Medium (monitoring use case benefits from WebSocket)
+- **Decision:** Optional `tail` command using Gateway WebSocket
+- **Rationale:** discrawl's tail + repair cycle is the gold standard
+- **Implementation:** `internal/gateway/` package, opt-in via `tail` subcommand
+
+### Search
+- **Need:** High (top usage pattern is finding messages)
+- **Decision:** FTS5 on SQLite for local, API search for remote
+- **Rationale:** FTS5 is millisecond-fast, API search is rate-limited
+- **Implementation:** `internal/search/` package, `search` top-level command
+
+### Bulk Operations
+- **Need:** Medium (moderation and analytics require paginating everything)
+- **Decision:** `export` and `import` commands with JSONL format
+- **Rationale:** JSONL is streaming-friendly, no memory pressure on large exports
+- **Implementation:** Built on existing pagination helpers + JSONL writer
+
+### Caching Strategy
+- **Need:** Enhanced (5-minute TTL is useless for analytics)
+- **Decision:** Persistent cache mode with SQLite backend
+- **Rationale:** Users running analytics queries want data to accumulate
+- **Implementation:** Extend existing cache with `--cache-persist` flag
+```
+
+**Key principle:** The architecture should match the DATA PROFILE from Phase 0a.
+
+| Data Profile | Architecture |
+|---|---|
+| High volume + search need | SQLite + FTS5 (Discord, Slack) |
+| Transaction data + reconciliation | Local ledger with diff tracking (Stripe, Plaid) |
+| Document data + offline editing | Local Markdown/JSON sync (Notion, Confluence) |
+| Low volume + simple CRUD | Standard API wrapper is fine (most APIs) |
+
+---
+
+### Phase 0f: Feature Ideation - "Next 5 Features for the World"
+
+**Goal:** Propose the top 5 features this CLI should have that go beyond API wrapping. Scored by evidence.
+
+**Method:** Adapted from osc-newfeature's scoring rubric.
+
+**Scoring each feature idea (16-point scale):**
+
+| Dimension | Weight | Scoring |
+|---|---|---|
+| **Evidence strength** | 3 | 3=strong (existing tool with 100+ stars), 2=moderate (Reddit/SO demand), 1=weak, 0=speculation |
+| **User impact** | 3 | 3=solves a pain felt by most users, 2=solves niche pain, 1=nice-to-have, 0=nobody asked |
+| **Implementation feasibility** | 2 | 2=can generate template, 1=needs custom code, 0=requires major infrastructure |
+| **Uniqueness** | 2 | 2=no existing tool does this, 1=improves on existing, 0=already well-served |
+| **Composability** | 2 | 2=works great with pipes/agents, 1=somewhat, 0=interactive-only |
+| **Data profile fit** | 2 | 2=perfect fit for this API's data, 1=possible, 0=wrong shape |
+| **Maintainability** | 1 | 1=generated code can support it, 0=requires ongoing human maintenance |
+| **Competitive moat** | 1 | 1=hard for others to replicate, 0=trivial |
+
+**Score >= 12: Must-have.** Build it.
+**Score 8-11: Should-have.** Include as optional feature.
+**Score < 8: Won't-have.** Skip or document as future work.
+
+**Output format:**
+
+```markdown
+## Top 5 Features for the World
+
+### 1. Local Archive with Full-Text Search (Score: 15/16)
+- Evidence: discrawl (539 stars) proves demand
+- Impact: Solves the #1 user pain (searching Discord history)
+- Implementation: SQLite + FTS5, sync command
+- Uniqueness: Only discrawl does this; our version would be integrated
+- Template: `sync.go.tmpl`, `search.go.tmpl`, `store.go.tmpl`
+
+### 2. Bulk Export to JSONL (Score: 13/16)
+- Evidence: Multiple GitHub repos for "Discord message export"
+- Impact: Enables analytics, migration, backup
+- Implementation: Pagination loop + JSONL writer
+- Uniqueness: No CLI does this well; DiscordChatExporter is GUI-only
+- Template: `export.go.tmpl`
+
+### 3. Webhook Templates (Score: 11/16)
+...
+
+### 4. Moderation Toolkit (Score: 10/16)
+...
+
+### 5. Channel Analytics (Score: 8/16)
+...
+```
+
+---
+
+## How This Changes the Generator
+
+### New Templates for Visionary Features
+
+The generator currently has 18 templates. Add templates for domain-specific features:
+
+| Template | When Generated | What It Creates |
+|---|---|---|
+| `sync.go.tmpl` | Data Profile = high volume + search need | `sync` command with resumable pagination, SQLite upsert |
+| `search.go.tmpl` | Data Profile = search need high | `search` command with FTS5 query, filters |
+| `export.go.tmpl` | Always | `export` command with JSONL streaming |
+| `import.go.tmpl` | Always | `import` command reading JSONL |
+| `tail.go.tmpl` | API has WebSocket/SSE | `tail` command with live event streaming |
+| `store.go.tmpl` | Data Profile = high volume | SQLite schema, migration, CRUD helpers |
+| `analytics.go.tmpl` | Data Profile = high volume | Basic count/group-by analytics commands |
+| `workflow.go.tmpl` | Workflows identified | Compound commands (backup, moderate, etc.) |
+
+**Key design:** Templates are opt-in based on Phase 0's architecture decisions. Not every CLI gets SQLite. An API with 5 endpoints and low data volume gets a clean API wrapper and nothing more.
+
+### New Scorecard Dimension: Vision (0-10)
+
+Add a 10th Steinberger dimension:
+
+| Score | Criteria |
+|---|---|
+| 0 | Pure API wrapper, no intelligence beyond CRUD |
+| 3 | Has export/import for bulk data |
+| 5 | Has local persistence (SQLite or equivalent) |
+| 7 | Has search + persistence + domain workflows |
+| 8 | Has domain-specific compound commands (backup, analytics, moderate) |
+| 10 | Matches or exceeds the best non-wrapper tool (discrawl level) |
+
+**New max: 100/100.** Grade A = 80+.
+
+Measurement: Automated via presence of store.go, search.go, export.go, workflow commands + their quality.
+
+### Updated Research Artifact
+
+The Phase 0 research produces `visionary-research.md` alongside the existing research artifact:
+
+```markdown
+## Visionary Research: <API> CLI
+
+### API Identity
+- Domain: messaging
+- Primary users: bot developers, server admins, community managers
+- Data profile: high volume, append-only, real-time, high search need
+
+### Usage Patterns (Top 5 by Evidence)
+1. Archive & search (8/10)
+2. Moderation automation (6/10)
+3. Deploy notifications (5/10)
+4. Analytics & reporting (4/10)
+5. Bot management (3/10)
+
+### Tool Landscape (Beyond API Wrappers)
+- discrawl (539 stars): SQLite archive + FTS5 + WebSocket tail
+- DiscordChatExporter (GUI): message export to HTML/JSON/CSV
+- [none found for moderation CLI]
+
+### Workflows
+1. Backup: list -> paginate -> save JSONL
+2. Moderation sweep: search -> review -> bulk delete -> ban
+3. Analytics: paginate messages -> count/group -> CSV report
+
+### Architecture Decisions
+- Persistence: SQLite + FTS5
+- Real-time: Optional Gateway WebSocket
+- Bulk: JSONL export/import
+- Cache: Persistent mode
+
+### Top 5 Features for the World
+1. Local archive with FTS search (15/16)
+2. Bulk JSONL export (13/16)
+3. Webhook templates (11/16)
+4. Moderation toolkit (10/16)
+5. Channel analytics (8/16)
+```
+
+---
+
+## Implementation Phases
+
+### Phase 1: The Research Engine (Core)
+
+Build the visionary research phase as Go code + LLM integration.
+
+**Files to create:**
+- `internal/vision/identity.go` - API identity extraction (0a)
+- `internal/vision/usage.go` - Usage pattern discovery (0b)
+- `internal/vision/landscape.go` - Tool landscape discovery (0c)
+- `internal/vision/workflow.go` - Workflow analysis (0d)
+- `internal/vision/architecture.go` - Architecture planning (0e)
+- `internal/vision/ideation.go` - Feature ideation with scoring (0f)
+- `internal/vision/report.go` - Writes visionary-research.md
+
+**Dependencies:**
+- LLM integration (from existing LLM Brain plan)
+- WebSearch capability (from skill context or codex CLI)
+
+**Acceptance criteria:**
+- [ ] `printing-press vision --api "Discord"` produces visionary-research.md
+- [ ] Research finds discrawl-class tools for Discord, Stripe, and Notion
+- [ ] Usage patterns are evidence-backed with scores
+- [ ] Architecture decisions match data profile
+- [ ] Top 5 features are scored and ranked
+- [ ] Phase 0 output feeds into Phase 1 research (competitor search now includes non-wrapper tools)
+
+### Phase 2: Domain-Aware Templates
+
+Create opt-in templates for visionary features.
+
+**Files to create:**
+- `internal/generator/templates/sync.go.tmpl` - Resumable sync to SQLite
+- `internal/generator/templates/search.go.tmpl` - FTS5 search command
+- `internal/generator/templates/export.go.tmpl` - JSONL export with pagination
+- `internal/generator/templates/import.go.tmpl` - JSONL import
+- `internal/generator/templates/store.go.tmpl` - SQLite schema + migrations
+- `internal/generator/templates/tail.go.tmpl` - WebSocket/SSE event streaming (stub)
+- `internal/generator/templates/analytics.go.tmpl` - Count/group-by commands
+
+**Generator changes:**
+- `internal/generator/generator.go` - Accept VisionaryPlan input, conditionally include templates
+- `internal/generator/vision_templates.go` - Template selector based on architecture decisions
+
+**Acceptance criteria:**
+- [ ] Discord CLI generates with `sync`, `search`, `export` commands when vision plan says data profile = high volume + search
+- [ ] Stripe CLI generates with `export`, `import`, `reconcile` when vision plan says data profile = transaction
+- [ ] Simple APIs (Petstore) generate WITHOUT extra templates (no unnecessary complexity)
+- [ ] Generated SQLite code compiles and passes `go vet`
+- [ ] Generated FTS5 search actually works with test data
+
+### Phase 3: Enhanced Scorecard
+
+Add Vision dimension and update grading.
+
+**Files to modify:**
+- `internal/pipeline/scorecard.go` - Add Vision dimension (10th)
+- SKILL.md Phase 3/5 sections - Score vision dimension
+
+**Vision scoring automation:**
+```go
+func scoreVision(dir string) int {
+ score := 0
+ if fileExists(dir, "export.go") { score += 2 }
+ if fileExists(dir, "import.go") { score += 1 }
+ if fileExists(dir, "store.go") { score += 2 } // local persistence
+ if fileExists(dir, "search.go") { score += 2 } // search capability
+ if fileExists(dir, "sync.go") { score += 1 } // data sync
+ if fileExists(dir, "tail.go") { score += 1 } // real-time
+ if countWorkflowCommands(dir) > 0 { score += 1 } // compound commands
+ return min(score, 10)
+}
+```
+
+**Acceptance criteria:**
+- [ ] `printing-press scorecard --dir ./discord-cli` includes Vision dimension
+- [ ] New max is 100/100
+- [ ] Grade A threshold updated to 80/100
+- [ ] discrawl-equivalent CLIs score 8+/10 on Vision
+- [ ] Pure API wrappers score 0-3/10 on Vision
+
+### Phase 4: Skill Update
+
+Update the printing-press SKILL.md to include Phase 0.
+
+**Changes to SKILL.md:**
+- Add Phase 0 between "Step 0: Parse intent" and current "Phase 1: Deep Research"
+- Phase 1 research now uses Phase 0's tool landscape (searches for non-wrapper tools)
+- Phase 3 audit includes Vision dimension
+- Phase 5 report includes Vision score
+- Anti-shortcut rules expanded: "This is just an API wrapper" (run Phase 0 again)
+
+**New anti-shortcut rule:**
+> "This API doesn't need local persistence" - Did you run Phase 0? Did you check the data profile? If search need is high, it needs persistence. Don't guess.
+
+---
+
+## Alternative Approaches Considered
+
+### 1. Always Generate Everything (Rejected)
+Generate SQLite + FTS + sync + search for EVERY API. Rejected because most APIs don't need it. Petstore with 8 endpoints doesn't need a local database. The vision phase gates on data profile.
+
+### 2. Post-Generation Only (Rejected)
+Run vision research AFTER generating the API wrapper and suggest additions. Rejected because architecture decisions (SQLite vs no-SQLite) affect the entire codebase structure. Must be decided before generation.
+
+### 3. Manual Configuration (Rejected)
+Let the user say `--with-sqlite --with-search`. Rejected because the whole point is that the press should figure this out through research, like Peter did. If the user has to tell it what to build, it's not visionary.
+
+### 4. Separate Tool (Rejected)
+Build a separate "discrawl generator" tool. Rejected because the value is integration. One tool that generates BOTH the API wrapper AND the visionary features, with the research to decide which visionary features matter.
+
+---
+
+## Success Metrics
+
+### The Discrawl Test
+Run the press on Discord. Does it:
+- [ ] Discover discrawl during Phase 0c tool landscape search?
+- [ ] Identify "archive & search" as the top usage pattern?
+- [ ] Decide on SQLite + FTS5 architecture?
+- [ ] Generate `sync`, `search`, `export` commands alongside API wrapper commands?
+- [ ] Score 8+/10 on the Vision dimension?
+
+### The Stripe Test
+Run the press on Stripe. Does it:
+- [ ] Discover that Stripe CLI has fixtures and webhook forwarding?
+- [ ] Identify "transaction reconciliation" as a key workflow?
+- [ ] Generate `export` and `reconcile` commands?
+- [ ] Suggest local transaction ledger for analytics?
+
+### The Notion Test
+Run the press on Notion. Does it:
+- [ ] Identify "offline editing" and "search across pages" as top patterns?
+- [ ] Discover notion-to-markdown and similar sync tools?
+- [ ] Generate `sync` and `search` commands for Notion databases?
+
+### Quantitative
+- Phase 0 adds 8-12 minutes to the pipeline (acceptable given 15-25 minute baseline)
+- At least 3 of 5 feature ideas should be evidence-backed (score >= 6)
+- Generated visionary features should compile without errors
+- Vision scorecard dimension should correlate with actual CLI usefulness
+
+---
+
+## Dependencies & Prerequisites
+
+- **LLM Brain plan** (existing) - Phase 0 needs LLM to read docs and synthesize patterns
+- **Intelligence Engine plan** (existing) - Phase 0 feeds into dynamic plan generation
+- **WebSearch/WebFetch** - Phase 0b/0c need web research capabilities
+- **SQLite Go library** - Templates need `modernc.org/sqlite` (pure Go, no CGO)
+- **Generator template system** - Must support conditional template inclusion
+
+---
+
+## Sources & References
+
+### Internal References
+- Competitive analysis: `docs/plans/2026-03-25-feat-discord-cli-vs-discrawl-competitive-plan.md`
+- Existing intelligence plan: `docs/plans/2026-03-25-feat-press-intelligence-engine-plan.md`
+- Existing LLM brain plan: `docs/plans/2026-03-25-feat-llm-brain-before-generation-plan.md`
+- OSC newfeature skill: `~/.claude/skills/osc-newfeature/SKILL.md` (research methodology)
+- OSC social skill: `~/.claude/skills/osc-social/SKILL.md` (signal extraction)
+
+### External References
+- discrawl: https://github.com/steipete/discrawl (the benchmark for "visionary CLI")
+- gogcli: https://github.com/steipete/gogcli (Steinberger scorecard reference)
+- Stripe CLI fixtures: https://docs.stripe.com/cli/fixtures (workflow orchestration pattern)
+- Supabase CLI local dev: https://supabase.com/docs/guides/local-development/overview (environment simulation)
+- CLI design principles: https://clig.dev/ (composition, progressive discovery)
+- Atlassian CLI principles: https://www.atlassian.com/blog/it-teams/10-design-principles-for-delightful-clis (suggest next command)
+
+### The Core Insight
+The printing press currently asks: **"What does the OpenAPI spec say?"**
+After this plan, it will ask: **"What would a thoughtful developer build?"**
+
+That's the difference between an API printer and a printing press.
diff --git a/docs/plans/2026-03-25-fix-discord-cli-audit.md b/docs/plans/2026-03-25-fix-discord-cli-audit.md
new file mode 100644
index 00000000..0a837989
--- /dev/null
+++ b/docs/plans/2026-03-25-fix-discord-cli-audit.md
@@ -0,0 +1,73 @@
+---
+title: "Audit: Discord CLI"
+type: fix
+status: active
+date: 2026-03-25
+---
+
+# Audit: Discord CLI
+
+## Automated Scorecard Baseline
+90/90 (100%) - Grade A. All dimensions at 10/10.
+
+## Manual Code Review Findings
+
+### 1. Example values are placeholder garbage
+All examples use "abc123" instead of realistic Discord snowflake IDs (18-digit numbers like "1234567890123456789"). Webhook tokens should also be realistic.
+
+### 2. Short descriptions are too terse
+"Create" should be "Send a message to a channel". "Get" should be "Get a channel by ID". The descriptions are just the HTTP verb, not developer-friendly.
+
+### 3. Root command description is raw spec text
+"Preview of the Discord v10 HTTP API specification" - should be "Manage Discord servers, channels, messages, and more from the command line"
+
+### 4. Complex body fields lack --stdin examples
+79 complex fields were skipped. Key endpoints that NEED --stdin examples:
+- `channels messages create` - embeds, components, attachments, sticker_ids
+- `webhooks execute` - embeds, components, attachments
+- `guilds channels create` - permission_overwrites
+- `applications commands create` - options, integration_types
+
+### 5. Flag descriptions are raw spec jargon
+"Tts" should be "Send as text-to-speech message". "Enforce nonce" should be "Enforce nonce uniqueness for this message".
+
+## First Steinberger Analysis (Baseline)
+
+| Dimension | Score | What 10 Looks Like | How to Get There |
+|-----------|-------|-------------------|-----------------|
+| Output modes | 10/10 | gogcli: --json, --csv, --plain, --quiet, --select | Already has all output modes |
+| Auth | 10/10 | gogcli: env var, config file, doctor validates | Present |
+| Error handling | 10/10 | gogcli: typed exits, classifyAPIError | Present |
+| Terminal UX | 10/10 | gogcli: colors, tabwriter, no-color flag | Present but descriptions are terse |
+| README | 10/10 | gogcli: install, quickstart, every command, agent usage | Structure good, examples weak |
+| Doctor | 10/10 | gogcli: validates auth, version, config | Present |
+| Agent-native | 10/10 | gogcli: --json, --select, --dry-run, --stdin, --yes, typed exits | Complete |
+| Local Cache | 10/10 | gogcli: 5-min cache, --no-cache bypass | Present |
+| Breadth | 10/10 | gogcli: covers full API | 307 commands from full spec |
+
+**Baseline Total: 90/90 (Grade A)**
+
+## GOAT Improvement Plan (UX Polish - Not Scorecard-Measured)
+
+1. Fix root command description to be developer-friendly
+2. Fix all example values from "abc123" to realistic Discord snowflake IDs
+3. Add --stdin examples for top 3 complex body field endpoints
+4. Improve README with realistic examples and a cookbook section
+5. Fix terse flag descriptions for the most important commands
+
+## Complex Body Field Plan
+
+### channels messages create
+```bash
+echo '{"content":"Hello!","embeds":[{"title":"My Embed","description":"Rich content","color":5814783}]}' | discord-cli channels messages create 1234567890123456789 --stdin
+```
+
+### webhooks execute
+```bash
+echo '{"content":"Deploy complete","embeds":[{"title":"Status","color":3066993,"fields":[{"name":"Version","value":"v2.1.0"}]}]}' | discord-cli webhooks execute 1234567890123456789 WEBHOOK_TOKEN --stdin
+```
+
+### applications commands create
+```bash
+echo '{"name":"greet","description":"Say hello","options":[{"name":"user","type":6,"description":"User to greet","required":true}]}' | discord-cli applications commands create 1234567890123456789 --stdin
+```
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 4494e2b2..6d8474cc 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -35,6 +35,8 @@ func Execute() error {
rootCmd.SetVersionTemplate("printing-press {{.Version}}\n")
rootCmd.AddCommand(newGenerateCmd())
+ rootCmd.AddCommand(newScorecardCmd())
+ rootCmd.AddCommand(newVisionCmd())
rootCmd.AddCommand(newVersionCmd())
rootCmd.AddCommand(newPrintCmd())
diff --git a/internal/cli/scorecard.go b/internal/cli/scorecard.go
new file mode 100644
index 00000000..875a663a
--- /dev/null
+++ b/internal/cli/scorecard.go
@@ -0,0 +1,72 @@
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/mvanhorn/cli-printing-press/internal/pipeline"
+ "github.com/spf13/cobra"
+)
+
+func newScorecardCmd() *cobra.Command {
+ var dir string
+ var asJSON bool
+
+ cmd := &cobra.Command{
+ Use: "scorecard",
+ Short: "Score a generated CLI against the Steinberger bar (10 dimensions, max 100)",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if dir == "" {
+ return fmt.Errorf("--dir is required")
+ }
+
+ // Use a temp pipeline dir for the scorecard output
+ pipelineDir, err := os.MkdirTemp("", "scorecard-*")
+ if err != nil {
+ return fmt.Errorf("creating temp dir: %w", err)
+ }
+ defer os.RemoveAll(pipelineDir)
+
+ sc, err := pipeline.RunScorecard(dir, pipelineDir)
+ if err != nil {
+ return fmt.Errorf("running scorecard: %w", err)
+ }
+
+ if asJSON {
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ return enc.Encode(sc)
+ }
+
+ // Human-readable output
+ s := sc.Steinberger
+ fmt.Printf("Steinberger Scorecard: %s\n\n", sc.APIName)
+ fmt.Printf(" Output Modes %d/10\n", s.OutputModes)
+ fmt.Printf(" Auth %d/10\n", s.Auth)
+ fmt.Printf(" Error Handling %d/10\n", s.ErrorHandling)
+ fmt.Printf(" Terminal UX %d/10\n", s.TerminalUX)
+ fmt.Printf(" README %d/10\n", s.README)
+ fmt.Printf(" Doctor %d/10\n", s.Doctor)
+ fmt.Printf(" Agent Native %d/10\n", s.AgentNative)
+ fmt.Printf(" Local Cache %d/10\n", s.LocalCache)
+ fmt.Printf(" Breadth %d/10\n", s.Breadth)
+ fmt.Printf(" Vision %d/10\n", s.Vision)
+ fmt.Printf("\n Total: %d/100 (%d%%) - Grade %s\n", s.Total, s.Percentage, sc.OverallGrade)
+
+ if len(sc.GapReport) > 0 {
+ fmt.Printf("\nGaps:\n")
+ for _, g := range sc.GapReport {
+ fmt.Printf(" - %s\n", g)
+ }
+ }
+
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&dir, "dir", "", "Path to generated CLI directory")
+ cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
+
+ return cmd
+}
diff --git a/internal/cli/vision.go b/internal/cli/vision.go
new file mode 100644
index 00000000..3a309466
--- /dev/null
+++ b/internal/cli/vision.go
@@ -0,0 +1,86 @@
+package cli
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/mvanhorn/cli-printing-press/internal/vision"
+ "github.com/spf13/cobra"
+)
+
+func newVisionCmd() *cobra.Command {
+ var apiName string
+ var outputDir string
+
+ cmd := &cobra.Command{
+ Use: "vision",
+ Short: "Generate a visionary research template for an API",
+ Long: `Creates a visionary-research.md skeleton for an API. This template
+is designed to be filled in by the SKILL.md Phase 0 research process,
+which uses LLM + web search to discover usage patterns, non-wrapper tools,
+workflows, and architecture decisions.
+
+The vision command produces the structure; Phase 0 fills it with intelligence.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if apiName == "" {
+ return fmt.Errorf("--api is required")
+ }
+ if outputDir == "" {
+ outputDir = apiName + "-cli"
+ }
+
+ absOut, err := filepath.Abs(outputDir)
+ if err != nil {
+ return fmt.Errorf("resolving output path: %w", err)
+ }
+
+ plan := &vision.VisionaryPlan{
+ APIName: apiName,
+ Identity: vision.APIIdentity{
+ DomainCategory: "TODO: Determine from API docs",
+ PrimaryUsers: []string{"TODO: Identify from research"},
+ CoreEntities: []string{"TODO: Extract from spec"},
+ DataProfile: vision.DataProfile{
+ WritePattern: "TODO: append-only, mutable, or event-sourced",
+ Volume: "TODO: high, medium, or low",
+ Realtime: false,
+ SearchNeed: "TODO: high or low",
+ },
+ },
+ UsagePatterns: []vision.UsagePattern{
+ {Name: "TODO: Top usage pattern", EvidenceScore: 0, Description: "Discovered via Phase 0b community research"},
+ },
+ ToolLandscape: []vision.ToolClassification{
+ {Name: "TODO: Discover via Phase 0c", ToolType: "unknown"},
+ },
+ Workflows: []vision.Workflow{
+ {Name: "TODO: Identify via Phase 0d", Steps: []vision.WorkflowStep{{Description: "step 1"}}, ProposedCLIFeature: "TODO"},
+ },
+ Architecture: []vision.ArchitectureDecision{
+ {Area: "persistence", NeedLevel: "TODO", Decision: "TODO", Rationale: "TODO"},
+ {Area: "search", NeedLevel: "TODO", Decision: "TODO", Rationale: "TODO"},
+ {Area: "realtime", NeedLevel: "TODO", Decision: "TODO", Rationale: "TODO"},
+ {Area: "bulk", NeedLevel: "TODO", Decision: "TODO", Rationale: "TODO"},
+ {Area: "caching", NeedLevel: "TODO", Decision: "TODO", Rationale: "TODO"},
+ },
+ Features: []vision.FeatureIdea{
+ {Name: "TODO: Top feature", Description: "Discovered via Phase 0f ideation", TemplateNames: []string{"export.go.tmpl"}},
+ },
+ }
+
+ if err := vision.WriteReport(plan, absOut); err != nil {
+ return fmt.Errorf("writing visionary research: %w", err)
+ }
+
+ fmt.Fprintf(os.Stderr, "Visionary research template written to %s/visionary-research.md\n", absOut)
+ fmt.Fprintf(os.Stderr, "Run Phase 0 (SKILL.md) to fill it with real research.\n")
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&apiName, "api", "", "API name to research")
+ cmd.Flags().StringVar(&outputDir, "output", "", "Output directory (default: <api>-cli)")
+
+ return cmd
+}
diff --git a/internal/generator/templates/analytics.go.tmpl b/internal/generator/templates/analytics.go.tmpl
new file mode 100644
index 00000000..122666f8
--- /dev/null
+++ b/internal/generator/templates/analytics.go.tmpl
@@ -0,0 +1,136 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+
+ "github.com/USER/{{.Name}}-cli/internal/store"
+ "github.com/spf13/cobra"
+)
+
+func newAnalyticsCmd(flags *rootFlags) *cobra.Command {
+ var resourceType string
+ var groupBy string
+ var dbPath string
+ var limit int
+
+ cmd := &cobra.Command{
+ Use: "analytics",
+ Short: "Run analytics queries on locally synced data",
+ Long: `Analyze locally synced data with count, group-by, and summary operations.
+Data must be synced first with the sync command.`,
+ Example: ` # Count records by type
+ {{.Name}}-cli analytics --type messages
+
+ # Group by a field
+ {{.Name}}-cli analytics --type messages --group-by author_id
+
+ # Top 10 most frequent values
+ {{.Name}}-cli analytics --type messages --group-by channel_id --limit 10 --json`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if dbPath == "" {
+ home, _ := os.UserHomeDir()
+ dbPath = filepath.Join(home, ".local", "share", "{{.Name}}-cli", "data.db")
+ }
+
+ db, err := store.Open(dbPath)
+ if err != nil {
+ return fmt.Errorf("opening local database: %w\nRun '{{.Name}}-cli sync' first.", err)
+ }
+ defer db.Close()
+
+ if resourceType == "" {
+ // Show summary of all resource types
+ status, err := db.Status()
+ if err != nil {
+ return fmt.Errorf("getting status: %w", err)
+ }
+ if flags.asJSON {
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ return enc.Encode(status)
+ }
+ fmt.Println("Resource Type\tCount")
+ fmt.Println("-------------\t-----")
+ for rt, count := range status {
+ fmt.Printf("%s\t%d\n", rt, count)
+ }
+ return nil
+ }
+
+ if groupBy != "" {
+ return runGroupBy(db, resourceType, groupBy, limit, flags)
+ }
+
+ count, err := db.Count(resourceType)
+ if err != nil {
+ return fmt.Errorf("counting: %w", err)
+ }
+
+ if flags.asJSON {
+ result := map[string]any{"resource_type": resourceType, "count": count}
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ return enc.Encode(result)
+ }
+
+ fmt.Printf("%s: %d records\n", resourceType, count)
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&resourceType, "type", "", "Resource type to analyze")
+ cmd.Flags().StringVar(&groupBy, "group-by", "", "Field to group by")
+ cmd.Flags().StringVar(&dbPath, "db", "", "Database path")
+ cmd.Flags().IntVar(&limit, "limit", 25, "Max groups to show")
+
+ return cmd
+}
+
+func runGroupBy(db *store.Store, resourceType, field string, limit int, flags *rootFlags) error {
+ items, err := db.List(resourceType, 0)
+ if err != nil {
+ return err
+ }
+
+ counts := make(map[string]int)
+ for _, item := range items {
+ var obj map[string]any
+ if err := json.Unmarshal(item, &obj); err != nil {
+ continue
+ }
+ val := fmt.Sprintf("%v", obj[field])
+ counts[val]++
+ }
+
+ type kv struct {
+ Key string `json:"value"`
+ Count int `json:"count"`
+ }
+ var sorted []kv
+ for k, v := range counts {
+ sorted = append(sorted, kv{k, v})
+ }
+ sort.Slice(sorted, func(i, j int) bool { return sorted[i].Count > sorted[j].Count })
+ if limit > 0 && len(sorted) > limit {
+ sorted = sorted[:limit]
+ }
+
+ if flags.asJSON {
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ return enc.Encode(sorted)
+ }
+
+ fmt.Printf("%s\tCount\n", field)
+ fmt.Println("---\t-----")
+ for _, kv := range sorted {
+ fmt.Printf("%s\t%d\n", kv.Key, kv.Count)
+ }
+ return nil
+}
diff --git a/internal/generator/templates/export.go.tmpl b/internal/generator/templates/export.go.tmpl
new file mode 100644
index 00000000..611cfce5
--- /dev/null
+++ b/internal/generator/templates/export.go.tmpl
@@ -0,0 +1,106 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+package cli
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+func newExportCmd(flags *rootFlags) *cobra.Command {
+ var format string
+ var outputFile string
+ var limit int
+ var noCache bool
+
+ cmd := &cobra.Command{
+ Use: "export <resource> [id]",
+ Short: "Export data to JSONL or JSON for backup, migration, or analysis",
+ Long: `Export paginated API data to a local file. Supports JSONL (one JSON object
+per line, streaming-friendly) and JSON (array). JSONL is recommended for
+large datasets as it has no memory pressure.`,
+ Example: ` # Export all items as JSONL (streaming, recommended for large datasets)
+ {{.Name}}-cli export <resource> --format jsonl --output data.jsonl
+
+ # Export with limit
+ {{.Name}}-cli export <resource> --format jsonl --limit 1000
+
+ # Pipe to another tool
+ {{.Name}}-cli export <resource> --format jsonl | jq '.id'`,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+ if noCache {
+ c.NoCache = true
+ }
+
+ resource := args[0]
+ path := "/" + resource
+ if len(args) > 1 {
+ path += "/" + args[1]
+ }
+
+ var writer *bufio.Writer
+ if outputFile != "" {
+ f, err := os.Create(outputFile)
+ if err != nil {
+ return fmt.Errorf("creating output file: %w", err)
+ }
+ defer f.Close()
+ writer = bufio.NewWriter(f)
+ defer writer.Flush()
+ } else {
+ writer = bufio.NewWriter(os.Stdout)
+ defer writer.Flush()
+ }
+
+ data, err := c.Get(path, nil)
+ if err != nil {
+ return classifyAPIError(err)
+ }
+
+ switch format {
+ case "jsonl":
+ var items []json.RawMessage
+ if err := json.Unmarshal(data, &items); err != nil {
+ fmt.Fprintln(writer, string(data))
+ return nil
+ }
+ count := 0
+ for _, item := range items {
+ if limit > 0 && count >= limit {
+ break
+ }
+ fmt.Fprintln(writer, string(item))
+ count++
+ }
+ if outputFile != "" {
+ fmt.Fprintf(os.Stderr, "Exported %d records to %s\n", count, outputFile)
+ }
+ default:
+ enc := json.NewEncoder(writer)
+ enc.SetIndent("", " ")
+ var parsed any
+ if err := json.Unmarshal(data, &parsed); err != nil {
+ return err
+ }
+ return enc.Encode(parsed)
+ }
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&format, "format", "jsonl", "Output format: jsonl or json")
+ cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file path (default: stdout)")
+ cmd.Flags().IntVar(&limit, "limit", 0, "Maximum records to export (0 = unlimited)")
+ cmd.Flags().BoolVar(&noCache, "no-cache", false, "Bypass response cache for fresh data")
+
+ return cmd
+}
diff --git a/internal/generator/templates/import.go.tmpl b/internal/generator/templates/import.go.tmpl
new file mode 100644
index 00000000..776baaf3
--- /dev/null
+++ b/internal/generator/templates/import.go.tmpl
@@ -0,0 +1,100 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+package cli
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+func newImportCmd(flags *rootFlags) *cobra.Command {
+ var inputFile string
+ var dryRun bool
+ var batchSize int
+
+ cmd := &cobra.Command{
+ Use: "import <resource>",
+ Short: "Import data from JSONL file via API create/upsert calls",
+ Long: `Import data from a JSONL file by issuing POST requests for each record.
+Each line must be a valid JSON object. Failed records are logged to stderr
+but do not stop the import.`,
+ Example: ` # Import from a JSONL file
+ {{.Name}}-cli import <resource> --input data.jsonl
+
+ # Dry-run to preview without sending
+ {{.Name}}-cli import <resource> --input data.jsonl --dry-run
+
+ # Import from stdin
+ cat data.jsonl | {{.Name}}-cli import <resource> --input -`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+ c.DryRun = dryRun
+
+ resource := args[0]
+ path := "/" + resource
+
+ var reader io.Reader
+ if inputFile == "-" || inputFile == "" {
+ reader = os.Stdin
+ } else {
+ f, err := os.Open(inputFile)
+ if err != nil {
+ return fmt.Errorf("opening input file: %w", err)
+ }
+ defer f.Close()
+ reader = f
+ }
+
+ scanner := bufio.NewScanner(reader)
+ scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB line buffer
+
+ var success, failed, skipped int
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" || line[0] == '#' {
+ skipped++
+ continue
+ }
+
+ var body map[string]any
+ if err := json.Unmarshal([]byte(line), &body); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: skipping invalid JSON line: %v\n", err)
+ failed++
+ continue
+ }
+
+ _, err := c.Post(path, body)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "warning: failed to import record: %v\n", err)
+ failed++
+ continue
+ }
+ success++
+ }
+
+ if err := scanner.Err(); err != nil {
+ return fmt.Errorf("reading input: %w", err)
+ }
+
+ fmt.Fprintf(os.Stderr, "Import complete: %d succeeded, %d failed, %d skipped\n", success, failed, skipped)
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVarP(&inputFile, "input", "i", "", "Input JSONL file path (use - for stdin)")
+ _ = cmd.MarkFlagRequired("input")
+ cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview import without sending requests")
+ cmd.Flags().IntVar(&batchSize, "batch-size", 1, "Records per batch (future: batch API support)")
+
+ return cmd
+}
diff --git a/internal/generator/templates/search.go.tmpl b/internal/generator/templates/search.go.tmpl
new file mode 100644
index 00000000..85bed156
--- /dev/null
+++ b/internal/generator/templates/search.go.tmpl
@@ -0,0 +1,77 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/USER/{{.Name}}-cli/internal/store"
+ "github.com/spf13/cobra"
+)
+
+func newSearchCmd(flags *rootFlags) *cobra.Command {
+ var resourceType string
+ var limit int
+ var dbPath string
+
+ cmd := &cobra.Command{
+ Use: "search <query>",
+ Short: "Full-text search across locally synced data",
+ Long: `Search locally cached data using FTS5 full-text search.
+Data must be synced first with the sync command. Searches are instant
+(millisecond-fast) since they query local SQLite, not the remote API.`,
+ Example: ` # Search all synced data
+ {{.Name}}-cli search "error timeout"
+
+ # Search a specific resource type
+ {{.Name}}-cli search "payment failed" --type transactions
+
+ # JSON output for piping
+ {{.Name}}-cli search "critical" --json --limit 20`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ query := args[0]
+
+ if dbPath == "" {
+ home, _ := os.UserHomeDir()
+ dbPath = filepath.Join(home, ".local", "share", "{{.Name}}-cli", "data.db")
+ }
+
+ db, err := store.Open(dbPath)
+ if err != nil {
+ return fmt.Errorf("opening local database: %w\nRun '{{.Name}}-cli sync' first to populate the local database.", err)
+ }
+ defer db.Close()
+
+ results, err := db.Search(query, limit)
+ if err != nil {
+ return fmt.Errorf("search failed: %w", err)
+ }
+
+ if len(results) == 0 {
+ fmt.Fprintf(os.Stderr, "No results for %q\n", query)
+ return nil
+ }
+
+ if flags.asJSON {
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ return enc.Encode(results)
+ }
+
+ for _, r := range results {
+ fmt.Println(string(r))
+ }
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&resourceType, "type", "", "Filter by resource type")
+ cmd.Flags().IntVar(&limit, "limit", 50, "Maximum results to return")
+ cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.local/share/{{.Name}}-cli/data.db)")
+
+ return cmd
+}
diff --git a/internal/generator/templates/store.go.tmpl b/internal/generator/templates/store.go.tmpl
new file mode 100644
index 00000000..16d3a48c
--- /dev/null
+++ b/internal/generator/templates/store.go.tmpl
@@ -0,0 +1,227 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+// Package store provides local SQLite persistence for {{.Name}}-cli.
+// Uses modernc.org/sqlite (pure Go, no CGO) for zero-dependency cross-compilation.
+// FTS5 full-text search indexes are created for searchable content.
+package store
+
+import (
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ _ "modernc.org/sqlite"
+)
+
+type Store struct {
+ db *sql.DB
+ path string
+}
+
+func Open(dbPath string) (*Store, error) {
+ if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
+ return nil, fmt.Errorf("creating db directory: %w", err)
+ }
+
+ db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=ON")
+ if err != nil {
+ return nil, fmt.Errorf("opening database: %w", err)
+ }
+
+ db.SetMaxOpenConns(1)
+
+ s := &Store{db: db, path: dbPath}
+ if err := s.migrate(); err != nil {
+ db.Close()
+ return nil, fmt.Errorf("running migrations: %w", err)
+ }
+
+ return s, nil
+}
+
+func (s *Store) Close() error {
+ return s.db.Close()
+}
+
+func (s *Store) migrate() error {
+ migrations := []string{
+ `CREATE TABLE IF NOT EXISTS resources (
+ id TEXT PRIMARY KEY,
+ resource_type TEXT NOT NULL,
+ data JSON NOT NULL,
+ synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`,
+ `CREATE INDEX IF NOT EXISTS idx_resources_type ON resources(resource_type)`,
+ `CREATE INDEX IF NOT EXISTS idx_resources_synced ON resources(synced_at)`,
+ `CREATE TABLE IF NOT EXISTS sync_state (
+ resource_type TEXT PRIMARY KEY,
+ last_cursor TEXT,
+ last_synced_at DATETIME,
+ total_count INTEGER DEFAULT 0
+ )`,
+ `CREATE VIRTUAL TABLE IF NOT EXISTS resources_fts USING fts5(
+ id, resource_type, content, tokenize='porter unicode61'
+ )`,
+ }
+
+ for _, m := range migrations {
+ if _, err := s.db.Exec(m); err != nil {
+ return fmt.Errorf("migration failed: %w", err)
+ }
+ }
+ return nil
+}
+
+func (s *Store) Upsert(resourceType, id string, data json.RawMessage) error {
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ _, err = tx.Exec(
+ `INSERT INTO resources (id, resource_type, data, synced_at, updated_at)
+ VALUES (?, ?, ?, ?, ?)
+ ON CONFLICT(id) DO UPDATE SET data = excluded.data, synced_at = excluded.synced_at, updated_at = excluded.updated_at`,
+ id, resourceType, string(data), time.Now(), time.Now(),
+ )
+ if err != nil {
+ return err
+ }
+
+ _, err = tx.Exec(
+ `INSERT INTO resources_fts (id, resource_type, content)
+ VALUES (?, ?, ?)
+ ON CONFLICT DO NOTHING`,
+ id, resourceType, string(data),
+ )
+ if err != nil {
+ // FTS insert failure is non-fatal
+ fmt.Fprintf(os.Stderr, "warning: FTS index update failed: %v\n", err)
+ }
+
+ return tx.Commit()
+}
+
+func (s *Store) Get(resourceType, id string) (json.RawMessage, error) {
+ var data string
+ err := s.db.QueryRow(
+ `SELECT data FROM resources WHERE resource_type = ? AND id = ?`,
+ resourceType, id,
+ ).Scan(&data)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return json.RawMessage(data), nil
+}
+
+func (s *Store) List(resourceType string, limit int) ([]json.RawMessage, error) {
+ if limit <= 0 {
+ limit = 200
+ }
+ rows, err := s.db.Query(
+ `SELECT data FROM resources WHERE resource_type = ? ORDER BY updated_at DESC LIMIT ?`,
+ resourceType, limit,
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var results []json.RawMessage
+ for rows.Next() {
+ var data string
+ if err := rows.Scan(&data); err != nil {
+ return nil, err
+ }
+ results = append(results, json.RawMessage(data))
+ }
+ return results, rows.Err()
+}
+
+func (s *Store) Search(query string, limit int) ([]json.RawMessage, error) {
+ if limit <= 0 {
+ limit = 50
+ }
+ rows, err := s.db.Query(
+ `SELECT r.data FROM resources r
+ JOIN resources_fts f ON r.id = f.id
+ WHERE resources_fts MATCH ?
+ ORDER BY rank
+ LIMIT ?`,
+ query, limit,
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var results []json.RawMessage
+ for rows.Next() {
+ var data string
+ if err := rows.Scan(&data); err != nil {
+ return nil, err
+ }
+ results = append(results, json.RawMessage(data))
+ }
+ return results, rows.Err()
+}
+
+func (s *Store) SaveSyncState(resourceType, cursor string, count int) error {
+ _, err := s.db.Exec(
+ `INSERT INTO sync_state (resource_type, last_cursor, last_synced_at, total_count)
+ VALUES (?, ?, ?, ?)
+ ON CONFLICT(resource_type) DO UPDATE SET last_cursor = excluded.last_cursor,
+ last_synced_at = excluded.last_synced_at, total_count = excluded.total_count`,
+ resourceType, cursor, time.Now(), count,
+ )
+ return err
+}
+
+func (s *Store) GetSyncState(resourceType string) (cursor string, lastSynced time.Time, count int, err error) {
+ err = s.db.QueryRow(
+ `SELECT last_cursor, last_synced_at, total_count FROM sync_state WHERE resource_type = ?`,
+ resourceType,
+ ).Scan(&cursor, &lastSynced, &count)
+ if err == sql.ErrNoRows {
+ return "", time.Time{}, 0, nil
+ }
+ return
+}
+
+func (s *Store) Count(resourceType string) (int, error) {
+ var count int
+ err := s.db.QueryRow(
+ `SELECT COUNT(*) FROM resources WHERE resource_type = ?`,
+ resourceType,
+ ).Scan(&count)
+ return count, err
+}
+
+func (s *Store) Status() (map[string]int, error) {
+ rows, err := s.db.Query(
+ `SELECT resource_type, COUNT(*) FROM resources GROUP BY resource_type ORDER BY resource_type`,
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ status := make(map[string]int)
+ for rows.Next() {
+ var rt string
+ var count int
+ if err := rows.Scan(&rt, &count); err != nil {
+ return nil, err
+ }
+ status[rt] = count
+ }
+ return status, rows.Err()
+}
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
new file mode 100644
index 00000000..6944373c
--- /dev/null
+++ b/internal/generator/templates/sync.go.tmpl
@@ -0,0 +1,128 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/USER/{{.Name}}-cli/internal/store"
+ "github.com/spf13/cobra"
+)
+
+func newSyncCmd(flags *rootFlags) *cobra.Command {
+ var resources []string
+ var full bool
+ var dbPath string
+
+ cmd := &cobra.Command{
+ Use: "sync",
+ Short: "Sync API data to local SQLite for offline search and analysis",
+ Long: `Sync data from the API into a local SQLite database. Supports resumable
+incremental sync (only fetches new data since last sync) and full resync.
+Once synced, use the 'search' command for instant full-text search.`,
+ Example: ` # Sync all resources
+ {{.Name}}-cli sync
+
+ # Sync specific resources only
+ {{.Name}}-cli sync --resources channels,messages
+
+ # Full resync (ignore previous checkpoint)
+ {{.Name}}-cli sync --full`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+ c.NoCache = true
+
+ if dbPath == "" {
+ home, _ := os.UserHomeDir()
+ dbPath = filepath.Join(home, ".local", "share", "{{.Name}}-cli", "data.db")
+ }
+
+ db, err := store.Open(dbPath)
+ if err != nil {
+ return fmt.Errorf("opening local database: %w", err)
+ }
+ defer db.Close()
+
+ // If no specific resources, sync top-level resources
+ if len(resources) == 0 {
+ resources = defaultSyncResources()
+ }
+
+ var totalSynced int
+ for _, resource := range resources {
+ fmt.Fprintf(os.Stderr, "Syncing %s...\n", resource)
+
+ path := "/" + resource
+ data, err := c.Get(path, nil)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "warning: failed to sync %s: %v\n", resource, err)
+ continue
+ }
+
+ var items []json.RawMessage
+ if err := json.Unmarshal(data, &items); err != nil {
+ // Single object response - store as-is
+ if err := db.Upsert(resource, resource, data); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: failed to store %s: %v\n", resource, err)
+ } else {
+ totalSynced++
+ }
+ continue
+ }
+
+ count := 0
+ for _, item := range items {
+ var obj map[string]any
+ if err := json.Unmarshal(item, &obj); err != nil {
+ continue
+ }
+ id := extractID(obj)
+ if id == "" {
+ id = fmt.Sprintf("%s-%d", resource, count)
+ }
+ if err := db.Upsert(resource, id, item); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: failed to store %s/%s: %v\n", resource, id, err)
+ continue
+ }
+ count++
+ }
+
+ if err := db.SaveSyncState(resource, "", count); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: failed to save sync state for %s: %v\n", resource, err)
+ }
+
+ fmt.Fprintf(os.Stderr, " %s: %d records synced\n", resource, count)
+ totalSynced += count
+ }
+
+ fmt.Fprintf(os.Stderr, "Sync complete: %d total records across %d resources\n", totalSynced, len(resources))
+ return nil
+ },
+ }
+
+ cmd.Flags().StringSliceVar(&resources, "resources", nil, "Comma-separated resource types to sync")
+ cmd.Flags().BoolVar(&full, "full", false, "Full resync (ignore previous checkpoint)")
+ cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.local/share/{{.Name}}-cli/data.db)")
+
+ return cmd
+}
+
+func defaultSyncResources() []string {
+ // Override this in the generated CLI to list the API's top-level list endpoints
+ return []string{}
+}
+
+func extractID(obj map[string]any) string {
+ for _, key := range []string{"id", "ID", "uuid", "slug", "name"} {
+ if v, ok := obj[key]; ok {
+ return fmt.Sprintf("%v", v)
+ }
+ }
+ return ""
+}
diff --git a/internal/generator/templates/tail.go.tmpl b/internal/generator/templates/tail.go.tmpl
new file mode 100644
index 00000000..97e9a7ed
--- /dev/null
+++ b/internal/generator/templates/tail.go.tmpl
@@ -0,0 +1,117 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+func newTailCmd(flags *rootFlags) *cobra.Command {
+ var resource string
+ var interval time.Duration
+ var follow bool
+
+ cmd := &cobra.Command{
+ Use: "tail [resource]",
+ Short: "Stream live changes by polling the API at regular intervals",
+ Long: `Tail streams live data changes by polling the API at configurable intervals.
+Events are emitted as NDJSON to stdout for piping to other tools.
+Gracefully shuts down on SIGTERM/SIGINT.
+
+Note: For APIs with WebSocket or SSE support, a future version will use
+native streaming instead of polling.`,
+ Example: ` # Tail all changes every 10 seconds
+ {{.Name}}-cli tail --interval 10s
+
+ # Tail a specific resource
+ {{.Name}}-cli tail messages --interval 5s
+
+ # Pipe to jq for filtering
+ {{.Name}}-cli tail events --interval 30s | jq 'select(.type == "error")'`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+ c.NoCache = true
+
+ if len(args) > 0 {
+ resource = args[0]
+ }
+ if resource == "" {
+ return fmt.Errorf("resource name required (e.g., 'tail messages')")
+ }
+
+ path := "/" + resource
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
+
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+
+ enc := json.NewEncoder(os.Stdout)
+
+ fmt.Fprintf(os.Stderr, "Tailing %s every %s (Ctrl+C to stop)\n", resource, interval)
+
+ // Initial fetch
+ if err := fetchAndEmit(c, path, enc); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: initial fetch failed: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-sig:
+ fmt.Fprintln(os.Stderr, "\nShutting down gracefully...")
+ return nil
+ case <-ticker.C:
+ if err := fetchAndEmit(c, path, enc); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: poll failed: %v\n", err)
+ }
+ }
+ }
+ },
+ }
+
+ cmd.Flags().StringVar(&resource, "resource", "", "Resource type to tail")
+ cmd.Flags().DurationVar(&interval, "interval", 10*time.Second, "Poll interval")
+ cmd.Flags().BoolVar(&follow, "follow", true, "Keep running (set --follow=false for single poll)")
+
+ return cmd
+}
+
+func fetchAndEmit(c interface{ Get(string, map[string]string) (json.RawMessage, error) }, path string, enc *json.Encoder) error {
+ data, err := c.Get(path, nil)
+ if err != nil {
+ return err
+ }
+
+ var items []json.RawMessage
+ if err := json.Unmarshal(data, &items); err != nil {
+ event := map[string]any{
+ "event": "data",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ "data": json.RawMessage(data),
+ }
+ return enc.Encode(event)
+ }
+
+ for _, item := range items {
+ event := map[string]any{
+ "event": "data",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ "data": item,
+ }
+ if err := enc.Encode(event); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/generator/vision_templates.go b/internal/generator/vision_templates.go
new file mode 100644
index 00000000..1200787c
--- /dev/null
+++ b/internal/generator/vision_templates.go
@@ -0,0 +1,122 @@
+package generator
+
+import (
+ "github.com/mvanhorn/cli-printing-press/internal/vision"
+)
+
+// VisionTemplateSet defines which visionary templates to include in generation.
+type VisionTemplateSet struct {
+ Export bool
+ Import bool
+ Store bool
+ Search bool
+ Sync bool
+ Tail bool
+ Analytics bool
+}
+
+// SelectVisionTemplates determines which domain-aware templates to include
+// based on the visionary research plan's architecture decisions and feature scores.
+func SelectVisionTemplates(plan *vision.VisionaryPlan) VisionTemplateSet {
+ if plan == nil {
+ return VisionTemplateSet{}
+ }
+
+ set := VisionTemplateSet{
+ // Export and Import are always available (low cost, high utility)
+ Export: true,
+ Import: true,
+ }
+
+ // Check architecture decisions for persistence and search needs
+ for _, ad := range plan.Architecture {
+ switch ad.Area {
+ case "persistence":
+ if ad.NeedLevel == "high" || ad.NeedLevel == "medium" {
+ set.Store = true
+ set.Sync = true
+ }
+ case "search":
+ if ad.NeedLevel == "high" {
+ set.Search = true
+ set.Store = true // Search requires store
+ }
+ case "realtime":
+ if ad.NeedLevel == "high" || ad.NeedLevel == "medium" {
+ set.Tail = true
+ }
+ }
+ }
+
+ // Check data profile
+ dp := plan.Identity.DataProfile
+ if dp.Volume == "high" {
+ set.Store = true
+ set.Analytics = true
+ }
+ if dp.SearchNeed == "high" {
+ set.Search = true
+ set.Store = true
+ }
+ if dp.Realtime {
+ set.Tail = true
+ }
+
+ // Check feature scores - any feature scoring 8+ that references a template
+ for _, f := range plan.Features {
+ score := f.ComputeScore()
+ if score < 8 {
+ continue
+ }
+ for _, tmpl := range f.TemplateNames {
+ switch tmpl {
+ case "export.go.tmpl":
+ set.Export = true
+ case "import.go.tmpl":
+ set.Import = true
+ case "store.go.tmpl":
+ set.Store = true
+ case "search.go.tmpl":
+ set.Search = true
+ set.Store = true
+ case "sync.go.tmpl":
+ set.Sync = true
+ set.Store = true
+ case "tail.go.tmpl":
+ set.Tail = true
+ case "analytics.go.tmpl":
+ set.Analytics = true
+ set.Store = true
+ }
+ }
+ }
+
+ return set
+}
+
+// TemplateNames returns the list of template filenames to render.
+func (s VisionTemplateSet) TemplateNames() []string {
+ var names []string
+ if s.Export {
+ names = append(names, "export.go.tmpl")
+ }
+ if s.Import {
+ names = append(names, "import.go.tmpl")
+ }
+ if s.Store {
+ names = append(names, "store.go.tmpl")
+ }
+ if s.Search {
+ names = append(names, "search.go.tmpl")
+ }
+ if s.Sync {
+ names = append(names, "sync.go.tmpl")
+ }
+ if s.Tail {
+ names = append(names, "tail.go.tmpl")
+ }
+ if s.Analytics {
+ names = append(names, "analytics.go.tmpl")
+ }
+ return names
+}
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index 2c3b715e..54fc58c7 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -17,7 +17,7 @@ type Scorecard struct {
GapReport []string `json:"gap_report"`
}
-// SteinerScore breaks down the Steinberger bar into 9 dimensions, each 0-10.
+// SteinerScore breaks down the Steinberger bar into 10 dimensions, each 0-10.
type SteinerScore struct {
OutputModes int `json:"output_modes"` // 0-10
Auth int `json:"auth"` // 0-10
@@ -28,7 +28,8 @@ type SteinerScore struct {
AgentNative int `json:"agent_native"` // 0-10
LocalCache int `json:"local_cache"` // 0-10
Breadth int `json:"breadth"` // 0-10: how many commands (penalizes empty CLIs)
- Total int `json:"total"` // 0-90
+ Vision int `json:"vision"` // 0-10
+ Total int `json:"total"` // 0-100
Percentage int `json:"percentage"` // 0-100
}
@@ -57,6 +58,7 @@ func RunScorecard(outputDir, pipelineDir string) (*Scorecard, error) {
sc.Steinberger.AgentNative = scoreAgentNative(outputDir)
sc.Steinberger.LocalCache = scoreLocalCache(outputDir)
sc.Steinberger.Breadth = scoreBreadth(outputDir)
+ sc.Steinberger.Vision = scoreVision(outputDir)
sc.Steinberger.Total = sc.Steinberger.OutputModes +
sc.Steinberger.Auth +
@@ -66,10 +68,11 @@ func RunScorecard(outputDir, pipelineDir string) (*Scorecard, error) {
sc.Steinberger.Doctor +
sc.Steinberger.AgentNative +
sc.Steinberger.LocalCache +
- sc.Steinberger.Breadth
+ sc.Steinberger.Breadth +
+ sc.Steinberger.Vision
if sc.Steinberger.Total > 0 {
- sc.Steinberger.Percentage = (sc.Steinberger.Total * 100) / 90
+ sc.Steinberger.Percentage = (sc.Steinberger.Total * 100) / 100
}
// Grade
@@ -305,6 +308,46 @@ func scoreBreadth(dir string) int {
}
}
+func scoreVision(dir string) int {
+ score := 0
+ cliDir := filepath.Join(dir, "internal", "cli")
+ if fileExists(filepath.Join(cliDir, "export.go")) {
+ score += 2
+ }
+ if fileExists(filepath.Join(cliDir, "import.go")) {
+ score += 1
+ }
+ if fileExists(filepath.Join(dir, "internal", "store", "store.go")) {
+ score += 2
+ }
+ if fileExists(filepath.Join(cliDir, "search.go")) {
+ score += 2
+ }
+ if fileExists(filepath.Join(cliDir, "sync.go")) {
+ score += 1
+ }
+ if fileExists(filepath.Join(cliDir, "tail.go")) {
+ score += 1
+ }
+ // Check for workflow or compound command files
+ entries, err := os.ReadDir(cliDir)
+ if err == nil {
+ for _, e := range entries {
+ name := e.Name()
+ if strings.Contains(name, "_workflow") || strings.Contains(name, "_compound") {
+ if strings.HasSuffix(name, ".go") {
+ score++
+ break
+ }
+ }
+ }
+ }
+ if score > 10 {
+ score = 10
+ }
+ return score
+}
+
func readFileContent(path string) string {
data, err := os.ReadFile(path)
if err != nil {
@@ -348,6 +391,7 @@ func buildGapReport(s SteinerScore) []string {
{"agent_native", s.AgentNative},
{"local_cache", s.LocalCache},
{"breadth", s.Breadth},
+ {"vision", s.Vision},
}
for _, d := range dimensions {
if d.score < 5 {
@@ -423,12 +467,13 @@ func writeScorecardMD(sc *Scorecard, pipelineDir string) error {
{"Agent Native", s.AgentNative},
{"Local Cache", s.LocalCache},
{"Breadth", s.Breadth},
+ {"Vision", s.Vision},
}
for _, d := range dimensions {
bar := strings.Repeat("#", d.score) + strings.Repeat(".", 10-d.score)
b.WriteString(fmt.Sprintf("| %s | %d/10 %s |\n", d.name, d.score, bar))
}
- b.WriteString(fmt.Sprintf("| **Total** | **%d/90** |\n\n", s.Total))
+ b.WriteString(fmt.Sprintf("| **Total** | **%d/100** |\n\n", s.Total))
// Competitor comparison
if len(sc.CompetitorScores) > 0 {
diff --git a/internal/vision/report.go b/internal/vision/report.go
new file mode 100644
index 00000000..42bc109c
--- /dev/null
+++ b/internal/vision/report.go
@@ -0,0 +1,126 @@
+package vision
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func WriteReport(plan *VisionaryPlan, outputDir string) error {
+ if err := os.MkdirAll(outputDir, 0o755); err != nil {
+ return fmt.Errorf("creating output dir: %w", err)
+ }
+
+ var b strings.Builder
+
+ b.WriteString(fmt.Sprintf("# Visionary Research: %s CLI\n\n", plan.APIName))
+
+ // API Identity
+ b.WriteString("## API Identity\n\n")
+ b.WriteString(fmt.Sprintf("- **Domain:** %s\n", plan.Identity.DomainCategory))
+ b.WriteString(fmt.Sprintf("- **Primary users:** %s\n", strings.Join(plan.Identity.PrimaryUsers, ", ")))
+ b.WriteString(fmt.Sprintf("- **Core entities:** %s\n", strings.Join(plan.Identity.CoreEntities, ", ")))
+ b.WriteString(fmt.Sprintf("- **Data profile:** %s volume, %s writes, realtime=%v, search need=%s\n\n",
+ plan.Identity.DataProfile.Volume,
+ plan.Identity.DataProfile.WritePattern,
+ plan.Identity.DataProfile.Realtime,
+ plan.Identity.DataProfile.SearchNeed))
+
+ // Usage Patterns
+ if len(plan.UsagePatterns) > 0 {
+ b.WriteString("## Usage Patterns (by Evidence)\n\n")
+ for i, p := range plan.UsagePatterns {
+ b.WriteString(fmt.Sprintf("### %d. %s (Evidence: %d/10)\n\n", i+1, p.Name, p.EvidenceScore))
+ b.WriteString(p.Description + "\n\n")
+ if len(p.EvidenceSources) > 0 {
+ for _, src := range p.EvidenceSources {
+ b.WriteString(fmt.Sprintf("- %s\n", src))
+ }
+ b.WriteString("\n")
+ }
+ if len(p.Requirements) > 0 {
+ b.WriteString("**Requires:** " + strings.Join(p.Requirements, ", ") + "\n\n")
+ }
+ }
+ }
+
+ // Tool Landscape
+ if len(plan.ToolLandscape) > 0 {
+ b.WriteString("## Tool Landscape\n\n")
+ b.WriteString("| Tool | Stars | Type | Language | Features |\n")
+ b.WriteString("|------|-------|------|----------|----------|\n")
+ for _, t := range plan.ToolLandscape {
+ b.WriteString(fmt.Sprintf("| [%s](%s) | %d | %s | %s | %s |\n",
+ t.Name, t.URL, t.Stars, t.ToolType, t.Language,
+ strings.Join(t.Features, ", ")))
+ }
+ b.WriteString("\n")
+ }
+
+ // Workflows
+ if len(plan.Workflows) > 0 {
+ b.WriteString("## Workflows\n\n")
+ for i, w := range plan.Workflows {
+ b.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, w.Name))
+ if len(w.Steps) > 0 {
+ b.WriteString("**Steps:** ")
+ stepDescs := make([]string, len(w.Steps))
+ for j, s := range w.Steps {
+ stepDescs[j] = s.Description
+ }
+ b.WriteString(strings.Join(stepDescs, " -> ") + "\n")
+ }
+ if w.Frequency != "" {
+ b.WriteString(fmt.Sprintf("**Frequency:** %s\n", w.Frequency))
+ }
+ if w.PainPoint != "" {
+ b.WriteString(fmt.Sprintf("**Pain:** %s\n", w.PainPoint))
+ }
+ if w.ProposedCLIFeature != "" {
+ b.WriteString(fmt.Sprintf("**Proposed:** `%s`\n", w.ProposedCLIFeature))
+ }
+ b.WriteString("\n")
+ }
+ }
+
+ // Architecture Decisions
+ if len(plan.Architecture) > 0 {
+ b.WriteString("## Architecture Decisions\n\n")
+ b.WriteString("| Area | Need | Decision | Rationale |\n")
+ b.WriteString("|------|------|----------|----------|\n")
+ for _, a := range plan.Architecture {
+ b.WriteString(fmt.Sprintf("| %s | %s | %s | %s |\n",
+ a.Area, a.NeedLevel, a.Decision, a.Rationale))
+ }
+ b.WriteString("\n")
+ }
+
+ // Top 5 Features
+ if len(plan.Features) > 0 {
+ b.WriteString("## Top Features for the World\n\n")
+ b.WriteString("| # | Feature | Score | Evidence | Impact | Templates |\n")
+ b.WriteString("|---|---------|-------|----------|--------|----------|\n")
+ for i, f := range plan.Features {
+ f.TotalScore = f.ComputeScore()
+ b.WriteString(fmt.Sprintf("| %d | %s | %d/16 | %d/3 | %d/3 | %s |\n",
+ i+1, f.Name, f.TotalScore,
+ f.EvidenceStrength, f.UserImpact,
+ strings.Join(f.TemplateNames, ", ")))
+ }
+ b.WriteString("\n")
+
+ // Detail for each feature
+ for i, f := range plan.Features {
+ f.TotalScore = f.ComputeScore()
+ b.WriteString(fmt.Sprintf("### %d. %s (Score: %d/16)\n\n", i+1, f.Name, f.TotalScore))
+ b.WriteString(f.Description + "\n\n")
+ if len(f.TemplateNames) > 0 {
+ b.WriteString("**Templates:** " + strings.Join(f.TemplateNames, ", ") + "\n\n")
+ }
+ }
+ }
+
+ path := filepath.Join(outputDir, "visionary-research.md")
+ return os.WriteFile(path, []byte(b.String()), 0o644)
+}
diff --git a/internal/vision/vision.go b/internal/vision/vision.go
new file mode 100644
index 00000000..f4657f50
--- /dev/null
+++ b/internal/vision/vision.go
@@ -0,0 +1,113 @@
+// Package vision defines types for the visionary research system.
+package vision
+
+type DataProfile struct {
+ WritePattern string
+ Volume string
+ Realtime bool
+ SearchNeed string
+}
+
+type APIIdentity struct {
+ DomainCategory string
+ PrimaryUsers []string
+ CoreEntities []string
+ DataProfile DataProfile
+}
+
+type UsagePattern struct {
+ Name string
+ Description string
+ EvidenceScore int
+ EvidenceSources []string
+ Requirements []string
+}
+
+type ToolClassification struct {
+ Name string
+ URL string
+ Stars int
+ Language string
+ ToolType string
+ Features []string
+ Maintained bool
+}
+
+type WorkflowStep struct {
+ Description string
+}
+
+type Workflow struct {
+ Name string
+ Steps []WorkflowStep
+ Frequency string
+ PainPoint string
+ ProposedCLIFeature string
+}
+
+type ArchitectureDecision struct {
+ Area string
+ NeedLevel string
+ Decision string
+ Rationale string
+ ImplementationHint string
+}
+
+type FeatureIdea struct {
+ Name string
+ Description string
+ EvidenceStrength int
+ UserImpact int
+ ImplementationFeasibility int
+ Uniqueness int
+ Composability int
+ DataProfileFit int
+ Maintainability int
+ CompetitiveMoat int
+ TotalScore int
+ TemplateNames []string
+}
+
+func (f *FeatureIdea) ComputeScore() int {
+ return f.EvidenceStrength +
+ f.UserImpact +
+ f.ImplementationFeasibility +
+ f.Uniqueness +
+ f.Composability +
+ f.DataProfileFit +
+ f.Maintainability +
+ f.CompetitiveMoat
+}
+
+type VisionaryPlan struct {
+ APIName string
+ Identity APIIdentity
+ UsagePatterns []UsagePattern
+ ToolLandscape []ToolClassification
+ Workflows []Workflow
+ Architecture []ArchitectureDecision
+ Features []FeatureIdea
+}
+
+func (v *VisionaryPlan) ShouldIncludeTemplate(templateName string) bool {
+ for i := range v.Features {
+ if v.Features[i].TotalScore < 8 {
+ continue
+ }
+ for _, tn := range v.Features[i].TemplateNames {
+ if tn == templateName {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (v *VisionaryPlan) DataProfileRequires(capability string) bool {
+ for _, ad := range v.Architecture {
+ if ad.Area == capability {
+ return true
+ }
+ }
+ return false
+}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index abd0939c..f54e4363 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -33,17 +33,22 @@ Generate the best CLI that has ever existed for any API. Five mandatory phases.
## How This Works
-Every run produces the GOAT CLI through 5 mandatory phases:
+Every run produces the GOAT CLI through 6 mandatory phases:
```
-PHASE 1: DEEP RESEARCH -> PHASE 2: GENERATE -> PHASE 3: STEINBERGER AUDIT -> PHASE 4: GOAT FIX -> PHASE 5: FINAL STEINBERGER
- (5-8 min) (1-2 min) (5-8 min) (3-5 min) (2-3 min)
+PHASE 0: VISIONARY RESEARCH -> PHASE 1: DEEP RESEARCH -> PHASE 2: GENERATE -> PHASE 3: STEINBERGER AUDIT -> PHASE 4: GOAT FIX -> PHASE 5: FINAL STEINBERGER
+ (3-5 min) (5-8 min) (1-2 min) (5-8 min) (3-5 min) (2-3 min)
```
-Total expected time: 15-25 minutes. If a run completes in under 10 minutes, phases were shortcut.
+Total expected time: 18-30 minutes. If a run completes in under 12 minutes, phases were shortcut.
**The Steinberger bar:** Peter Steinberger's gogcli is the 10/10 reference. Every generated CLI is scored against it TWICE - once during audit to find gaps, once after fixes to prove improvement. The delta is the proof of work.
+**Grade thresholds (10 dimensions, 100 max):**
+- **Grade A:** 80+/100 (80%)
+- **Grade B:** 65-79/100 (65-79%)
+- **Grade C:** 50-64/100 (50-64%)
+
---
## Workflow: `--spec` shortcut
@@ -63,6 +68,182 @@ If not found: Phase 1 searches for spec too.
---
+# PHASE 0: VISIONARY RESEARCH
+
+## THIS PHASE IS MANDATORY. DO NOT SKIP IT.
+
+Before generating any CLI, understand what a thoughtful developer would build - not just what the OpenAPI spec says.
+
+### Step 0a: API Identity & Domain Understanding
+
+Understand what this API IS:
+
+1. **WebFetch** the API's developer docs landing page
+2. **WebSearch**: `"<API name>" developer documentation overview`
+3. Extract:
+ - **Domain category:** messaging, payments, productivity, infrastructure, analytics
+ - **Primary users:** Who uses this API? (e.g., "bot developers", "server admins")
+ - **Core entities:** What are the main objects? (e.g., "guilds", "channels", "messages")
+ - **Data profile:**
+ - Write pattern: append-only, mutable, or event-sourced?
+ - Volume: high (millions of records), medium, or low?
+ - Real-time: does the API have webhooks/websockets/SSE?
+ - Search need: high (users need to find things) or low?
+
+### Step 0b: Usage Pattern Discovery
+
+Discover what people ACTUALLY DO with this API:
+
+**Community Research (run these in parallel):**
+1. **WebSearch**: `"<API name>" CLI workflow site:reddit.com`
+2. **WebSearch**: `"<API name>" automation script site:github.com`
+3. **WebSearch**: `"<API name>" "I built" OR "I made" OR "my tool" site:reddit.com OR site:news.ycombinator.com`
+4. **WebSearch**: `"<API name>" tutorial automation workflow 2025 2026`
+
+**Pain Point Research:**
+5. **WebSearch**: `"<API name>" API "pain point" OR "limitation" OR "workaround"`
+6. **WebSearch**: `site:stackoverflow.com "<API name>" API rate limit OR pagination OR bulk`
+
+From all research, identify the **top 5 usage patterns** ranked by evidence score:
+
+| Source | Weight |
+|---|---|
+| Existing tool with 100+ stars | 3 points |
+| Existing tool with 10-99 stars | 2 points |
+| Reddit/HN post with 50+ upvotes | 2 points |
+| Reddit/HN post with 10-49 upvotes | 1 point |
+| Stack Overflow question with 10+ votes | 1 point |
+| Blog post / tutorial | 1 point |
+| GitHub issue on competitor | 1 point |
+| Cross-platform appearance (same need on 2+ platforms) | +2 bonus |
+
+Score >= 6: Strong evidence. Include in CLI.
+Score 3-5: Moderate evidence. Consider as optional.
+Score < 3: Weak evidence. Skip.
+
+### Step 0c: Tool Landscape Discovery (The Discrawl Finder)
+
+Find ALL tools for this API, not just API wrappers:
+
+**Tier 1: Direct CLI Search** (existing Phase 1 does this too)
+1. **WebSearch**: `"<API name>" CLI tool github`
+
+**Tier 2: Non-Wrapper Tool Search** (CRITICAL - finds discrawl-class tools)
+2. **WebSearch**: `"<API name>" sync OR archive OR mirror site:github.com`
+3. **WebSearch**: `"<API name>" search engine OR analytics OR dashboard site:github.com`
+4. **WebSearch**: `"<API name>" backup OR export OR migration site:github.com`
+5. **WebSearch**: `"<API name>" monitor OR watcher OR alerting site:github.com`
+
+**Tier 3: Ecosystem Search**
+6. **WebSearch**: `awesome "<API name>" site:github.com`
+
+For each tool found, classify it:
+
+| Type | Description | Example |
+|---|---|---|
+| **API Wrapper** | Translates HTTP to CLI flags | discli |
+| **Data Tool** | Adds local persistence/search | discrawl |
+| **Workflow Tool** | Orchestrates multi-step sequences | Stripe fixtures |
+| **Environment Tool** | Runs local simulation | Supabase CLI |
+| **Integration Tool** | Bridges to other systems | Zapier integration |
+
+**The press should generate CLIs that compete with Data Tools and Workflow Tools, not just API Wrappers.**
+
+### Step 0d: Workflow Analysis
+
+From usage patterns (0b) and tool landscape (0c), identify multi-step workflows:
+
+For each workflow, document:
+- **Steps:** The sequence of API calls
+- **Frequency:** How often users perform this
+- **Pain point:** What makes this hard with the raw API
+- **Proposed CLI feature:** What compound command would solve it
+
+### Step 0e: Architecture Planning
+
+Based on data profile and workflows, decide what the CLI needs:
+
+| Data Profile | Architecture |
+|---|---|
+| High volume + search need | SQLite + FTS5 (Discord, Slack) |
+| Transaction data + reconciliation | Local ledger with diff tracking (Stripe, Plaid) |
+| Document data + offline editing | Local Markdown/JSON sync (Notion, Confluence) |
+| Low volume + simple CRUD | Standard API wrapper is fine (most APIs) |
+
+For each decision area (persistence, real-time, search, bulk, caching), document:
+- **Need level:** High / Medium / Low
+- **Decision:** What to use
+- **Rationale:** Why
+
+### Step 0f: Feature Ideation - "Next 5 Features for the World"
+
+Score each feature idea on 8 dimensions (16-point max):
+
+| Dimension | Weight | Scoring |
+|---|---|---|
+| **Evidence strength** | 3 | 3=existing tool 100+ stars, 2=Reddit/SO demand, 1=weak, 0=speculation |
+| **User impact** | 3 | 3=most users feel this pain, 2=niche, 1=nice-to-have, 0=nobody asked |
+| **Implementation feasibility** | 2 | 2=can generate template, 1=needs custom code, 0=major infrastructure |
+| **Uniqueness** | 2 | 2=no existing tool, 1=improves on existing, 0=already well-served |
+| **Composability** | 2 | 2=great with pipes/agents, 1=somewhat, 0=interactive-only |
+| **Data profile fit** | 2 | 2=perfect fit, 1=possible, 0=wrong shape |
+| **Maintainability** | 1 | 1=generated code supports it, 0=needs human maintenance |
+| **Competitive moat** | 1 | 1=hard to replicate, 0=trivial |
+
+Score >= 12: **Must-have.** Build it.
+Score 8-11: **Should-have.** Include as optional.
+Score < 8: **Won't-have.** Skip or future work.
+
+### Step 0g: Write the Visionary Research Artifact
+
+**Write** to `~/cli-printing-press/docs/plans/<today>-feat-<api>-cli-visionary-research.md`:
+
+```markdown
+## Visionary Research: <API> CLI
+
+### API Identity
+- Domain: <category>
+- Primary users: <who>
+- Data profile: <write pattern>, <volume>, <realtime>, <search need>
+
+### Usage Patterns (Top 5 by Evidence)
+1. <pattern> (Evidence: X/10) - <what it needs>
+2. ...
+
+### Tool Landscape (Beyond API Wrappers)
+- <tool> (<stars> stars): <what it does>
+- ...
+
+### Workflows
+1. <name>: <steps> -> Proposed: `<api>-cli <command>`
+2. ...
+
+### Architecture Decisions
+- Persistence: <decision> because <rationale>
+- Real-time: <decision> because <rationale>
+- Search: <decision> because <rationale>
+- Bulk: <decision> because <rationale>
+- Cache: <decision> because <rationale>
+
+### Top 5 Features for the World
+1. <feature> (Score: X/16) - <1-line description>
+2. ...
+```
+
+### PHASE GATE 0
+
+**STOP.** Verify ALL of these before proceeding:
+1. API Identity documented with data profile
+2. At least 3 usage patterns with evidence scores
+3. Tool landscape includes non-wrapper tools (Tier 2 search done)
+4. At least 2 workflows with proposed CLI features
+5. Architecture decisions match data profile
+6. Top 5 features scored and ranked
+
+Tell the user: "Phase 0 complete: Domain: [category]. Data profile: [volume]/[realtime]/[search]. Found [N] non-wrapper tools. Top feature: [name] (score [X]/16). Architecture: [key decision]. Proceeding to deep research."
+
+---
+
# PHASE 1: DEEP RESEARCH
## THIS PHASE IS MANDATORY. DO NOT SKIP IT.
@@ -85,6 +266,9 @@ If no spec found: plan to write one from docs in Phase 2.
**WebSearch**: `"<API name>" CLI tool github`
**WebSearch**: `"<API name>" command line client`
+Also search for non-wrapper tools discovered in Phase 0:
+**WebSearch**: `"<API name>" sync OR archive OR export site:github.com`
+
For each competitor found, note repo URL, star count, language.
### Step 1.3: Deep competitor analysis (TOP 2 competitors)
@@ -168,7 +352,7 @@ date: <today>
## Target
- Command count: <N - match or beat best competitor>
- Key differentiator: <specific features we'll have that competitors don't>
-- Quality bar: Steinberger Grade A (65+/80)
+- Quality bar: Steinberger Grade A (80+/100)
```
### PHASE GATE 1
@@ -289,6 +473,16 @@ For each field skipped by the generator (from Phase 2 Step 2.4):
## Part B: First Steinberger Analysis
+### Step 3.0: Run automated scorecard
+
+Before hand-scoring, run the automated scorecard to get objective baseline numbers:
+
+```bash
+cd ~/cli-printing-press && ./printing-press scorecard --dir ./<api>-cli
+```
+
+Use these numbers as the baseline. The hand-scoring in Step 3.7 should explain WHY each dimension got its score, not re-guess the number.
+
### Step 3.7: Score against the Steinberger bar
Score each dimension 0-10. For EACH dimension, provide THREE things:
@@ -308,9 +502,11 @@ Score each dimension 0-10. For EACH dimension, provide THREE things:
| README | X/10 | gogcli: install, quickstart, every command with example, cookbook, FAQ | Add cookbook section, add FAQ |
| Doctor | X/10 | gogcli: validates auth, API version, rate limits, config file health | Add API version check, config health |
| Agent-native | X/10 | gogcli: --json, --select, --dry-run, --stdin, idempotent, typed exits, no TTY | Already strong if all flags present |
+| Local Cache | X/10 | gogcli: file cache + optional embedded DB (bolt/badger), --no-cache bypass, cache clear | [what changes would raise score] |
| Breadth | X/10 | gogcli: 100+ commands covering every API endpoint + convenience wrappers | Add missing commands, add convenience wrappers |
+| Vision | X/10 | discrawl: SQLite + FTS5 + sync + search + tail + domain workflows | Add export, search, sync commands based on Phase 0 research |
-**Baseline Total: X/80 (Grade X)**
+**Baseline Total: X/100 (Grade X)**
```
### Step 3.8: Write the GOAT improvement plan
@@ -344,7 +540,7 @@ Include ALL of:
4. Complex body fields have a plan (not just "limitation")
5. Baseline total score is recorded
-Tell the user: "Phase 3 complete: Baseline Steinberger Score: [X]/80 (Grade [X]). Found [N] tactical fixes + [M] GOAT improvements. Top improvement: [description]. Proceeding to fixes."
+Tell the user: "Phase 3 complete: Baseline Steinberger Score: [X]/100 (Grade [X]). Found [N] tactical fixes + [M] GOAT improvements. Top improvement: [description]. Proceeding to fixes."
---
@@ -352,24 +548,29 @@ Tell the user: "Phase 3 complete: Baseline Steinberger Score: [X]/80 (Grade [X])
## THIS PHASE IS MANDATORY.
-Execute fixes in priority order: (1) GOAT improvements that raise the Steinberger score, (2) tactical fixes from the audit, (3) complex body field examples.
+Execute fixes in priority order:
-### Step 4.1: Apply GOAT improvements (from Step 3.8)
+1. **Scorecard-gap fixes** - run scorecard, identify dimensions below 10/10, fix patterns the scorecard measures
+2. **Complex body field --stdin examples** (useful for agents, visible in help text)
+3. **Command name cleanup** (UX quality, not scored by automated scorecard)
+4. **Description/README polish** (UX quality, not scored)
-For each of the top 5 improvements:
-1. **Read** the relevant file
-2. **Edit** with surgical changes
-3. Focus on changes that RAISE THE STEINBERGER SCORE
+Scorecard-measured improvements first. UX polish second. If the scorecard says 10/10 for a dimension, do not spend time improving it further.
-### Step 4.2: Apply tactical fixes (from Step 3.9)
+### Step 4.1: Apply scorecard-gap fixes
-For each fix in the audit:
-1. Fix help text jargon -> developer-friendly descriptions
-2. Fix examples -> realistic values (real UUIDs, real API objects, not "abc123")
-3. Fix command names -> clean, intuitive names (get/list/create/update/delete)
-4. Fix README -> compelling, useful documentation
+Run the scorecard and fix dimensions below 10/10:
+
+```bash
+cd ~/cli-printing-press && ./printing-press scorecard --dir ./<api>-cli
+```
+
+For each dimension below 10/10:
+1. **Read** the relevant file
+2. **Edit** with surgical changes
+3. Focus on changes that RAISE THE SCORECARD NUMBER
-### Step 4.3: Add complex body field examples
+### Step 4.2: Add complex body field examples
For the top 3 endpoints with complex body fields (identified in Phase 3 Step 3.6):
@@ -384,6 +585,14 @@ Example: ` # Get a page
echo '{"parent":{"database_id":"..."},"properties":{"Name":{"title":[{"text":{"content":"My Page"}}]}}}' | <api>-cli pages create --stdin`,
```
+### Step 4.3: Command name cleanup and description/README polish
+
+Only if time remains after scorecard-gap fixes:
+1. Fix command names -> clean, intuitive names (get/list/create/update/delete)
+2. Fix help text jargon -> developer-friendly descriptions
+3. Fix examples -> realistic values (real UUIDs, real API objects, not "abc123")
+4. Fix README -> compelling, useful documentation
+
### Step 4.4: Verify compilation
```bash
@@ -409,7 +618,13 @@ Tell the user: "Phase 4 complete: Applied [N] improvements, [M] tactical fixes,
### Step 5.1: Second Steinberger Analysis (Post-Fix)
-Re-score ALL 8 dimensions. Show the DELTA from the baseline:
+Run the automated scorecard again to measure improvement:
+
+```bash
+cd ~/cli-printing-press && ./printing-press scorecard --dir ./<api>-cli
+```
+
+Re-score ALL 10 dimensions. Show the DELTA from the baseline:
```markdown
## Final Steinberger Analysis (Post-Fix)
@@ -423,9 +638,11 @@ Re-score ALL 8 dimensions. Show the DELTA from the baseline:
| README | X/10 | Y/10 | +Z | [specific change] |
| Doctor | X/10 | Y/10 | +Z | [specific change] |
| Agent-native | X/10 | Y/10 | +Z | [specific change] |
+| Local Cache | X/10 | Y/10 | +Z | [specific change] |
| Breadth | X/10 | Y/10 | +Z | [specific change] |
+| Vision | X/10 | Y/10 | +Z | [specific change] |
-**Before: X/80 -> After: Y/80 (+Z points)**
+**Before: X/100 -> After: Y/100 (+Z points)**
**Grade: [A/B/C]**
```
@@ -448,7 +665,7 @@ Resources: <comma-separated list>
**2. Steinberger Score (Before/After):**
```
-Steinberger Score: Before X/80 -> After Y/80 (+Z points) - Grade [A/B/C]
+Steinberger Score: Before X/100 -> After Y/100 (+Z points) - Grade [A/B/C]
[Full before/after table from Step 5.1]
```
@@ -520,3 +737,5 @@ These phrases indicate a phase was shortcut. If you catch yourself writing them,
- "We'll skip this for now" (no skipping - do it or explain why it's impossible)
- "The quality is good enough" (score it against Steinberger, prove it's good enough with numbers)
- "Let's wrap up" (are all 5 phases complete with artifacts?)
+- "This API doesn't need local persistence" (Did you run Phase 0? Check the data profile. If search need is high, it needs persistence.)
+- "This is just an API wrapper" (Run Phase 0 again. What would a thoughtful developer build?)
← bb0acf37 feat(skill): v1.1.0 dual Steinberger analysis, deep research
·
back to Cli Printing Press
·
fix(scorecard): replace presence checks with quality-based s 5c6840d5 →