@drakulavich/oura-cli 0.5.2 → 0.7.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 +37 -0
- package/README.md +17 -4
- package/dist/index.js +754 -138
- package/docs/schemas/battery.json +21 -0
- package/docs/schemas/doctor.json +1 -1
- package/docs/schemas/resilience.json +26 -0
- package/docs/schemas/rest-mode.json +26 -0
- package/docs/schemas/ring.json +20 -0
- package/docs/schemas/session.json +26 -0
- package/docs/schemas/sleep-time.json +26 -0
- package/docs/schemas/tags.json +26 -0
- package/docs/schemas/vo2max.json +26 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sync --prune=<collections>` applies removals the truncation guard refuses. The guard keeps rows when a response drops most of what one request described, which is right for a partial or short answer and wrong for a genuine large correction — and it cannot tell them apart, so a genuine one was refused on every run with no way through. Narrowing the window did not help: the scope is the returned rows' own bounds, so the ratio never moved, and `sync --from D --to D` refused identically. The flag is the user's answer to the question the guard cannot ask, per run and never stored.
|
|
13
|
+
|
|
14
|
+
It is scoped to the collections named because consent is per collection: the refusal message names the collection to pass (`re-run with --prune=hr`), so lifting the guard for one does not lift it for another that answers short in the same run — a loss that never comes back behind `--from`, since the next ordinary sync resumes from the watermark. `--prune=all` covers every collection; a valueless `--prune` is refused, because as a string flag it would otherwise swallow whatever came next on the line. Removals that went past the guard are counted separately from ordinary reconciliation, in the JSON as `pruned` and on the collection line as `(N past the truncation guard)`, so a bypass is never silent. `refused` and `pruned` carry the collection name alongside the count (`{rows, collection}`), because they are the only fields that imply a follow-up command and the command takes a name the table-keyed payload did not contain — `sync` is JSON-only off a TTY, and an agent that cannot map `heartrate` back to `hr` would reach for `--prune=all`. A run that passed the flag also records `pruneScope`, so a kept JSON log can say whether the guard was lifted and over what. A piece that returns nothing still removes nothing, flag or not. Where two pieces overlap, every piece covering a row is consulted before that row is decided, and one doubting piece is enough to keep it — a verdict taken while walking the pieces would have been settled by whichever came first, so the same responses in a different order could delete a row instead of keeping it. (#100)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `sync` kept both rows when Oura reclassified a heart-rate sample (`awake` → `workout` shares the timestamp, and the unique index is `(timestamp, source)`), and no command could repair it. A re-fetched window is now reconciled against the response: rows the API no longer has are removed, inside the same transaction as the inserts. Verified on a copy of a real cache, where it removed exactly the five stale samples the exploratory session had found. (#91)
|
|
18
|
+
- The same for the five tables whose `day` is not unique (`sleep_model`, `workouts`, `sessions`, `rest_mode_periods`, `enhanced_tags`): a record re-issued under a new id used to leave both rows, so `report` averaged one night twice. The scope of a removal always comes from the response and from one request at a time — a range wider than the endpoint allows is fetched in pieces, and a piece that answers with nothing describes nothing, so its days keep their rows. A piece whose answer would drop most of what is stored for it is treated as truncated: the removal is refused and reported. (#71)
|
|
19
|
+
- `sync` reports `removed` and `refused` per table in its JSON, and the progress line says how many stale rows went and how many were kept because the response looked truncated.
|
|
20
|
+
- `ensureSchema` read the schema version and applied migrations with nothing serialising the two, so concurrent commands recorded the same migration several times — harmless only while every migration is `CREATE ... IF NOT EXISTS`, and a hard failure for the loser as soon as one is an `ALTER TABLE`. Check and apply now run under `BEGIN IMMEDIATE`. Two processes creating the same cache also raced on `PRAGMA journal_mode = WAL`, which answers `SQLITE_BUSY` without waiting out `busy_timeout`; losing that race is no longer an error, since the winner set the mode this process wanted. (#77)
|
|
21
|
+
- A misspelled global flag left its value in the command position, so the CLI reported the value as the command name — and for `--tok <token>` that put a Personal Access Token into an error message. A near miss of a known flag is now blamed on the flag (`Unknown flag "--tok". Did you mean --token?`), and a value that cannot be a command name is never quoted back. (#95)
|
|
22
|
+
- An empty `--token` or `OURA_TOKEN` fell through to the token file, so a wrapper expanding an unset variable authenticated as whoever that file holds; an empty `OURA_TZ` fell through to the system zone and shifted every day boundary silently. Both are now `BAD_ARGS`, like the `--db` case fixed in 0.6.0, through one shared guard. (#92)
|
|
23
|
+
- A database in a directory the user cannot write reported `attempt to write a readonly database` with a hint about the file's format. The file is fine; SQLite needs to create `-wal` and `-shm` beside it. Permission failures now get their own hint. (#78)
|
|
24
|
+
- `doctor` runs SQLite's `quick_check` and reports it as a new `integrity` check, so a cache with a damaged page is named instead of passing every command until something touches the broken page. `doctor` also survives reading a corrupt cache rather than crashing part-way through its own checks, and `healthcheck`'s description now says it proves the file opens, not that its contents are intact. (#78)
|
|
25
|
+
|
|
26
|
+
## [0.6.0] - 2026-09-06
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- A global flag between a command and its subcommand made the CLI reject the flag's *value* as a command: `db --format json today` failed with `Unknown command "json"`, and `db --db <path> today` echoed the whole path back. Global flags are now lifted to the end of the command line wherever they appear, so the three spellings of `--format json db today` all work. Tokens after `--` are left alone. (#79)
|
|
30
|
+
- `--no-color` left citty's help output coloured, because citty reads `NO_COLOR` when its module is evaluated and the variable was only set afterwards. The rule moved into `src/lib/color-mode.ts` and is applied by `src/lib/apply-color-mode.ts`, which the entry point imports first. Piped help is plain too unless `FORCE_COLOR` asks otherwise, `FORCE_COLOR=0` included, which chalk reads as off. (#80)
|
|
31
|
+
- `describe`, `manifest`, `healthcheck` and `login` accepted an unknown `--format` (`xml`) and exited 0, while every other command exited 1 with `BAD_ARGS`. The value is validated in `assertKnownArgs`, which every command calls, instead of only in the resolver the JSON-only commands never reach. (#81)
|
|
32
|
+
- `db week` lost its column alignment whenever colour was on: `padStart` counted the ANSI escapes chalk had already wrapped each score in, so it padded by nothing. Piped output was correct, which is why the tests never saw it. Padding now measures visible width (`src/lib/pad.ts`), and `report` uses the same helper. (#82)
|
|
33
|
+
- An empty `--db` (or `OURA_DB_PATH`) was falsy, so it fell through to the default and every command silently opened — and migrated — the home cache at `~/.oura-cli/oura.db`. A wrapper expanding an unset variable into `--db "$VAR"` therefore wrote to the user's real database. The same went for `--db=` and a trailing `--db` with no value. All of them now fail with `BAD_ARGS`, like the blank-but-not-empty value already did, and an `OURA_DB_PATH` set to an empty string is an error rather than a request for the default. `doctor` reports it as an argument error too, instead of a `database: fail` health finding with a database exit code. (#76)
|
|
34
|
+
- `sync` fetched nothing, and said nothing, for any collection whose last stored day was after the end of the window. A timezone west of the one the cache was built in (`--tz`, `OURA_TZ`, a laptop that travelled) did this to every daily collection for up to a day; a row dated in the future — a tag on next month, which Oura allows — did it to that collection on every run from then on. A collection now resumes from its newest stored day *at or before* the end of the window, so a future-dated row is ignored rather than freezing the collection, and the days behind it are still fetched. An explicit `--from` after the end of the window is rejected with `BAD_ARGS`. (#69)
|
|
35
|
+
- `sync` never fetched the heart-rate samples Oura adds to days already behind the watermark: on a live account 8,004 `source='workout'` samples over nine days were missing from an incrementally synced cache and present in a fresh one, with nothing in the output to say so. A collection can now declare `syncLookbackDays`, and `hr` re-walks the last 14 days on every sync. The window `sync` reports still describes what the cache needed, so a lookback does not make every run announce a fortnight. Samples Oura later reclassifies (`awake` → `workout`) are still kept, since `heartrate` never deletes. (#68)
|
|
36
|
+
- `report` decided whether a day's activity was complete from the newest heart-rate sample, but Oura publishes heart-rate days after the daily summaries: right after a ring sync the report called three fully synced days "still accumulating". A day now counts as complete once a later day has its own activity record; heart-rate plays no part. `lastUpload` stays in the JSON as information and the note no longer quotes it. (#57)
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Eight collections from the Oura OpenAPI spec (1.37), each available through `fetch <name>`, stored by `sync`, listed in `describe`/`manifest`, counted by `db stats`, with a JSON Schema under `docs/schemas/`: `resilience` (daily_resilience), `vo2max` (vO2_max — its table has existed since 0.1 and is finally populated), `sleep-time` (sleep_time), `session` (session), `rest-mode` (rest_mode_period), `tags` (enhanced_tag; the legacy `tag` endpoint is not offered), `ring` (ring_configuration) and `battery` (ring_battery_level, a timestamp-keyed timeseries fetched in pieces of at most 30 days like `hr`). Schema migration 3 creates the seven new tables. (#44)
|
|
40
|
+
- Collection registry: `rangeParams: 'none'` for snapshot endpoints that take no range. `fetch` rejects `--day`/`--from`/`--to`/`--days` for them with `BAD_ARGS` instead of ignoring the flags, and `sync` replaces the table with each response (a ring removed from the account disappears locally too) instead of keeping a watermark — also under `--from/--to`, since a snapshot has no history to backfill.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- The `sync` text summary lists every registry collection by its `fetch` name (`sleep 28 (+2) readiness 0 (+0)`), in a fixed-width grid whose column count follows the terminal width (up to four, and a single unpadded cell per line once two no longer fit — around 46 columns, sooner when a count is wide), instead of a hand-written list of nine tables. Cells no longer break across lines on a narrow terminal, and the counts line up. (#83)
|
|
44
|
+
- `sync` prints a progress line for every collection, including those that returned nothing, and names both the collection and its table (` + tags (enhanced_tags): 0 fetched, 0 new`) so the lines and the summary can be read together. A silent collection used to be indistinguishable from a failed one while the summary listed it anyway. (#83)
|
|
45
|
+
|
|
9
46
|
## [0.5.2] - 2026-09-06
|
|
10
47
|
|
|
11
48
|
### Fixed
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ oura-cli sync # first sync fetches the last 30 days; later syncs resume from
|
|
|
54
54
|
oura-cli report # weekly digest in the terminal
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Subsequent `oura-cli sync` re-fetches each collection from its own last stored day (Oura revises recent days, so the overlap is deliberate) and reports rows fetched (+new). `oura-cli sync --from 2026-08-01 [--to 2026-08-07]` re-fetches an explicit window for every collection instead — for example after an interrupted sync. `oura-cli db today` / `oura-cli db week` read the local cache instantly, no API call.
|
|
57
|
+
Subsequent `oura-cli sync` re-fetches each collection from its own last stored day (Oura revises recent days, so the overlap is deliberate) and reports rows fetched (+new). Heart rate goes back two weeks each time, because Oura publishes workout samples days after the day they belong to. A re-fetched window ends up holding exactly what the API returned for it: a sample Oura reclassified, or a record it re-issued under a new id, replaces the row it supersedes instead of joining it. When a response drops most of what one request covered — the shape of a partial or short answer — those rows are kept rather than deleted, and `sync` names the collection and how many. `oura-cli sync --prune=hr` then applies them for that collection once you have judged the correction genuine; `oura-cli sync --prune=all` does it for every collection in the run, which also lifts the guard on any collection that answers short in the same run. `oura-cli sync --from 2026-08-01 [--to 2026-08-07]` re-fetches an explicit window for every collection instead — for example after an interrupted sync. `oura-cli db today` / `oura-cli db week` read the local cache instantly, no API call.
|
|
58
58
|
|
|
59
59
|
### If something looks wrong
|
|
60
60
|
|
|
@@ -116,7 +116,9 @@ oura-cli fetch workout --from 2026-05-01 --to 2026-05-31
|
|
|
116
116
|
oura-cli fetch sleep-periods --day 2026-06-01 | jq '.[] | {day, type, average_hrv}'
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Collections: `sleep readiness activity hr spo2 stress workout sleep-periods cv-age`.
|
|
119
|
+
Collections: `sleep readiness activity hr spo2 stress workout sleep-periods cv-age resilience vo2max sleep-time session rest-mode tags ring battery`.
|
|
120
|
+
|
|
121
|
+
`ring` is a snapshot of your ring's hardware, not a day range, so it takes no `--day`/`--from`/`--days`. `battery` is a timeseries like `hr`: keyed by timestamp, fetched in pieces of at most 30 days.
|
|
120
122
|
|
|
121
123
|
### Piping to other tools
|
|
122
124
|
|
|
@@ -136,6 +138,9 @@ oura-cli db trends 90 > trends.json
|
|
|
136
138
|
| Database path | `--db` | `OURA_DB_PATH` | `~/.oura-cli/oura.db` |
|
|
137
139
|
| Timezone | `--tz` | `OURA_TZ` | system timezone, else `UTC` |
|
|
138
140
|
| Output format | `--format` | | auto-detect (TTY → table) |
|
|
141
|
+
| Colour | `--no-color`| `NO_COLOR` | on for a terminal, off when piped |
|
|
142
|
+
|
|
143
|
+
These are global: they may appear anywhere on the command line, before the command, between a command and its subcommand, or at the end. `oura-cli --format json db today`, `oura-cli db --format json today` and `oura-cli db today --format json` are the same command.
|
|
139
144
|
|
|
140
145
|
## Security
|
|
141
146
|
|
|
@@ -162,6 +167,14 @@ This tool reads your personal health data — handle the token with care.
|
|
|
162
167
|
| Workouts | Oura V2 `workout` | `workouts` |
|
|
163
168
|
| Sleep model | Oura V2 `sleep` | `sleep_model` |
|
|
164
169
|
| Cardiovascular age | Oura V2 `cardiovascular_age` | `cardiovascular_age` |
|
|
170
|
+
| Resilience | Oura V2 `daily_resilience` | `daily_resilience` |
|
|
171
|
+
| VO₂ max | Oura V2 `vO2_max` | `vo2max` |
|
|
172
|
+
| Sleep time | Oura V2 `sleep_time` | `sleep_time` |
|
|
173
|
+
| Sessions | Oura V2 `session` | `sessions` |
|
|
174
|
+
| Rest mode | Oura V2 `rest_mode_period` | `rest_mode_periods` |
|
|
175
|
+
| Tags | Oura V2 `enhanced_tag` | `enhanced_tags` |
|
|
176
|
+
| Ring | Oura V2 `ring_configuration` | `ring_configuration` |
|
|
177
|
+
| Battery | Oura V2 `ring_battery_level` | `ring_battery_level` |
|
|
165
178
|
|
|
166
179
|
Runtime: [Bun](https://bun.sh). Storage: built-in `bun:sqlite`. CLI parsing: [citty](https://github.com/unjs/citty). Output styling: [chalk](https://github.com/chalk/chalk). One ~110 kB `dist/index.js`, no native deps.
|
|
167
180
|
|
|
@@ -170,12 +183,12 @@ Runtime: [Bun](https://bun.sh). Storage: built-in `bun:sqlite`. CLI parsing: [ci
|
|
|
170
183
|
If you're driving the CLI from a script or LLM harness:
|
|
171
184
|
|
|
172
185
|
- `oura-cli describe` — JSON manifest of every command, argument, and output schema. Agents discover capabilities without scraping `--help`.
|
|
173
|
-
- `oura-cli healthcheck` — `{ok, version, latencyMs}` JSON for liveness probes, plus `error` when `ok` is false.
|
|
186
|
+
- `oura-cli healthcheck` — `{ok, version, latencyMs}` JSON for liveness probes, plus `error` when `ok` is false. It proves the database opens and answers a query; it does not inspect the contents. `oura-cli doctor` runs SQLite's `quick_check` for that.
|
|
174
187
|
- 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`.
|
|
175
188
|
- Errors emit a stable JSON envelope on stderr: `{"error":{"code":"…","message":"…","hint":"…"}}`.
|
|
176
189
|
- Documented exit codes: `0` success, `1` user error, `2` auth, `3` API, `4` storage.
|
|
177
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.
|
|
178
|
-
- Two contract quirks, kept for compatibility: `report --period month` returns its window as `weekStart`/`weekEnd`, and `heartrate.day` in the cache is the date written in Oura's timestamp (UTC in practice) while every `--day`/`--tz` argument is local.
|
|
191
|
+
- 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.
|
|
179
192
|
|
|
180
193
|
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.
|
|
181
194
|
|