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 blockSize-minute slots, where blockSize is a per-day setting of
15, 30, or 60 (blockIndex = minutesSinceMidnight / blockSize). The CLI reads
day.blockSize from the day payload and does all slot math against it. Accepted forms
(examples at blockSize 30):
- (omitted) — the slot containing the current device time
14:30(also9: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. Ranges do not cross midnight.
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 promptsy/N. Declining aborts with no write.--yesskips the prompt. Non-TTY without--yesaborts with an error — scripts must opt in explicitly. dots clearshows 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 authopens 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 — rundots 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):
cli_tokenstable — clone ofwidget_tokens(user id, token, created at). Separate row so revoking the widget token doesn't kill the CLI and vice versa./clipage — signed-in page with generate/copy/revoke, mirroring the widget token UI.- Shared auth helper
resolveUserId(request)— tries Clerkauth()first, falls back toBearer→getUserIdByCliToken. Applied to exactly:GET /api/widget/data(additionally accept CLI tokens)POST/PUT /api/days/[date]/blocksPOST /api/days/[date]/notes
- 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
tzto the API. internal/dayis pure functions (no I/O) — the testable core.- Structured for a later
dots tuisubcommand (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/apiagainsthttptestservers using fixture captures of realwidget/datapayloads.- 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 installfrom 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.