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,350 @@
|
|
|
1
|
+
import { storedRawPayload, isPartialOrStored } from "../history/fallback.js";
|
|
2
|
+
import { getMetricsOn } from "../history/store.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { appConfig } from "../config.js";
|
|
5
|
+
import { buildToolCacheKey, withCache } from "../garmin/cache.js";
|
|
6
|
+
import { withGarminClient } from "../garmin/client.js";
|
|
7
|
+
import { clamp, formatIsoDate, getDateRange, getYesterday } from "../utils/helpers.js";
|
|
8
|
+
// SECTION: Recovery Scoring
|
|
9
|
+
const DEFAULT_WEIGHTS = {
|
|
10
|
+
hrv: 0.3,
|
|
11
|
+
sleep: 0.3,
|
|
12
|
+
stress: 0.2,
|
|
13
|
+
restingHr: 0.2,
|
|
14
|
+
};
|
|
15
|
+
export function normalizeWeights(weights) {
|
|
16
|
+
// Spreading the caller's object was wrong: getRecoveryStatus builds
|
|
17
|
+
// { hrv: input.hrv_weight, ... } from an input that usually carries no
|
|
18
|
+
// weights at all, so every key is present and explicitly undefined, and a
|
|
19
|
+
// spread of explicit undefined overwrites the default it was meant to fall
|
|
20
|
+
// back to. The total then came out NaN; `NaN <= 0` is false, so the guard
|
|
21
|
+
// below waved it through, every component was multiplied by NaN, and the
|
|
22
|
+
// score was NaN -- which JSON.stringify writes as null. That is how the
|
|
23
|
+
// cache, the tool output and the watch all ended up showing
|
|
24
|
+
// "Recovery score: null/100" with four healthy component scores beneath it.
|
|
25
|
+
const merged = { ...DEFAULT_WEIGHTS };
|
|
26
|
+
for (const key of Object.keys(DEFAULT_WEIGHTS)) {
|
|
27
|
+
const value = weights?.[key];
|
|
28
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
29
|
+
merged[key] = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const total = merged.hrv + merged.sleep + merged.stress + merged.restingHr;
|
|
33
|
+
if (!Number.isFinite(total) || total <= 0) {
|
|
34
|
+
return DEFAULT_WEIGHTS;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
hrv: merged.hrv / total,
|
|
38
|
+
sleep: merged.sleep / total,
|
|
39
|
+
stress: merged.stress / total,
|
|
40
|
+
restingHr: merged.restingHr / total,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function scoreFromHrv(hrv, status) {
|
|
44
|
+
if (hrv === null) {
|
|
45
|
+
if (status === "BALANCED") {
|
|
46
|
+
return 80;
|
|
47
|
+
}
|
|
48
|
+
if (status === "LOW") {
|
|
49
|
+
return 45;
|
|
50
|
+
}
|
|
51
|
+
return 60;
|
|
52
|
+
}
|
|
53
|
+
if (hrv >= 60) {
|
|
54
|
+
return 95;
|
|
55
|
+
}
|
|
56
|
+
if (hrv >= 45) {
|
|
57
|
+
return 80;
|
|
58
|
+
}
|
|
59
|
+
if (hrv >= 30) {
|
|
60
|
+
return 60;
|
|
61
|
+
}
|
|
62
|
+
return 40;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Null when the night was not measured at all.
|
|
66
|
+
*
|
|
67
|
+
* The call site passed `sleep?.sleepTimeSeconds ?? 0`, so a night with no sleep
|
|
68
|
+
* record became zero seconds, fell through every band, and scored 35 out of 100
|
|
69
|
+
* -- a confident bad-night verdict manufactured out of an unworn watch. An hour
|
|
70
|
+
* is the floor for "this is a measurement": nobody's real night is shorter, and
|
|
71
|
+
* Garmin does report tiny durations for naps and for partial wear.
|
|
72
|
+
*/
|
|
73
|
+
export function scoreFromSleep(score, durationSeconds) {
|
|
74
|
+
if (score !== null) {
|
|
75
|
+
return clamp(score, 0, 100);
|
|
76
|
+
}
|
|
77
|
+
if (durationSeconds < 3600) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const hours = durationSeconds / 3600;
|
|
81
|
+
if (hours >= 8) {
|
|
82
|
+
return 90;
|
|
83
|
+
}
|
|
84
|
+
if (hours >= 7) {
|
|
85
|
+
return 75;
|
|
86
|
+
}
|
|
87
|
+
if (hours >= 6) {
|
|
88
|
+
return 55;
|
|
89
|
+
}
|
|
90
|
+
return 35;
|
|
91
|
+
}
|
|
92
|
+
export function scoreFromStress(stress) {
|
|
93
|
+
if (stress === null) {
|
|
94
|
+
return 60;
|
|
95
|
+
}
|
|
96
|
+
if (stress <= 15) {
|
|
97
|
+
return 95;
|
|
98
|
+
}
|
|
99
|
+
if (stress <= 25) {
|
|
100
|
+
return 75;
|
|
101
|
+
}
|
|
102
|
+
if (stress <= 35) {
|
|
103
|
+
return 55;
|
|
104
|
+
}
|
|
105
|
+
return 35;
|
|
106
|
+
}
|
|
107
|
+
export function scoreFromRestingHr(restingHr, baseline) {
|
|
108
|
+
if (restingHr === null) {
|
|
109
|
+
return 60;
|
|
110
|
+
}
|
|
111
|
+
if (baseline === null) {
|
|
112
|
+
if (restingHr <= 50) {
|
|
113
|
+
return 90;
|
|
114
|
+
}
|
|
115
|
+
if (restingHr <= 60) {
|
|
116
|
+
return 80;
|
|
117
|
+
}
|
|
118
|
+
if (restingHr <= 70) {
|
|
119
|
+
return 65;
|
|
120
|
+
}
|
|
121
|
+
return 45;
|
|
122
|
+
}
|
|
123
|
+
const delta = restingHr - baseline;
|
|
124
|
+
if (delta <= -2) {
|
|
125
|
+
return 90;
|
|
126
|
+
}
|
|
127
|
+
if (delta <= 2) {
|
|
128
|
+
return 75;
|
|
129
|
+
}
|
|
130
|
+
if (delta <= 5) {
|
|
131
|
+
return 55;
|
|
132
|
+
}
|
|
133
|
+
return 35;
|
|
134
|
+
}
|
|
135
|
+
export function buildRecoveryStatus(components, weights) {
|
|
136
|
+
// A component that was never measured is dropped and its weight redistributed
|
|
137
|
+
// across the rest, rather than being scored as though it had been measured
|
|
138
|
+
// badly. Scoring an absence is how an unworn night became "fatigued".
|
|
139
|
+
const parts = [
|
|
140
|
+
{ value: components.hrvScore, weight: weights.hrv },
|
|
141
|
+
{ value: components.stressScore, weight: weights.stress },
|
|
142
|
+
{ value: components.restingHrScore, weight: weights.restingHr },
|
|
143
|
+
];
|
|
144
|
+
if (components.sleepScore !== null) {
|
|
145
|
+
parts.push({ value: components.sleepScore, weight: weights.sleep });
|
|
146
|
+
}
|
|
147
|
+
const totalWeight = parts.reduce((sum, part) => sum + part.weight, 0);
|
|
148
|
+
const score = totalWeight > 0
|
|
149
|
+
? Math.round(parts.reduce((sum, part) => sum + part.value * part.weight, 0) / totalWeight)
|
|
150
|
+
: 0;
|
|
151
|
+
let status = "good";
|
|
152
|
+
let recommendation = "Moderate training is reasonable. Listen to your body during hard efforts.";
|
|
153
|
+
if (score >= 80) {
|
|
154
|
+
status = "recovered";
|
|
155
|
+
recommendation = "You look recovered. Hard training or a quality session is appropriate today.";
|
|
156
|
+
}
|
|
157
|
+
else if (score < 60) {
|
|
158
|
+
status = "fatigued";
|
|
159
|
+
recommendation = "Recovery looks limited. Favor easy training, mobility, or a rest day.";
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
score,
|
|
163
|
+
status,
|
|
164
|
+
recommendation,
|
|
165
|
+
components,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* How far back the store is searched for the most recent night with a record.
|
|
170
|
+
* Past this the answer stops being a recovery score and becomes an anecdote,
|
|
171
|
+
* and saying nothing is better than scoring it.
|
|
172
|
+
*/
|
|
173
|
+
const STORED_SIGNAL_WINDOW_DAYS = 30;
|
|
174
|
+
/**
|
|
175
|
+
* The same signals, read out of the store, when Connect will not answer.
|
|
176
|
+
*
|
|
177
|
+
* Every component already has an honest null path -- `scoreFromSleep` returns
|
|
178
|
+
* null for a night that was not measured and the weights renormalise around it
|
|
179
|
+
* -- so a partial reconstruction degrades rather than inventing. What it must
|
|
180
|
+
* not do is pretend to be current, which is what `storedThrough` is for: the
|
|
181
|
+
* renderer names the date and `buildWatchSummary` refuses the card once that
|
|
182
|
+
* date is old.
|
|
183
|
+
*/
|
|
184
|
+
function storedRecoverySignals() {
|
|
185
|
+
for (const date of getDateRange(STORED_SIGNAL_WINDOW_DAYS)) {
|
|
186
|
+
const iso = formatIsoDate(date);
|
|
187
|
+
const raw = storedRawPayload(iso, "sleep");
|
|
188
|
+
const metrics = getMetricsOn(iso);
|
|
189
|
+
if (raw?.dailySleepDTO) {
|
|
190
|
+
return {
|
|
191
|
+
sleepData: raw,
|
|
192
|
+
restingHeartRate: metrics.get("resting_hr") ?? null,
|
|
193
|
+
baselineRestingHeartRate: null,
|
|
194
|
+
storedThrough: iso,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// No archived response, but the derived rows survive pruning forever.
|
|
198
|
+
const sleepSeconds = metrics.get("sleep_seconds");
|
|
199
|
+
if (sleepSeconds !== undefined) {
|
|
200
|
+
return {
|
|
201
|
+
sleepData: {
|
|
202
|
+
dailySleepDTO: {
|
|
203
|
+
sleepTimeSeconds: sleepSeconds,
|
|
204
|
+
avgSleepStress: metrics.get("sleep_stress") ?? null,
|
|
205
|
+
sleepScores: { overall: { value: metrics.get("sleep_score") ?? null } },
|
|
206
|
+
},
|
|
207
|
+
avgOvernightHrv: metrics.get("hrv_overnight") ?? null,
|
|
208
|
+
hrvStatus: null,
|
|
209
|
+
},
|
|
210
|
+
restingHeartRate: metrics.get("resting_hr") ?? null,
|
|
211
|
+
baselineRestingHeartRate: null,
|
|
212
|
+
storedThrough: iso,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
async function fetchRecoverySignals() {
|
|
219
|
+
const today = new Date();
|
|
220
|
+
const candidates = [getYesterday(), ...getDateRange(3).slice(1)];
|
|
221
|
+
try {
|
|
222
|
+
return await withGarminClient(async (client) => {
|
|
223
|
+
let sleepData = { dailySleepDTO: undefined };
|
|
224
|
+
for (const date of candidates) {
|
|
225
|
+
const candidate = await client.getSleepData(date);
|
|
226
|
+
if (candidate.dailySleepDTO) {
|
|
227
|
+
sleepData = candidate;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const heartRate = await client.getHeartRate(today);
|
|
232
|
+
return {
|
|
233
|
+
sleepData,
|
|
234
|
+
restingHeartRate: heartRate.restingHeartRate ?? null,
|
|
235
|
+
baselineRestingHeartRate: heartRate.lastSevenDaysAvgRestingHeartRate ?? null,
|
|
236
|
+
storedThrough: null,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// A recovery score built on an expired session used to be a thrown tool
|
|
242
|
+
// error, which an MCP client reports as having no access to the user's
|
|
243
|
+
// data. The signals are in the store; what changes is the date they
|
|
244
|
+
// describe, and that is something to say rather than a reason to say
|
|
245
|
+
// nothing at all.
|
|
246
|
+
const stored = storedRecoverySignals();
|
|
247
|
+
if (!stored) {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
logger.info({ err: error, storedThrough: stored.storedThrough }, "Recovery signals came from the stored history");
|
|
251
|
+
return stored;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// SECTION: Tool Handler
|
|
255
|
+
/**
|
|
256
|
+
* The date is passed in rather than read from the clock here, which is what
|
|
257
|
+
* keeps this pure and testable -- the handler below supplies it.
|
|
258
|
+
*/
|
|
259
|
+
export function renderRecoveryText(payload) {
|
|
260
|
+
const { recovery } = payload;
|
|
261
|
+
const provenance = payload.storedThrough
|
|
262
|
+
? [
|
|
263
|
+
`Garmin could not be reached, so this was computed from TrainBud's stored record for ${payload.storedThrough} rather than from today's figures.`,
|
|
264
|
+
"It describes that day. Do not present it as current, and do not report it as an absence of data.",
|
|
265
|
+
"",
|
|
266
|
+
]
|
|
267
|
+
: [];
|
|
268
|
+
return [
|
|
269
|
+
...provenance,
|
|
270
|
+
`Recovery score: ${recovery.score}/100 (${recovery.status})`,
|
|
271
|
+
recovery.recommendation,
|
|
272
|
+
"",
|
|
273
|
+
"Component scores:",
|
|
274
|
+
`- HRV: ${recovery.components.hrvScore}`,
|
|
275
|
+
`- Sleep: ${recovery.components.sleepScore ?? "not measured"}`,
|
|
276
|
+
`- Stress: ${recovery.components.stressScore}`,
|
|
277
|
+
`- Resting HR: ${recovery.components.restingHrScore}`,
|
|
278
|
+
"",
|
|
279
|
+
`Date: ${payload.date}`,
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
export async function getRecoveryStatus(input) {
|
|
283
|
+
const weights = normalizeWeights({
|
|
284
|
+
hrv: input.hrv_weight,
|
|
285
|
+
sleep: input.sleep_weight,
|
|
286
|
+
stress: input.stress_weight,
|
|
287
|
+
restingHr: input.resting_hr_weight,
|
|
288
|
+
});
|
|
289
|
+
const cacheKey = buildToolCacheKey("get_recovery_status", {
|
|
290
|
+
hrv: weights.hrv,
|
|
291
|
+
sleep: weights.sleep,
|
|
292
|
+
stress: weights.stress,
|
|
293
|
+
restingHr: weights.restingHr,
|
|
294
|
+
});
|
|
295
|
+
const computed = await withCache(cacheKey, appConfig.cacheTtlStats, async () => {
|
|
296
|
+
const signals = await fetchRecoverySignals();
|
|
297
|
+
const sleep = signals.sleepData.dailySleepDTO;
|
|
298
|
+
const components = {
|
|
299
|
+
hrvScore: scoreFromHrv(signals.sleepData.avgOvernightHrv ?? null, signals.sleepData.hrvStatus ?? null),
|
|
300
|
+
sleepScore: scoreFromSleep(sleep?.sleepScores?.overall?.value ?? null, sleep?.sleepTimeSeconds ?? 0),
|
|
301
|
+
stressScore: scoreFromStress(sleep?.avgSleepStress ?? null),
|
|
302
|
+
restingHrScore: scoreFromRestingHr(signals.restingHeartRate, signals.baselineRestingHeartRate),
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
recovery: buildRecoveryStatus(components, weights),
|
|
306
|
+
storedThrough: signals.storedThrough,
|
|
307
|
+
};
|
|
308
|
+
}, {
|
|
309
|
+
isPartial: (value) => isPartialOrStored({ unreachableDays: 0, storedDays: value.storedThrough ? 1 : 0 }),
|
|
310
|
+
});
|
|
311
|
+
const payload = {
|
|
312
|
+
// The day the score describes, which is the stored day when the signals
|
|
313
|
+
// came from the store. Stamping it with today is how a fortnight-old night
|
|
314
|
+
// would have been served as this morning's recovery.
|
|
315
|
+
date: computed.storedThrough ?? formatIsoDate(new Date()),
|
|
316
|
+
recovery: computed.recovery,
|
|
317
|
+
storedThrough: computed.storedThrough,
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
type: "text",
|
|
321
|
+
text: renderRecoveryText(payload),
|
|
322
|
+
data: payload,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
export const recoveryToolDefinitions = [
|
|
326
|
+
{
|
|
327
|
+
name: "get_recovery_status",
|
|
328
|
+
description: "Combines HRV, sleep, stress, and resting heart rate into a recovery score and training recommendation.",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
hrv_weight: {
|
|
331
|
+
type: "number",
|
|
332
|
+
description: "Optional weight for HRV in the recovery score.",
|
|
333
|
+
},
|
|
334
|
+
sleep_weight: {
|
|
335
|
+
type: "number",
|
|
336
|
+
description: "Optional weight for sleep in the recovery score.",
|
|
337
|
+
},
|
|
338
|
+
stress_weight: {
|
|
339
|
+
type: "number",
|
|
340
|
+
description: "Optional weight for stress in the recovery score.",
|
|
341
|
+
},
|
|
342
|
+
resting_hr_weight: {
|
|
343
|
+
type: "number",
|
|
344
|
+
description: "Optional weight for resting heart rate in the recovery score.",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
handler: getRecoveryStatus,
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
//# sourceMappingURL=recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/tools/recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SleepNightSummary, ToolResult } from "../garmin/types.js";
|
|
2
|
+
import type { SleepPayload, StoredProvenance } from "./payloads.js";
|
|
3
|
+
import type { ToolDefinition } from "./types.js";
|
|
4
|
+
export declare function buildSleepPayload(nights: SleepNightSummary[], requestedNights: number, unreachableNights?: number, stored?: StoredProvenance): SleepPayload;
|
|
5
|
+
export declare function renderSleepText(payload: SleepPayload): string;
|
|
6
|
+
export declare function getSleepDataTool(input: {
|
|
7
|
+
nights?: number;
|
|
8
|
+
}): Promise<ToolResult<SleepPayload>>;
|
|
9
|
+
export declare const sleepToolDefinitions: ToolDefinition[];
|
|
10
|
+
//# sourceMappingURL=sleep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/tools/sleep.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAqEjD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,eAAe,EAAE,MAAM,EACvB,iBAAiB,SAAI,EACrB,MAAM,GAAE,gBAIP,GACA,YAAY,CAoBd;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CA0B7D;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAkBnC;AAED,eAAO,MAAM,oBAAoB,EAAE,cAAc,EAYhD,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { appConfig } from "../config.js";
|
|
2
|
+
import { buildToolCacheKey, withCache } from "../garmin/cache.js";
|
|
3
|
+
import { withGarminClient } from "../garmin/client.js";
|
|
4
|
+
import { fetchSleepDay, mapSleepData } from "../garmin/daily.js";
|
|
5
|
+
import { fetchEachDay } from "../garmin/partial.js";
|
|
6
|
+
import { fetchDaysOrStore, isPartialOrStored, storedFetchNote, } from "../history/fallback.js";
|
|
7
|
+
import { formatDuration, getDateRange } from "../utils/helpers.js";
|
|
8
|
+
// SECTION: Sleep Mapping
|
|
9
|
+
async function fetchSleepNights(days) {
|
|
10
|
+
const dates = getDateRange(days);
|
|
11
|
+
return fetchDaysOrStore({
|
|
12
|
+
dates,
|
|
13
|
+
source: "sleep",
|
|
14
|
+
live: () => withGarminClient(async (client) => fetchEachDay(dates, async (date) => (await fetchSleepDay(client, date)).mapped, "sleep")),
|
|
15
|
+
// The same mapper the live path uses, re-run over the archived response. A
|
|
16
|
+
// second mapping here would be a second place for "which field is the sleep
|
|
17
|
+
// score" to be answered, and this project has already shipped a mapper
|
|
18
|
+
// reading a field Connect does not send.
|
|
19
|
+
fromRaw: (date, payload) => mapSleepData(date, payload),
|
|
20
|
+
fromMetrics: (date, metrics) => {
|
|
21
|
+
const totalSleepSeconds = metrics.get("sleep_seconds");
|
|
22
|
+
if (totalSleepSeconds === undefined) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
date,
|
|
27
|
+
totalSleepSeconds,
|
|
28
|
+
deepSleepSeconds: null,
|
|
29
|
+
lightSleepSeconds: null,
|
|
30
|
+
remSleepSeconds: null,
|
|
31
|
+
awakeCount: null,
|
|
32
|
+
sleepScore: metrics.get("sleep_score") ?? null,
|
|
33
|
+
avgSleepStress: metrics.get("sleep_stress") ?? null,
|
|
34
|
+
avgOvernightHrv: metrics.get("hrv_overnight") ?? null,
|
|
35
|
+
hrvStatus: null,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
dateOf: (night) => night.date,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function formatSleepNight(night) {
|
|
42
|
+
const stages = night.deepSleepSeconds === null &&
|
|
43
|
+
night.lightSleepSeconds === null &&
|
|
44
|
+
night.remSleepSeconds === null
|
|
45
|
+
? " Stages: not kept for this night"
|
|
46
|
+
: ` Deep: ${formatDuration(night.deepSleepSeconds)} | Light: ${formatDuration(night.lightSleepSeconds)} | REM: ${formatDuration(night.remSleepSeconds)}`;
|
|
47
|
+
return [
|
|
48
|
+
`${night.date}:`,
|
|
49
|
+
` Total sleep: ${formatDuration(night.totalSleepSeconds)}`,
|
|
50
|
+
stages,
|
|
51
|
+
` Score: ${night.sleepScore ?? "n/a"} | Awakenings: ${night.awakeCount ?? "n/a"}`,
|
|
52
|
+
` Avg sleep stress: ${night.avgSleepStress ?? "n/a"}`,
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
// SECTION: Tool Handler
|
|
56
|
+
export function buildSleepPayload(nights, requestedNights, unreachableNights = 0, stored = {
|
|
57
|
+
storedDays: 0,
|
|
58
|
+
storedThrough: null,
|
|
59
|
+
storedWindowMoved: false,
|
|
60
|
+
}) {
|
|
61
|
+
const scored = nights
|
|
62
|
+
.map((night) => night.sleepScore)
|
|
63
|
+
.filter((score) => score !== null);
|
|
64
|
+
const averageScore = scored.length > 0
|
|
65
|
+
? Math.round(scored.reduce((sum, score) => sum + score, 0) / scored.length)
|
|
66
|
+
: null;
|
|
67
|
+
return {
|
|
68
|
+
requestedNights,
|
|
69
|
+
recordedNights: nights.length,
|
|
70
|
+
unreachableNights,
|
|
71
|
+
averageScore,
|
|
72
|
+
nights,
|
|
73
|
+
storedNights: stored.storedDays,
|
|
74
|
+
storedThrough: stored.storedThrough,
|
|
75
|
+
storedWindowMoved: stored.storedWindowMoved,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function renderSleepText(payload) {
|
|
79
|
+
const note = storedFetchNote({
|
|
80
|
+
values: payload.nights,
|
|
81
|
+
unreachableDays: payload.unreachableNights,
|
|
82
|
+
requestedDays: payload.requestedNights,
|
|
83
|
+
storedDays: payload.storedNights,
|
|
84
|
+
storedThrough: payload.storedThrough,
|
|
85
|
+
storedWindowMoved: payload.storedWindowMoved,
|
|
86
|
+
}, "nights");
|
|
87
|
+
if (payload.recordedNights === 0) {
|
|
88
|
+
return note || `No sleep data found for the last ${payload.requestedNights} nights.`;
|
|
89
|
+
}
|
|
90
|
+
return [
|
|
91
|
+
note,
|
|
92
|
+
`Sleep summary for last ${payload.recordedNights} recorded nights:`,
|
|
93
|
+
payload.averageScore !== null ? `Average sleep score: ${payload.averageScore}` : "",
|
|
94
|
+
"",
|
|
95
|
+
...payload.nights.map(formatSleepNight),
|
|
96
|
+
]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
export async function getSleepDataTool(input) {
|
|
101
|
+
const nights = input.nights ?? 7;
|
|
102
|
+
const cacheKey = buildToolCacheKey("get_sleep_data", { nights });
|
|
103
|
+
const fetched = await withCache(cacheKey, appConfig.cacheTtlSleep, async () => fetchSleepNights(nights), { isPartial: isPartialOrStored });
|
|
104
|
+
const payload = buildSleepPayload(fetched.values, nights, fetched.unreachableDays, fetched);
|
|
105
|
+
return {
|
|
106
|
+
type: "text",
|
|
107
|
+
text: renderSleepText(payload),
|
|
108
|
+
data: payload,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export const sleepToolDefinitions = [
|
|
112
|
+
{
|
|
113
|
+
name: "get_sleep_data",
|
|
114
|
+
description: "Returns sleep duration, quality score, stage breakdown, and interruptions for recent nights.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
nights: {
|
|
117
|
+
type: "number",
|
|
118
|
+
description: "Number of recent nights to include. Defaults to 7.",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
handler: getSleepDataTool,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
//# sourceMappingURL=sleep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../src/tools/sleep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type DailyStressSummary } from "../garmin/rawApi.js";
|
|
2
|
+
import type { ToolResult } from "../garmin/types.js";
|
|
3
|
+
import type { StressPayload, StoredProvenance } from "./payloads.js";
|
|
4
|
+
import type { ToolDefinition } from "./types.js";
|
|
5
|
+
export declare function buildStressPayload(days: DailyStressSummary[], requestedDays: number, unreachableDays?: number, stored?: StoredProvenance): StressPayload;
|
|
6
|
+
export declare function renderStressText(payload: StressPayload): string;
|
|
7
|
+
export declare function getStressLevels(input: {
|
|
8
|
+
days?: number;
|
|
9
|
+
}): Promise<ToolResult<StressPayload>>;
|
|
10
|
+
export declare const stressToolDefinitions: ToolDefinition[];
|
|
11
|
+
//# sourceMappingURL=stress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stress.d.ts","sourceRoot":"","sources":["../../src/tools/stress.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAwCjD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,kBAAkB,EAAE,EAC1B,aAAa,EAAE,MAAM,EACrB,eAAe,SAAI,EACnB,MAAM,GAAE,gBAIP,GACA,aAAa,CAgBf;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqC/D;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACvB,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAkBpC;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAc,EAYjD,CAAC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { appConfig } from "../config.js";
|
|
2
|
+
import { buildToolCacheKey, withCache } from "../garmin/cache.js";
|
|
3
|
+
import { withGarminClient } from "../garmin/client.js";
|
|
4
|
+
import { fetchStressDay } from "../garmin/daily.js";
|
|
5
|
+
import { mapDailyStress } from "../garmin/rawApi.js";
|
|
6
|
+
import { fetchEachDay } from "../garmin/partial.js";
|
|
7
|
+
import { fetchDaysOrStore, isPartialOrStored, storedFetchNote, } from "../history/fallback.js";
|
|
8
|
+
import { average, calculateTrend, getDateRange } from "../utils/helpers.js";
|
|
9
|
+
async function fetchStressDays(days) {
|
|
10
|
+
const dates = getDateRange(days);
|
|
11
|
+
return fetchDaysOrStore({
|
|
12
|
+
dates,
|
|
13
|
+
source: "stress",
|
|
14
|
+
live: () => withGarminClient(async (client) => fetchEachDay(dates, async (date) => (await fetchStressDay(client, date)).mapped, "stress")),
|
|
15
|
+
fromRaw: (date, payload) => mapDailyStress(date, payload),
|
|
16
|
+
fromMetrics: (date, metrics) => {
|
|
17
|
+
const averageStress = metrics.get("stress_avg") ?? null;
|
|
18
|
+
const maxStress = metrics.get("stress_max") ?? null;
|
|
19
|
+
if (averageStress === null && maxStress === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
date,
|
|
24
|
+
averageStress,
|
|
25
|
+
maxStress,
|
|
26
|
+
restStress: null,
|
|
27
|
+
stressDurationSeconds: null,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
dateOf: (day) => day.date,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function buildStressPayload(days, requestedDays, unreachableDays = 0, stored = {
|
|
34
|
+
storedDays: 0,
|
|
35
|
+
storedThrough: null,
|
|
36
|
+
storedWindowMoved: false,
|
|
37
|
+
}) {
|
|
38
|
+
const averages = days
|
|
39
|
+
.map((day) => day.averageStress)
|
|
40
|
+
.filter((value) => value !== null);
|
|
41
|
+
return {
|
|
42
|
+
requestedDays,
|
|
43
|
+
recordedDays: days.length,
|
|
44
|
+
unreachableDays,
|
|
45
|
+
averageStress: averages.length > 0 ? Math.round(average(averages)) : null,
|
|
46
|
+
trend: calculateTrend(averages, true),
|
|
47
|
+
days,
|
|
48
|
+
storedDays: stored.storedDays,
|
|
49
|
+
storedThrough: stored.storedThrough,
|
|
50
|
+
storedWindowMoved: stored.storedWindowMoved,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function renderStressText(payload) {
|
|
54
|
+
const note = storedFetchNote({
|
|
55
|
+
values: payload.days,
|
|
56
|
+
unreachableDays: payload.unreachableDays,
|
|
57
|
+
requestedDays: payload.requestedDays,
|
|
58
|
+
storedDays: payload.storedDays,
|
|
59
|
+
storedThrough: payload.storedThrough,
|
|
60
|
+
storedWindowMoved: payload.storedWindowMoved,
|
|
61
|
+
}, "days");
|
|
62
|
+
if (payload.recordedDays === 0) {
|
|
63
|
+
return note || `No stress data found for the last ${payload.requestedDays} days.`;
|
|
64
|
+
}
|
|
65
|
+
// The trend line is conditional on more than one *measured* day, not on more
|
|
66
|
+
// than one recorded day: Connect returns -1 and -2 as sentinels for days the
|
|
67
|
+
// watch was not worn, and those are mapped to null rather than averaged in.
|
|
68
|
+
const measuredDays = payload.days.filter((day) => day.averageStress !== null).length;
|
|
69
|
+
const lines = payload.days.slice(0, 7).map((day) => {
|
|
70
|
+
return `${day.date}: avg ${day.averageStress ?? "n/a"}, max ${day.maxStress ?? "n/a"}`;
|
|
71
|
+
});
|
|
72
|
+
return [
|
|
73
|
+
note,
|
|
74
|
+
`Stress levels over ${payload.recordedDays} recorded days:`,
|
|
75
|
+
payload.averageStress !== null ? `Average stress: ${payload.averageStress}` : "",
|
|
76
|
+
measuredDays > 1 ? `Trend: ${payload.trend}` : "",
|
|
77
|
+
"",
|
|
78
|
+
"Recent days:",
|
|
79
|
+
...lines,
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join("\n");
|
|
83
|
+
}
|
|
84
|
+
export async function getStressLevels(input) {
|
|
85
|
+
const days = input.days ?? 7;
|
|
86
|
+
const cacheKey = buildToolCacheKey("get_stress_levels", { days });
|
|
87
|
+
const fetched = await withCache(cacheKey, appConfig.cacheTtlStats, async () => fetchStressDays(days), { isPartial: isPartialOrStored });
|
|
88
|
+
const payload = buildStressPayload(fetched.values, days, fetched.unreachableDays, fetched);
|
|
89
|
+
return {
|
|
90
|
+
type: "text",
|
|
91
|
+
text: renderStressText(payload),
|
|
92
|
+
data: payload,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export const stressToolDefinitions = [
|
|
96
|
+
{
|
|
97
|
+
name: "get_stress_levels",
|
|
98
|
+
description: "Returns daily stress averages and trends from Garmin Connect.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
days: {
|
|
101
|
+
type: "number",
|
|
102
|
+
description: "Number of days to analyze. Defaults to 7.",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
handler: getStressLevels,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
//# sourceMappingURL=stress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stress.js","sourceRoot":"","sources":["../../src/tools/stress.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolResult } from "../garmin/types.js";
|
|
2
|
+
import type { TrainingInsightsPayload } from "./payloads.js";
|
|
3
|
+
import type { ToolDefinition } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The sub-tools' rendered text is passed in rather than re-derived here. This
|
|
6
|
+
* tool embeds their output verbatim, so calling three renderers from a fourth
|
|
7
|
+
* would duplicate the composition the handler already does -- and would make
|
|
8
|
+
* this function impure for no gain.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderTrainingInsightsText(payload: TrainingInsightsPayload, sleepText: string, recoveryText: string, stressText: string): string;
|
|
11
|
+
export declare function getTrainingInsights(input: {
|
|
12
|
+
days?: number;
|
|
13
|
+
}): Promise<ToolResult<TrainingInsightsPayload>>;
|
|
14
|
+
export declare const trainingInsightsToolDefinitions: ToolDefinition[];
|
|
15
|
+
//# sourceMappingURL=trainingInsights.d.ts.map
|