trainbud 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.env.example +11 -11
  2. package/CHANGELOG.md +719 -675
  3. package/QUICKSTART.md +185 -182
  4. package/README.md +323 -322
  5. package/dist/appDb.js +51 -51
  6. package/dist/dashboard.js +820 -820
  7. package/dist/dashboardCharts.js +42 -42
  8. package/dist/history/context.js +35 -35
  9. package/dist/history/schema.js +66 -66
  10. package/dist/promptApi.js +5 -5
  11. package/dist/usage.js +17 -17
  12. package/package.json +78 -77
  13. package/dist/appDb.d.ts.map +0 -1
  14. package/dist/appDb.js.map +0 -1
  15. package/dist/check.d.ts.map +0 -1
  16. package/dist/check.js.map +0 -1
  17. package/dist/cli.d.ts.map +0 -1
  18. package/dist/cli.js.map +0 -1
  19. package/dist/config.d.ts.map +0 -1
  20. package/dist/config.js.map +0 -1
  21. package/dist/dashboard.d.ts.map +0 -1
  22. package/dist/dashboard.js.map +0 -1
  23. package/dist/dashboardCharts.d.ts.map +0 -1
  24. package/dist/dashboardCharts.js.map +0 -1
  25. package/dist/dashboardData.d.ts.map +0 -1
  26. package/dist/dashboardData.js.map +0 -1
  27. package/dist/detect/baseline.d.ts.map +0 -1
  28. package/dist/detect/baseline.js.map +0 -1
  29. package/dist/detect/countdown.d.ts.map +0 -1
  30. package/dist/detect/countdown.js.map +0 -1
  31. package/dist/detect/detectors.d.ts.map +0 -1
  32. package/dist/detect/detectors.js.map +0 -1
  33. package/dist/detect/findings.d.ts.map +0 -1
  34. package/dist/detect/findings.js.map +0 -1
  35. package/dist/detect/forecast.d.ts.map +0 -1
  36. package/dist/detect/forecast.js.map +0 -1
  37. package/dist/detect/index.d.ts.map +0 -1
  38. package/dist/detect/index.js.map +0 -1
  39. package/dist/detect/sleepQuality.d.ts.map +0 -1
  40. package/dist/detect/sleepQuality.js.map +0 -1
  41. package/dist/detect/trimp.d.ts.map +0 -1
  42. package/dist/detect/trimp.js.map +0 -1
  43. package/dist/detect/week.d.ts.map +0 -1
  44. package/dist/detect/week.js.map +0 -1
  45. package/dist/garmin/auth.d.ts.map +0 -1
  46. package/dist/garmin/auth.js.map +0 -1
  47. package/dist/garmin/cache.d.ts.map +0 -1
  48. package/dist/garmin/cache.js.map +0 -1
  49. package/dist/garmin/client.d.ts.map +0 -1
  50. package/dist/garmin/client.js.map +0 -1
  51. package/dist/garmin/daily.d.ts.map +0 -1
  52. package/dist/garmin/daily.js.map +0 -1
  53. package/dist/garmin/garminApiTypes.d.ts.map +0 -1
  54. package/dist/garmin/garminApiTypes.js.map +0 -1
  55. package/dist/garmin/garminConnect.d.ts.map +0 -1
  56. package/dist/garmin/garminConnect.js.map +0 -1
  57. package/dist/garmin/partial.d.ts.map +0 -1
  58. package/dist/garmin/partial.js.map +0 -1
  59. package/dist/garmin/rawApi.d.ts.map +0 -1
  60. package/dist/garmin/rawApi.js.map +0 -1
  61. package/dist/garmin/types.d.ts.map +0 -1
  62. package/dist/garmin/types.js.map +0 -1
  63. package/dist/history/capture.d.ts.map +0 -1
  64. package/dist/history/capture.js.map +0 -1
  65. package/dist/history/context.d.ts.map +0 -1
  66. package/dist/history/context.js.map +0 -1
  67. package/dist/history/fallback.d.ts.map +0 -1
  68. package/dist/history/fallback.js.map +0 -1
  69. package/dist/history/ingest.d.ts.map +0 -1
  70. package/dist/history/ingest.js.map +0 -1
  71. package/dist/history/scheduler.d.ts.map +0 -1
  72. package/dist/history/scheduler.js.map +0 -1
  73. package/dist/history/schema.d.ts.map +0 -1
  74. package/dist/history/schema.js.map +0 -1
  75. package/dist/history/store.d.ts.map +0 -1
  76. package/dist/history/store.js.map +0 -1
  77. package/dist/httpServer.d.ts.map +0 -1
  78. package/dist/httpServer.js.map +0 -1
  79. package/dist/index.d.ts.map +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/mcpConfig.d.ts.map +0 -1
  82. package/dist/mcpConfig.js.map +0 -1
  83. package/dist/pairApi.d.ts.map +0 -1
  84. package/dist/pairApi.js.map +0 -1
  85. package/dist/paths.d.ts.map +0 -1
  86. package/dist/paths.js.map +0 -1
  87. package/dist/profile.d.ts.map +0 -1
  88. package/dist/profile.js.map +0 -1
  89. package/dist/promptApi.d.ts.map +0 -1
  90. package/dist/promptApi.js.map +0 -1
  91. package/dist/promptSuggestions.d.ts.map +0 -1
  92. package/dist/promptSuggestions.js.map +0 -1
  93. package/dist/selfTest.d.ts.map +0 -1
  94. package/dist/selfTest.js.map +0 -1
  95. package/dist/server.d.ts.map +0 -1
  96. package/dist/server.js.map +0 -1
  97. package/dist/setup.d.ts.map +0 -1
  98. package/dist/setup.js.map +0 -1
  99. package/dist/toolErrors.d.ts.map +0 -1
  100. package/dist/toolErrors.js.map +0 -1
  101. package/dist/tools/activities.d.ts.map +0 -1
  102. package/dist/tools/activities.js.map +0 -1
  103. package/dist/tools/bodyComposition.d.ts.map +0 -1
  104. package/dist/tools/bodyComposition.js.map +0 -1
  105. package/dist/tools/context.d.ts.map +0 -1
  106. package/dist/tools/context.js.map +0 -1
  107. package/dist/tools/findings.d.ts.map +0 -1
  108. package/dist/tools/findings.js.map +0 -1
  109. package/dist/tools/heartRate.d.ts.map +0 -1
  110. package/dist/tools/heartRate.js.map +0 -1
  111. package/dist/tools/index.d.ts.map +0 -1
  112. package/dist/tools/index.js.map +0 -1
  113. package/dist/tools/payloads.d.ts.map +0 -1
  114. package/dist/tools/payloads.js.map +0 -1
  115. package/dist/tools/recovery.d.ts.map +0 -1
  116. package/dist/tools/recovery.js.map +0 -1
  117. package/dist/tools/sleep.d.ts.map +0 -1
  118. package/dist/tools/sleep.js.map +0 -1
  119. package/dist/tools/stress.d.ts.map +0 -1
  120. package/dist/tools/stress.js.map +0 -1
  121. package/dist/tools/trainingInsights.d.ts.map +0 -1
  122. package/dist/tools/trainingInsights.js.map +0 -1
  123. package/dist/tools/types.d.ts.map +0 -1
  124. package/dist/tools/types.js.map +0 -1
  125. package/dist/tools/vo2Max.d.ts.map +0 -1
  126. package/dist/tools/vo2Max.js.map +0 -1
  127. package/dist/tools/week.d.ts.map +0 -1
  128. package/dist/tools/week.js.map +0 -1
  129. package/dist/usage.d.ts.map +0 -1
  130. package/dist/usage.js.map +0 -1
  131. package/dist/utils/batch.d.ts.map +0 -1
  132. package/dist/utils/batch.js.map +0 -1
  133. package/dist/utils/helpers.d.ts.map +0 -1
  134. package/dist/utils/helpers.js.map +0 -1
  135. package/dist/utils/jsonFile.d.ts.map +0 -1
  136. package/dist/utils/jsonFile.js.map +0 -1
  137. package/dist/utils/logger.d.ts.map +0 -1
  138. package/dist/utils/logger.js.map +0 -1
  139. package/dist/utils/secretFile.d.ts.map +0 -1
  140. package/dist/utils/secretFile.js.map +0 -1
  141. package/dist/utils/stdio.d.ts.map +0 -1
  142. package/dist/utils/stdio.js.map +0 -1
  143. package/dist/version.d.ts.map +0 -1
  144. package/dist/version.js.map +0 -1
  145. package/dist/watchApi.d.ts.map +0 -1
  146. package/dist/watchApi.js.map +0 -1
@@ -58,9 +58,9 @@ function round(value, places = 2) {
58
58
  * whole file exists to preserve.
59
59
  */
60
60
  function emptyChart(width, height, message) {
61
- return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(message)}" preserveAspectRatio="none">
62
- <text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle"
63
- fill="${CHART_COLORS.muted}" font-size="13">${escapeXml(message)}</text>
61
+ return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(message)}" preserveAspectRatio="none">
62
+ <text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle"
63
+ fill="${CHART_COLORS.muted}" font-size="13">${escapeXml(message)}</text>
64
64
  </svg>`;
65
65
  }
66
66
  /**
@@ -152,30 +152,30 @@ export function lineChart(points, options) {
152
152
  const last = [...points].reverse().find((p) => p.value !== null);
153
153
  const lastIndex = last ? points.findIndex((p) => p.date === last.date) : -1;
154
154
  const baselineMarkup = options.baseline
155
- ? `<line x1="${padLeft}" x2="${width - padRight}" y1="${round(y(options.baseline.value))}" y2="${round(y(options.baseline.value))}"
155
+ ? `<line x1="${padLeft}" x2="${width - padRight}" y1="${round(y(options.baseline.value))}" y2="${round(y(options.baseline.value))}"
156
156
  stroke="${CHART_COLORS.muted}" stroke-width="1" stroke-dasharray="3 3" opacity="0.7"><title>${escapeXml(options.baseline.label)}</title></line>`
157
157
  : "";
158
158
  const dots = points
159
159
  .map((point, index) => point.value === null
160
160
  ? ""
161
- : `<circle cx="${round(x(index))}" cy="${round(y(point.value))}" r="4"
162
- fill="${CHART_COLORS.series}" stroke="${CHART_COLORS.surface}" stroke-width="2"
161
+ : `<circle cx="${round(x(index))}" cy="${round(y(point.value))}" r="4"
162
+ fill="${CHART_COLORS.series}" stroke="${CHART_COLORS.surface}" stroke-width="2"
163
163
  class="dot"><title>${escapeXml(point.date)}: ${escapeXml(format(point.value))}${escapeXml(options.unit ?? "")}</title></circle>`)
164
164
  .join("");
165
- return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img"
166
- aria-label="${escapeXml(options.label)}" preserveAspectRatio="none">
167
- <line x1="${padLeft}" x2="${width - padRight}" y1="${padTop}" y2="${padTop}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
168
- <line x1="${padLeft}" x2="${width - padRight}" y1="${padTop + plotHeight}" y2="${padTop + plotHeight}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
169
- <text x="4" y="${padTop + 4}" fill="${CHART_COLORS.muted}" font-size="10">${escapeXml(format(scale.max))}</text>
170
- <text x="4" y="${padTop + plotHeight}" fill="${CHART_COLORS.muted}" font-size="10">${escapeXml(format(scale.min))}</text>
171
- ${baselineMarkup}
172
- <path d="${segments.join(" ")}" fill="none" stroke="${CHART_COLORS.series}" stroke-width="2"
173
- stroke-linejoin="round" stroke-linecap="round"/>
174
- ${dots}
165
+ return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img"
166
+ aria-label="${escapeXml(options.label)}" preserveAspectRatio="none">
167
+ <line x1="${padLeft}" x2="${width - padRight}" y1="${padTop}" y2="${padTop}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
168
+ <line x1="${padLeft}" x2="${width - padRight}" y1="${padTop + plotHeight}" y2="${padTop + plotHeight}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
169
+ <text x="4" y="${padTop + 4}" fill="${CHART_COLORS.muted}" font-size="10">${escapeXml(format(scale.max))}</text>
170
+ <text x="4" y="${padTop + plotHeight}" fill="${CHART_COLORS.muted}" font-size="10">${escapeXml(format(scale.min))}</text>
171
+ ${baselineMarkup}
172
+ <path d="${segments.join(" ")}" fill="none" stroke="${CHART_COLORS.series}" stroke-width="2"
173
+ stroke-linejoin="round" stroke-linecap="round"/>
174
+ ${dots}
175
175
  ${lastIndex >= 0 && last?.value !== null && last !== undefined
176
- ? `<text x="${width - padRight}" y="${round(y(last.value)) - 8}" text-anchor="end"
176
+ ? `<text x="${width - padRight}" y="${round(y(last.value)) - 8}" text-anchor="end"
177
177
  fill="${CHART_COLORS.ink}" font-size="11">${escapeXml(format(last.value))}${escapeXml(options.unit ?? "")}</text>`
178
- : ""}
178
+ : ""}
179
179
  </svg>`;
180
180
  }
181
181
  /**
@@ -204,9 +204,9 @@ export function columnChart(points, options) {
204
204
  // data and left a hole in the page.
205
205
  if (maxValue <= 0) {
206
206
  const flat = 54;
207
- return `<svg class="chart" viewBox="0 0 ${width} ${flat}" role="img" aria-label="${escapeXml(options.label)}" preserveAspectRatio="none">
208
- <line x1="${padLeft}" x2="${width - padRight}" y1="${flat - 14}" y2="${flat - 14}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
209
- <text x="${width / 2}" y="${flat / 2 - 4}" text-anchor="middle" fill="${CHART_COLORS.muted}" font-size="12">Nothing spent</text>
207
+ return `<svg class="chart" viewBox="0 0 ${width} ${flat}" role="img" aria-label="${escapeXml(options.label)}" preserveAspectRatio="none">
208
+ <line x1="${padLeft}" x2="${width - padRight}" y1="${flat - 14}" y2="${flat - 14}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
209
+ <text x="${width / 2}" y="${flat / 2 - 4}" text-anchor="middle" fill="${CHART_COLORS.muted}" font-size="12">Nothing spent</text>
210
210
  </svg>`;
211
211
  }
212
212
  const plotWidth = width - padLeft - padRight;
@@ -223,22 +223,22 @@ export function columnChart(points, options) {
223
223
  if (barHeight <= 0) {
224
224
  // A recorded zero still gets a hit target, so hovering an empty day
225
225
  // says "nothing on this day" rather than nothing at all.
226
- return `<rect x="${bx}" y="${round(padTop + plotHeight - 1)}" width="${round(barWidth)}" height="1"
226
+ return `<rect x="${bx}" y="${round(padTop + plotHeight - 1)}" width="${round(barWidth)}" height="1"
227
227
  fill="${CHART_COLORS.grid}" class="bar"><title>${title}</title></rect>`;
228
228
  }
229
- return `<rect x="${bx}" y="${by}" width="${round(barWidth)}" height="${round(barHeight)}"
229
+ return `<rect x="${bx}" y="${by}" width="${round(barWidth)}" height="${round(barHeight)}"
230
230
  rx="${round(radius)}" fill="${CHART_COLORS.series}" class="bar"><title>${title}</title></rect>`;
231
231
  })
232
232
  .join("");
233
233
  const peakIndex = points.findIndex((p) => p.value === maxValue);
234
234
  const peakX = round(padLeft + peakIndex * slot + slot / 2);
235
235
  const peakY = round(padTop + plotHeight - (maxValue / maxValue) * plotHeight) - 4;
236
- return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img"
237
- aria-label="${escapeXml(options.label)}" preserveAspectRatio="none">
238
- <line x1="${padLeft}" x2="${width - padRight}" y1="${padTop + plotHeight}" y2="${padTop + plotHeight}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
239
- <text x="4" y="${padTop + 4}" fill="${CHART_COLORS.muted}" font-size="10">${escapeXml(format(maxValue))}</text>
240
- ${bars}
241
- <text x="${peakX}" y="${peakY}" text-anchor="middle" fill="${CHART_COLORS.ink}" font-size="11">${escapeXml(format(maxValue))}</text>
236
+ return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img"
237
+ aria-label="${escapeXml(options.label)}" preserveAspectRatio="none">
238
+ <line x1="${padLeft}" x2="${width - padRight}" y1="${padTop + plotHeight}" y2="${padTop + plotHeight}" stroke="${CHART_COLORS.grid}" stroke-width="1"/>
239
+ <text x="4" y="${padTop + 4}" fill="${CHART_COLORS.muted}" font-size="10">${escapeXml(format(maxValue))}</text>
240
+ ${bars}
241
+ <text x="${peakX}" y="${peakY}" text-anchor="middle" fill="${CHART_COLORS.ink}" font-size="11">${escapeXml(format(maxValue))}</text>
242
242
  </svg>`;
243
243
  }
244
244
  /**
@@ -288,30 +288,30 @@ export function dumbbellChart(rows, options = {}) {
288
288
  const px = labelWidth + (previous / max) * trackWidth;
289
289
  const cx = labelWidth + (current / max) * trackWidth;
290
290
  const y = firstRowY + index * rowHeight;
291
- return `
292
- <text x="0" y="${y + 4}" fill="${CHART_COLORS.muted}" font-size="11">${escapeXml(row.label)}</text>
293
- <line x1="${round(px)}" x2="${round(cx)}" y1="${y}" y2="${y}" stroke="${CHART_COLORS.grid}" stroke-width="2" stroke-linecap="round"/>
291
+ return `
292
+ <text x="0" y="${y + 4}" fill="${CHART_COLORS.muted}" font-size="11">${escapeXml(row.label)}</text>
293
+ <line x1="${round(px)}" x2="${round(cx)}" y1="${y}" y2="${y}" stroke="${CHART_COLORS.grid}" stroke-width="2" stroke-linecap="round"/>
294
294
  ${unchanged
295
295
  ? // Both weeks land on the same point. Drawn as a grey halo behind the
296
296
  // accent dot: with the normal marks, this week's 2px surface ring
297
297
  // covers last week's dot completely and "no change" renders as "last
298
298
  // week is missing" -- an absence, which is the one thing this codebase
299
299
  // must never draw a measurement as.
300
- `<circle cx="${round(cx)}" cy="${y}" r="8" fill="${CHART_COLORS.muted}" opacity="0.45"><title>No change: ${escapeXml(String(current))}${escapeXml(row.unit)}</title></circle>
300
+ `<circle cx="${round(cx)}" cy="${y}" r="8" fill="${CHART_COLORS.muted}" opacity="0.45"><title>No change: ${escapeXml(String(current))}${escapeXml(row.unit)}</title></circle>
301
301
  <circle cx="${round(cx)}" cy="${y}" r="4" fill="${CHART_COLORS.series}"><title>This week: ${escapeXml(String(current))}${escapeXml(row.unit)}</title></circle>`
302
- : `<circle cx="${round(px)}" cy="${y}" r="4" fill="${CHART_COLORS.muted}" stroke="${CHART_COLORS.surface}" stroke-width="2"><title>Last week: ${escapeXml(String(previous))}${escapeXml(row.unit)}</title></circle>
303
- <circle cx="${round(cx)}" cy="${y}" r="5" fill="${CHART_COLORS.series}" stroke="${CHART_COLORS.surface}" stroke-width="2"><title>This week: ${escapeXml(String(current))}${escapeXml(row.unit)}</title></circle>`}
302
+ : `<circle cx="${round(px)}" cy="${y}" r="4" fill="${CHART_COLORS.muted}" stroke="${CHART_COLORS.surface}" stroke-width="2"><title>Last week: ${escapeXml(String(previous))}${escapeXml(row.unit)}</title></circle>
303
+ <circle cx="${round(cx)}" cy="${y}" r="5" fill="${CHART_COLORS.series}" stroke="${CHART_COLORS.surface}" stroke-width="2"><title>This week: ${escapeXml(String(current))}${escapeXml(row.unit)}</title></circle>`}
304
304
  <text x="${width}" y="${y + 4}" text-anchor="end" fill="${CHART_COLORS.ink}" font-size="11">${escapeXml(String(current))}${escapeXml(row.unit)}</text>`;
305
305
  })
306
306
  .join("");
307
307
  // Two marks means a legend, always. Identity never rests on colour alone.
308
- return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img"
309
- aria-label="This week against last week" preserveAspectRatio="none">
310
- <circle cx="4" cy="6" r="4" fill="${CHART_COLORS.muted}"/>
311
- <text x="13" y="10" fill="${CHART_COLORS.muted}" font-size="10">last week</text>
312
- <circle cx="76" cy="6" r="5" fill="${CHART_COLORS.series}"/>
313
- <text x="86" y="10" fill="${CHART_COLORS.muted}" font-size="10">this week</text>
314
- ${body}
308
+ return `<svg class="chart" viewBox="0 0 ${width} ${height}" role="img"
309
+ aria-label="This week against last week" preserveAspectRatio="none">
310
+ <circle cx="4" cy="6" r="4" fill="${CHART_COLORS.muted}"/>
311
+ <text x="13" y="10" fill="${CHART_COLORS.muted}" font-size="10">last week</text>
312
+ <circle cx="76" cy="6" r="5" fill="${CHART_COLORS.series}"/>
313
+ <text x="86" y="10" fill="${CHART_COLORS.muted}" font-size="10">this week</text>
314
+ ${body}
315
315
  </svg>`;
316
316
  }
317
317
  //# sourceMappingURL=dashboardCharts.js.map
@@ -28,7 +28,7 @@ export function addContextEntry(kind, text, options = {}) {
28
28
  createdAt: nowSeconds(),
29
29
  };
30
30
  const result = getHistoryDb()
31
- .prepare(`INSERT INTO context_entry (kind, text, effective_from, effective_to, created_at)
31
+ .prepare(`INSERT INTO context_entry (kind, text, effective_from, effective_to, created_at)
32
32
  VALUES (?, ?, ?, ?, ?)`)
33
33
  .run(entry.kind, entry.text, entry.effectiveFrom, entry.effectiveTo, entry.createdAt);
34
34
  return { id: Number(result.lastInsertRowid), ...entry };
@@ -36,16 +36,16 @@ export function addContextEntry(kind, text, options = {}) {
36
36
  /** Everything true on the given date, newest first. */
37
37
  export function activeContext(onDate) {
38
38
  return getHistoryDb()
39
- .prepare(`SELECT
40
- id,
41
- kind,
42
- text,
43
- effective_from AS effectiveFrom,
44
- effective_to AS effectiveTo,
45
- created_at AS createdAt
46
- FROM context_entry
47
- WHERE effective_from <= ?
48
- AND (effective_to IS NULL OR effective_to > ?)
39
+ .prepare(`SELECT
40
+ id,
41
+ kind,
42
+ text,
43
+ effective_from AS effectiveFrom,
44
+ effective_to AS effectiveTo,
45
+ created_at AS createdAt
46
+ FROM context_entry
47
+ WHERE effective_from <= ?
48
+ AND (effective_to IS NULL OR effective_to > ?)
49
49
  ORDER BY effective_from DESC, id DESC`)
50
50
  .all(onDate, onDate);
51
51
  }
@@ -61,29 +61,29 @@ export function activeContext(onDate) {
61
61
  */
62
62
  export function upcomingContext(onDate) {
63
63
  return getHistoryDb()
64
- .prepare(`SELECT
65
- id,
66
- kind,
67
- text,
68
- effective_from AS effectiveFrom,
69
- effective_to AS effectiveTo,
70
- created_at AS createdAt
71
- FROM context_entry
72
- WHERE effective_from > ?
64
+ .prepare(`SELECT
65
+ id,
66
+ kind,
67
+ text,
68
+ effective_from AS effectiveFrom,
69
+ effective_to AS effectiveTo,
70
+ created_at AS createdAt
71
+ FROM context_entry
72
+ WHERE effective_from > ?
73
73
  ORDER BY effective_from ASC, id ASC`)
74
74
  .all(onDate);
75
75
  }
76
76
  /** Every entry ever recorded, newest first — for a dashboard or a tool listing. */
77
77
  export function allContext() {
78
78
  return getHistoryDb()
79
- .prepare(`SELECT
80
- id,
81
- kind,
82
- text,
83
- effective_from AS effectiveFrom,
84
- effective_to AS effectiveTo,
85
- created_at AS createdAt
86
- FROM context_entry
79
+ .prepare(`SELECT
80
+ id,
81
+ kind,
82
+ text,
83
+ effective_from AS effectiveFrom,
84
+ effective_to AS effectiveTo,
85
+ created_at AS createdAt
86
+ FROM context_entry
87
87
  ORDER BY effective_from DESC, id DESC`)
88
88
  .all();
89
89
  }
@@ -104,18 +104,18 @@ export function logSubjective(date, kind, value, note) {
104
104
  throw new Error(`A ${kind} rating must be a number between ${SUBJECTIVE_MIN} and ${SUBJECTIVE_MAX}.`);
105
105
  }
106
106
  getHistoryDb()
107
- .prepare(`INSERT INTO subjective (date, kind, value, note, created_at)
108
- VALUES (?, ?, ?, ?, ?)
109
- ON CONFLICT(date, kind) DO UPDATE SET
110
- value = excluded.value,
111
- note = excluded.note,
107
+ .prepare(`INSERT INTO subjective (date, kind, value, note, created_at)
108
+ VALUES (?, ?, ?, ?, ?)
109
+ ON CONFLICT(date, kind) DO UPDATE SET
110
+ value = excluded.value,
111
+ note = excluded.note,
112
112
  created_at = excluded.created_at`)
113
113
  .run(date, kind, value, note?.trim() || null, nowSeconds());
114
114
  }
115
115
  export function subjectiveSeries(kind, startDate, endDate) {
116
116
  return getHistoryDb()
117
- .prepare(`SELECT date, value, note FROM subjective
118
- WHERE kind = ? AND date >= ? AND date <= ?
117
+ .prepare(`SELECT date, value, note FROM subjective
118
+ WHERE kind = ? AND date >= ? AND date <= ?
119
119
  ORDER BY date ASC`)
120
120
  .all(kind, startDate, endDate);
121
121
  }
@@ -21,71 +21,71 @@ export const SUBJECTIVE_KINDS = ["rpe", "soreness", "mood"];
21
21
  * Connect does not send, and re-deriving from the archive beats re-fetching a
22
22
  * year against an unofficial API.
23
23
  */
24
- export const HISTORY_SCHEMA = `
25
- CREATE TABLE IF NOT EXISTS daily_metric (
26
- date TEXT NOT NULL,
27
- kind TEXT NOT NULL,
28
- value REAL NOT NULL,
29
- fetched_at INTEGER NOT NULL,
30
- PRIMARY KEY (date, kind)
31
- );
32
-
33
- CREATE TABLE IF NOT EXISTS raw_payload (
34
- id INTEGER PRIMARY KEY AUTOINCREMENT,
35
- date TEXT NOT NULL,
36
- source TEXT NOT NULL,
37
- fetched_at INTEGER NOT NULL,
38
- json TEXT NOT NULL
39
- );
40
-
41
- CREATE INDEX IF NOT EXISTS raw_payload_date_source
42
- ON raw_payload (date, source, fetched_at);
43
-
44
- CREATE TABLE IF NOT EXISTS ingest_day (
45
- date TEXT NOT NULL,
46
- source TEXT NOT NULL,
47
- fetched_at INTEGER NOT NULL,
48
- outcome TEXT NOT NULL,
49
- PRIMARY KEY (date, source)
50
- );
51
-
52
- CREATE TABLE IF NOT EXISTS activity (
53
- activity_id INTEGER PRIMARY KEY,
54
- date TEXT NOT NULL,
55
- start_time_local TEXT NOT NULL,
56
- name TEXT NOT NULL,
57
- type TEXT NOT NULL,
58
- distance_m REAL,
59
- duration_s REAL,
60
- avg_hr REAL,
61
- max_hr REAL,
62
- elevation_gain_m REAL,
63
- calories REAL,
64
- avg_speed_mps REAL,
65
- fetched_at INTEGER NOT NULL
66
- );
67
-
68
- CREATE INDEX IF NOT EXISTS activity_date ON activity (date);
69
-
70
- CREATE TABLE IF NOT EXISTS context_entry (
71
- id INTEGER PRIMARY KEY AUTOINCREMENT,
72
- kind TEXT NOT NULL,
73
- text TEXT NOT NULL,
74
- effective_from TEXT NOT NULL,
75
- effective_to TEXT,
76
- created_at INTEGER NOT NULL
77
- );
78
-
79
- CREATE INDEX IF NOT EXISTS context_entry_range
80
- ON context_entry (effective_from, effective_to);
81
-
82
- CREATE TABLE IF NOT EXISTS subjective (
83
- date TEXT NOT NULL,
84
- kind TEXT NOT NULL,
85
- value REAL NOT NULL,
86
- note TEXT,
87
- created_at INTEGER NOT NULL,
88
- PRIMARY KEY (date, kind)
89
- );
24
+ export const HISTORY_SCHEMA = `
25
+ CREATE TABLE IF NOT EXISTS daily_metric (
26
+ date TEXT NOT NULL,
27
+ kind TEXT NOT NULL,
28
+ value REAL NOT NULL,
29
+ fetched_at INTEGER NOT NULL,
30
+ PRIMARY KEY (date, kind)
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS raw_payload (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ date TEXT NOT NULL,
36
+ source TEXT NOT NULL,
37
+ fetched_at INTEGER NOT NULL,
38
+ json TEXT NOT NULL
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS raw_payload_date_source
42
+ ON raw_payload (date, source, fetched_at);
43
+
44
+ CREATE TABLE IF NOT EXISTS ingest_day (
45
+ date TEXT NOT NULL,
46
+ source TEXT NOT NULL,
47
+ fetched_at INTEGER NOT NULL,
48
+ outcome TEXT NOT NULL,
49
+ PRIMARY KEY (date, source)
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS activity (
53
+ activity_id INTEGER PRIMARY KEY,
54
+ date TEXT NOT NULL,
55
+ start_time_local TEXT NOT NULL,
56
+ name TEXT NOT NULL,
57
+ type TEXT NOT NULL,
58
+ distance_m REAL,
59
+ duration_s REAL,
60
+ avg_hr REAL,
61
+ max_hr REAL,
62
+ elevation_gain_m REAL,
63
+ calories REAL,
64
+ avg_speed_mps REAL,
65
+ fetched_at INTEGER NOT NULL
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS activity_date ON activity (date);
69
+
70
+ CREATE TABLE IF NOT EXISTS context_entry (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ kind TEXT NOT NULL,
73
+ text TEXT NOT NULL,
74
+ effective_from TEXT NOT NULL,
75
+ effective_to TEXT,
76
+ created_at INTEGER NOT NULL
77
+ );
78
+
79
+ CREATE INDEX IF NOT EXISTS context_entry_range
80
+ ON context_entry (effective_from, effective_to);
81
+
82
+ CREATE TABLE IF NOT EXISTS subjective (
83
+ date TEXT NOT NULL,
84
+ kind TEXT NOT NULL,
85
+ value REAL NOT NULL,
86
+ note TEXT,
87
+ created_at INTEGER NOT NULL,
88
+ PRIMARY KEY (date, kind)
89
+ );
90
90
  `;
91
91
  //# sourceMappingURL=schema.js.map
package/dist/promptApi.js CHANGED
@@ -209,11 +209,11 @@ async function callClaude(prompt, healthContext, meta) {
209
209
  const message = await client.messages.create({
210
210
  model,
211
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.
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
217
  ${healthContext}`,
218
218
  messages: [{ role: "user", content: prompt }],
219
219
  });
package/dist/usage.js CHANGED
@@ -35,9 +35,9 @@ export function recordAiUsage(input) {
35
35
  const row = { ...input, at, costUsd };
36
36
  try {
37
37
  getAppDb()
38
- .prepare(`INSERT INTO ai_usage
39
- (at, kind, model, source, input_tokens, output_tokens,
40
- cache_read_tokens, cache_write_tokens, cost_usd)
38
+ .prepare(`INSERT INTO ai_usage
39
+ (at, kind, model, source, input_tokens, output_tokens,
40
+ cache_read_tokens, cache_write_tokens, cost_usd)
41
41
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
42
42
  .run(at, input.kind, input.model, input.source, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, costUsd);
43
43
  }
@@ -50,12 +50,12 @@ export function recordAiUsage(input) {
50
50
  export function aiSpendSince(sinceUnixSeconds) {
51
51
  try {
52
52
  const row = getAppDb()
53
- .prepare(`SELECT COUNT(*) AS calls,
54
- COALESCE(SUM(input_tokens), 0) AS input_tokens,
55
- COALESCE(SUM(output_tokens), 0) AS output_tokens,
56
- COALESCE(SUM(cost_usd), 0) AS cost_usd,
57
- COALESCE(SUM(cost_usd IS NULL), 0) AS unpriced
58
- FROM ai_usage
53
+ .prepare(`SELECT COUNT(*) AS calls,
54
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
55
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
56
+ COALESCE(SUM(cost_usd), 0) AS cost_usd,
57
+ COALESCE(SUM(cost_usd IS NULL), 0) AS unpriced
58
+ FROM ai_usage
59
59
  WHERE at >= ?`)
60
60
  .get(sinceUnixSeconds);
61
61
  return {
@@ -121,9 +121,9 @@ export function assertWithinBudget() {
121
121
  export function recentAiUsage(limit = 20) {
122
122
  try {
123
123
  const rows = getAppDb()
124
- .prepare(`SELECT at, kind, model, source, input_tokens, output_tokens, cost_usd
125
- FROM ai_usage
126
- ORDER BY at DESC, id DESC
124
+ .prepare(`SELECT at, kind, model, source, input_tokens, output_tokens, cost_usd
125
+ FROM ai_usage
126
+ ORDER BY at DESC, id DESC
127
127
  LIMIT ?`)
128
128
  .all(limit);
129
129
  return rows.map((row) => ({
@@ -194,7 +194,7 @@ export function recordFeature(name) {
194
194
  }
195
195
  const trimmed = name.slice(0, 64);
196
196
  getAppDb()
197
- .prepare(`INSERT INTO feature_usage (name, day, count) VALUES (?, ?, 1)
197
+ .prepare(`INSERT INTO feature_usage (name, day, count) VALUES (?, ?, 1)
198
198
  ON CONFLICT(name, day) DO UPDATE SET count = count + 1`)
199
199
  .run(trimmed, day);
200
200
  }
@@ -209,10 +209,10 @@ export function featureCounts(days = 30) {
209
209
  return [];
210
210
  }
211
211
  return getAppDb()
212
- .prepare(`SELECT name, SUM(count) AS count
213
- FROM feature_usage
214
- WHERE day >= ?
215
- GROUP BY name
212
+ .prepare(`SELECT name, SUM(count) AS count
213
+ FROM feature_usage
214
+ WHERE day >= ?
215
+ GROUP BY name
216
216
  ORDER BY count DESC, name ASC`)
217
217
  .all(since);
218
218
  }
package/package.json CHANGED
@@ -1,77 +1,78 @@
1
- {
2
- "name": "trainbud",
3
- "version": "0.5.0",
4
- "description": "TrainBud \u2014 talk to your Connect fitness data through Claude and other MCP clients",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "files": [
8
- "dist",
9
- "README.md",
10
- "QUICKSTART.md",
11
- "CHANGELOG.md",
12
- "LICENSE",
13
- ".env.example"
14
- ],
15
- "bin": {
16
- "trainbud": "dist/index.js"
17
- },
18
- "scripts": {
19
- "build": "tsc",
20
- "dev": "tsx src/index.ts start",
21
- "start": "node dist/index.js start",
22
- "test": "node scripts/run-tests.mjs",
23
- "test:vitest": "vitest run",
24
- "test:watch": "vitest",
25
- "test:coverage": "vitest run --coverage",
26
- "lint": "eslint src tests",
27
- "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
28
- "prepublishOnly": "npm run build"
29
- },
30
- "keywords": [
31
- "garmin",
32
- "garmin-connect",
33
- "mcp",
34
- "model-context-protocol",
35
- "claude",
36
- "fitness",
37
- "health",
38
- "trainbud"
39
- ],
40
- "author": "Zsadigzade",
41
- "license": "MIT",
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/Zsadigzade/trainbud.git"
45
- },
46
- "homepage": "https://github.com/Zsadigzade/trainbud#readme",
47
- "bugs": {
48
- "url": "https://github.com/Zsadigzade/trainbud/issues"
49
- },
50
- "engines": {
51
- "node": ">=20.0.0"
52
- },
53
- "devDependencies": {
54
- "@eslint/js": "^10.0.1",
55
- "@types/better-sqlite3": "^7.6.13",
56
- "@types/luxon": "^3.7.2",
57
- "@types/node": "^26.0.1",
58
- "@vitest/coverage-v8": "^4.1.9",
59
- "eslint": "^10.5.0",
60
- "tsx": "^4.22.4",
61
- "typescript": "^6.0.3",
62
- "typescript-eslint": "^8.62.0",
63
- "vitest": "^4.1.9"
64
- },
65
- "dependencies": {
66
- "@anthropic-ai/sdk": "^0.106.0",
67
- "@modelcontextprotocol/sdk": "^1.29.0",
68
- "better-sqlite3": "^12.11.1",
69
- "commander": "^15.0.0",
70
- "dotenv": "^17.4.2",
71
- "garmin-connect": "^1.6.2",
72
- "luxon": "^3.7.2",
73
- "pino": "^10.3.1",
74
- "pino-pretty": "^13.1.3",
75
- "zod": "^4.4.3"
76
- }
77
- }
1
+ {
2
+ "name": "trainbud",
3
+ "version": "0.5.1",
4
+ "description": "TrainBud \u2014 talk to your Connect fitness data through Claude and other MCP clients",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "!dist/**/*.map",
10
+ "README.md",
11
+ "QUICKSTART.md",
12
+ "CHANGELOG.md",
13
+ "LICENSE",
14
+ ".env.example"
15
+ ],
16
+ "bin": {
17
+ "trainbud": "dist/index.js"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsx src/index.ts start",
22
+ "start": "node dist/index.js start",
23
+ "test": "node scripts/run-tests.mjs",
24
+ "test:vitest": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "lint": "eslint src tests",
28
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "garmin",
33
+ "garmin-connect",
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "claude",
37
+ "fitness",
38
+ "health",
39
+ "trainbud"
40
+ ],
41
+ "author": "Zsadigzade",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/Zsadigzade/trainbud.git"
46
+ },
47
+ "homepage": "https://github.com/Zsadigzade/trainbud#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/Zsadigzade/trainbud/issues"
50
+ },
51
+ "engines": {
52
+ "node": ">=20.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@eslint/js": "^10.0.1",
56
+ "@types/better-sqlite3": "^7.6.13",
57
+ "@types/luxon": "^3.7.2",
58
+ "@types/node": "^26.0.1",
59
+ "@vitest/coverage-v8": "^4.1.9",
60
+ "eslint": "^10.5.0",
61
+ "tsx": "^4.22.4",
62
+ "typescript": "^6.0.3",
63
+ "typescript-eslint": "^8.62.0",
64
+ "vitest": "^4.1.9"
65
+ },
66
+ "dependencies": {
67
+ "@anthropic-ai/sdk": "^0.106.0",
68
+ "@modelcontextprotocol/sdk": "^1.29.0",
69
+ "better-sqlite3": "^12.11.1",
70
+ "commander": "^15.0.0",
71
+ "dotenv": "^17.4.2",
72
+ "garmin-connect": "^1.6.2",
73
+ "luxon": "^3.7.2",
74
+ "pino": "^10.3.1",
75
+ "pino-pretty": "^13.1.3",
76
+ "zod": "^4.4.3"
77
+ }
78
+ }