@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/CHANGELOG.md +89 -0
- package/README.md +46 -19
- package/dist/index.js +1562 -1167
- package/docs/schemas/activity.json +15 -4
- package/docs/schemas/cv-age.json +26 -0
- package/docs/schemas/describe.json +1 -0
- package/docs/schemas/doctor.json +25 -0
- package/docs/schemas/hr.json +10 -3
- package/docs/schemas/readiness.json +15 -4
- package/docs/schemas/sleep-periods.json +26 -0
- package/docs/schemas/sleep.json +15 -4
- package/docs/schemas/spo2.json +15 -4
- package/docs/schemas/stress.json +15 -4
- package/docs/schemas/workout.json +15 -4
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 = /[
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
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\.
|
|
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,
|
|
389
|
-
if (
|
|
390
|
-
throw new Error("The `level`
|
|
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
|
|
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
|
-
},
|
|
416
|
+
}, createModelConverters = (model, type) => {
|
|
417
|
+
const style = ansi_styles_default[type];
|
|
400
418
|
if (model === "rgb") {
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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_) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return
|
|
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
|
|
747
|
-
return
|
|
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
|
|
1132
|
-
process.stderr.write(
|
|
1160
|
+
const env = formatError(err, format);
|
|
1161
|
+
process.stderr.write(env.text + `
|
|
1133
1162
|
`);
|
|
1134
1163
|
}
|
|
1135
1164
|
|
|
1136
|
-
// src/commands/
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
|
|
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/
|
|
1317
|
-
|
|
1318
|
-
import { dirname as dirname2 } from "path";
|
|
1174
|
+
// src/commands/run-command.ts
|
|
1175
|
+
init_source();
|
|
1319
1176
|
|
|
1320
|
-
// src/
|
|
1321
|
-
import { Database } from "bun:sqlite";
|
|
1322
|
-
import { resolve
|
|
1323
|
-
import { homedir
|
|
1324
|
-
import {
|
|
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/
|
|
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/
|
|
1492
|
-
var
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
return
|
|
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
|
|
1501
|
-
|
|
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
|
|
1504
|
-
|
|
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/
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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/
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
-
|
|
1863
|
-
|
|
1864
|
-
if (
|
|
1865
|
-
return
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
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
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
|
1888
|
-
if (
|
|
1889
|
-
|
|
1890
|
-
|
|
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
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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 =
|
|
2445
|
+
var syncCommand = dataCommand({
|
|
1914
2446
|
meta: { name: "sync", description: "Import latest data from Oura API and return today's summary" },
|
|
1915
|
-
args: {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2076
|
-
run(
|
|
2077
|
-
|
|
2078
|
-
|
|
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:
|
|
2468
|
+
date: dataCommand({
|
|
2091
2469
|
meta: { name: "date", description: "Summary for specific date from local database" },
|
|
2092
|
-
args: {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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:
|
|
2478
|
+
week: dataCommand({
|
|
2111
2479
|
meta: { name: "week", description: "Last 7 days from local database" },
|
|
2112
|
-
|
|
2113
|
-
run(
|
|
2114
|
-
|
|
2115
|
-
|
|
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:
|
|
2486
|
+
trends: dataCommand({
|
|
2132
2487
|
meta: { name: "trends", description: "Score and metric trends over N days (default: 30)" },
|
|
2133
|
-
args: {
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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:
|
|
2496
|
+
stats: dataCommand({
|
|
2153
2497
|
meta: { name: "stats", description: "Row counts, date range, and record highs from local database" },
|
|
2154
|
-
|
|
2155
|
-
run(
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
const
|
|
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 =
|
|
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 =
|
|
2741
|
+
var reportCommand = dataCommand({
|
|
2440
2742
|
meta: { name: "report", description: "Generate a narrative health report from local data." },
|
|
2441
|
-
args: {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
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 =
|
|
2476
|
-
|
|
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/
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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/
|
|
2524
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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/
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
2663
|
-
|
|
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
|
+
}
|