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.
- package/.env.example +11 -11
- package/CHANGELOG.md +719 -675
- package/QUICKSTART.md +185 -182
- package/README.md +323 -322
- package/dist/appDb.js +51 -51
- package/dist/dashboard.js +820 -820
- package/dist/dashboardCharts.js +42 -42
- package/dist/history/context.js +35 -35
- package/dist/history/schema.js +66 -66
- package/dist/promptApi.js +5 -5
- package/dist/usage.js +17 -17
- package/package.json +78 -77
- package/dist/appDb.d.ts.map +0 -1
- package/dist/appDb.js.map +0 -1
- package/dist/check.d.ts.map +0 -1
- package/dist/check.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/dashboard.d.ts.map +0 -1
- package/dist/dashboard.js.map +0 -1
- package/dist/dashboardCharts.d.ts.map +0 -1
- package/dist/dashboardCharts.js.map +0 -1
- package/dist/dashboardData.d.ts.map +0 -1
- package/dist/dashboardData.js.map +0 -1
- package/dist/detect/baseline.d.ts.map +0 -1
- package/dist/detect/baseline.js.map +0 -1
- package/dist/detect/countdown.d.ts.map +0 -1
- package/dist/detect/countdown.js.map +0 -1
- package/dist/detect/detectors.d.ts.map +0 -1
- package/dist/detect/detectors.js.map +0 -1
- package/dist/detect/findings.d.ts.map +0 -1
- package/dist/detect/findings.js.map +0 -1
- package/dist/detect/forecast.d.ts.map +0 -1
- package/dist/detect/forecast.js.map +0 -1
- package/dist/detect/index.d.ts.map +0 -1
- package/dist/detect/index.js.map +0 -1
- package/dist/detect/sleepQuality.d.ts.map +0 -1
- package/dist/detect/sleepQuality.js.map +0 -1
- package/dist/detect/trimp.d.ts.map +0 -1
- package/dist/detect/trimp.js.map +0 -1
- package/dist/detect/week.d.ts.map +0 -1
- package/dist/detect/week.js.map +0 -1
- package/dist/garmin/auth.d.ts.map +0 -1
- package/dist/garmin/auth.js.map +0 -1
- package/dist/garmin/cache.d.ts.map +0 -1
- package/dist/garmin/cache.js.map +0 -1
- package/dist/garmin/client.d.ts.map +0 -1
- package/dist/garmin/client.js.map +0 -1
- package/dist/garmin/daily.d.ts.map +0 -1
- package/dist/garmin/daily.js.map +0 -1
- package/dist/garmin/garminApiTypes.d.ts.map +0 -1
- package/dist/garmin/garminApiTypes.js.map +0 -1
- package/dist/garmin/garminConnect.d.ts.map +0 -1
- package/dist/garmin/garminConnect.js.map +0 -1
- package/dist/garmin/partial.d.ts.map +0 -1
- package/dist/garmin/partial.js.map +0 -1
- package/dist/garmin/rawApi.d.ts.map +0 -1
- package/dist/garmin/rawApi.js.map +0 -1
- package/dist/garmin/types.d.ts.map +0 -1
- package/dist/garmin/types.js.map +0 -1
- package/dist/history/capture.d.ts.map +0 -1
- package/dist/history/capture.js.map +0 -1
- package/dist/history/context.d.ts.map +0 -1
- package/dist/history/context.js.map +0 -1
- package/dist/history/fallback.d.ts.map +0 -1
- package/dist/history/fallback.js.map +0 -1
- package/dist/history/ingest.d.ts.map +0 -1
- package/dist/history/ingest.js.map +0 -1
- package/dist/history/scheduler.d.ts.map +0 -1
- package/dist/history/scheduler.js.map +0 -1
- package/dist/history/schema.d.ts.map +0 -1
- package/dist/history/schema.js.map +0 -1
- package/dist/history/store.d.ts.map +0 -1
- package/dist/history/store.js.map +0 -1
- package/dist/httpServer.d.ts.map +0 -1
- package/dist/httpServer.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mcpConfig.d.ts.map +0 -1
- package/dist/mcpConfig.js.map +0 -1
- package/dist/pairApi.d.ts.map +0 -1
- package/dist/pairApi.js.map +0 -1
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js.map +0 -1
- package/dist/profile.d.ts.map +0 -1
- package/dist/profile.js.map +0 -1
- package/dist/promptApi.d.ts.map +0 -1
- package/dist/promptApi.js.map +0 -1
- package/dist/promptSuggestions.d.ts.map +0 -1
- package/dist/promptSuggestions.js.map +0 -1
- package/dist/selfTest.d.ts.map +0 -1
- package/dist/selfTest.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/toolErrors.d.ts.map +0 -1
- package/dist/toolErrors.js.map +0 -1
- package/dist/tools/activities.d.ts.map +0 -1
- package/dist/tools/activities.js.map +0 -1
- package/dist/tools/bodyComposition.d.ts.map +0 -1
- package/dist/tools/bodyComposition.js.map +0 -1
- package/dist/tools/context.d.ts.map +0 -1
- package/dist/tools/context.js.map +0 -1
- package/dist/tools/findings.d.ts.map +0 -1
- package/dist/tools/findings.js.map +0 -1
- package/dist/tools/heartRate.d.ts.map +0 -1
- package/dist/tools/heartRate.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/payloads.d.ts.map +0 -1
- package/dist/tools/payloads.js.map +0 -1
- package/dist/tools/recovery.d.ts.map +0 -1
- package/dist/tools/recovery.js.map +0 -1
- package/dist/tools/sleep.d.ts.map +0 -1
- package/dist/tools/sleep.js.map +0 -1
- package/dist/tools/stress.d.ts.map +0 -1
- package/dist/tools/stress.js.map +0 -1
- package/dist/tools/trainingInsights.d.ts.map +0 -1
- package/dist/tools/trainingInsights.js.map +0 -1
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js.map +0 -1
- package/dist/tools/vo2Max.d.ts.map +0 -1
- package/dist/tools/vo2Max.js.map +0 -1
- package/dist/tools/week.d.ts.map +0 -1
- package/dist/tools/week.js.map +0 -1
- package/dist/usage.d.ts.map +0 -1
- package/dist/usage.js.map +0 -1
- package/dist/utils/batch.d.ts.map +0 -1
- package/dist/utils/batch.js.map +0 -1
- package/dist/utils/helpers.d.ts.map +0 -1
- package/dist/utils/helpers.js.map +0 -1
- package/dist/utils/jsonFile.d.ts.map +0 -1
- package/dist/utils/jsonFile.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/secretFile.d.ts.map +0 -1
- package/dist/utils/secretFile.js.map +0 -1
- package/dist/utils/stdio.d.ts.map +0 -1
- package/dist/utils/stdio.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/watchApi.d.ts.map +0 -1
- package/dist/watchApi.js.map +0 -1
package/dist/dashboardCharts.js
CHANGED
|
@@ -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
|
package/dist/history/context.js
CHANGED
|
@@ -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
|
}
|
package/dist/history/schema.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"test
|
|
24
|
-
"test:
|
|
25
|
-
"test:
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"garmin
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"@
|
|
56
|
-
"@types/
|
|
57
|
-
"@types/
|
|
58
|
-
"@
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"typescript
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"@
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"pino
|
|
75
|
-
"
|
|
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
|
+
}
|