@drakulavich/oura-cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] - 2026-09-13
10
+
11
+ ### Added
12
+ - `db rows <collection>` prints the cached rows of any collection, as stored, in both output modes: the cache-side twin of `fetch`, with the same range flags and defaults (`--day`, `--from/--to`, `--days`; today by default; none for `ring`). Eight of the seventeen collections were write-only from the user's side: `sync` filled resilience, VO₂ max, bedtime guidance, sessions, rest mode, tags, ring and battery, `db stats` counted them, and nothing read them back short of `fetch`, which goes to the API and ignores the cache. The table view shows one column per stored column, numbers right-aligned, text cells cut at 40 characters and, on a terminal, squeezed further so the table fits the screen; a column name is never cut, and when the names alone would not fit (sleep-periods has 19 columns) each row is printed as a block of `name value` lines instead. `--limit N` prints the first N rows and the title says how many the range holds; a day of heart rate is hundreds. A newline in a tag comment shows as ⏎ instead of breaking the row. The JSON is the rows exactly as stored, so a JSON blob such as `contributors` stays a string. Heart-rate and battery samples are bounded on the local day asked for, the way `fetch` bounds its request, not on the UTC date their `day` column stores. An empty result names the range asked for and how to fill it. The `describe` manifest lists the subcommand with the collection enum. (#73)
13
+
14
+ ### Fixed
15
+ - `doctor` no longer calls a cache current while `report` and `db week` mark its newest day as still accumulating. The `data` check compared calendar days and only warned past two, so on 2026-09-12 a cache ending on 2026-09-10 passed. It now measures hours from the end of the newest day (its local midnight in the cache timezone) and warns past 36: one missed night is ordinary ring lag, a second is worth the nudge. The detail names the limit, and `nextStep` points at `oura-cli sync`. (#114)
16
+ - Text output is tidier. Every view draws its horizontal rule at the same two-column indent as the text under it (`db today`, `db week`, `db trends`, `db stats`, `doctor` and the panel after `sync` drew theirs at column 0), as wide as the widest line it delimits (a fixed 50 sat under 63-column trend rows), and never wider than the terminal, where it wrapped onto a second line. `db week` no longer pads its last column, which left up to nine trailing spaces on every row. The empty-today panel after `sync` explains that Oura publishes a day's summary once the night's sleep has synced, as `db today` already did, instead of printing bare dashes. `oura-cli db` on its own, `db toady`, and `db --format json` with no subcommand name the subcommands `db` takes and point at `oura-cli db --help` rather than the root help; on a terminal a bare `oura-cli db` prints that help, as a bare `oura-cli` does. (#61)
17
+ - A `429 Too Many Requests` no longer ends a long `fetch` or `sync` on the spot. The page is retried up to three times after the wait `Retry-After` asks for (seconds or an HTTP date, each wait capped at one minute), or one, two and four seconds without it, and one command waits at most three minutes on 429 answers in total; only then is it an `API_ERROR`, with a hint that says how often the page was retried, how long the command waited, and to try again later or ask for a shorter range. A transport failure (DNS, connection refused) is an `API_ERROR` with a hint too, where it was an `UNKNOWN` with none. On a terminal, `fetch` and `sync` show a one-line progress counter on stderr (`fetching heartrate: page 41, 40000 rows so far…`), which also says when the client is waiting on a 429 (`; rate limited, retrying in 30 s…`) and is wiped when the command finishes; a 61-day `fetch hr` is 77 pages and was over a minute of silence, and a rate-limit wait was the same silence. On a pipe stderr stays clean, since it carries the error envelope. `doctor` names a next step when the API could not be reached, where it reported `ok: false` with `nextStep: null`. No upper bound was put on the range: a long backfill is a legitimate use, and pacing rather than refusing is what the API asks for. (#45)
18
+ - A malformed response body from the Oura API is now an `API_ERROR` (exit 3) naming the endpoint and what came back, instead of an `UNKNOWN` with an internal expression. `sync` and `fetch` walked whatever sat under `data`: an object failed with `{} is not iterable`, and a string was iterated character by character and reported as dropped samples that never existed. A missing or null `data` is still an empty page; a `next_token` that is not a string or null is the same error, not a last page. (#112)
19
+ - `db date` and `db today` mark a day whose activity totals are not final the way `db week` and `report` already do: `2026-09-10*` in the header and one note under the numbers. The JSON has carried `partial` since 0.7.1; the table output did not show it, so drilling from a marked week row into the day lost the mark. The note's wording is now one string shared by the day and week views. (#113)
20
+ - Small text inconsistencies from the eight-collection expansion. `db stats` names each table by both its collection name and its table (`sleep-periods (sleep_model)`), as the `sync` lines already did, and prints `1 row`, not `1 rows`; its JSON rows carry a `collection` field. `db stats` explains itself when the cache holds rows but no daily summaries, instead of printing counts and nothing else. The `*` note in `report` says "activity totals are not final" like the day and week views, which reads right when the marked day is in the past. `sync --help` describes what the command prints; the unknown-flag hint no longer claims there are no single-letter flags when `-v` and `-h` exist; README names the identity fields the per-collection schemas actually pin. (#72)
21
+ - A `sync` response that includes a row from outside the window it was asked for no longer widens that request's reconciliation scope. The scope of one request was taken from the timestamps that came back, so a single early or empty-string timestamp reached back over every stored sample between it and the window: on a 60-sample cache one stray value put 59 rows up as "not returned", with the truncation guard's own message naming the `--prune=hr` flag that would have deleted them. Each request now carries the range it asked for and only rows inside it count towards the scope; rows outside are still stored, they just vouch for nothing. (#111)
22
+
23
+ ## [0.7.1] - 2026-09-12
24
+
25
+ ### Fixed
26
+ - Whether a day's activity totals are final is now read off the day itself. Oura's `class_5_min` carries one character per five-minute slot, so a day reporting a full 288 of them has been closed out; `sync` stores the length in a new `daily_activity.class_5_min_slots` column and `report` reads it. The converse does not hold, and the rule does not claim it: a count *below* 288 is not evidence a day is still open. A day's span is `timestamp(d+1) − timestamp(d)`, so travelling east shortens it — 2023-12-15 came back with 276 slots and 13,637 steps, 2024-01-13 with 270 and 15,736, both closed long ago — while 288 also comes back for 23-hour days and caps longer ones. So the slot count is read as positive evidence of closure only, and a day short of 288 falls back to the old next-day rule rather than being called open. The old rule inferred it instead — a day counted as complete once a *later* day had its own record — which described the ring's behaviour rather than the day, and came apart wherever the two did: a ring that stopped uploading froze its last day as "still accumulating" forever, keeping real steps out of every average; a report timezone west of the ring's discarded a day the cache had already closed; and `completeThrough` could name a day holding no activity record at all, promising an average over a day the table never printed. Rows written before the column exists keep NULL and take the same fallback, so an upgraded cache fills in as `sync` re-fetches each day — `sync --from` fills older days on demand. Because the slot count can only ever close a day and never reopen one, upgrading never makes a day with an activity record *less* complete than it already was: over a real 1,044-day cache the two rules together name exactly the days the old rule named. (#74)
27
+ - `db trends`, `db stats` and `db week` now use that same judgement, so the screens stop disagreeing about one week (the all-time averages in `db stats` move by a few steps for the same reason: the day in progress no longer counts). `db trends` averaged the day in progress as though it were whole while `report` excluded it, and `db week` showed its part-day steps unmarked: the same seven days gave a 7,610 step average in one place and 6,639 in another. Activity, steps and active calories now stop at the last complete day in the window; sleep and readiness are final once they exist and still cover all of it. The week table marks the day with `*` and `DaySummary` carries `partial`, matching what `report` has always shown. (#75)
28
+ - `sync` no longer aborts on a heart-rate sample without a `timestamp`. The `day` column is derived from it, so a null threw a `TypeError` at insert time, outside the CLI's error mapping, and every collection after `hr` in the run never synced. A row whose identity field is missing, null, empty, or not the type the column stores is now dropped before insert, counted in the JSON under `dropped` and named on the collection's line (`1 dropped (no timestamp/source)`); the row was unusable anyway, since those columns are what it would have been stored under. A row is judged by the columns its table keys on, not by the manifest's identity fields: a daily summary Oura sends without an `id` is still stored under its `day`. (#106)
29
+ - An empty `200` from a snapshot endpoint no longer clears its table. `ring` is fetched whole every run and was replaced with whatever came back, so a partial read or an upstream hiccup deleted every ring and the JSON showed nothing in `removed`. The snapshot path now follows the rule the ranged collections already had: an empty answer describes nothing, so the rows are kept and reported under `refused` with the `--prune=ring` hint; a ring genuinely gone from the account is applied with that flag. Rings a non-empty response no longer lists are counted in `removed`, as they always should have been. (#105)
30
+ - `db trends` and `db stats` say what is missing on an empty cache instead of printing a header over nothing, or seventeen lines of `0 rows`. Both now carry the same "run `oura-cli sync`, then this command again" line that `db today` and `db week` already print. (#85)
31
+ - `report --period month` chunks its week buckets from the newest day back, so the remainder of a 30-day window is the oldest row and is labelled with its size (`2026-08-13 (2 days)`), not an unlabelled stub at the bottom that read as activity collapsing by 85 %. The `*` note names the bucket it marks (`the week of …`) instead of saying "today" above a table with no day rows. (#84)
32
+ - The `describe` and `manifest` snapshots now cover the real command registry. The old snapshot was built from a hand-listed fixture that did not include `sync`, so #103 added `--prune` to a published contract and no test moved. The registry lives in `src/commands/registry.ts`, the contract test asserts it matches the argv normalizer's command set, and the fixture-based tests stay as they were, since a controlled input is what they need. (#104)
33
+
9
34
  ## [0.7.0] - 2026-09-09
10
35
 
11
36
  ### Added
package/README.md CHANGED
@@ -96,7 +96,7 @@ oura-cli report # weekly (default)
96
96
  oura-cli report --period month # 30-day window with weekly buckets
97
97
  ```
98
98
 
99
- Reports cover daily scores, averages, deltas vs the previous window, sleep details, and a short recommendation block. A day whose activity is still accumulating (today, or the last day before the ring stopped syncing) is shown with a `*` and kept out of the activity averages and recommendations; the JSON says so via `days[].partial` and `completeThrough`.
99
+ Reports cover daily scores, averages, deltas vs the previous window, sleep details, and a short recommendation block. A day whose activity is still accumulating normally just today is shown with a `*` and kept out of the activity averages and recommendations; the JSON says so via `days[].partial` and `completeThrough`. `db today`, `db date` and `db week` carry the same `partial` flag and mark the day with the same `*` and note, so the screens cannot disagree. A day is treated as closed once Oura reports a full 24 hours of five-minute activity slots for it, so a ring that stops syncing no longer freezes its last day as unfinished.
100
100
 
101
101
  ### Trends and stats
102
102
 
@@ -105,6 +105,16 @@ oura-cli db trends 30 # score trends across the last 30 days
105
105
  oura-cli db stats # row counts, date range, personal bests
106
106
  ```
107
107
 
108
+ ### Cached rows of any collection
109
+
110
+ `db rows` prints what the cache holds for one collection, as stored, in either output mode. It takes the same range flags and defaults as `fetch` (`--day`, `--from/--to`, `--days`; today by default; none for `ring`), so the two are twins: `fetch` reads the API, `db rows` reads the cache. This is how the collections no summary shows — tags, sessions, resilience, VO₂ max, bedtime guidance, rest mode, ring, battery — are read back.
111
+
112
+ ```bash
113
+ oura-cli db rows tags --days 30 # your own annotations for the month
114
+ oura-cli db rows battery --day 2026-09-01 # the battery curve for one day
115
+ oura-cli db rows ring --format json # every ring on the account, as JSON
116
+ ```
117
+
108
118
  ### Raw API records
109
119
 
110
120
  `fetch` returns one collection straight from the Oura API as JSON, without touching the local cache.
@@ -187,7 +197,7 @@ If you're driving the CLI from a script or LLM harness:
187
197
  - Gate on `.ok`, not on the exit code: `doctor` exits 0 with `ok: false` for any warning-level check (no data yet, stale data, Oura API unreachable), and `healthcheck` exits 0 with `ok: false` for an unusable database (the probe itself ran). `doctor --offline` skips the token-validation call, and a skipped check still counts towards `ok`.
188
198
  - Errors emit a stable JSON envelope on stderr: `{"error":{"code":"…","message":"…","hint":"…"}}`.
189
199
  - Documented exit codes: `0` success, `1` user error, `2` auth, `3` API, `4` storage.
190
- - JSON Schemas under [`docs/schemas/`](docs/schemas/) cover `fetch <collection>`, `doctor` and the `describe` manifest itself, semver-stable; `describe` names the schema next to each command. Per-collection schemas pin the identity fields (`id`, `day`, `timestamp`) and allow the rest of the Oura record through unchanged, so new upstream fields never break validation. The local-data commands (`sync`, `db *`, `report`) have no schema files yet; their shapes are versioned through the CHANGELOG.
200
+ - JSON Schemas under [`docs/schemas/`](docs/schemas/) cover `fetch <collection>`, `doctor` and the `describe` manifest itself, semver-stable; `describe` names the schema next to each command. Per-collection schemas pin each collection's identity fields (`id` and `day` for the daily summaries, `timestamp` for the heart-rate and battery series, `id` and `start_day` for rest-mode and tags, `id` alone for ring) and allow the rest of the Oura record through unchanged, so new upstream fields never break validation. The local-data commands (`sync`, `db *`, `report`) have no schema files yet; their shapes are versioned through the CHANGELOG.
191
201
  - Two contract quirks, kept for compatibility: `report --period month` returns its window as `weekStart`/`weekEnd`, and `heartrate.day` (likewise `ring_battery_level.day`) in the cache is the date written in Oura's timestamp (UTC in practice) while every `--day`/`--tz` argument is local.
192
202
 
193
203
  Plays cleanly with [OpenClaw](https://github.com/openclaw/openclaw) — `oura-cli manifest` returns the tool-registry shape. A first-party `oura-mcp` companion is on the roadmap.