Build a fast, scriptable CLI for Hardcover that lets you:
- View your current reading list
- Search books
- Set an “active” book
- Update reading progress by page
- Change status, using
okufor the “to read” pile - Stay reliable via cache-first behavior and secure token storage
Non-goals (v1):
- Full-screen TUI
- Social features (reviews, feed, etc.)
- MCP server (planned later)
- I can authenticate with my Hardcover API token securely.
- I can list my books by status:
reading,oku,finished,dnf. - I can search for a book and choose it.
- I can set an “active book” so commands default to it.
- I can update progress via page number quickly.
- I can change status quickly from the CLI.
- The tool works well in a terminal workflow (pipeable
--json, cache-first, fast).
Binary name: oku
-
oku auth set-token- Stores token securely (macOS Keychain preferred).
- Accept token via prompt or stdin.
-
Token priority:
HARDCOVER_TOKENenv var- Keychain token
- (optional) config token if user explicitly enables plaintext storage
oku list <reading|oku|finished|dnf> [--json] [--refresh]- Reads from cache by default
--refreshforces API sync
Convenience shortcuts (optional but ergonomic):
oku readingoku okuoku finishedoku dnf
oku now [--json] [--refresh]- Equivalent to
oku list reading
- Equivalent to
oku search <query> [--json] [--limit N]- Returns book results (title/author/id/page count if available)
-
oku active [--json]- Shows the active book (from local state)
-
oku set-active --book <id>- Sets active book explicitly
-
oku open- Interactive picker to choose a book and set active
- Uses
fzfif installed, otherwise a simple numbered prompt
oku update --page <N|+N|-N> [--book <id>] [--note <text>]-
If
--bookomitted, targets active book -
--pageparsing:123→ set page to 123+10→ add 10 pages-5→ subtract 5 pages
-
Validation:
- clamp to >= 0
- if total pages known, clamp <= total (or warn + allow if you prefer)
-
oku status <reading|oku|finished|dnf> [--book <id>]- Default target: active book
Backwards-compatible aliases (recommended):
want,wtr,want-to-read→oku
oku sync- Refresh all tracked lists into cache (reading/oku/finished/dnf)
- Refresh active book details
-
oku config edit- Opens config file in
$EDITOR(fallbackvi)
- Opens config file in
-
oku config show- Prints current config values (no secrets)
Exit codes:
0success1user/input error (missing token, missing active book, invalid args)2network/API error
Internal enum:
readingokufinisheddnf
Display:
- “Oku” in headings/output (or configurable)
Path: ~/.config/oku/config.toml (or XDG)
Suggested keys:
editor = "nvim"notes_dir = "~/notes/books"(optional, v1 can omit notes)use_fzf = truedefault_list = "reading"
- macOS Keychain entry (service
oku, accounthardcover) - env var override
HARDCOVER_TOKEN
SQLite at ~/.local/share/oku/cache.db (or XDG)
Suggested tables:
-
booksid TEXT PRIMARY KEYtitle TEXTauthors TEXT(JSON array or joined string)page_count INTEGERupdated_at TEXT
-
user_booksbook_id TEXTstatus TEXT(reading|oku|finished|dnf)current_page INTEGERlast_progress_at TEXTlast_sync_at TEXTPRIMARY KEY (book_id)
-
statekey TEXT PRIMARY KEYvalue TEXT- store
active_book_id, timestamps, etc.
Cache strategy:
- Serve lists from cache immediately
- Refresh only when asked (
--refreshoroku sync) - Keep API calls minimal to avoid throttling
Use a GraphQL client:
- Preferred: typed generation (e.g.
genqlient) - Alternative: lightweight client (e.g.
machinebox/graphql)
Requirements:
- Set
authorization: <token>header - Rate limit to stay under Hardcover’s 60 req/min
- Retry/backoff on 429 + transient 5xx
- Normalize API objects into internal domain structs
Minimum operations to implement:
GetMe(validate token)ListUserBooks(status)forreading,oku,finished,dnfSearchBooks(query, limit)SetUserBookStatus(bookId, status)UpdateReadingProgress(bookId, page)
Keep list queries minimal (id/title/authors/page_count/current_page/status/updated_at).
Suggested layout:
cmd/oku/
internal/
app/ // business logic: list/search/update/status/active
api/ // hardcover graphql client + operations
store/ // sqlite cache + state + token provider
cli/ // command definitions + output formatting
picker/ // fzf integration + fallback prompt
config/ // config load/validate
Rule:
internal/appis the “engine” used by commands.internal/clishould not know about GraphQL response types.
Deliver:
oku auth set-tokenoku nowoku list <status>oku searchoku open(fzf + fallback)oku set-active,oku activeoku update --pageoku status <reading|oku|finished|dnf>- SQLite cache + state
Acceptance:
- You can do a full loop:
oku search "…",oku openoku status readingoku update --page 123oku status finished
Deliver:
--jsonfor list/search/active/nowoku sync- better errors +
--debuglogging (never prints token) - Homebrew formula/tap (optional)
Unit tests:
- page parsing (
123,+10,-5, invalid) - alias mapping (
want→oku) - state handling (active book set/get)
- cache upsert behavior
Integration tests (optional):
- mock GraphQL responses, ensure commands behave as expected
Manual QA:
- missing token
- invalid token
- missing active book
- offline behavior (cached list still works)
- rate limit (simulate 429)
- Add TUI (
okufullscreen) - Add MCP server adapter on top of
internal/app - Notes integration (
oku note, open in nvim) - Audiobook/time progress