trainbud 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/CHANGELOG.md +675 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +182 -0
- package/README.md +322 -0
- package/dist/appDb.d.ts +83 -0
- package/dist/appDb.d.ts.map +1 -0
- package/dist/appDb.js +239 -0
- package/dist/appDb.js.map +1 -0
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +230 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +457 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +241 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +1004 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/dashboardCharts.d.ts +99 -0
- package/dist/dashboardCharts.d.ts.map +1 -0
- package/dist/dashboardCharts.js +317 -0
- package/dist/dashboardCharts.js.map +1 -0
- package/dist/dashboardData.d.ts +89 -0
- package/dist/dashboardData.d.ts.map +1 -0
- package/dist/dashboardData.js +228 -0
- package/dist/dashboardData.js.map +1 -0
- package/dist/detect/baseline.d.ts +17 -0
- package/dist/detect/baseline.d.ts.map +1 -0
- package/dist/detect/baseline.js +62 -0
- package/dist/detect/baseline.js.map +1 -0
- package/dist/detect/countdown.d.ts +30 -0
- package/dist/detect/countdown.d.ts.map +1 -0
- package/dist/detect/countdown.js +79 -0
- package/dist/detect/countdown.js.map +1 -0
- package/dist/detect/detectors.d.ts +20 -0
- package/dist/detect/detectors.d.ts.map +1 -0
- package/dist/detect/detectors.js +291 -0
- package/dist/detect/detectors.js.map +1 -0
- package/dist/detect/findings.d.ts +29 -0
- package/dist/detect/findings.d.ts.map +1 -0
- package/dist/detect/findings.js +2 -0
- package/dist/detect/findings.js.map +1 -0
- package/dist/detect/forecast.d.ts +33 -0
- package/dist/detect/forecast.d.ts.map +1 -0
- package/dist/detect/forecast.js +122 -0
- package/dist/detect/forecast.js.map +1 -0
- package/dist/detect/index.d.ts +50 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +114 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/detect/sleepQuality.d.ts +21 -0
- package/dist/detect/sleepQuality.d.ts.map +1 -0
- package/dist/detect/sleepQuality.js +164 -0
- package/dist/detect/sleepQuality.js.map +1 -0
- package/dist/detect/trimp.d.ts +22 -0
- package/dist/detect/trimp.d.ts.map +1 -0
- package/dist/detect/trimp.js +77 -0
- package/dist/detect/trimp.js.map +1 -0
- package/dist/detect/week.d.ts +31 -0
- package/dist/detect/week.d.ts.map +1 -0
- package/dist/detect/week.js +184 -0
- package/dist/detect/week.js.map +1 -0
- package/dist/garmin/auth.d.ts +10 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +81 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/cache.d.ts +42 -0
- package/dist/garmin/cache.d.ts.map +1 -0
- package/dist/garmin/cache.js +141 -0
- package/dist/garmin/cache.js.map +1 -0
- package/dist/garmin/client.d.ts +22 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +237 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +24 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +96 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/garminApiTypes.d.ts +70 -0
- package/dist/garmin/garminApiTypes.d.ts.map +1 -0
- package/dist/garmin/garminApiTypes.js +3 -0
- package/dist/garmin/garminApiTypes.js.map +1 -0
- package/dist/garmin/garminConnect.d.ts +17 -0
- package/dist/garmin/garminConnect.d.ts.map +1 -0
- package/dist/garmin/garminConnect.js +5 -0
- package/dist/garmin/garminConnect.js.map +1 -0
- package/dist/garmin/partial.d.ts +32 -0
- package/dist/garmin/partial.d.ts.map +1 -0
- package/dist/garmin/partial.js +38 -0
- package/dist/garmin/partial.js.map +1 -0
- package/dist/garmin/rawApi.d.ts +59 -0
- package/dist/garmin/rawApi.d.ts.map +1 -0
- package/dist/garmin/rawApi.js +113 -0
- package/dist/garmin/rawApi.js.map +1 -0
- package/dist/garmin/types.d.ts +85 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +11 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/history/capture.d.ts +12 -0
- package/dist/history/capture.d.ts.map +1 -0
- package/dist/history/capture.js +63 -0
- package/dist/history/capture.js.map +1 -0
- package/dist/history/context.d.ts +51 -0
- package/dist/history/context.d.ts.map +1 -0
- package/dist/history/context.js +122 -0
- package/dist/history/context.js.map +1 -0
- package/dist/history/fallback.d.ts +72 -0
- package/dist/history/fallback.d.ts.map +1 -0
- package/dist/history/fallback.js +185 -0
- package/dist/history/fallback.js.map +1 -0
- package/dist/history/ingest.d.ts +44 -0
- package/dist/history/ingest.d.ts.map +1 -0
- package/dist/history/ingest.js +347 -0
- package/dist/history/ingest.js.map +1 -0
- package/dist/history/scheduler.d.ts +20 -0
- package/dist/history/scheduler.d.ts.map +1 -0
- package/dist/history/scheduler.js +54 -0
- package/dist/history/scheduler.js.map +1 -0
- package/dist/history/schema.d.ts +41 -0
- package/dist/history/schema.d.ts.map +1 -0
- package/dist/history/schema.js +91 -0
- package/dist/history/schema.js.map +1 -0
- package/dist/history/store.d.ts +112 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/store.js +282 -0
- package/dist/history/store.js.map +1 -0
- package/dist/httpServer.d.ts +55 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1027 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcpConfig.d.ts +56 -0
- package/dist/mcpConfig.d.ts.map +1 -0
- package/dist/mcpConfig.js +106 -0
- package/dist/mcpConfig.js.map +1 -0
- package/dist/pairApi.d.ts +14 -0
- package/dist/pairApi.d.ts.map +1 -0
- package/dist/pairApi.js +38 -0
- package/dist/pairApi.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +105 -0
- package/dist/paths.js.map +1 -0
- package/dist/profile.d.ts +123 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +327 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptApi.d.ts +43 -0
- package/dist/promptApi.d.ts.map +1 -0
- package/dist/promptApi.js +328 -0
- package/dist/promptApi.js.map +1 -0
- package/dist/promptSuggestions.d.ts +16 -0
- package/dist/promptSuggestions.d.ts.map +1 -0
- package/dist/promptSuggestions.js +141 -0
- package/dist/promptSuggestions.js.map +1 -0
- package/dist/selfTest.d.ts +53 -0
- package/dist/selfTest.d.ts.map +1 -0
- package/dist/selfTest.js +225 -0
- package/dist/selfTest.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +210 -0
- package/dist/setup.js.map +1 -0
- package/dist/toolErrors.d.ts +2 -0
- package/dist/toolErrors.d.ts.map +1 -0
- package/dist/toolErrors.js +18 -0
- package/dist/toolErrors.js.map +1 -0
- package/dist/tools/activities.d.ts +18 -0
- package/dist/tools/activities.d.ts.map +1 -0
- package/dist/tools/activities.js +182 -0
- package/dist/tools/activities.js.map +1 -0
- package/dist/tools/bodyComposition.d.ts +10 -0
- package/dist/tools/bodyComposition.d.ts.map +1 -0
- package/dist/tools/bodyComposition.js +139 -0
- package/dist/tools/bodyComposition.js.map +1 -0
- package/dist/tools/context.d.ts +8 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +148 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/findings.d.ts +7 -0
- package/dist/tools/findings.d.ts.map +1 -0
- package/dist/tools/findings.js +51 -0
- package/dist/tools/findings.js.map +1 -0
- package/dist/tools/heartRate.d.ts +10 -0
- package/dist/tools/heartRate.d.ts.map +1 -0
- package/dist/tools/heartRate.js +105 -0
- package/dist/tools/heartRate.js.map +1 -0
- package/dist/tools/index.d.ts +66 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +101 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/payloads.d.ts +148 -0
- package/dist/tools/payloads.d.ts.map +1 -0
- package/dist/tools/payloads.js +2 -0
- package/dist/tools/payloads.js.map +1 -0
- package/dist/tools/recovery.d.ts +31 -0
- package/dist/tools/recovery.d.ts.map +1 -0
- package/dist/tools/recovery.js +350 -0
- package/dist/tools/recovery.js.map +1 -0
- package/dist/tools/sleep.d.ts +10 -0
- package/dist/tools/sleep.d.ts.map +1 -0
- package/dist/tools/sleep.js +124 -0
- package/dist/tools/sleep.js.map +1 -0
- package/dist/tools/stress.d.ts +11 -0
- package/dist/tools/stress.d.ts.map +1 -0
- package/dist/tools/stress.js +108 -0
- package/dist/tools/stress.js.map +1 -0
- package/dist/tools/trainingInsights.d.ts +15 -0
- package/dist/tools/trainingInsights.d.ts.map +1 -0
- package/dist/tools/trainingInsights.js +79 -0
- package/dist/tools/trainingInsights.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/vo2Max.d.ts +11 -0
- package/dist/tools/vo2Max.d.ts.map +1 -0
- package/dist/tools/vo2Max.js +110 -0
- package/dist/tools/vo2Max.js.map +1 -0
- package/dist/tools/week.d.ts +12 -0
- package/dist/tools/week.d.ts.map +1 -0
- package/dist/tools/week.js +74 -0
- package/dist/tools/week.js.map +1 -0
- package/dist/usage.d.ts +104 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +247 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -0
- package/dist/utils/batch.d.ts.map +1 -0
- package/dist/utils/batch.js +18 -0
- package/dist/utils/batch.js.map +1 -0
- package/dist/utils/helpers.d.ts +32 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +188 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/jsonFile.d.ts +19 -0
- package/dist/utils/jsonFile.d.ts.map +1 -0
- package/dist/utils/jsonFile.js +24 -0
- package/dist/utils/jsonFile.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +97 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/secretFile.d.ts +15 -0
- package/dist/utils/secretFile.d.ts.map +1 -0
- package/dist/utils/secretFile.js +47 -0
- package/dist/utils/secretFile.js.map +1 -0
- package/dist/utils/stdio.d.ts +10 -0
- package/dist/utils/stdio.d.ts.map +1 -0
- package/dist/utils/stdio.js +70 -0
- package/dist/utils/stdio.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/watchApi.d.ts +264 -0
- package/dist/watchApi.d.ts.map +1 -0
- package/dist/watchApi.js +333 -0
- package/dist/watchApi.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pino from "pino";
|
|
4
|
+
import { appConfig } from "../config.js";
|
|
5
|
+
import { restrictExistingFile } from "./secretFile.js";
|
|
6
|
+
let loggerInstance = null;
|
|
7
|
+
/**
|
|
8
|
+
* pino.transport() runs pino-pretty in a worker thread, and a live worker keeps
|
|
9
|
+
* the Node event loop open. Nothing here ever closes the logger, so a process
|
|
10
|
+
* whose only remaining handle was the transport -- `node --test`, a CI step,
|
|
11
|
+
* any short-lived command -- would sometimes finish all its work and then hang
|
|
12
|
+
* until something killed it. It was intermittent because it depended on whether
|
|
13
|
+
* the worker had wound down on its own by the time the last test ended.
|
|
14
|
+
*
|
|
15
|
+
* Colour and column alignment are only worth anything on an attached terminal.
|
|
16
|
+
* Everywhere else, write plain NDJSON straight to fd 2 with no worker at all.
|
|
17
|
+
*/
|
|
18
|
+
function prettyStderrStream() {
|
|
19
|
+
if (!process.stderr.isTTY) {
|
|
20
|
+
return pino.destination(2);
|
|
21
|
+
}
|
|
22
|
+
return pino.transport({
|
|
23
|
+
target: "pino-pretty",
|
|
24
|
+
options: {
|
|
25
|
+
colorize: true,
|
|
26
|
+
translateTime: "SYS:standard",
|
|
27
|
+
destination: 2,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Shared options, and the reason this file has any.
|
|
33
|
+
*
|
|
34
|
+
* pino applies its built-in Error serializer to the key `err` and to nothing
|
|
35
|
+
* else. This codebase logs `{ error }` in twenty places and `{ err }` in three,
|
|
36
|
+
* and an Error has no enumerable own properties -- so twenty of those wrote
|
|
37
|
+
* literally `"error":{}` to the log file. The server's error log recorded that
|
|
38
|
+
* something failed and never once recorded why.
|
|
39
|
+
*
|
|
40
|
+
* Found by reading the log after "History catch-up failed" and discovering the
|
|
41
|
+
* cause had been thrown away by the line that was supposed to preserve it. That
|
|
42
|
+
* is the same fault as the rest of this release: a diagnostic that names
|
|
43
|
+
* nothing. Both keys are now serialized, so neither spelling loses the message
|
|
44
|
+
* or the stack.
|
|
45
|
+
*/
|
|
46
|
+
function baseOptions() {
|
|
47
|
+
return {
|
|
48
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
49
|
+
serializers: {
|
|
50
|
+
err: pino.stdSerializers.err,
|
|
51
|
+
error: pino.stdSerializers.err,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createStderrLogger() {
|
|
56
|
+
return pino(baseOptions(), prettyStderrStream());
|
|
57
|
+
}
|
|
58
|
+
function createFileLogger(logPath) {
|
|
59
|
+
const directory = path.dirname(logPath);
|
|
60
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
61
|
+
// The log records every request path and, on this machine, still contains an
|
|
62
|
+
// API key written before the query string was redacted. pino creates the file
|
|
63
|
+
// at the process umask, which is 0644 on a default install.
|
|
64
|
+
if (!fs.existsSync(logPath)) {
|
|
65
|
+
fs.writeFileSync(logPath, "", { mode: 0o600 });
|
|
66
|
+
}
|
|
67
|
+
restrictExistingFile(logPath);
|
|
68
|
+
return pino(baseOptions(), pino.multistream([
|
|
69
|
+
{
|
|
70
|
+
stream: pino.destination({
|
|
71
|
+
dest: logPath,
|
|
72
|
+
sync: false,
|
|
73
|
+
mkdir: true,
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
stream: prettyStderrStream(),
|
|
78
|
+
},
|
|
79
|
+
]));
|
|
80
|
+
}
|
|
81
|
+
function getLogger() {
|
|
82
|
+
if (!loggerInstance) {
|
|
83
|
+
loggerInstance = createStderrLogger();
|
|
84
|
+
}
|
|
85
|
+
return loggerInstance;
|
|
86
|
+
}
|
|
87
|
+
export function configureLogger(logPath = appConfig.logPath) {
|
|
88
|
+
loggerInstance = createFileLogger(logPath);
|
|
89
|
+
}
|
|
90
|
+
export const logger = new Proxy({}, {
|
|
91
|
+
get(_target, property, receiver) {
|
|
92
|
+
const instance = getLogger();
|
|
93
|
+
const value = Reflect.get(instance, property, receiver);
|
|
94
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,IAAI,cAAc,GAAkB,IAAI,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,SAAS,kBAAkB;IACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE;YACP,QAAQ,EAAE,IAAI;YACd,aAAa,EAAE,cAAc;YAC7B,WAAW,EAAE,CAAC;SACf;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,WAAW;IAClB,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;QACtC,WAAW,EAAE;YACX,GAAG,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG;YAC5B,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG;SAC/B;KACF,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE7C,6EAA6E;IAC7E,8EAA8E;IAC9E,4DAA4D;IAC5D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAE9B,OAAO,IAAI,CACT,WAAW,EAAE,EACb,IAAI,CAAC,WAAW,CAAC;QACf;YACE,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC;gBACvB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,KAAK;gBACX,KAAK,EAAE,IAAI;aACZ,CAAC;SACH;QACD;YACE,MAAM,EAAE,kBAAkB,EAAE;SAC7B;KACF,CAAC,CACH,CAAC;AACJ,CAAC;AAED,SAAS,SAAS;IAChB,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,cAAc,GAAG,kBAAkB,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO;IACzD,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW,IAAI,KAAK,CAAC,EAAY,EAAE;IACpD,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ;QAC7B,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACxD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACpE,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function writeSecretFile(filePath: string, contents: string): void;
|
|
2
|
+
/**
|
|
3
|
+
* Tighten a file this process did not write itself.
|
|
4
|
+
*
|
|
5
|
+
* `.trainbud/app.db` is created by better-sqlite3, not by writeSecretFile, so it
|
|
6
|
+
* missed the hardening the other three credential files got -- and it holds the
|
|
7
|
+
* Anthropic API key the dashboard saves, in the clear, in a table. It landed
|
|
8
|
+
* 0644 like every other SQLite file, readable by every account on the machine.
|
|
9
|
+
*
|
|
10
|
+
* Silent on failure on purpose: a database that cannot be chmod'ed is still a
|
|
11
|
+
* working database, and refusing to start over a permission bit would be a
|
|
12
|
+
* worse outcome than the bit.
|
|
13
|
+
*/
|
|
14
|
+
export declare function restrictExistingFile(filePath: string): void;
|
|
15
|
+
//# sourceMappingURL=secretFile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretFile.d.ts","sourceRoot":"","sources":["../../src/utils/secretFile.ts"],"names":[],"mappings":"AAkBA,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAS3D"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
// SECTION: Secret files on disk
|
|
4
|
+
//
|
|
5
|
+
// Three files this server writes are credentials in the clear: `.env` (the
|
|
6
|
+
// Connect password and the API key), `.trainbud/session.json` (live OAuth
|
|
7
|
+
// tokens for the Connect account) and the MCP client config (the same Connect
|
|
8
|
+
// password, written into an editor's config directory). All three were written
|
|
9
|
+
// with writeFileSync's default mode, so on Linux and macOS they landed as 0644
|
|
10
|
+
// and every other account on the machine could read them.
|
|
11
|
+
//
|
|
12
|
+
// The chmod is not redundant: writeFileSync applies `mode` when it creates a
|
|
13
|
+
// file, not when it truncates an existing one, so a file written by an earlier
|
|
14
|
+
// version keeps its old permissions forever without it.
|
|
15
|
+
const OWNER_ONLY = 0o600;
|
|
16
|
+
export function writeSecretFile(filePath, contents) {
|
|
17
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
18
|
+
fs.writeFileSync(filePath, contents, { encoding: "utf8", mode: OWNER_ONLY });
|
|
19
|
+
// Windows has no POSIX bits and chmod there is a no-op at best.
|
|
20
|
+
if (process.platform !== "win32") {
|
|
21
|
+
fs.chmodSync(filePath, OWNER_ONLY);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tighten a file this process did not write itself.
|
|
26
|
+
*
|
|
27
|
+
* `.trainbud/app.db` is created by better-sqlite3, not by writeSecretFile, so it
|
|
28
|
+
* missed the hardening the other three credential files got -- and it holds the
|
|
29
|
+
* Anthropic API key the dashboard saves, in the clear, in a table. It landed
|
|
30
|
+
* 0644 like every other SQLite file, readable by every account on the machine.
|
|
31
|
+
*
|
|
32
|
+
* Silent on failure on purpose: a database that cannot be chmod'ed is still a
|
|
33
|
+
* working database, and refusing to start over a permission bit would be a
|
|
34
|
+
* worse outcome than the bit.
|
|
35
|
+
*/
|
|
36
|
+
export function restrictExistingFile(filePath) {
|
|
37
|
+
if (process.platform === "win32") {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
fs.chmodSync(filePath, OWNER_ONLY);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Nothing useful to do: the data still works, the bits do not.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=secretFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretFile.js","sourceRoot":"","sources":["../../src/utils/secretFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,gCAAgC;AAChC,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,0DAA0D;AAC1D,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,wDAAwD;AAExD,MAAM,UAAU,GAAG,KAAK,CAAC;AAEzB,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,QAAgB;IAChE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAE7E,gEAAgE;IAChE,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send every console method that defaults to stdout to stderr instead.
|
|
3
|
+
*
|
|
4
|
+
* Idempotent: calling it twice must not capture the already-redirected
|
|
5
|
+
* functions as the originals, or `restoreStdout` would restore the redirect and
|
|
6
|
+
* make it permanent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function protectStdout(): void;
|
|
9
|
+
export declare function restoreStdout(): void;
|
|
10
|
+
//# sourceMappingURL=stdio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/utils/stdio.ts"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAYpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAapC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SECTION: Claiming stdout for the MCP protocol
|
|
2
|
+
//
|
|
3
|
+
// `trainbud start` runs StdioServerTransport on stdout. Every byte on that
|
|
4
|
+
// stream has to be JSON-RPC, and `console.log` writes to stdout.
|
|
5
|
+
//
|
|
6
|
+
// garmin-connect does this, at HttpClient.js:354:
|
|
7
|
+
//
|
|
8
|
+
// console.log('login page title:', title);
|
|
9
|
+
//
|
|
10
|
+
// so any re-login inside a tool call injects two lines of English into the
|
|
11
|
+
// middle of the protocol stream and the client -- Claude Desktop, Cursor, the
|
|
12
|
+
// product's primary surface -- loses the session. The user sees the AI lose
|
|
13
|
+
// access to their data, which in this release has now had three distinct
|
|
14
|
+
// causes.
|
|
15
|
+
//
|
|
16
|
+
// `withQuietUpstreamErrors` in garmin/client.ts already existed for exactly
|
|
17
|
+
// this hazard and covered `console.error` only, scoped to the login call.
|
|
18
|
+
// Adding `console.log` beside it would have been the same fix in the same
|
|
19
|
+
// shape a fourth time, and it would still leave every other dependency, and
|
|
20
|
+
// every future one, able to reach stdout. A transport cannot be correct because
|
|
21
|
+
// today's libraries happen to be polite.
|
|
22
|
+
//
|
|
23
|
+
// So stdout is claimed for the lifetime of the process instead. `console.*`
|
|
24
|
+
// goes to stderr, where the logger already writes and where a person can still
|
|
25
|
+
// read it; the transport keeps writing to `process.stdout` directly, untouched.
|
|
26
|
+
const REDIRECTED = ["log", "info", "debug", "dir"];
|
|
27
|
+
let original = null;
|
|
28
|
+
/**
|
|
29
|
+
* Send every console method that defaults to stdout to stderr instead.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: calling it twice must not capture the already-redirected
|
|
32
|
+
* functions as the originals, or `restoreStdout` would restore the redirect and
|
|
33
|
+
* make it permanent.
|
|
34
|
+
*/
|
|
35
|
+
export function protectStdout() {
|
|
36
|
+
if (original) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
original = {};
|
|
40
|
+
for (const method of REDIRECTED) {
|
|
41
|
+
original[method] = console[method].bind(console);
|
|
42
|
+
console[method] = (...args) => {
|
|
43
|
+
process.stderr.write(`${args.map(formatArgument).join(" ")}\n`);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function restoreStdout() {
|
|
48
|
+
if (!original) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const method of REDIRECTED) {
|
|
52
|
+
const restored = original[method];
|
|
53
|
+
if (restored) {
|
|
54
|
+
console[method] = restored;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
original = null;
|
|
58
|
+
}
|
|
59
|
+
function formatArgument(value) {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(value) ?? String(value);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/utils/stdio.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,2EAA2E;AAC3E,iEAAiE;AACjE,EAAE;AACF,kDAAkD;AAClD,EAAE;AACF,+CAA+C;AAC/C,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,4EAA4E;AAC5E,yEAAyE;AACzE,UAAU;AACV,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,0EAA0E;AAC1E,4EAA4E;AAC5E,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,gFAAgF;AAIhF,MAAM,UAAU,GAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;AAEpE,IAAI,QAAQ,GAAwE,IAAI,CAAC;AAEzF;;;;;;GAMG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO;IACT,CAAC;IAED,QAAQ,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAiC,CAAC;QACjF,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAe,EAAQ,EAAE;YAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClE,CAAC,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;IACT,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,GAAG,QAA8B,CAAC;QACnD,CAAC;IACH,CAAC;IAED,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,cAAc,QAAc,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readJsonFile } from "./utils/jsonFile.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
5
|
+
const pkg = readJsonFile(packageJsonPath);
|
|
6
|
+
export const packageVersion = pkg.version;
|
|
7
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAC/B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAC5C,IAAI,EACJ,cAAc,CACf,CAAC;AAEF,MAAM,GAAG,GAAG,YAAY,CAAsB,eAAe,CAAC,CAAC;AAE/D,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { type MetricState } from "./profile.js";
|
|
3
|
+
import { type WeekReview } from "./detect/week.js";
|
|
4
|
+
import { type RaceCountdown } from "./detect/countdown.js";
|
|
5
|
+
import { type ContextEntry } from "./history/context.js";
|
|
6
|
+
import type { Finding } from "./detect/findings.js";
|
|
7
|
+
import type { HeartRatePayload, LatestActivityPayload, RecoveryPayload, SleepPayload, StressPayload, Vo2MaxPayload } from "./tools/payloads.js";
|
|
8
|
+
export interface WatchRecovery {
|
|
9
|
+
score: number;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
export interface WatchSleep {
|
|
13
|
+
hours: number;
|
|
14
|
+
score: number | null;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WatchActivity {
|
|
18
|
+
name: string;
|
|
19
|
+
/**
|
|
20
|
+
* Kilometres, always. Kept unconditionally metric because a build already on
|
|
21
|
+
* someone's wrist reads this field and would silently relabel miles as km:
|
|
22
|
+
* fields may be added here, never redefined.
|
|
23
|
+
*/
|
|
24
|
+
distance_km: number | null;
|
|
25
|
+
/**
|
|
26
|
+
* The same distance already written in the user's own units, e.g. "3.11 mi".
|
|
27
|
+
* Added rather than converting the field above, for the reason given there.
|
|
28
|
+
* A watch too old to know about it goes on drawing kilometres, which is what
|
|
29
|
+
* it has always drawn.
|
|
30
|
+
*/
|
|
31
|
+
distance_display: string | null;
|
|
32
|
+
duration_min: number | null;
|
|
33
|
+
avg_hr: number | null;
|
|
34
|
+
date: string;
|
|
35
|
+
}
|
|
36
|
+
export interface WatchHeartRate {
|
|
37
|
+
resting: number;
|
|
38
|
+
max: number | null;
|
|
39
|
+
}
|
|
40
|
+
export interface WatchDailyOverview {
|
|
41
|
+
recovery: number | null;
|
|
42
|
+
sleep_h: number | null;
|
|
43
|
+
stress: number | null;
|
|
44
|
+
vo2max: number | null;
|
|
45
|
+
}
|
|
46
|
+
export interface WatchStress {
|
|
47
|
+
avg: number;
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
export interface WatchVo2Max {
|
|
51
|
+
value: number;
|
|
52
|
+
trend: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The watch gets the sentence, not the numbers to build one. It cannot wrap
|
|
56
|
+
* arbitrary text well -- a fixed-font activity name already clips on a 390 px
|
|
57
|
+
* round screen -- so the detail paragraph and the raw values stay on the server.
|
|
58
|
+
*/
|
|
59
|
+
export interface WatchFinding {
|
|
60
|
+
kind: string;
|
|
61
|
+
severity: string;
|
|
62
|
+
headline: string;
|
|
63
|
+
}
|
|
64
|
+
export interface WatchCoverage {
|
|
65
|
+
days: number;
|
|
66
|
+
ready: boolean;
|
|
67
|
+
/** Newest date the store holds any measurement for, or null when empty. */
|
|
68
|
+
throughDate: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* How many days old that is. `ready` is false once this passes the grace
|
|
71
|
+
* period: a store that stopped three weeks ago has plenty of days and nothing
|
|
72
|
+
* recent to compare, and "nothing stands out" would be a claim about a day
|
|
73
|
+
* that was never recorded.
|
|
74
|
+
*/
|
|
75
|
+
staleDays: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The week, compressed to what a 208 px screen can hold.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately a handful of scalars rather than the whole WeekReview: the watch
|
|
81
|
+
* response is parsed on the device, a body Connect IQ cannot hold comes back as
|
|
82
|
+
* -402 NETWORK_RESPONSE_TOO_LARGE, and every field here has to be drawn by hand
|
|
83
|
+
* anyway. The full review stays on the server, where `get_week_review` and the
|
|
84
|
+
* dashboard can render it in as much detail as they like.
|
|
85
|
+
*/
|
|
86
|
+
export interface WatchWeek {
|
|
87
|
+
sessions: number;
|
|
88
|
+
previous_sessions: number;
|
|
89
|
+
moving_minutes: number;
|
|
90
|
+
/** TRIMP this week against last week, as a whole-number percentage. */
|
|
91
|
+
load_delta_pct: number | null;
|
|
92
|
+
sleep_debt_h: number | null;
|
|
93
|
+
/** This person's own habitual night, in hours. Not eight. */
|
|
94
|
+
sleep_habitual_h: number | null;
|
|
95
|
+
sleep_consistency: "steady" | "variable" | "erratic" | "unknown";
|
|
96
|
+
/** Where the acute:chronic ratio lands if next week repeats this one. */
|
|
97
|
+
forecast_ratio: number | null;
|
|
98
|
+
forecast_verdict: "spike_ahead" | "detraining_ahead" | "on_track" | "unknown";
|
|
99
|
+
ready: boolean;
|
|
100
|
+
headline: string;
|
|
101
|
+
}
|
|
102
|
+
export interface WatchRace {
|
|
103
|
+
text: string;
|
|
104
|
+
days_away: number;
|
|
105
|
+
phase: "race_week" | "taper" | "build" | "far_out";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What each metric means, already decided.
|
|
109
|
+
*
|
|
110
|
+
* The watch used to grade these itself: `recoveryColor`, `sleepColor`,
|
|
111
|
+
* `stressColor` and `heartRateColor` were four copies of four thresholds in
|
|
112
|
+
* Monkey C, so the wrist and the browser could disagree about whether the same
|
|
113
|
+
* score was good, and nothing reconciled them. Personalising the bands would
|
|
114
|
+
* have made that disagreement permanent -- the watch has no idea what the user
|
|
115
|
+
* set in the dashboard, and shipping the numbers to it would mean two
|
|
116
|
+
* implementations of the same comparison, one of which is in a language with
|
|
117
|
+
* no tests.
|
|
118
|
+
*
|
|
119
|
+
* So the server grades, and the watch colours. `unknown` is a real answer and
|
|
120
|
+
* has to survive to the screen: an unworn watch is not a recovery score of
|
|
121
|
+
* zero, and a card that renders an absence in red has invented a measurement.
|
|
122
|
+
*/
|
|
123
|
+
export interface WatchStates {
|
|
124
|
+
recovery: MetricState;
|
|
125
|
+
sleep: MetricState;
|
|
126
|
+
stress: MetricState;
|
|
127
|
+
resting_hr: MetricState;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* How this user wants the app to look, resolved for a client that cannot ask.
|
|
131
|
+
*
|
|
132
|
+
* `cards` is the visible carousel in the user's order, by id. The watch walks
|
|
133
|
+
* this list instead of its own compiled-in constants, so hiding a card or
|
|
134
|
+
* moving one happens in the dashboard and is live on the next fetch -- no
|
|
135
|
+
* Connect IQ settings sync, no store update.
|
|
136
|
+
*/
|
|
137
|
+
export interface WatchDisplay {
|
|
138
|
+
name: string | null;
|
|
139
|
+
units: "metric" | "imperial";
|
|
140
|
+
cards: string[];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The worst thing standing out right now, as one number the watch can badge.
|
|
144
|
+
*
|
|
145
|
+
* Findings are already on the payload, but reading them means parsing an array
|
|
146
|
+
* on a device where that costs memory, and the glance view has room for a dot
|
|
147
|
+
* and nothing else.
|
|
148
|
+
*/
|
|
149
|
+
export interface WatchAlert {
|
|
150
|
+
level: "none" | "notice" | "warn";
|
|
151
|
+
count: number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether an Ask would be refused before the watch spends a round trip finding
|
|
155
|
+
* out.
|
|
156
|
+
*
|
|
157
|
+
* `exceeded` is only ever true when the user set a cap themselves; there is no
|
|
158
|
+
* default ceiling. `incomplete` says the total behind that decision is a floor
|
|
159
|
+
* rather than a total, because some calls could not be priced.
|
|
160
|
+
*/
|
|
161
|
+
export interface WatchBudget {
|
|
162
|
+
exceeded: boolean;
|
|
163
|
+
incomplete: boolean;
|
|
164
|
+
}
|
|
165
|
+
export interface WatchSummary {
|
|
166
|
+
daily_overview: WatchDailyOverview;
|
|
167
|
+
recovery: WatchRecovery | null;
|
|
168
|
+
sleep: WatchSleep | null;
|
|
169
|
+
activity: WatchActivity | null;
|
|
170
|
+
stress: WatchStress | null;
|
|
171
|
+
vo2max: WatchVo2Max | null;
|
|
172
|
+
heart_rate: WatchHeartRate | null;
|
|
173
|
+
findings: WatchFinding[];
|
|
174
|
+
coverage: WatchCoverage;
|
|
175
|
+
/** This week against last week. Null on a server too old to compute it. */
|
|
176
|
+
week: WatchWeek | null;
|
|
177
|
+
/** The next race on record, or null when nothing is on the calendar. */
|
|
178
|
+
race: WatchRace | null;
|
|
179
|
+
/**
|
|
180
|
+
* The five Ask prompts for today. The watch reads these instead of the
|
|
181
|
+
* hardcoded strings in strings.xml, so the menu asks about what actually
|
|
182
|
+
* happened rather than the same five questions every day.
|
|
183
|
+
*/
|
|
184
|
+
prompts: string[];
|
|
185
|
+
ai_insight: string | null;
|
|
186
|
+
/**
|
|
187
|
+
* Whether an AI key is configured at all, from either the dashboard or the
|
|
188
|
+
* environment.
|
|
189
|
+
*
|
|
190
|
+
* ai_insight being null does not answer that question: it is also null when
|
|
191
|
+
* a key exists and the call failed, and when today's insight has simply not
|
|
192
|
+
* been generated yet. The watch drew one "AI unavailable" screen for all
|
|
193
|
+
* three, which told the user nothing they could act on -- the same fault the
|
|
194
|
+
* single "Pairing failed" message had. With this flag the watch can say
|
|
195
|
+
* "AI is not set up" and name the dashboard.
|
|
196
|
+
*/
|
|
197
|
+
ai_configured: boolean;
|
|
198
|
+
/** Each metric already graded against this user's own bands. */
|
|
199
|
+
states: WatchStates;
|
|
200
|
+
/** Name, units and the visible carousel, in the user's order. */
|
|
201
|
+
display: WatchDisplay;
|
|
202
|
+
/** The worst finding right now, as one badge-able level. */
|
|
203
|
+
alert: WatchAlert;
|
|
204
|
+
/** Whether the user's own AI spending cap would refuse an Ask. */
|
|
205
|
+
budget: WatchBudget;
|
|
206
|
+
updated_at: string;
|
|
207
|
+
}
|
|
208
|
+
export declare function toWatchRecovery(payload: RecoveryPayload | null): WatchRecovery | null;
|
|
209
|
+
export declare function toWatchSleep(payload: SleepPayload | null): WatchSleep | null;
|
|
210
|
+
export declare function toWatchActivity(payload: LatestActivityPayload | null): WatchActivity | null;
|
|
211
|
+
export declare function toWatchStress(payload: StressPayload | null): WatchStress | null;
|
|
212
|
+
export declare function toWatchVo2Max(payload: Vo2MaxPayload | null): WatchVo2Max | null;
|
|
213
|
+
export declare function toWatchHeartRate(payload: HeartRatePayload | null): WatchHeartRate | null;
|
|
214
|
+
export declare function toWatchFindings(findings: Finding[]): WatchFinding[];
|
|
215
|
+
export interface WatchSummaryParts {
|
|
216
|
+
recovery: RecoveryPayload | null;
|
|
217
|
+
sleep: SleepPayload | null;
|
|
218
|
+
activity: LatestActivityPayload | null;
|
|
219
|
+
stress: StressPayload | null;
|
|
220
|
+
vo2max: Vo2MaxPayload | null;
|
|
221
|
+
heartRate: HeartRatePayload | null;
|
|
222
|
+
findings: Finding[];
|
|
223
|
+
coverage: WatchCoverage;
|
|
224
|
+
context: ContextEntry[];
|
|
225
|
+
week: WeekReview | null;
|
|
226
|
+
race: RaceCountdown | null;
|
|
227
|
+
updatedAt: string;
|
|
228
|
+
aiConfigured: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Distance from this person's own resting-HR median, or null when there is
|
|
231
|
+
* not yet enough history to have one. Null and zero are different answers
|
|
232
|
+
* and the grading has to be able to tell them apart.
|
|
233
|
+
*/
|
|
234
|
+
restingHrDeltaBpm: number | null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The worst severity among the findings, and how many there are.
|
|
238
|
+
*
|
|
239
|
+
* `info` deliberately does not raise the level above `none`: an informational
|
|
240
|
+
* finding is worth a line on the Today card and is not worth a badge on a
|
|
241
|
+
* watch face.
|
|
242
|
+
*/
|
|
243
|
+
export declare function toWatchAlert(findings: Finding[]): WatchAlert;
|
|
244
|
+
export declare function toWatchWeek(review: WeekReview | null): WatchWeek | null;
|
|
245
|
+
export declare function toWatchRace(race: RaceCountdown | null): WatchRace | null;
|
|
246
|
+
export declare function buildWatchSummaryFrom(parts: WatchSummaryParts): Omit<WatchSummary, "ai_insight">;
|
|
247
|
+
/**
|
|
248
|
+
* Drop a card the watch would draw as current when it is not.
|
|
249
|
+
*
|
|
250
|
+
* The test is the age of the NEWEST day in the card, whatever its source --
|
|
251
|
+
* not whether the store was involved. Two reasons that is the right question:
|
|
252
|
+
*
|
|
253
|
+
* * A partly stored answer whose recent days came off the wire is current
|
|
254
|
+
* where it matters, because every card's headline figure (`nights[0]`,
|
|
255
|
+
* `days[0]`) is the newest day.
|
|
256
|
+
* * The cache could already serve a day-old reading as "current resting HR"
|
|
257
|
+
* before any of this existed. One rule covers both.
|
|
258
|
+
*
|
|
259
|
+
* With a healthy connection the newest day is today or yesterday, so this is a
|
|
260
|
+
* no-op in the normal case.
|
|
261
|
+
*/
|
|
262
|
+
export declare function freshEnoughForWatch<T>(payload: T | null, newestDate: (payload: T) => string | null | undefined, today?: DateTime, graceDays?: number): T | null;
|
|
263
|
+
export declare function buildWatchSummary(): Promise<WatchSummary>;
|
|
264
|
+
//# sourceMappingURL=watchApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchApi.d.ts","sourceRoot":"","sources":["../src/watchApi.ts"],"names":[],"mappings":"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"}
|