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,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,2 @@
1
+ export declare const packageVersion: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,cAAc,QAAc,CAAC"}
@@ -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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"}