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