@drakulavich/oura-cli 0.4.5 → 0.5.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/dist/index.js CHANGED
@@ -1,20 +1,49 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var __defProp = Object.defineProperty;
4
- var __returnValue = (v) => v;
5
- function __exportSetter(name, newValue) {
6
- this[name] = __returnValue.bind(null, newValue);
7
- }
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, {
11
- get: all[name],
12
- enumerable: true,
13
- configurable: true,
14
- set: __exportSetter.bind(all, name)
15
- });
3
+ var __esm = (fn, res, err) => () => {
4
+ if (fn)
5
+ try {
6
+ res = fn(fn = 0);
7
+ } catch (e) {
8
+ err = [e];
9
+ }
10
+ if (err)
11
+ throw err[0];
12
+ return res;
16
13
  };
17
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+
15
+ // node_modules/chalk/source/utilities.js
16
+ function stringReplaceAll(string, substring, postfix) {
17
+ let index = string.indexOf(substring);
18
+ if (index === -1) {
19
+ return string;
20
+ }
21
+ const substringLength = substring.length;
22
+ let endIndex = 0;
23
+ let returnValue = "";
24
+ do {
25
+ returnValue += string.slice(endIndex, index) + substring + postfix;
26
+ endIndex = index + substringLength;
27
+ index = string.indexOf(substring, endIndex);
28
+ } while (index !== -1);
29
+ returnValue += string.slice(endIndex);
30
+ return returnValue;
31
+ }
32
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
33
+ let endIndex = 0;
34
+ let returnValue = "";
35
+ do {
36
+ const isGotCR = string[index - 1] === "\r";
37
+ returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? `\r
38
+ ` : `
39
+ `) + postfix;
40
+ endIndex = index + 1;
41
+ index = string.indexOf(`
42
+ `, endIndex);
43
+ } while (index !== -1);
44
+ returnValue += string.slice(endIndex);
45
+ return returnValue;
46
+ }
18
47
 
19
48
  // node_modules/chalk/source/vendor/ansi-styles/index.js
20
49
  function assembleStyles() {
@@ -26,7 +55,7 @@ function assembleStyles() {
26
55
  close: `\x1B[${style[1]}m`
27
56
  };
28
57
  group[styleName] = styles[styleName];
29
- codes.set(style[0], style[1]);
58
+ codes.set(Number.parseInt(style[0], 10), style[1]);
30
59
  }
31
60
  Object.defineProperty(styles, groupName, {
32
61
  value: group,
@@ -39,12 +68,16 @@ function assembleStyles() {
39
68
  });
40
69
  styles.color.close = "\x1B[39m";
41
70
  styles.bgColor.close = "\x1B[49m";
71
+ styles.underlineColor.close = "\x1B[59m";
42
72
  styles.color.ansi = wrapAnsi16();
43
73
  styles.color.ansi256 = wrapAnsi256();
44
74
  styles.color.ansi16m = wrapAnsi16m();
45
75
  styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
46
76
  styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
47
77
  styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
78
+ styles.underlineColor.ansi = wrapUnderlineAnsi;
79
+ styles.underlineColor.ansi256 = wrapAnsi256(ANSI_UNDERLINE_OFFSET);
80
+ styles.underlineColor.ansi16m = wrapAnsi16m(ANSI_UNDERLINE_OFFSET);
48
81
  Object.defineProperties(styles, {
49
82
  rgbToAnsi256: {
50
83
  value(red, green, blue) {
@@ -63,7 +96,7 @@ function assembleStyles() {
63
96
  },
64
97
  hexToRgb: {
65
98
  value(hex) {
66
- const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
99
+ const matches = /[\da-f]{6}|[\da-f]{3}/i.exec(hex.toString(16));
67
100
  if (!matches) {
68
101
  return [0, 0, 0];
69
102
  }
@@ -129,7 +162,7 @@ function assembleStyles() {
129
162
  });
130
163
  return styles;
131
164
  }
132
- var ANSI_BACKGROUND_OFFSET = 10, wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`, wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`, wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`, styles, modifierNames, foregroundColorNames, backgroundColorNames, colorNames, ansiStyles, ansi_styles_default;
165
+ var ANSI_BACKGROUND_OFFSET = 10, ANSI_UNDERLINE_OFFSET = 20, wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`, wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`, wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`, wrapUnderlineAnsi = (code) => `\x1B[58;5;${code < 90 ? code - 30 : code - 90 + 8}m`, styles, modifierNames, foregroundColorNames, backgroundColorNames, underlineColorNames, colorNames, ansiStyles, ansi_styles_default;
133
166
  var init_ansi_styles = __esm(() => {
134
167
  styles = {
135
168
  modifier: {
@@ -138,6 +171,10 @@ var init_ansi_styles = __esm(() => {
138
171
  dim: [2, 22],
139
172
  italic: [3, 23],
140
173
  underline: [4, 24],
174
+ underlineDouble: ["4:2", 24],
175
+ underlineCurly: ["4:3", 24],
176
+ underlineDotted: ["4:4", 24],
177
+ underlineDashed: ["4:5", 24],
141
178
  overline: [53, 55],
142
179
  inverse: [7, 27],
143
180
  hidden: [8, 28],
@@ -182,11 +219,32 @@ var init_ansi_styles = __esm(() => {
182
219
  bgMagentaBright: [105, 49],
183
220
  bgCyanBright: [106, 49],
184
221
  bgWhiteBright: [107, 49]
222
+ },
223
+ underlineColor: {
224
+ underlineBlack: ["58;5;0", 59],
225
+ underlineRed: ["58;5;1", 59],
226
+ underlineGreen: ["58;5;2", 59],
227
+ underlineYellow: ["58;5;3", 59],
228
+ underlineBlue: ["58;5;4", 59],
229
+ underlineMagenta: ["58;5;5", 59],
230
+ underlineCyan: ["58;5;6", 59],
231
+ underlineWhite: ["58;5;7", 59],
232
+ underlineBlackBright: ["58;5;8", 59],
233
+ underlineGray: ["58;5;8", 59],
234
+ underlineGrey: ["58;5;8", 59],
235
+ underlineRedBright: ["58;5;9", 59],
236
+ underlineGreenBright: ["58;5;10", 59],
237
+ underlineYellowBright: ["58;5;11", 59],
238
+ underlineBlueBright: ["58;5;12", 59],
239
+ underlineMagentaBright: ["58;5;13", 59],
240
+ underlineCyanBright: ["58;5;14", 59],
241
+ underlineWhiteBright: ["58;5;15", 59]
185
242
  }
186
243
  };
187
244
  modifierNames = Object.keys(styles.modifier);
188
245
  foregroundColorNames = Object.keys(styles.color);
189
246
  backgroundColorNames = Object.keys(styles.bgColor);
247
+ underlineColorNames = Object.keys(styles.underlineColor);
190
248
  colorNames = [...foregroundColorNames, ...backgroundColorNames];
191
249
  ansiStyles = assembleStyles();
192
250
  ansi_styles_default = ansiStyles;
@@ -202,16 +260,23 @@ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.
202
260
  const terminatorPosition = argv.indexOf("--");
203
261
  return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
204
262
  }
263
+ function hasNumericForceColor() {
264
+ return /^\d+$/.test(env.FORCE_COLOR);
265
+ }
205
266
  function envForceColor() {
206
- if ("FORCE_COLOR" in env) {
207
- if (env.FORCE_COLOR === "true") {
208
- return 1;
209
- }
210
- if (env.FORCE_COLOR === "false") {
211
- return 0;
212
- }
213
- return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
267
+ if (!("FORCE_COLOR" in env)) {
268
+ return;
269
+ }
270
+ if (env.FORCE_COLOR === "false") {
271
+ return 0;
272
+ }
273
+ if (env.FORCE_COLOR === "true" || env.FORCE_COLOR.length === 0) {
274
+ return 1;
275
+ }
276
+ if (!hasNumericForceColor()) {
277
+ return;
214
278
  }
279
+ return Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
215
280
  }
216
281
  function translateLevel(level) {
217
282
  if (level === 0) {
@@ -241,6 +306,9 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
241
306
  return 2;
242
307
  }
243
308
  }
309
+ if (forceColor !== undefined && hasNumericForceColor()) {
310
+ return forceColor;
311
+ }
244
312
  if ("TF_BUILD" in env && "AGENT_NAME" in env) {
245
313
  return 1;
246
314
  }
@@ -268,7 +336,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
268
336
  return min;
269
337
  }
270
338
  if ("TEAMCITY_VERSION" in env) {
271
- return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
339
+ return /^(?:9\.0*[1-9]\d*\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
272
340
  }
273
341
  if (env.COLORTERM === "truecolor") {
274
342
  return 3;
@@ -283,7 +351,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
283
351
  return 3;
284
352
  }
285
353
  if ("TERM_PROGRAM" in env) {
286
- const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
354
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".", 1)[0], 10);
287
355
  switch (env.TERM_PROGRAM) {
288
356
  case "iTerm.app": {
289
357
  return version >= 3 ? 3 : 2;
@@ -293,7 +361,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
293
361
  }
294
362
  }
295
363
  }
296
- if (/-256(color)?$/i.test(env.TERM)) {
364
+ if (/-256(?:color)?$/i.test(env.TERM)) {
297
365
  return 2;
298
366
  }
299
367
  if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
@@ -326,90 +394,39 @@ var init_supports_color = __esm(() => {
326
394
  supports_color_default = supportsColor;
327
395
  });
328
396
 
329
- // node_modules/chalk/source/utilities.js
330
- function stringReplaceAll(string, substring, replacer) {
331
- let index = string.indexOf(substring);
332
- if (index === -1) {
333
- return string;
334
- }
335
- const substringLength = substring.length;
336
- let endIndex = 0;
337
- let returnValue = "";
338
- do {
339
- returnValue += string.slice(endIndex, index) + substring + replacer;
340
- endIndex = index + substringLength;
341
- index = string.indexOf(substring, endIndex);
342
- } while (index !== -1);
343
- returnValue += string.slice(endIndex);
344
- return returnValue;
345
- }
346
- function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
347
- let endIndex = 0;
348
- let returnValue = "";
349
- do {
350
- const gotCR = string[index - 1] === "\r";
351
- returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? `\r
352
- ` : `
353
- `) + postfix;
354
- endIndex = index + 1;
355
- index = string.indexOf(`
356
- `, endIndex);
357
- } while (index !== -1);
358
- returnValue += string.slice(endIndex);
359
- return returnValue;
360
- }
361
-
362
397
  // node_modules/chalk/source/index.js
363
- var exports_source = {};
364
- __export(exports_source, {
365
- supportsColorStderr: () => stderrColor,
366
- supportsColor: () => stdoutColor,
367
- modifiers: () => modifierNames,
368
- modifierNames: () => modifierNames,
369
- foregroundColors: () => foregroundColorNames,
370
- foregroundColorNames: () => foregroundColorNames,
371
- default: () => source_default,
372
- colors: () => colorNames,
373
- colorNames: () => colorNames,
374
- chalkStderr: () => chalkStderr,
375
- backgroundColors: () => backgroundColorNames,
376
- backgroundColorNames: () => backgroundColorNames,
377
- Chalk: () => Chalk
378
- });
379
-
380
- class Chalk {
381
- constructor(options) {
382
- return chalkFactory(options);
383
- }
384
- }
385
398
  function createChalk(options) {
386
399
  return chalkFactory(options);
387
400
  }
388
- var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, levelMapping, styles2, applyOptions = (object, options = {}) => {
389
- if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
390
- throw new Error("The `level` option should be an integer from 0 to 3");
401
+ var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, LEVEL, styles2, assertValidLevel = (level) => {
402
+ if (!Number.isSafeInteger(level) || level < 0 || level > 3) {
403
+ throw new Error("The `level` should be an integer from 0 to 3");
404
+ }
405
+ }, levelDescriptor, applyOptions = (object, options = {}) => {
406
+ if (options.level !== undefined) {
407
+ assertValidLevel(options.level);
391
408
  }
392
409
  const colorLevel = stdoutColor ? stdoutColor.level : 0;
393
- object.level = options.level === undefined ? colorLevel : options.level;
410
+ object[LEVEL] = options.level === undefined ? colorLevel : options.level;
394
411
  }, chalkFactory = (options) => {
395
412
  const chalk = (...strings) => strings.join(" ");
396
413
  applyOptions(chalk, options);
397
414
  Object.setPrototypeOf(chalk, createChalk.prototype);
398
415
  return chalk;
399
- }, getModelAnsi = (model, level, type, ...arguments_) => {
416
+ }, createModelConverters = (model, type) => {
417
+ const style = ansi_styles_default[type];
400
418
  if (model === "rgb") {
401
- if (level === "ansi16m") {
402
- return ansi_styles_default[type].ansi16m(...arguments_);
403
- }
404
- if (level === "ansi256") {
405
- return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
406
- }
407
- return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
419
+ const ansi = (red, green, blue) => style.ansi(ansi_styles_default.rgbToAnsi(red, green, blue));
420
+ const ansi256 = (red, green, blue) => style.ansi256(ansi_styles_default.rgbToAnsi256(red, green, blue));
421
+ return [ansi, ansi, ansi256, style.ansi16m];
408
422
  }
409
423
  if (model === "hex") {
410
- return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
424
+ const ansi = (hex) => style.ansi(ansi_styles_default.hexToAnsi(hex));
425
+ const ansi256 = (hex) => style.ansi256(ansi_styles_default.hexToAnsi256(hex));
426
+ return [ansi, ansi, ansi256, (hex) => style.ansi16m(...ansi_styles_default.hexToRgb(hex))];
411
427
  }
412
- return ansi_styles_default[type][model](...arguments_);
428
+ const ansi = (code) => style.ansi(ansi_styles_default.ansi256ToAnsi(code));
429
+ return [ansi, ansi, style.ansi256, style.ansi256];
413
430
  }, usedModels, proto, createStyler = (open, close, parent) => {
414
431
  let openAll;
415
432
  let closeAll;
@@ -428,14 +445,22 @@ var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, levelMapping, styles2
428
445
  parent
429
446
  };
430
447
  }, createBuilder = (self, _styler, _isEmpty) => {
431
- const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
448
+ const builder = (...arguments_) => {
449
+ if (arguments_.length === 1) {
450
+ return applyStyle(builder, "" + arguments_[0]);
451
+ }
452
+ if (arguments_.length === 2) {
453
+ return applyStyle(builder, arguments_[0] + " " + arguments_[1]);
454
+ }
455
+ return applyStyle(builder, arguments_.join(" "));
456
+ };
432
457
  Object.setPrototypeOf(builder, proto);
433
- builder[GENERATOR] = self;
458
+ builder[GENERATOR] = self[GENERATOR] ?? self;
434
459
  builder[STYLER] = _styler;
435
460
  builder[IS_EMPTY] = _isEmpty;
436
461
  return builder;
437
462
  }, applyStyle = (self, string) => {
438
- if (self.level <= 0 || !string) {
463
+ if (self[GENERATOR][LEVEL] <= 0 || !string) {
439
464
  return self[IS_EMPTY] ? "" : string;
440
465
  }
441
466
  let styler = self[STYLER];
@@ -459,18 +484,22 @@ var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, levelMapping, styles2
459
484
  var init_source = __esm(() => {
460
485
  init_ansi_styles();
461
486
  init_supports_color();
462
- init_ansi_styles();
463
487
  ({ stdout: stdoutColor, stderr: stderrColor } = supports_color_default);
464
488
  GENERATOR = Symbol("GENERATOR");
465
489
  STYLER = Symbol("STYLER");
466
490
  IS_EMPTY = Symbol("IS_EMPTY");
467
- levelMapping = [
468
- "ansi",
469
- "ansi",
470
- "ansi256",
471
- "ansi16m"
472
- ];
491
+ LEVEL = Symbol("LEVEL");
473
492
  styles2 = Object.create(null);
493
+ levelDescriptor = {
494
+ enumerable: true,
495
+ get() {
496
+ return this[LEVEL];
497
+ },
498
+ set(level) {
499
+ assertValidLevel(level);
500
+ this[LEVEL] = level;
501
+ }
502
+ };
474
503
  Object.setPrototypeOf(createChalk.prototype, Function.prototype);
475
504
  for (const [styleName, style] of Object.entries(ansi_styles_default)) {
476
505
  styles2[styleName] = {
@@ -490,25 +519,25 @@ var init_source = __esm(() => {
490
519
  };
491
520
  usedModels = ["rgb", "hex", "ansi256"];
492
521
  for (const model of usedModels) {
493
- styles2[model] = {
494
- get() {
495
- const { level } = this;
496
- return function(...arguments_) {
497
- const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
498
- return createBuilder(this, styler, this[IS_EMPTY]);
499
- };
500
- }
501
- };
502
- const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
503
- styles2[bgModel] = {
504
- get() {
505
- const { level } = this;
506
- return function(...arguments_) {
507
- const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
508
- return createBuilder(this, styler, this[IS_EMPTY]);
509
- };
510
- }
511
- };
522
+ const capitalizedModel = model[0].toUpperCase() + model.slice(1);
523
+ for (const [styleName, type] of [
524
+ [model, "color"],
525
+ ["bg" + capitalizedModel, "bgColor"],
526
+ ["underline" + capitalizedModel, "underlineColor"]
527
+ ]) {
528
+ const { close } = ansi_styles_default[type];
529
+ const converters = createModelConverters(model, type);
530
+ styles2[styleName] = {
531
+ get() {
532
+ const styleFunction = function(first, second, third) {
533
+ const open = converters[this.level](first, second, third);
534
+ return createBuilder(this, createStyler(open, close, this[STYLER]), this[IS_EMPTY]);
535
+ };
536
+ Object.defineProperty(this, styleName, { value: styleFunction });
537
+ return styleFunction;
538
+ }
539
+ };
540
+ }
512
541
  }
513
542
  proto = Object.defineProperties(() => {}, {
514
543
  ...styles2,
@@ -522,7 +551,7 @@ var init_source = __esm(() => {
522
551
  }
523
552
  }
524
553
  });
525
- Object.defineProperties(createChalk.prototype, styles2);
554
+ Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
526
555
  chalk = createChalk();
527
556
  chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
528
557
  source_default = chalk;
@@ -530,6 +559,7 @@ var init_source = __esm(() => {
530
559
 
531
560
  // src/index.ts
532
561
  init_source();
562
+ import { readFileSync as readFileSync2 } from "fs";
533
563
 
534
564
  // node_modules/citty/dist/_chunks/libs/scule.mjs
535
565
  var NUMBER_CHAR_RE = /\d/;
@@ -743,8 +773,8 @@ function parseRawArgs(args = [], opts = {}) {
743
773
  return out;
744
774
  }
745
775
  var noColor = /* @__PURE__ */ (() => {
746
- const env2 = globalThis.process?.env ?? {};
747
- return env2.NO_COLOR === "1" || env2.TERM === "dumb" || env2.TEST || env2.CI;
776
+ const env = globalThis.process?.env ?? {};
777
+ return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
748
778
  })();
749
779
  var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
750
780
  var bold = /* @__PURE__ */ _c(1, 22);
@@ -1078,7 +1108,6 @@ init_source();
1078
1108
  import { writeFileSync, chmodSync, mkdirSync } from "fs";
1079
1109
  import { resolve, dirname } from "path";
1080
1110
  import { homedir } from "os";
1081
- import { createInterface } from "readline/promises";
1082
1111
 
1083
1112
  // src/lib/errors.ts
1084
1113
  init_source();
@@ -1128,12 +1157,60 @@ function formatError(err, format) {
1128
1157
  hint: ${hint}` : head };
1129
1158
  }
1130
1159
  function emitError(err, format) {
1131
- const env2 = formatError(err, format);
1132
- process.stderr.write(env2.text + `
1160
+ const env = formatError(err, format);
1161
+ process.stderr.write(env.text + `
1133
1162
  `);
1134
1163
  }
1135
1164
 
1165
+ // src/commands/common.ts
1166
+ var commonArgs = {
1167
+ format: { type: "string", description: "Output format: table | json (auto-detected by TTY)" },
1168
+ token: { type: "string", description: "Inline access token (prefer env vars or `oura-cli login`)" },
1169
+ db: { type: "string", description: "Path to SQLite database file (env: OURA_DB_PATH)" },
1170
+ tz: { type: "string", description: "Display timezone (env: OURA_TZ; auto-detected)" },
1171
+ "no-color": { type: "boolean", default: false, description: "Disable ANSI colors (also honors NO_COLOR env)" }
1172
+ };
1173
+
1136
1174
  // src/commands/login.ts
1175
+ async function readHiddenToken(input, output) {
1176
+ if (!input.isTTY || !input.setRawMode) {
1177
+ throw new CliError("BAD_ARGS", "Interactive login requires a terminal.", "Use `oura-cli login --token` for non-interactive use.");
1178
+ }
1179
+ output.write("Paste your token (input hidden): ");
1180
+ input.setRawMode(true);
1181
+ input.resume();
1182
+ return new Promise((resolve, reject) => {
1183
+ let token = "";
1184
+ const finish = () => {
1185
+ input.off("data", onData);
1186
+ input.setRawMode?.(false);
1187
+ input.pause();
1188
+ output.write(`
1189
+ `);
1190
+ };
1191
+ const onData = (chunk) => {
1192
+ for (const char of chunk.toString("utf8")) {
1193
+ if (char === "\r" || char === `
1194
+ `) {
1195
+ finish();
1196
+ resolve(token);
1197
+ return;
1198
+ }
1199
+ if (char === "\x03" || char === "\x04") {
1200
+ finish();
1201
+ reject(new CliError("BAD_ARGS", "Login cancelled."));
1202
+ return;
1203
+ }
1204
+ if (char === "\b" || char === "\x7F") {
1205
+ token = token.slice(0, -1);
1206
+ } else if (char >= " ") {
1207
+ token += char;
1208
+ }
1209
+ }
1210
+ };
1211
+ input.on("data", onData);
1212
+ });
1213
+ }
1137
1214
  function writeToken(path, token) {
1138
1215
  const trimmed = token.trim();
1139
1216
  if (trimmed.length === 0) {
@@ -1150,37 +1227,459 @@ var loginCommand = defineCommand({
1150
1227
  args: {
1151
1228
  token: { type: "string", description: "Pass token non-interactively (e.g. for scripts)" },
1152
1229
  path: { type: "string", description: "Where to save the token (default: $OURA_TOKEN_PATH or ~/.oura-token)" },
1153
- "no-color": { type: "boolean", default: false, description: "Disable ANSI colors (also honors NO_COLOR env)" }
1230
+ "no-color": commonArgs["no-color"]
1154
1231
  },
1155
1232
  async run({ args }) {
1156
- if (args["no-color"] || process.env.NO_COLOR) {
1157
- const { default: chk } = await Promise.resolve().then(() => (init_source(), exports_source));
1158
- chk.level = 0;
1159
- }
1160
- const target = args.path ?? process.env.OURA_TOKEN_PATH ?? resolve(homedir(), ".oura-token");
1161
- let token = args.token;
1162
- if (!token) {
1163
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1164
- console.log("Get a Personal Access Token at https://cloud.ouraring.com/personal-access-tokens");
1165
- token = await rl.question("Paste your token: ");
1166
- rl.close();
1233
+ try {
1234
+ if (args["no-color"] || process.env.NO_COLOR) {
1235
+ await Promise.resolve().then(() => init_source());
1236
+ source_default.level = 0;
1237
+ }
1238
+ const target = args.path ?? process.env.OURA_TOKEN_PATH ?? resolve(homedir(), ".oura-token");
1239
+ let token = args.token;
1240
+ if (!token) {
1241
+ console.log("Get a Personal Access Token at https://cloud.ouraring.com/personal-access-tokens");
1242
+ token = await readHiddenToken(process.stdin, process.stdout);
1243
+ }
1244
+ writeToken(target, token);
1245
+ console.log(source_default.green(`Saved to ${target}`));
1246
+ } catch (err) {
1247
+ emitError(err, "table");
1248
+ process.exit(exitCodeFor(err));
1167
1249
  }
1168
- writeToken(target, token);
1169
- console.log(source_default.green(`Saved to ${target}`));
1170
1250
  }
1171
1251
  });
1172
1252
 
1253
+ // src/lib/time.ts
1254
+ function nowUtc() {
1255
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
1256
+ }
1257
+ function formatLocal(utcStr, timezone) {
1258
+ const dt = new Date(utcStr);
1259
+ const parts = new Intl.DateTimeFormat("sv-SE", {
1260
+ timeZone: timezone,
1261
+ year: "numeric",
1262
+ month: "2-digit",
1263
+ day: "2-digit",
1264
+ hour: "2-digit",
1265
+ minute: "2-digit",
1266
+ hour12: false
1267
+ }).formatToParts(dt);
1268
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
1269
+ return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
1270
+ }
1271
+ function formatLocalDate(utcStr, timezone) {
1272
+ return formatLocal(utcStr, timezone).split(" ")[0];
1273
+ }
1274
+ function todayLocal(timezone) {
1275
+ return formatLocalDate(nowUtc(), timezone);
1276
+ }
1277
+ function getTimezoneOffsetMs(utc, tz) {
1278
+ const tzPart = new Intl.DateTimeFormat("en-US", {
1279
+ timeZone: tz,
1280
+ timeZoneName: "longOffset"
1281
+ }).formatToParts(utc).find((p) => p.type === "timeZoneName")?.value ?? "GMT";
1282
+ const m = tzPart.match(/GMT([+-])(\d{2}):(\d{2})/);
1283
+ if (!m)
1284
+ return 0;
1285
+ const sign = m[1] === "+" ? 1 : -1;
1286
+ return sign * (parseInt(m[2], 10) * 3600 + parseInt(m[3], 10) * 60) * 1000;
1287
+ }
1288
+ function localMidnightMs(day, tz) {
1289
+ const naiveMs = new Date(`${day}T00:00:00Z`).getTime();
1290
+ const guessMs = naiveMs - getTimezoneOffsetMs(new Date(naiveMs), tz);
1291
+ return naiveMs - getTimezoneOffsetMs(new Date(guessMs), tz);
1292
+ }
1293
+ function localDateToUtcRange(localDate, timezone) {
1294
+ const iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
1295
+ return [iso(localMidnightMs(localDate, timezone)), iso(localMidnightMs(shiftDay(localDate, 1), timezone))];
1296
+ }
1297
+ function resolveDefaultTimezone() {
1298
+ if (process.env.OURA_TZ)
1299
+ return process.env.OURA_TZ;
1300
+ try {
1301
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
1302
+ } catch {
1303
+ return "UTC";
1304
+ }
1305
+ }
1306
+ function today(timezone) {
1307
+ return todayLocal(timezone ?? resolveDefaultTimezone());
1308
+ }
1309
+ function shiftDay(day, delta) {
1310
+ const ms = new Date(`${day}T00:00:00Z`).getTime() + delta * 86400000;
1311
+ return new Date(ms).toISOString().slice(0, 10);
1312
+ }
1313
+ function daysBack(endDay, n) {
1314
+ const out = [];
1315
+ for (let i = n - 1;i >= 0; i--)
1316
+ out.push(shiftDay(endDay, -i));
1317
+ return out;
1318
+ }
1319
+ var CALENDAR_DATE = /^\d{4}-\d{2}-\d{2}$/;
1320
+ function isCalendarDate(value) {
1321
+ if (!CALENDAR_DATE.test(value))
1322
+ return false;
1323
+ const ms = new Date(`${value}T00:00:00Z`).getTime();
1324
+ return !Number.isNaN(ms) && new Date(ms).toISOString().slice(0, 10) === value;
1325
+ }
1326
+
1327
+ // src/collections/types.ts
1328
+ function defineCollection(c) {
1329
+ return c;
1330
+ }
1331
+
1332
+ // src/collections/sleep.ts
1333
+ var sleep = defineCollection({
1334
+ name: "sleep",
1335
+ endpoint: "daily_sleep",
1336
+ table: "daily_sleep",
1337
+ description: "Daily sleep score and contributors",
1338
+ conflict: "replace",
1339
+ rangeParams: "date",
1340
+ identity: [
1341
+ { field: "id", description: "Oura record id" },
1342
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1343
+ ],
1344
+ columns: [
1345
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1346
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1347
+ { name: "score", type: "INTEGER", pick: (r) => r.score },
1348
+ { name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
1349
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
1350
+ ]
1351
+ });
1352
+
1353
+ // src/collections/readiness.ts
1354
+ var readiness = defineCollection({
1355
+ name: "readiness",
1356
+ endpoint: "daily_readiness",
1357
+ table: "daily_readiness",
1358
+ description: "Daily readiness score, contributors and temperature deviation",
1359
+ conflict: "replace",
1360
+ rangeParams: "date",
1361
+ identity: [
1362
+ { field: "id", description: "Oura record id" },
1363
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1364
+ ],
1365
+ columns: [
1366
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1367
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1368
+ { name: "score", type: "INTEGER", pick: (r) => r.score },
1369
+ { name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
1370
+ { name: "temperature_deviation", type: "REAL", pick: (r) => r.temperature_deviation },
1371
+ { name: "temperature_trend_deviation", type: "REAL", pick: (r) => r.temperature_trend_deviation },
1372
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
1373
+ ]
1374
+ });
1375
+
1376
+ // src/collections/activity.ts
1377
+ var activity = defineCollection({
1378
+ name: "activity",
1379
+ endpoint: "daily_activity",
1380
+ table: "daily_activity",
1381
+ description: "Daily activity score, steps, calories and activity-time buckets",
1382
+ conflict: "replace",
1383
+ rangeParams: "date",
1384
+ identity: [
1385
+ { field: "id", description: "Oura record id" },
1386
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1387
+ ],
1388
+ columns: [
1389
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1390
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1391
+ { name: "score", type: "INTEGER", pick: (r) => r.score },
1392
+ { name: "active_calories", type: "INTEGER", pick: (r) => r.active_calories },
1393
+ { name: "steps", type: "INTEGER", pick: (r) => r.steps },
1394
+ { name: "equivalent_walking_distance", type: "REAL", pick: (r) => r.equivalent_walking_distance },
1395
+ { name: "high_activity_time", type: "INTEGER", pick: (r) => r.high_activity_time },
1396
+ { name: "medium_activity_time", type: "INTEGER", pick: (r) => r.medium_activity_time },
1397
+ { name: "low_activity_time", type: "INTEGER", pick: (r) => r.low_activity_time },
1398
+ { name: "sedentary_time", type: "INTEGER", pick: (r) => r.sedentary_time },
1399
+ { name: "total_calories", type: "INTEGER", pick: (r) => r.total_calories },
1400
+ { name: "target_calories", type: "INTEGER", pick: (r) => r.target_calories },
1401
+ { name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
1402
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
1403
+ ]
1404
+ });
1405
+
1406
+ // src/collections/hr.ts
1407
+ var hr = defineCollection({
1408
+ name: "hr",
1409
+ endpoint: "heartrate",
1410
+ table: "heartrate",
1411
+ description: "Heart rate samples (bpm) with source",
1412
+ conflict: "ignore",
1413
+ rangeParams: "datetime",
1414
+ maxRangeDays: 30,
1415
+ identity: [{ field: "timestamp", format: "date-time", description: "ISO 8601 timestamp of the sample" }],
1416
+ columns: [
1417
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp },
1418
+ { name: "bpm", type: "INTEGER", pick: (r) => r.bpm },
1419
+ { name: "source", type: "TEXT", pick: (r) => r.source },
1420
+ { name: "day", type: "TEXT", pick: (r) => r.timestamp.slice(0, 10) }
1421
+ ],
1422
+ indexes: [
1423
+ { name: "idx_heartrate_ts", columns: ["timestamp"] },
1424
+ { name: "idx_heartrate_unique", columns: ["timestamp", "source"], unique: true },
1425
+ { name: "idx_heartrate_day", columns: ["day"] }
1426
+ ]
1427
+ });
1428
+
1429
+ // src/collections/spo2.ts
1430
+ var spo2 = defineCollection({
1431
+ name: "spo2",
1432
+ endpoint: "daily_spo2",
1433
+ table: "daily_spo2",
1434
+ description: "Daily blood-oxygen average and breathing disturbance index",
1435
+ conflict: "replace",
1436
+ rangeParams: "date",
1437
+ identity: [
1438
+ { field: "id", description: "Oura record id" },
1439
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1440
+ ],
1441
+ columns: [
1442
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1443
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1444
+ { name: "spo2_average", type: "REAL", pick: (r) => r.spo2_percentage?.average ?? null },
1445
+ { name: "breathing_disturbance_index", type: "REAL", pick: (r) => r.breathing_disturbance_index }
1446
+ ]
1447
+ });
1448
+
1449
+ // src/collections/stress.ts
1450
+ var stress = defineCollection({
1451
+ name: "stress",
1452
+ endpoint: "daily_stress",
1453
+ table: "daily_stress",
1454
+ description: "Daily stress summary with high-stress and high-recovery seconds",
1455
+ conflict: "replace",
1456
+ rangeParams: "date",
1457
+ identity: [
1458
+ { field: "id", description: "Oura record id" },
1459
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1460
+ ],
1461
+ columns: [
1462
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1463
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1464
+ { name: "day_summary", type: "TEXT", pick: (r) => r.day_summary ?? null },
1465
+ { name: "recovery_high", type: "INTEGER", pick: (r) => r.recovery_high },
1466
+ { name: "stress_high", type: "INTEGER", pick: (r) => r.stress_high }
1467
+ ]
1468
+ });
1469
+
1470
+ // src/collections/workout.ts
1471
+ var workout = defineCollection({
1472
+ name: "workout",
1473
+ endpoint: "workout",
1474
+ table: "workouts",
1475
+ description: "Workout sessions with activity, calories, distance and intensity",
1476
+ conflict: "replace",
1477
+ rangeParams: "date",
1478
+ identity: [
1479
+ { field: "id", description: "Oura record id" },
1480
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1481
+ ],
1482
+ columns: [
1483
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1484
+ { name: "day", type: "TEXT", pick: (r) => r.day },
1485
+ { name: "activity", type: "TEXT", pick: (r) => r.activity },
1486
+ { name: "calories", type: "REAL", pick: (r) => r.calories },
1487
+ { name: "distance", type: "REAL", pick: (r) => r.distance },
1488
+ { name: "start_datetime", type: "TEXT", pick: (r) => r.start_datetime },
1489
+ { name: "end_datetime", type: "TEXT", pick: (r) => r.end_datetime },
1490
+ { name: "intensity", type: "TEXT", pick: (r) => r.intensity },
1491
+ { name: "label", type: "TEXT", pick: (r) => r.label ?? "" },
1492
+ { name: "source", type: "TEXT", pick: (r) => r.source }
1493
+ ]
1494
+ });
1495
+
1496
+ // src/collections/sleep-periods.ts
1497
+ var sleepPeriods = defineCollection({
1498
+ name: "sleep-periods",
1499
+ endpoint: "sleep",
1500
+ table: "sleep_model",
1501
+ description: "Individual sleep periods with stages, HRV, heart rate and efficiency",
1502
+ conflict: "replace",
1503
+ rangeParams: "date",
1504
+ identity: [
1505
+ { field: "id", description: "Oura record id" },
1506
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1507
+ ],
1508
+ columns: [
1509
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1510
+ { name: "day", type: "TEXT", pick: (r) => r.day },
1511
+ { name: "average_breath", type: "REAL", pick: (r) => r.average_breath },
1512
+ { name: "average_heart_rate", type: "REAL", pick: (r) => r.average_heart_rate },
1513
+ { name: "average_hrv", type: "REAL", pick: (r) => r.average_hrv },
1514
+ { name: "awake_time", type: "INTEGER", pick: (r) => r.awake_time },
1515
+ { name: "bedtime_end", type: "TEXT", pick: (r) => r.bedtime_end },
1516
+ { name: "bedtime_start", type: "TEXT", pick: (r) => r.bedtime_start },
1517
+ { name: "deep_sleep_duration", type: "INTEGER", pick: (r) => r.deep_sleep_duration },
1518
+ { name: "efficiency", type: "INTEGER", pick: (r) => r.efficiency },
1519
+ { name: "latency", type: "INTEGER", pick: (r) => r.latency },
1520
+ { name: "light_sleep_duration", type: "INTEGER", pick: (r) => r.light_sleep_duration },
1521
+ { name: "lowest_heart_rate", type: "INTEGER", pick: (r) => r.lowest_heart_rate },
1522
+ { name: "period", type: "INTEGER", pick: (r) => r.period },
1523
+ { name: "rem_sleep_duration", type: "INTEGER", pick: (r) => r.rem_sleep_duration },
1524
+ { name: "restless_periods", type: "INTEGER", pick: (r) => r.restless_periods },
1525
+ { name: "time_in_bed", type: "INTEGER", pick: (r) => r.time_in_bed },
1526
+ { name: "total_sleep_duration", type: "INTEGER", pick: (r) => r.total_sleep_duration },
1527
+ { name: "type", type: "TEXT", pick: (r) => r.type ?? null }
1528
+ ]
1529
+ });
1530
+
1531
+ // src/collections/cv-age.ts
1532
+ var cvAge = defineCollection({
1533
+ name: "cv-age",
1534
+ endpoint: "daily_cardiovascular_age",
1535
+ table: "cardiovascular_age",
1536
+ description: "Daily cardiovascular (vascular) age estimate",
1537
+ conflict: "replace",
1538
+ rangeParams: "date",
1539
+ identity: [
1540
+ { field: "id", description: "Oura record id" },
1541
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1542
+ ],
1543
+ columns: [
1544
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1545
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1546
+ { name: "vascular_age", type: "INTEGER", pick: (r) => r.vascular_age }
1547
+ ]
1548
+ });
1549
+
1550
+ // src/collections/index.ts
1551
+ var COLLECTIONS = [
1552
+ sleep,
1553
+ readiness,
1554
+ activity,
1555
+ hr,
1556
+ spo2,
1557
+ stress,
1558
+ workout,
1559
+ sleepPeriods,
1560
+ cvAge
1561
+ ];
1562
+ function names() {
1563
+ return COLLECTIONS.map((c) => c.name);
1564
+ }
1565
+ function byName(name) {
1566
+ return COLLECTIONS.find((c) => c.name === name);
1567
+ }
1568
+ function insertSql(c) {
1569
+ const verb = c.conflict === "replace" ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
1570
+ const cols = c.columns.map((col) => col.name).join(", ");
1571
+ const marks = c.columns.map(() => "?").join(", ");
1572
+ return `${verb} INTO ${c.table} (${cols}) VALUES (${marks})`;
1573
+ }
1574
+ function rowValues(c, row) {
1575
+ return c.columns.map((col) => col.pick(row));
1576
+ }
1577
+ var MS_PER_DAY = 86400000;
1578
+ function dateQueries(start, end, maxDays) {
1579
+ if (!maxDays)
1580
+ return [{ start_date: start, end_date: end }];
1581
+ const out = [];
1582
+ for (let s = start;s <= end; s = shiftDay(s, maxDays)) {
1583
+ const e = shiftDay(s, maxDays - 1);
1584
+ out.push({ start_date: s, end_date: e < end ? e : end });
1585
+ }
1586
+ return out;
1587
+ }
1588
+ function datetimeQueries(start, end, tz, maxDays) {
1589
+ const from = Date.parse(localDateToUtcRange(start, tz)[0]);
1590
+ const to = Date.parse(localDateToUtcRange(end, tz)[1]) - 1;
1591
+ const span = (maxDays ?? Infinity) * MS_PER_DAY;
1592
+ const out = [];
1593
+ for (let s = from;s <= to; s += span) {
1594
+ out.push({ start_datetime: new Date(s).toISOString(), end_datetime: new Date(Math.min(s + span - 1, to)).toISOString() });
1595
+ }
1596
+ return out;
1597
+ }
1598
+ function rangeQueries(c, start, end, tz) {
1599
+ if (start > end)
1600
+ return [];
1601
+ return c.rangeParams === "date" ? dateQueries(start, end, c.maxRangeDays) : datetimeQueries(start, end, tz, c.maxRangeDays);
1602
+ }
1603
+ async function fetchCollection(client, c, start, end, tz) {
1604
+ const rows = [];
1605
+ for (const query of rangeQueries(c, start, end, tz)) {
1606
+ for (const row of await client.fetch(c.endpoint, query))
1607
+ rows.push(row);
1608
+ }
1609
+ return rows;
1610
+ }
1611
+
1173
1612
  // src/commands/describe.ts
1174
- function buildManifest(version) {
1613
+ var OUTPUT_SCHEMAS = { doctor: "docs/schemas/doctor.json" };
1614
+ var ENUM_ARGS = {
1615
+ fetch: { collection: names() },
1616
+ report: { period: ["week", "month"] }
1617
+ };
1618
+ function resolved(def) {
1619
+ if (typeof def === "function" || def instanceof Promise) {
1620
+ throw new Error("describe: lazy subcommands are not supported; register plain CommandDef objects.");
1621
+ }
1622
+ return def;
1623
+ }
1624
+ function describeArgs(command, args) {
1625
+ const out = [];
1626
+ for (const [key, raw] of Object.entries(args ?? {})) {
1627
+ if (raw === commonArgs[key])
1628
+ continue;
1629
+ const a = raw;
1630
+ const values = ENUM_ARGS[command]?.[key] ?? (a.type === "enum" ? [...a.options ?? []] : undefined);
1631
+ if (a.type === "positional") {
1632
+ out.push({
1633
+ name: a.required ? `<${key}>` : `[${key}]`,
1634
+ type: "string",
1635
+ required: a.required === true,
1636
+ description: a.description,
1637
+ ...values ? { values } : {}
1638
+ });
1639
+ } else {
1640
+ out.push({
1641
+ name: `--${key}`,
1642
+ type: values ? "enum" : String(a.type ?? "string"),
1643
+ required: false,
1644
+ description: a.description,
1645
+ ...values ? { values } : {}
1646
+ });
1647
+ }
1648
+ }
1649
+ return out;
1650
+ }
1651
+ function resolvedMeta(meta) {
1652
+ if (typeof meta === "function" || meta instanceof Promise)
1653
+ return {};
1654
+ return meta ?? {};
1655
+ }
1656
+ function describeCommandDef(name, def) {
1657
+ const meta = resolvedMeta(def.meta);
1658
+ const cmd = {
1659
+ name,
1660
+ description: meta.description ?? "",
1661
+ args: describeArgs(name, def.args)
1662
+ };
1663
+ if (OUTPUT_SCHEMAS[name])
1664
+ cmd.outputSchema = OUTPUT_SCHEMAS[name];
1665
+ if (name === "fetch")
1666
+ cmd.outputSchemas = Object.fromEntries(names().map((n) => [n, `docs/schemas/${n}.json`]));
1667
+ const subs = def.subCommands;
1668
+ if (subs) {
1669
+ cmd.subcommands = Object.entries(subs).map(([subName, subDef]) => {
1670
+ const sub = resolved(subDef);
1671
+ const subMeta = resolvedMeta(sub.meta);
1672
+ return { name: subName, description: subMeta.description ?? "", args: describeArgs(subName, sub.args) };
1673
+ });
1674
+ }
1675
+ return cmd;
1676
+ }
1677
+ function buildManifest(version, commands) {
1175
1678
  return {
1176
1679
  name: "oura-cli",
1177
1680
  version,
1178
1681
  compatManifestCommand: "oura-cli manifest",
1179
- auth: {
1180
- envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"],
1181
- tokenFile: "~/.oura-token",
1182
- loginCommand: "oura-cli login"
1183
- },
1682
+ auth: { envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"], tokenFile: "~/.oura-token", loginCommand: "oura-cli login" },
1184
1683
  globalFlags: [
1185
1684
  { name: "--format", type: "enum", values: ["table", "json"], description: "Output format (auto-detected by TTY when omitted)" },
1186
1685
  { name: "--db", type: "string", description: "Override SQLite database path (env: OURA_DB_PATH)" },
@@ -1195,396 +1694,48 @@ function buildManifest(version) {
1195
1694
  { code: 3, meaning: "API or network error" },
1196
1695
  { code: 4, meaning: "database or local storage error" }
1197
1696
  ],
1198
- commands: [
1199
- { name: "login", description: "Save an Oura Personal Access Token for future commands.", args: [
1200
- { name: "--token", type: "string", required: false, description: "Pass token non-interactively" },
1201
- { name: "--path", type: "string", required: false, description: "Override token file path" }
1202
- ] },
1203
- { name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs.", args: [] },
1204
- {
1205
- name: "sleep",
1206
- description: "Fetch daily sleep scores from Oura API. Pick a subcommand: today | date <day> | week.",
1207
- args: [],
1208
- outputSchema: "docs/schemas/sleep.json",
1209
- subcommands: [
1210
- { name: "today", description: "Today's sleep data.", args: [] },
1211
- { name: "date", description: "Sleep data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1212
- { name: "week", description: "Last 7 days of sleep data.", args: [] }
1213
- ]
1214
- },
1215
- {
1216
- name: "readiness",
1217
- description: "Fetch daily readiness scores from Oura API. Pick a subcommand: today | date <day> | week.",
1218
- args: [],
1219
- outputSchema: "docs/schemas/readiness.json",
1220
- subcommands: [
1221
- { name: "today", description: "Today's readiness data.", args: [] },
1222
- { name: "date", description: "Readiness data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1223
- { name: "week", description: "Last 7 days of readiness data.", args: [] }
1224
- ]
1225
- },
1226
- {
1227
- name: "activity",
1228
- description: "Fetch daily activity scores from Oura API. Pick a subcommand: today | date <day> | week.",
1229
- args: [],
1230
- outputSchema: "docs/schemas/activity.json",
1231
- subcommands: [
1232
- { name: "today", description: "Today's activity data.", args: [] },
1233
- { name: "date", description: "Activity data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1234
- { name: "week", description: "Last 7 days of activity data.", args: [] }
1235
- ]
1236
- },
1237
- {
1238
- name: "hr",
1239
- description: "Fetch heart rate samples from Oura API. Pick a subcommand: today | date <day> | week.",
1240
- args: [],
1241
- outputSchema: "docs/schemas/hr.json",
1242
- subcommands: [
1243
- { name: "today", description: "Today's heart rate data.", args: [] },
1244
- { name: "date", description: "Heart rate data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1245
- { name: "week", description: "Last 7 days of heart rate data.", args: [] }
1246
- ]
1247
- },
1248
- {
1249
- name: "spo2",
1250
- description: "Fetch blood oxygen (SpO2) data from Oura API. Pick a subcommand: today | date <day> | week.",
1251
- args: [],
1252
- outputSchema: "docs/schemas/spo2.json",
1253
- subcommands: [
1254
- { name: "today", description: "Today's SpO2 data.", args: [] },
1255
- { name: "date", description: "SpO2 data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1256
- { name: "week", description: "Last 7 days of SpO2 data.", args: [] }
1257
- ]
1258
- },
1259
- {
1260
- name: "stress",
1261
- description: "Fetch daily stress data from Oura API. Pick a subcommand: today | date <day> | week.",
1262
- args: [],
1263
- outputSchema: "docs/schemas/stress.json",
1264
- subcommands: [
1265
- { name: "today", description: "Today's stress data.", args: [] },
1266
- { name: "date", description: "Stress data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1267
- { name: "week", description: "Last 7 days of stress data.", args: [] }
1268
- ]
1269
- },
1270
- {
1271
- name: "workout",
1272
- description: "Fetch workout data from Oura API. Pick a subcommand: today | date <day> | week.",
1273
- args: [],
1274
- outputSchema: "docs/schemas/workout.json",
1275
- subcommands: [
1276
- { name: "today", description: "Today's workout data.", args: [] },
1277
- { name: "date", description: "Workout data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1278
- { name: "week", description: "Last 7 days of workout data.", args: [] }
1279
- ]
1280
- },
1281
- { name: "sync", description: "Sync all Oura collections into the local database.", args: [] },
1282
- {
1283
- name: "db",
1284
- description: "Query and manage the local SQLite cache. Pick a subcommand.",
1285
- args: [],
1286
- subcommands: [
1287
- { name: "today", description: "Today's summary from local DB.", args: [] },
1288
- { name: "date", description: "Summary for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
1289
- { name: "week", description: "Last 7 days from local DB.", args: [] },
1290
- { name: "trends", description: "Score and metric trends over N days (default 30).", args: [{ name: "[days]", type: "number", required: false, description: "Window size in days." }] },
1291
- { name: "stats", description: "Row counts, date range, record highs.", args: [] },
1292
- { name: "import", description: "Sync new data from Oura API into the local DB.", args: [] },
1293
- { name: "reset", description: "Destroy and rebuild the database from exported CSVs.", args: [] }
1294
- ]
1295
- },
1296
- {
1297
- name: "report",
1298
- description: "Generate a narrative health report from local data.",
1299
- args: [
1300
- { name: "--period", type: "enum", values: ["week", "month"], description: "Report window (default week)." }
1301
- ]
1302
- }
1303
- ]
1697
+ commands: Object.entries(commands).map(([name, def]) => describeCommandDef(name, resolved(def)))
1304
1698
  };
1305
1699
  }
1306
- function describeCommand(version) {
1700
+ function describeCommand(version, getCommands) {
1307
1701
  return defineCommand({
1308
1702
  meta: { name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs." },
1309
1703
  args: {},
1310
1704
  run() {
1311
- console.log(JSON.stringify(buildManifest(version), null, 2));
1705
+ console.log(JSON.stringify(buildManifest(version, getCommands()), null, 2));
1312
1706
  }
1313
1707
  });
1314
1708
  }
1315
1709
 
1316
- // src/commands/sync.ts
1317
- import { mkdirSync as mkdirSync3 } from "fs";
1318
- import { dirname as dirname2 } from "path";
1319
-
1320
- // src/lib/db.ts
1321
- import { Database } from "bun:sqlite";
1322
- import { resolve as resolve2 } from "path";
1323
- import { homedir as homedir2 } from "os";
1324
- import { mkdirSync as mkdirSync2 } from "fs";
1325
- function getDbPath(options = {}) {
1326
- if (options.dbPath)
1327
- return options.dbPath;
1328
- if (options.envVar && process.env[options.envVar])
1329
- return process.env[options.envVar];
1330
- const dir = options.defaultDir ?? ".oura-cli";
1331
- const file = options.defaultFile ?? "oura.db";
1332
- return resolve2(homedir2(), dir, file);
1333
- }
1334
- function openDatabase(options = {}) {
1335
- const dbPath = getDbPath(options);
1336
- if (dbPath !== ":memory:") {
1337
- mkdirSync2(resolve2(dbPath, ".."), { recursive: true });
1710
+ // src/db/sync.ts
1711
+ var BACKFILL_DAYS = 30;
1712
+ var FRESHNESS_TABLES = ["daily_sleep", "daily_readiness", "daily_activity"];
1713
+ async function importDaily(db, client, clock, log) {
1714
+ const { today, tz } = clock;
1715
+ const _log = log ?? (() => {});
1716
+ const lastDates = [];
1717
+ for (const tbl of FRESHNESS_TABLES) {
1718
+ const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
1719
+ if (row?.d)
1720
+ lastDates.push(row.d);
1338
1721
  }
1339
- const db = new Database(dbPath);
1340
- db.exec("PRAGMA journal_mode = WAL");
1341
- db.exec("PRAGMA foreign_keys = ON");
1342
- return db;
1343
- }
1344
- function getSchemaVersion(db) {
1345
- db.exec("CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)");
1346
- const row = db.query("SELECT MAX(version) AS v FROM _schema_version").get();
1347
- return row?.v ?? 0;
1348
- }
1349
- function ensureSchema(db, migrations) {
1350
- const current = getSchemaVersion(db);
1351
- for (const m of migrations) {
1352
- if (m.version > current) {
1353
- db.exec(m.sql);
1354
- db.query("INSERT INTO _schema_version (version) VALUES (?)").run(m.version);
1355
- }
1722
+ const isFirstSync = lastDates.length === 0;
1723
+ const startDate = isFirstSync ? shiftDay(today, -BACKFILL_DAYS) : lastDates.sort()[0];
1724
+ _log(isFirstSync ? `First sync \u2014 backfilling the last ${BACKFILL_DAYS} days: ${startDate} \u2192 ${today}` : `Syncing ${startDate} \u2192 ${today}`);
1725
+ const counts = {};
1726
+ for (const c of COLLECTIONS) {
1727
+ const rows = await fetchCollection(client, c, startDate, today, tz);
1728
+ const stmt = db.query(insertSql(c));
1729
+ db.transaction((rs) => {
1730
+ for (const r of rs)
1731
+ stmt.run(...rowValues(c, r));
1732
+ })(rows);
1733
+ counts[c.table] = rows.length;
1734
+ if (rows.length > 0)
1735
+ _log(` + ${c.table}: ${rows.length} rows`);
1356
1736
  }
1357
- }
1358
-
1359
- // src/db/schema.ts
1360
- var MIGRATIONS = [
1361
- {
1362
- version: 1,
1363
- sql: `
1364
- CREATE TABLE IF NOT EXISTS daily_sleep (
1365
- id TEXT PRIMARY KEY,
1366
- day TEXT UNIQUE,
1367
- score INTEGER,
1368
- contributors TEXT,
1369
- timestamp TEXT
1370
- );
1371
- CREATE TABLE IF NOT EXISTS daily_readiness (
1372
- id TEXT PRIMARY KEY,
1373
- day TEXT UNIQUE,
1374
- score INTEGER,
1375
- contributors TEXT,
1376
- temperature_deviation REAL,
1377
- temperature_trend_deviation REAL,
1378
- timestamp TEXT
1379
- );
1380
- CREATE TABLE IF NOT EXISTS daily_activity (
1381
- id TEXT PRIMARY KEY,
1382
- day TEXT UNIQUE,
1383
- score INTEGER,
1384
- active_calories INTEGER,
1385
- steps INTEGER,
1386
- equivalent_walking_distance REAL,
1387
- high_activity_time INTEGER,
1388
- medium_activity_time INTEGER,
1389
- low_activity_time INTEGER,
1390
- sedentary_time INTEGER,
1391
- total_calories INTEGER,
1392
- target_calories INTEGER,
1393
- contributors TEXT,
1394
- timestamp TEXT
1395
- );
1396
- CREATE TABLE IF NOT EXISTS daily_spo2 (
1397
- id TEXT PRIMARY KEY,
1398
- day TEXT UNIQUE,
1399
- spo2_average REAL,
1400
- breathing_disturbance_index REAL
1401
- );
1402
- CREATE TABLE IF NOT EXISTS daily_stress (
1403
- id TEXT PRIMARY KEY,
1404
- day TEXT UNIQUE,
1405
- day_summary TEXT,
1406
- recovery_high INTEGER,
1407
- stress_high INTEGER
1408
- );
1409
- CREATE TABLE IF NOT EXISTS heartrate (
1410
- timestamp TEXT,
1411
- bpm INTEGER,
1412
- source TEXT,
1413
- day TEXT
1414
- );
1415
- CREATE INDEX IF NOT EXISTS idx_heartrate_ts ON heartrate(timestamp);
1416
- CREATE UNIQUE INDEX IF NOT EXISTS idx_heartrate_unique ON heartrate(timestamp, source);
1417
- CREATE INDEX IF NOT EXISTS idx_heartrate_day ON heartrate(day);
1418
- CREATE TABLE IF NOT EXISTS vo2max (
1419
- id TEXT PRIMARY KEY,
1420
- day TEXT UNIQUE,
1421
- vo2_max REAL,
1422
- timestamp TEXT
1423
- );
1424
- CREATE TABLE IF NOT EXISTS cardiovascular_age (
1425
- id TEXT PRIMARY KEY,
1426
- day TEXT UNIQUE,
1427
- vascular_age INTEGER
1428
- );
1429
- CREATE TABLE IF NOT EXISTS workouts (
1430
- id TEXT PRIMARY KEY,
1431
- day TEXT,
1432
- activity TEXT,
1433
- calories REAL,
1434
- distance REAL,
1435
- start_datetime TEXT,
1436
- end_datetime TEXT,
1437
- intensity TEXT,
1438
- label TEXT,
1439
- source TEXT
1440
- );
1441
- CREATE TABLE IF NOT EXISTS sleep_model (
1442
- id TEXT PRIMARY KEY,
1443
- day TEXT,
1444
- average_breath REAL,
1445
- average_heart_rate REAL,
1446
- average_hrv REAL,
1447
- awake_time INTEGER,
1448
- bedtime_end TEXT,
1449
- bedtime_start TEXT,
1450
- deep_sleep_duration INTEGER,
1451
- efficiency INTEGER,
1452
- latency INTEGER,
1453
- light_sleep_duration INTEGER,
1454
- lowest_heart_rate INTEGER,
1455
- period INTEGER,
1456
- rem_sleep_duration INTEGER,
1457
- restless_periods INTEGER,
1458
- time_in_bed INTEGER,
1459
- total_sleep_duration INTEGER,
1460
- type TEXT
1461
- );
1462
- `
1463
- },
1464
- {
1465
- version: 2,
1466
- sql: `
1467
- CREATE VIEW IF NOT EXISTS v_weekly_sleep AS
1468
- SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score, COUNT(*) as days
1469
- FROM daily_sleep WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
1470
-
1471
- CREATE VIEW IF NOT EXISTS v_weekly_readiness AS
1472
- SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
1473
- ROUND(AVG(temperature_deviation),2) as avg_temp_dev, COUNT(*) as days
1474
- FROM daily_readiness WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
1475
-
1476
- CREATE VIEW IF NOT EXISTS v_weekly_activity AS
1477
- SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
1478
- SUM(steps) as total_steps, SUM(active_calories) as total_active_cal, COUNT(*) as days
1479
- FROM daily_activity WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
1480
-
1481
- CREATE VIEW IF NOT EXISTS v_sleep_detail AS
1482
- SELECT day, ROUND(total_sleep_duration/3600.0,1) as sleep_hours,
1483
- ROUND(deep_sleep_duration/3600.0,1) as deep_hours,
1484
- ROUND(rem_sleep_duration/3600.0,1) as rem_hours,
1485
- average_hrv, average_heart_rate as avg_hr, lowest_heart_rate as lowest_hr, efficiency
1486
- FROM sleep_model ORDER BY day DESC;
1487
- `
1488
- }
1489
- ];
1490
-
1491
- // src/db/database.ts
1492
- var DB_OPTIONS = {
1493
- envVar: "OURA_DB_PATH",
1494
- defaultDir: ".oura-cli",
1495
- defaultFile: "oura.db"
1496
- };
1497
- function getDbPath2(options = {}) {
1498
- return getDbPath({ ...DB_OPTIONS, ...options });
1499
- }
1500
- function openDatabase2(options = {}) {
1501
- return openDatabase({ ...DB_OPTIONS, ...options });
1502
- }
1503
- function ensureSchema2(db) {
1504
- ensureSchema(db, MIGRATIONS);
1505
- }
1506
-
1507
- // src/db/import.ts
1508
- async function importDaily(db, client, log) {
1509
- const _log = log ?? (() => {});
1510
- const today = new Date().toISOString().slice(0, 10);
1511
- const lastDates = [];
1512
- for (const tbl of ["daily_sleep", "daily_readiness", "daily_activity"]) {
1513
- const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
1514
- if (row?.d)
1515
- lastDates.push(row.d);
1516
- }
1517
- const startDate = lastDates.length > 0 ? lastDates.sort()[0] : new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
1518
- _log(`Syncing from ${startDate} to ${today}`);
1519
- const counts = {};
1520
- const sleepData = await client.fetch("daily_sleep", startDate, today);
1521
- const insertSleep = db.query("INSERT OR REPLACE INTO daily_sleep VALUES (?,?,?,?,?)");
1522
- for (const s of sleepData) {
1523
- insertSleep.run(s.id, s.day, s.score, JSON.stringify(s.contributors), s.timestamp);
1524
- _log(` + sleep ${s.day}`);
1525
- }
1526
- counts.daily_sleep = sleepData.length;
1527
- const readinessData = await client.fetch("daily_readiness", startDate, today);
1528
- const insertReadiness = db.query("INSERT OR REPLACE INTO daily_readiness VALUES (?,?,?,?,?,?,?)");
1529
- for (const r of readinessData) {
1530
- insertReadiness.run(r.id, r.day, r.score, JSON.stringify(r.contributors), r.temperature_deviation, r.temperature_trend_deviation, r.timestamp);
1531
- _log(` + readiness ${r.day}`);
1532
- }
1533
- counts.daily_readiness = readinessData.length;
1534
- const activityData = await client.fetch("daily_activity", startDate, today);
1535
- const insertActivity = db.query("INSERT OR REPLACE INTO daily_activity VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
1536
- for (const a of activityData) {
1537
- insertActivity.run(a.id, a.day, a.score, a.active_calories, a.steps, a.equivalent_walking_distance, a.high_activity_time, a.medium_activity_time, a.low_activity_time, a.sedentary_time, a.total_calories, a.target_calories, JSON.stringify(a.contributors), a.timestamp);
1538
- _log(` + activity ${a.day}`);
1539
- }
1540
- counts.daily_activity = activityData.length;
1541
- const hrData = await client.fetch("heartrate", today, today);
1542
- const insertHr = db.query("INSERT OR IGNORE INTO heartrate VALUES (?,?,?,?)");
1543
- for (const h of hrData) {
1544
- const day = h.timestamp.slice(0, 10);
1545
- insertHr.run(h.timestamp, h.bpm, h.source, day);
1546
- }
1547
- counts.heartrate = hrData.length;
1548
- if (hrData.length > 0)
1549
- _log(` + heartrate ${hrData.length} records`);
1550
- const spo2Data = await client.fetch("daily_spo2", startDate, today);
1551
- const insertSpo2 = db.query("INSERT OR REPLACE INTO daily_spo2 VALUES (?,?,?,?)");
1552
- for (const s of spo2Data) {
1553
- const avg = s.spo2_percentage?.average ?? null;
1554
- insertSpo2.run(s.id, s.day, avg, s.breathing_disturbance_index);
1555
- _log(` + spo2 ${s.day}`);
1556
- }
1557
- counts.daily_spo2 = spo2Data.length;
1558
- const stressData = await client.fetch("daily_stress", startDate, today);
1559
- const insertStress = db.query("INSERT OR REPLACE INTO daily_stress VALUES (?,?,?,?,?)");
1560
- for (const s of stressData) {
1561
- insertStress.run(s.id, s.day, s.day_summary, s.recovery_high, s.stress_high);
1562
- _log(` + stress ${s.day}`);
1563
- }
1564
- counts.daily_stress = stressData.length;
1565
- const workoutData = await client.fetch("workout", startDate, today);
1566
- const insertWorkout = db.query("INSERT OR REPLACE INTO workouts VALUES (?,?,?,?,?,?,?,?,?,?)");
1567
- for (const w of workoutData) {
1568
- insertWorkout.run(w.id, w.day, w.activity, w.calories, w.distance, w.start_datetime, w.end_datetime, w.intensity, w.label ?? "", w.source);
1569
- _log(` + workout ${w.day} ${w.activity}`);
1570
- }
1571
- counts.workouts = workoutData.length;
1572
- const sleepPeriods = await client.fetch("sleep", startDate, today);
1573
- const insertSleepModel = db.query(`INSERT OR REPLACE INTO sleep_model VALUES (${Array(19).fill("?").join(",")})`);
1574
- for (const sp of sleepPeriods) {
1575
- insertSleepModel.run(sp.id, sp.day, sp.average_breath, sp.average_heart_rate, sp.average_hrv, sp.awake_time, sp.bedtime_end, sp.bedtime_start, sp.deep_sleep_duration, sp.efficiency, sp.latency, sp.light_sleep_duration, sp.lowest_heart_rate, sp.period, sp.rem_sleep_duration, sp.restless_periods, sp.time_in_bed, sp.total_sleep_duration, sp.type);
1576
- _log(` + sleep_period ${sp.day} (${sp.type})`);
1577
- }
1578
- counts.sleep_model = sleepPeriods.length;
1579
- const cvData = await client.fetch("daily_cardiovascular_age", startDate, today);
1580
- const insertCv = db.query("INSERT OR REPLACE INTO cardiovascular_age VALUES (?,?,?)");
1581
- for (const c of cvData) {
1582
- insertCv.run(c.id, c.day, c.vascular_age);
1583
- _log(` + cardiovascular_age ${c.day}`);
1584
- }
1585
- counts.cardiovascular_age = cvData.length;
1586
- _log("Import complete.");
1587
- return { startDate, endDate: today, counts };
1737
+ _log("Import complete.");
1738
+ return { startDate, endDate: today, counts, isFirstSync };
1588
1739
  }
1589
1740
 
1590
1741
  // src/db/queries.ts
@@ -1612,9 +1763,8 @@ function getDaySummary(db, day) {
1612
1763
  efficiency: sm?.efficiency ?? null
1613
1764
  };
1614
1765
  }
1615
- function getTrends(db, days) {
1616
- const today = new Date().toISOString().slice(0, 10);
1617
- const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
1766
+ function getTrends(db, days, today) {
1767
+ const start = shiftDay(today, -days);
1618
1768
  const results = [];
1619
1769
  const metrics = [
1620
1770
  ["Sleep Score", "daily_sleep", "score"],
@@ -1635,25 +1785,13 @@ function getTrends(db, days) {
1635
1785
  }
1636
1786
  return results;
1637
1787
  }
1638
- function getStats(db) {
1639
- const tableNames = [
1640
- "daily_sleep",
1641
- "daily_readiness",
1642
- "daily_activity",
1643
- "daily_spo2",
1644
- "daily_stress",
1645
- "heartrate",
1646
- "vo2max",
1647
- "cardiovascular_age",
1648
- "workouts",
1649
- "sleep_model"
1650
- ];
1651
- const tables = tableNames.map((table) => {
1652
- const row = db.query(`SELECT COUNT(*) as cnt FROM ${table}`).get();
1653
- return { table, rows: row.cnt };
1788
+ function getStats(db, today) {
1789
+ const tables = COLLECTIONS.map((c) => {
1790
+ const row = db.query(`SELECT COUNT(*) as cnt FROM ${c.table}`).get();
1791
+ return { table: c.table, rows: row.cnt };
1654
1792
  });
1655
1793
  const range = db.query("SELECT MIN(day) as first, MAX(day) as last FROM daily_sleep").get();
1656
- const trends = getTrends(db, 99999);
1794
+ const trends = getTrends(db, 99999, today);
1657
1795
  const mostSteps = db.query("SELECT day, steps FROM daily_activity WHERE steps IS NOT NULL ORDER BY steps DESC LIMIT 1").get();
1658
1796
  const bestSleep = db.query("SELECT day, score FROM daily_sleep WHERE score IS NOT NULL ORDER BY score DESC LIMIT 1").get();
1659
1797
  return {
@@ -1667,7 +1805,7 @@ function getStats(db) {
1667
1805
  };
1668
1806
  }
1669
1807
 
1670
- // src/format.ts
1808
+ // src/render/format.ts
1671
1809
  init_source();
1672
1810
  function scoreColor(score) {
1673
1811
  if (score === null)
@@ -1683,9 +1821,22 @@ function fmtHours(h) {
1683
1821
  return source_default.gray("\u2014");
1684
1822
  return `${h}h`;
1685
1823
  }
1686
- function formatDaySummary(summary, format) {
1824
+ function isEmptyDay(s) {
1825
+ return s.sleep_score === null && s.readiness_score === null && s.activity_score === null && s.steps === null && s.stress === null && s.spo2 === null && s.temp_deviation === null && s.sleep_hours === null && s.deep_hours === null && s.rem_hours === null && s.avg_hrv === null && s.lowest_hr === null && s.efficiency === null;
1826
+ }
1827
+ function formatDaySummary(summary, format, emptyHint) {
1687
1828
  if (format === "json")
1688
1829
  return JSON.stringify(summary, null, 2);
1830
+ if (emptyHint && isEmptyDay(summary)) {
1831
+ return [
1832
+ "",
1833
+ source_default.bold(` ${summary.day}`),
1834
+ source_default.gray("\u2500".repeat(50)),
1835
+ ` No Oura data for ${summary.day} yet.`,
1836
+ ` ${emptyHint}`
1837
+ ].join(`
1838
+ `);
1839
+ }
1689
1840
  const lines = [
1690
1841
  "",
1691
1842
  source_default.bold(` ${summary.day}`),
@@ -1709,9 +1860,26 @@ function formatDaySummary(summary, format) {
1709
1860
  return lines.join(`
1710
1861
  `);
1711
1862
  }
1712
- function formatWeekTable(days, format) {
1863
+ function formatImportSummary(result) {
1864
+ const c = result.counts;
1865
+ return [
1866
+ ` Imported ${result.startDate} \u2192 ${result.endDate}:`,
1867
+ ` sleep ${c.daily_sleep ?? 0} readiness ${c.daily_readiness ?? 0} activity ${c.daily_activity ?? 0} sleep periods ${c.sleep_model ?? 0}`,
1868
+ ` spo2 ${c.daily_spo2 ?? 0} stress ${c.daily_stress ?? 0} workouts ${c.workouts ?? 0} heart rate ${c.heartrate ?? 0} cardiovascular age ${c.cardiovascular_age ?? 0}`
1869
+ ].join(`
1870
+ `);
1871
+ }
1872
+ function formatWeekTable(days, format, emptyHint) {
1713
1873
  if (format === "json")
1714
1874
  return JSON.stringify(days, null, 2);
1875
+ if (emptyHint && days.length > 0 && days.every(isEmptyDay)) {
1876
+ return [
1877
+ "",
1878
+ " No Oura data for the last 7 days yet.",
1879
+ ` ${emptyHint}`
1880
+ ].join(`
1881
+ `);
1882
+ }
1715
1883
  const header = `${"Day".padEnd(12)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Activity".padStart(9)} ${"Steps".padStart(7)} ${"Stress".padEnd(10)}`;
1716
1884
  const sep = source_default.gray("\u2500".repeat(56));
1717
1885
  const rows = days.map((d) => `${d.day.padEnd(12)} ${scoreColor(d.sleep_score).padStart(6)} ${scoreColor(d.readiness_score).padStart(6)} ` + `${scoreColor(d.activity_score).padStart(9)} ${String(d.steps ?? "\u2014").padStart(7)} ${(d.stress ?? "\u2014").padEnd(10)}`);
@@ -1762,32 +1930,236 @@ function formatStats(stats, format) {
1762
1930
  `);
1763
1931
  }
1764
1932
 
1765
- // src/api/client.ts
1933
+ // src/commands/run-command.ts
1934
+ init_source();
1935
+
1936
+ // src/db/open.ts
1937
+ import { Database } from "bun:sqlite";
1938
+ import { resolve as resolve2, dirname as dirname2 } from "path";
1939
+ import { homedir as homedir2 } from "os";
1940
+ import { mkdirSync as mkdirSync2 } from "fs";
1941
+
1942
+ // src/db/migrations.ts
1943
+ var MIGRATIONS = [
1944
+ {
1945
+ version: 1,
1946
+ sql: `
1947
+ CREATE TABLE IF NOT EXISTS daily_sleep (
1948
+ id TEXT PRIMARY KEY,
1949
+ day TEXT UNIQUE,
1950
+ score INTEGER,
1951
+ contributors TEXT,
1952
+ timestamp TEXT
1953
+ );
1954
+ CREATE TABLE IF NOT EXISTS daily_readiness (
1955
+ id TEXT PRIMARY KEY,
1956
+ day TEXT UNIQUE,
1957
+ score INTEGER,
1958
+ contributors TEXT,
1959
+ temperature_deviation REAL,
1960
+ temperature_trend_deviation REAL,
1961
+ timestamp TEXT
1962
+ );
1963
+ CREATE TABLE IF NOT EXISTS daily_activity (
1964
+ id TEXT PRIMARY KEY,
1965
+ day TEXT UNIQUE,
1966
+ score INTEGER,
1967
+ active_calories INTEGER,
1968
+ steps INTEGER,
1969
+ equivalent_walking_distance REAL,
1970
+ high_activity_time INTEGER,
1971
+ medium_activity_time INTEGER,
1972
+ low_activity_time INTEGER,
1973
+ sedentary_time INTEGER,
1974
+ total_calories INTEGER,
1975
+ target_calories INTEGER,
1976
+ contributors TEXT,
1977
+ timestamp TEXT
1978
+ );
1979
+ CREATE TABLE IF NOT EXISTS daily_spo2 (
1980
+ id TEXT PRIMARY KEY,
1981
+ day TEXT UNIQUE,
1982
+ spo2_average REAL,
1983
+ breathing_disturbance_index REAL
1984
+ );
1985
+ CREATE TABLE IF NOT EXISTS daily_stress (
1986
+ id TEXT PRIMARY KEY,
1987
+ day TEXT UNIQUE,
1988
+ day_summary TEXT,
1989
+ recovery_high INTEGER,
1990
+ stress_high INTEGER
1991
+ );
1992
+ CREATE TABLE IF NOT EXISTS heartrate (
1993
+ timestamp TEXT,
1994
+ bpm INTEGER,
1995
+ source TEXT,
1996
+ day TEXT
1997
+ );
1998
+ CREATE INDEX IF NOT EXISTS idx_heartrate_ts ON heartrate(timestamp);
1999
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_heartrate_unique ON heartrate(timestamp, source);
2000
+ CREATE INDEX IF NOT EXISTS idx_heartrate_day ON heartrate(day);
2001
+ CREATE TABLE IF NOT EXISTS vo2max (
2002
+ id TEXT PRIMARY KEY,
2003
+ day TEXT UNIQUE,
2004
+ vo2_max REAL,
2005
+ timestamp TEXT
2006
+ );
2007
+ CREATE TABLE IF NOT EXISTS cardiovascular_age (
2008
+ id TEXT PRIMARY KEY,
2009
+ day TEXT UNIQUE,
2010
+ vascular_age INTEGER
2011
+ );
2012
+ CREATE TABLE IF NOT EXISTS workouts (
2013
+ id TEXT PRIMARY KEY,
2014
+ day TEXT,
2015
+ activity TEXT,
2016
+ calories REAL,
2017
+ distance REAL,
2018
+ start_datetime TEXT,
2019
+ end_datetime TEXT,
2020
+ intensity TEXT,
2021
+ label TEXT,
2022
+ source TEXT
2023
+ );
2024
+ CREATE TABLE IF NOT EXISTS sleep_model (
2025
+ id TEXT PRIMARY KEY,
2026
+ day TEXT,
2027
+ average_breath REAL,
2028
+ average_heart_rate REAL,
2029
+ average_hrv REAL,
2030
+ awake_time INTEGER,
2031
+ bedtime_end TEXT,
2032
+ bedtime_start TEXT,
2033
+ deep_sleep_duration INTEGER,
2034
+ efficiency INTEGER,
2035
+ latency INTEGER,
2036
+ light_sleep_duration INTEGER,
2037
+ lowest_heart_rate INTEGER,
2038
+ period INTEGER,
2039
+ rem_sleep_duration INTEGER,
2040
+ restless_periods INTEGER,
2041
+ time_in_bed INTEGER,
2042
+ total_sleep_duration INTEGER,
2043
+ type TEXT
2044
+ );
2045
+ `
2046
+ },
2047
+ {
2048
+ version: 2,
2049
+ sql: `
2050
+ CREATE VIEW IF NOT EXISTS v_weekly_sleep AS
2051
+ SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score, COUNT(*) as days
2052
+ FROM daily_sleep WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
2053
+
2054
+ CREATE VIEW IF NOT EXISTS v_weekly_readiness AS
2055
+ SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
2056
+ ROUND(AVG(temperature_deviation),2) as avg_temp_dev, COUNT(*) as days
2057
+ FROM daily_readiness WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
2058
+
2059
+ CREATE VIEW IF NOT EXISTS v_weekly_activity AS
2060
+ SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
2061
+ SUM(steps) as total_steps, SUM(active_calories) as total_active_cal, COUNT(*) as days
2062
+ FROM daily_activity WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
2063
+
2064
+ CREATE VIEW IF NOT EXISTS v_sleep_detail AS
2065
+ SELECT day, ROUND(total_sleep_duration/3600.0,1) as sleep_hours,
2066
+ ROUND(deep_sleep_duration/3600.0,1) as deep_hours,
2067
+ ROUND(rem_sleep_duration/3600.0,1) as rem_hours,
2068
+ average_hrv, average_heart_rate as avg_hr, lowest_heart_rate as lowest_hr, efficiency
2069
+ FROM sleep_model ORDER BY day DESC;
2070
+ `
2071
+ }
2072
+ ];
2073
+
2074
+ // src/db/open.ts
2075
+ function getDbPath(explicit) {
2076
+ if (explicit)
2077
+ return explicit;
2078
+ if (process.env.OURA_DB_PATH)
2079
+ return process.env.OURA_DB_PATH;
2080
+ return resolve2(homedir2(), ".oura-cli", "oura.db");
2081
+ }
2082
+ function openDatabase(explicit) {
2083
+ const dbPath = getDbPath(explicit);
2084
+ if (dbPath !== ":memory:")
2085
+ mkdirSync2(dirname2(dbPath), { recursive: true });
2086
+ const db = new Database(dbPath);
2087
+ db.exec("PRAGMA journal_mode = WAL");
2088
+ db.exec("PRAGMA foreign_keys = ON");
2089
+ return db;
2090
+ }
2091
+ function schemaVersion(db) {
2092
+ db.exec("CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)");
2093
+ const row = db.query("SELECT MAX(version) AS v FROM _schema_version").get();
2094
+ return row?.v ?? 0;
2095
+ }
2096
+ function ensureSchema(db, migrations = MIGRATIONS) {
2097
+ const current = schemaVersion(db);
2098
+ for (const m of migrations) {
2099
+ if (m.version > current) {
2100
+ db.exec(m.sql);
2101
+ db.query("INSERT INTO _schema_version (version) VALUES (?)").run(m.version);
2102
+ }
2103
+ }
2104
+ }
2105
+
2106
+ // src/api/token.ts
1766
2107
  import { readFileSync } from "fs";
1767
2108
  import { resolve as resolve3 } from "path";
1768
2109
  import { homedir as homedir3 } from "os";
2110
+ function defaultTokenPath() {
2111
+ return process.env.OURA_TOKEN_PATH ?? resolve3(homedir3(), ".oura-token");
2112
+ }
2113
+ function resolveToken(explicit, tokenPath) {
2114
+ if (explicit)
2115
+ return { token: explicit.trim(), source: "--token" };
2116
+ if (process.env.OURA_TOKEN)
2117
+ return { token: process.env.OURA_TOKEN.trim(), source: "OURA_TOKEN" };
2118
+ const path = tokenPath ?? defaultTokenPath();
2119
+ try {
2120
+ return { token: readFileSync(path, "utf-8").trim(), source: path };
2121
+ } catch {
2122
+ return { token: null, source: path };
2123
+ }
2124
+ }
2125
+
2126
+ // src/api/client.ts
1769
2127
  var BASE_URL = "https://api.ouraring.com/v2/usercollection";
2128
+ var MAX_PAGES = 1e4;
1770
2129
 
1771
2130
  class OuraClient {
1772
2131
  token;
1773
2132
  constructor(options = {}) {
1774
- const direct = options.token ?? process.env.OURA_TOKEN;
1775
- if (direct) {
1776
- this.token = direct.trim();
1777
- } else {
1778
- const tokenPath = options.tokenPath ?? process.env.OURA_TOKEN_PATH ?? resolve3(homedir3(), ".oura-token");
1779
- try {
1780
- this.token = readFileSync(tokenPath, "utf-8").trim();
1781
- } catch {
1782
- throw new CliError("TOKEN_MISSING", `No Oura access token at ${tokenPath}.`, "Run `oura-cli login` or set OURA_TOKEN.");
1783
- }
2133
+ const { token, source } = resolveToken(options.token, options.tokenPath);
2134
+ if (!token) {
2135
+ throw new CliError("TOKEN_MISSING", `No Oura access token at ${source}.`, "Run `oura-cli login` or set OURA_TOKEN.");
1784
2136
  }
2137
+ this.token = token;
2138
+ }
2139
+ async fetch(endpoint, query) {
2140
+ const rows = [];
2141
+ const seenTokens = new Set;
2142
+ let nextToken = null;
2143
+ do {
2144
+ if (seenTokens.size >= MAX_PAGES) {
2145
+ throw new CliError("API_ERROR", `Oura API returned more than ${MAX_PAGES} pages for ${endpoint}; stopping.`);
2146
+ }
2147
+ const params = new URLSearchParams(query);
2148
+ if (nextToken)
2149
+ params.set("next_token", nextToken);
2150
+ const page = await this.getPage(`${BASE_URL}/${endpoint}?${params}`);
2151
+ for (const row of page.data)
2152
+ rows.push(row);
2153
+ nextToken = page.next_token;
2154
+ if (nextToken && seenTokens.has(nextToken)) {
2155
+ throw new CliError("API_ERROR", `Oura API repeated pagination token for ${endpoint}; stopping to avoid a loop.`);
2156
+ }
2157
+ if (nextToken)
2158
+ seenTokens.add(nextToken);
2159
+ } while (nextToken);
2160
+ return rows;
1785
2161
  }
1786
- async fetch(endpoint, startDate, endDate) {
1787
- const params = new URLSearchParams({ start_date: startDate });
1788
- if (endDate)
1789
- params.set("end_date", endDate);
1790
- const url = `${BASE_URL}/${endpoint}?${params}`;
2162
+ async getPage(url) {
1791
2163
  const response = await fetch(url, {
1792
2164
  headers: { Authorization: `Bearer ${this.token}` }
1793
2165
  });
@@ -1806,59 +2178,11 @@ class OuraClient {
1806
2178
  } catch {
1807
2179
  throw new CliError("API_ERROR", "Empty response body from Oura API.");
1808
2180
  }
1809
- return json.data ?? [];
2181
+ const body = json;
2182
+ return { data: body.data ?? [], next_token: body.next_token ?? null };
1810
2183
  }
1811
2184
  }
1812
2185
 
1813
- // src/lib/time.ts
1814
- function nowUtc() {
1815
- return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
1816
- }
1817
- function formatLocal(utcStr, timezone) {
1818
- const dt = new Date(utcStr);
1819
- const parts = new Intl.DateTimeFormat("sv-SE", {
1820
- timeZone: timezone,
1821
- year: "numeric",
1822
- month: "2-digit",
1823
- day: "2-digit",
1824
- hour: "2-digit",
1825
- minute: "2-digit",
1826
- hour12: false
1827
- }).formatToParts(dt);
1828
- const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
1829
- return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
1830
- }
1831
- function formatLocalDate(utcStr, timezone) {
1832
- return formatLocal(utcStr, timezone).split(" ")[0];
1833
- }
1834
- function todayLocal(timezone) {
1835
- return formatLocalDate(nowUtc(), timezone);
1836
- }
1837
- function resolveDefaultTimezone() {
1838
- if (process.env.OURA_TZ)
1839
- return process.env.OURA_TZ;
1840
- try {
1841
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
1842
- } catch {
1843
- return "UTC";
1844
- }
1845
- }
1846
-
1847
- // src/commands/helpers.ts
1848
- function getClient(opts) {
1849
- return new OuraClient(opts.token ? { token: opts.token } : {});
1850
- }
1851
- function todayDate(timezone) {
1852
- return todayLocal(timezone ?? resolveDefaultTimezone());
1853
- }
1854
- function dateRange(days, timezone) {
1855
- const tz = timezone ?? resolveDefaultTimezone();
1856
- const end = todayLocal(tz);
1857
- const startMs = new Date(`${end}T00:00:00Z`).getTime() - (days - 1) * 86400000;
1858
- const start = new Date(startMs).toISOString().slice(0, 10);
1859
- return { start, end };
1860
- }
1861
-
1862
2186
  // src/lib/format-resolve.ts
1863
2187
  function resolveFormat({ explicit, isTty }) {
1864
2188
  if (explicit === undefined)
@@ -1868,341 +2192,146 @@ function resolveFormat({ explicit, isTty }) {
1868
2192
  throw new CliError("BAD_ARGS", `Unknown --format value: "${explicit}". Use "table" or "json".`);
1869
2193
  }
1870
2194
 
1871
- // src/commands/common.ts
1872
- var commonArgs = {
1873
- format: { type: "string", description: "Output format: table | json (auto-detected by TTY)" },
1874
- token: { type: "string", description: "Inline access token (prefer env vars or `oura-cli login`)" },
1875
- db: { type: "string", description: "Path to SQLite database file (env: OURA_DB_PATH)" },
1876
- tz: { type: "string", description: "Display timezone (env: OURA_TZ; auto-detected)" },
1877
- "no-color": { type: "boolean", default: false, description: "Disable ANSI colors (also honors NO_COLOR env)" }
1878
- };
1879
- function handleError(err, args) {
1880
- const fmt = resolveFormat({
1881
- explicit: args.format,
1882
- isTty: process.stdout.isTTY === true
1883
- });
1884
- emitError(err, fmt);
1885
- process.exit(exitCodeFor(err));
1886
- }
1887
- function applyNoColor(args) {
1888
- if (args["no-color"] || process.env.NO_COLOR) {
1889
- Promise.resolve().then(() => (init_source(), exports_source)).then(({ default: chalk2 }) => {
1890
- chalk2.level = 0;
1891
- });
2195
+ // src/lib/validate.ts
2196
+ function assertCalendarDate(value, label) {
2197
+ if (!isCalendarDate(value)) {
2198
+ throw new CliError("BAD_ARGS", `${label} must be a real YYYY-MM-DD date, got "${value}".`);
1892
2199
  }
2200
+ return value;
1893
2201
  }
1894
-
1895
- // src/commands/sync.ts
1896
- async function runSync(opts) {
1897
- const format = resolveFormat({ explicit: opts.format, isTty: process.stdout.isTTY === true });
1898
- const dbPath = getDbPath2({ dbPath: opts.db });
1899
- mkdirSync3(dirname2(dbPath), { recursive: true });
1900
- const db = openDatabase2({ dbPath: opts.db });
1901
- ensureSchema2(db);
1902
- const client = getClient(opts);
1903
- const log = format === "table" ? console.log : undefined;
1904
- const importResult = await importDaily(db, client, log);
1905
- const today = getDaySummary(db, todayDate(opts.tz));
1906
- db.close();
1907
- if (format === "json") {
1908
- console.log(JSON.stringify({ import: importResult, today }, null, 2));
1909
- } else {
1910
- console.log(formatDaySummary(today, format));
2202
+ function assertPositiveInt(value, label) {
2203
+ const n = Number(value);
2204
+ if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(n) || n < 1) {
2205
+ throw new CliError("BAD_ARGS", `${label} must be a positive integer, got "${value}".`);
1911
2206
  }
2207
+ return n;
1912
2208
  }
1913
- var syncCommand = defineCommand({
1914
- meta: { name: "sync", description: "Import latest data from Oura API and return today's summary" },
1915
- args: { ...commonArgs },
1916
- async run({ args }) {
1917
- applyNoColor(args);
1918
- try {
1919
- await runSync({ format: args.format, db: args.db, token: args.token, tz: args.tz });
1920
- } catch (err) {
1921
- handleError(err, args);
1922
- }
2209
+ function assertTimezone(tz) {
2210
+ try {
2211
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
2212
+ return tz;
2213
+ } catch {
2214
+ throw new CliError("BAD_ARGS", `Unknown timezone "${tz}".`, "Use an IANA name such as Europe/Berlin (env: OURA_TZ, flag: --tz).");
1923
2215
  }
1924
- });
1925
-
1926
- // src/commands/db.ts
1927
- import { mkdirSync as mkdirSync4, unlinkSync } from "fs";
1928
- import { dirname as dirname3 } from "path";
1929
-
1930
- // src/db/csv-import.ts
1931
- import { readFileSync as readFileSync2, existsSync } from "fs";
1932
- import { join } from "path";
1933
- var CSV_DIR = join(process.env.HOME ?? "", "Documents/OpenClaw/projects/oura-ring/data/App Data");
1934
- function parseCSV(filename) {
1935
- const path = join(CSV_DIR, filename);
1936
- if (!existsSync(path))
1937
- return [];
1938
- const text = readFileSync2(path, "utf-8");
1939
- const lines = text.split(`
1940
- `).filter((l) => l.trim());
1941
- if (lines.length < 2)
1942
- return [];
1943
- const headers = lines[0].split(";");
1944
- return lines.slice(1).map((line) => {
1945
- const vals = line.split(";");
1946
- const row = {};
1947
- headers.forEach((h, i) => {
1948
- row[h] = vals[i] ?? "";
1949
- });
1950
- return row;
1951
- });
1952
- }
1953
- function num(v) {
1954
- if (!v || v === "")
1955
- return null;
1956
- const n = Number(v);
1957
- return isNaN(n) ? null : n;
1958
2216
  }
1959
- function str(v) {
1960
- return v && v !== "" ? v : null;
1961
- }
1962
- function importFromCSV(db, log) {
1963
- if (!existsSync(CSV_DIR)) {
1964
- throw new Error(`CSV directory not found: ${CSV_DIR}`);
1965
- }
1966
- const sleep = parseCSV("dailysleep.csv");
1967
- const insertSleep = db.query("INSERT OR REPLACE INTO daily_sleep VALUES (?,?,?,?,?)");
1968
- const sleepTx = db.transaction(() => {
1969
- for (const r of sleep)
1970
- insertSleep.run(r.id, r.day, num(r.score), str(r.contributors), str(r.timestamp));
1971
- });
1972
- sleepTx();
1973
- log(`daily_sleep: ${sleep.length} rows`);
1974
- const readiness = parseCSV("dailyreadiness.csv");
1975
- const insertReadiness = db.query("INSERT OR REPLACE INTO daily_readiness VALUES (?,?,?,?,?,?,?)");
1976
- const readinessTx = db.transaction(() => {
1977
- for (const r of readiness)
1978
- insertReadiness.run(r.id, r.day, num(r.score), str(r.contributors), num(r.temperature_deviation), num(r.temperature_trend_deviation), str(r.timestamp));
1979
- });
1980
- readinessTx();
1981
- log(`daily_readiness: ${readiness.length} rows`);
1982
- const activity = parseCSV("dailyactivity.csv");
1983
- const insertActivity = db.query("INSERT OR REPLACE INTO daily_activity VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
1984
- const activityTx = db.transaction(() => {
1985
- for (const r of activity)
1986
- insertActivity.run(r.id, r.day, num(r.score), num(r.active_calories), num(r.steps), num(r.equivalent_walking_distance), num(r.high_activity_time), num(r.medium_activity_time), num(r.low_activity_time), num(r.sedentary_time), num(r.total_calories), num(r.target_calories), str(r.contributors), str(r.timestamp));
1987
- });
1988
- activityTx();
1989
- log(`daily_activity: ${activity.length} rows`);
1990
- const spo2 = parseCSV("dailyspo2.csv");
1991
- const insertSpo2 = db.query("INSERT OR REPLACE INTO daily_spo2 VALUES (?,?,?,?)");
1992
- const spo2Tx = db.transaction(() => {
1993
- for (const r of spo2) {
1994
- let avg = null;
1995
- try {
1996
- const parsed = JSON.parse(r.spo2_percentage);
1997
- avg = parsed?.average ?? null;
1998
- } catch {}
1999
- insertSpo2.run(r.id, r.day, avg, num(r.breathing_disturbance_index));
2217
+
2218
+ // src/commands/run-command.ts
2219
+ var processIo = {
2220
+ stdout: (s) => {
2221
+ process.stdout.write(s + `
2222
+ `);
2223
+ },
2224
+ stderr: (s) => {
2225
+ process.stderr.write(s + `
2226
+ `);
2227
+ },
2228
+ exit: (code) => process.exit(code),
2229
+ isTty: process.stdout.isTTY === true
2230
+ };
2231
+ async function execute(def, args, io = processIo) {
2232
+ if (args["no-color"] || process.env.NO_COLOR)
2233
+ source_default.level = 0;
2234
+ let db;
2235
+ let format = io.isTty ? "table" : "json";
2236
+ let exitCode = 0;
2237
+ try {
2238
+ format = resolveFormat({ explicit: args.format, isTty: io.isTty });
2239
+ const outputFormat = def.jsonOnly ? "json" : format;
2240
+ const tz = assertTimezone(args.tz ?? resolveDefaultTimezone());
2241
+ const ctx = { format: outputFormat, tz, today: today(tz) };
2242
+ if (def.needs?.db) {
2243
+ db = openDatabase(args.db);
2244
+ ensureSchema(db);
2245
+ ctx.db = db;
2000
2246
  }
2001
- });
2002
- spo2Tx();
2003
- log(`daily_spo2: ${spo2.length} rows`);
2004
- const stress = parseCSV("dailystress.csv");
2005
- const insertStress = db.query("INSERT OR REPLACE INTO daily_stress VALUES (?,?,?,?,?)");
2006
- const stressTx = db.transaction(() => {
2007
- for (const r of stress)
2008
- insertStress.run(r.id, r.day, str(r.day_summary), num(r.recovery_high), num(r.stress_high));
2009
- });
2010
- stressTx();
2011
- log(`daily_stress: ${stress.length} rows`);
2012
- const hr = parseCSV("heartrate.csv");
2013
- const insertHr = db.query("INSERT OR IGNORE INTO heartrate VALUES (?,?,?,?)");
2014
- const hrTx = db.transaction(() => {
2015
- for (const r of hr) {
2016
- const day = r.timestamp?.slice(0, 10) ?? null;
2017
- insertHr.run(r.timestamp, num(r.bpm), str(r.source), day);
2247
+ if (def.needs?.client) {
2248
+ ctx.client = new OuraClient(args.token ? { token: args.token } : {});
2018
2249
  }
2250
+ const out = await def.run(ctx, args);
2251
+ io.stdout(outputFormat === "json" ? JSON.stringify(out.json, null, 2) : out.text());
2252
+ exitCode = out.exitCode ?? 0;
2253
+ } catch (err) {
2254
+ io.stderr(formatError(err, format).text);
2255
+ exitCode = exitCodeFor(err);
2256
+ } finally {
2257
+ db?.close();
2258
+ }
2259
+ if (exitCode !== 0)
2260
+ io.exit(exitCode);
2261
+ }
2262
+ function dataCommand(def) {
2263
+ return defineCommand({
2264
+ meta: def.meta,
2265
+ args: { ...commonArgs, ...def.args ?? {} },
2266
+ run: ({ args }) => execute(def, args, processIo)
2019
2267
  });
2020
- hrTx();
2021
- log(`heartrate: ${hr.length} rows`);
2022
- const sleepModel = parseCSV("sleepmodel.csv");
2023
- const insertSM = db.query("INSERT OR REPLACE INTO sleep_model VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
2024
- const smTx = db.transaction(() => {
2025
- for (const r of sleepModel)
2026
- insertSM.run(r.id, r.day, num(r.average_breath), num(r.average_heart_rate), num(r.average_hrv), num(r.awake_time), str(r.bedtime_end), str(r.bedtime_start), num(r.deep_sleep_duration), num(r.efficiency), num(r.latency), num(r.light_sleep_duration), num(r.lowest_heart_rate), num(r.period), num(r.rem_sleep_duration), num(r.restless_periods), num(r.time_in_bed), num(r.total_sleep_duration), str(r.type));
2027
- });
2028
- smTx();
2029
- log(`sleep_model: ${sleepModel.length} rows`);
2030
- const vo2 = parseCSV("vo2max.csv");
2031
- const insertVo2 = db.query("INSERT OR REPLACE INTO vo2max VALUES (?,?,?,?)");
2032
- const vo2Tx = db.transaction(() => {
2033
- for (const r of vo2)
2034
- insertVo2.run(r.id, r.day, num(r.vo2_max), str(r.timestamp));
2035
- });
2036
- vo2Tx();
2037
- log(`vo2max: ${vo2.length} rows`);
2038
- const cv = parseCSV("dailycardiovascularage.csv");
2039
- const insertCv = db.query("INSERT OR REPLACE INTO cardiovascular_age VALUES (?,?,?)");
2040
- const cvTx = db.transaction(() => {
2041
- for (const r of cv)
2042
- insertCv.run(r.id, r.day, num(r.vascular_age));
2043
- });
2044
- cvTx();
2045
- log(`cardiovascular_age: ${cv.length} rows`);
2046
- const workouts = parseCSV("workout.csv");
2047
- const insertW = db.query("INSERT OR REPLACE INTO workouts VALUES (?,?,?,?,?,?,?,?,?,?)");
2048
- const wTx = db.transaction(() => {
2049
- for (const r of workouts)
2050
- insertW.run(r.id, r.day, str(r.activity), num(r.calories), num(r.distance), str(r.start_datetime), str(r.end_datetime), str(r.intensity), str(r.label), str(r.source));
2051
- });
2052
- wTx();
2053
- log(`workouts: ${workouts.length} rows`);
2054
- log("CSV import complete.");
2055
2268
  }
2056
2269
 
2270
+ // src/commands/sync.ts
2271
+ async function runSync(ctx) {
2272
+ const lines = [];
2273
+ const log = ctx.format === "table" ? (m) => lines.push(m) : undefined;
2274
+ const importResult = await importDaily(ctx.db, ctx.client, { today: ctx.today, tz: ctx.tz }, log);
2275
+ const today = getDaySummary(ctx.db, ctx.today);
2276
+ return {
2277
+ json: { import: importResult, today },
2278
+ text: () => [...lines, formatImportSummary(importResult), formatDaySummary(today, "table")].join(`
2279
+ `)
2280
+ };
2281
+ }
2282
+ var syncCommand = dataCommand({
2283
+ meta: { name: "sync", description: "Import latest data from Oura API and return today's summary" },
2284
+ needs: { db: true, client: true },
2285
+ run: runSync
2286
+ });
2287
+
2057
2288
  // src/commands/db.ts
2289
+ var SYNC_HINT = "Run `oura-cli sync` to download your data. Oura publishes a day's summary after that night's sleep syncs from the ring.";
2058
2290
  var dbCommand = defineCommand({
2059
2291
  meta: { name: "db", description: "Query and manage the local SQLite database" },
2060
2292
  subCommands: {
2061
- import: defineCommand({
2062
- meta: { name: "import", description: "Sync new data from Oura API into local database (alias of sync)" },
2063
- args: { ...commonArgs },
2064
- async run({ args }) {
2065
- applyNoColor(args);
2066
- try {
2067
- await runSync({ format: args.format, db: args.db, token: args.token, tz: args.tz });
2068
- } catch (err) {
2069
- handleError(err, args);
2070
- }
2071
- }
2072
- }),
2073
- today: defineCommand({
2293
+ today: dataCommand({
2074
2294
  meta: { name: "today", description: "Today's summary from local database" },
2075
- args: { ...commonArgs },
2076
- run({ args }) {
2077
- applyNoColor(args);
2078
- try {
2079
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2080
- const db = openDatabase2({ dbPath: args.db });
2081
- ensureSchema2(db);
2082
- const summary = getDaySummary(db, todayDate(args.tz));
2083
- console.log(formatDaySummary(summary, format));
2084
- db.close();
2085
- } catch (err) {
2086
- handleError(err, args);
2087
- }
2295
+ needs: { db: true },
2296
+ run(ctx) {
2297
+ const summary = getDaySummary(ctx.db, ctx.today);
2298
+ return { json: summary, text: () => formatDaySummary(summary, "table", SYNC_HINT) };
2088
2299
  }
2089
2300
  }),
2090
- date: defineCommand({
2301
+ date: dataCommand({
2091
2302
  meta: { name: "date", description: "Summary for specific date from local database" },
2092
- args: {
2093
- ...commonArgs,
2094
- day: { type: "positional", required: true, description: "Target date (YYYY-MM-DD)" }
2095
- },
2096
- run({ args }) {
2097
- applyNoColor(args);
2098
- try {
2099
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2100
- const db = openDatabase2({ dbPath: args.db });
2101
- ensureSchema2(db);
2102
- const summary = getDaySummary(db, args.day);
2103
- console.log(formatDaySummary(summary, format));
2104
- db.close();
2105
- } catch (err) {
2106
- handleError(err, args);
2107
- }
2303
+ args: { day: { type: "positional", required: true, description: "Target date (YYYY-MM-DD)" } },
2304
+ needs: { db: true },
2305
+ run(ctx, args) {
2306
+ const day = assertCalendarDate(String(args.day), "<day>");
2307
+ const summary = getDaySummary(ctx.db, day);
2308
+ return { json: summary, text: () => formatDaySummary(summary, "table") };
2108
2309
  }
2109
2310
  }),
2110
- week: defineCommand({
2311
+ week: dataCommand({
2111
2312
  meta: { name: "week", description: "Last 7 days from local database" },
2112
- args: { ...commonArgs },
2113
- run({ args }) {
2114
- applyNoColor(args);
2115
- try {
2116
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2117
- const db = openDatabase2({ dbPath: args.db });
2118
- ensureSchema2(db);
2119
- const days = [];
2120
- for (let i = 6;i >= 0; i--) {
2121
- const d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
2122
- days.push(getDaySummary(db, d));
2123
- }
2124
- console.log(formatWeekTable(days, format));
2125
- db.close();
2126
- } catch (err) {
2127
- handleError(err, args);
2128
- }
2313
+ needs: { db: true },
2314
+ run(ctx) {
2315
+ const days = daysBack(ctx.today, 7).map((d) => getDaySummary(ctx.db, d));
2316
+ return { json: days, text: () => formatWeekTable(days, "table", "Run `oura-cli sync`, then `oura-cli db week` again.") };
2129
2317
  }
2130
2318
  }),
2131
- trends: defineCommand({
2319
+ trends: dataCommand({
2132
2320
  meta: { name: "trends", description: "Score and metric trends over N days (default: 30)" },
2133
- args: {
2134
- ...commonArgs,
2135
- days: { type: "positional", required: false, description: "Window size in days (default: 30)" }
2136
- },
2137
- run({ args }) {
2138
- applyNoColor(args);
2139
- try {
2140
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2141
- const n = args.days ? parseInt(args.days, 10) : 30;
2142
- const db = openDatabase2({ dbPath: args.db });
2143
- ensureSchema2(db);
2144
- const trends = getTrends(db, n);
2145
- console.log(formatTrends(trends, n, format));
2146
- db.close();
2147
- } catch (err) {
2148
- handleError(err, args);
2149
- }
2321
+ args: { days: { type: "positional", required: false, description: "Window size in days (default: 30)" } },
2322
+ needs: { db: true },
2323
+ run(ctx, args) {
2324
+ const n = args.days === undefined ? 30 : assertPositiveInt(String(args.days), "<days>");
2325
+ const trends = getTrends(ctx.db, n, ctx.today);
2326
+ return { json: trends, text: () => formatTrends(trends, n, "table") };
2150
2327
  }
2151
2328
  }),
2152
- stats: defineCommand({
2329
+ stats: dataCommand({
2153
2330
  meta: { name: "stats", description: "Row counts, date range, and record highs from local database" },
2154
- args: { ...commonArgs },
2155
- run({ args }) {
2156
- applyNoColor(args);
2157
- try {
2158
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2159
- const db = openDatabase2({ dbPath: args.db });
2160
- ensureSchema2(db);
2161
- const stats = getStats(db);
2162
- console.log(formatStats(stats, format));
2163
- db.close();
2164
- } catch (err) {
2165
- handleError(err, args);
2166
- }
2167
- }
2168
- }),
2169
- reset: defineCommand({
2170
- meta: { name: "reset", description: "Destroy and rebuild database from exported CSV files" },
2171
- args: {
2172
- ...commonArgs,
2173
- force: { type: "boolean", default: false, description: "Confirm destructive reset" }
2174
- },
2175
- run({ args }) {
2176
- applyNoColor(args);
2177
- try {
2178
- if (!args.force) {
2179
- console.log(JSON.stringify({ error: "Use --force to confirm destructive reset." }));
2180
- process.exit(1);
2181
- }
2182
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2183
- const dbPath = getDbPath2({ dbPath: args.db });
2184
- try {
2185
- unlinkSync(dbPath);
2186
- } catch {}
2187
- try {
2188
- unlinkSync(dbPath + "-wal");
2189
- } catch {}
2190
- try {
2191
- unlinkSync(dbPath + "-shm");
2192
- } catch {}
2193
- const log = format === "table" ? console.log : undefined;
2194
- log?.("Database deleted.");
2195
- mkdirSync4(dirname3(dbPath), { recursive: true });
2196
- const db = openDatabase2({ dbPath: args.db });
2197
- ensureSchema2(db);
2198
- importFromCSV(db, log ?? (() => {}));
2199
- if (format === "json") {
2200
- console.log(JSON.stringify({ status: "reset complete" }));
2201
- }
2202
- db.close();
2203
- } catch (err) {
2204
- handleError(err, args);
2205
- }
2331
+ needs: { db: true },
2332
+ run(ctx) {
2333
+ const stats = getStats(ctx.db, ctx.today);
2334
+ return { json: stats, text: () => formatStats(stats, "table") };
2206
2335
  }
2207
2336
  })
2208
2337
  }
@@ -2217,17 +2346,15 @@ function dayLabel(dateStr) {
2217
2346
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
2218
2347
  return `${day} ${dd}/${mm}`;
2219
2348
  }
2220
- function getReport(db, days) {
2349
+ function getReport(db, days, today) {
2221
2350
  const period = days <= 7 ? "week" : "month";
2222
- const today = new Date;
2223
- const weekEnd = today.toISOString().slice(0, 10);
2224
- const weekStartDate = new Date(today.getTime() - (days - 1) * 86400000);
2225
- const weekStart = weekStartDate.toISOString().slice(0, 10);
2226
- const prevWeekEnd = new Date(today.getTime() - days * 86400000).toISOString().slice(0, 10);
2227
- const prevWeekStart = new Date(today.getTime() - (days * 2 - 1) * 86400000).toISOString().slice(0, 10);
2351
+ const weekEnd = today;
2352
+ const weekStart = shiftDay(today, -(days - 1));
2353
+ const prevWeekEnd = shiftDay(today, -days);
2354
+ const prevWeekStart = shiftDay(today, -(days * 2 - 1));
2228
2355
  const dailyRows = [];
2229
2356
  for (let i = days - 1;i >= 0; i--) {
2230
- const d = new Date(today.getTime() - i * 86400000).toISOString().slice(0, 10);
2357
+ const d = shiftDay(today, -i);
2231
2358
  const sl = db.query("SELECT score FROM daily_sleep WHERE day=?").get(d);
2232
2359
  const rd = db.query("SELECT score FROM daily_readiness WHERE day=?").get(d);
2233
2360
  const ac = db.query("SELECT score, steps FROM daily_activity WHERE day=?").get(d);
@@ -2295,7 +2422,7 @@ function getReport(db, days) {
2295
2422
  return { period, weekStart, weekEnd, days: dailyRows, averages, spo2, patterns: { lowSleep, lowReadiness, highActivity }, sleepDetails, recommendations };
2296
2423
  }
2297
2424
 
2298
- // src/format-report.ts
2425
+ // src/render/format-report.ts
2299
2426
  init_source();
2300
2427
  function colorizeScore(n) {
2301
2428
  if (n >= 85)
@@ -2365,6 +2492,14 @@ function formatReport(data, format, period) {
2365
2492
  }
2366
2493
  lines.push(source_default.gray(` ${data.weekStart} \u2014 ${data.weekEnd}`));
2367
2494
  lines.push("");
2495
+ const hasReportData = data.days.some((day) => day.sleep !== null || day.readiness !== null || day.activity !== null || day.steps !== null) || data.averages.length > 0 || data.spo2 !== null || data.sleepDetails !== null;
2496
+ if (!hasReportData) {
2497
+ lines.push(" No Oura data is available for this report yet.");
2498
+ lines.push(" Run `oura-cli sync` to download your data, then run `oura-cli report` again.");
2499
+ lines.push("");
2500
+ return lines.join(`
2501
+ `);
2502
+ }
2368
2503
  if (period === "week") {
2369
2504
  lines.push(source_default.bold(" Last 7 Days:"));
2370
2505
  lines.push(source_default.gray(" " + "\u2500".repeat(52)));
@@ -2436,29 +2571,17 @@ function formatReport(data, format, period) {
2436
2571
  }
2437
2572
 
2438
2573
  // src/commands/report.ts
2439
- var reportCommand = defineCommand({
2574
+ var reportCommand = dataCommand({
2440
2575
  meta: { name: "report", description: "Generate a narrative health report from local data." },
2441
- args: {
2442
- ...commonArgs,
2443
- period: { type: "string", description: "Report window: week | month", default: "week" }
2444
- },
2445
- run({ args }) {
2446
- applyNoColor(args);
2447
- try {
2448
- const period = args.period;
2449
- if (period !== "week" && period !== "month") {
2450
- throw new CliError("BAD_ARGS", `--period must be "week" or "month", got "${period}".`);
2451
- }
2452
- const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
2453
- const db = openDatabase2({ dbPath: args.db });
2454
- ensureSchema2(db);
2455
- const days = period === "week" ? 7 : 30;
2456
- const data = getReport(db, days);
2457
- console.log(formatReport(data, format, period));
2458
- db.close();
2459
- } catch (err) {
2460
- handleError(err, args);
2576
+ args: { period: { type: "string", description: "Report window: week | month", default: "week" } },
2577
+ needs: { db: true },
2578
+ run(ctx, args) {
2579
+ const period = args.period;
2580
+ if (period !== "week" && period !== "month") {
2581
+ throw new CliError("BAD_ARGS", `--period must be "week" or "month", got "${period}".`);
2461
2582
  }
2583
+ const data = getReport(ctx.db, period === "week" ? 7 : 30, ctx.today);
2584
+ return { json: data, text: () => formatReport(data, "table", period) };
2462
2585
  }
2463
2586
  });
2464
2587
 
@@ -2472,8 +2595,8 @@ function healthcheckCommand(version) {
2472
2595
  let ok = true;
2473
2596
  let error;
2474
2597
  try {
2475
- const db = openDatabase2({ dbPath: args.db });
2476
- ensureSchema2(db);
2598
+ const db = openDatabase(args.db);
2599
+ ensureSchema(db);
2477
2600
  db.query("SELECT 1").get();
2478
2601
  db.close();
2479
2602
  } catch (err) {
@@ -2485,96 +2608,209 @@ function healthcheckCommand(version) {
2485
2608
  });
2486
2609
  }
2487
2610
 
2611
+ // src/render/doctor-table.ts
2612
+ init_source();
2613
+ function statusSymbol(status) {
2614
+ if (status === "ok")
2615
+ return source_default.green("\u2713");
2616
+ if (status === "warn")
2617
+ return source_default.yellow("!");
2618
+ return source_default.red("\u2717");
2619
+ }
2620
+ function formatDoctorTable(result) {
2621
+ const lines = ["", source_default.bold(" Doctor"), source_default.gray("\u2500".repeat(50))];
2622
+ for (const c of result.checks) {
2623
+ lines.push(` ${statusSymbol(c.status)} ${c.id.padEnd(12)} ${c.detail}`);
2624
+ }
2625
+ lines.push("");
2626
+ const next = result.nextStep ?? (result.ok ? "nothing \u2014 everything looks healthy." : "see the failing checks above.");
2627
+ lines.push(` Next: ${next}`);
2628
+ return lines.join(`
2629
+ `);
2630
+ }
2631
+
2632
+ // src/commands/doctor.ts
2633
+ async function runChecks(deps) {
2634
+ const checks = [];
2635
+ const { token, source } = deps.resolveToken();
2636
+ if (token) {
2637
+ checks.push({ id: "token", status: "ok", detail: `Token found via ${source}.` });
2638
+ } else {
2639
+ checks.push({ id: "token", status: "fail", detail: `No token found (checked ${source}).`, fix: "oura-cli login" });
2640
+ }
2641
+ if (!token) {
2642
+ checks.push({ id: "token-valid", status: "fail", detail: "No token to validate.", fix: "oura-cli login" });
2643
+ } else if (deps.offline) {
2644
+ checks.push({ id: "token-valid", status: "ok", detail: "Skipped (--offline)." });
2645
+ } else {
2646
+ try {
2647
+ const client = deps.createClient(token);
2648
+ await client.fetch("daily_sleep", { start_date: deps.today, end_date: deps.today });
2649
+ checks.push({ id: "token-valid", status: "ok", detail: "Token accepted by the Oura API." });
2650
+ } catch (err) {
2651
+ if (err instanceof CliError && err.code === "TOKEN_INVALID") {
2652
+ checks.push({ id: "token-valid", status: "fail", detail: err.message, fix: "oura-cli login" });
2653
+ } else {
2654
+ const msg = err instanceof Error ? err.message : String(err);
2655
+ checks.push({ id: "token-valid", status: "warn", detail: `Could not reach the Oura API: ${msg}` });
2656
+ }
2657
+ }
2658
+ }
2659
+ let db = null;
2660
+ try {
2661
+ const opened = deps.openDb();
2662
+ db = opened.db;
2663
+ checks.push({ id: "database", status: "ok", detail: `Database ready at ${opened.path}.` });
2664
+ } catch (err) {
2665
+ const msg = err instanceof Error ? err.message : String(err);
2666
+ checks.push({ id: "database", status: "fail", detail: msg });
2667
+ }
2668
+ if (db) {
2669
+ const last = latestDataDay(db);
2670
+ if (!last) {
2671
+ checks.push({ id: "data", status: "warn", detail: "No data in the local cache yet.", fix: "oura-cli sync" });
2672
+ } else {
2673
+ const ageDays = Math.round((new Date(`${deps.today}T00:00:00Z`).getTime() - new Date(`${last}T00:00:00Z`).getTime()) / 86400000);
2674
+ if (ageDays > 2) {
2675
+ checks.push({ id: "data", status: "warn", detail: `Most recent data is from ${last} (${ageDays} days ago).`, fix: "oura-cli sync" });
2676
+ } else {
2677
+ checks.push({ id: "data", status: "ok", detail: `Data current through ${last}.` });
2678
+ }
2679
+ }
2680
+ } else {
2681
+ checks.push({ id: "data", status: "fail", detail: "Cannot check data \u2014 database unavailable." });
2682
+ }
2683
+ db?.close();
2684
+ const ok = checks.every((c) => c.status === "ok");
2685
+ const nextStep = checks.find((c) => c.status !== "ok")?.fix ?? null;
2686
+ return { ok, checks, nextStep };
2687
+ }
2688
+ var DATA_TABLES = ["daily_sleep", "daily_readiness", "daily_activity"];
2689
+ function latestDataDay(db) {
2690
+ let latest = null;
2691
+ for (const tbl of DATA_TABLES) {
2692
+ const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
2693
+ if (row?.d && (!latest || row.d > latest))
2694
+ latest = row.d;
2695
+ }
2696
+ return latest;
2697
+ }
2698
+ function exitCodeForChecks(checks) {
2699
+ const fail = checks.find((c) => c.status === "fail");
2700
+ if (!fail)
2701
+ return 0;
2702
+ if (fail.id === "token" || fail.id === "token-valid")
2703
+ return exitCodeFor(new CliError("TOKEN_MISSING", fail.detail));
2704
+ return exitCodeFor(new CliError("DB_ERROR", fail.detail));
2705
+ }
2706
+ var doctorCommand = dataCommand({
2707
+ meta: { name: "doctor", description: "Diagnose token, database, and sync health, and suggest the next step." },
2708
+ args: { offline: { type: "boolean", default: false, description: "Skip the live Oura API token-validation call" } },
2709
+ async run(ctx, args) {
2710
+ const deps = {
2711
+ resolveToken: () => resolveToken(args.token),
2712
+ openDb: () => {
2713
+ const db = openDatabase(args.db);
2714
+ ensureSchema(db);
2715
+ return { db, path: getDbPath(args.db) };
2716
+ },
2717
+ createClient: (token) => new OuraClient({ token }),
2718
+ offline: args.offline === true,
2719
+ today: ctx.today
2720
+ };
2721
+ const result = await runChecks(deps);
2722
+ return {
2723
+ json: result,
2724
+ text: () => formatDoctorTable(result),
2725
+ exitCode: exitCodeForChecks(result.checks)
2726
+ };
2727
+ }
2728
+ });
2729
+
2488
2730
  // src/commands/manifest.ts
2489
- function manifestCommand(version) {
2731
+ var EXAMPLES = {
2732
+ fetch: ["oura-cli fetch sleep", "oura-cli fetch hr --days 7", "oura-cli fetch workout --from 2026-05-01 --to 2026-05-31"],
2733
+ db: ["oura-cli db today", "oura-cli db week --format json"],
2734
+ report: ["oura-cli report --period week"],
2735
+ doctor: ["oura-cli doctor --offline"]
2736
+ };
2737
+ function buildOpenclawManifest(version, commands) {
2738
+ const m = buildManifest(version, commands);
2739
+ return {
2740
+ id: "oura-cli",
2741
+ version,
2742
+ runtime: "bun",
2743
+ bin: "oura-cli",
2744
+ description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.",
2745
+ commands: m.commands.map((c) => ({
2746
+ name: c.name,
2747
+ description: c.description,
2748
+ examples: EXAMPLES[c.name] ?? [`oura-cli ${c.name}`]
2749
+ })),
2750
+ envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH", "OURA_DB_PATH", "OURA_TZ"],
2751
+ healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number" } }
2752
+ };
2753
+ }
2754
+ function manifestCommand(version, getCommands) {
2490
2755
  return defineCommand({
2491
2756
  meta: { name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON." },
2492
2757
  args: {},
2493
2758
  run() {
2494
- console.log(JSON.stringify({
2495
- id: "oura-cli",
2496
- version,
2497
- runtime: "bun",
2498
- bin: "oura-cli",
2499
- description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.",
2500
- commands: [
2501
- { name: "login", description: "Save an Oura Personal Access Token.", examples: ["oura-cli login"] },
2502
- { name: "describe", description: "Emit a machine-readable manifest of commands.", examples: ["oura-cli describe"] },
2503
- { name: "sleep", description: "Fetch daily sleep scores from Oura API.", examples: ["oura-cli sleep --start 2026-05-01"] },
2504
- { name: "readiness", description: "Fetch daily readiness scores from Oura API.", examples: ["oura-cli readiness --start 2026-05-01"] },
2505
- { name: "activity", description: "Fetch daily activity scores from Oura API.", examples: ["oura-cli activity --start 2026-05-01"] },
2506
- { name: "hr", description: "Fetch heart rate samples from Oura API.", examples: ["oura-cli hr --start 2026-05-01"] },
2507
- { name: "spo2", description: "Fetch blood oxygen (SpO2) data from Oura API.", examples: ["oura-cli spo2 --start 2026-05-01"] },
2508
- { name: "stress", description: "Fetch daily stress data from Oura API.", examples: ["oura-cli stress --start 2026-05-01"] },
2509
- { name: "workout", description: "Fetch workout data from Oura API.", examples: ["oura-cli workout --start 2026-05-01"] },
2510
- { name: "sync", description: "Sync all Oura collections into the local DB.", examples: ["oura-cli sync"] },
2511
- { name: "db", description: "Query the local SQLite cache.", examples: ["oura-cli db today"] },
2512
- { name: "report", description: "Render a weekly or monthly summary report.", examples: ["oura-cli report --period week"] },
2513
- { name: "healthcheck", description: "Quick local DB health probe.", examples: ["oura-cli healthcheck"] },
2514
- { name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON.", examples: ["oura-cli manifest"] }
2515
- ],
2516
- envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH", "OURA_DB_PATH", "OURA_TZ"],
2517
- healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number" } }
2518
- }, null, 2));
2759
+ console.log(JSON.stringify(buildOpenclawManifest(version, getCommands()), null, 2));
2519
2760
  }
2520
2761
  });
2521
2762
  }
2522
2763
 
2523
- // src/commands/api-command.ts
2524
- function createApiCommand(name, description, endpoint) {
2525
- return defineCommand({
2526
- meta: { name, description },
2527
- subCommands: {
2528
- today: defineCommand({
2529
- meta: { name: "today", description: `Today's ${name} data` },
2530
- args: { ...commonArgs },
2531
- async run({ args }) {
2532
- applyNoColor(args);
2533
- try {
2534
- const client = getClient({ token: args.token });
2535
- const day = todayDate(args.tz);
2536
- const data = await client.fetch(endpoint, day, day);
2537
- console.log(JSON.stringify(data, null, 2));
2538
- } catch (err) {
2539
- handleError(err, args);
2540
- }
2541
- }
2542
- }),
2543
- date: defineCommand({
2544
- meta: { name: "date", description: `${name} data for a specific date (YYYY-MM-DD)` },
2545
- args: {
2546
- ...commonArgs,
2547
- day: { type: "positional", required: true, description: "Target date (YYYY-MM-DD)" }
2548
- },
2549
- async run({ args }) {
2550
- applyNoColor(args);
2551
- try {
2552
- const client = getClient({ token: args.token });
2553
- const data = await client.fetch(endpoint, args.day, args.day);
2554
- console.log(JSON.stringify(data, null, 2));
2555
- } catch (err) {
2556
- handleError(err, args);
2557
- }
2558
- }
2559
- }),
2560
- week: defineCommand({
2561
- meta: { name: "week", description: `Last 7 days of ${name} data` },
2562
- args: { ...commonArgs },
2563
- async run({ args }) {
2564
- applyNoColor(args);
2565
- try {
2566
- const client = getClient({ token: args.token });
2567
- const { start, end } = dateRange(7, args.tz);
2568
- const data = await client.fetch(endpoint, start, end);
2569
- console.log(JSON.stringify(data, null, 2));
2570
- } catch (err) {
2571
- handleError(err, args);
2572
- }
2573
- }
2574
- })
2575
- }
2576
- });
2577
- }
2764
+ // src/commands/fetch.ts
2765
+ function resolveRange(opts) {
2766
+ const modes = [opts.day !== undefined, opts.from !== undefined || opts.to !== undefined, opts.days !== undefined].filter(Boolean).length;
2767
+ if (modes > 1)
2768
+ throw new CliError("BAD_ARGS", "Use only one of --day, --from/--to, or --days.");
2769
+ if (opts.day !== undefined) {
2770
+ const d = assertCalendarDate(opts.day, "--day");
2771
+ return { start: d, end: d };
2772
+ }
2773
+ if (opts.from !== undefined || opts.to !== undefined) {
2774
+ if (opts.from === undefined || opts.to === undefined)
2775
+ throw new CliError("BAD_ARGS", "--from and --to must be given together.");
2776
+ const start = assertCalendarDate(opts.from, "--from");
2777
+ const end = assertCalendarDate(opts.to, "--to");
2778
+ if (start > end)
2779
+ throw new CliError("BAD_ARGS", `--from (${start}) must not be after --to (${end}).`);
2780
+ return { start, end };
2781
+ }
2782
+ if (opts.days !== undefined) {
2783
+ const n = assertPositiveInt(opts.days, "--days");
2784
+ return { start: shiftDay(opts.today, -(n - 1)), end: opts.today };
2785
+ }
2786
+ return { start: opts.today, end: opts.today };
2787
+ }
2788
+ var fetchCommand = dataCommand({
2789
+ meta: { name: "fetch", description: "Fetch raw records for one Oura collection straight from the API (JSON)." },
2790
+ args: {
2791
+ collection: { type: "positional", required: true, description: `Collection: ${names().join(" | ")}` },
2792
+ day: { type: "string", description: "Single day (YYYY-MM-DD). Default: today." },
2793
+ from: { type: "string", description: "Range start (YYYY-MM-DD); requires --to" },
2794
+ to: { type: "string", description: "Range end (YYYY-MM-DD); requires --from" },
2795
+ days: { type: "string", description: "Last N days ending today" }
2796
+ },
2797
+ jsonOnly: true,
2798
+ async run(ctx, args) {
2799
+ const c = byName(args.collection);
2800
+ if (!c)
2801
+ throw new CliError("BAD_ARGS", `Unknown collection "${args.collection}".`, `Valid collections: ${names().join(", ")}`);
2802
+ const { start, end } = resolveRange({
2803
+ day: args.day,
2804
+ from: args.from,
2805
+ to: args.to,
2806
+ days: args.days,
2807
+ today: ctx.today
2808
+ });
2809
+ const client = new OuraClient(args.token ? { token: args.token } : {});
2810
+ const data = await fetchCollection(client, c, start, end, ctx.tz);
2811
+ return { json: data, text: () => JSON.stringify(data, null, 2) };
2812
+ }
2813
+ });
2578
2814
 
2579
2815
  // src/lib/argv-normalize.ts
2580
2816
  var GLOBAL_FLAGS_WITH_VALUE = new Set(["--format", "--token", "--db", "--tz"]);
@@ -2583,14 +2819,9 @@ var SUBCOMMANDS = new Set([
2583
2819
  "login",
2584
2820
  "describe",
2585
2821
  "healthcheck",
2822
+ "doctor",
2586
2823
  "manifest",
2587
- "sleep",
2588
- "readiness",
2589
- "activity",
2590
- "hr",
2591
- "spo2",
2592
- "stress",
2593
- "workout",
2824
+ "fetch",
2594
2825
  "sync",
2595
2826
  "db",
2596
2827
  "report"
@@ -2631,10 +2862,21 @@ function normalizeArgv(argv) {
2631
2862
  }
2632
2863
 
2633
2864
  // src/index.ts
2634
- var VERSION = "0.4.4";
2865
+ var VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8")).version;
2635
2866
  if (process.argv.includes("--no-color") || process.env.NO_COLOR) {
2636
2867
  source_default.level = 0;
2637
2868
  }
2869
+ var subCommands = {
2870
+ login: loginCommand,
2871
+ describe: describeCommand(VERSION, () => subCommands),
2872
+ healthcheck: healthcheckCommand(VERSION),
2873
+ doctor: doctorCommand,
2874
+ manifest: manifestCommand(VERSION, () => subCommands),
2875
+ fetch: fetchCommand,
2876
+ sync: syncCommand,
2877
+ db: dbCommand,
2878
+ report: reportCommand
2879
+ };
2638
2880
  var main = defineCommand({
2639
2881
  meta: {
2640
2882
  name: "oura-cli",
@@ -2642,22 +2884,7 @@ var main = defineCommand({
2642
2884
  description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents."
2643
2885
  },
2644
2886
  args: { ...commonArgs },
2645
- subCommands: {
2646
- login: loginCommand,
2647
- describe: describeCommand(VERSION),
2648
- healthcheck: healthcheckCommand(VERSION),
2649
- manifest: manifestCommand(VERSION),
2650
- sleep: createApiCommand("sleep", "Fetch daily sleep scores from Oura API.", "daily_sleep"),
2651
- readiness: createApiCommand("readiness", "Fetch daily readiness scores from Oura API.", "daily_readiness"),
2652
- activity: createApiCommand("activity", "Fetch daily activity scores from Oura API.", "daily_activity"),
2653
- hr: createApiCommand("hr", "Fetch heart rate samples from Oura API.", "heartrate"),
2654
- spo2: createApiCommand("spo2", "Fetch blood oxygen (SpO2) data from Oura API.", "daily_spo2"),
2655
- stress: createApiCommand("stress", "Fetch daily stress data from Oura API.", "daily_stress"),
2656
- workout: createApiCommand("workout", "Fetch workout data from Oura API.", "workout"),
2657
- sync: syncCommand,
2658
- db: dbCommand,
2659
- report: reportCommand
2660
- }
2887
+ subCommands
2661
2888
  });
2662
2889
  var normalized = normalizeArgv(process.argv);
2663
2890
  runMain(main, { rawArgs: normalized.slice(2) });