▍ humdrum codex / dots v1.0.0

docs: dots-cli v1 design spec

667fea5df0aeb61abb87dea2b3020e2d0eb24e6a
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-07-29 15:20

docs: dots-cli v1 design spec

Approved in brainstorming 2026-07-29: Go thin client (log/clear/note/today/types),
paste-token auth via /cli page + cli_tokens table, plan-fill rule, no-silent-overwrite
confirmation rule, one small server PR reusing widget-token machinery.

1 files changed

docs/superpowers/specs/2026-07-29-dots-cli-design.md +0 −148
@@ -1,148 +0,0 @@
-# dots-cli — v1 design
-
-**Date:** 2026-07-29
-**Status:** Approved pending user review
-**Repos touched:** this repo (new, Go CLI) + `humdrum-tiv/dots` (small server PR)
-
-## Purpose
-
-A local command-line client for Dots (dots.humdrum.one) optimized for reflex-speed
-logging from the terminal: paint a block, add a day note, glance at today — faster than
-reaching for the phone. Third client after web and iOS; rides the same API surface.
-
-Explicitly **not** an offline client: no local store, no sync engine, no outbox. Every
-command is one or two HTTP calls against the production API. If the network is down the
-command fails fast and says so.
-
-## Command surface (v1)
-
-```
-dots auth                          Link CLI to account (one-time token paste)
-dots log [time|range] [activity]   Paint logged block(s)
-dots clear <time|range>            Clear logged block(s)
-dots note "text" [--date D]        Append a day note
-dots today [--date D] [--json]     Day grid readout
-dots types                         List active activity types
-```
-
-Global flags: `--date YYYY-MM-DD` where noted (defaults to today, device timezone),
-`--yes`/`-y` to skip confirmation prompts, `DOTS_URL` env var to override the server
-(dev/testing; defaults to `https://dots.humdrum.one`).
-
-### Time syntax
-
-Dots blocks are 30-minute slots. Accepted forms:
-
-- *(omitted)* — the slot containing the current device time
-- `14:30` (also `9:00`, `09:00`) — the single slot containing that time; times snap
-  down to the slot boundary (`14:47` → `14:30`)
-- `13:00-15:00` — range, end-exclusive: slots 13:00, 13:30, 14:00, 14:30
-
-Argument parsing: the first positional arg is treated as a time/range only when it
-matches the time syntax (`H:MM`, `HH:MM`, or a `-`-joined pair); otherwise it is the
-activity. So `dots log archer` = current slot + archer, `dots log 14:30` = that slot +
-plan-fill.
-
-### Logging semantics
-
-- `dots log <time> <activity>` paints each target slot with the matched activity type.
-- **Plan-fill rule:** activity omitted → each target slot takes the activity of its
-  *plan block*. Slots in the target with no plan block cause an error listing those
-  slots, and **nothing is painted** (all-or-nothing). This mirrors the native app's
-  long-press plan-fill gesture.
-- **No silent overwrite (hard rule):** before any write, the CLI fetches the day and
-  diffs targets against existing logged blocks. If any target slot is already logged,
-  it prints a change table (`14:30  archer → foofaraw`) and prompts `y/N`. Declining
-  aborts with no write. `--yes` skips the prompt. **Non-TTY without `--yes` aborts
-  with an error** — scripts must opt in explicitly.
-- `dots clear` shows the same table (what will be cleared) with the same confirm rule.
-- Painting empty slots needs no confirmation.
-
-### Activity matching
-
-Client-side fuzzy match against the active types returned by the day payload:
-case-insensitive prefix match first, then substring. Multiple hits → numbered pick
-prompt (error under non-TTY). Zero hits → error listing closest names. The CLI cannot
-create activity types (same v1 rule as the native app); `dots types` shows what exists.
-
-### `dots today` output
-
-- TTY: truecolor dot strip in activity-type colors rendered with lipgloss; planned
-  blocks shown dim/hollow; legend of the day's types below; current slot marked.
-- Not a TTY or `--json`: raw JSON of the day payload (blocks, plan blocks, types) for
-  scripting (tock, Raycast). Color output auto-degrades on non-truecolor terminals.
-
-## Auth
-
-- Server page `dots.humdrum.one/cli` (Clerk-authenticated) shows generate / copy /
-  revoke for a long-lived CLI token.
-- `dots auth` opens that page in the browser, prompts for a paste, verifies the token
-  with one API call, then stores it in the macOS Keychain (zalando/go-keyring; fallback
-  `~/.config/dots/token`, chmod 600, on other platforms).
-- All requests send `Authorization: Bearer <token>`. A 401 response prints
-  "not linked — run `dots auth`".
-- Never prompts for Clerk credentials in the terminal.
-
-## Server changes (`humdrum-tiv/dots`, one small PR)
-
-Modeled on the existing widget-token machinery (`lib/db/widgetTokens.ts`,
-`/api/widget/token`, `/api/widget/data`):
-
-1. **`cli_tokens` table** — clone of `widget_tokens` (user id, token, created at).
-   Separate row so revoking the widget token doesn't kill the CLI and vice versa.
-2. **`/cli` page** — signed-in page with generate/copy/revoke, mirroring the widget
-   token UI.
-3. **Shared auth helper** `resolveUserId(request)` — tries Clerk `auth()` first, falls
-   back to `Bearer` → `getUserIdByCliToken`. Applied to exactly:
-   - `GET /api/widget/data` (additionally accept CLI tokens)
-   - `POST/PUT /api/days/[date]/blocks`
-   - `POST /api/days/[date]/notes`
-4. Nothing else changes. Block-write business logic is reused exactly as web/iOS use it.
-
-`GET /api/widget/data?date=D&tz=TZ` already returns blocks, plan blocks, activity
-types, and events — everything the CLI reads. One GET feeds `today`, type matching,
-plan-fill lookup, and the overwrite diff; write commands are 1 GET + 1 POST.
-
-## CLI internals
-
-```
-cmd/dots/main.go        cobra root + command wiring
-internal/api/           HTTP client: GetDay, WriteBlocks, ClearBlocks, AddNote, VerifyToken
-internal/auth/          keychain load/store, auth command flow
-internal/day/           slot math (time→slot index, tz), range parsing, fuzzy match,
-                        plan-fill resolution, overwrite diff
-internal/render/        today grid (lipgloss truecolor), --json passthrough
-```
-
-- Go 1.23+. Dependencies: cobra, lipgloss, zalando/go-keyring. No local DB, no config
-  file beyond the stored token.
-- Timezone: device-local at call time, passed as `tz` to the API.
-- `internal/day` is pure functions (no I/O) — the testable core.
-- Structured for a later `dots tui` subcommand (Bubbletea) and an optional Wish/SSH
-  wrapper over the same model; neither is in v1.
-
-## Errors
-
-Plain one-line messages to stderr, exit code 1. Network unreachable → "can't reach
-dots.humdrum.one". No retry/backoff in v1 — it's a reflex tool; the user reruns.
-
-## Testing
-
-- Table-driven unit tests for everything in `internal/day`: slot math, range parsing
-  (including end-exclusivity and snap-down), fuzzy matching, plan-fill resolution,
-  overwrite-diff computation.
-- `internal/api` against `httptest` servers using fixture captures of real
-  `widget/data` payloads.
-- Server PR: route tests covering both auth kinds (Clerk session, CLI bearer) plus
-  rejection of widget tokens on CLI-only paths and vice versa where applicable.
-
-## Release / repo
-
-- `go install` from the repo for now; binary lands in PATH. No packaging in v1.
-- Repo: jj-colocated, GitHub `humdrum-tiv/dots-cli` (private), Backlog.md initialized
-  with the standard 4 statuses.
-
-## Out of scope (v1)
-
-Offline queueing, plan-block editing, type creation, timeline/stats/week views,
-`dots tui`, SSH serving, non-macOS packaging, retry logic.