trainbud 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/.env.example +11 -0
  2. package/CHANGELOG.md +675 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +182 -0
  5. package/README.md +322 -0
  6. package/dist/appDb.d.ts +83 -0
  7. package/dist/appDb.d.ts.map +1 -0
  8. package/dist/appDb.js +239 -0
  9. package/dist/appDb.js.map +1 -0
  10. package/dist/check.d.ts +12 -0
  11. package/dist/check.d.ts.map +1 -0
  12. package/dist/check.js +230 -0
  13. package/dist/check.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +457 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +61 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +241 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/dashboard.d.ts +8 -0
  23. package/dist/dashboard.d.ts.map +1 -0
  24. package/dist/dashboard.js +1004 -0
  25. package/dist/dashboard.js.map +1 -0
  26. package/dist/dashboardCharts.d.ts +99 -0
  27. package/dist/dashboardCharts.d.ts.map +1 -0
  28. package/dist/dashboardCharts.js +317 -0
  29. package/dist/dashboardCharts.js.map +1 -0
  30. package/dist/dashboardData.d.ts +89 -0
  31. package/dist/dashboardData.d.ts.map +1 -0
  32. package/dist/dashboardData.js +228 -0
  33. package/dist/dashboardData.js.map +1 -0
  34. package/dist/detect/baseline.d.ts +17 -0
  35. package/dist/detect/baseline.d.ts.map +1 -0
  36. package/dist/detect/baseline.js +62 -0
  37. package/dist/detect/baseline.js.map +1 -0
  38. package/dist/detect/countdown.d.ts +30 -0
  39. package/dist/detect/countdown.d.ts.map +1 -0
  40. package/dist/detect/countdown.js +79 -0
  41. package/dist/detect/countdown.js.map +1 -0
  42. package/dist/detect/detectors.d.ts +20 -0
  43. package/dist/detect/detectors.d.ts.map +1 -0
  44. package/dist/detect/detectors.js +291 -0
  45. package/dist/detect/detectors.js.map +1 -0
  46. package/dist/detect/findings.d.ts +29 -0
  47. package/dist/detect/findings.d.ts.map +1 -0
  48. package/dist/detect/findings.js +2 -0
  49. package/dist/detect/findings.js.map +1 -0
  50. package/dist/detect/forecast.d.ts +33 -0
  51. package/dist/detect/forecast.d.ts.map +1 -0
  52. package/dist/detect/forecast.js +122 -0
  53. package/dist/detect/forecast.js.map +1 -0
  54. package/dist/detect/index.d.ts +50 -0
  55. package/dist/detect/index.d.ts.map +1 -0
  56. package/dist/detect/index.js +114 -0
  57. package/dist/detect/index.js.map +1 -0
  58. package/dist/detect/sleepQuality.d.ts +21 -0
  59. package/dist/detect/sleepQuality.d.ts.map +1 -0
  60. package/dist/detect/sleepQuality.js +164 -0
  61. package/dist/detect/sleepQuality.js.map +1 -0
  62. package/dist/detect/trimp.d.ts +22 -0
  63. package/dist/detect/trimp.d.ts.map +1 -0
  64. package/dist/detect/trimp.js +77 -0
  65. package/dist/detect/trimp.js.map +1 -0
  66. package/dist/detect/week.d.ts +31 -0
  67. package/dist/detect/week.d.ts.map +1 -0
  68. package/dist/detect/week.js +184 -0
  69. package/dist/detect/week.js.map +1 -0
  70. package/dist/garmin/auth.d.ts +10 -0
  71. package/dist/garmin/auth.d.ts.map +1 -0
  72. package/dist/garmin/auth.js +81 -0
  73. package/dist/garmin/auth.js.map +1 -0
  74. package/dist/garmin/cache.d.ts +42 -0
  75. package/dist/garmin/cache.d.ts.map +1 -0
  76. package/dist/garmin/cache.js +141 -0
  77. package/dist/garmin/cache.js.map +1 -0
  78. package/dist/garmin/client.d.ts +22 -0
  79. package/dist/garmin/client.d.ts.map +1 -0
  80. package/dist/garmin/client.js +237 -0
  81. package/dist/garmin/client.js.map +1 -0
  82. package/dist/garmin/daily.d.ts +24 -0
  83. package/dist/garmin/daily.d.ts.map +1 -0
  84. package/dist/garmin/daily.js +96 -0
  85. package/dist/garmin/daily.js.map +1 -0
  86. package/dist/garmin/garminApiTypes.d.ts +70 -0
  87. package/dist/garmin/garminApiTypes.d.ts.map +1 -0
  88. package/dist/garmin/garminApiTypes.js +3 -0
  89. package/dist/garmin/garminApiTypes.js.map +1 -0
  90. package/dist/garmin/garminConnect.d.ts +17 -0
  91. package/dist/garmin/garminConnect.d.ts.map +1 -0
  92. package/dist/garmin/garminConnect.js +5 -0
  93. package/dist/garmin/garminConnect.js.map +1 -0
  94. package/dist/garmin/partial.d.ts +32 -0
  95. package/dist/garmin/partial.d.ts.map +1 -0
  96. package/dist/garmin/partial.js +38 -0
  97. package/dist/garmin/partial.js.map +1 -0
  98. package/dist/garmin/rawApi.d.ts +59 -0
  99. package/dist/garmin/rawApi.d.ts.map +1 -0
  100. package/dist/garmin/rawApi.js +113 -0
  101. package/dist/garmin/rawApi.js.map +1 -0
  102. package/dist/garmin/types.d.ts +85 -0
  103. package/dist/garmin/types.d.ts.map +1 -0
  104. package/dist/garmin/types.js +11 -0
  105. package/dist/garmin/types.js.map +1 -0
  106. package/dist/history/capture.d.ts +12 -0
  107. package/dist/history/capture.d.ts.map +1 -0
  108. package/dist/history/capture.js +63 -0
  109. package/dist/history/capture.js.map +1 -0
  110. package/dist/history/context.d.ts +51 -0
  111. package/dist/history/context.d.ts.map +1 -0
  112. package/dist/history/context.js +122 -0
  113. package/dist/history/context.js.map +1 -0
  114. package/dist/history/fallback.d.ts +72 -0
  115. package/dist/history/fallback.d.ts.map +1 -0
  116. package/dist/history/fallback.js +185 -0
  117. package/dist/history/fallback.js.map +1 -0
  118. package/dist/history/ingest.d.ts +44 -0
  119. package/dist/history/ingest.d.ts.map +1 -0
  120. package/dist/history/ingest.js +347 -0
  121. package/dist/history/ingest.js.map +1 -0
  122. package/dist/history/scheduler.d.ts +20 -0
  123. package/dist/history/scheduler.d.ts.map +1 -0
  124. package/dist/history/scheduler.js +54 -0
  125. package/dist/history/scheduler.js.map +1 -0
  126. package/dist/history/schema.d.ts +41 -0
  127. package/dist/history/schema.d.ts.map +1 -0
  128. package/dist/history/schema.js +91 -0
  129. package/dist/history/schema.js.map +1 -0
  130. package/dist/history/store.d.ts +112 -0
  131. package/dist/history/store.d.ts.map +1 -0
  132. package/dist/history/store.js +282 -0
  133. package/dist/history/store.js.map +1 -0
  134. package/dist/httpServer.d.ts +55 -0
  135. package/dist/httpServer.d.ts.map +1 -0
  136. package/dist/httpServer.js +1027 -0
  137. package/dist/httpServer.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +11 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/mcpConfig.d.ts +56 -0
  143. package/dist/mcpConfig.d.ts.map +1 -0
  144. package/dist/mcpConfig.js +106 -0
  145. package/dist/mcpConfig.js.map +1 -0
  146. package/dist/pairApi.d.ts +14 -0
  147. package/dist/pairApi.d.ts.map +1 -0
  148. package/dist/pairApi.js +38 -0
  149. package/dist/pairApi.js.map +1 -0
  150. package/dist/paths.d.ts +45 -0
  151. package/dist/paths.d.ts.map +1 -0
  152. package/dist/paths.js +105 -0
  153. package/dist/paths.js.map +1 -0
  154. package/dist/profile.d.ts +123 -0
  155. package/dist/profile.d.ts.map +1 -0
  156. package/dist/profile.js +327 -0
  157. package/dist/profile.js.map +1 -0
  158. package/dist/promptApi.d.ts +43 -0
  159. package/dist/promptApi.d.ts.map +1 -0
  160. package/dist/promptApi.js +328 -0
  161. package/dist/promptApi.js.map +1 -0
  162. package/dist/promptSuggestions.d.ts +16 -0
  163. package/dist/promptSuggestions.d.ts.map +1 -0
  164. package/dist/promptSuggestions.js +141 -0
  165. package/dist/promptSuggestions.js.map +1 -0
  166. package/dist/selfTest.d.ts +53 -0
  167. package/dist/selfTest.d.ts.map +1 -0
  168. package/dist/selfTest.js +225 -0
  169. package/dist/selfTest.js.map +1 -0
  170. package/dist/server.d.ts +10 -0
  171. package/dist/server.d.ts.map +1 -0
  172. package/dist/server.js +67 -0
  173. package/dist/server.js.map +1 -0
  174. package/dist/setup.d.ts +2 -0
  175. package/dist/setup.d.ts.map +1 -0
  176. package/dist/setup.js +210 -0
  177. package/dist/setup.js.map +1 -0
  178. package/dist/toolErrors.d.ts +2 -0
  179. package/dist/toolErrors.d.ts.map +1 -0
  180. package/dist/toolErrors.js +18 -0
  181. package/dist/toolErrors.js.map +1 -0
  182. package/dist/tools/activities.d.ts +18 -0
  183. package/dist/tools/activities.d.ts.map +1 -0
  184. package/dist/tools/activities.js +182 -0
  185. package/dist/tools/activities.js.map +1 -0
  186. package/dist/tools/bodyComposition.d.ts +10 -0
  187. package/dist/tools/bodyComposition.d.ts.map +1 -0
  188. package/dist/tools/bodyComposition.js +139 -0
  189. package/dist/tools/bodyComposition.js.map +1 -0
  190. package/dist/tools/context.d.ts +8 -0
  191. package/dist/tools/context.d.ts.map +1 -0
  192. package/dist/tools/context.js +148 -0
  193. package/dist/tools/context.js.map +1 -0
  194. package/dist/tools/findings.d.ts +7 -0
  195. package/dist/tools/findings.d.ts.map +1 -0
  196. package/dist/tools/findings.js +51 -0
  197. package/dist/tools/findings.js.map +1 -0
  198. package/dist/tools/heartRate.d.ts +10 -0
  199. package/dist/tools/heartRate.d.ts.map +1 -0
  200. package/dist/tools/heartRate.js +105 -0
  201. package/dist/tools/heartRate.js.map +1 -0
  202. package/dist/tools/index.d.ts +66 -0
  203. package/dist/tools/index.d.ts.map +1 -0
  204. package/dist/tools/index.js +101 -0
  205. package/dist/tools/index.js.map +1 -0
  206. package/dist/tools/payloads.d.ts +148 -0
  207. package/dist/tools/payloads.d.ts.map +1 -0
  208. package/dist/tools/payloads.js +2 -0
  209. package/dist/tools/payloads.js.map +1 -0
  210. package/dist/tools/recovery.d.ts +31 -0
  211. package/dist/tools/recovery.d.ts.map +1 -0
  212. package/dist/tools/recovery.js +350 -0
  213. package/dist/tools/recovery.js.map +1 -0
  214. package/dist/tools/sleep.d.ts +10 -0
  215. package/dist/tools/sleep.d.ts.map +1 -0
  216. package/dist/tools/sleep.js +124 -0
  217. package/dist/tools/sleep.js.map +1 -0
  218. package/dist/tools/stress.d.ts +11 -0
  219. package/dist/tools/stress.d.ts.map +1 -0
  220. package/dist/tools/stress.js +108 -0
  221. package/dist/tools/stress.js.map +1 -0
  222. package/dist/tools/trainingInsights.d.ts +15 -0
  223. package/dist/tools/trainingInsights.d.ts.map +1 -0
  224. package/dist/tools/trainingInsights.js +79 -0
  225. package/dist/tools/trainingInsights.js.map +1 -0
  226. package/dist/tools/types.d.ts +12 -0
  227. package/dist/tools/types.d.ts.map +1 -0
  228. package/dist/tools/types.js +2 -0
  229. package/dist/tools/types.js.map +1 -0
  230. package/dist/tools/vo2Max.d.ts +11 -0
  231. package/dist/tools/vo2Max.d.ts.map +1 -0
  232. package/dist/tools/vo2Max.js +110 -0
  233. package/dist/tools/vo2Max.js.map +1 -0
  234. package/dist/tools/week.d.ts +12 -0
  235. package/dist/tools/week.d.ts.map +1 -0
  236. package/dist/tools/week.js +74 -0
  237. package/dist/tools/week.js.map +1 -0
  238. package/dist/usage.d.ts +104 -0
  239. package/dist/usage.d.ts.map +1 -0
  240. package/dist/usage.js +247 -0
  241. package/dist/usage.js.map +1 -0
  242. package/dist/utils/batch.d.ts +2 -0
  243. package/dist/utils/batch.d.ts.map +1 -0
  244. package/dist/utils/batch.js +18 -0
  245. package/dist/utils/batch.js.map +1 -0
  246. package/dist/utils/helpers.d.ts +32 -0
  247. package/dist/utils/helpers.d.ts.map +1 -0
  248. package/dist/utils/helpers.js +188 -0
  249. package/dist/utils/helpers.js.map +1 -0
  250. package/dist/utils/jsonFile.d.ts +19 -0
  251. package/dist/utils/jsonFile.d.ts.map +1 -0
  252. package/dist/utils/jsonFile.js +24 -0
  253. package/dist/utils/jsonFile.js.map +1 -0
  254. package/dist/utils/logger.d.ts +4 -0
  255. package/dist/utils/logger.d.ts.map +1 -0
  256. package/dist/utils/logger.js +97 -0
  257. package/dist/utils/logger.js.map +1 -0
  258. package/dist/utils/secretFile.d.ts +15 -0
  259. package/dist/utils/secretFile.d.ts.map +1 -0
  260. package/dist/utils/secretFile.js +47 -0
  261. package/dist/utils/secretFile.js.map +1 -0
  262. package/dist/utils/stdio.d.ts +10 -0
  263. package/dist/utils/stdio.d.ts.map +1 -0
  264. package/dist/utils/stdio.js +70 -0
  265. package/dist/utils/stdio.js.map +1 -0
  266. package/dist/version.d.ts +2 -0
  267. package/dist/version.d.ts.map +1 -0
  268. package/dist/version.js +7 -0
  269. package/dist/version.js.map +1 -0
  270. package/dist/watchApi.d.ts +264 -0
  271. package/dist/watchApi.d.ts.map +1 -0
  272. package/dist/watchApi.js +333 -0
  273. package/dist/watchApi.js.map +1 -0
  274. package/package.json +77 -0
package/dist/usage.js ADDED
@@ -0,0 +1,247 @@
1
+ import { DateTime } from "luxon";
2
+ import { getAppDb } from "./appDb.js";
3
+ import { getProfile } from "./profile.js";
4
+ import { logger } from "./utils/logger.js";
5
+ /**
6
+ * Published per-million-token rates.
7
+ *
8
+ * Deliberately a small table of the models this product can actually be
9
+ * pointed at, rather than a guess at a general pricing API. A model missing
10
+ * from here is unpriced, and says so.
11
+ */
12
+ const PRICES_PER_MTOK = {
13
+ "claude-haiku-4-5": { input: 1.0, output: 5.0 },
14
+ "claude-sonnet-5": { input: 2.0, output: 10.0 },
15
+ "claude-opus-5": { input: 5.0, output: 25.0 },
16
+ };
17
+ /** Cache reads bill at about a tenth of fresh input; writes at about 1.25x. */
18
+ const CACHE_READ_MULTIPLIER = 0.1;
19
+ const CACHE_WRITE_MULTIPLIER = 1.25;
20
+ /** Cost in USD, or null when this model has no price here. */
21
+ export function priceOf(model, tokens) {
22
+ const rate = PRICES_PER_MTOK[model];
23
+ if (!rate) {
24
+ return null;
25
+ }
26
+ const perToken = (perMTok) => perMTok / 1_000_000;
27
+ return (tokens.inputTokens * perToken(rate.input) +
28
+ tokens.outputTokens * perToken(rate.output) +
29
+ (tokens.cacheReadTokens ?? 0) * perToken(rate.input) * CACHE_READ_MULTIPLIER +
30
+ (tokens.cacheWriteTokens ?? 0) * perToken(rate.input) * CACHE_WRITE_MULTIPLIER);
31
+ }
32
+ export function recordAiUsage(input) {
33
+ const at = input.at ?? Math.floor(Date.now() / 1000);
34
+ const costUsd = priceOf(input.model, input);
35
+ const row = { ...input, at, costUsd };
36
+ try {
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)
41
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
42
+ .run(at, input.kind, input.model, input.source, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, costUsd);
43
+ }
44
+ catch (err) {
45
+ // Accounting must not take down the answer it was accounting for.
46
+ logger.warn({ err }, "could not record AI usage");
47
+ }
48
+ return row;
49
+ }
50
+ export function aiSpendSince(sinceUnixSeconds) {
51
+ try {
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
59
+ WHERE at >= ?`)
60
+ .get(sinceUnixSeconds);
61
+ return {
62
+ calls: row.calls,
63
+ inputTokens: row.input_tokens,
64
+ outputTokens: row.output_tokens,
65
+ costUsd: row.cost_usd,
66
+ unpricedCalls: row.unpriced,
67
+ };
68
+ }
69
+ catch (err) {
70
+ logger.warn({ err }, "could not read AI usage");
71
+ return { calls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, unpricedCalls: 0 };
72
+ }
73
+ }
74
+ /**
75
+ * Spend since the first of the current month, in the user's own local time.
76
+ *
77
+ * A rolling thirty days would be a different and worse number: a monthly cap
78
+ * that resets on a sliding window never resets, and the user compares this
79
+ * against a bill that runs calendar months.
80
+ */
81
+ export function monthToDateSpend() {
82
+ const start = DateTime.local().startOf("month").toSeconds();
83
+ return aiSpendSince(Math.floor(start));
84
+ }
85
+ export function budgetState() {
86
+ const cap = getProfile().budget.monthlyUsd;
87
+ const spend = monthToDateSpend();
88
+ if (cap === null) {
89
+ return {
90
+ capUsd: null,
91
+ spentUsd: spend.costUsd,
92
+ remainingUsd: null,
93
+ exceeded: false,
94
+ incomplete: spend.unpricedCalls > 0,
95
+ };
96
+ }
97
+ const remaining = Math.max(0, cap - spend.costUsd);
98
+ return {
99
+ capUsd: cap,
100
+ spentUsd: spend.costUsd,
101
+ remainingUsd: remaining,
102
+ exceeded: spend.costUsd >= cap,
103
+ incomplete: spend.unpricedCalls > 0,
104
+ };
105
+ }
106
+ /** Thrown when a request is refused because the user's own cap is reached. */
107
+ export class BudgetExceededError extends Error {
108
+ constructor(state) {
109
+ super(`Monthly AI budget of $${state.capUsd?.toFixed(2)} reached ` +
110
+ `($${state.spentUsd.toFixed(2)} spent). Raise or clear the cap in the dashboard.`);
111
+ }
112
+ }
113
+ /** Throws when a cap is set and already reached. No cap means never throws. */
114
+ export function assertWithinBudget() {
115
+ const state = budgetState();
116
+ if (state.exceeded) {
117
+ throw new BudgetExceededError(state);
118
+ }
119
+ }
120
+ /** The most recent calls, newest first — the ledger behind the total. */
121
+ export function recentAiUsage(limit = 20) {
122
+ try {
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
127
+ LIMIT ?`)
128
+ .all(limit);
129
+ return rows.map((row) => ({
130
+ at: row.at,
131
+ kind: row.kind,
132
+ model: row.model,
133
+ source: row.source,
134
+ inputTokens: row.input_tokens,
135
+ outputTokens: row.output_tokens,
136
+ costUsd: row.cost_usd,
137
+ }));
138
+ }
139
+ catch (err) {
140
+ logger.warn({ err }, "could not read recent AI usage");
141
+ return [];
142
+ }
143
+ }
144
+ /**
145
+ * Cost per day for the last `days` days, including the days that cost nothing.
146
+ *
147
+ * The zero days are the point. A chart built only from rows that exist draws a
148
+ * continuous line through a week the user never opened the app, which reads as
149
+ * steady daily spend rather than as the four days it actually was.
150
+ */
151
+ export function dailyAiSpend(days = 30) {
152
+ const today = DateTime.local().startOf("day");
153
+ const byDay = new Map();
154
+ for (let i = days - 1; i >= 0; i -= 1) {
155
+ const day = today.minus({ days: i }).toISODate();
156
+ if (day) {
157
+ byDay.set(day, { day, costUsd: 0, calls: 0 });
158
+ }
159
+ }
160
+ try {
161
+ const since = Math.floor(today.minus({ days: days - 1 }).toSeconds());
162
+ const rows = getAppDb()
163
+ .prepare("SELECT at, cost_usd FROM ai_usage WHERE at >= ?")
164
+ .all(since);
165
+ for (const row of rows) {
166
+ const day = DateTime.fromSeconds(row.at).toISODate();
167
+ const bucket = day ? byDay.get(day) : undefined;
168
+ if (bucket) {
169
+ bucket.costUsd += row.cost_usd ?? 0;
170
+ bucket.calls += 1;
171
+ }
172
+ }
173
+ }
174
+ catch (err) {
175
+ logger.warn({ err }, "could not read daily AI spend");
176
+ }
177
+ return [...byDay.values()];
178
+ }
179
+ /**
180
+ * Count one use of one feature, today.
181
+ *
182
+ * Everything about this is best-effort. It is called from render paths and
183
+ * request handlers that have real work to do, and a counter that can throw is
184
+ * a counter that takes down the feature it was measuring.
185
+ */
186
+ export function recordFeature(name) {
187
+ try {
188
+ if (!getProfile().analytics.enabled) {
189
+ return;
190
+ }
191
+ const day = DateTime.local().toISODate();
192
+ if (!day) {
193
+ return;
194
+ }
195
+ const trimmed = name.slice(0, 64);
196
+ getAppDb()
197
+ .prepare(`INSERT INTO feature_usage (name, day, count) VALUES (?, ?, 1)
198
+ ON CONFLICT(name, day) DO UPDATE SET count = count + 1`)
199
+ .run(trimmed, day);
200
+ }
201
+ catch (err) {
202
+ logger.warn({ err }, "could not record feature usage");
203
+ }
204
+ }
205
+ export function featureCounts(days = 30) {
206
+ try {
207
+ const since = DateTime.local().minus({ days }).toISODate();
208
+ if (!since) {
209
+ return [];
210
+ }
211
+ return getAppDb()
212
+ .prepare(`SELECT name, SUM(count) AS count
213
+ FROM feature_usage
214
+ WHERE day >= ?
215
+ GROUP BY name
216
+ ORDER BY count DESC, name ASC`)
217
+ .all(since);
218
+ }
219
+ catch (err) {
220
+ logger.warn({ err }, "could not read feature usage");
221
+ return [];
222
+ }
223
+ }
224
+ /**
225
+ * Delete every stored feature counter.
226
+ *
227
+ * Offered because "stop counting" and "forget what you counted" are different
228
+ * requests, and a privacy control that only does the first is the kind that
229
+ * gets described as a privacy control and is not one. AI spend is deliberately
230
+ * NOT cleared here: that is the user's own billing record against a cap they
231
+ * set, and losing it would make the cap wrong for the rest of the month.
232
+ */
233
+ export function clearFeatureUsage() {
234
+ try {
235
+ getAppDb().prepare("DELETE FROM feature_usage").run();
236
+ }
237
+ catch (err) {
238
+ logger.warn({ err }, "could not clear feature usage");
239
+ }
240
+ }
241
+ /** Tests only. */
242
+ export function __clearUsageForTests() {
243
+ const db = getAppDb();
244
+ db.prepare("DELETE FROM ai_usage").run();
245
+ db.prepare("DELETE FROM feature_usage").run();
246
+ }
247
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function mapInBatches<T, R>(items: T[], fn: (item: T) => Promise<R>, concurrency?: number): Promise<R[]>;
2
+ //# sourceMappingURL=batch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/utils/batch.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,CAAC,CAAC,EAAE,CAAC,EACrC,KAAK,EAAE,CAAC,EAAE,EACV,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC3B,WAAW,SAAsB,GAChC,OAAO,CAAC,CAAC,EAAE,CAAC,CAkBd"}
@@ -0,0 +1,18 @@
1
+ const DEFAULT_CONCURRENCY = 6;
2
+ export async function mapInBatches(items, fn, concurrency = DEFAULT_CONCURRENCY) {
3
+ if (items.length === 0) {
4
+ return [];
5
+ }
6
+ const results = new Array(items.length);
7
+ let index = 0;
8
+ async function worker() {
9
+ while (index < items.length) {
10
+ const current = index++;
11
+ results[current] = await fn(items[current]);
12
+ }
13
+ }
14
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
15
+ await Promise.all(workers);
16
+ return results;
17
+ }
18
+ //# sourceMappingURL=batch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/utils/batch.ts"],"names":[],"mappings":"AAAA,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAU,EACV,EAA2B,EAC3B,WAAW,GAAG,mBAAmB;IAEjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,OAAO,GAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,UAAU,MAAM;QACnB,OAAO,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;YACxB,OAAO,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAM,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5F,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { DateTime } from "luxon";
2
+ import type { ActivitySummary } from "../garmin/types.js";
3
+ export type TrendDirection = "improving" | "declining" | "stable" | "insufficient_data";
4
+ export declare function parseIsoDate(value: string): Date;
5
+ export declare function formatIsoDate(date: Date): string;
6
+ export declare function getDateRange(days: number): Date[];
7
+ export declare function getYesterday(): Date;
8
+ export declare function getDatesBetween(startDate: string, endDate: string): Date[];
9
+ export declare function parseActivityLocalDateTime(value: string): DateTime;
10
+ export declare function filterActivitiesByRange(activities: ActivitySummary[], startDate: string, endDate: string): ActivitySummary[];
11
+ export declare function formatDuration(seconds: number | null | undefined): string;
12
+ /**
13
+ * Distance in the units the user thinks in.
14
+ *
15
+ * The unit is a parameter rather than a lookup so these stay pure functions --
16
+ * every caller already sits inside a tool that knows the profile, and reaching
17
+ * for a database from a string formatter is how a formatter becomes untestable.
18
+ *
19
+ * This exists because the setting shipped before the conversion did: the
20
+ * profile accepted `imperial`, the system prompt told the model to answer in
21
+ * miles, and every number it was given was still in kilometres. A setting that
22
+ * claims to change the units and does not is worse than no setting -- the
23
+ * reader has no way to tell which one they are looking at.
24
+ */
25
+ export declare function formatDistanceMeters(meters: number | null | undefined, units?: "metric" | "imperial"): string;
26
+ export declare function formatPaceMetersPerSecond(metersPerSecond: number | null | undefined, units?: "metric" | "imperial"): string;
27
+ export declare function calculateTrend(values: number[], lowerIsBetter?: boolean): TrendDirection;
28
+ export declare function average(values: number[]): number;
29
+ export declare function clamp(value: number, min: number, max: number): number;
30
+ export declare function hashParams(params: Record<string, unknown>): string;
31
+ export declare function sanitizeErrorMessage(message: string): string;
32
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,mBAAmB,CAAC;AAuBxF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAEhD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,CASjD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,CAqB1E;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAYlE;AAED,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,eAAe,EAAE,EAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,eAAe,EAAE,CAYnB;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAkBzE;AAKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,KAAK,GAAE,QAAQ,GAAG,UAAqB,GACtC,MAAM,CAkBR;AAED,wBAAgB,yBAAyB,CACvC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC1C,KAAK,GAAE,QAAQ,GAAG,UAAqB,GACtC,MAAM,CAsBR;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,UAAQ,GAAG,cAAc,CA6BtF;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAMhD;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAGlE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK5D"}
@@ -0,0 +1,188 @@
1
+ import { DateTime } from "luxon";
2
+ // SECTION: Calendar days
3
+ //
4
+ // Every Date in this file is LOCAL midnight for the day it names, not UTC
5
+ // midnight.
6
+ //
7
+ // That is not a style choice. garmin-connect turns a Date into the day it asks
8
+ // Garmin about with `toDateString`, which subtracts getTimezoneOffset() and
9
+ // then reads the UTC date -- a function that round-trips a LOCAL-midnight Date
10
+ // and shifts a UTC-midnight one. Feeding it UTC midnight therefore asked for the
11
+ // wrong calendar day everywhere west of UTC: at UTC-5, `new Date("2026-08-19")`
12
+ // is 2026-08-18T19:00 local, and the library asked Garmin for the 18th while
13
+ // this code stored the answer under the 19th. Every daily metric -- sleep,
14
+ // heart rate, stress, weight -- was off by one day for those users, silently,
15
+ // with plausible numbers under the wrong dates.
16
+ //
17
+ // It never showed up here because the machine this was written on is UTC+4,
18
+ // where the shift lands inside the same day.
19
+ //
20
+ // A calendar day is a local question anyway: "how did I sleep on Tuesday" means
21
+ // the user's Tuesday.
22
+ export function parseIsoDate(value) {
23
+ const parsed = DateTime.fromISO(value);
24
+ if (!parsed.isValid) {
25
+ throw new Error(`Invalid date "${value}". Use ISO 8601 format such as 2026-06-01.`);
26
+ }
27
+ return parsed.toJSDate();
28
+ }
29
+ export function formatIsoDate(date) {
30
+ return DateTime.fromJSDate(date).toISODate() ?? "";
31
+ }
32
+ export function getDateRange(days) {
33
+ const dates = [];
34
+ const today = DateTime.local().startOf("day");
35
+ for (let offset = 0; offset < days; offset += 1) {
36
+ dates.push(today.minus({ days: offset }).toJSDate());
37
+ }
38
+ return dates;
39
+ }
40
+ export function getYesterday() {
41
+ return DateTime.local().minus({ days: 1 }).startOf("day").toJSDate();
42
+ }
43
+ export function getDatesBetween(startDate, endDate) {
44
+ const start = DateTime.fromISO(startDate).startOf("day");
45
+ const end = DateTime.fromISO(endDate).startOf("day");
46
+ if (!start.isValid || !end.isValid) {
47
+ throw new Error("Invalid date range. Use ISO 8601 dates such as 2026-06-01.");
48
+ }
49
+ if (end < start) {
50
+ throw new Error("end_date must be on or after start_date.");
51
+ }
52
+ const dates = [];
53
+ let cursor = start;
54
+ while (cursor <= end) {
55
+ dates.push(cursor.toJSDate());
56
+ cursor = cursor.plus({ days: 1 });
57
+ }
58
+ return dates;
59
+ }
60
+ export function parseActivityLocalDateTime(value) {
61
+ const fromIso = DateTime.fromISO(value, { setZone: true });
62
+ if (fromIso.isValid) {
63
+ return fromIso;
64
+ }
65
+ const fromSql = DateTime.fromSQL(value, { setZone: true });
66
+ if (fromSql.isValid) {
67
+ return fromSql;
68
+ }
69
+ return DateTime.invalid("unparsable");
70
+ }
71
+ export function filterActivitiesByRange(activities, startDate, endDate) {
72
+ const start = DateTime.fromISO(startDate).startOf("day");
73
+ const end = DateTime.fromISO(endDate).startOf("day");
74
+ return activities.filter((activity) => {
75
+ const activityDay = parseActivityLocalDateTime(activity.startTimeLocal).startOf("day");
76
+ if (!activityDay.isValid) {
77
+ return false;
78
+ }
79
+ return activityDay.toMillis() >= start.toMillis() && activityDay.toMillis() <= end.toMillis();
80
+ });
81
+ }
82
+ export function formatDuration(seconds) {
83
+ if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {
84
+ return "n/a";
85
+ }
86
+ const hours = Math.floor(seconds / 3600);
87
+ const minutes = Math.floor((seconds % 3600) / 60);
88
+ const remainingSeconds = Math.floor(seconds % 60);
89
+ if (hours > 0) {
90
+ return `${hours}h ${minutes}m ${remainingSeconds}s`;
91
+ }
92
+ if (minutes > 0) {
93
+ return `${minutes}m ${remainingSeconds}s`;
94
+ }
95
+ return `${remainingSeconds}s`;
96
+ }
97
+ const METRES_PER_MILE = 1609.344;
98
+ const METRES_PER_FOOT = 0.3048;
99
+ /**
100
+ * Distance in the units the user thinks in.
101
+ *
102
+ * The unit is a parameter rather than a lookup so these stay pure functions --
103
+ * every caller already sits inside a tool that knows the profile, and reaching
104
+ * for a database from a string formatter is how a formatter becomes untestable.
105
+ *
106
+ * This exists because the setting shipped before the conversion did: the
107
+ * profile accepted `imperial`, the system prompt told the model to answer in
108
+ * miles, and every number it was given was still in kilometres. A setting that
109
+ * claims to change the units and does not is worse than no setting -- the
110
+ * reader has no way to tell which one they are looking at.
111
+ */
112
+ export function formatDistanceMeters(meters, units = "metric") {
113
+ if (meters === null || meters === undefined || !Number.isFinite(meters)) {
114
+ return "n/a";
115
+ }
116
+ if (units === "imperial") {
117
+ // A quarter of a mile is the point below which a runner thinks in feet.
118
+ if (meters >= METRES_PER_MILE / 4) {
119
+ return `${(meters / METRES_PER_MILE).toFixed(2)} mi`;
120
+ }
121
+ return `${(meters / METRES_PER_FOOT).toFixed(0)} ft`;
122
+ }
123
+ if (meters >= 1000) {
124
+ return `${(meters / 1000).toFixed(2)} km`;
125
+ }
126
+ return `${meters.toFixed(0)} m`;
127
+ }
128
+ export function formatPaceMetersPerSecond(metersPerSecond, units = "metric") {
129
+ if (metersPerSecond === null ||
130
+ metersPerSecond === undefined ||
131
+ !Number.isFinite(metersPerSecond) ||
132
+ metersPerSecond <= 0) {
133
+ return "n/a";
134
+ }
135
+ const perUnit = units === "imperial" ? METRES_PER_MILE : 1000;
136
+ const secondsPerUnit = perUnit / metersPerSecond;
137
+ const minutes = Math.floor(secondsPerUnit / 60);
138
+ const seconds = Math.round(secondsPerUnit % 60);
139
+ // 4:60 /km is not a pace. Rounding the seconds can carry, and the carry has
140
+ // to reach the minutes or the string is wrong once every sixty paces.
141
+ const carried = seconds === 60;
142
+ const finalMinutes = carried ? minutes + 1 : minutes;
143
+ const finalSeconds = carried ? 0 : seconds;
144
+ return `${finalMinutes}:${finalSeconds.toString().padStart(2, "0")} ${units === "imperial" ? "/mi" : "/km"}`;
145
+ }
146
+ export function calculateTrend(values, lowerIsBetter = false) {
147
+ const filtered = values.filter((value) => Number.isFinite(value));
148
+ if (filtered.length < 2) {
149
+ return "insufficient_data";
150
+ }
151
+ const midpoint = Math.floor(filtered.length / 2);
152
+ const recent = filtered.slice(0, midpoint);
153
+ const older = filtered.slice(midpoint);
154
+ if (recent.length === 0 || older.length === 0) {
155
+ return "insufficient_data";
156
+ }
157
+ const recentAverage = average(recent);
158
+ const olderAverage = average(older);
159
+ const delta = recentAverage - olderAverage;
160
+ const threshold = Math.max(Math.abs(olderAverage) * 0.02, 0.5);
161
+ if (Math.abs(delta) <= threshold) {
162
+ return "stable";
163
+ }
164
+ if (lowerIsBetter) {
165
+ return delta < 0 ? "improving" : "declining";
166
+ }
167
+ return delta > 0 ? "improving" : "declining";
168
+ }
169
+ export function average(values) {
170
+ if (values.length === 0) {
171
+ return 0;
172
+ }
173
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
174
+ }
175
+ export function clamp(value, min, max) {
176
+ return Math.min(max, Math.max(min, value));
177
+ }
178
+ export function hashParams(params) {
179
+ const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right));
180
+ return JSON.stringify(Object.fromEntries(sortedEntries));
181
+ }
182
+ export function sanitizeErrorMessage(message) {
183
+ return message
184
+ .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "[email]")
185
+ .replace(/(?:[A-Za-z]:\\|\/)[^\s]+/g, "[path]")
186
+ .replace(/\bpassword[^\s]*/gi, "[redacted]");
187
+ }
188
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Windows PowerShell 5.1's `Set-Content -Encoding utf8` writes a BOM, and
3
+ * `JSON.parse` treats a leading U+FEFF as a syntax error. Every JSON file this
4
+ * project reads can have been written or edited by a Windows tool: the watch
5
+ * setup file is written by `scripts/start-watch-stack.ps1`, and Claude Desktop
6
+ * and Cursor both ship BOM'd config on Windows.
7
+ *
8
+ * This was not theoretical. `.trainbud/watch-setup.json` held the correct
9
+ * tunnel URL, and `appConfig.publicUrl` returned "" for it, because the parse
10
+ * threw on the BOM and the getter's `catch` swallowed the reason. `trainbud
11
+ * doctor` then reported "No public URL is configured, so the watch has no
12
+ * address to call" while the tunnel was up and answering 200 — a diagnostic
13
+ * naming the wrong fault, about the one file whose whole job is to name an
14
+ * address.
15
+ */
16
+ export declare function stripBom(text: string): string;
17
+ /** Read and parse a JSON file, tolerating a byte order mark. Throws like JSON.parse. */
18
+ export declare function readJsonFile<T>(filePath: string): T;
19
+ //# sourceMappingURL=jsonFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonFile.d.ts","sourceRoot":"","sources":["../../src/utils/jsonFile.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAEnD"}
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ /**
3
+ * Windows PowerShell 5.1's `Set-Content -Encoding utf8` writes a BOM, and
4
+ * `JSON.parse` treats a leading U+FEFF as a syntax error. Every JSON file this
5
+ * project reads can have been written or edited by a Windows tool: the watch
6
+ * setup file is written by `scripts/start-watch-stack.ps1`, and Claude Desktop
7
+ * and Cursor both ship BOM'd config on Windows.
8
+ *
9
+ * This was not theoretical. `.trainbud/watch-setup.json` held the correct
10
+ * tunnel URL, and `appConfig.publicUrl` returned "" for it, because the parse
11
+ * threw on the BOM and the getter's `catch` swallowed the reason. `trainbud
12
+ * doctor` then reported "No public URL is configured, so the watch has no
13
+ * address to call" while the tunnel was up and answering 200 — a diagnostic
14
+ * naming the wrong fault, about the one file whose whole job is to name an
15
+ * address.
16
+ */
17
+ export function stripBom(text) {
18
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
19
+ }
20
+ /** Read and parse a JSON file, tolerating a byte order mark. Throws like JSON.parse. */
21
+ export function readJsonFile(filePath) {
22
+ return JSON.parse(stripBom(fs.readFileSync(filePath, "utf8")));
23
+ }
24
+ //# sourceMappingURL=jsonFile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonFile.js","sourceRoot":"","sources":["../../src/utils/jsonFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAI,QAAgB;IAC9C,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAM,CAAC;AACtE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Logger } from "pino";
2
+ export declare function configureLogger(logPath?: string): void;
3
+ export declare const logger: Logger;
4
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAiGnC,wBAAgB,eAAe,CAAC,OAAO,SAAoB,GAAG,IAAI,CAEjE;AAED,eAAO,MAAM,MAAM,EAAE,MAMnB,CAAC"}