@elizaos/plugin-health 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +107 -0
  2. package/dist/actions/index.d.ts +20 -0
  3. package/dist/actions/index.d.ts.map +1 -0
  4. package/dist/actions/index.js +5 -0
  5. package/dist/actions/index.js.map +1 -0
  6. package/dist/anchors/index.d.ts +19 -0
  7. package/dist/anchors/index.d.ts.map +1 -0
  8. package/dist/anchors/index.js +9 -0
  9. package/dist/anchors/index.js.map +1 -0
  10. package/dist/connectors/contract-stubs.d.ts +112 -0
  11. package/dist/connectors/contract-stubs.d.ts.map +1 -0
  12. package/dist/connectors/contract-stubs.js +1 -0
  13. package/dist/connectors/contract-stubs.js.map +1 -0
  14. package/dist/connectors/index.d.ts +28 -0
  15. package/dist/connectors/index.d.ts.map +1 -0
  16. package/dist/connectors/index.js +202 -0
  17. package/dist/connectors/index.js.map +1 -0
  18. package/dist/contracts/circadian-default.d.ts +15 -0
  19. package/dist/contracts/circadian-default.d.ts.map +1 -0
  20. package/dist/contracts/circadian-default.js +30 -0
  21. package/dist/contracts/circadian-default.js.map +1 -0
  22. package/dist/contracts/circadian.d.ts +92 -0
  23. package/dist/contracts/circadian.d.ts.map +1 -0
  24. package/dist/contracts/circadian.js +14 -0
  25. package/dist/contracts/circadian.js.map +1 -0
  26. package/dist/contracts/health.d.ts +9 -0
  27. package/dist/contracts/health.d.ts.map +1 -0
  28. package/dist/contracts/health.js +21 -0
  29. package/dist/contracts/health.js.map +1 -0
  30. package/dist/contracts/lifeops-connector-degradation.d.ts +9 -0
  31. package/dist/contracts/lifeops-connector-degradation.d.ts.map +1 -0
  32. package/dist/contracts/lifeops-connector-degradation.js +17 -0
  33. package/dist/contracts/lifeops-connector-degradation.js.map +1 -0
  34. package/dist/contracts/lifeops.d.ts +3123 -0
  35. package/dist/contracts/lifeops.d.ts.map +1 -0
  36. package/dist/contracts/lifeops.js +635 -0
  37. package/dist/contracts/lifeops.js.map +1 -0
  38. package/dist/contracts/permissions.d.ts +39 -0
  39. package/dist/contracts/permissions.d.ts.map +1 -0
  40. package/dist/contracts/permissions.js +1 -0
  41. package/dist/contracts/permissions.js.map +1 -0
  42. package/dist/default-packs/bedtime.d.ts +14 -0
  43. package/dist/default-packs/bedtime.d.ts.map +1 -0
  44. package/dist/default-packs/bedtime.js +48 -0
  45. package/dist/default-packs/bedtime.js.map +1 -0
  46. package/dist/default-packs/contract-stubs.d.ts +161 -0
  47. package/dist/default-packs/contract-stubs.d.ts.map +1 -0
  48. package/dist/default-packs/contract-stubs.js +1 -0
  49. package/dist/default-packs/contract-stubs.js.map +1 -0
  50. package/dist/default-packs/index.d.ts +18 -0
  51. package/dist/default-packs/index.d.ts.map +1 -0
  52. package/dist/default-packs/index.js +39 -0
  53. package/dist/default-packs/index.js.map +1 -0
  54. package/dist/default-packs/sleep-recap.d.ts +14 -0
  55. package/dist/default-packs/sleep-recap.d.ts.map +1 -0
  56. package/dist/default-packs/sleep-recap.js +51 -0
  57. package/dist/default-packs/sleep-recap.js.map +1 -0
  58. package/dist/default-packs/wake-up.d.ts +14 -0
  59. package/dist/default-packs/wake-up.d.ts.map +1 -0
  60. package/dist/default-packs/wake-up.js +61 -0
  61. package/dist/default-packs/wake-up.js.map +1 -0
  62. package/dist/health-bridge/health-bridge.d.ts +57 -0
  63. package/dist/health-bridge/health-bridge.d.ts.map +1 -0
  64. package/dist/health-bridge/health-bridge.js +558 -0
  65. package/dist/health-bridge/health-bridge.js.map +1 -0
  66. package/dist/health-bridge/health-connectors.d.ts +23 -0
  67. package/dist/health-bridge/health-connectors.d.ts.map +1 -0
  68. package/dist/health-bridge/health-connectors.js +1018 -0
  69. package/dist/health-bridge/health-connectors.js.map +1 -0
  70. package/dist/health-bridge/health-oauth.d.ts +62 -0
  71. package/dist/health-bridge/health-oauth.d.ts.map +1 -0
  72. package/dist/health-bridge/health-oauth.js +432 -0
  73. package/dist/health-bridge/health-oauth.js.map +1 -0
  74. package/dist/health-bridge/health-provider-registry.d.ts +89 -0
  75. package/dist/health-bridge/health-provider-registry.d.ts.map +1 -0
  76. package/dist/health-bridge/health-provider-registry.js +141 -0
  77. package/dist/health-bridge/health-provider-registry.js.map +1 -0
  78. package/dist/health-bridge/health-records.d.ts +14 -0
  79. package/dist/health-bridge/health-records.d.ts.map +1 -0
  80. package/dist/health-bridge/health-records.js +45 -0
  81. package/dist/health-bridge/health-records.js.map +1 -0
  82. package/dist/health-bridge/index.d.ts +22 -0
  83. package/dist/health-bridge/index.d.ts.map +1 -0
  84. package/dist/health-bridge/index.js +7 -0
  85. package/dist/health-bridge/index.js.map +1 -0
  86. package/dist/health-bridge/service-normalize-health.d.ts +3 -0
  87. package/dist/health-bridge/service-normalize-health.d.ts.map +1 -0
  88. package/dist/health-bridge/service-normalize-health.js +96 -0
  89. package/dist/health-bridge/service-normalize-health.js.map +1 -0
  90. package/dist/index.d.ts +41 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +62 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/screen-time/index.d.ts +23 -0
  95. package/dist/screen-time/index.d.ts.map +1 -0
  96. package/dist/screen-time/index.js +1 -0
  97. package/dist/screen-time/index.js.map +1 -0
  98. package/dist/sleep/awake-probability.d.ts +11 -0
  99. package/dist/sleep/awake-probability.d.ts.map +1 -0
  100. package/dist/sleep/awake-probability.js +163 -0
  101. package/dist/sleep/awake-probability.js.map +1 -0
  102. package/dist/sleep/circadian-rules.d.ts +45 -0
  103. package/dist/sleep/circadian-rules.d.ts.map +1 -0
  104. package/dist/sleep/circadian-rules.js +258 -0
  105. package/dist/sleep/circadian-rules.js.map +1 -0
  106. package/dist/sleep/index.d.ts +21 -0
  107. package/dist/sleep/index.d.ts.map +1 -0
  108. package/dist/sleep/index.js +11 -0
  109. package/dist/sleep/index.js.map +1 -0
  110. package/dist/sleep/sleep-cycle-dispatch.d.ts +75 -0
  111. package/dist/sleep/sleep-cycle-dispatch.d.ts.map +1 -0
  112. package/dist/sleep/sleep-cycle-dispatch.js +102 -0
  113. package/dist/sleep/sleep-cycle-dispatch.js.map +1 -0
  114. package/dist/sleep/sleep-cycle.d.ts +38 -0
  115. package/dist/sleep/sleep-cycle.d.ts.map +1 -0
  116. package/dist/sleep/sleep-cycle.js +418 -0
  117. package/dist/sleep/sleep-cycle.js.map +1 -0
  118. package/dist/sleep/sleep-episode-store.d.ts +25 -0
  119. package/dist/sleep/sleep-episode-store.d.ts.map +1 -0
  120. package/dist/sleep/sleep-episode-store.js +69 -0
  121. package/dist/sleep/sleep-episode-store.js.map +1 -0
  122. package/dist/sleep/sleep-episode-types.d.ts +38 -0
  123. package/dist/sleep/sleep-episode-types.d.ts.map +1 -0
  124. package/dist/sleep/sleep-episode-types.js +14 -0
  125. package/dist/sleep/sleep-episode-types.js.map +1 -0
  126. package/dist/sleep/sleep-recap.d.ts +19 -0
  127. package/dist/sleep/sleep-recap.d.ts.map +1 -0
  128. package/dist/sleep/sleep-recap.js +1 -0
  129. package/dist/sleep/sleep-recap.js.map +1 -0
  130. package/dist/sleep/sleep-regularity.d.ts +19 -0
  131. package/dist/sleep/sleep-regularity.d.ts.map +1 -0
  132. package/dist/sleep/sleep-regularity.js +242 -0
  133. package/dist/sleep/sleep-regularity.js.map +1 -0
  134. package/dist/sleep/sleep-wake-events.d.ts +58 -0
  135. package/dist/sleep/sleep-wake-events.d.ts.map +1 -0
  136. package/dist/sleep/sleep-wake-events.js +135 -0
  137. package/dist/sleep/sleep-wake-events.js.map +1 -0
  138. package/dist/sleep/source-reliability.d.ts +38 -0
  139. package/dist/sleep/source-reliability.d.ts.map +1 -0
  140. package/dist/sleep/source-reliability.js +62 -0
  141. package/dist/sleep/source-reliability.js.map +1 -0
  142. package/dist/util/index.d.ts +10 -0
  143. package/dist/util/index.d.ts.map +1 -0
  144. package/dist/util/index.js +3 -0
  145. package/dist/util/index.js.map +1 -0
  146. package/dist/util/normalize.d.ts +22 -0
  147. package/dist/util/normalize.d.ts.map +1 -0
  148. package/dist/util/normalize.js +62 -0
  149. package/dist/util/normalize.js.map +1 -0
  150. package/dist/util/time-util.d.ts +10 -0
  151. package/dist/util/time-util.d.ts.map +1 -0
  152. package/dist/util/time-util.js +14 -0
  153. package/dist/util/time-util.js.map +1 -0
  154. package/dist/util/time.d.ts +17 -0
  155. package/dist/util/time.d.ts.map +1 -0
  156. package/dist/util/time.js +152 -0
  157. package/dist/util/time.js.map +1 -0
  158. package/dist/util/token-encryption.d.ts +42 -0
  159. package/dist/util/token-encryption.d.ts.map +1 -0
  160. package/dist/util/token-encryption.js +96 -0
  161. package/dist/util/token-encryption.js.map +1 -0
  162. package/package.json +46 -0
@@ -0,0 +1,1018 @@
1
+ import { logger } from "@elizaos/core";
2
+ import { requireHealthProviderSpec } from "./health-provider-registry.js";
3
+ import {
4
+ createLifeOpsHealthMetricSample,
5
+ createLifeOpsHealthSleepEpisode,
6
+ createLifeOpsHealthWorkout
7
+ } from "./health-records.js";
8
+ const HEALTH_CONNECTOR_TIMEOUT_MS = 15e3;
9
+ const MAX_PAGINATION_PAGES = 5;
10
+ class HealthConnectorApiError extends Error {
11
+ constructor(status, provider, message) {
12
+ super(message);
13
+ this.status = status;
14
+ this.provider = provider;
15
+ this.name = "HealthConnectorApiError";
16
+ }
17
+ status;
18
+ provider;
19
+ }
20
+ function asRecord(value) {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
22
+ return null;
23
+ }
24
+ return value;
25
+ }
26
+ function asRecordArray(value) {
27
+ if (!Array.isArray(value)) {
28
+ return [];
29
+ }
30
+ return value.map(asRecord).filter((record) => record !== null);
31
+ }
32
+ function getRecord(source, key) {
33
+ return asRecord(source[key]);
34
+ }
35
+ function getArray(source, key) {
36
+ return asRecordArray(source[key]);
37
+ }
38
+ function getText(source, key) {
39
+ const value = source[key];
40
+ if (typeof value === "string" && value.trim().length > 0) {
41
+ return value.trim();
42
+ }
43
+ if (typeof value === "number" && Number.isFinite(value)) {
44
+ return String(value);
45
+ }
46
+ return null;
47
+ }
48
+ function getNumber(source, key) {
49
+ const value = source[key];
50
+ if (typeof value === "number" && Number.isFinite(value)) {
51
+ return value;
52
+ }
53
+ if (typeof value === "string" && value.trim().length > 0) {
54
+ const parsed = Number(value);
55
+ if (Number.isFinite(parsed)) {
56
+ return parsed;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ function getBoolean(source, key) {
62
+ const value = source[key];
63
+ return typeof value === "boolean" ? value : null;
64
+ }
65
+ function isoFromUnixSeconds(value) {
66
+ if (value === null) {
67
+ return null;
68
+ }
69
+ return new Date(value * 1e3).toISOString();
70
+ }
71
+ function normalizeIso(value) {
72
+ if (!value) {
73
+ return null;
74
+ }
75
+ const parsed = Date.parse(value);
76
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
77
+ }
78
+ function localDateFromIso(value) {
79
+ return value.slice(0, 10);
80
+ }
81
+ function addDays(date, days) {
82
+ const parsed = Date.parse(`${date}T00:00:00.000Z`);
83
+ return new Date(parsed + days * 864e5).toISOString().slice(0, 10);
84
+ }
85
+ function dateRange(startDate, endDate, maxDays = 31) {
86
+ const dates = [];
87
+ let current = startDate;
88
+ while (current <= endDate && dates.length < maxDays) {
89
+ dates.push(current);
90
+ current = addDays(current, 1);
91
+ }
92
+ return dates;
93
+ }
94
+ function ymdCompact(date) {
95
+ return date.replace(/-/g, "");
96
+ }
97
+ function authHeader(token) {
98
+ const type = token.tokenType.trim().length > 0 ? token.tokenType : "Bearer";
99
+ return `${type} ${token.accessToken}`;
100
+ }
101
+ function providerMockBase(provider) {
102
+ const key = `ELIZA_MOCK_${provider.toUpperCase()}_BASE`;
103
+ const value = process.env[key] ?? process.env.ELIZA_MOCK_HEALTH_BASE;
104
+ if (!value) {
105
+ return null;
106
+ }
107
+ const url = new URL(value);
108
+ if (!["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname)) {
109
+ throw new HealthConnectorApiError(
110
+ 409,
111
+ provider,
112
+ "Health connector mock base must point to loopback."
113
+ );
114
+ }
115
+ return url.toString().replace(/\/+$/, "");
116
+ }
117
+ function providerBaseUrl(provider) {
118
+ const mock = providerMockBase(provider);
119
+ if (mock) {
120
+ return mock;
121
+ }
122
+ return requireHealthProviderSpec(provider).apiBaseUrl;
123
+ }
124
+ async function readJsonResponse(response, provider) {
125
+ const text = await response.text();
126
+ if (!response.ok) {
127
+ throw new HealthConnectorApiError(
128
+ response.status,
129
+ provider,
130
+ text || `${provider} API request failed with HTTP ${response.status}.`
131
+ );
132
+ }
133
+ if (text.trim().length === 0) {
134
+ return {};
135
+ }
136
+ return JSON.parse(text);
137
+ }
138
+ async function fetchHealthJson(args) {
139
+ const json = await fetchHealthValue(args);
140
+ const record = asRecord(json);
141
+ if (!record) {
142
+ throw new HealthConnectorApiError(
143
+ 502,
144
+ args.token.provider,
145
+ `${args.token.provider} API returned a non-object response.`
146
+ );
147
+ }
148
+ const status = getNumber(record, "status");
149
+ if (status !== null && status !== 0) {
150
+ throw new HealthConnectorApiError(
151
+ 502,
152
+ args.token.provider,
153
+ `${args.token.provider} API returned status ${status}.`
154
+ );
155
+ }
156
+ return record;
157
+ }
158
+ async function fetchHealthValue(args) {
159
+ const url = new URL(`${providerBaseUrl(args.token.provider)}${args.path}`);
160
+ for (const [key, value] of Object.entries(args.query ?? {})) {
161
+ if (value !== null && value !== void 0) {
162
+ url.searchParams.set(key, String(value));
163
+ }
164
+ }
165
+ const response = await fetch(url, {
166
+ method: args.method ?? "GET",
167
+ headers: {
168
+ Accept: "application/json",
169
+ Authorization: authHeader(args.token),
170
+ ...args.form ? { "Content-Type": "application/x-www-form-urlencoded" } : {}
171
+ },
172
+ body: args.form,
173
+ signal: AbortSignal.timeout(HEALTH_CONNECTOR_TIMEOUT_MS)
174
+ });
175
+ return readJsonResponse(response, args.token.provider);
176
+ }
177
+ function sample(args) {
178
+ if (args.value === null || !Number.isFinite(args.value) || !args.startAt) {
179
+ return null;
180
+ }
181
+ return createLifeOpsHealthMetricSample({
182
+ agentId: args.token.agentId,
183
+ provider: args.token.provider,
184
+ grantId: args.grantId,
185
+ metric: args.metric,
186
+ value: args.value,
187
+ unit: args.unit,
188
+ startAt: args.startAt,
189
+ endAt: args.endAt ?? args.startAt,
190
+ localDate: localDateFromIso(args.startAt),
191
+ sourceExternalId: args.sourceExternalId,
192
+ metadata: args.metadata ?? {}
193
+ });
194
+ }
195
+ function compactSamples(samples) {
196
+ return samples.filter(
197
+ (entry) => entry !== null
198
+ );
199
+ }
200
+ async function syncStrava(args) {
201
+ const after = Math.floor(
202
+ Date.parse(`${args.startDate}T00:00:00.000Z`) / 1e3
203
+ );
204
+ const before = Math.floor(
205
+ Date.parse(`${args.endDate}T23:59:59.999Z`) / 1e3
206
+ );
207
+ const [athlete, activitiesJson] = await Promise.all([
208
+ fetchHealthJson({ token: args.token, path: "/athlete" }),
209
+ fetchHealthValue({
210
+ token: args.token,
211
+ path: "/athlete/activities",
212
+ query: { after, before, per_page: 200 }
213
+ })
214
+ ]);
215
+ const activities = asRecordArray(activitiesJson);
216
+ const workouts = [];
217
+ const samples = [];
218
+ for (const activity of activities) {
219
+ const id = getText(activity, "id");
220
+ const startAt = normalizeIso(getText(activity, "start_date"));
221
+ if (!id || !startAt) {
222
+ continue;
223
+ }
224
+ const elapsedSeconds = getNumber(activity, "elapsed_time");
225
+ const movingSeconds = getNumber(activity, "moving_time");
226
+ const durationSeconds = Math.trunc(movingSeconds ?? elapsedSeconds ?? 0);
227
+ const endAt = durationSeconds > 0 ? new Date(Date.parse(startAt) + durationSeconds * 1e3).toISOString() : null;
228
+ const calories = getNumber(activity, "calories");
229
+ const distance = getNumber(activity, "distance");
230
+ const averageHeartRate = getNumber(activity, "average_heartrate");
231
+ const maxHeartRate = getNumber(activity, "max_heartrate");
232
+ workouts.push(
233
+ createLifeOpsHealthWorkout({
234
+ agentId: args.token.agentId,
235
+ provider: "strava",
236
+ grantId: args.grantId,
237
+ sourceExternalId: id,
238
+ workoutType: getText(activity, "sport_type") ?? getText(activity, "type") ?? "run",
239
+ title: getText(activity, "name") ?? "",
240
+ startAt,
241
+ endAt,
242
+ durationSeconds,
243
+ distanceMeters: distance,
244
+ calories,
245
+ averageHeartRate,
246
+ maxHeartRate,
247
+ metadata: {
248
+ elapsedSeconds,
249
+ movingSeconds,
250
+ elevationGainMeters: getNumber(activity, "total_elevation_gain"),
251
+ averageSpeedMetersPerSecond: getNumber(activity, "average_speed"),
252
+ maxSpeedMetersPerSecond: getNumber(activity, "max_speed"),
253
+ averageWatts: getNumber(activity, "average_watts"),
254
+ averageCadence: getNumber(activity, "average_cadence")
255
+ }
256
+ })
257
+ );
258
+ samples.push(
259
+ ...compactSamples([
260
+ sample({
261
+ token: args.token,
262
+ grantId: args.grantId,
263
+ metric: "distance_meters",
264
+ value: distance,
265
+ unit: "m",
266
+ startAt,
267
+ endAt,
268
+ sourceExternalId: `${id}:distance_meters`
269
+ }),
270
+ sample({
271
+ token: args.token,
272
+ grantId: args.grantId,
273
+ metric: "active_minutes",
274
+ value: durationSeconds / 60,
275
+ unit: "min",
276
+ startAt,
277
+ endAt,
278
+ sourceExternalId: `${id}:active_minutes`
279
+ }),
280
+ sample({
281
+ token: args.token,
282
+ grantId: args.grantId,
283
+ metric: "calories",
284
+ value: calories,
285
+ unit: "kcal",
286
+ startAt,
287
+ endAt,
288
+ sourceExternalId: `${id}:calories`
289
+ }),
290
+ sample({
291
+ token: args.token,
292
+ grantId: args.grantId,
293
+ metric: "heart_rate",
294
+ value: averageHeartRate,
295
+ unit: "bpm",
296
+ startAt,
297
+ endAt,
298
+ sourceExternalId: `${id}:heart_rate`
299
+ })
300
+ ])
301
+ );
302
+ }
303
+ return {
304
+ samples,
305
+ workouts,
306
+ sleepEpisodes: [],
307
+ identity: athlete,
308
+ cursor: null
309
+ };
310
+ }
311
+ async function syncFitbit(args) {
312
+ const dates = dateRange(args.startDate, args.endDate);
313
+ const identityJson = await fetchHealthJson({
314
+ token: args.token,
315
+ path: "/1/user/-/profile.json"
316
+ });
317
+ const samples = [];
318
+ const sleepEpisodes = [];
319
+ const workouts = [];
320
+ for (const date of dates) {
321
+ const [activity, sleep, heart, weight] = await Promise.all([
322
+ fetchHealthJson({
323
+ token: args.token,
324
+ path: `/1/user/-/activities/date/${date}.json`
325
+ }),
326
+ fetchHealthJson({
327
+ token: args.token,
328
+ path: `/1.2/user/-/sleep/date/${date}.json`
329
+ }),
330
+ fetchHealthJson({
331
+ token: args.token,
332
+ path: `/1/user/-/activities/heart/date/${date}/1d.json`
333
+ }),
334
+ fetchHealthJson({
335
+ token: args.token,
336
+ path: `/1/user/-/body/log/weight/date/${date}.json`
337
+ })
338
+ ]);
339
+ const dayAt = `${date}T12:00:00.000Z`;
340
+ const summary = getRecord(activity, "summary") ?? {};
341
+ const distances = getArray(summary, "distances");
342
+ const totalDistance = distances.reduce((sum, entry) => {
343
+ const distance = getNumber(entry, "distance");
344
+ return sum + (distance ?? 0);
345
+ }, 0);
346
+ samples.push(
347
+ ...compactSamples([
348
+ sample({
349
+ token: args.token,
350
+ grantId: args.grantId,
351
+ metric: "steps",
352
+ value: getNumber(summary, "steps"),
353
+ unit: "count",
354
+ startAt: dayAt,
355
+ sourceExternalId: `${date}:fitbit:steps`
356
+ }),
357
+ sample({
358
+ token: args.token,
359
+ grantId: args.grantId,
360
+ metric: "active_minutes",
361
+ value: (getNumber(summary, "fairlyActiveMinutes") ?? 0) + (getNumber(summary, "veryActiveMinutes") ?? 0),
362
+ unit: "min",
363
+ startAt: dayAt,
364
+ sourceExternalId: `${date}:fitbit:active_minutes`
365
+ }),
366
+ sample({
367
+ token: args.token,
368
+ grantId: args.grantId,
369
+ metric: "calories",
370
+ value: getNumber(summary, "caloriesOut"),
371
+ unit: "kcal",
372
+ startAt: dayAt,
373
+ sourceExternalId: `${date}:fitbit:calories`
374
+ }),
375
+ sample({
376
+ token: args.token,
377
+ grantId: args.grantId,
378
+ metric: "distance_meters",
379
+ value: totalDistance > 0 ? totalDistance * 1e3 : null,
380
+ unit: "m",
381
+ startAt: dayAt,
382
+ sourceExternalId: `${date}:fitbit:distance_meters`
383
+ })
384
+ ])
385
+ );
386
+ const heartEntries = getArray(heart, "activities-heart");
387
+ const heartValue = heartEntries.map((entry) => getRecord(entry, "value")).find((entry) => entry !== null);
388
+ samples.push(
389
+ ...compactSamples([
390
+ sample({
391
+ token: args.token,
392
+ grantId: args.grantId,
393
+ metric: "resting_heart_rate",
394
+ value: heartValue ? getNumber(heartValue, "restingHeartRate") : null,
395
+ unit: "bpm",
396
+ startAt: dayAt,
397
+ sourceExternalId: `${date}:fitbit:resting_heart_rate`
398
+ })
399
+ ])
400
+ );
401
+ const sleepSummary = getRecord(sleep, "summary") ?? {};
402
+ samples.push(
403
+ ...compactSamples([
404
+ sample({
405
+ token: args.token,
406
+ grantId: args.grantId,
407
+ metric: "sleep_hours",
408
+ value: getNumber(sleepSummary, "totalMinutesAsleep") !== null ? (getNumber(sleepSummary, "totalMinutesAsleep") ?? 0) / 60 : null,
409
+ unit: "h",
410
+ startAt: dayAt,
411
+ sourceExternalId: `${date}:fitbit:sleep_hours`
412
+ })
413
+ ])
414
+ );
415
+ for (const sleepLog of getArray(sleep, "sleep")) {
416
+ const logId = getText(sleepLog, "logId") ?? `${date}:${getText(sleepLog, "startTime") ?? "sleep"}`;
417
+ const startAt = normalizeIso(getText(sleepLog, "startTime"));
418
+ const endAt = normalizeIso(getText(sleepLog, "endTime"));
419
+ if (!startAt || !endAt) {
420
+ continue;
421
+ }
422
+ sleepEpisodes.push(
423
+ createLifeOpsHealthSleepEpisode({
424
+ agentId: args.token.agentId,
425
+ provider: "fitbit",
426
+ grantId: args.grantId,
427
+ sourceExternalId: logId,
428
+ localDate: date,
429
+ timezone: null,
430
+ startAt,
431
+ endAt,
432
+ isMainSleep: getBoolean(sleepLog, "isMainSleep") ?? false,
433
+ sleepType: getText(sleepLog, "type"),
434
+ durationSeconds: Math.trunc(
435
+ (getNumber(sleepLog, "duration") ?? 0) / 1e3
436
+ ),
437
+ timeInBedSeconds: getNumber(sleepLog, "timeInBed") !== null ? Math.trunc((getNumber(sleepLog, "timeInBed") ?? 0) * 60) : null,
438
+ efficiency: getNumber(sleepLog, "efficiency"),
439
+ latencySeconds: getNumber(sleepLog, "minutesToFallAsleep") !== null ? Math.trunc(
440
+ (getNumber(sleepLog, "minutesToFallAsleep") ?? 0) * 60
441
+ ) : null,
442
+ awakeSeconds: getNumber(sleepLog, "minutesAwake") !== null ? Math.trunc((getNumber(sleepLog, "minutesAwake") ?? 0) * 60) : null,
443
+ lightSleepSeconds: null,
444
+ deepSleepSeconds: null,
445
+ remSleepSeconds: null,
446
+ sleepScore: getNumber(sleepLog, "efficiency"),
447
+ readinessScore: null,
448
+ averageHeartRate: null,
449
+ lowestHeartRate: null,
450
+ averageHrvMs: null,
451
+ respiratoryRate: null,
452
+ bloodOxygenPercent: null,
453
+ stageSamples: fitbitStageSamples(sleepLog),
454
+ metadata: { rawDateOfSleep: getText(sleepLog, "dateOfSleep") }
455
+ })
456
+ );
457
+ }
458
+ for (const log of getArray(weight, "weight")) {
459
+ const loggedAt = normalizeIso(
460
+ `${getText(log, "date") ?? date}T${getText(log, "time") ?? "12:00:00"}`
461
+ );
462
+ samples.push(
463
+ ...compactSamples([
464
+ sample({
465
+ token: args.token,
466
+ grantId: args.grantId,
467
+ metric: "weight_kg",
468
+ value: getNumber(log, "weight"),
469
+ unit: "kg",
470
+ startAt: loggedAt,
471
+ sourceExternalId: getText(log, "logId") ?? `${date}:fitbit:weight_kg`,
472
+ metadata: { providerUnit: getText(log, "weightUnit") }
473
+ })
474
+ ])
475
+ );
476
+ }
477
+ }
478
+ return {
479
+ samples,
480
+ workouts,
481
+ sleepEpisodes,
482
+ identity: getRecord(identityJson, "user") ?? identityJson,
483
+ cursor: null
484
+ };
485
+ }
486
+ function fitbitStageSamples(sleepLog) {
487
+ const levels = getRecord(sleepLog, "levels");
488
+ const data = levels ? getArray(levels, "data") : [];
489
+ const samples = [];
490
+ for (const entry of data) {
491
+ const startAt = normalizeIso(getText(entry, "dateTime"));
492
+ const seconds = getNumber(entry, "seconds");
493
+ if (!startAt || seconds === null) {
494
+ continue;
495
+ }
496
+ samples.push({
497
+ stage: fitbitStage(getText(entry, "level")),
498
+ startAt,
499
+ endAt: new Date(Date.parse(startAt) + seconds * 1e3).toISOString(),
500
+ confidence: null,
501
+ providerCode: getText(entry, "level")
502
+ });
503
+ }
504
+ return samples;
505
+ }
506
+ function fitbitStage(value) {
507
+ if (value === "wake" || value === "awake") return "awake";
508
+ if (value === "light") return "light";
509
+ if (value === "deep") return "deep";
510
+ if (value === "rem") return "rem";
511
+ if (value === "restless") return "restless";
512
+ return "unknown";
513
+ }
514
+ async function fetchOuraCollection(args) {
515
+ const items = [];
516
+ let nextToken = null;
517
+ for (let page = 0; page < MAX_PAGINATION_PAGES; page += 1) {
518
+ const json = await fetchHealthJson({
519
+ token: args.token,
520
+ path: args.path,
521
+ query: { ...args.query, next_token: nextToken }
522
+ });
523
+ items.push(...getArray(json, "data"));
524
+ nextToken = getText(json, "next_token");
525
+ if (!nextToken) {
526
+ break;
527
+ }
528
+ }
529
+ return items;
530
+ }
531
+ async function syncOura(args) {
532
+ const startDatetime = `${args.startDate}T00:00:00Z`;
533
+ const endDatetime = `${args.endDate}T23:59:59Z`;
534
+ const [
535
+ personal,
536
+ dailyActivity,
537
+ dailyReadiness,
538
+ sleep,
539
+ heartRate,
540
+ workoutsRaw
541
+ ] = await Promise.all([
542
+ fetchHealthJson({
543
+ token: args.token,
544
+ path: "/v2/usercollection/personal_info"
545
+ }),
546
+ fetchOuraCollection({
547
+ token: args.token,
548
+ path: "/v2/usercollection/daily_activity",
549
+ query: { start_date: args.startDate, end_date: args.endDate }
550
+ }),
551
+ fetchOuraCollection({
552
+ token: args.token,
553
+ path: "/v2/usercollection/daily_readiness",
554
+ query: { start_date: args.startDate, end_date: args.endDate }
555
+ }),
556
+ fetchOuraCollection({
557
+ token: args.token,
558
+ path: "/v2/usercollection/sleep",
559
+ query: { start_date: args.startDate, end_date: args.endDate }
560
+ }),
561
+ fetchOuraCollection({
562
+ token: args.token,
563
+ path: "/v2/usercollection/heartrate",
564
+ query: { start_datetime: startDatetime, end_datetime: endDatetime }
565
+ }),
566
+ fetchOuraCollection({
567
+ token: args.token,
568
+ path: "/v2/usercollection/workout",
569
+ query: { start_date: args.startDate, end_date: args.endDate }
570
+ })
571
+ ]);
572
+ const samples = [];
573
+ const sleepEpisodes = [];
574
+ const workouts = [];
575
+ for (const day of dailyActivity) {
576
+ const date = getText(day, "day");
577
+ const startAt = date ? `${date}T12:00:00.000Z` : null;
578
+ const id = getText(day, "id") ?? date ?? "daily_activity";
579
+ samples.push(
580
+ ...compactSamples([
581
+ sample({
582
+ token: args.token,
583
+ grantId: args.grantId,
584
+ metric: "steps",
585
+ value: getNumber(day, "steps"),
586
+ unit: "count",
587
+ startAt,
588
+ sourceExternalId: `${id}:steps`
589
+ }),
590
+ sample({
591
+ token: args.token,
592
+ grantId: args.grantId,
593
+ metric: "calories",
594
+ value: getNumber(day, "total_calories") ?? getNumber(day, "active_calories"),
595
+ unit: "kcal",
596
+ startAt,
597
+ sourceExternalId: `${id}:calories`
598
+ }),
599
+ sample({
600
+ token: args.token,
601
+ grantId: args.grantId,
602
+ metric: "distance_meters",
603
+ value: getNumber(day, "equivalent_walking_distance"),
604
+ unit: "m",
605
+ startAt,
606
+ sourceExternalId: `${id}:distance_meters`
607
+ })
608
+ ])
609
+ );
610
+ }
611
+ for (const readiness of dailyReadiness) {
612
+ const date = getText(readiness, "day");
613
+ const startAt = date ? `${date}T12:00:00.000Z` : null;
614
+ samples.push(
615
+ ...compactSamples([
616
+ sample({
617
+ token: args.token,
618
+ grantId: args.grantId,
619
+ metric: "readiness_score",
620
+ value: getNumber(readiness, "score"),
621
+ unit: "score",
622
+ startAt,
623
+ sourceExternalId: `${getText(readiness, "id") ?? date}:readiness_score`
624
+ })
625
+ ])
626
+ );
627
+ }
628
+ for (const entry of sleep) {
629
+ const id = getText(entry, "id");
630
+ const startAt = normalizeIso(getText(entry, "bedtime_start"));
631
+ const endAt = normalizeIso(getText(entry, "bedtime_end"));
632
+ if (!id || !startAt || !endAt) {
633
+ continue;
634
+ }
635
+ const date = getText(entry, "day") ?? localDateFromIso(startAt);
636
+ const sleepScore = getNumber(entry, "score");
637
+ samples.push(
638
+ ...compactSamples([
639
+ sample({
640
+ token: args.token,
641
+ grantId: args.grantId,
642
+ metric: "sleep_hours",
643
+ value: getNumber(entry, "total_sleep_duration") !== null ? (getNumber(entry, "total_sleep_duration") ?? 0) / 3600 : null,
644
+ unit: "h",
645
+ startAt,
646
+ endAt,
647
+ sourceExternalId: `${id}:sleep_hours`
648
+ }),
649
+ sample({
650
+ token: args.token,
651
+ grantId: args.grantId,
652
+ metric: "sleep_score",
653
+ value: sleepScore,
654
+ unit: "score",
655
+ startAt,
656
+ endAt,
657
+ sourceExternalId: `${id}:sleep_score`
658
+ })
659
+ ])
660
+ );
661
+ sleepEpisodes.push(
662
+ createLifeOpsHealthSleepEpisode({
663
+ agentId: args.token.agentId,
664
+ provider: "oura",
665
+ grantId: args.grantId,
666
+ sourceExternalId: id,
667
+ localDate: date,
668
+ timezone: getText(entry, "timezone"),
669
+ startAt,
670
+ endAt,
671
+ isMainSleep: getText(entry, "type") === "long_sleep",
672
+ sleepType: getText(entry, "type"),
673
+ durationSeconds: Math.trunc(
674
+ getNumber(entry, "total_sleep_duration") ?? 0
675
+ ),
676
+ timeInBedSeconds: Math.trunc(getNumber(entry, "time_in_bed") ?? 0) || null,
677
+ efficiency: getNumber(entry, "efficiency"),
678
+ latencySeconds: Math.trunc(getNumber(entry, "latency") ?? 0) || null,
679
+ awakeSeconds: Math.trunc(getNumber(entry, "awake_time") ?? 0) || null,
680
+ lightSleepSeconds: Math.trunc(getNumber(entry, "light_sleep_duration") ?? 0) || null,
681
+ deepSleepSeconds: Math.trunc(getNumber(entry, "deep_sleep_duration") ?? 0) || null,
682
+ remSleepSeconds: Math.trunc(getNumber(entry, "rem_sleep_duration") ?? 0) || null,
683
+ sleepScore,
684
+ readinessScore: null,
685
+ averageHeartRate: getNumber(entry, "average_heart_rate"),
686
+ lowestHeartRate: getNumber(entry, "lowest_heart_rate"),
687
+ averageHrvMs: getNumber(entry, "average_hrv"),
688
+ respiratoryRate: getNumber(entry, "average_breath"),
689
+ bloodOxygenPercent: null,
690
+ stageSamples: [],
691
+ metadata: { source: "oura_sleep" }
692
+ })
693
+ );
694
+ }
695
+ for (const entry of heartRate) {
696
+ const timestamp = normalizeIso(getText(entry, "timestamp"));
697
+ const id = getText(entry, "id") ?? getText(entry, "timestamp") ?? "heart";
698
+ samples.push(
699
+ ...compactSamples([
700
+ sample({
701
+ token: args.token,
702
+ grantId: args.grantId,
703
+ metric: "heart_rate",
704
+ value: getNumber(entry, "bpm"),
705
+ unit: "bpm",
706
+ startAt: timestamp,
707
+ sourceExternalId: `${id}:heart_rate`,
708
+ metadata: { source: getText(entry, "source") }
709
+ })
710
+ ])
711
+ );
712
+ }
713
+ for (const workout of workoutsRaw) {
714
+ const id = getText(workout, "id");
715
+ const startAt = normalizeIso(getText(workout, "start_datetime"));
716
+ const endAt = normalizeIso(getText(workout, "end_datetime"));
717
+ if (!id || !startAt) {
718
+ continue;
719
+ }
720
+ workouts.push(
721
+ createLifeOpsHealthWorkout({
722
+ agentId: args.token.agentId,
723
+ provider: "oura",
724
+ grantId: args.grantId,
725
+ sourceExternalId: id,
726
+ workoutType: getText(workout, "activity") ?? "workout",
727
+ title: getText(workout, "activity") ?? "",
728
+ startAt,
729
+ endAt,
730
+ durationSeconds: endAt !== null ? Math.max(
731
+ 0,
732
+ Math.trunc((Date.parse(endAt) - Date.parse(startAt)) / 1e3)
733
+ ) : 0,
734
+ distanceMeters: getNumber(workout, "distance"),
735
+ calories: getNumber(workout, "calories"),
736
+ averageHeartRate: null,
737
+ maxHeartRate: null,
738
+ metadata: { source: "oura_workout" }
739
+ })
740
+ );
741
+ }
742
+ return {
743
+ samples,
744
+ workouts,
745
+ sleepEpisodes,
746
+ identity: getRecord(personal, "data") ?? personal,
747
+ cursor: null
748
+ };
749
+ }
750
+ async function withingsPost(token, path, formValues) {
751
+ const form = new URLSearchParams();
752
+ for (const [key, value] of Object.entries(formValues)) {
753
+ if (value !== null && value !== void 0) {
754
+ form.set(key, String(value));
755
+ }
756
+ }
757
+ return fetchHealthJson({ token, path, method: "POST", form });
758
+ }
759
+ async function syncWithings(args) {
760
+ const startUnix = Math.floor(
761
+ Date.parse(`${args.startDate}T00:00:00.000Z`) / 1e3
762
+ );
763
+ const endUnix = Math.floor(
764
+ Date.parse(`${args.endDate}T23:59:59.999Z`) / 1e3
765
+ );
766
+ const [activityJson, sleepJson, measuresJson] = await Promise.all([
767
+ withingsPost(args.token, "/v2/measure", {
768
+ action: "getactivity",
769
+ startdateymd: ymdCompact(args.startDate),
770
+ enddateymd: ymdCompact(args.endDate)
771
+ }),
772
+ withingsPost(args.token, "/v2/sleep", {
773
+ action: "getsummary",
774
+ startdateymd: ymdCompact(args.startDate),
775
+ enddateymd: ymdCompact(args.endDate)
776
+ }),
777
+ withingsPost(args.token, "/measure", {
778
+ action: "getmeas",
779
+ startdate: startUnix,
780
+ enddate: endUnix,
781
+ meastype: "1,9,10,11,54,71,73"
782
+ })
783
+ ]);
784
+ const samples = [];
785
+ const workouts = [];
786
+ const sleepEpisodes = [];
787
+ const activityBody = getRecord(activityJson, "body") ?? {};
788
+ for (const entry of getArray(activityBody, "activities")) {
789
+ const date = getText(entry, "date");
790
+ const startAt = date ? `${date}T12:00:00.000Z` : null;
791
+ const id = date ?? "activity";
792
+ samples.push(
793
+ ...compactSamples([
794
+ sample({
795
+ token: args.token,
796
+ grantId: args.grantId,
797
+ metric: "steps",
798
+ value: getNumber(entry, "steps"),
799
+ unit: "count",
800
+ startAt,
801
+ sourceExternalId: `${id}:withings:steps`
802
+ }),
803
+ sample({
804
+ token: args.token,
805
+ grantId: args.grantId,
806
+ metric: "active_minutes",
807
+ value: getNumber(entry, "active"),
808
+ unit: "min",
809
+ startAt,
810
+ sourceExternalId: `${id}:withings:active_minutes`
811
+ }),
812
+ sample({
813
+ token: args.token,
814
+ grantId: args.grantId,
815
+ metric: "calories",
816
+ value: getNumber(entry, "totalcalories") ?? getNumber(entry, "calories"),
817
+ unit: "kcal",
818
+ startAt,
819
+ sourceExternalId: `${id}:withings:calories`
820
+ }),
821
+ sample({
822
+ token: args.token,
823
+ grantId: args.grantId,
824
+ metric: "distance_meters",
825
+ value: getNumber(entry, "distance"),
826
+ unit: "m",
827
+ startAt,
828
+ sourceExternalId: `${id}:withings:distance_meters`
829
+ }),
830
+ sample({
831
+ token: args.token,
832
+ grantId: args.grantId,
833
+ metric: "heart_rate",
834
+ value: getNumber(entry, "hr_average"),
835
+ unit: "bpm",
836
+ startAt,
837
+ sourceExternalId: `${id}:withings:heart_rate`
838
+ }),
839
+ sample({
840
+ token: args.token,
841
+ grantId: args.grantId,
842
+ metric: "resting_heart_rate",
843
+ value: getNumber(entry, "hr_resting"),
844
+ unit: "bpm",
845
+ startAt,
846
+ sourceExternalId: `${id}:withings:resting_heart_rate`
847
+ })
848
+ ])
849
+ );
850
+ }
851
+ const sleepBody = getRecord(sleepJson, "body") ?? {};
852
+ for (const entry of getArray(sleepBody, "series")) {
853
+ const startAt = isoFromUnixSeconds(getNumber(entry, "startdate"));
854
+ const endAt = isoFromUnixSeconds(getNumber(entry, "enddate"));
855
+ if (!startAt || !endAt) {
856
+ continue;
857
+ }
858
+ const data = getRecord(entry, "data") ?? {};
859
+ const externalId = getText(entry, "id") ?? `${getText(entry, "startdate") ?? startAt}:sleep`;
860
+ const date = getText(entry, "date") ?? localDateFromIso(startAt);
861
+ const durationSeconds = getNumber(data, "total_sleep_time") ?? Math.max(
862
+ 0,
863
+ Math.trunc((Date.parse(endAt) - Date.parse(startAt)) / 1e3)
864
+ );
865
+ samples.push(
866
+ ...compactSamples([
867
+ sample({
868
+ token: args.token,
869
+ grantId: args.grantId,
870
+ metric: "sleep_hours",
871
+ value: durationSeconds / 3600,
872
+ unit: "h",
873
+ startAt,
874
+ endAt,
875
+ sourceExternalId: `${externalId}:sleep_hours`
876
+ })
877
+ ])
878
+ );
879
+ sleepEpisodes.push(
880
+ createLifeOpsHealthSleepEpisode({
881
+ agentId: args.token.agentId,
882
+ provider: "withings",
883
+ grantId: args.grantId,
884
+ sourceExternalId: externalId,
885
+ localDate: date,
886
+ timezone: getText(entry, "timezone"),
887
+ startAt,
888
+ endAt,
889
+ isMainSleep: true,
890
+ sleepType: "summary",
891
+ durationSeconds,
892
+ timeInBedSeconds: Math.trunc(getNumber(data, "wakeupduration") ?? 0) + durationSeconds,
893
+ efficiency: null,
894
+ latencySeconds: getNumber(data, "durationtosleep"),
895
+ awakeSeconds: getNumber(data, "wakeupduration"),
896
+ lightSleepSeconds: getNumber(data, "lightduration"),
897
+ deepSleepSeconds: getNumber(data, "deepduration"),
898
+ remSleepSeconds: getNumber(data, "remduration"),
899
+ sleepScore: null,
900
+ readinessScore: null,
901
+ averageHeartRate: getNumber(data, "hr_average"),
902
+ lowestHeartRate: getNumber(data, "hr_min"),
903
+ averageHrvMs: null,
904
+ respiratoryRate: getNumber(data, "rr_average"),
905
+ bloodOxygenPercent: null,
906
+ stageSamples: [],
907
+ metadata: { source: "withings_sleep_summary" }
908
+ })
909
+ );
910
+ }
911
+ const measuresBody = getRecord(measuresJson, "body") ?? {};
912
+ for (const group of getArray(measuresBody, "measuregrps")) {
913
+ const measuredAt = isoFromUnixSeconds(getNumber(group, "date"));
914
+ const groupId = getText(group, "grpid") ?? getText(group, "date") ?? "measure";
915
+ for (const measure of getArray(group, "measures")) {
916
+ const mapped = mapWithingsMeasure(measure);
917
+ if (!mapped) {
918
+ continue;
919
+ }
920
+ samples.push(
921
+ ...compactSamples([
922
+ sample({
923
+ token: args.token,
924
+ grantId: args.grantId,
925
+ metric: mapped.metric,
926
+ value: mapped.value,
927
+ unit: mapped.unit,
928
+ startAt: measuredAt,
929
+ sourceExternalId: `${groupId}:withings:${mapped.metric}`,
930
+ metadata: { withingsType: getNumber(measure, "type") }
931
+ })
932
+ ])
933
+ );
934
+ }
935
+ }
936
+ return {
937
+ samples,
938
+ workouts,
939
+ sleepEpisodes,
940
+ identity: args.token.identity,
941
+ cursor: null
942
+ };
943
+ }
944
+ function mapWithingsMeasure(measure) {
945
+ const type = getNumber(measure, "type");
946
+ const value = getNumber(measure, "value");
947
+ const unit = getNumber(measure, "unit");
948
+ if (type === null || value === null || unit === null) {
949
+ return null;
950
+ }
951
+ const normalizedValue = value * 10 ** unit;
952
+ switch (type) {
953
+ case 1:
954
+ return { metric: "weight_kg", value: normalizedValue, unit: "kg" };
955
+ case 9:
956
+ return {
957
+ metric: "blood_pressure_diastolic",
958
+ value: normalizedValue,
959
+ unit: "mmHg"
960
+ };
961
+ case 10:
962
+ return {
963
+ metric: "blood_pressure_systolic",
964
+ value: normalizedValue,
965
+ unit: "mmHg"
966
+ };
967
+ case 11:
968
+ return { metric: "heart_rate", value: normalizedValue, unit: "bpm" };
969
+ case 54:
970
+ return {
971
+ metric: "blood_oxygen_percent",
972
+ value: normalizedValue,
973
+ unit: "%"
974
+ };
975
+ case 71:
976
+ return {
977
+ metric: "body_temperature_celsius",
978
+ value: normalizedValue,
979
+ unit: "C"
980
+ };
981
+ case 73:
982
+ return {
983
+ metric: "heart_rate_variability",
984
+ value: normalizedValue,
985
+ unit: "ms"
986
+ };
987
+ default:
988
+ return null;
989
+ }
990
+ }
991
+ async function syncHealthConnectorData(args) {
992
+ logger.debug(
993
+ {
994
+ boundary: "lifeops",
995
+ operation: "health_connector_sync",
996
+ provider: args.token.provider,
997
+ agentId: args.token.agentId,
998
+ startDate: args.startDate,
999
+ endDate: args.endDate
1000
+ },
1001
+ "[lifeops] Syncing health connector data"
1002
+ );
1003
+ switch (args.token.provider) {
1004
+ case "strava":
1005
+ return syncStrava(args);
1006
+ case "fitbit":
1007
+ return syncFitbit(args);
1008
+ case "withings":
1009
+ return syncWithings(args);
1010
+ case "oura":
1011
+ return syncOura(args);
1012
+ }
1013
+ }
1014
+ export {
1015
+ HealthConnectorApiError,
1016
+ syncHealthConnectorData
1017
+ };
1018
+ //# sourceMappingURL=health-connectors.js.map