@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 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 EXIT_CODE_BY_CODE[err.code] ?? 1;
2660
- return 1;
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 body = await response.text();
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 ? { tokenPath: opts.token } : {});
2773
+ return new OuraClient(opts.token ? { token: opts.token } : {});
2723
2774
  }
2724
- function todayDate() {
2725
- return new Date().toISOString().slice(0, 10);
2775
+ function todayDate(timezone) {
2776
+ return todayLocal(timezone ?? resolveDefaultTimezone());
2726
2777
  }
2727
- function dateRange(days) {
2728
- const end = todayDate();
2729
- const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
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.parent.parent.opts();
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.parent.parent.opts();
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.parent.parent.opts();
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.1.2";
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"));
@@ -7,6 +7,7 @@
7
7
  "properties": {
8
8
  "name": { "const": "oura-cli" },
9
9
  "version": { "type": "string" },
10
+ "compatManifestCommand": { "type": "string" },
10
11
  "auth": {
11
12
  "type": "object",
12
13
  "required": ["envVars", "tokenFile", "loginCommand"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/oura-cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Oura Ring CLI — query and analyze Oura Ring health data from the command line, designed for humans and AI agents.",
5
5
  "keywords": [
6
6
  "oura",