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.
Files changed (274) hide show
  1. package/.env.example +11 -0
  2. package/CHANGELOG.md +675 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +182 -0
  5. package/README.md +322 -0
  6. package/dist/appDb.d.ts +83 -0
  7. package/dist/appDb.d.ts.map +1 -0
  8. package/dist/appDb.js +239 -0
  9. package/dist/appDb.js.map +1 -0
  10. package/dist/check.d.ts +12 -0
  11. package/dist/check.d.ts.map +1 -0
  12. package/dist/check.js +230 -0
  13. package/dist/check.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +457 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +61 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +241 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/dashboard.d.ts +8 -0
  23. package/dist/dashboard.d.ts.map +1 -0
  24. package/dist/dashboard.js +1004 -0
  25. package/dist/dashboard.js.map +1 -0
  26. package/dist/dashboardCharts.d.ts +99 -0
  27. package/dist/dashboardCharts.d.ts.map +1 -0
  28. package/dist/dashboardCharts.js +317 -0
  29. package/dist/dashboardCharts.js.map +1 -0
  30. package/dist/dashboardData.d.ts +89 -0
  31. package/dist/dashboardData.d.ts.map +1 -0
  32. package/dist/dashboardData.js +228 -0
  33. package/dist/dashboardData.js.map +1 -0
  34. package/dist/detect/baseline.d.ts +17 -0
  35. package/dist/detect/baseline.d.ts.map +1 -0
  36. package/dist/detect/baseline.js +62 -0
  37. package/dist/detect/baseline.js.map +1 -0
  38. package/dist/detect/countdown.d.ts +30 -0
  39. package/dist/detect/countdown.d.ts.map +1 -0
  40. package/dist/detect/countdown.js +79 -0
  41. package/dist/detect/countdown.js.map +1 -0
  42. package/dist/detect/detectors.d.ts +20 -0
  43. package/dist/detect/detectors.d.ts.map +1 -0
  44. package/dist/detect/detectors.js +291 -0
  45. package/dist/detect/detectors.js.map +1 -0
  46. package/dist/detect/findings.d.ts +29 -0
  47. package/dist/detect/findings.d.ts.map +1 -0
  48. package/dist/detect/findings.js +2 -0
  49. package/dist/detect/findings.js.map +1 -0
  50. package/dist/detect/forecast.d.ts +33 -0
  51. package/dist/detect/forecast.d.ts.map +1 -0
  52. package/dist/detect/forecast.js +122 -0
  53. package/dist/detect/forecast.js.map +1 -0
  54. package/dist/detect/index.d.ts +50 -0
  55. package/dist/detect/index.d.ts.map +1 -0
  56. package/dist/detect/index.js +114 -0
  57. package/dist/detect/index.js.map +1 -0
  58. package/dist/detect/sleepQuality.d.ts +21 -0
  59. package/dist/detect/sleepQuality.d.ts.map +1 -0
  60. package/dist/detect/sleepQuality.js +164 -0
  61. package/dist/detect/sleepQuality.js.map +1 -0
  62. package/dist/detect/trimp.d.ts +22 -0
  63. package/dist/detect/trimp.d.ts.map +1 -0
  64. package/dist/detect/trimp.js +77 -0
  65. package/dist/detect/trimp.js.map +1 -0
  66. package/dist/detect/week.d.ts +31 -0
  67. package/dist/detect/week.d.ts.map +1 -0
  68. package/dist/detect/week.js +184 -0
  69. package/dist/detect/week.js.map +1 -0
  70. package/dist/garmin/auth.d.ts +10 -0
  71. package/dist/garmin/auth.d.ts.map +1 -0
  72. package/dist/garmin/auth.js +81 -0
  73. package/dist/garmin/auth.js.map +1 -0
  74. package/dist/garmin/cache.d.ts +42 -0
  75. package/dist/garmin/cache.d.ts.map +1 -0
  76. package/dist/garmin/cache.js +141 -0
  77. package/dist/garmin/cache.js.map +1 -0
  78. package/dist/garmin/client.d.ts +22 -0
  79. package/dist/garmin/client.d.ts.map +1 -0
  80. package/dist/garmin/client.js +237 -0
  81. package/dist/garmin/client.js.map +1 -0
  82. package/dist/garmin/daily.d.ts +24 -0
  83. package/dist/garmin/daily.d.ts.map +1 -0
  84. package/dist/garmin/daily.js +96 -0
  85. package/dist/garmin/daily.js.map +1 -0
  86. package/dist/garmin/garminApiTypes.d.ts +70 -0
  87. package/dist/garmin/garminApiTypes.d.ts.map +1 -0
  88. package/dist/garmin/garminApiTypes.js +3 -0
  89. package/dist/garmin/garminApiTypes.js.map +1 -0
  90. package/dist/garmin/garminConnect.d.ts +17 -0
  91. package/dist/garmin/garminConnect.d.ts.map +1 -0
  92. package/dist/garmin/garminConnect.js +5 -0
  93. package/dist/garmin/garminConnect.js.map +1 -0
  94. package/dist/garmin/partial.d.ts +32 -0
  95. package/dist/garmin/partial.d.ts.map +1 -0
  96. package/dist/garmin/partial.js +38 -0
  97. package/dist/garmin/partial.js.map +1 -0
  98. package/dist/garmin/rawApi.d.ts +59 -0
  99. package/dist/garmin/rawApi.d.ts.map +1 -0
  100. package/dist/garmin/rawApi.js +113 -0
  101. package/dist/garmin/rawApi.js.map +1 -0
  102. package/dist/garmin/types.d.ts +85 -0
  103. package/dist/garmin/types.d.ts.map +1 -0
  104. package/dist/garmin/types.js +11 -0
  105. package/dist/garmin/types.js.map +1 -0
  106. package/dist/history/capture.d.ts +12 -0
  107. package/dist/history/capture.d.ts.map +1 -0
  108. package/dist/history/capture.js +63 -0
  109. package/dist/history/capture.js.map +1 -0
  110. package/dist/history/context.d.ts +51 -0
  111. package/dist/history/context.d.ts.map +1 -0
  112. package/dist/history/context.js +122 -0
  113. package/dist/history/context.js.map +1 -0
  114. package/dist/history/fallback.d.ts +72 -0
  115. package/dist/history/fallback.d.ts.map +1 -0
  116. package/dist/history/fallback.js +185 -0
  117. package/dist/history/fallback.js.map +1 -0
  118. package/dist/history/ingest.d.ts +44 -0
  119. package/dist/history/ingest.d.ts.map +1 -0
  120. package/dist/history/ingest.js +347 -0
  121. package/dist/history/ingest.js.map +1 -0
  122. package/dist/history/scheduler.d.ts +20 -0
  123. package/dist/history/scheduler.d.ts.map +1 -0
  124. package/dist/history/scheduler.js +54 -0
  125. package/dist/history/scheduler.js.map +1 -0
  126. package/dist/history/schema.d.ts +41 -0
  127. package/dist/history/schema.d.ts.map +1 -0
  128. package/dist/history/schema.js +91 -0
  129. package/dist/history/schema.js.map +1 -0
  130. package/dist/history/store.d.ts +112 -0
  131. package/dist/history/store.d.ts.map +1 -0
  132. package/dist/history/store.js +282 -0
  133. package/dist/history/store.js.map +1 -0
  134. package/dist/httpServer.d.ts +55 -0
  135. package/dist/httpServer.d.ts.map +1 -0
  136. package/dist/httpServer.js +1027 -0
  137. package/dist/httpServer.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +11 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/mcpConfig.d.ts +56 -0
  143. package/dist/mcpConfig.d.ts.map +1 -0
  144. package/dist/mcpConfig.js +106 -0
  145. package/dist/mcpConfig.js.map +1 -0
  146. package/dist/pairApi.d.ts +14 -0
  147. package/dist/pairApi.d.ts.map +1 -0
  148. package/dist/pairApi.js +38 -0
  149. package/dist/pairApi.js.map +1 -0
  150. package/dist/paths.d.ts +45 -0
  151. package/dist/paths.d.ts.map +1 -0
  152. package/dist/paths.js +105 -0
  153. package/dist/paths.js.map +1 -0
  154. package/dist/profile.d.ts +123 -0
  155. package/dist/profile.d.ts.map +1 -0
  156. package/dist/profile.js +327 -0
  157. package/dist/profile.js.map +1 -0
  158. package/dist/promptApi.d.ts +43 -0
  159. package/dist/promptApi.d.ts.map +1 -0
  160. package/dist/promptApi.js +328 -0
  161. package/dist/promptApi.js.map +1 -0
  162. package/dist/promptSuggestions.d.ts +16 -0
  163. package/dist/promptSuggestions.d.ts.map +1 -0
  164. package/dist/promptSuggestions.js +141 -0
  165. package/dist/promptSuggestions.js.map +1 -0
  166. package/dist/selfTest.d.ts +53 -0
  167. package/dist/selfTest.d.ts.map +1 -0
  168. package/dist/selfTest.js +225 -0
  169. package/dist/selfTest.js.map +1 -0
  170. package/dist/server.d.ts +10 -0
  171. package/dist/server.d.ts.map +1 -0
  172. package/dist/server.js +67 -0
  173. package/dist/server.js.map +1 -0
  174. package/dist/setup.d.ts +2 -0
  175. package/dist/setup.d.ts.map +1 -0
  176. package/dist/setup.js +210 -0
  177. package/dist/setup.js.map +1 -0
  178. package/dist/toolErrors.d.ts +2 -0
  179. package/dist/toolErrors.d.ts.map +1 -0
  180. package/dist/toolErrors.js +18 -0
  181. package/dist/toolErrors.js.map +1 -0
  182. package/dist/tools/activities.d.ts +18 -0
  183. package/dist/tools/activities.d.ts.map +1 -0
  184. package/dist/tools/activities.js +182 -0
  185. package/dist/tools/activities.js.map +1 -0
  186. package/dist/tools/bodyComposition.d.ts +10 -0
  187. package/dist/tools/bodyComposition.d.ts.map +1 -0
  188. package/dist/tools/bodyComposition.js +139 -0
  189. package/dist/tools/bodyComposition.js.map +1 -0
  190. package/dist/tools/context.d.ts +8 -0
  191. package/dist/tools/context.d.ts.map +1 -0
  192. package/dist/tools/context.js +148 -0
  193. package/dist/tools/context.js.map +1 -0
  194. package/dist/tools/findings.d.ts +7 -0
  195. package/dist/tools/findings.d.ts.map +1 -0
  196. package/dist/tools/findings.js +51 -0
  197. package/dist/tools/findings.js.map +1 -0
  198. package/dist/tools/heartRate.d.ts +10 -0
  199. package/dist/tools/heartRate.d.ts.map +1 -0
  200. package/dist/tools/heartRate.js +105 -0
  201. package/dist/tools/heartRate.js.map +1 -0
  202. package/dist/tools/index.d.ts +66 -0
  203. package/dist/tools/index.d.ts.map +1 -0
  204. package/dist/tools/index.js +101 -0
  205. package/dist/tools/index.js.map +1 -0
  206. package/dist/tools/payloads.d.ts +148 -0
  207. package/dist/tools/payloads.d.ts.map +1 -0
  208. package/dist/tools/payloads.js +2 -0
  209. package/dist/tools/payloads.js.map +1 -0
  210. package/dist/tools/recovery.d.ts +31 -0
  211. package/dist/tools/recovery.d.ts.map +1 -0
  212. package/dist/tools/recovery.js +350 -0
  213. package/dist/tools/recovery.js.map +1 -0
  214. package/dist/tools/sleep.d.ts +10 -0
  215. package/dist/tools/sleep.d.ts.map +1 -0
  216. package/dist/tools/sleep.js +124 -0
  217. package/dist/tools/sleep.js.map +1 -0
  218. package/dist/tools/stress.d.ts +11 -0
  219. package/dist/tools/stress.d.ts.map +1 -0
  220. package/dist/tools/stress.js +108 -0
  221. package/dist/tools/stress.js.map +1 -0
  222. package/dist/tools/trainingInsights.d.ts +15 -0
  223. package/dist/tools/trainingInsights.d.ts.map +1 -0
  224. package/dist/tools/trainingInsights.js +79 -0
  225. package/dist/tools/trainingInsights.js.map +1 -0
  226. package/dist/tools/types.d.ts +12 -0
  227. package/dist/tools/types.d.ts.map +1 -0
  228. package/dist/tools/types.js +2 -0
  229. package/dist/tools/types.js.map +1 -0
  230. package/dist/tools/vo2Max.d.ts +11 -0
  231. package/dist/tools/vo2Max.d.ts.map +1 -0
  232. package/dist/tools/vo2Max.js +110 -0
  233. package/dist/tools/vo2Max.js.map +1 -0
  234. package/dist/tools/week.d.ts +12 -0
  235. package/dist/tools/week.d.ts.map +1 -0
  236. package/dist/tools/week.js +74 -0
  237. package/dist/tools/week.js.map +1 -0
  238. package/dist/usage.d.ts +104 -0
  239. package/dist/usage.d.ts.map +1 -0
  240. package/dist/usage.js +247 -0
  241. package/dist/usage.js.map +1 -0
  242. package/dist/utils/batch.d.ts +2 -0
  243. package/dist/utils/batch.d.ts.map +1 -0
  244. package/dist/utils/batch.js +18 -0
  245. package/dist/utils/batch.js.map +1 -0
  246. package/dist/utils/helpers.d.ts +32 -0
  247. package/dist/utils/helpers.d.ts.map +1 -0
  248. package/dist/utils/helpers.js +188 -0
  249. package/dist/utils/helpers.js.map +1 -0
  250. package/dist/utils/jsonFile.d.ts +19 -0
  251. package/dist/utils/jsonFile.d.ts.map +1 -0
  252. package/dist/utils/jsonFile.js +24 -0
  253. package/dist/utils/jsonFile.js.map +1 -0
  254. package/dist/utils/logger.d.ts +4 -0
  255. package/dist/utils/logger.d.ts.map +1 -0
  256. package/dist/utils/logger.js +97 -0
  257. package/dist/utils/logger.js.map +1 -0
  258. package/dist/utils/secretFile.d.ts +15 -0
  259. package/dist/utils/secretFile.d.ts.map +1 -0
  260. package/dist/utils/secretFile.js +47 -0
  261. package/dist/utils/secretFile.js.map +1 -0
  262. package/dist/utils/stdio.d.ts +10 -0
  263. package/dist/utils/stdio.d.ts.map +1 -0
  264. package/dist/utils/stdio.js +70 -0
  265. package/dist/utils/stdio.js.map +1 -0
  266. package/dist/version.d.ts +2 -0
  267. package/dist/version.d.ts.map +1 -0
  268. package/dist/version.js +7 -0
  269. package/dist/version.js.map +1 -0
  270. package/dist/watchApi.d.ts +264 -0
  271. package/dist/watchApi.d.ts.map +1 -0
  272. package/dist/watchApi.js +333 -0
  273. package/dist/watchApi.js.map +1 -0
  274. package/package.json +77 -0
@@ -0,0 +1,328 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { getProfile } from "./profile.js";
3
+ import { recordAiUsage, assertWithinBudget, } from "./usage.js";
4
+ import { randomBytes } from "node:crypto";
5
+ import { DateTime } from "luxon";
6
+ import { createPromptJob, deleteSetting, getPromptJob, getSetting, listSettingKeys, setSetting, updatePromptJob, } from "./appDb.js";
7
+ import { appConfig } from "./config.js";
8
+ import { buildWatchSummary } from "./watchApi.js";
9
+ import { describeFindingsCoverage, runDetectors } from "./detect/index.js";
10
+ import { activeContext } from "./history/context.js";
11
+ import { logger } from "./utils/logger.js";
12
+ // SECTION: Prompt API — Claude integration
13
+ // The model and the answer length are no longer constants here. Both are
14
+ // profile settings now (`ai.model`, `ai.length`), because the person paying for
15
+ // the tokens is the person who should decide how many of them to buy. The
16
+ // default is still the cheapest current model, for the same reason it always
17
+ // was: these answers are two or three sentences read on a watch.
18
+ const INSIGHT_PREFIX = "daily_insight:";
19
+ /**
20
+ * The dashboard writes the key to the settings table, but the server only
21
+ * copied it into process.env at startup — so a key saved from the dashboard did
22
+ * nothing until the server was restarted. Resolving on every call fixes that.
23
+ * The stored value wins because it is the one the user set most recently
24
+ * through the UI; the environment is the fallback for headless setups.
25
+ */
26
+ function resolveAnthropicKey() {
27
+ return getSetting("anthropic_api_key") ?? appConfig.anthropicApiKey ?? "";
28
+ }
29
+ /** True when AI features are usable, from either source. */
30
+ export function isAiConfigured() {
31
+ return resolveAnthropicKey().length > 0;
32
+ }
33
+ function buildJobId() {
34
+ return randomBytes(12).toString("hex");
35
+ }
36
+ /**
37
+ * What the model is told before it writes a sentence.
38
+ *
39
+ * This used to be four current numbers, which is why the daily insight read
40
+ * like a horoscope: with nothing but today's figures there was nothing to say
41
+ * that the numbers did not already say themselves. Findings refer to things
42
+ * that happened -- a run of days, a deficit against a personal median -- and
43
+ * context says who it is happening to.
44
+ *
45
+ * Cold start is stated explicitly. Handed an empty findings list with no
46
+ * explanation, a model will confidently reassure the user out of no data at all.
47
+ */
48
+ export function formatFindingsContext(result, context) {
49
+ const lines = [];
50
+ // The same three-way classification every other surface reads. It used to be
51
+ // re-derived here from `staleDays > 3`, which is how three sibling surfaces
52
+ // went on printing the cold-start sentence at a 74-day store: the knowledge
53
+ // lived in one `if` in one file.
54
+ const coverageState = describeFindingsCoverage(result.coverage).state;
55
+ if (coverageState === "stale") {
56
+ // The record has plenty of days and stops weeks ago. Saying "nothing stands
57
+ // out" here is a claim about days that were never recorded, and it is the
58
+ // single most misleading thing this prompt can contain: the model repeats it
59
+ // as reassurance.
60
+ lines.push(`The stored record has ${result.coverage.days} days but ENDS ON ${result.coverage.throughDate}, which is ${result.coverage.staleDays} days ago. Nothing is known about the days since. Do not reassure the user that things look fine, and do not describe today: say the record is out of date and that running \`trainbud backfill\` will bring it current.`);
61
+ }
62
+ else if (coverageState === "cold") {
63
+ lines.push(`Still gathering data: only ${result.coverage.days} days of history are stored, which is not yet enough to compare anything against a baseline. Say so rather than reassuring the user.`);
64
+ }
65
+ else if (result.findings.length === 0) {
66
+ lines.push(`Across ${result.coverage.days} days of history, nothing stands out against this user's own baselines.`);
67
+ }
68
+ else {
69
+ lines.push(`What stands out, from ${result.coverage.days} days of history:`);
70
+ for (const finding of result.findings) {
71
+ lines.push(`- [${finding.severity}] ${finding.headline}`);
72
+ }
73
+ }
74
+ if (context.length === 0) {
75
+ lines.push("Nothing on record about this user's goals, races or injuries.");
76
+ }
77
+ else {
78
+ lines.push("On record about this user:");
79
+ for (const entry of context) {
80
+ lines.push(`- ${entry.kind}: ${entry.text}`);
81
+ }
82
+ }
83
+ return lines.join("\n");
84
+ }
85
+ export function formatHealthContext(summary) {
86
+ const lines = [];
87
+ if (summary.recovery) {
88
+ lines.push(`- Recovery: ${summary.recovery.score}/100 (${summary.recovery.label})`);
89
+ }
90
+ if (summary.sleep) {
91
+ lines.push(`- Sleep last night: ${summary.sleep.hours}h${summary.sleep.score ? `, score ${summary.sleep.score}` : ""} (${summary.sleep.label})`);
92
+ }
93
+ if (summary.stress) {
94
+ lines.push(`- Avg stress (7d): ${summary.stress.avg} (${summary.stress.label})`);
95
+ }
96
+ if (summary.vo2max) {
97
+ lines.push(`- VO2 Max: ${summary.vo2max.value} (${summary.vo2max.trend})`);
98
+ }
99
+ if (summary.heart_rate) {
100
+ lines.push(`- Resting HR: ${summary.heart_rate.resting} bpm`);
101
+ }
102
+ if (summary.activity) {
103
+ lines.push(`- Last activity: ${summary.activity.name}${summary.activity.duration_min ? `, ${summary.activity.duration_min}min` : ""}${summary.activity.distance_km ? `, ${summary.activity.distance_km}km` : ""}`);
104
+ }
105
+ // The week and the calendar, which change what today's numbers mean.
106
+ //
107
+ // Without these the model answers every question as though the user exists
108
+ // only today: a falling load ratio reads as detraining even when it is a
109
+ // deliberate taper, and "should I train today" gets the same answer three
110
+ // days before a race as it does in January. Both facts are in the store and
111
+ // neither was ever passed to the model.
112
+ if (summary.week && summary.week.ready) {
113
+ const week = summary.week;
114
+ const parts = [`${week.sessions} session(s) this week vs ${week.previous_sessions} last week`];
115
+ if (week.load_delta_pct !== null) {
116
+ parts.push(`training load ${week.load_delta_pct >= 0 ? "up" : "down"} ${Math.abs(week.load_delta_pct)}%`);
117
+ }
118
+ if (week.forecast_verdict !== "unknown" && week.forecast_ratio !== null) {
119
+ parts.push(`if this week repeats, the acute:chronic load ratio lands at ${week.forecast_ratio} (${week.forecast_verdict.replace(/_/g, " ")})`);
120
+ }
121
+ if (week.sleep_debt_h !== null && Math.abs(week.sleep_debt_h) >= 1) {
122
+ parts.push(`${Math.abs(week.sleep_debt_h)}h of sleep ${week.sleep_debt_h > 0 ? "debt" : "surplus"} against their own usual night`);
123
+ }
124
+ if (week.sleep_consistency !== "unknown" && week.sleep_consistency !== "steady") {
125
+ parts.push(`sleep timing is ${week.sleep_consistency}`);
126
+ }
127
+ lines.push(`- This week: ${parts.join("; ")}.`);
128
+ }
129
+ if (summary.race) {
130
+ lines.push(`- Next race: ${summary.race.text} in ${summary.race.days_away} day(s). Training phase: ${summary.race.phase.replace(/_/g, " ")}. A falling load in a taper is intended, not a lapse.`);
131
+ }
132
+ // An empty section headed "Current health snapshot:" is worse than no section.
133
+ //
134
+ // Every field above comes from a live Garmin call, and when that call fails --
135
+ // an expired session, a rate limit, no network -- all of them are null. The
136
+ // header was printed unconditionally, so the model received a heading with
137
+ // nothing under it and correctly concluded it had been given no data about the
138
+ // user. What the user then reads is "the AI cannot see my data", which is true,
139
+ // and infers "the app is broken", which is a different thing from "Garmin did
140
+ // not answer just now".
141
+ //
142
+ // Say which it is, and point the model at the stored history above, which is
143
+ // real and still worth reasoning about.
144
+ if (lines.length === 0) {
145
+ return [
146
+ "No live metrics are available right now: the request to Garmin did not return today's figures. This is a connection problem, not an absence of data about this user.",
147
+ "Say that today's numbers could not be fetched, then answer from the stored history above if it supports an answer. Never invent current values, and do not tell the user you have no access to their data.",
148
+ ].join("\n");
149
+ }
150
+ return ["Current health snapshot:", ...lines].join("\n");
151
+ }
152
+ /**
153
+ * How the answer should sound, and how long it is allowed to be.
154
+ *
155
+ * Length is two settings in one: the sentence instruction the model reads, and
156
+ * the `max_tokens` ceiling that stops a run-on answer costing more than it is
157
+ * worth. They have to move together -- raising the ceiling without changing
158
+ * the instruction just pays for the same three sentences.
159
+ */
160
+ const LENGTH_SPEC = {
161
+ short: { sentences: "2-3 short sentences maximum", maxTokens: 300 },
162
+ normal: { sentences: "4-5 sentences maximum", maxTokens: 500 },
163
+ detailed: { sentences: "up to two short paragraphs", maxTokens: 900 },
164
+ };
165
+ const TONE_SPEC = {
166
+ direct: "Be direct and actionable. No hedging, no encouragement padding.",
167
+ supportive: "Be warm and encouraging while still being specific and honest.",
168
+ technical: "Use precise physiological terms and name the metric behind each claim.",
169
+ };
170
+ /**
171
+ * Who the answer is for.
172
+ *
173
+ * Everything here comes from the profile rather than from Garmin, because
174
+ * Garmin has none of it. The same load ratio means something different to a
175
+ * lifter and a marathoner, and a model told neither will average the two.
176
+ */
177
+ function audienceContext(profile) {
178
+ const parts = [];
179
+ if (profile.displayName) {
180
+ parts.push(`The user's name is ${profile.displayName}.`);
181
+ }
182
+ if (profile.primarySport) {
183
+ parts.push(`Their primary sport is ${profile.primarySport}.`);
184
+ }
185
+ if (profile.weeklyGoal.sessions !== null) {
186
+ parts.push(`They aim for ${profile.weeklyGoal.sessions} sessions a week.`);
187
+ }
188
+ if (profile.weeklyGoal.minutes !== null) {
189
+ parts.push(`They aim for ${profile.weeklyGoal.minutes} minutes of training a week.`);
190
+ }
191
+ parts.push(profile.units === "imperial"
192
+ ? "Use imperial units (miles, pounds)."
193
+ : "Use metric units (kilometres, kilograms).");
194
+ return parts.join(" ");
195
+ }
196
+ async function callClaude(prompt, healthContext, meta) {
197
+ const apiKey = resolveAnthropicKey();
198
+ if (!apiKey) {
199
+ throw new Error("ANTHROPIC_API_KEY not configured. Set it in the dashboard or .env file.");
200
+ }
201
+ // The cap is checked before the request, not after it. Recording an
202
+ // overspend that already happened is bookkeeping; refusing the request is
203
+ // the feature. No cap set means this never throws.
204
+ assertWithinBudget();
205
+ const profile = getProfile();
206
+ const model = profile.ai.model;
207
+ const length = LENGTH_SPEC[profile.ai.length];
208
+ const client = new Anthropic({ apiKey });
209
+ const message = await client.messages.create({
210
+ model,
211
+ max_tokens: length.maxTokens,
212
+ system: `You are a concise fitness coach assistant shown on a small smartwatch screen.
213
+ Answer in ${length.sentences}. ${TONE_SPEC[profile.ai.tone]} No markdown formatting.
214
+ ${audienceContext(profile)}
215
+ Give general training and wellness guidance only. Do not diagnose conditions or give
216
+ medical advice; if asked something medical, say it is outside what you can advise on.
217
+ ${healthContext}`,
218
+ messages: [{ role: "user", content: prompt }],
219
+ });
220
+ // Recorded before the content check: the tokens were spent whether or not
221
+ // the shape of the reply was what this code expected, and a meter that only
222
+ // counts successes under-reports exactly the calls worth investigating.
223
+ recordAiUsage({
224
+ kind: meta.kind,
225
+ source: meta.source,
226
+ model,
227
+ inputTokens: message.usage.input_tokens,
228
+ outputTokens: message.usage.output_tokens,
229
+ cacheReadTokens: message.usage.cache_read_input_tokens ?? 0,
230
+ cacheWriteTokens: message.usage.cache_creation_input_tokens ?? 0,
231
+ });
232
+ const block = message.content[0];
233
+ if (block?.type !== "text") {
234
+ throw new Error("Unexpected response type from Claude");
235
+ }
236
+ return block.text.trim();
237
+ }
238
+ export function submitPrompt(prompt) {
239
+ const id = buildJobId();
240
+ createPromptJob(id, prompt);
241
+ // Fire-and-forget — process asynchronously
242
+ processPromptJob(id, prompt).catch((err) => {
243
+ logger.error({ err, id }, "Prompt job processing failed unexpectedly");
244
+ });
245
+ return { job_id: id };
246
+ }
247
+ async function processPromptJob(id, prompt) {
248
+ updatePromptJob(id, { status: "running" });
249
+ try {
250
+ const summary = await buildWatchSummary();
251
+ const detection = runDetectors();
252
+ const context = activeContext(DateTime.local().toISODate() ?? "");
253
+ const healthContext = [
254
+ formatFindingsContext(detection, context),
255
+ "",
256
+ formatHealthContext(summary),
257
+ ].join("\n");
258
+ const result = await callClaude(prompt, healthContext, { kind: "ask", source: "watch" });
259
+ updatePromptJob(id, { status: "done", result });
260
+ }
261
+ catch (err) {
262
+ const message = err instanceof Error ? err.message : String(err);
263
+ logger.error({ err, id }, "Prompt job failed");
264
+ updatePromptJob(id, { status: "error", error: message });
265
+ }
266
+ }
267
+ export function getPromptStatus(id) {
268
+ const job = getPromptJob(id);
269
+ if (!job)
270
+ return null;
271
+ return {
272
+ status: job.status,
273
+ result: job.result ?? undefined,
274
+ error: job.error ?? undefined,
275
+ };
276
+ }
277
+ /**
278
+ * The daily insight used to be generated inline on every /api/watch request, so
279
+ * a cold fetch blocked the watch for a full Claude round trip and spent API
280
+ * credit on every sync. It is now generated once per local day and cached in the
281
+ * settings table.
282
+ */
283
+ export async function generateDailyInsight(summary, options = {}) {
284
+ const apiKey = resolveAnthropicKey();
285
+ if (!apiKey)
286
+ return null;
287
+ const cacheKey = `${INSIGHT_PREFIX}${DateTime.local().toISODate()}`;
288
+ if (!options.force) {
289
+ const cached = getSetting(cacheKey);
290
+ if (cached)
291
+ return cached;
292
+ }
293
+ try {
294
+ // Both: the findings say what changed, the snapshot says where things stand.
295
+ const detection = runDetectors();
296
+ const context = activeContext(DateTime.local().toISODate() ?? "");
297
+ const healthContext = [
298
+ formatFindingsContext(detection, context),
299
+ "",
300
+ formatHealthContext(summary),
301
+ ].join("\n");
302
+ const result = await callClaude("Give me one sentence of actionable advice for today. Refer to what actually stands out rather than restating the numbers.", healthContext, { kind: "insight", source: "server" });
303
+ setSetting(cacheKey, result);
304
+ pruneOldInsights(cacheKey);
305
+ return result;
306
+ }
307
+ catch (err) {
308
+ logger.warn({ err }, "Daily insight generation failed");
309
+ // Fall back to an earlier insight rather than showing nothing on the watch.
310
+ return getSetting(cacheKey);
311
+ }
312
+ }
313
+ /** Returns today's cached insight without contacting the API. */
314
+ export function getCachedDailyInsight() {
315
+ return getSetting(`${INSIGHT_PREFIX}${DateTime.local().toISODate()}`);
316
+ }
317
+ /** Drops today's cached insight so the next request regenerates it. */
318
+ export function clearDailyInsight() {
319
+ deleteSetting(`${INSIGHT_PREFIX}${DateTime.local().toISODate()}`);
320
+ }
321
+ function pruneOldInsights(keepKey) {
322
+ for (const key of listSettingKeys(INSIGHT_PREFIX)) {
323
+ if (key !== keepKey) {
324
+ deleteSetting(key);
325
+ }
326
+ }
327
+ }
328
+ //# sourceMappingURL=promptApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptApi.js","sourceRoot":"","sources":["../src/promptApi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type DetectionResult } from "./detect/index.js";
2
+ import type { ContextEntry } from "./history/context.js";
3
+ /**
4
+ * Always exactly five. The watch lays out a fixed menu and a short list would
5
+ * leave it rendering stale entries from its cache.
6
+ */
7
+ export declare function buildPromptSuggestions(result: DetectionResult, context?: ContextEntry[], custom?: readonly string[]): string[];
8
+ /**
9
+ * Exported for the test that keeps every prompt inside the watch's width, and
10
+ * for the profile schema, which refuses to store a question the menu could
11
+ * never show. This file owns both numbers: the menu is what they describe.
12
+ */
13
+ export declare const PROMPT_MAX_LENGTH = 32;
14
+ /** How many the watch draws. Also the cap on how many the user may write. */
15
+ export declare const PROMPT_SLOTS = 5;
16
+ //# sourceMappingURL=promptSuggestions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptSuggestions.d.ts","sourceRoot":"","sources":["../src/promptSuggestions.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AA0HzD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,YAAY,EAAO,EAC5B,MAAM,GAAE,SAAS,MAAM,EAAO,GAC7B,MAAM,EAAE,CA+BV;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,KAAa,CAAC;AAE5C,6EAA6E;AAC7E,eAAO,MAAM,YAAY,IAAe,CAAC"}
@@ -0,0 +1,141 @@
1
+ import { describeFindingsCoverage } from "./detect/index.js";
2
+ // SECTION: Ask prompts
3
+ //
4
+ // The watch's Ask menu is five hardcoded strings in strings.xml -- "Should I
5
+ // train today?", "Am I overtraining?" -- and they would read exactly the same
6
+ // on an app with no memory at all. Generated from what actually fired they
7
+ // become questions only this app could have known to offer, which makes the
8
+ // menu the demo rather than a nicety.
9
+ //
10
+ // Pure: same input, same five prompts. The watch caches the summary and shows
11
+ // them again from storage, so a list that reshuffled would look like the data
12
+ // had changed when it had not.
13
+ /** Anything longer wraps or clips on a 390 px round screen. */
14
+ const MAX_LENGTH = 32;
15
+ const PROMPT_COUNT = 5;
16
+ const FROM_FINDING = {
17
+ rhr_elevated: "Why is my resting HR up?",
18
+ sleep_debt: "How do I clear sleep debt?",
19
+ hrv_trend_break: "Why is my HRV dropping?",
20
+ load_ratio_high: "Am I ramping up too fast?",
21
+ load_ratio_low: "Have I lost fitness?",
22
+ };
23
+ /**
24
+ * Ordered by how often they are worth asking with nothing else to go on. These
25
+ * fill the menu out; they never displace a question raised by real data.
26
+ */
27
+ const GENERIC = [
28
+ "Should I train today?",
29
+ "How is my recovery?",
30
+ "Summarize my week",
31
+ "How is my sleep trending?",
32
+ "What should I focus on?",
33
+ ];
34
+ const COLD_START = [
35
+ "How much data do you have?",
36
+ "What can you tell me yet?",
37
+ "Should I train today?",
38
+ "How is my recovery?",
39
+ "Summarize my week",
40
+ ];
41
+ /**
42
+ * A store with months in it that stopped three weeks ago is not a cold start,
43
+ * and offering "What can you tell me yet?" to someone with 74 days of history
44
+ * misdescribes their own data back to them. The useful questions here are about
45
+ * the record itself and about the period it does cover -- both of which this app
46
+ * can answer perfectly well without a single live request.
47
+ */
48
+ const STALE = [
49
+ "Why is my data out of date?",
50
+ "How was my last month?",
51
+ "How is my sleep trending?",
52
+ "Have I lost fitness?",
53
+ "What should I focus on?",
54
+ ];
55
+ function contextPrompt(entries) {
56
+ const race = entries.find((entry) => entry.kind === "race");
57
+ if (race) {
58
+ return "How is my race prep going?";
59
+ }
60
+ const injury = entries.find((entry) => entry.kind === "injury");
61
+ if (injury) {
62
+ return "Is my injury affecting this?";
63
+ }
64
+ const goal = entries.find((entry) => entry.kind === "goal");
65
+ if (goal) {
66
+ return "Am I on track for my goal?";
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * The user's questions, reduced to what the menu can actually show.
72
+ *
73
+ * `profile.ts` refuses a blank, an over-wide or a sixth question on write, so
74
+ * in practice nothing is dropped here. It is done anyway because the watch
75
+ * payload must not depend on when a stored row happened to be written: this
76
+ * field existed in the schema from 0.5.0 with nothing reading or validating it,
77
+ * and a row saved by hand or by an older build still has to render.
78
+ */
79
+ function usableCustom(custom) {
80
+ const chosen = [];
81
+ for (const raw of custom) {
82
+ const prompt = raw.trim();
83
+ if (!prompt || prompt.length > MAX_LENGTH || chosen.includes(prompt)) {
84
+ continue;
85
+ }
86
+ chosen.push(prompt);
87
+ if (chosen.length >= PROMPT_COUNT) {
88
+ break;
89
+ }
90
+ }
91
+ return chosen;
92
+ }
93
+ function fill(seed, pool) {
94
+ const chosen = [...seed];
95
+ for (const candidate of pool) {
96
+ if (chosen.length >= PROMPT_COUNT) {
97
+ break;
98
+ }
99
+ if (!chosen.includes(candidate)) {
100
+ chosen.push(candidate);
101
+ }
102
+ }
103
+ return chosen.slice(0, PROMPT_COUNT);
104
+ }
105
+ /**
106
+ * Always exactly five. The watch lays out a fixed menu and a short list would
107
+ * leave it rendering stale entries from its cache.
108
+ */
109
+ export function buildPromptSuggestions(result, context = [], custom = []) {
110
+ // The user's own questions lead, in every state. A question someone took the
111
+ // trouble to write is a better use of one of five slots than one this file
112
+ // guessed -- and on a cold start it is the only question here that is not
113
+ // about data the app does not have yet.
114
+ const own = usableCustom(custom);
115
+ if (!result.coverage.ready) {
116
+ return fill(own, describeFindingsCoverage(result.coverage).state === "stale" ? STALE : COLD_START);
117
+ }
118
+ const fromFindings = result.findings
119
+ .map((finding) => FROM_FINDING[finding.kind])
120
+ .filter((prompt) => typeof prompt === "string");
121
+ const seed = [...own];
122
+ for (const prompt of fromFindings) {
123
+ if (!seed.includes(prompt)) {
124
+ seed.push(prompt);
125
+ }
126
+ }
127
+ const fromContext = contextPrompt(context);
128
+ if (fromContext && !seed.includes(fromContext)) {
129
+ seed.push(fromContext);
130
+ }
131
+ return fill(seed, GENERIC);
132
+ }
133
+ /**
134
+ * Exported for the test that keeps every prompt inside the watch's width, and
135
+ * for the profile schema, which refuses to store a question the menu could
136
+ * never show. This file owns both numbers: the menu is what they describe.
137
+ */
138
+ export const PROMPT_MAX_LENGTH = MAX_LENGTH;
139
+ /** How many the watch draws. Also the cap on how many the user may write. */
140
+ export const PROMPT_SLOTS = PROMPT_COUNT;
141
+ //# sourceMappingURL=promptSuggestions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptSuggestions.js","sourceRoot":"","sources":["../src/promptSuggestions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /** Mirrors Fail.mc on the watch, so both surfaces name a failure the same way. */
2
+ export type ReachClass = "ok" | "unreachable" | "not_server" | "unauthorized" | "refused" | "not_configured";
3
+ export interface CheckLine {
4
+ name: string;
5
+ ok: boolean;
6
+ /** True for something that is not wrong yet but will bite. */
7
+ warning?: boolean;
8
+ detail: string;
9
+ /** The single next action, when there is one. */
10
+ fix?: string;
11
+ }
12
+ export interface SelfTestResult {
13
+ reach: ReachClass;
14
+ publicUrl: string;
15
+ checks: CheckLine[];
16
+ ok: boolean;
17
+ }
18
+ export interface ProbeOutcome {
19
+ reach: ReachClass;
20
+ status: number | null;
21
+ contentType: string | null;
22
+ bodyStart: string;
23
+ error?: string;
24
+ }
25
+ /**
26
+ * Fetches `${baseUrl}/health` the way the watch would.
27
+ *
28
+ * Injectable fetch so the tests never touch the network -- the previous
29
+ * generation of tests in this project reached real services and a real
30
+ * database, and both cost a debugging session.
31
+ */
32
+ export declare function probePublicUrl(baseUrl: string, fetchImpl?: typeof fetch): Promise<ProbeOutcome>;
33
+ export interface HistoryCoverage {
34
+ days: number;
35
+ ready: boolean;
36
+ throughDate: string | null;
37
+ staleDays: number;
38
+ }
39
+ /**
40
+ * Fourteen days is the threshold every detector uses before it will compare a
41
+ * day against a baseline. Depth alone is not the whole test, and reading only
42
+ * `days` here reproduced the exact bug `ready` was added to prevent: a store
43
+ * holding 74 days that stops on 08-21 answered "enough to compare a day against
44
+ * your own baselines" on 09-03. Every detector then correctly found nothing --
45
+ * because there was nothing recent to find -- and the one command whose job is
46
+ * to say what is wrong said everything was fine.
47
+ *
48
+ * Extracted so this can be tested without standing up a history database; the
49
+ * version that shipped the bug was only reachable through one.
50
+ */
51
+ export declare function describeHistoryCoverage(coverage: HistoryCoverage): CheckLine;
52
+ export declare function runSelfTest(fetchImpl?: typeof fetch): Promise<SelfTestResult>;
53
+ //# sourceMappingURL=selfTest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfTest.d.ts","sourceRoot":"","sources":["../src/selfTest.ts"],"names":[],"mappings":"AAsBA,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAClB,IAAI,GACJ,aAAa,GACb,YAAY,GACZ,cAAc,GACd,SAAS,GACT,gBAAgB,CAAC;AAErB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,EAAE,EAAE,OAAO,CAAC;CACb;AAuBD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,YAAY,CAAC,CA2CvB;AA4CD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,CAkB5E;AAED,wBAAsB,WAAW,CAC/B,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,cAAc,CAAC,CAyFzB"}