@drakulavich/oura-cli 0.1.2 → 0.2.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 +46 -0
- package/README.md +16 -0
- package/dist/index.js +84 -27
- package/docs/schemas/describe.json +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,50 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-05-13
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **BREAKING (typescript only):** `ErrorCode` is now a closed union of the
|
|
13
|
+
documented codes (`BAD_ARGS`, `TOKEN_MISSING`, `TOKEN_INVALID`, `API_ERROR`,
|
|
14
|
+
`DB_ERROR`, `UNKNOWN`). External code constructing `CliError` with a custom
|
|
15
|
+
string code will fail to compile. Runtime behaviour for already-built code
|
|
16
|
+
is unchanged. (#1, item 5)
|
|
17
|
+
- `exitCodeFor` is now an exhaustive `switch` over the closed union; future
|
|
18
|
+
additions to `ErrorCode` require a corresponding branch.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `describe` manifest now includes `compatManifestCommand: "oura-cli manifest"`
|
|
22
|
+
so agents can discover the OpenClaw-compatible second manifest. (#1, item 6)
|
|
23
|
+
- README documents the two-manifest split under "Manifest formats".
|
|
24
|
+
- `getGlobalOpts(command)` helper in `src/commands/helpers.ts` walks the
|
|
25
|
+
commander parent chain to the root program; api-command.ts and others now
|
|
26
|
+
use it instead of `command.parent!.parent!.opts()`. (#1, item 7)
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
- API error messages now redact `Bearer <token>` and `"token":"<value>"`
|
|
30
|
+
patterns and truncate bodies past 200 chars before printing. (#1, item 18)
|
|
31
|
+
|
|
32
|
+
## [0.1.3] - 2026-05-13
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- `--token <pat>` global flag now uses the value as an inline token instead of
|
|
36
|
+
trying to read it as a file path (#1, item 1).
|
|
37
|
+
- `--no-color` is now wired through to chalk and honors the `NO_COLOR` env
|
|
38
|
+
variable; it was previously advertised in the `describe` manifest but never
|
|
39
|
+
took effect (#1, item 2).
|
|
40
|
+
- `todayDate()` and `dateRange()` now use the configured/system timezone via
|
|
41
|
+
`todayLocal(resolveDefaultTimezone())` instead of UTC-only `.toISOString()`,
|
|
42
|
+
so users in non-UTC timezones get the correct "today" near midnight (#1,
|
|
43
|
+
item 3).
|
|
44
|
+
- Single-day `start == end` API calls verified safe: `oura-cli sleep today`
|
|
45
|
+
(which calls `client.fetch(endpoint, today, today)`) returns data correctly.
|
|
46
|
+
No change needed to `api-command.ts` (#1, item 4).
|
|
47
|
+
|
|
48
|
+
### Note
|
|
49
|
+
- Several smells covered in the audit issue (#1) remain open for v0.2+:
|
|
50
|
+
closed `ErrorCode` union, manifest documentation, commander chain helper,
|
|
51
|
+
schema-validated test fixtures.
|
|
52
|
+
|
|
9
53
|
## [0.1.2] - 2026-05-13
|
|
10
54
|
|
|
11
55
|
### Fixed
|
|
@@ -42,6 +86,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
42
86
|
- Local SQLite cache at `~/.oura-cli/oura.db`.
|
|
43
87
|
- Auth via `oura-cli login`, `OURA_TOKEN`, `OURA_TOKEN_PATH`, or `~/.oura-token`.
|
|
44
88
|
|
|
89
|
+
[0.2.0]: https://github.com/drakulavich/oura-cli/releases/tag/v0.2.0
|
|
90
|
+
[0.1.3]: https://github.com/drakulavich/oura-cli/releases/tag/v0.1.3
|
|
45
91
|
[0.1.2]: https://github.com/drakulavich/oura-cli/releases/tag/v0.1.2
|
|
46
92
|
[0.1.1]: https://github.com/drakulavich/oura-cli/releases/tag/v0.1.1
|
|
47
93
|
[0.1.0]: https://github.com/drakulavich/oura-cli/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -90,6 +90,22 @@ oura-cli healthcheck # JSON: {ok, version, latencyMs}
|
|
|
90
90
|
| 3 | API or network error |
|
|
91
91
|
| 4 | database or local storage error |
|
|
92
92
|
|
|
93
|
+
## Manifest formats
|
|
94
|
+
|
|
95
|
+
Two manifest commands, two audiences:
|
|
96
|
+
|
|
97
|
+
- **`oura-cli describe`** — neutral, agent-friendly. Lists every command, its
|
|
98
|
+
args, output schema refs, and exit-code semantics. Use this when integrating
|
|
99
|
+
with generic LLM harnesses, MCP wrappers, or your own custom scripts.
|
|
100
|
+
- **`oura-cli manifest`** — [OpenClaw](https://github.com/openclaw/openclaw)
|
|
101
|
+
`tool-registry` shape. Strictly smaller, optimised for OpenClaw's skill
|
|
102
|
+
discovery and health-aggregation flow. Use this only if you're plugging
|
|
103
|
+
oura-cli into an OpenClaw gateway.
|
|
104
|
+
|
|
105
|
+
Both return JSON. `describe` references `manifest` via the
|
|
106
|
+
`compatManifestCommand` field so an agent can discover the second format
|
|
107
|
+
without prior knowledge.
|
|
108
|
+
|
|
93
109
|
## What's Inside
|
|
94
110
|
|
|
95
111
|
| Endpoint | Source | Cached table |
|
package/dist/index.js
CHANGED
|
@@ -2142,11 +2142,6 @@ var {
|
|
|
2142
2142
|
Help
|
|
2143
2143
|
} = import__.default;
|
|
2144
2144
|
|
|
2145
|
-
// src/api/client.ts
|
|
2146
|
-
import { readFileSync } from "fs";
|
|
2147
|
-
import { resolve } from "path";
|
|
2148
|
-
import { homedir } from "os";
|
|
2149
|
-
|
|
2150
2145
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
2151
2146
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
2152
2147
|
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
@@ -2636,6 +2631,11 @@ var chalk = createChalk();
|
|
|
2636
2631
|
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
2637
2632
|
var source_default = chalk;
|
|
2638
2633
|
|
|
2634
|
+
// src/api/client.ts
|
|
2635
|
+
import { readFileSync } from "fs";
|
|
2636
|
+
import { resolve } from "path";
|
|
2637
|
+
import { homedir } from "os";
|
|
2638
|
+
|
|
2639
2639
|
// src/lib/errors.ts
|
|
2640
2640
|
class CliError extends Error {
|
|
2641
2641
|
code;
|
|
@@ -2647,17 +2647,25 @@ class CliError extends Error {
|
|
|
2647
2647
|
this.name = "CliError";
|
|
2648
2648
|
}
|
|
2649
2649
|
}
|
|
2650
|
-
var EXIT_CODE_BY_CODE = {
|
|
2651
|
-
BAD_ARGS: 1,
|
|
2652
|
-
TOKEN_MISSING: 2,
|
|
2653
|
-
TOKEN_INVALID: 2,
|
|
2654
|
-
API_ERROR: 3,
|
|
2655
|
-
DB_ERROR: 4
|
|
2656
|
-
};
|
|
2657
2650
|
function exitCodeFor(err) {
|
|
2658
|
-
if (err instanceof CliError)
|
|
2659
|
-
return
|
|
2660
|
-
|
|
2651
|
+
if (!(err instanceof CliError))
|
|
2652
|
+
return 1;
|
|
2653
|
+
switch (err.code) {
|
|
2654
|
+
case "BAD_ARGS":
|
|
2655
|
+
return 1;
|
|
2656
|
+
case "TOKEN_MISSING":
|
|
2657
|
+
case "TOKEN_INVALID":
|
|
2658
|
+
return 2;
|
|
2659
|
+
case "API_ERROR":
|
|
2660
|
+
return 3;
|
|
2661
|
+
case "DB_ERROR":
|
|
2662
|
+
return 4;
|
|
2663
|
+
case "UNKNOWN":
|
|
2664
|
+
return 1;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
function redactSecrets(s) {
|
|
2668
|
+
return s.replace(/Bearer\s+[A-Za-z0-9._\-]{8,}/g, "Bearer [REDACTED]").replace(/"token"\s*:\s*"[^"]{8,}"/g, '"token":"[REDACTED]"');
|
|
2661
2669
|
}
|
|
2662
2670
|
function formatError(err, format) {
|
|
2663
2671
|
const code = err instanceof CliError ? err.code : "UNKNOWN";
|
|
@@ -2706,7 +2714,9 @@ class OuraClient {
|
|
|
2706
2714
|
headers: { Authorization: `Bearer ${this.token}` }
|
|
2707
2715
|
});
|
|
2708
2716
|
if (!response.ok) {
|
|
2709
|
-
const
|
|
2717
|
+
const rawBody = await response.text();
|
|
2718
|
+
const redacted = redactSecrets(rawBody);
|
|
2719
|
+
const body = redacted.length > 200 ? redacted.slice(0, 200) + "\u2026 (truncated)" : redacted;
|
|
2710
2720
|
if (response.status === 401 || response.status === 403) {
|
|
2711
2721
|
throw new CliError("TOKEN_INVALID", `Oura API ${response.status}: ${body}`);
|
|
2712
2722
|
}
|
|
@@ -2717,16 +2727,59 @@ class OuraClient {
|
|
|
2717
2727
|
}
|
|
2718
2728
|
}
|
|
2719
2729
|
|
|
2730
|
+
// src/lib/time.ts
|
|
2731
|
+
function nowUtc() {
|
|
2732
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2733
|
+
}
|
|
2734
|
+
function formatLocal(utcStr, timezone) {
|
|
2735
|
+
const dt = new Date(utcStr);
|
|
2736
|
+
const parts = new Intl.DateTimeFormat("sv-SE", {
|
|
2737
|
+
timeZone: timezone,
|
|
2738
|
+
year: "numeric",
|
|
2739
|
+
month: "2-digit",
|
|
2740
|
+
day: "2-digit",
|
|
2741
|
+
hour: "2-digit",
|
|
2742
|
+
minute: "2-digit",
|
|
2743
|
+
hour12: false
|
|
2744
|
+
}).formatToParts(dt);
|
|
2745
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
2746
|
+
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
|
|
2747
|
+
}
|
|
2748
|
+
function formatLocalDate(utcStr, timezone) {
|
|
2749
|
+
return formatLocal(utcStr, timezone).split(" ")[0];
|
|
2750
|
+
}
|
|
2751
|
+
function todayLocal(timezone) {
|
|
2752
|
+
return formatLocalDate(nowUtc(), timezone);
|
|
2753
|
+
}
|
|
2754
|
+
function resolveDefaultTimezone() {
|
|
2755
|
+
if (process.env.OURA_TZ)
|
|
2756
|
+
return process.env.OURA_TZ;
|
|
2757
|
+
try {
|
|
2758
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
2759
|
+
} catch {
|
|
2760
|
+
return "UTC";
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2720
2764
|
// src/commands/helpers.ts
|
|
2765
|
+
function getGlobalOpts(command) {
|
|
2766
|
+
let node = command;
|
|
2767
|
+
while (node?.parent) {
|
|
2768
|
+
node = node.parent;
|
|
2769
|
+
}
|
|
2770
|
+
return node?.opts() ?? {};
|
|
2771
|
+
}
|
|
2721
2772
|
function getClient(opts) {
|
|
2722
|
-
return new OuraClient(opts.token ? {
|
|
2773
|
+
return new OuraClient(opts.token ? { token: opts.token } : {});
|
|
2723
2774
|
}
|
|
2724
|
-
function todayDate() {
|
|
2725
|
-
return
|
|
2775
|
+
function todayDate(timezone) {
|
|
2776
|
+
return todayLocal(timezone ?? resolveDefaultTimezone());
|
|
2726
2777
|
}
|
|
2727
|
-
function dateRange(days) {
|
|
2728
|
-
const
|
|
2729
|
-
const
|
|
2778
|
+
function dateRange(days, timezone) {
|
|
2779
|
+
const tz = timezone ?? resolveDefaultTimezone();
|
|
2780
|
+
const end = todayLocal(tz);
|
|
2781
|
+
const startMs = new Date(`${end}T00:00:00Z`).getTime() - (days - 1) * 86400000;
|
|
2782
|
+
const start = new Date(startMs).toISOString().slice(0, 10);
|
|
2730
2783
|
return { start, end };
|
|
2731
2784
|
}
|
|
2732
2785
|
|
|
@@ -2734,19 +2787,19 @@ function dateRange(days) {
|
|
|
2734
2787
|
function createApiCommand(name, description, endpoint) {
|
|
2735
2788
|
const cmd = new Command(name).description(description);
|
|
2736
2789
|
cmd.command("today").description(`Today's ${name} data`).action(async (_, command) => {
|
|
2737
|
-
const opts = command
|
|
2790
|
+
const opts = getGlobalOpts(command);
|
|
2738
2791
|
const client = getClient(opts);
|
|
2739
2792
|
const data = await client.fetch(endpoint, todayDate(), todayDate());
|
|
2740
2793
|
console.log(JSON.stringify(data, null, 2));
|
|
2741
2794
|
});
|
|
2742
2795
|
cmd.command("date <day>").description(`${name} data for specific date (YYYY-MM-DD)`).action(async (day, _, command) => {
|
|
2743
|
-
const opts = command
|
|
2796
|
+
const opts = getGlobalOpts(command);
|
|
2744
2797
|
const client = getClient(opts);
|
|
2745
2798
|
const data = await client.fetch(endpoint, day, day);
|
|
2746
2799
|
console.log(JSON.stringify(data, null, 2));
|
|
2747
2800
|
});
|
|
2748
2801
|
cmd.command("week").description(`Last 7 days of ${name} data`).action(async (_, command) => {
|
|
2749
|
-
const opts = command
|
|
2802
|
+
const opts = getGlobalOpts(command);
|
|
2750
2803
|
const client = getClient(opts);
|
|
2751
2804
|
const { start, end } = dateRange(7);
|
|
2752
2805
|
const data = await client.fetch(endpoint, start, end);
|
|
@@ -3687,6 +3740,7 @@ function buildManifest(version) {
|
|
|
3687
3740
|
return {
|
|
3688
3741
|
name: "oura-cli",
|
|
3689
3742
|
version,
|
|
3743
|
+
compatManifestCommand: "oura-cli manifest",
|
|
3690
3744
|
auth: {
|
|
3691
3745
|
envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"],
|
|
3692
3746
|
tokenFile: "~/.oura-token",
|
|
@@ -3822,9 +3876,12 @@ function describeCommand(version) {
|
|
|
3822
3876
|
}
|
|
3823
3877
|
|
|
3824
3878
|
// src/index.ts
|
|
3825
|
-
var VERSION = "0.
|
|
3879
|
+
var VERSION = "0.2.0";
|
|
3880
|
+
if (process.argv.includes("--no-color") || process.env.NO_COLOR) {
|
|
3881
|
+
source_default.level = 0;
|
|
3882
|
+
}
|
|
3826
3883
|
var program2 = new Command;
|
|
3827
|
-
program2.name("oura-cli").description("Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.").version(VERSION).option("--format <format>", "Output format: table | json (default auto-detect by TTY)").option("--token <pat>", "Inline access token (prefer env vars or `oura-cli login`)").option("--db <path>", "Path to SQLite database file (env: OURA_DB_PATH)").option("--tz <timezone>", "Display timezone (env: OURA_TZ; default auto-detect)");
|
|
3884
|
+
program2.name("oura-cli").description("Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.").version(VERSION).option("--format <format>", "Output format: table | json (default auto-detect by TTY)").option("--token <pat>", "Inline access token (prefer env vars or `oura-cli login`)").option("--db <path>", "Path to SQLite database file (env: OURA_DB_PATH)").option("--tz <timezone>", "Display timezone (env: OURA_TZ; default auto-detect)").option("--no-color", "Disable ANSI colors in human output (also honors NO_COLOR env)");
|
|
3828
3885
|
program2.addCommand(loginCommand());
|
|
3829
3886
|
program2.addCommand(describeCommand(VERSION));
|
|
3830
3887
|
program2.addCommand(createApiCommand("sleep", "Fetch daily sleep scores from Oura API.", "daily_sleep"));
|
package/package.json
CHANGED