@drakulavich/oura-cli 0.4.5 → 0.5.1

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);
@@ -1075,10 +1105,9 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
1075
1105
 
1076
1106
  // src/commands/login.ts
1077
1107
  init_source();
1078
- import { writeFileSync, chmodSync, mkdirSync } from "fs";
1079
- import { resolve, dirname } from "path";
1080
- import { homedir } from "os";
1081
- import { createInterface } from "readline/promises";
1108
+ import { writeFileSync, chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "fs";
1109
+ import { resolve as resolve3, dirname as dirname2 } from "path";
1110
+ import { homedir as homedir3 } from "os";
1082
1111
 
1083
1112
  // src/lib/errors.ts
1084
1113
  init_source();
@@ -1115,8 +1144,8 @@ function redactSecrets(s) {
1115
1144
  }
1116
1145
  function formatError(err, format) {
1117
1146
  const code = err instanceof CliError ? err.code : "UNKNOWN";
1118
- const message = err instanceof Error ? err.message : String(err);
1119
- const hint = err instanceof CliError ? err.hint : undefined;
1147
+ const message = redactSecrets(err instanceof Error ? err.message : String(err));
1148
+ const hint = err instanceof CliError && err.hint ? redactSecrets(err.hint) : undefined;
1120
1149
  if (format === "json") {
1121
1150
  return {
1122
1151
  kind: "json",
@@ -1128,235 +1157,30 @@ 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
 
1136
- // src/commands/login.ts
1137
- function writeToken(path, token) {
1138
- const trimmed = token.trim();
1139
- if (trimmed.length === 0) {
1140
- throw new CliError("BAD_ARGS", "Token cannot be empty.");
1141
- }
1142
- mkdirSync(dirname(path), { recursive: true });
1143
- writeFileSync(path, trimmed, { encoding: "utf-8" });
1144
- if (process.platform !== "win32") {
1145
- chmodSync(path, 384);
1146
- }
1147
- }
1148
- var loginCommand = defineCommand({
1149
- meta: { name: "login", description: "Save an Oura Personal Access Token for future commands." },
1150
- args: {
1151
- token: { type: "string", description: "Pass token non-interactively (e.g. for scripts)" },
1152
- 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)" }
1154
- },
1155
- 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();
1167
- }
1168
- writeToken(target, token);
1169
- console.log(source_default.green(`Saved to ${target}`));
1170
- }
1171
- });
1172
-
1173
- // src/commands/describe.ts
1174
- function buildManifest(version) {
1175
- return {
1176
- name: "oura-cli",
1177
- version,
1178
- compatManifestCommand: "oura-cli manifest",
1179
- auth: {
1180
- envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"],
1181
- tokenFile: "~/.oura-token",
1182
- loginCommand: "oura-cli login"
1183
- },
1184
- globalFlags: [
1185
- { name: "--format", type: "enum", values: ["table", "json"], description: "Output format (auto-detected by TTY when omitted)" },
1186
- { name: "--db", type: "string", description: "Override SQLite database path (env: OURA_DB_PATH)" },
1187
- { name: "--tz", type: "string", description: "Display timezone (env: OURA_TZ; default auto-detected)" },
1188
- { name: "--token", type: "string", description: "Inline access token (prefer env vars or `login`)" },
1189
- { name: "--no-color", type: "boolean", description: "Disable ANSI colors in human output" }
1190
- ],
1191
- exitCodes: [
1192
- { code: 0, meaning: "success" },
1193
- { code: 1, meaning: "user error (bad arguments)" },
1194
- { code: 2, meaning: "auth error (missing or invalid token)" },
1195
- { code: 3, meaning: "API or network error" },
1196
- { code: 4, meaning: "database or local storage error" }
1197
- ],
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
- ]
1304
- };
1305
- }
1306
- function describeCommand(version) {
1307
- return defineCommand({
1308
- meta: { name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs." },
1309
- args: {},
1310
- run() {
1311
- console.log(JSON.stringify(buildManifest(version), null, 2));
1312
- }
1313
- });
1314
- }
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
+ };
1315
1173
 
1316
- // src/commands/sync.ts
1317
- import { mkdirSync as mkdirSync3 } from "fs";
1318
- import { dirname as dirname2 } from "path";
1174
+ // src/commands/run-command.ts
1175
+ init_source();
1319
1176
 
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 });
1338
- }
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
- }
1356
- }
1357
- }
1177
+ // src/db/open.ts
1178
+ import { Database, SQLiteError } from "bun:sqlite";
1179
+ import { resolve, dirname } from "path";
1180
+ import { homedir } from "os";
1181
+ import { chmodSync, existsSync, mkdirSync } from "fs";
1358
1182
 
1359
- // src/db/schema.ts
1183
+ // src/db/migrations.ts
1360
1184
  var MIGRATIONS = [
1361
1185
  {
1362
1186
  version: 1,
@@ -1488,306 +1312,124 @@ CREATE VIEW IF NOT EXISTS v_sleep_detail AS
1488
1312
  }
1489
1313
  ];
1490
1314
 
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 });
1315
+ // src/db/open.ts
1316
+ var DB_HINT = "Check the path in --db / OURA_DB_PATH and that the file is a SQLite database oura-cli created.";
1317
+ var BUSY_HINT = "Another oura-cli process is using this database; wait for it to finish and retry.";
1318
+ var BUSY_TIMEOUT_MS = 5000;
1319
+ function dbError(what, err) {
1320
+ const detail = err instanceof Error ? err.message : String(err);
1321
+ const hint = /database is locked|SQLITE_BUSY/i.test(detail) ? BUSY_HINT : DB_HINT;
1322
+ return new CliError("DB_ERROR", `${what}: ${detail}`, hint);
1323
+ }
1324
+ function asDbError(err) {
1325
+ return err instanceof SQLiteError ? dbError("Database query failed", err) : undefined;
1326
+ }
1327
+ function getDbPath(explicit) {
1328
+ if (explicit)
1329
+ return explicit;
1330
+ if (process.env.OURA_DB_PATH)
1331
+ return process.env.OURA_DB_PATH;
1332
+ return resolve(homedir(), ".oura-cli", "oura.db");
1499
1333
  }
1500
- function openDatabase2(options = {}) {
1501
- return openDatabase({ ...DB_OPTIONS, ...options });
1334
+ function openDatabase(explicit) {
1335
+ const dbPath = getDbPath(explicit);
1336
+ try {
1337
+ const onDisk = dbPath !== ":memory:";
1338
+ const isNew = onDisk && !existsSync(dbPath);
1339
+ if (onDisk)
1340
+ mkdirSync(dirname(dbPath), { recursive: true, mode: 448 });
1341
+ const db = new Database(dbPath);
1342
+ if (isNew)
1343
+ chmodSync(dbPath, 384);
1344
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
1345
+ const mode = db.query("PRAGMA journal_mode").get();
1346
+ if (mode.journal_mode !== "wal")
1347
+ db.exec("PRAGMA journal_mode = WAL");
1348
+ db.exec("PRAGMA foreign_keys = ON");
1349
+ return db;
1350
+ } catch (err) {
1351
+ throw dbError(`Cannot open database ${dbPath}`, err);
1352
+ }
1353
+ }
1354
+ function schemaVersion(db) {
1355
+ db.exec("CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)");
1356
+ const row = db.query("SELECT MAX(version) AS v FROM _schema_version").get();
1357
+ return row?.v ?? 0;
1502
1358
  }
1503
- function ensureSchema2(db) {
1504
- ensureSchema(db, MIGRATIONS);
1359
+ function ensureSchema(db, migrations = MIGRATIONS) {
1360
+ try {
1361
+ const current = schemaVersion(db);
1362
+ for (const m of migrations) {
1363
+ if (m.version > current) {
1364
+ db.exec(m.sql);
1365
+ db.query("INSERT INTO _schema_version (version) VALUES (?)").run(m.version);
1366
+ }
1367
+ }
1368
+ } catch (err) {
1369
+ throw dbError("Schema migration failed", err);
1370
+ }
1505
1371
  }
1506
1372
 
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 };
1373
+ // src/api/token.ts
1374
+ import { readFileSync } from "fs";
1375
+ import { resolve as resolve2 } from "path";
1376
+ import { homedir as homedir2 } from "os";
1377
+ function defaultTokenPath() {
1378
+ return process.env.OURA_TOKEN_PATH ?? resolve2(homedir2(), ".oura-token");
1379
+ }
1380
+ function resolveToken(explicit, tokenPath) {
1381
+ if (explicit)
1382
+ return { token: explicit.trim(), source: "--token" };
1383
+ if (process.env.OURA_TOKEN)
1384
+ return { token: process.env.OURA_TOKEN.trim(), source: "OURA_TOKEN" };
1385
+ const path = tokenPath ?? defaultTokenPath();
1386
+ try {
1387
+ return { token: readFileSync(path, "utf-8").trim(), source: path };
1388
+ } catch {
1389
+ return { token: null, source: path };
1390
+ }
1588
1391
  }
1589
1392
 
1590
- // src/db/queries.ts
1591
- function getDaySummary(db, day) {
1592
- const sl = db.query("SELECT score FROM daily_sleep WHERE day=?").get(day);
1593
- const rd = db.query("SELECT score, temperature_deviation FROM daily_readiness WHERE day=?").get(day);
1594
- const ac = db.query("SELECT score, steps FROM daily_activity WHERE day=?").get(day);
1595
- const st = db.query("SELECT day_summary FROM daily_stress WHERE day=?").get(day);
1596
- const sp = db.query("SELECT spo2_average FROM daily_spo2 WHERE day=?").get(day);
1597
- const sm = db.query(`SELECT total_sleep_duration, deep_sleep_duration, rem_sleep_duration, average_hrv, lowest_heart_rate, efficiency FROM sleep_model WHERE day=? AND type='long_sleep'`).get(day);
1598
- return {
1599
- day,
1600
- sleep_score: sl?.score ?? null,
1601
- readiness_score: rd?.score ?? null,
1602
- activity_score: ac?.score ?? null,
1603
- steps: ac?.steps ?? null,
1604
- stress: st?.day_summary ?? null,
1605
- spo2: sp?.spo2_average ?? null,
1606
- temp_deviation: rd?.temperature_deviation ?? null,
1607
- sleep_hours: sm?.total_sleep_duration ? +(sm.total_sleep_duration / 3600).toFixed(1) : null,
1608
- deep_hours: sm?.deep_sleep_duration ? +(sm.deep_sleep_duration / 3600).toFixed(1) : null,
1609
- rem_hours: sm?.rem_sleep_duration ? +(sm.rem_sleep_duration / 3600).toFixed(1) : null,
1610
- avg_hrv: sm?.average_hrv ?? null,
1611
- lowest_hr: sm?.lowest_heart_rate ?? null,
1612
- efficiency: sm?.efficiency ?? null
1613
- };
1614
- }
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);
1618
- const results = [];
1619
- const metrics = [
1620
- ["Sleep Score", "daily_sleep", "score"],
1621
- ["Readiness", "daily_readiness", "score"],
1622
- ["Activity", "daily_activity", "score"],
1623
- ["Steps", "daily_activity", "steps"],
1624
- ["Active Cal", "daily_activity", "active_calories"]
1625
- ];
1626
- for (const [label, table, col] of metrics) {
1627
- const row = db.query(`SELECT AVG(${col}) as avg, MIN(${col}) as min, MAX(${col}) as max, COUNT(${col}) as count FROM ${table} WHERE day BETWEEN ? AND ?`).get(start, today);
1628
- if (row.count > 0 && row.avg !== null) {
1629
- results.push({ label, avg: +row.avg.toFixed(0), min: row.min, max: row.max, count: row.count });
1393
+ // src/api/client.ts
1394
+ var BASE_URL = "https://api.ouraring.com/v2/usercollection";
1395
+ var MAX_PAGES = 1e4;
1396
+
1397
+ class OuraClient {
1398
+ token;
1399
+ constructor(options = {}) {
1400
+ const { token, source } = resolveToken(options.token, options.tokenPath);
1401
+ if (!token) {
1402
+ throw new CliError("TOKEN_MISSING", `No Oura access token at ${source}.`, "Run `oura-cli login` or set OURA_TOKEN.");
1630
1403
  }
1404
+ if (/\s/.test(token)) {
1405
+ throw new CliError("TOKEN_INVALID", `The token from ${source} contains whitespace or a line break; a token is a single line.`, "Fix the file or variable, or run `oura-cli login` again.");
1406
+ }
1407
+ this.token = token;
1408
+ }
1409
+ async fetch(endpoint, query) {
1410
+ const rows = [];
1411
+ const seenTokens = new Set;
1412
+ let nextToken = null;
1413
+ do {
1414
+ if (seenTokens.size >= MAX_PAGES) {
1415
+ throw new CliError("API_ERROR", `Oura API returned more than ${MAX_PAGES} pages for ${endpoint}; stopping.`);
1416
+ }
1417
+ const params = new URLSearchParams(query);
1418
+ if (nextToken)
1419
+ params.set("next_token", nextToken);
1420
+ const page = await this.getPage(`${BASE_URL}/${endpoint}?${params}`);
1421
+ for (const row of page.data)
1422
+ rows.push(row);
1423
+ nextToken = page.next_token;
1424
+ if (nextToken && seenTokens.has(nextToken)) {
1425
+ throw new CliError("API_ERROR", `Oura API repeated pagination token for ${endpoint}; stopping to avoid a loop.`);
1426
+ }
1427
+ if (nextToken)
1428
+ seenTokens.add(nextToken);
1429
+ } while (nextToken);
1430
+ return rows;
1631
1431
  }
1632
- const sp = db.query("SELECT AVG(spo2_average) as avg, MIN(spo2_average) as min, MAX(spo2_average) as max, COUNT(*) as count FROM daily_spo2 WHERE day BETWEEN ? AND ?").get(start, today);
1633
- if (sp.count > 0 && sp.avg !== null) {
1634
- results.push({ label: "SpO2", avg: +sp.avg.toFixed(1), min: +sp.min.toFixed(1), max: +sp.max.toFixed(1), count: sp.count });
1635
- }
1636
- return results;
1637
- }
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 };
1654
- });
1655
- const range = db.query("SELECT MIN(day) as first, MAX(day) as last FROM daily_sleep").get();
1656
- const trends = getTrends(db, 99999);
1657
- const mostSteps = db.query("SELECT day, steps FROM daily_activity WHERE steps IS NOT NULL ORDER BY steps DESC LIMIT 1").get();
1658
- const bestSleep = db.query("SELECT day, score FROM daily_sleep WHERE score IS NOT NULL ORDER BY score DESC LIMIT 1").get();
1659
- return {
1660
- tables,
1661
- dateRange: range,
1662
- trends,
1663
- records: {
1664
- mostSteps: mostSteps ?? null,
1665
- bestSleep: bestSleep ?? null
1666
- }
1667
- };
1668
- }
1669
-
1670
- // src/format.ts
1671
- init_source();
1672
- function scoreColor(score) {
1673
- if (score === null)
1674
- return source_default.gray("\u2014");
1675
- if (score >= 85)
1676
- return source_default.green(String(score));
1677
- if (score >= 70)
1678
- return source_default.yellow(String(score));
1679
- return source_default.red(String(score));
1680
- }
1681
- function fmtHours(h) {
1682
- if (h === null)
1683
- return source_default.gray("\u2014");
1684
- return `${h}h`;
1685
- }
1686
- function formatDaySummary(summary, format) {
1687
- if (format === "json")
1688
- return JSON.stringify(summary, null, 2);
1689
- const lines = [
1690
- "",
1691
- source_default.bold(` ${summary.day}`),
1692
- source_default.gray("\u2500".repeat(50)),
1693
- ` Sleep: ${scoreColor(summary.sleep_score)} Readiness: ${scoreColor(summary.readiness_score)} Activity: ${scoreColor(summary.activity_score)}`,
1694
- ` Steps: ${summary.steps ?? source_default.gray("\u2014")}`
1695
- ];
1696
- if (summary.spo2 !== null)
1697
- lines.push(` SpO2: ${summary.spo2}%`);
1698
- if (summary.temp_deviation !== null) {
1699
- const sign = summary.temp_deviation >= 0 ? "+" : "";
1700
- lines.push(` Temp: ${sign}${summary.temp_deviation}\xB0C`);
1701
- }
1702
- if (summary.stress)
1703
- lines.push(` Stress: ${summary.stress}`);
1704
- if (summary.sleep_hours !== null) {
1705
- lines.push("");
1706
- lines.push(` Sleep: ${fmtHours(summary.sleep_hours)} total | ${fmtHours(summary.deep_hours)} deep | ${fmtHours(summary.rem_hours)} REM`);
1707
- lines.push(` HRV: ${summary.avg_hrv ?? "\u2014"} Lowest HR: ${summary.lowest_hr ?? "\u2014"} Efficiency: ${summary.efficiency ?? "\u2014"}%`);
1708
- }
1709
- return lines.join(`
1710
- `);
1711
- }
1712
- function formatWeekTable(days, format) {
1713
- if (format === "json")
1714
- return JSON.stringify(days, null, 2);
1715
- const header = `${"Day".padEnd(12)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Activity".padStart(9)} ${"Steps".padStart(7)} ${"Stress".padEnd(10)}`;
1716
- const sep = source_default.gray("\u2500".repeat(56));
1717
- 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)}`);
1718
- return [`
1719
- Last 7 Days`, sep, ` ${header}`, sep, ...rows.map((r) => ` ${r}`)].join(`
1720
- `);
1721
- }
1722
- function formatTrends(trends, days, format) {
1723
- if (format === "json")
1724
- return JSON.stringify(trends, null, 2);
1725
- const lines = [
1726
- "",
1727
- source_default.bold(` Trends: last ${days} days`),
1728
- source_default.gray("\u2500".repeat(50))
1729
- ];
1730
- for (const t of trends) {
1731
- lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)} (${t.count} days)`);
1732
- }
1733
- return lines.join(`
1734
- `);
1735
- }
1736
- function formatStats(stats, format) {
1737
- if (format === "json")
1738
- return JSON.stringify(stats, null, 2);
1739
- const lines = [
1740
- "",
1741
- source_default.bold(" Database Statistics"),
1742
- source_default.gray("\u2550".repeat(50))
1743
- ];
1744
- for (const t of stats.tables) {
1745
- lines.push(` ${t.table.padEnd(22)} ${String(t.rows).padStart(8)} rows`);
1746
- }
1747
- if (stats.dateRange.first) {
1748
- lines.push(`
1749
- Date range: ${stats.dateRange.first} \u2192 ${stats.dateRange.last}`);
1750
- }
1751
- for (const t of stats.trends) {
1752
- lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)}`);
1753
- }
1754
- if (stats.records.mostSteps) {
1755
- lines.push(`
1756
- Most steps: ${stats.records.mostSteps.steps} on ${stats.records.mostSteps.day}`);
1757
- }
1758
- if (stats.records.bestSleep) {
1759
- lines.push(` Best sleep: ${stats.records.bestSleep.score} on ${stats.records.bestSleep.day}`);
1760
- }
1761
- return lines.join(`
1762
- `);
1763
- }
1764
-
1765
- // src/api/client.ts
1766
- import { readFileSync } from "fs";
1767
- import { resolve as resolve3 } from "path";
1768
- import { homedir as homedir3 } from "os";
1769
- var BASE_URL = "https://api.ouraring.com/v2/usercollection";
1770
-
1771
- class OuraClient {
1772
- token;
1773
- 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
- }
1784
- }
1785
- }
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}`;
1432
+ async getPage(url) {
1791
1433
  const response = await fetch(url, {
1792
1434
  headers: { Authorization: `Bearer ${this.token}` }
1793
1435
  });
@@ -1806,7 +1448,102 @@ class OuraClient {
1806
1448
  } catch {
1807
1449
  throw new CliError("API_ERROR", "Empty response body from Oura API.");
1808
1450
  }
1809
- return json.data ?? [];
1451
+ const body = json;
1452
+ return { data: body.data ?? [], next_token: body.next_token ?? null };
1453
+ }
1454
+ }
1455
+
1456
+ // src/lib/argv-normalize.ts
1457
+ var GLOBAL_FLAGS_WITH_VALUE = new Set(["--format", "--token", "--db", "--tz"]);
1458
+ var GLOBAL_FLAGS_BOOLEAN = new Set(["--no-color"]);
1459
+ var SUBCOMMANDS = new Set([
1460
+ "login",
1461
+ "describe",
1462
+ "healthcheck",
1463
+ "doctor",
1464
+ "manifest",
1465
+ "fetch",
1466
+ "sync",
1467
+ "db",
1468
+ "report"
1469
+ ]);
1470
+ function normalizeArgv(argv) {
1471
+ const [bun, script, ...rest] = argv;
1472
+ const subIdx = rest.findIndex((a) => SUBCOMMANDS.has(a));
1473
+ if (subIdx < 0)
1474
+ return argv;
1475
+ const before = rest.slice(0, subIdx);
1476
+ const subcommandOnwards = rest.slice(subIdx);
1477
+ const hoisted = [];
1478
+ const leftover = [];
1479
+ for (let i = 0;i < before.length; i++) {
1480
+ const tok = before[i];
1481
+ if (tok.includes("=")) {
1482
+ const name = tok.slice(0, tok.indexOf("="));
1483
+ if (GLOBAL_FLAGS_WITH_VALUE.has(name) || GLOBAL_FLAGS_BOOLEAN.has(name)) {
1484
+ hoisted.push(tok);
1485
+ continue;
1486
+ }
1487
+ }
1488
+ if (GLOBAL_FLAGS_WITH_VALUE.has(tok)) {
1489
+ hoisted.push(tok);
1490
+ if (i + 1 < before.length) {
1491
+ hoisted.push(before[i + 1]);
1492
+ i++;
1493
+ }
1494
+ continue;
1495
+ }
1496
+ if (GLOBAL_FLAGS_BOOLEAN.has(tok)) {
1497
+ hoisted.push(tok);
1498
+ continue;
1499
+ }
1500
+ leftover.push(tok);
1501
+ }
1502
+ const dd = subcommandOnwards.indexOf("--");
1503
+ if (dd >= 0) {
1504
+ return [bun, script, ...leftover, ...subcommandOnwards.slice(0, dd), ...hoisted, ...subcommandOnwards.slice(dd)];
1505
+ }
1506
+ return [bun, script, ...leftover, ...subcommandOnwards, ...hoisted];
1507
+ }
1508
+ function isVersionRequest(rawArgs) {
1509
+ for (let i = 0;i < rawArgs.length; i++) {
1510
+ const tok = rawArgs[i];
1511
+ if (SUBCOMMANDS.has(tok))
1512
+ return false;
1513
+ if (GLOBAL_FLAGS_WITH_VALUE.has(tok)) {
1514
+ i++;
1515
+ continue;
1516
+ }
1517
+ if (tok === "--version" || tok === "-v")
1518
+ return true;
1519
+ }
1520
+ return false;
1521
+ }
1522
+
1523
+ // src/lib/format-resolve.ts
1524
+ function resolveFormat({ explicit, isTty }) {
1525
+ if (explicit === undefined)
1526
+ return isTty ? "table" : "json";
1527
+ if (explicit === "table" || explicit === "json")
1528
+ return explicit;
1529
+ throw new CliError("BAD_ARGS", `Unknown --format value: "${explicit}". Use "table" or "json".`);
1530
+ }
1531
+ function formatFromArgv(argv, isTty) {
1532
+ let explicit;
1533
+ for (let i = 0;i < argv.length; i++) {
1534
+ const a = argv[i];
1535
+ if (a === "--format") {
1536
+ explicit = argv[i + 1];
1537
+ i++;
1538
+ } else if (a.startsWith("--format="))
1539
+ explicit = a.slice("--format=".length);
1540
+ else if (GLOBAL_FLAGS_WITH_VALUE.has(a))
1541
+ i++;
1542
+ }
1543
+ try {
1544
+ return resolveFormat({ explicit, isTty });
1545
+ } catch {
1546
+ return isTty ? "table" : "json";
1810
1547
  }
1811
1548
  }
1812
1549
 
@@ -1834,6 +1571,26 @@ function formatLocalDate(utcStr, timezone) {
1834
1571
  function todayLocal(timezone) {
1835
1572
  return formatLocalDate(nowUtc(), timezone);
1836
1573
  }
1574
+ function getTimezoneOffsetMs(utc, tz) {
1575
+ const tzPart = new Intl.DateTimeFormat("en-US", {
1576
+ timeZone: tz,
1577
+ timeZoneName: "longOffset"
1578
+ }).formatToParts(utc).find((p) => p.type === "timeZoneName")?.value ?? "GMT";
1579
+ const m = tzPart.match(/GMT([+-])(\d{2}):(\d{2})/);
1580
+ if (!m)
1581
+ return 0;
1582
+ const sign = m[1] === "+" ? 1 : -1;
1583
+ return sign * (parseInt(m[2], 10) * 3600 + parseInt(m[3], 10) * 60) * 1000;
1584
+ }
1585
+ function localMidnightMs(day, tz) {
1586
+ const naiveMs = new Date(`${day}T00:00:00Z`).getTime();
1587
+ const guessMs = naiveMs - getTimezoneOffsetMs(new Date(naiveMs), tz);
1588
+ return naiveMs - getTimezoneOffsetMs(new Date(guessMs), tz);
1589
+ }
1590
+ function localDateToUtcRange(localDate, timezone) {
1591
+ const iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
1592
+ return [iso(localMidnightMs(localDate, timezone)), iso(localMidnightMs(shiftDay(localDate, 1), timezone))];
1593
+ }
1837
1594
  function resolveDefaultTimezone() {
1838
1595
  if (process.env.OURA_TZ)
1839
1596
  return process.env.OURA_TZ;
@@ -1843,366 +1600,905 @@ function resolveDefaultTimezone() {
1843
1600
  return "UTC";
1844
1601
  }
1845
1602
  }
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 };
1603
+ function today(timezone) {
1604
+ return todayLocal(timezone ?? resolveDefaultTimezone());
1605
+ }
1606
+ function shiftDay(day, delta) {
1607
+ const ms = new Date(`${day}T00:00:00Z`).getTime() + delta * 86400000;
1608
+ return new Date(ms).toISOString().slice(0, 10);
1609
+ }
1610
+ function daysBack(endDay, n) {
1611
+ const out = [];
1612
+ for (let i = n - 1;i >= 0; i--)
1613
+ out.push(shiftDay(endDay, -i));
1614
+ return out;
1615
+ }
1616
+ var CALENDAR_DATE = /^\d{4}-\d{2}-\d{2}$/;
1617
+ function isCalendarDate(value) {
1618
+ if (!CALENDAR_DATE.test(value))
1619
+ return false;
1620
+ const ms = new Date(`${value}T00:00:00Z`).getTime();
1621
+ return !Number.isNaN(ms) && new Date(ms).toISOString().slice(0, 10) === value;
1622
+ }
1623
+
1624
+ // src/lib/validate.ts
1625
+ function assertCalendarDate(value, label) {
1626
+ if (!isCalendarDate(value)) {
1627
+ throw new CliError("BAD_ARGS", `${label} must be a real YYYY-MM-DD date, got "${value}".`);
1628
+ }
1629
+ return value;
1630
+ }
1631
+ function assertPositiveInt(value, label) {
1632
+ const n = Number(value);
1633
+ if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(n) || n < 1) {
1634
+ throw new CliError("BAD_ARGS", `${label} must be a positive integer, got "${value}".`);
1635
+ }
1636
+ return n;
1637
+ }
1638
+ function assertTimezone(tz) {
1639
+ try {
1640
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
1641
+ return tz;
1642
+ } catch {
1643
+ throw new CliError("BAD_ARGS", `Unknown timezone "${tz}".`, "Use an IANA name such as Europe/Berlin (env: OURA_TZ, flag: --tz).");
1644
+ }
1645
+ }
1646
+
1647
+ // src/commands/run-command.ts
1648
+ var processIo = {
1649
+ stdout: (s) => {
1650
+ process.stdout.write(s + `
1651
+ `);
1652
+ },
1653
+ stderr: (s) => {
1654
+ process.stderr.write(s + `
1655
+ `);
1656
+ },
1657
+ exit: (code) => process.exit(code),
1658
+ isTty: process.stdout.isTTY === true
1659
+ };
1660
+ var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1661
+ function assertKnownArgs(declared, args) {
1662
+ const known = new Set(["_"]);
1663
+ let positionals = 0;
1664
+ for (const [name, def] of Object.entries(declared)) {
1665
+ known.add(name);
1666
+ known.add(camel(name));
1667
+ if (name.startsWith("no-"))
1668
+ known.add(name.slice(3));
1669
+ for (const alias of [def.alias ?? []].flat())
1670
+ known.add(alias);
1671
+ if (def.type === "positional")
1672
+ positionals++;
1673
+ }
1674
+ const unknown = Object.keys(args).filter((k) => !known.has(k));
1675
+ if (unknown.length > 0) {
1676
+ const flags = unknown.map((f) => f.length === 1 ? `-${f}` : `--${f}`).join(", ");
1677
+ const hint = unknown.every((f) => f.length === 1) ? 'oura-cli has no single-letter flags; a value that starts with "-" must come after "--".' : "Run the command with --help to see its flags.";
1678
+ throw new CliError("BAD_ARGS", `Unknown flag${unknown.length > 1 ? "s" : ""}: ${flags}.`, hint);
1679
+ }
1680
+ const extra = (args._ ?? []).slice(positionals);
1681
+ if (extra.length > 0) {
1682
+ throw new CliError("BAD_ARGS", `Unexpected argument${extra.length > 1 ? "s" : ""}: ${extra.join(" ")}.`, "Run the command with --help to see its arguments.");
1683
+ }
1684
+ }
1685
+ async function execute(def, args, io = processIo) {
1686
+ if (args["no-color"] === true || args.color === false || process.env.NO_COLOR)
1687
+ source_default.level = 0;
1688
+ let db;
1689
+ let format = io.isTty ? "table" : "json";
1690
+ let exitCode = 0;
1691
+ try {
1692
+ format = resolveFormat({ explicit: args.format, isTty: io.isTty });
1693
+ assertKnownArgs({ ...commonArgs, ...def.args ?? {} }, args);
1694
+ const outputFormat = def.jsonOnly ? "json" : format;
1695
+ const tz = assertTimezone(args.tz ?? resolveDefaultTimezone());
1696
+ const ctx = { format: outputFormat, tz, today: today(tz) };
1697
+ if (def.needs?.db) {
1698
+ db = openDatabase(args.db);
1699
+ ensureSchema(db);
1700
+ ctx.db = db;
1701
+ }
1702
+ if (def.needs?.client) {
1703
+ ctx.client = new OuraClient(args.token ? { token: args.token } : {});
1704
+ }
1705
+ const out = await def.run(ctx, args);
1706
+ io.stdout(outputFormat === "json" ? JSON.stringify(out.json, null, 2) : out.text());
1707
+ exitCode = out.exitCode ?? 0;
1708
+ } catch (raw) {
1709
+ const err = asDbError(raw) ?? raw;
1710
+ io.stderr(formatError(err, format).text);
1711
+ exitCode = exitCodeFor(err);
1712
+ } finally {
1713
+ db?.close();
1714
+ }
1715
+ if (exitCode !== 0)
1716
+ io.exit(exitCode);
1717
+ }
1718
+ function dataCommand(def) {
1719
+ return defineCommand({
1720
+ meta: def.meta,
1721
+ args: { ...commonArgs, ...def.args ?? {} },
1722
+ run: ({ args }) => execute(def, args, processIo)
1723
+ });
1724
+ }
1725
+
1726
+ // src/commands/login.ts
1727
+ async function readHiddenToken(input, output) {
1728
+ if (!input.isTTY || !input.setRawMode) {
1729
+ throw new CliError("BAD_ARGS", "Interactive login requires a terminal.", "Use `oura-cli login --token` for non-interactive use.");
1730
+ }
1731
+ output.write("Paste your token (input hidden): ");
1732
+ input.setRawMode(true);
1733
+ input.resume();
1734
+ return new Promise((resolve, reject) => {
1735
+ let token = "";
1736
+ const finish = () => {
1737
+ input.off("data", onData);
1738
+ input.setRawMode?.(false);
1739
+ input.pause();
1740
+ output.write(`
1741
+ `);
1742
+ };
1743
+ const onData = (chunk) => {
1744
+ for (const char of chunk.toString("utf8")) {
1745
+ if (char === "\r" || char === `
1746
+ `) {
1747
+ finish();
1748
+ resolve(token);
1749
+ return;
1750
+ }
1751
+ if (char === "\x03" || char === "\x04") {
1752
+ finish();
1753
+ reject(new CliError("BAD_ARGS", "Login cancelled."));
1754
+ return;
1755
+ }
1756
+ if (char === "\b" || char === "\x7F") {
1757
+ token = token.slice(0, -1);
1758
+ } else if (char >= " ") {
1759
+ token += char;
1760
+ }
1761
+ }
1762
+ };
1763
+ input.on("data", onData);
1764
+ });
1765
+ }
1766
+ function writeToken(path, token) {
1767
+ const trimmed = token.trim();
1768
+ if (trimmed.length === 0) {
1769
+ throw new CliError("BAD_ARGS", "Token cannot be empty.");
1770
+ }
1771
+ mkdirSync2(dirname2(path), { recursive: true });
1772
+ writeFileSync(path, trimmed, { encoding: "utf-8" });
1773
+ if (process.platform !== "win32") {
1774
+ chmodSync2(path, 384);
1775
+ }
1776
+ }
1777
+ var loginCommand = defineCommand({
1778
+ meta: { name: "login", description: "Save an Oura Personal Access Token for future commands." },
1779
+ args: {
1780
+ ...commonArgs,
1781
+ token: { type: "string", description: "Pass token non-interactively (e.g. for scripts)" },
1782
+ path: { type: "string", description: "Where to save the token (default: $OURA_TOKEN_PATH or ~/.oura-token)" }
1783
+ },
1784
+ async run({ args }) {
1785
+ try {
1786
+ assertKnownArgs({ ...commonArgs, token: {}, path: {} }, args);
1787
+ if (args["no-color"] || process.env.NO_COLOR) {
1788
+ await Promise.resolve().then(() => init_source());
1789
+ source_default.level = 0;
1790
+ }
1791
+ const target = args.path ?? process.env.OURA_TOKEN_PATH ?? resolve3(homedir3(), ".oura-token");
1792
+ let token = args.token;
1793
+ if (!token) {
1794
+ console.log("Get a Personal Access Token at https://cloud.ouraring.com/personal-access-tokens");
1795
+ token = await readHiddenToken(process.stdin, process.stdout);
1796
+ }
1797
+ writeToken(target, token);
1798
+ console.log(source_default.green(`Saved to ${target}`));
1799
+ } catch (err) {
1800
+ emitError(err, "table");
1801
+ process.exit(exitCodeFor(err));
1802
+ }
1803
+ }
1804
+ });
1805
+
1806
+ // src/collections/types.ts
1807
+ function defineCollection(c) {
1808
+ return c;
1809
+ }
1810
+
1811
+ // src/collections/sleep.ts
1812
+ var sleep = defineCollection({
1813
+ name: "sleep",
1814
+ endpoint: "daily_sleep",
1815
+ table: "daily_sleep",
1816
+ description: "Daily sleep score and contributors",
1817
+ conflict: "replace",
1818
+ rangeParams: "date",
1819
+ identity: [
1820
+ { field: "id", description: "Oura record id" },
1821
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1822
+ ],
1823
+ columns: [
1824
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1825
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1826
+ { name: "score", type: "INTEGER", pick: (r) => r.score },
1827
+ { name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
1828
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
1829
+ ]
1830
+ });
1831
+
1832
+ // src/collections/readiness.ts
1833
+ var readiness = defineCollection({
1834
+ name: "readiness",
1835
+ endpoint: "daily_readiness",
1836
+ table: "daily_readiness",
1837
+ description: "Daily readiness score, contributors and temperature deviation",
1838
+ conflict: "replace",
1839
+ rangeParams: "date",
1840
+ identity: [
1841
+ { field: "id", description: "Oura record id" },
1842
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1843
+ ],
1844
+ columns: [
1845
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1846
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1847
+ { name: "score", type: "INTEGER", pick: (r) => r.score },
1848
+ { name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
1849
+ { name: "temperature_deviation", type: "REAL", pick: (r) => r.temperature_deviation },
1850
+ { name: "temperature_trend_deviation", type: "REAL", pick: (r) => r.temperature_trend_deviation },
1851
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
1852
+ ]
1853
+ });
1854
+
1855
+ // src/collections/activity.ts
1856
+ var activity = defineCollection({
1857
+ name: "activity",
1858
+ endpoint: "daily_activity",
1859
+ table: "daily_activity",
1860
+ description: "Daily activity score, steps, calories and activity-time buckets",
1861
+ conflict: "replace",
1862
+ rangeParams: "date",
1863
+ identity: [
1864
+ { field: "id", description: "Oura record id" },
1865
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1866
+ ],
1867
+ columns: [
1868
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1869
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1870
+ { name: "score", type: "INTEGER", pick: (r) => r.score },
1871
+ { name: "active_calories", type: "INTEGER", pick: (r) => r.active_calories },
1872
+ { name: "steps", type: "INTEGER", pick: (r) => r.steps },
1873
+ { name: "equivalent_walking_distance", type: "REAL", pick: (r) => r.equivalent_walking_distance },
1874
+ { name: "high_activity_time", type: "INTEGER", pick: (r) => r.high_activity_time },
1875
+ { name: "medium_activity_time", type: "INTEGER", pick: (r) => r.medium_activity_time },
1876
+ { name: "low_activity_time", type: "INTEGER", pick: (r) => r.low_activity_time },
1877
+ { name: "sedentary_time", type: "INTEGER", pick: (r) => r.sedentary_time },
1878
+ { name: "total_calories", type: "INTEGER", pick: (r) => r.total_calories },
1879
+ { name: "target_calories", type: "INTEGER", pick: (r) => r.target_calories },
1880
+ { name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
1881
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
1882
+ ]
1883
+ });
1884
+
1885
+ // src/collections/hr.ts
1886
+ var hr = defineCollection({
1887
+ name: "hr",
1888
+ endpoint: "heartrate",
1889
+ table: "heartrate",
1890
+ description: "Heart rate samples (bpm) with source",
1891
+ conflict: "ignore",
1892
+ rangeParams: "datetime",
1893
+ maxRangeDays: 30,
1894
+ identity: [{ field: "timestamp", format: "date-time", description: "ISO 8601 timestamp of the sample" }],
1895
+ columns: [
1896
+ { name: "timestamp", type: "TEXT", pick: (r) => r.timestamp },
1897
+ { name: "bpm", type: "INTEGER", pick: (r) => r.bpm },
1898
+ { name: "source", type: "TEXT", pick: (r) => r.source },
1899
+ { name: "day", type: "TEXT", pick: (r) => r.timestamp.slice(0, 10) }
1900
+ ],
1901
+ indexes: [
1902
+ { name: "idx_heartrate_ts", columns: ["timestamp"] },
1903
+ { name: "idx_heartrate_unique", columns: ["timestamp", "source"], unique: true },
1904
+ { name: "idx_heartrate_day", columns: ["day"] }
1905
+ ]
1906
+ });
1907
+
1908
+ // src/collections/spo2.ts
1909
+ var spo2 = defineCollection({
1910
+ name: "spo2",
1911
+ endpoint: "daily_spo2",
1912
+ table: "daily_spo2",
1913
+ description: "Daily blood-oxygen average and breathing disturbance index",
1914
+ conflict: "replace",
1915
+ rangeParams: "date",
1916
+ identity: [
1917
+ { field: "id", description: "Oura record id" },
1918
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1919
+ ],
1920
+ columns: [
1921
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1922
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1923
+ { name: "spo2_average", type: "REAL", pick: (r) => r.spo2_percentage?.average ?? null },
1924
+ { name: "breathing_disturbance_index", type: "REAL", pick: (r) => r.breathing_disturbance_index }
1925
+ ]
1926
+ });
1927
+
1928
+ // src/collections/stress.ts
1929
+ var stress = defineCollection({
1930
+ name: "stress",
1931
+ endpoint: "daily_stress",
1932
+ table: "daily_stress",
1933
+ description: "Daily stress summary with high-stress and high-recovery seconds",
1934
+ conflict: "replace",
1935
+ rangeParams: "date",
1936
+ identity: [
1937
+ { field: "id", description: "Oura record id" },
1938
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1939
+ ],
1940
+ columns: [
1941
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1942
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
1943
+ { name: "day_summary", type: "TEXT", pick: (r) => r.day_summary ?? null },
1944
+ { name: "recovery_high", type: "INTEGER", pick: (r) => r.recovery_high },
1945
+ { name: "stress_high", type: "INTEGER", pick: (r) => r.stress_high }
1946
+ ]
1947
+ });
1948
+
1949
+ // src/collections/workout.ts
1950
+ var workout = defineCollection({
1951
+ name: "workout",
1952
+ endpoint: "workout",
1953
+ table: "workouts",
1954
+ description: "Workout sessions with activity, calories, distance and intensity",
1955
+ conflict: "replace",
1956
+ rangeParams: "date",
1957
+ dayRangeOffset: [0, 1],
1958
+ identity: [
1959
+ { field: "id", description: "Oura record id" },
1960
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1961
+ ],
1962
+ columns: [
1963
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1964
+ { name: "day", type: "TEXT", pick: (r) => r.day },
1965
+ { name: "activity", type: "TEXT", pick: (r) => r.activity },
1966
+ { name: "calories", type: "REAL", pick: (r) => r.calories },
1967
+ { name: "distance", type: "REAL", pick: (r) => r.distance },
1968
+ { name: "start_datetime", type: "TEXT", pick: (r) => r.start_datetime },
1969
+ { name: "end_datetime", type: "TEXT", pick: (r) => r.end_datetime },
1970
+ { name: "intensity", type: "TEXT", pick: (r) => r.intensity },
1971
+ { name: "label", type: "TEXT", pick: (r) => r.label ?? "" },
1972
+ { name: "source", type: "TEXT", pick: (r) => r.source }
1973
+ ]
1974
+ });
1975
+
1976
+ // src/collections/sleep-periods.ts
1977
+ var sleepPeriods = defineCollection({
1978
+ name: "sleep-periods",
1979
+ endpoint: "sleep",
1980
+ table: "sleep_model",
1981
+ description: "Individual sleep periods with stages, HRV, heart rate and efficiency",
1982
+ conflict: "replace",
1983
+ rangeParams: "date",
1984
+ dayRangeOffset: [-1, 0],
1985
+ identity: [
1986
+ { field: "id", description: "Oura record id" },
1987
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
1988
+ ],
1989
+ columns: [
1990
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
1991
+ { name: "day", type: "TEXT", pick: (r) => r.day },
1992
+ { name: "average_breath", type: "REAL", pick: (r) => r.average_breath },
1993
+ { name: "average_heart_rate", type: "REAL", pick: (r) => r.average_heart_rate },
1994
+ { name: "average_hrv", type: "REAL", pick: (r) => r.average_hrv },
1995
+ { name: "awake_time", type: "INTEGER", pick: (r) => r.awake_time },
1996
+ { name: "bedtime_end", type: "TEXT", pick: (r) => r.bedtime_end },
1997
+ { name: "bedtime_start", type: "TEXT", pick: (r) => r.bedtime_start },
1998
+ { name: "deep_sleep_duration", type: "INTEGER", pick: (r) => r.deep_sleep_duration },
1999
+ { name: "efficiency", type: "INTEGER", pick: (r) => r.efficiency },
2000
+ { name: "latency", type: "INTEGER", pick: (r) => r.latency },
2001
+ { name: "light_sleep_duration", type: "INTEGER", pick: (r) => r.light_sleep_duration },
2002
+ { name: "lowest_heart_rate", type: "INTEGER", pick: (r) => r.lowest_heart_rate },
2003
+ { name: "period", type: "INTEGER", pick: (r) => r.period },
2004
+ { name: "rem_sleep_duration", type: "INTEGER", pick: (r) => r.rem_sleep_duration },
2005
+ { name: "restless_periods", type: "INTEGER", pick: (r) => r.restless_periods },
2006
+ { name: "time_in_bed", type: "INTEGER", pick: (r) => r.time_in_bed },
2007
+ { name: "total_sleep_duration", type: "INTEGER", pick: (r) => r.total_sleep_duration },
2008
+ { name: "type", type: "TEXT", pick: (r) => r.type ?? null }
2009
+ ]
2010
+ });
2011
+
2012
+ // src/collections/cv-age.ts
2013
+ var cvAge = defineCollection({
2014
+ name: "cv-age",
2015
+ endpoint: "daily_cardiovascular_age",
2016
+ table: "cardiovascular_age",
2017
+ description: "Daily cardiovascular (vascular) age estimate",
2018
+ conflict: "replace",
2019
+ rangeParams: "date",
2020
+ identity: [
2021
+ { field: "id", description: "Oura record id" },
2022
+ { field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
2023
+ ],
2024
+ columns: [
2025
+ { name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
2026
+ { name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
2027
+ { name: "vascular_age", type: "INTEGER", pick: (r) => r.vascular_age }
2028
+ ]
2029
+ });
2030
+
2031
+ // src/collections/index.ts
2032
+ var COLLECTIONS = [
2033
+ sleep,
2034
+ readiness,
2035
+ activity,
2036
+ hr,
2037
+ spo2,
2038
+ stress,
2039
+ workout,
2040
+ sleepPeriods,
2041
+ cvAge
2042
+ ];
2043
+ function names() {
2044
+ return COLLECTIONS.map((c) => c.name);
2045
+ }
2046
+ function byName(name) {
2047
+ return COLLECTIONS.find((c) => c.name === name);
2048
+ }
2049
+ function insertSql(c) {
2050
+ const verb = c.conflict === "replace" ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
2051
+ const cols = c.columns.map((col) => col.name).join(", ");
2052
+ const marks = c.columns.map(() => "?").join(", ");
2053
+ return `${verb} INTO ${c.table} (${cols}) VALUES (${marks})`;
2054
+ }
2055
+ function rowValues(c, row) {
2056
+ return c.columns.map((col) => col.pick(row));
2057
+ }
2058
+ var MS_PER_DAY = 86400000;
2059
+ function dateQueries(start, end, maxDays, offset) {
2060
+ const query = (s, e) => ({ start_date: shiftDay(s, offset[0]), end_date: shiftDay(e, offset[1]) });
2061
+ if (!maxDays)
2062
+ return [query(start, end)];
2063
+ const out = [];
2064
+ for (let s = start;s <= end; s = shiftDay(s, maxDays)) {
2065
+ const e = shiftDay(s, maxDays - 1);
2066
+ out.push(query(s, e < end ? e : end));
2067
+ }
2068
+ return out;
2069
+ }
2070
+ function datetimeQueries(start, end, tz, maxDays) {
2071
+ const from = Date.parse(localDateToUtcRange(start, tz)[0]);
2072
+ const to = Date.parse(localDateToUtcRange(end, tz)[1]) - 1;
2073
+ const span = (maxDays ?? Infinity) * MS_PER_DAY;
2074
+ const out = [];
2075
+ for (let s = from;s <= to; s += span) {
2076
+ out.push({ start_datetime: new Date(s).toISOString(), end_datetime: new Date(Math.min(s + span - 1, to)).toISOString() });
2077
+ }
2078
+ return out;
2079
+ }
2080
+ function rangeQueries(c, start, end, tz) {
2081
+ if (start > end)
2082
+ return [];
2083
+ return c.rangeParams === "date" ? dateQueries(start, end, c.maxRangeDays, c.dayRangeOffset ?? [0, 0]) : datetimeQueries(start, end, tz, c.maxRangeDays);
2084
+ }
2085
+ async function fetchCollection(client, c, start, end, tz) {
2086
+ const rows = [];
2087
+ for (const query of rangeQueries(c, start, end, tz)) {
2088
+ for (const row of await client.fetch(c.endpoint, query))
2089
+ rows.push(row);
2090
+ }
2091
+ return rows;
2092
+ }
2093
+
2094
+ // src/commands/describe.ts
2095
+ var OUTPUT_SCHEMAS = { doctor: "docs/schemas/doctor.json" };
2096
+ var ENUM_ARGS = {
2097
+ fetch: { collection: names() },
2098
+ report: { period: ["week", "month"] }
2099
+ };
2100
+ function resolved(def) {
2101
+ if (typeof def === "function" || def instanceof Promise) {
2102
+ throw new Error("describe: lazy subcommands are not supported; register plain CommandDef objects.");
2103
+ }
2104
+ return def;
2105
+ }
2106
+ function describeArgs(command, args) {
2107
+ const out = [];
2108
+ for (const [key, raw] of Object.entries(args ?? {})) {
2109
+ if (raw === commonArgs[key])
2110
+ continue;
2111
+ const a = raw;
2112
+ const values = ENUM_ARGS[command]?.[key] ?? (a.type === "enum" ? [...a.options ?? []] : undefined);
2113
+ if (a.type === "positional") {
2114
+ out.push({
2115
+ name: a.required ? `<${key}>` : `[${key}]`,
2116
+ type: "string",
2117
+ required: a.required === true,
2118
+ description: a.description,
2119
+ ...values ? { values } : {}
2120
+ });
2121
+ } else {
2122
+ out.push({
2123
+ name: `--${key}`,
2124
+ type: values ? "enum" : String(a.type ?? "string"),
2125
+ required: false,
2126
+ description: a.description,
2127
+ ...values ? { values } : {}
2128
+ });
2129
+ }
2130
+ }
2131
+ return out;
2132
+ }
2133
+ function resolvedMeta(meta) {
2134
+ if (typeof meta === "function" || meta instanceof Promise)
2135
+ return {};
2136
+ return meta ?? {};
2137
+ }
2138
+ function describeCommandDef(name, def) {
2139
+ const meta = resolvedMeta(def.meta);
2140
+ const cmd = {
2141
+ name,
2142
+ description: meta.description ?? "",
2143
+ args: describeArgs(name, def.args)
2144
+ };
2145
+ if (OUTPUT_SCHEMAS[name])
2146
+ cmd.outputSchema = OUTPUT_SCHEMAS[name];
2147
+ if (name === "fetch")
2148
+ cmd.outputSchemas = Object.fromEntries(names().map((n) => [n, `docs/schemas/${n}.json`]));
2149
+ const subs = def.subCommands;
2150
+ if (subs) {
2151
+ cmd.subcommands = Object.entries(subs).map(([subName, subDef]) => {
2152
+ const sub = resolved(subDef);
2153
+ const subMeta = resolvedMeta(sub.meta);
2154
+ return { name: subName, description: subMeta.description ?? "", args: describeArgs(subName, sub.args) };
2155
+ });
2156
+ }
2157
+ return cmd;
2158
+ }
2159
+ function buildManifest(version, commands) {
2160
+ return {
2161
+ name: "oura-cli",
2162
+ version,
2163
+ compatManifestCommand: "oura-cli manifest",
2164
+ auth: { envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"], tokenFile: "~/.oura-token", loginCommand: "oura-cli login" },
2165
+ globalFlags: [
2166
+ { name: "--format", type: "enum", values: ["table", "json"], description: "Output format (auto-detected by TTY when omitted)" },
2167
+ { name: "--db", type: "string", description: "Override SQLite database path (env: OURA_DB_PATH)" },
2168
+ { name: "--tz", type: "string", description: "Display timezone (env: OURA_TZ; default auto-detected)" },
2169
+ { name: "--token", type: "string", description: "Inline access token (prefer env vars or `login`)" },
2170
+ { name: "--no-color", type: "boolean", description: "Disable ANSI colors in human output" }
2171
+ ],
2172
+ exitCodes: [
2173
+ { code: 0, meaning: "success" },
2174
+ { code: 1, meaning: "user error (bad arguments)" },
2175
+ { code: 2, meaning: "auth error (missing or invalid token)" },
2176
+ { code: 3, meaning: "API or network error" },
2177
+ { code: 4, meaning: "database or local storage error" }
2178
+ ],
2179
+ commands: Object.entries(commands).map(([name, def]) => describeCommandDef(name, resolved(def)))
2180
+ };
2181
+ }
2182
+ function describeCommand(version, getCommands) {
2183
+ return defineCommand({
2184
+ meta: { name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs." },
2185
+ args: { ...commonArgs },
2186
+ run({ args }) {
2187
+ assertKnownArgs(commonArgs, args);
2188
+ console.log(JSON.stringify(buildManifest(version, getCommands()), null, 2));
2189
+ }
2190
+ });
2191
+ }
2192
+
2193
+ // src/db/sync.ts
2194
+ var BACKFILL_DAYS = 30;
2195
+ function lastDay(db, table) {
2196
+ return db.query(`SELECT MAX(day) AS d FROM ${table}`).get().d;
2197
+ }
2198
+ function rowCount(db, table) {
2199
+ return db.query(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
2200
+ }
2201
+ async function importDaily(db, client, clock, log, window = {}) {
2202
+ const { today, tz } = clock;
2203
+ const _log = log ?? (() => {});
2204
+ const end = window.to ?? today;
2205
+ const backfillStart = shiftDay(end, -(BACKFILL_DAYS - 1));
2206
+ const plan = COLLECTIONS.map((c) => {
2207
+ const last = lastDay(db, c.table);
2208
+ return { c, last, start: window.from ?? last ?? backfillStart };
2209
+ });
2210
+ const isFirstSync = plan.every((p) => p.last === null);
2211
+ const startDate = plan.map((p) => p.start).sort()[0];
2212
+ _log(isFirstSync && window.from === undefined ? `First sync \u2014 backfilling the last ${BACKFILL_DAYS} days: ${startDate} \u2192 ${end}` : `Syncing ${startDate} \u2192 ${end}`);
2213
+ const fetched = {};
2214
+ const added = {};
2215
+ for (const { c, start } of plan) {
2216
+ const rows = await fetchCollection(client, c, start, end, tz);
2217
+ const before = rowCount(db, c.table);
2218
+ const stmt = db.query(insertSql(c));
2219
+ db.transaction((rs) => {
2220
+ for (const r of rs)
2221
+ stmt.run(...rowValues(c, r));
2222
+ })(rows);
2223
+ fetched[c.table] = rows.length;
2224
+ added[c.table] = rowCount(db, c.table) - before;
2225
+ if (rows.length > 0)
2226
+ _log(` + ${c.table}: ${rows.length} fetched, ${added[c.table]} new`);
2227
+ }
2228
+ _log("Import complete.");
2229
+ return { startDate, endDate: end, fetched, added, isFirstSync };
2230
+ }
2231
+
2232
+ // src/db/queries.ts
2233
+ function getDaySummary(db, day) {
2234
+ const sl = db.query("SELECT score FROM daily_sleep WHERE day=?").get(day);
2235
+ const rd = db.query("SELECT score, temperature_deviation FROM daily_readiness WHERE day=?").get(day);
2236
+ const ac = db.query("SELECT score, steps FROM daily_activity WHERE day=?").get(day);
2237
+ const st = db.query("SELECT day_summary FROM daily_stress WHERE day=?").get(day);
2238
+ const sp = db.query("SELECT spo2_average FROM daily_spo2 WHERE day=?").get(day);
2239
+ const sm = db.query(`SELECT total_sleep_duration, deep_sleep_duration, rem_sleep_duration, average_hrv, lowest_heart_rate, efficiency FROM sleep_model WHERE day=? AND type='long_sleep'`).get(day);
2240
+ return {
2241
+ day,
2242
+ sleep_score: sl?.score ?? null,
2243
+ readiness_score: rd?.score ?? null,
2244
+ activity_score: ac?.score ?? null,
2245
+ steps: ac?.steps ?? null,
2246
+ stress: st?.day_summary ?? null,
2247
+ spo2: sp?.spo2_average ?? null,
2248
+ temp_deviation: rd?.temperature_deviation ?? null,
2249
+ sleep_hours: sm?.total_sleep_duration ? +(sm.total_sleep_duration / 3600).toFixed(1) : null,
2250
+ deep_hours: sm?.deep_sleep_duration ? +(sm.deep_sleep_duration / 3600).toFixed(1) : null,
2251
+ rem_hours: sm?.rem_sleep_duration ? +(sm.rem_sleep_duration / 3600).toFixed(1) : null,
2252
+ avg_hrv: sm?.average_hrv ?? null,
2253
+ lowest_hr: sm?.lowest_heart_rate ?? null,
2254
+ efficiency: sm?.efficiency ?? null
2255
+ };
2256
+ }
2257
+ function getTrends(db, days, today) {
2258
+ const start = shiftDay(today, -days);
2259
+ const results = [];
2260
+ const metrics = [
2261
+ ["Sleep Score", "daily_sleep", "score"],
2262
+ ["Readiness", "daily_readiness", "score"],
2263
+ ["Activity", "daily_activity", "score"],
2264
+ ["Steps", "daily_activity", "steps"],
2265
+ ["Active Cal", "daily_activity", "active_calories"]
2266
+ ];
2267
+ for (const [label, table, col] of metrics) {
2268
+ const row = db.query(`SELECT AVG(${col}) as avg, MIN(${col}) as min, MAX(${col}) as max, COUNT(${col}) as count FROM ${table} WHERE day BETWEEN ? AND ?`).get(start, today);
2269
+ if (row.count > 0 && row.avg !== null) {
2270
+ results.push({ label, avg: +row.avg.toFixed(0), min: row.min, max: row.max, count: row.count });
2271
+ }
2272
+ }
2273
+ const sp = db.query("SELECT AVG(spo2_average) as avg, MIN(spo2_average) as min, MAX(spo2_average) as max, COUNT(*) as count FROM daily_spo2 WHERE day BETWEEN ? AND ?").get(start, today);
2274
+ if (sp.count > 0 && sp.avg !== null) {
2275
+ results.push({ label: "SpO2", avg: +sp.avg.toFixed(1), min: +sp.min.toFixed(1), max: +sp.max.toFixed(1), count: sp.count });
2276
+ }
2277
+ return results;
2278
+ }
2279
+ function getStats(db, today) {
2280
+ const tables = COLLECTIONS.map((c) => {
2281
+ const row = db.query(`SELECT COUNT(*) as cnt FROM ${c.table}`).get();
2282
+ return { table: c.table, rows: row.cnt };
2283
+ });
2284
+ const range = db.query("SELECT MIN(day) as first, MAX(day) as last FROM daily_sleep").get();
2285
+ const trends = getTrends(db, 99999, today);
2286
+ const mostSteps = db.query("SELECT day, steps FROM daily_activity WHERE steps IS NOT NULL ORDER BY steps DESC LIMIT 1").get();
2287
+ const bestSleep = db.query("SELECT day, score FROM daily_sleep WHERE score IS NOT NULL ORDER BY score DESC LIMIT 1").get();
2288
+ return {
2289
+ tables,
2290
+ dateRange: range,
2291
+ trends,
2292
+ records: {
2293
+ mostSteps: mostSteps ?? null,
2294
+ bestSleep: bestSleep ?? null
2295
+ }
2296
+ };
2297
+ }
2298
+
2299
+ // src/render/format.ts
2300
+ init_source();
2301
+ function scoreColor(score) {
2302
+ if (score === null)
2303
+ return source_default.gray("\u2014");
2304
+ if (score >= 85)
2305
+ return source_default.green(String(score));
2306
+ if (score >= 70)
2307
+ return source_default.yellow(String(score));
2308
+ return source_default.red(String(score));
2309
+ }
2310
+ function fmtHours(h) {
2311
+ if (h === null)
2312
+ return source_default.gray("\u2014");
2313
+ return `${h}h`;
2314
+ }
2315
+ function isEmptyDay(s) {
2316
+ 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;
2317
+ }
2318
+ function formatDaySummary(summary, format, emptyHint) {
2319
+ if (format === "json")
2320
+ return JSON.stringify(summary, null, 2);
2321
+ if (emptyHint && isEmptyDay(summary)) {
2322
+ return [
2323
+ "",
2324
+ source_default.bold(` ${summary.day}`),
2325
+ source_default.gray("\u2500".repeat(50)),
2326
+ ` No Oura data for ${summary.day} yet.`,
2327
+ ` ${emptyHint}`
2328
+ ].join(`
2329
+ `);
2330
+ }
2331
+ const lines = [
2332
+ "",
2333
+ source_default.bold(` ${summary.day}`),
2334
+ source_default.gray("\u2500".repeat(50)),
2335
+ ` Sleep: ${scoreColor(summary.sleep_score)} Readiness: ${scoreColor(summary.readiness_score)} Activity: ${scoreColor(summary.activity_score)}`,
2336
+ ` Steps: ${summary.steps ?? source_default.gray("\u2014")}`
2337
+ ];
2338
+ if (summary.spo2 !== null)
2339
+ lines.push(` SpO2: ${summary.spo2}%`);
2340
+ if (summary.temp_deviation !== null) {
2341
+ const sign = summary.temp_deviation >= 0 ? "+" : "";
2342
+ lines.push(` Temp: ${sign}${summary.temp_deviation}\xB0C`);
2343
+ }
2344
+ if (summary.stress)
2345
+ lines.push(` Stress: ${summary.stress}`);
2346
+ if (summary.sleep_hours !== null) {
2347
+ lines.push("");
2348
+ lines.push(` Sleep: ${fmtHours(summary.sleep_hours)} total | ${fmtHours(summary.deep_hours)} deep | ${fmtHours(summary.rem_hours)} REM`);
2349
+ lines.push(` HRV: ${summary.avg_hrv ?? "\u2014"} Lowest HR: ${summary.lowest_hr ?? "\u2014"} Efficiency: ${summary.efficiency ?? "\u2014"}%`);
2350
+ }
2351
+ return lines.join(`
2352
+ `);
2353
+ }
2354
+ function formatImportSummary(result) {
2355
+ const n = (table) => `${result.fetched[table] ?? 0} (+${result.added[table] ?? 0})`;
2356
+ return [
2357
+ ` Fetched ${result.startDate} \u2192 ${result.endDate}, rows fetched (+new):`,
2358
+ ` sleep ${n("daily_sleep")} readiness ${n("daily_readiness")} activity ${n("daily_activity")} sleep periods ${n("sleep_model")}`,
2359
+ ` spo2 ${n("daily_spo2")} stress ${n("daily_stress")} workouts ${n("workouts")} heart rate ${n("heartrate")} cardiovascular age ${n("cardiovascular_age")}`
2360
+ ].join(`
2361
+ `);
1860
2362
  }
1861
-
1862
- // src/lib/format-resolve.ts
1863
- function resolveFormat({ explicit, isTty }) {
1864
- if (explicit === undefined)
1865
- return isTty ? "table" : "json";
1866
- if (explicit === "table" || explicit === "json")
1867
- return explicit;
1868
- throw new CliError("BAD_ARGS", `Unknown --format value: "${explicit}". Use "table" or "json".`);
2363
+ function formatWeekTable(days, format, emptyHint) {
2364
+ if (format === "json")
2365
+ return JSON.stringify(days, null, 2);
2366
+ if (emptyHint && days.length > 0 && days.every(isEmptyDay)) {
2367
+ return [
2368
+ "",
2369
+ " No Oura data for the last 7 days yet.",
2370
+ ` ${emptyHint}`
2371
+ ].join(`
2372
+ `);
2373
+ }
2374
+ const header = `${"Day".padEnd(12)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Activity".padStart(9)} ${"Steps".padStart(7)} ${"Stress".padEnd(10)}`;
2375
+ const sep = source_default.gray("\u2500".repeat(56));
2376
+ 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)}`);
2377
+ return [`
2378
+ Last 7 Days`, sep, ` ${header}`, sep, ...rows.map((r) => ` ${r}`)].join(`
2379
+ `);
1869
2380
  }
1870
-
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));
2381
+ function formatTrends(trends, days, format) {
2382
+ if (format === "json")
2383
+ return JSON.stringify(trends, null, 2);
2384
+ const lines = [
2385
+ "",
2386
+ source_default.bold(` Trends: last ${days} days`),
2387
+ source_default.gray("\u2500".repeat(50))
2388
+ ];
2389
+ for (const t of trends) {
2390
+ lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)} (${t.count} days)`);
2391
+ }
2392
+ return lines.join(`
2393
+ `);
1886
2394
  }
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
- });
2395
+ function formatStats(stats, format) {
2396
+ if (format === "json")
2397
+ return JSON.stringify(stats, null, 2);
2398
+ const lines = [
2399
+ "",
2400
+ source_default.bold(" Database Statistics"),
2401
+ source_default.gray("\u2550".repeat(50))
2402
+ ];
2403
+ for (const t of stats.tables) {
2404
+ lines.push(` ${t.table.padEnd(22)} ${String(t.rows).padStart(8)} rows`);
2405
+ }
2406
+ if (stats.dateRange.first) {
2407
+ lines.push(`
2408
+ Date range: ${stats.dateRange.first} \u2192 ${stats.dateRange.last}`);
2409
+ }
2410
+ for (const t of stats.trends) {
2411
+ lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)}`);
2412
+ }
2413
+ if (stats.records.mostSteps) {
2414
+ lines.push(`
2415
+ Most steps: ${stats.records.mostSteps.steps} on ${stats.records.mostSteps.day}`);
2416
+ }
2417
+ if (stats.records.bestSleep) {
2418
+ lines.push(` Best sleep: ${stats.records.bestSleep.score} on ${stats.records.bestSleep.day}`);
1892
2419
  }
2420
+ return lines.join(`
2421
+ `);
1893
2422
  }
1894
2423
 
1895
2424
  // 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));
1911
- }
2425
+ function resolveWindow(opts) {
2426
+ if (opts.to !== undefined && opts.from === undefined)
2427
+ throw new CliError("BAD_ARGS", "--to requires --from.");
2428
+ const from = opts.from === undefined ? undefined : assertCalendarDate(opts.from, "--from");
2429
+ const to = opts.to === undefined ? undefined : assertCalendarDate(opts.to, "--to");
2430
+ if (from !== undefined && to !== undefined && from > to)
2431
+ throw new CliError("BAD_ARGS", `--from (${from}) must not be after --to (${to}).`);
2432
+ return { from, to };
2433
+ }
2434
+ async function runSync(ctx, window = {}) {
2435
+ const lines = [];
2436
+ const log = ctx.format === "table" ? (m) => lines.push(m) : undefined;
2437
+ const importResult = await importDaily(ctx.db, ctx.client, { today: ctx.today, tz: ctx.tz }, log, window);
2438
+ const today = getDaySummary(ctx.db, ctx.today);
2439
+ return {
2440
+ json: { import: importResult, today },
2441
+ text: () => [...lines, formatImportSummary(importResult), formatDaySummary(today, "table")].join(`
2442
+ `)
2443
+ };
1912
2444
  }
1913
- var syncCommand = defineCommand({
2445
+ var syncCommand = dataCommand({
1914
2446
  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
- }
1923
- }
2447
+ args: {
2448
+ from: { type: "string", description: "Re-fetch every collection from this day (YYYY-MM-DD) instead of from its last stored day" },
2449
+ to: { type: "string", description: "End of the explicit window (YYYY-MM-DD, default: today); requires --from" }
2450
+ },
2451
+ needs: { db: true, client: true },
2452
+ run: (ctx, args) => runSync(ctx, resolveWindow({ from: args.from, to: args.to }))
1924
2453
  });
1925
2454
 
1926
2455
  // 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
- }
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));
2000
- }
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);
2018
- }
2019
- });
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
- }
2056
-
2057
- // src/commands/db.ts
2456
+ 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
2457
  var dbCommand = defineCommand({
2059
2458
  meta: { name: "db", description: "Query and manage the local SQLite database" },
2060
2459
  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({
2460
+ today: dataCommand({
2074
2461
  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
- }
2462
+ needs: { db: true },
2463
+ run(ctx) {
2464
+ const summary = getDaySummary(ctx.db, ctx.today);
2465
+ return { json: summary, text: () => formatDaySummary(summary, "table", SYNC_HINT) };
2088
2466
  }
2089
2467
  }),
2090
- date: defineCommand({
2468
+ date: dataCommand({
2091
2469
  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
- }
2470
+ args: { day: { type: "positional", required: true, description: "Target date (YYYY-MM-DD)" } },
2471
+ needs: { db: true },
2472
+ run(ctx, args) {
2473
+ const day = assertCalendarDate(String(args.day), "<day>");
2474
+ const summary = getDaySummary(ctx.db, day);
2475
+ return { json: summary, text: () => formatDaySummary(summary, "table") };
2108
2476
  }
2109
2477
  }),
2110
- week: defineCommand({
2478
+ week: dataCommand({
2111
2479
  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
- }
2480
+ needs: { db: true },
2481
+ run(ctx) {
2482
+ const days = daysBack(ctx.today, 7).map((d) => getDaySummary(ctx.db, d));
2483
+ return { json: days, text: () => formatWeekTable(days, "table", "Run `oura-cli sync`, then `oura-cli db week` again.") };
2129
2484
  }
2130
2485
  }),
2131
- trends: defineCommand({
2486
+ trends: dataCommand({
2132
2487
  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
- }
2488
+ args: { days: { type: "positional", required: false, description: "Window size in days (default: 30)" } },
2489
+ needs: { db: true },
2490
+ run(ctx, args) {
2491
+ const n = args.days === undefined ? 30 : assertPositiveInt(String(args.days), "<days>");
2492
+ const trends = getTrends(ctx.db, n, ctx.today);
2493
+ return { json: trends, text: () => formatTrends(trends, n, "table") };
2150
2494
  }
2151
2495
  }),
2152
- stats: defineCommand({
2496
+ stats: dataCommand({
2153
2497
  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
- }
2498
+ needs: { db: true },
2499
+ run(ctx) {
2500
+ const stats = getStats(ctx.db, ctx.today);
2501
+ return { json: stats, text: () => formatStats(stats, "table") };
2206
2502
  }
2207
2503
  })
2208
2504
  }
@@ -2217,17 +2513,15 @@ function dayLabel(dateStr) {
2217
2513
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
2218
2514
  return `${day} ${dd}/${mm}`;
2219
2515
  }
2220
- function getReport(db, days) {
2516
+ function getReport(db, days, today) {
2221
2517
  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);
2518
+ const weekEnd = today;
2519
+ const weekStart = shiftDay(today, -(days - 1));
2520
+ const prevWeekEnd = shiftDay(today, -days);
2521
+ const prevWeekStart = shiftDay(today, -(days * 2 - 1));
2228
2522
  const dailyRows = [];
2229
2523
  for (let i = days - 1;i >= 0; i--) {
2230
- const d = new Date(today.getTime() - i * 86400000).toISOString().slice(0, 10);
2524
+ const d = shiftDay(today, -i);
2231
2525
  const sl = db.query("SELECT score FROM daily_sleep WHERE day=?").get(d);
2232
2526
  const rd = db.query("SELECT score FROM daily_readiness WHERE day=?").get(d);
2233
2527
  const ac = db.query("SELECT score, steps FROM daily_activity WHERE day=?").get(d);
@@ -2295,7 +2589,7 @@ function getReport(db, days) {
2295
2589
  return { period, weekStart, weekEnd, days: dailyRows, averages, spo2, patterns: { lowSleep, lowReadiness, highActivity }, sleepDetails, recommendations };
2296
2590
  }
2297
2591
 
2298
- // src/format-report.ts
2592
+ // src/render/format-report.ts
2299
2593
  init_source();
2300
2594
  function colorizeScore(n) {
2301
2595
  if (n >= 85)
@@ -2365,6 +2659,14 @@ function formatReport(data, format, period) {
2365
2659
  }
2366
2660
  lines.push(source_default.gray(` ${data.weekStart} \u2014 ${data.weekEnd}`));
2367
2661
  lines.push("");
2662
+ 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;
2663
+ if (!hasReportData) {
2664
+ lines.push(" No Oura data is available for this report yet.");
2665
+ lines.push(" Run `oura-cli sync` to download your data, then run `oura-cli report` again.");
2666
+ lines.push("");
2667
+ return lines.join(`
2668
+ `);
2669
+ }
2368
2670
  if (period === "week") {
2369
2671
  lines.push(source_default.bold(" Last 7 Days:"));
2370
2672
  lines.push(source_default.gray(" " + "\u2500".repeat(52)));
@@ -2436,29 +2738,17 @@ function formatReport(data, format, period) {
2436
2738
  }
2437
2739
 
2438
2740
  // src/commands/report.ts
2439
- var reportCommand = defineCommand({
2741
+ var reportCommand = dataCommand({
2440
2742
  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);
2743
+ args: { period: { type: "string", description: "Report window: week | month", default: "week" } },
2744
+ needs: { db: true },
2745
+ run(ctx, args) {
2746
+ const period = args.period;
2747
+ if (period !== "week" && period !== "month") {
2748
+ throw new CliError("BAD_ARGS", `--period must be "week" or "month", got "${period}".`);
2461
2749
  }
2750
+ const data = getReport(ctx.db, period === "week" ? 7 : 30, ctx.today);
2751
+ return { json: data, text: () => formatReport(data, "table", period) };
2462
2752
  }
2463
2753
  });
2464
2754
 
@@ -2468,12 +2758,13 @@ function healthcheckCommand(version) {
2468
2758
  meta: { name: "healthcheck", description: "Quick local DB health probe (JSON: {ok, version, latencyMs})." },
2469
2759
  args: { ...commonArgs },
2470
2760
  run({ args }) {
2761
+ assertKnownArgs(commonArgs, args);
2471
2762
  const start = Date.now();
2472
2763
  let ok = true;
2473
2764
  let error;
2474
2765
  try {
2475
- const db = openDatabase2({ dbPath: args.db });
2476
- ensureSchema2(db);
2766
+ const db = openDatabase(args.db);
2767
+ ensureSchema(db);
2477
2768
  db.query("SELECT 1").get();
2478
2769
  db.close();
2479
2770
  } catch (err) {
@@ -2485,156 +2776,264 @@ function healthcheckCommand(version) {
2485
2776
  });
2486
2777
  }
2487
2778
 
2488
- // src/commands/manifest.ts
2489
- function manifestCommand(version) {
2490
- return defineCommand({
2491
- meta: { name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON." },
2492
- args: {},
2493
- 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));
2779
+ // src/render/doctor-table.ts
2780
+ init_source();
2781
+ function statusSymbol(status) {
2782
+ if (status === "ok")
2783
+ return source_default.green("\u2713");
2784
+ if (status === "warn")
2785
+ return source_default.yellow("!");
2786
+ if (status === "skip")
2787
+ return source_default.gray("\u2013");
2788
+ return source_default.red("\u2717");
2789
+ }
2790
+ function formatDoctorTable(result) {
2791
+ const lines = ["", source_default.bold(" Doctor"), source_default.gray("\u2500".repeat(50))];
2792
+ for (const c of result.checks) {
2793
+ lines.push(` ${statusSymbol(c.status)} ${c.id.padEnd(12)} ${c.detail}`);
2794
+ }
2795
+ lines.push("");
2796
+ const next = result.nextStep ?? (result.ok ? "nothing \u2014 everything looks healthy." : "see the failing checks above.");
2797
+ lines.push(` Next: ${next}`);
2798
+ return lines.join(`
2799
+ `);
2800
+ }
2801
+
2802
+ // src/commands/doctor.ts
2803
+ async function runChecks(deps) {
2804
+ const checks = [];
2805
+ const { token, source } = deps.resolveToken();
2806
+ if (token) {
2807
+ checks.push({ id: "token", status: "ok", detail: `Token found via ${source}.` });
2808
+ } else {
2809
+ checks.push({ id: "token", status: "fail", detail: `No token found (checked ${source}).`, fix: "oura-cli login" });
2810
+ }
2811
+ if (!token) {
2812
+ checks.push({ id: "token-valid", status: "fail", detail: "No token to validate.", fix: "oura-cli login" });
2813
+ } else if (deps.offline) {
2814
+ checks.push({ id: "token-valid", status: "skip", detail: "Not checked (--offline)." });
2815
+ } else {
2816
+ try {
2817
+ const client = deps.createClient(token);
2818
+ await client.fetch("daily_sleep", { start_date: deps.today, end_date: deps.today });
2819
+ checks.push({ id: "token-valid", status: "ok", detail: "Token accepted by the Oura API." });
2820
+ } catch (err) {
2821
+ if (err instanceof CliError && err.code === "TOKEN_INVALID") {
2822
+ checks.push({ id: "token-valid", status: "fail", detail: err.message, fix: "oura-cli login" });
2823
+ } else {
2824
+ const msg = err instanceof Error ? err.message : String(err);
2825
+ checks.push({ id: "token-valid", status: "warn", detail: `Could not reach the Oura API: ${msg}` });
2826
+ }
2519
2827
  }
2520
- });
2828
+ }
2829
+ let db = null;
2830
+ try {
2831
+ const opened = deps.openDb();
2832
+ db = opened.db;
2833
+ checks.push({ id: "database", status: "ok", detail: `Database ready at ${opened.path}.` });
2834
+ } catch (err) {
2835
+ const msg = err instanceof Error ? err.message : String(err);
2836
+ checks.push({ id: "database", status: "fail", detail: msg });
2837
+ }
2838
+ if (db) {
2839
+ const last = latestDataDay(db);
2840
+ if (!last) {
2841
+ checks.push({ id: "data", status: "warn", detail: "No data in the local cache yet.", fix: "oura-cli sync" });
2842
+ } else {
2843
+ const ageDays = Math.round((new Date(`${deps.today}T00:00:00Z`).getTime() - new Date(`${last}T00:00:00Z`).getTime()) / 86400000);
2844
+ if (ageDays > 2) {
2845
+ checks.push({ id: "data", status: "warn", detail: `Most recent data is from ${last} (${ageDays} days ago).`, fix: "oura-cli sync" });
2846
+ } else {
2847
+ checks.push({ id: "data", status: "ok", detail: `Data current through ${last}.` });
2848
+ }
2849
+ }
2850
+ } else {
2851
+ checks.push({ id: "data", status: "fail", detail: "Cannot check data \u2014 database unavailable." });
2852
+ }
2853
+ db?.close();
2854
+ const settled = (s) => s === "ok" || s === "skip";
2855
+ const ok = checks.every((c) => settled(c.status));
2856
+ const nextStep = checks.find((c) => !settled(c.status))?.fix ?? null;
2857
+ return { ok, checks, nextStep };
2858
+ }
2859
+ var DATA_TABLES = ["daily_sleep", "daily_readiness", "daily_activity"];
2860
+ function latestDataDay(db) {
2861
+ let latest = null;
2862
+ for (const tbl of DATA_TABLES) {
2863
+ const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
2864
+ if (row?.d && (!latest || row.d > latest))
2865
+ latest = row.d;
2866
+ }
2867
+ return latest;
2521
2868
  }
2869
+ function exitCodeForChecks(checks) {
2870
+ const fail = checks.find((c) => c.status === "fail");
2871
+ if (!fail)
2872
+ return 0;
2873
+ if (fail.id === "token" || fail.id === "token-valid")
2874
+ return exitCodeFor(new CliError("TOKEN_MISSING", fail.detail));
2875
+ return exitCodeFor(new CliError("DB_ERROR", fail.detail));
2876
+ }
2877
+ var doctorCommand = dataCommand({
2878
+ meta: { name: "doctor", description: "Diagnose token, database, and sync health, and suggest the next step." },
2879
+ args: { offline: { type: "boolean", default: false, description: "Skip the live Oura API token-validation call" } },
2880
+ async run(ctx, args) {
2881
+ const deps = {
2882
+ resolveToken: () => resolveToken(args.token),
2883
+ openDb: () => {
2884
+ const db = openDatabase(args.db);
2885
+ ensureSchema(db);
2886
+ return { db, path: getDbPath(args.db) };
2887
+ },
2888
+ createClient: (token) => new OuraClient({ token }),
2889
+ offline: args.offline === true,
2890
+ today: ctx.today
2891
+ };
2892
+ const result = await runChecks(deps);
2893
+ return {
2894
+ json: result,
2895
+ text: () => formatDoctorTable(result),
2896
+ exitCode: exitCodeForChecks(result.checks)
2897
+ };
2898
+ }
2899
+ });
2522
2900
 
2523
- // src/commands/api-command.ts
2524
- function createApiCommand(name, description, endpoint) {
2901
+ // src/commands/manifest.ts
2902
+ var EXAMPLES = {
2903
+ fetch: ["oura-cli fetch sleep", "oura-cli fetch hr --days 7", "oura-cli fetch workout --from 2026-05-01 --to 2026-05-31"],
2904
+ db: ["oura-cli db today", "oura-cli db week --format json"],
2905
+ report: ["oura-cli report --period week"],
2906
+ doctor: ["oura-cli doctor --offline"]
2907
+ };
2908
+ function buildOpenclawManifest(version, commands) {
2909
+ const m = buildManifest(version, commands);
2910
+ return {
2911
+ id: "oura-cli",
2912
+ version,
2913
+ runtime: "bun",
2914
+ bin: "oura-cli",
2915
+ description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.",
2916
+ commands: m.commands.map((c) => ({
2917
+ name: c.name,
2918
+ description: c.description,
2919
+ examples: EXAMPLES[c.name] ?? [`oura-cli ${c.name}`]
2920
+ })),
2921
+ envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH", "OURA_DB_PATH", "OURA_TZ"],
2922
+ healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number" } }
2923
+ };
2924
+ }
2925
+ function manifestCommand(version, getCommands) {
2525
2926
  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
- })
2927
+ meta: { name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON." },
2928
+ args: { ...commonArgs },
2929
+ run({ args }) {
2930
+ assertKnownArgs(commonArgs, args);
2931
+ console.log(JSON.stringify(buildOpenclawManifest(version, getCommands()), null, 2));
2575
2932
  }
2576
2933
  });
2577
2934
  }
2578
2935
 
2579
- // src/lib/argv-normalize.ts
2580
- var GLOBAL_FLAGS_WITH_VALUE = new Set(["--format", "--token", "--db", "--tz"]);
2581
- var GLOBAL_FLAGS_BOOLEAN = new Set(["--no-color"]);
2582
- var SUBCOMMANDS = new Set([
2583
- "login",
2584
- "describe",
2585
- "healthcheck",
2586
- "manifest",
2587
- "sleep",
2588
- "readiness",
2589
- "activity",
2590
- "hr",
2591
- "spo2",
2592
- "stress",
2593
- "workout",
2594
- "sync",
2595
- "db",
2596
- "report"
2597
- ]);
2598
- function normalizeArgv(argv) {
2599
- const [bun, script, ...rest] = argv;
2600
- const subIdx = rest.findIndex((a) => SUBCOMMANDS.has(a));
2601
- if (subIdx < 0)
2602
- return argv;
2603
- const before = rest.slice(0, subIdx);
2604
- const subcommandOnwards = rest.slice(subIdx);
2605
- const hoisted = [];
2606
- const leftover = [];
2607
- for (let i = 0;i < before.length; i++) {
2608
- const tok = before[i];
2609
- if (tok.includes("=")) {
2610
- const name = tok.slice(0, tok.indexOf("="));
2611
- if (GLOBAL_FLAGS_WITH_VALUE.has(name) || GLOBAL_FLAGS_BOOLEAN.has(name)) {
2612
- hoisted.push(tok);
2613
- continue;
2614
- }
2615
- }
2616
- if (GLOBAL_FLAGS_WITH_VALUE.has(tok)) {
2617
- hoisted.push(tok);
2618
- if (i + 1 < before.length) {
2619
- hoisted.push(before[i + 1]);
2620
- i++;
2621
- }
2622
- continue;
2623
- }
2624
- if (GLOBAL_FLAGS_BOOLEAN.has(tok)) {
2625
- hoisted.push(tok);
2626
- continue;
2936
+ // src/commands/fetch.ts
2937
+ function resolveRange(opts) {
2938
+ const modes = [opts.day !== undefined, opts.from !== undefined || opts.to !== undefined, opts.days !== undefined].filter(Boolean).length;
2939
+ if (modes > 1)
2940
+ throw new CliError("BAD_ARGS", "Use only one of --day, --from/--to, or --days.");
2941
+ if (opts.day !== undefined) {
2942
+ const d = assertCalendarDate(opts.day, "--day");
2943
+ return { start: d, end: d };
2944
+ }
2945
+ if (opts.from !== undefined || opts.to !== undefined) {
2946
+ if (opts.from === undefined || opts.to === undefined)
2947
+ throw new CliError("BAD_ARGS", "--from and --to must be given together.");
2948
+ const start = assertCalendarDate(opts.from, "--from");
2949
+ const end = assertCalendarDate(opts.to, "--to");
2950
+ if (start > end)
2951
+ throw new CliError("BAD_ARGS", `--from (${start}) must not be after --to (${end}).`);
2952
+ return { start, end };
2953
+ }
2954
+ if (opts.days !== undefined) {
2955
+ const n = assertPositiveInt(opts.days, "--days");
2956
+ return { start: shiftDay(opts.today, -(n - 1)), end: opts.today };
2957
+ }
2958
+ return { start: opts.today, end: opts.today };
2959
+ }
2960
+ var fetchCommand = dataCommand({
2961
+ meta: { name: "fetch", description: "Fetch raw records for one Oura collection straight from the API (JSON)." },
2962
+ args: {
2963
+ collection: { type: "positional", required: true, description: `Collection: ${names().join(" | ")}` },
2964
+ day: { type: "string", description: "Single day (YYYY-MM-DD). Default: today." },
2965
+ from: { type: "string", description: "Range start (YYYY-MM-DD); requires --to" },
2966
+ to: { type: "string", description: "Range end (YYYY-MM-DD); requires --from" },
2967
+ days: { type: "string", description: "Last N days ending today" }
2968
+ },
2969
+ jsonOnly: true,
2970
+ async run(ctx, args) {
2971
+ const c = byName(args.collection);
2972
+ if (!c)
2973
+ throw new CliError("BAD_ARGS", `Unknown collection "${args.collection}".`, `Valid collections: ${names().join(", ")}`);
2974
+ const { start, end } = resolveRange({
2975
+ day: args.day,
2976
+ from: args.from,
2977
+ to: args.to,
2978
+ days: args.days,
2979
+ today: ctx.today
2980
+ });
2981
+ const client = new OuraClient(args.token ? { token: args.token } : {});
2982
+ const data = await fetchCollection(client, c, start, end, ctx.tz);
2983
+ return { json: data, text: () => JSON.stringify(data, null, 2) };
2984
+ }
2985
+ });
2986
+
2987
+ // src/lib/citty-error.ts
2988
+ var ANSI = /\u001b\[[0-9;]*m/g;
2989
+ function fromCittyError(err, removedCommandHints = {}) {
2990
+ const code = err?.code;
2991
+ if (typeof code !== "string")
2992
+ return err;
2993
+ const message = (err instanceof Error ? err.message : String(err)).replace(ANSI, "");
2994
+ switch (code) {
2995
+ case "E_UNKNOWN_COMMAND": {
2996
+ const name = message.replace(/^Unknown command\s*/, "").trim();
2997
+ const hint = Object.hasOwn(removedCommandHints, name) ? removedCommandHints[name] : "Run `oura-cli --help` for the list of commands.";
2998
+ return new CliError("BAD_ARGS", `Unknown command "${name}".`, hint);
2627
2999
  }
2628
- leftover.push(tok);
3000
+ case "EARG":
3001
+ return new CliError("BAD_ARGS", message.endsWith(".") ? message : `${message}.`, "Run the command with --help to see its arguments.");
3002
+ case "E_NO_COMMAND":
3003
+ return new CliError("BAD_ARGS", "No command specified.", "Run `oura-cli --help` for the list of commands.");
3004
+ default:
3005
+ return err;
2629
3006
  }
2630
- return [bun, script, ...leftover, ...subcommandOnwards, ...hoisted];
2631
3007
  }
2632
3008
 
2633
3009
  // src/index.ts
2634
- var VERSION = "0.4.4";
3010
+ var VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8")).version;
2635
3011
  if (process.argv.includes("--no-color") || process.env.NO_COLOR) {
2636
3012
  source_default.level = 0;
2637
3013
  }
3014
+ var subCommands = Object.assign(Object.create(null), {
3015
+ login: loginCommand,
3016
+ describe: describeCommand(VERSION, () => subCommands),
3017
+ healthcheck: healthcheckCommand(VERSION),
3018
+ doctor: doctorCommand,
3019
+ manifest: manifestCommand(VERSION, () => subCommands),
3020
+ fetch: fetchCommand,
3021
+ sync: syncCommand,
3022
+ db: dbCommand,
3023
+ report: reportCommand
3024
+ });
3025
+ var FETCH_HINT = "The per-collection commands were replaced in 0.5.0 by `oura-cli fetch <collection>`, e.g. `oura-cli fetch sleep --day 2026-09-01`. Run `oura-cli fetch --help`.";
3026
+ var REMOVED_COMMANDS = {
3027
+ reset: "`db reset` was removed in 0.5.0; delete the database file (`--db` / OURA_DB_PATH) and run `oura-cli sync` to rebuild it.",
3028
+ import: "`db import` was removed in 0.5.0; `oura-cli sync` downloads and stores everything.",
3029
+ sleep: FETCH_HINT,
3030
+ readiness: FETCH_HINT,
3031
+ activity: FETCH_HINT,
3032
+ hr: FETCH_HINT,
3033
+ spo2: FETCH_HINT,
3034
+ stress: FETCH_HINT,
3035
+ workout: FETCH_HINT
3036
+ };
2638
3037
  var main = defineCommand({
2639
3038
  meta: {
2640
3039
  name: "oura-cli",
@@ -2642,22 +3041,18 @@ var main = defineCommand({
2642
3041
  description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents."
2643
3042
  },
2644
3043
  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
- }
3044
+ subCommands
2661
3045
  });
2662
- var normalized = normalizeArgv(process.argv);
2663
- runMain(main, { rawArgs: normalized.slice(2) });
3046
+ var rawArgs = normalizeArgv(process.argv).slice(2);
3047
+ var wantsHelp = rawArgs.some((a) => a === "--help" || a === "-h") || rawArgs.length === 0 && process.stdout.isTTY === true;
3048
+ if (isVersionRequest(rawArgs)) {
3049
+ console.log(VERSION);
3050
+ } else if (wantsHelp) {
3051
+ runMain(main, { rawArgs });
3052
+ } else {
3053
+ runCommand(main, { rawArgs }).catch((raw) => {
3054
+ const err = fromCittyError(raw, REMOVED_COMMANDS);
3055
+ emitError(err, formatFromArgv(rawArgs, process.stdout.isTTY === true));
3056
+ process.exit(exitCodeFor(err));
3057
+ });
3058
+ }