@contractspec/integration.providers-impls 2.10.0 → 3.0.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 (57) hide show
  1. package/README.md +7 -1
  2. package/dist/impls/async-event-queue.d.ts +8 -0
  3. package/dist/impls/async-event-queue.js +47 -0
  4. package/dist/impls/health/base-health-provider.d.ts +64 -13
  5. package/dist/impls/health/base-health-provider.js +506 -156
  6. package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
  7. package/dist/impls/health/hybrid-health-providers.js +1088 -0
  8. package/dist/impls/health/official-health-providers.d.ts +78 -0
  9. package/dist/impls/health/official-health-providers.js +968 -0
  10. package/dist/impls/health/provider-normalizers.d.ts +28 -0
  11. package/dist/impls/health/provider-normalizers.js +287 -0
  12. package/dist/impls/health/providers.d.ts +2 -39
  13. package/dist/impls/health/providers.js +895 -184
  14. package/dist/impls/health-provider-factory.js +1009 -196
  15. package/dist/impls/index.d.ts +6 -0
  16. package/dist/impls/index.js +1950 -278
  17. package/dist/impls/messaging-github.d.ts +17 -0
  18. package/dist/impls/messaging-github.js +110 -0
  19. package/dist/impls/messaging-slack.d.ts +14 -0
  20. package/dist/impls/messaging-slack.js +80 -0
  21. package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
  22. package/dist/impls/messaging-whatsapp-meta.js +52 -0
  23. package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
  24. package/dist/impls/messaging-whatsapp-twilio.js +82 -0
  25. package/dist/impls/mistral-conversational.d.ts +23 -0
  26. package/dist/impls/mistral-conversational.js +476 -0
  27. package/dist/impls/mistral-conversational.session.d.ts +32 -0
  28. package/dist/impls/mistral-conversational.session.js +206 -0
  29. package/dist/impls/mistral-stt.d.ts +17 -0
  30. package/dist/impls/mistral-stt.js +167 -0
  31. package/dist/impls/provider-factory.d.ts +5 -1
  32. package/dist/impls/provider-factory.js +1943 -277
  33. package/dist/impls/stripe-payments.js +1 -1
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +1953 -278
  36. package/dist/messaging.d.ts +1 -0
  37. package/dist/messaging.js +3 -0
  38. package/dist/node/impls/async-event-queue.js +46 -0
  39. package/dist/node/impls/health/base-health-provider.js +506 -156
  40. package/dist/node/impls/health/hybrid-health-providers.js +1087 -0
  41. package/dist/node/impls/health/official-health-providers.js +967 -0
  42. package/dist/node/impls/health/provider-normalizers.js +286 -0
  43. package/dist/node/impls/health/providers.js +895 -184
  44. package/dist/node/impls/health-provider-factory.js +1009 -196
  45. package/dist/node/impls/index.js +1950 -278
  46. package/dist/node/impls/messaging-github.js +109 -0
  47. package/dist/node/impls/messaging-slack.js +79 -0
  48. package/dist/node/impls/messaging-whatsapp-meta.js +51 -0
  49. package/dist/node/impls/messaging-whatsapp-twilio.js +81 -0
  50. package/dist/node/impls/mistral-conversational.js +475 -0
  51. package/dist/node/impls/mistral-conversational.session.js +205 -0
  52. package/dist/node/impls/mistral-stt.js +166 -0
  53. package/dist/node/impls/provider-factory.js +1943 -277
  54. package/dist/node/impls/stripe-payments.js +1 -1
  55. package/dist/node/index.js +1953 -278
  56. package/dist/node/messaging.js +2 -0
  57. package/package.json +156 -12
@@ -1,3 +1,47 @@
1
+ // src/impls/async-event-queue.ts
2
+ class AsyncEventQueue {
3
+ values = [];
4
+ waiters = [];
5
+ done = false;
6
+ push(value) {
7
+ if (this.done) {
8
+ return;
9
+ }
10
+ const waiter = this.waiters.shift();
11
+ if (waiter) {
12
+ waiter({ value, done: false });
13
+ return;
14
+ }
15
+ this.values.push(value);
16
+ }
17
+ close() {
18
+ if (this.done) {
19
+ return;
20
+ }
21
+ this.done = true;
22
+ for (const waiter of this.waiters) {
23
+ waiter({ value: undefined, done: true });
24
+ }
25
+ this.waiters.length = 0;
26
+ }
27
+ [Symbol.asyncIterator]() {
28
+ return {
29
+ next: async () => {
30
+ const value = this.values.shift();
31
+ if (value != null) {
32
+ return { value, done: false };
33
+ }
34
+ if (this.done) {
35
+ return { value: undefined, done: true };
36
+ }
37
+ return new Promise((resolve) => {
38
+ this.waiters.push(resolve);
39
+ });
40
+ }
41
+ };
42
+ }
43
+ }
44
+
1
45
  // src/impls/elevenlabs-voice.ts
2
46
  import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
3
47
  var FORMAT_MAP = {
@@ -2015,7 +2059,286 @@ async function safeReadError3(response) {
2015
2059
  }
2016
2060
  }
2017
2061
 
2062
+ // src/impls/health/provider-normalizers.ts
2063
+ var DEFAULT_LIST_KEYS = [
2064
+ "items",
2065
+ "data",
2066
+ "records",
2067
+ "activities",
2068
+ "workouts",
2069
+ "sleep",
2070
+ "biometrics",
2071
+ "nutrition"
2072
+ ];
2073
+ function asRecord(value) {
2074
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2075
+ return;
2076
+ }
2077
+ return value;
2078
+ }
2079
+ function asArray2(value) {
2080
+ return Array.isArray(value) ? value : undefined;
2081
+ }
2082
+ function readString2(record, keys) {
2083
+ if (!record)
2084
+ return;
2085
+ for (const key of keys) {
2086
+ const value = record[key];
2087
+ if (typeof value === "string" && value.trim().length > 0) {
2088
+ return value;
2089
+ }
2090
+ }
2091
+ return;
2092
+ }
2093
+ function readNumber(record, keys) {
2094
+ if (!record)
2095
+ return;
2096
+ for (const key of keys) {
2097
+ const value = record[key];
2098
+ if (typeof value === "number" && Number.isFinite(value)) {
2099
+ return value;
2100
+ }
2101
+ if (typeof value === "string" && value.trim().length > 0) {
2102
+ const parsed = Number(value);
2103
+ if (Number.isFinite(parsed)) {
2104
+ return parsed;
2105
+ }
2106
+ }
2107
+ }
2108
+ return;
2109
+ }
2110
+ function readBoolean2(record, keys) {
2111
+ if (!record)
2112
+ return;
2113
+ for (const key of keys) {
2114
+ const value = record[key];
2115
+ if (typeof value === "boolean") {
2116
+ return value;
2117
+ }
2118
+ }
2119
+ return;
2120
+ }
2121
+ function extractList(payload, listKeys = DEFAULT_LIST_KEYS) {
2122
+ const root = asRecord(payload);
2123
+ if (!root) {
2124
+ return asArray2(payload)?.map((item) => asRecord(item)).filter((item) => Boolean(item)) ?? [];
2125
+ }
2126
+ for (const key of listKeys) {
2127
+ const arrayValue = asArray2(root[key]);
2128
+ if (!arrayValue)
2129
+ continue;
2130
+ return arrayValue.map((item) => asRecord(item)).filter((item) => Boolean(item));
2131
+ }
2132
+ return [];
2133
+ }
2134
+ function extractPagination(payload) {
2135
+ const root = asRecord(payload);
2136
+ const nestedPagination = asRecord(root?.pagination);
2137
+ const nextCursor = readString2(nestedPagination, ["nextCursor", "next_cursor"]) ?? readString2(root, [
2138
+ "nextCursor",
2139
+ "next_cursor",
2140
+ "cursor",
2141
+ "next_page_token"
2142
+ ]);
2143
+ const hasMore = readBoolean2(nestedPagination, ["hasMore", "has_more"]) ?? readBoolean2(root, ["hasMore", "has_more"]);
2144
+ return {
2145
+ nextCursor,
2146
+ hasMore: hasMore ?? Boolean(nextCursor)
2147
+ };
2148
+ }
2149
+ function toHealthActivity(item, context, fallbackType = "activity") {
2150
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:${fallbackType}`;
2151
+ const id = readString2(item, ["id", "uuid", "workout_id"]) ?? `${context.providerKey}:activity:${externalId}`;
2152
+ return {
2153
+ id,
2154
+ externalId,
2155
+ tenantId: context.tenantId,
2156
+ connectionId: context.connectionId ?? "unknown",
2157
+ userId: readString2(item, ["user_id", "userId", "athlete_id"]),
2158
+ providerKey: context.providerKey,
2159
+ activityType: readString2(item, ["activity_type", "type", "sport_type", "sport"]) ?? fallbackType,
2160
+ startedAt: readIsoDate(item, [
2161
+ "started_at",
2162
+ "start_time",
2163
+ "start_date",
2164
+ "created_at"
2165
+ ]),
2166
+ endedAt: readIsoDate(item, ["ended_at", "end_time"]),
2167
+ durationSeconds: readNumber(item, [
2168
+ "duration_seconds",
2169
+ "duration",
2170
+ "elapsed_time"
2171
+ ]),
2172
+ distanceMeters: readNumber(item, ["distance_meters", "distance"]),
2173
+ caloriesKcal: readNumber(item, [
2174
+ "calories_kcal",
2175
+ "calories",
2176
+ "active_kilocalories"
2177
+ ]),
2178
+ steps: readNumber(item, ["steps"])?.valueOf(),
2179
+ metadata: item
2180
+ };
2181
+ }
2182
+ function toHealthWorkout(item, context, fallbackType = "workout") {
2183
+ const activity = toHealthActivity(item, context, fallbackType);
2184
+ return {
2185
+ id: activity.id,
2186
+ externalId: activity.externalId,
2187
+ tenantId: activity.tenantId,
2188
+ connectionId: activity.connectionId,
2189
+ userId: activity.userId,
2190
+ providerKey: activity.providerKey,
2191
+ workoutType: readString2(item, [
2192
+ "workout_type",
2193
+ "sport_type",
2194
+ "type",
2195
+ "activity_type"
2196
+ ]) ?? fallbackType,
2197
+ startedAt: activity.startedAt,
2198
+ endedAt: activity.endedAt,
2199
+ durationSeconds: activity.durationSeconds,
2200
+ distanceMeters: activity.distanceMeters,
2201
+ caloriesKcal: activity.caloriesKcal,
2202
+ averageHeartRateBpm: readNumber(item, [
2203
+ "average_heart_rate",
2204
+ "avg_hr",
2205
+ "average_heart_rate_bpm"
2206
+ ]),
2207
+ maxHeartRateBpm: readNumber(item, [
2208
+ "max_heart_rate",
2209
+ "max_hr",
2210
+ "max_heart_rate_bpm"
2211
+ ]),
2212
+ metadata: item
2213
+ };
2214
+ }
2215
+ function toHealthSleep(item, context) {
2216
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:sleep`;
2217
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:sleep:${externalId}`;
2218
+ const startedAt = readIsoDate(item, ["started_at", "start_time", "bedtime_start", "start"]) ?? new Date(0).toISOString();
2219
+ const endedAt = readIsoDate(item, ["ended_at", "end_time", "bedtime_end", "end"]) ?? startedAt;
2220
+ return {
2221
+ id,
2222
+ externalId,
2223
+ tenantId: context.tenantId,
2224
+ connectionId: context.connectionId ?? "unknown",
2225
+ userId: readString2(item, ["user_id", "userId"]),
2226
+ providerKey: context.providerKey,
2227
+ startedAt,
2228
+ endedAt,
2229
+ durationSeconds: readNumber(item, [
2230
+ "duration_seconds",
2231
+ "duration",
2232
+ "total_sleep_duration"
2233
+ ]),
2234
+ deepSleepSeconds: readNumber(item, [
2235
+ "deep_sleep_seconds",
2236
+ "deep_sleep_duration"
2237
+ ]),
2238
+ lightSleepSeconds: readNumber(item, [
2239
+ "light_sleep_seconds",
2240
+ "light_sleep_duration"
2241
+ ]),
2242
+ remSleepSeconds: readNumber(item, [
2243
+ "rem_sleep_seconds",
2244
+ "rem_sleep_duration"
2245
+ ]),
2246
+ awakeSeconds: readNumber(item, ["awake_seconds", "awake_time"]),
2247
+ sleepScore: readNumber(item, ["sleep_score", "score"]),
2248
+ metadata: item
2249
+ };
2250
+ }
2251
+ function toHealthBiometric(item, context, metricTypeFallback = "metric") {
2252
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:biometric`;
2253
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:biometric:${externalId}`;
2254
+ return {
2255
+ id,
2256
+ externalId,
2257
+ tenantId: context.tenantId,
2258
+ connectionId: context.connectionId ?? "unknown",
2259
+ userId: readString2(item, ["user_id", "userId"]),
2260
+ providerKey: context.providerKey,
2261
+ metricType: readString2(item, ["metric_type", "metric", "type", "name"]) ?? metricTypeFallback,
2262
+ value: readNumber(item, ["value", "score", "measurement"]) ?? 0,
2263
+ unit: readString2(item, ["unit"]),
2264
+ measuredAt: readIsoDate(item, ["measured_at", "timestamp", "created_at"]) ?? new Date().toISOString(),
2265
+ metadata: item
2266
+ };
2267
+ }
2268
+ function toHealthNutrition(item, context) {
2269
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:nutrition`;
2270
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:nutrition:${externalId}`;
2271
+ return {
2272
+ id,
2273
+ externalId,
2274
+ tenantId: context.tenantId,
2275
+ connectionId: context.connectionId ?? "unknown",
2276
+ userId: readString2(item, ["user_id", "userId"]),
2277
+ providerKey: context.providerKey,
2278
+ loggedAt: readIsoDate(item, ["logged_at", "created_at", "date", "timestamp"]) ?? new Date().toISOString(),
2279
+ caloriesKcal: readNumber(item, ["calories_kcal", "calories"]),
2280
+ proteinGrams: readNumber(item, ["protein_grams", "protein"]),
2281
+ carbsGrams: readNumber(item, ["carbs_grams", "carbs"]),
2282
+ fatGrams: readNumber(item, ["fat_grams", "fat"]),
2283
+ fiberGrams: readNumber(item, ["fiber_grams", "fiber"]),
2284
+ hydrationMl: readNumber(item, ["hydration_ml", "water_ml", "water"]),
2285
+ metadata: item
2286
+ };
2287
+ }
2288
+ function toHealthConnectionStatus(payload, params, source) {
2289
+ const record = asRecord(payload);
2290
+ const rawStatus = readString2(record, ["status", "connection_status", "health"]) ?? "healthy";
2291
+ return {
2292
+ tenantId: params.tenantId,
2293
+ connectionId: params.connectionId,
2294
+ status: rawStatus === "healthy" || rawStatus === "degraded" || rawStatus === "error" || rawStatus === "disconnected" ? rawStatus : "healthy",
2295
+ source,
2296
+ lastCheckedAt: readIsoDate(record, ["last_checked_at", "lastCheckedAt"]) ?? new Date().toISOString(),
2297
+ errorCode: readString2(record, ["error_code", "errorCode"]),
2298
+ errorMessage: readString2(record, ["error_message", "errorMessage"]),
2299
+ metadata: asRecord(record?.metadata)
2300
+ };
2301
+ }
2302
+ function toHealthWebhookEvent(payload, providerKey, verified) {
2303
+ const record = asRecord(payload);
2304
+ const entityType = readString2(record, ["entity_type", "entityType", "type"]);
2305
+ const normalizedEntityType = entityType === "activity" || entityType === "workout" || entityType === "sleep" || entityType === "biometric" || entityType === "nutrition" ? entityType : undefined;
2306
+ return {
2307
+ providerKey,
2308
+ eventType: readString2(record, ["event_type", "eventType", "event"]),
2309
+ externalEntityId: readString2(record, [
2310
+ "external_entity_id",
2311
+ "externalEntityId",
2312
+ "entity_id",
2313
+ "entityId",
2314
+ "id"
2315
+ ]),
2316
+ entityType: normalizedEntityType,
2317
+ receivedAt: new Date().toISOString(),
2318
+ verified,
2319
+ payload,
2320
+ metadata: asRecord(record?.metadata)
2321
+ };
2322
+ }
2323
+ function readIsoDate(record, keys) {
2324
+ const value = readString2(record, keys);
2325
+ if (!value)
2326
+ return;
2327
+ const parsed = new Date(value);
2328
+ if (Number.isNaN(parsed.getTime()))
2329
+ return;
2330
+ return parsed.toISOString();
2331
+ }
2332
+
2018
2333
  // src/impls/health/base-health-provider.ts
2334
+ class HealthProviderCapabilityError extends Error {
2335
+ code = "NOT_SUPPORTED";
2336
+ constructor(message) {
2337
+ super(message);
2338
+ this.name = "HealthProviderCapabilityError";
2339
+ }
2340
+ }
2341
+
2019
2342
  class BaseHealthProvider {
2020
2343
  providerKey;
2021
2344
  transport;
@@ -2023,146 +2346,191 @@ class BaseHealthProvider {
2023
2346
  mcpUrl;
2024
2347
  apiKey;
2025
2348
  accessToken;
2349
+ refreshToken;
2026
2350
  mcpAccessToken;
2027
2351
  webhookSecret;
2352
+ webhookSignatureHeader;
2353
+ route;
2354
+ aggregatorKey;
2355
+ oauth;
2028
2356
  fetchFn;
2029
2357
  mcpRequestId = 0;
2030
2358
  constructor(options) {
2031
2359
  this.providerKey = options.providerKey;
2032
2360
  this.transport = options.transport;
2033
- this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
2361
+ this.apiBaseUrl = options.apiBaseUrl;
2034
2362
  this.mcpUrl = options.mcpUrl;
2035
2363
  this.apiKey = options.apiKey;
2036
2364
  this.accessToken = options.accessToken;
2365
+ this.refreshToken = options.oauth?.refreshToken;
2037
2366
  this.mcpAccessToken = options.mcpAccessToken;
2038
2367
  this.webhookSecret = options.webhookSecret;
2368
+ this.webhookSignatureHeader = options.webhookSignatureHeader ?? "x-webhook-signature";
2369
+ this.route = options.route ?? "primary";
2370
+ this.aggregatorKey = options.aggregatorKey;
2371
+ this.oauth = options.oauth ?? {};
2039
2372
  this.fetchFn = options.fetchFn ?? fetch;
2040
2373
  }
2041
- async listActivities(params) {
2042
- const result = await this.fetchList("activities", params);
2043
- return {
2044
- activities: result.items,
2045
- nextCursor: result.nextCursor,
2046
- hasMore: result.hasMore,
2047
- source: this.currentSource()
2048
- };
2374
+ async listActivities(_params) {
2375
+ throw this.unsupported("activities");
2049
2376
  }
2050
- async listWorkouts(params) {
2051
- const result = await this.fetchList("workouts", params);
2377
+ async listWorkouts(_params) {
2378
+ throw this.unsupported("workouts");
2379
+ }
2380
+ async listSleep(_params) {
2381
+ throw this.unsupported("sleep");
2382
+ }
2383
+ async listBiometrics(_params) {
2384
+ throw this.unsupported("biometrics");
2385
+ }
2386
+ async listNutrition(_params) {
2387
+ throw this.unsupported("nutrition");
2388
+ }
2389
+ async getConnectionStatus(params) {
2390
+ return this.fetchConnectionStatus(params, {
2391
+ mcpTool: `${this.providerSlug()}_connection_status`
2392
+ });
2393
+ }
2394
+ async syncActivities(params) {
2395
+ return this.syncFromList(() => this.listActivities(params));
2396
+ }
2397
+ async syncWorkouts(params) {
2398
+ return this.syncFromList(() => this.listWorkouts(params));
2399
+ }
2400
+ async syncSleep(params) {
2401
+ return this.syncFromList(() => this.listSleep(params));
2402
+ }
2403
+ async syncBiometrics(params) {
2404
+ return this.syncFromList(() => this.listBiometrics(params));
2405
+ }
2406
+ async syncNutrition(params) {
2407
+ return this.syncFromList(() => this.listNutrition(params));
2408
+ }
2409
+ async parseWebhook(request) {
2410
+ const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
2411
+ const verified = await this.verifyWebhook(request);
2412
+ return toHealthWebhookEvent(payload, this.providerKey, verified);
2413
+ }
2414
+ async verifyWebhook(request) {
2415
+ if (!this.webhookSecret)
2416
+ return true;
2417
+ const signature = readHeader(request.headers, this.webhookSignatureHeader);
2418
+ return signature === this.webhookSecret;
2419
+ }
2420
+ async fetchActivities(params, config) {
2421
+ const response = await this.fetchList(params, config);
2052
2422
  return {
2053
- workouts: result.items,
2054
- nextCursor: result.nextCursor,
2055
- hasMore: result.hasMore,
2423
+ activities: response.items,
2424
+ nextCursor: response.nextCursor,
2425
+ hasMore: response.hasMore,
2056
2426
  source: this.currentSource()
2057
2427
  };
2058
2428
  }
2059
- async listSleep(params) {
2060
- const result = await this.fetchList("sleep", params);
2429
+ async fetchWorkouts(params, config) {
2430
+ const response = await this.fetchList(params, config);
2061
2431
  return {
2062
- sleep: result.items,
2063
- nextCursor: result.nextCursor,
2064
- hasMore: result.hasMore,
2432
+ workouts: response.items,
2433
+ nextCursor: response.nextCursor,
2434
+ hasMore: response.hasMore,
2065
2435
  source: this.currentSource()
2066
2436
  };
2067
2437
  }
2068
- async listBiometrics(params) {
2069
- const result = await this.fetchList("biometrics", params);
2438
+ async fetchSleep(params, config) {
2439
+ const response = await this.fetchList(params, config);
2070
2440
  return {
2071
- biometrics: result.items,
2072
- nextCursor: result.nextCursor,
2073
- hasMore: result.hasMore,
2441
+ sleep: response.items,
2442
+ nextCursor: response.nextCursor,
2443
+ hasMore: response.hasMore,
2074
2444
  source: this.currentSource()
2075
2445
  };
2076
2446
  }
2077
- async listNutrition(params) {
2078
- const result = await this.fetchList("nutrition", params);
2447
+ async fetchBiometrics(params, config) {
2448
+ const response = await this.fetchList(params, config);
2079
2449
  return {
2080
- nutrition: result.items,
2081
- nextCursor: result.nextCursor,
2082
- hasMore: result.hasMore,
2450
+ biometrics: response.items,
2451
+ nextCursor: response.nextCursor,
2452
+ hasMore: response.hasMore,
2083
2453
  source: this.currentSource()
2084
2454
  };
2085
2455
  }
2086
- async getConnectionStatus(params) {
2087
- const payload = await this.fetchRecord("connection/status", params);
2088
- const status = readString2(payload, "status") ?? "healthy";
2456
+ async fetchNutrition(params, config) {
2457
+ const response = await this.fetchList(params, config);
2089
2458
  return {
2090
- tenantId: params.tenantId,
2091
- connectionId: params.connectionId,
2092
- status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
2093
- source: this.currentSource(),
2094
- lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
2095
- errorCode: readString2(payload, "errorCode"),
2096
- errorMessage: readString2(payload, "errorMessage"),
2097
- metadata: asRecord(payload.metadata)
2459
+ nutrition: response.items,
2460
+ nextCursor: response.nextCursor,
2461
+ hasMore: response.hasMore,
2462
+ source: this.currentSource()
2098
2463
  };
2099
2464
  }
2100
- async syncActivities(params) {
2101
- return this.sync("activities", params);
2102
- }
2103
- async syncWorkouts(params) {
2104
- return this.sync("workouts", params);
2105
- }
2106
- async syncSleep(params) {
2107
- return this.sync("sleep", params);
2108
- }
2109
- async syncBiometrics(params) {
2110
- return this.sync("biometrics", params);
2111
- }
2112
- async syncNutrition(params) {
2113
- return this.sync("nutrition", params);
2465
+ async fetchConnectionStatus(params, config) {
2466
+ const payload = await this.fetchPayload(config, params);
2467
+ return toHealthConnectionStatus(payload, params, this.currentSource());
2114
2468
  }
2115
- async parseWebhook(request) {
2116
- const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
2117
- const body = asRecord(payload);
2469
+ currentSource() {
2118
2470
  return {
2119
2471
  providerKey: this.providerKey,
2120
- eventType: readString2(body, "eventType") ?? readString2(body, "event"),
2121
- externalEntityId: readString2(body, "externalEntityId") ?? readString2(body, "entityId"),
2122
- entityType: normalizeEntityType(readString2(body, "entityType") ?? readString2(body, "type")),
2123
- receivedAt: new Date().toISOString(),
2124
- verified: await this.verifyWebhook(request),
2125
- payload
2472
+ transport: this.transport,
2473
+ route: this.route,
2474
+ aggregatorKey: this.aggregatorKey
2126
2475
  };
2127
2476
  }
2128
- async verifyWebhook(request) {
2129
- if (!this.webhookSecret) {
2130
- return true;
2131
- }
2132
- const signature = readHeader(request.headers, "x-webhook-signature");
2133
- return signature === this.webhookSecret;
2477
+ providerSlug() {
2478
+ return this.providerKey.replace("health.", "").replace(/-/g, "_");
2134
2479
  }
2135
- async fetchList(resource, params) {
2136
- const payload = await this.fetchRecord(resource, params);
2137
- const items = asArray2(payload.items) ?? asArray2(payload[resource]) ?? asArray2(payload.records) ?? [];
2480
+ unsupported(capability) {
2481
+ return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
2482
+ }
2483
+ async syncFromList(executor) {
2484
+ const result = await executor();
2485
+ const records = countResultRecords(result);
2138
2486
  return {
2139
- items,
2140
- nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
2141
- hasMore: readBoolean2(payload, "hasMore")
2487
+ synced: records,
2488
+ failed: 0,
2489
+ nextCursor: undefined,
2490
+ source: result.source
2142
2491
  };
2143
2492
  }
2144
- async sync(resource, params) {
2145
- const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
2493
+ async fetchList(params, config) {
2494
+ const payload = await this.fetchPayload(config, params);
2495
+ const items = extractList(payload, config.listKeys).map((item) => config.mapItem(item, params)).filter((item) => Boolean(item));
2496
+ const pagination = extractPagination(payload);
2146
2497
  return {
2147
- synced: readNumber(payload, "synced") ?? 0,
2148
- failed: readNumber(payload, "failed") ?? 0,
2149
- nextCursor: readString2(payload, "nextCursor"),
2150
- errors: asArray2(payload.errors)?.map((item) => String(item)),
2151
- source: this.currentSource()
2498
+ items,
2499
+ nextCursor: pagination.nextCursor,
2500
+ hasMore: pagination.hasMore
2152
2501
  };
2153
2502
  }
2154
- async fetchRecord(resource, params, method = "GET") {
2155
- if (this.transport.endsWith("mcp")) {
2156
- return this.callMcpTool(resource, params);
2503
+ async fetchPayload(config, params) {
2504
+ const method = config.method ?? "GET";
2505
+ const query = config.buildQuery?.(params);
2506
+ const body = config.buildBody?.(params);
2507
+ if (this.isMcpTransport()) {
2508
+ return this.callMcpTool(config.mcpTool, {
2509
+ ...query ?? {},
2510
+ ...body ?? {}
2511
+ });
2512
+ }
2513
+ if (!config.apiPath || !this.apiBaseUrl) {
2514
+ throw new Error(`${this.providerKey} transport is missing an API path.`);
2157
2515
  }
2158
- const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
2159
- if (method === "GET") {
2160
- for (const [key, value] of Object.entries(params)) {
2516
+ if (method === "POST") {
2517
+ return this.requestApi(config.apiPath, "POST", undefined, body);
2518
+ }
2519
+ return this.requestApi(config.apiPath, "GET", query, undefined);
2520
+ }
2521
+ isMcpTransport() {
2522
+ return this.transport.endsWith("mcp") || this.transport === "unofficial";
2523
+ }
2524
+ async requestApi(path, method, query, body) {
2525
+ const url = new URL(path, ensureTrailingSlash(this.apiBaseUrl ?? ""));
2526
+ if (query) {
2527
+ for (const [key, value] of Object.entries(query)) {
2161
2528
  if (value == null)
2162
2529
  continue;
2163
2530
  if (Array.isArray(value)) {
2164
- value.forEach((item) => {
2165
- url.searchParams.append(key, String(item));
2531
+ value.forEach((entry) => {
2532
+ if (entry != null)
2533
+ url.searchParams.append(key, String(entry));
2166
2534
  });
2167
2535
  continue;
2168
2536
  }
@@ -2171,22 +2539,22 @@ class BaseHealthProvider {
2171
2539
  }
2172
2540
  const response = await this.fetchFn(url, {
2173
2541
  method,
2174
- headers: {
2175
- "Content-Type": "application/json",
2176
- ...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
2177
- },
2178
- body: method === "POST" ? JSON.stringify(params) : undefined
2542
+ headers: this.authorizationHeaders(),
2543
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
2179
2544
  });
2180
- if (!response.ok) {
2181
- const errorBody = await safeResponseText(response);
2182
- throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
2545
+ if (response.status === 401 && await this.refreshAccessToken()) {
2546
+ const retryResponse = await this.fetchFn(url, {
2547
+ method,
2548
+ headers: this.authorizationHeaders(),
2549
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
2550
+ });
2551
+ return this.readResponsePayload(retryResponse, path);
2183
2552
  }
2184
- const data = await response.json();
2185
- return asRecord(data) ?? {};
2553
+ return this.readResponsePayload(response, path);
2186
2554
  }
2187
- async callMcpTool(resource, params) {
2555
+ async callMcpTool(toolName, args) {
2188
2556
  if (!this.mcpUrl) {
2189
- return {};
2557
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
2190
2558
  }
2191
2559
  const response = await this.fetchFn(this.mcpUrl, {
2192
2560
  method: "POST",
@@ -2199,78 +2567,103 @@ class BaseHealthProvider {
2199
2567
  id: ++this.mcpRequestId,
2200
2568
  method: "tools/call",
2201
2569
  params: {
2202
- name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
2203
- arguments: params
2570
+ name: toolName,
2571
+ arguments: args
2204
2572
  }
2205
2573
  })
2206
2574
  });
2207
- if (!response.ok) {
2208
- const errorBody = await safeResponseText(response);
2209
- throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
2210
- }
2211
- const rpcPayload = await response.json();
2212
- const rpc = asRecord(rpcPayload);
2213
- const result = asRecord(rpc?.result) ?? {};
2214
- const structured = asRecord(result.structuredContent);
2215
- if (structured)
2216
- return structured;
2217
- const data = asRecord(result.data);
2218
- if (data)
2219
- return data;
2220
- return result;
2221
- }
2222
- currentSource() {
2575
+ const payload = await this.readResponsePayload(response, toolName);
2576
+ const rpcEnvelope = asRecord(payload);
2577
+ if (!rpcEnvelope)
2578
+ return payload;
2579
+ const rpcResult = asRecord(rpcEnvelope.result);
2580
+ if (rpcResult) {
2581
+ return rpcResult.structuredContent ?? rpcResult.data ?? rpcResult;
2582
+ }
2583
+ return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
2584
+ }
2585
+ authorizationHeaders() {
2586
+ const token = this.accessToken ?? this.apiKey;
2223
2587
  return {
2224
- providerKey: this.providerKey,
2225
- transport: this.transport,
2226
- route: "primary"
2588
+ "Content-Type": "application/json",
2589
+ ...token ? { Authorization: `Bearer ${token}` } : {}
2227
2590
  };
2228
2591
  }
2229
- }
2230
- function safeJsonParse(raw) {
2231
- try {
2232
- return JSON.parse(raw);
2233
- } catch {
2234
- return { rawBody: raw };
2592
+ async refreshAccessToken() {
2593
+ if (!this.oauth.tokenUrl || !this.refreshToken) {
2594
+ return false;
2595
+ }
2596
+ const tokenUrl = new URL(this.oauth.tokenUrl);
2597
+ const body = new URLSearchParams({
2598
+ grant_type: "refresh_token",
2599
+ refresh_token: this.refreshToken,
2600
+ ...this.oauth.clientId ? { client_id: this.oauth.clientId } : {},
2601
+ ...this.oauth.clientSecret ? { client_secret: this.oauth.clientSecret } : {}
2602
+ });
2603
+ const response = await this.fetchFn(tokenUrl, {
2604
+ method: "POST",
2605
+ headers: {
2606
+ "Content-Type": "application/x-www-form-urlencoded"
2607
+ },
2608
+ body: body.toString()
2609
+ });
2610
+ if (!response.ok) {
2611
+ return false;
2612
+ }
2613
+ const payload = await response.json();
2614
+ this.accessToken = payload.access_token;
2615
+ this.refreshToken = payload.refresh_token ?? this.refreshToken;
2616
+ if (typeof payload.expires_in === "number") {
2617
+ this.oauth.tokenExpiresAt = new Date(Date.now() + payload.expires_in * 1000).toISOString();
2618
+ }
2619
+ return Boolean(this.accessToken);
2620
+ }
2621
+ async readResponsePayload(response, context) {
2622
+ if (!response.ok) {
2623
+ const message = await safeReadText2(response);
2624
+ throw new Error(`${this.providerKey} request ${context} failed (${response.status}): ${message}`);
2625
+ }
2626
+ if (response.status === 204) {
2627
+ return {};
2628
+ }
2629
+ return response.json();
2235
2630
  }
2236
2631
  }
2237
2632
  function readHeader(headers, key) {
2238
- const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
2239
- if (!match)
2633
+ const target = key.toLowerCase();
2634
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
2635
+ if (!entry)
2240
2636
  return;
2241
- const value = match[1];
2637
+ const value = entry[1];
2242
2638
  return Array.isArray(value) ? value[0] : value;
2243
2639
  }
2244
- function normalizeEntityType(value) {
2245
- if (!value)
2246
- return;
2247
- if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
2248
- return value;
2249
- }
2250
- return;
2251
- }
2252
- function asRecord(value) {
2253
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2254
- return;
2640
+ function countResultRecords(result) {
2641
+ const listKeys = [
2642
+ "activities",
2643
+ "workouts",
2644
+ "sleep",
2645
+ "biometrics",
2646
+ "nutrition"
2647
+ ];
2648
+ for (const key of listKeys) {
2649
+ const value = result[key];
2650
+ if (Array.isArray(value)) {
2651
+ return value.length;
2652
+ }
2255
2653
  }
2256
- return value;
2257
- }
2258
- function asArray2(value) {
2259
- return Array.isArray(value) ? value : undefined;
2260
- }
2261
- function readString2(record, key) {
2262
- const value = record?.[key];
2263
- return typeof value === "string" ? value : undefined;
2654
+ return 0;
2264
2655
  }
2265
- function readBoolean2(record, key) {
2266
- const value = record?.[key];
2267
- return typeof value === "boolean" ? value : undefined;
2656
+ function ensureTrailingSlash(value) {
2657
+ return value.endsWith("/") ? value : `${value}/`;
2268
2658
  }
2269
- function readNumber(record, key) {
2270
- const value = record?.[key];
2271
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2659
+ function safeJsonParse(raw) {
2660
+ try {
2661
+ return JSON.parse(raw);
2662
+ } catch {
2663
+ return { rawBody: raw };
2664
+ }
2272
2665
  }
2273
- async function safeResponseText(response) {
2666
+ async function safeReadText2(response) {
2274
2667
  try {
2275
2668
  return await response.text();
2276
2669
  } catch {
@@ -2278,133 +2671,530 @@ async function safeResponseText(response) {
2278
2671
  }
2279
2672
  }
2280
2673
 
2281
- // src/impls/health/providers.ts
2282
- function createProviderOptions(options, fallbackTransport) {
2674
+ // src/impls/health/official-health-providers.ts
2675
+ function buildSharedQuery(params) {
2283
2676
  return {
2284
- ...options,
2285
- transport: options.transport ?? fallbackTransport
2677
+ tenantId: params.tenantId,
2678
+ connectionId: params.connectionId,
2679
+ userId: params.userId,
2680
+ from: params.from,
2681
+ to: params.to,
2682
+ cursor: params.cursor,
2683
+ pageSize: params.pageSize
2684
+ };
2685
+ }
2686
+ function withMetricTypes(params) {
2687
+ return {
2688
+ ...buildSharedQuery(params),
2689
+ metricTypes: params.metricTypes
2286
2690
  };
2287
2691
  }
2288
2692
 
2289
2693
  class OpenWearablesHealthProvider extends BaseHealthProvider {
2694
+ upstreamProvider;
2290
2695
  constructor(options) {
2291
2696
  super({
2292
- providerKey: "health.openwearables",
2293
- ...createProviderOptions(options, "aggregator-api")
2697
+ providerKey: options.providerKey ?? "health.openwearables",
2698
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
2699
+ webhookSignatureHeader: "x-openwearables-signature",
2700
+ ...options
2294
2701
  });
2702
+ this.upstreamProvider = options.upstreamProvider;
2295
2703
  }
2296
- }
2297
-
2298
- class WhoopHealthProvider extends BaseHealthProvider {
2299
- constructor(options) {
2300
- super({
2301
- providerKey: "health.whoop",
2302
- ...createProviderOptions(options, "official-api")
2704
+ async listActivities(params) {
2705
+ return this.fetchActivities(params, {
2706
+ apiPath: "/v1/activities",
2707
+ mcpTool: "openwearables_list_activities",
2708
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2709
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
2303
2710
  });
2304
2711
  }
2305
- }
2306
-
2307
- class AppleHealthBridgeProvider extends BaseHealthProvider {
2308
- constructor(options) {
2309
- super({
2310
- providerKey: "health.apple-health",
2311
- ...createProviderOptions(options, "aggregator-api")
2712
+ async listWorkouts(params) {
2713
+ return this.fetchWorkouts(params, {
2714
+ apiPath: "/v1/workouts",
2715
+ mcpTool: "openwearables_list_workouts",
2716
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2717
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2312
2718
  });
2313
2719
  }
2314
- }
2315
-
2316
- class OuraHealthProvider extends BaseHealthProvider {
2317
- constructor(options) {
2318
- super({
2319
- providerKey: "health.oura",
2320
- ...createProviderOptions(options, "official-api")
2720
+ async listSleep(params) {
2721
+ return this.fetchSleep(params, {
2722
+ apiPath: "/v1/sleep",
2723
+ mcpTool: "openwearables_list_sleep",
2724
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2725
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2321
2726
  });
2322
2727
  }
2323
- }
2324
-
2325
- class StravaHealthProvider extends BaseHealthProvider {
2326
- constructor(options) {
2327
- super({
2328
- providerKey: "health.strava",
2329
- ...createProviderOptions(options, "official-api")
2728
+ async listBiometrics(params) {
2729
+ return this.fetchBiometrics(params, {
2730
+ apiPath: "/v1/biometrics",
2731
+ mcpTool: "openwearables_list_biometrics",
2732
+ buildQuery: (input) => this.withUpstreamProvider(withMetricTypes(input)),
2733
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input))
2330
2734
  });
2331
2735
  }
2332
- }
2736
+ async listNutrition(params) {
2737
+ return this.fetchNutrition(params, {
2738
+ apiPath: "/v1/nutrition",
2739
+ mcpTool: "openwearables_list_nutrition",
2740
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2741
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
2742
+ });
2743
+ }
2744
+ async getConnectionStatus(params) {
2745
+ return this.fetchConnectionStatus(params, {
2746
+ apiPath: `/v1/connections/${encodeURIComponent(params.connectionId)}/status`,
2747
+ mcpTool: "openwearables_connection_status"
2748
+ });
2749
+ }
2750
+ withUpstreamProvider(query) {
2751
+ return {
2752
+ ...query,
2753
+ ...this.upstreamProvider ? { upstreamProvider: this.upstreamProvider } : {}
2754
+ };
2755
+ }
2756
+ context(params) {
2757
+ return {
2758
+ tenantId: params.tenantId,
2759
+ connectionId: params.connectionId,
2760
+ providerKey: this.providerKey
2761
+ };
2762
+ }
2763
+ }
2333
2764
 
2334
- class GarminHealthProvider extends BaseHealthProvider {
2765
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
2335
2766
  constructor(options) {
2336
2767
  super({
2337
- providerKey: "health.garmin",
2338
- ...createProviderOptions(options, "official-api")
2768
+ ...options,
2769
+ providerKey: "health.apple-health",
2770
+ upstreamProvider: "apple-health"
2771
+ });
2772
+ }
2773
+ }
2774
+
2775
+ class WhoopHealthProvider extends BaseHealthProvider {
2776
+ constructor(options) {
2777
+ super({
2778
+ providerKey: "health.whoop",
2779
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.prod.whoop.com",
2780
+ webhookSignatureHeader: "x-whoop-signature",
2781
+ oauth: {
2782
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.prod.whoop.com/oauth/oauth2/token",
2783
+ ...options.oauth
2784
+ },
2785
+ ...options
2786
+ });
2787
+ }
2788
+ async listActivities(params) {
2789
+ return this.fetchActivities(params, {
2790
+ apiPath: "/v2/activity/workout",
2791
+ mcpTool: "whoop_list_activities",
2792
+ buildQuery: buildSharedQuery,
2793
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "workout")
2794
+ });
2795
+ }
2796
+ async listWorkouts(params) {
2797
+ return this.fetchWorkouts(params, {
2798
+ apiPath: "/v2/activity/workout",
2799
+ mcpTool: "whoop_list_workouts",
2800
+ buildQuery: buildSharedQuery,
2801
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2802
+ });
2803
+ }
2804
+ async listSleep(params) {
2805
+ return this.fetchSleep(params, {
2806
+ apiPath: "/v2/activity/sleep",
2807
+ mcpTool: "whoop_list_sleep",
2808
+ buildQuery: buildSharedQuery,
2809
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2810
+ });
2811
+ }
2812
+ async listBiometrics(params) {
2813
+ return this.fetchBiometrics(params, {
2814
+ apiPath: "/v2/recovery",
2815
+ mcpTool: "whoop_list_biometrics",
2816
+ buildQuery: withMetricTypes,
2817
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "recovery_score")
2818
+ });
2819
+ }
2820
+ async listNutrition(_params) {
2821
+ throw this.unsupported("nutrition");
2822
+ }
2823
+ async getConnectionStatus(params) {
2824
+ return this.fetchConnectionStatus(params, {
2825
+ apiPath: "/v2/user/profile/basic",
2826
+ mcpTool: "whoop_connection_status"
2827
+ });
2828
+ }
2829
+ context(params) {
2830
+ return {
2831
+ tenantId: params.tenantId,
2832
+ connectionId: params.connectionId,
2833
+ providerKey: this.providerKey
2834
+ };
2835
+ }
2836
+ }
2837
+
2838
+ class OuraHealthProvider extends BaseHealthProvider {
2839
+ constructor(options) {
2840
+ super({
2841
+ providerKey: "health.oura",
2842
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.ouraring.com",
2843
+ webhookSignatureHeader: "x-oura-signature",
2844
+ oauth: {
2845
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.ouraring.com/oauth/token",
2846
+ ...options.oauth
2847
+ },
2848
+ ...options
2849
+ });
2850
+ }
2851
+ async listActivities(params) {
2852
+ return this.fetchActivities(params, {
2853
+ apiPath: "/v2/usercollection/daily_activity",
2854
+ mcpTool: "oura_list_activities",
2855
+ buildQuery: buildSharedQuery,
2856
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2857
+ });
2858
+ }
2859
+ async listWorkouts(params) {
2860
+ return this.fetchWorkouts(params, {
2861
+ apiPath: "/v2/usercollection/workout",
2862
+ mcpTool: "oura_list_workouts",
2863
+ buildQuery: buildSharedQuery,
2864
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2865
+ });
2866
+ }
2867
+ async listSleep(params) {
2868
+ return this.fetchSleep(params, {
2869
+ apiPath: "/v2/usercollection/sleep",
2870
+ mcpTool: "oura_list_sleep",
2871
+ buildQuery: buildSharedQuery,
2872
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2873
+ });
2874
+ }
2875
+ async listBiometrics(params) {
2876
+ return this.fetchBiometrics(params, {
2877
+ apiPath: "/v2/usercollection/daily_readiness",
2878
+ mcpTool: "oura_list_biometrics",
2879
+ buildQuery: withMetricTypes,
2880
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "readiness_score")
2881
+ });
2882
+ }
2883
+ async listNutrition(_params) {
2884
+ throw this.unsupported("nutrition");
2885
+ }
2886
+ async getConnectionStatus(params) {
2887
+ return this.fetchConnectionStatus(params, {
2888
+ apiPath: "/v2/usercollection/personal_info",
2889
+ mcpTool: "oura_connection_status"
2890
+ });
2891
+ }
2892
+ context(params) {
2893
+ return {
2894
+ tenantId: params.tenantId,
2895
+ connectionId: params.connectionId,
2896
+ providerKey: this.providerKey
2897
+ };
2898
+ }
2899
+ }
2900
+
2901
+ class StravaHealthProvider extends BaseHealthProvider {
2902
+ constructor(options) {
2903
+ super({
2904
+ providerKey: "health.strava",
2905
+ apiBaseUrl: options.apiBaseUrl ?? "https://www.strava.com",
2906
+ webhookSignatureHeader: "x-strava-signature",
2907
+ oauth: {
2908
+ tokenUrl: options.oauth?.tokenUrl ?? "https://www.strava.com/oauth/token",
2909
+ ...options.oauth
2910
+ },
2911
+ ...options
2912
+ });
2913
+ }
2914
+ async listActivities(params) {
2915
+ return this.fetchActivities(params, {
2916
+ apiPath: "/api/v3/athlete/activities",
2917
+ mcpTool: "strava_list_activities",
2918
+ buildQuery: buildSharedQuery,
2919
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2920
+ });
2921
+ }
2922
+ async listWorkouts(params) {
2923
+ return this.fetchWorkouts(params, {
2924
+ apiPath: "/api/v3/athlete/activities",
2925
+ mcpTool: "strava_list_workouts",
2926
+ buildQuery: buildSharedQuery,
2927
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2928
+ });
2929
+ }
2930
+ async listSleep(_params) {
2931
+ throw this.unsupported("sleep");
2932
+ }
2933
+ async listBiometrics(_params) {
2934
+ throw this.unsupported("biometrics");
2935
+ }
2936
+ async listNutrition(_params) {
2937
+ throw this.unsupported("nutrition");
2938
+ }
2939
+ async getConnectionStatus(params) {
2940
+ return this.fetchConnectionStatus(params, {
2941
+ apiPath: "/api/v3/athlete",
2942
+ mcpTool: "strava_connection_status"
2339
2943
  });
2340
2944
  }
2945
+ context(params) {
2946
+ return {
2947
+ tenantId: params.tenantId,
2948
+ connectionId: params.connectionId,
2949
+ providerKey: this.providerKey
2950
+ };
2951
+ }
2341
2952
  }
2342
2953
 
2343
2954
  class FitbitHealthProvider extends BaseHealthProvider {
2344
2955
  constructor(options) {
2345
2956
  super({
2346
2957
  providerKey: "health.fitbit",
2347
- ...createProviderOptions(options, "official-api")
2958
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.fitbit.com",
2959
+ webhookSignatureHeader: "x-fitbit-signature",
2960
+ oauth: {
2961
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.fitbit.com/oauth2/token",
2962
+ ...options.oauth
2963
+ },
2964
+ ...options
2965
+ });
2966
+ }
2967
+ async listActivities(params) {
2968
+ return this.fetchActivities(params, {
2969
+ apiPath: "/1/user/-/activities/list.json",
2970
+ mcpTool: "fitbit_list_activities",
2971
+ buildQuery: buildSharedQuery,
2972
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2973
+ });
2974
+ }
2975
+ async listWorkouts(params) {
2976
+ return this.fetchWorkouts(params, {
2977
+ apiPath: "/1/user/-/activities/list.json",
2978
+ mcpTool: "fitbit_list_workouts",
2979
+ buildQuery: buildSharedQuery,
2980
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2981
+ });
2982
+ }
2983
+ async listSleep(params) {
2984
+ return this.fetchSleep(params, {
2985
+ apiPath: "/1.2/user/-/sleep/list.json",
2986
+ mcpTool: "fitbit_list_sleep",
2987
+ buildQuery: buildSharedQuery,
2988
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2348
2989
  });
2349
2990
  }
2991
+ async listBiometrics(params) {
2992
+ return this.fetchBiometrics(params, {
2993
+ apiPath: "/1/user/-/body/log/weight/date/today/1m.json",
2994
+ mcpTool: "fitbit_list_biometrics",
2995
+ buildQuery: withMetricTypes,
2996
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "weight")
2997
+ });
2998
+ }
2999
+ async listNutrition(params) {
3000
+ return this.fetchNutrition(params, {
3001
+ apiPath: "/1/user/-/foods/log/date/today.json",
3002
+ mcpTool: "fitbit_list_nutrition",
3003
+ buildQuery: buildSharedQuery,
3004
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
3005
+ });
3006
+ }
3007
+ async getConnectionStatus(params) {
3008
+ return this.fetchConnectionStatus(params, {
3009
+ apiPath: "/1/user/-/profile.json",
3010
+ mcpTool: "fitbit_connection_status"
3011
+ });
3012
+ }
3013
+ context(params) {
3014
+ return {
3015
+ tenantId: params.tenantId,
3016
+ connectionId: params.connectionId,
3017
+ providerKey: this.providerKey
3018
+ };
3019
+ }
2350
3020
  }
2351
3021
 
2352
- class MyFitnessPalHealthProvider extends BaseHealthProvider {
3022
+ // src/impls/health/hybrid-health-providers.ts
3023
+ var LIMITED_PROVIDER_SLUG = {
3024
+ "health.garmin": "garmin",
3025
+ "health.myfitnesspal": "myfitnesspal",
3026
+ "health.eightsleep": "eightsleep",
3027
+ "health.peloton": "peloton"
3028
+ };
3029
+ function buildSharedQuery2(params) {
3030
+ return {
3031
+ tenantId: params.tenantId,
3032
+ connectionId: params.connectionId,
3033
+ userId: params.userId,
3034
+ from: params.from,
3035
+ to: params.to,
3036
+ cursor: params.cursor,
3037
+ pageSize: params.pageSize
3038
+ };
3039
+ }
3040
+
3041
+ class GarminHealthProvider extends OpenWearablesHealthProvider {
2353
3042
  constructor(options) {
2354
3043
  super({
3044
+ ...options,
3045
+ providerKey: "health.garmin",
3046
+ upstreamProvider: "garmin"
3047
+ });
3048
+ }
3049
+ }
3050
+
3051
+ class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
3052
+ constructor(options) {
3053
+ super({
3054
+ ...options,
2355
3055
  providerKey: "health.myfitnesspal",
2356
- ...createProviderOptions(options, "official-api")
3056
+ upstreamProvider: "myfitnesspal"
2357
3057
  });
2358
3058
  }
2359
3059
  }
2360
3060
 
2361
- class EightSleepHealthProvider extends BaseHealthProvider {
3061
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
2362
3062
  constructor(options) {
2363
3063
  super({
3064
+ ...options,
2364
3065
  providerKey: "health.eightsleep",
2365
- ...createProviderOptions(options, "official-api")
3066
+ upstreamProvider: "eightsleep"
2366
3067
  });
2367
3068
  }
2368
3069
  }
2369
3070
 
2370
- class PelotonHealthProvider extends BaseHealthProvider {
3071
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
2371
3072
  constructor(options) {
2372
3073
  super({
3074
+ ...options,
2373
3075
  providerKey: "health.peloton",
2374
- ...createProviderOptions(options, "official-api")
3076
+ upstreamProvider: "peloton"
2375
3077
  });
2376
3078
  }
2377
3079
  }
2378
3080
 
2379
3081
  class UnofficialHealthAutomationProvider extends BaseHealthProvider {
3082
+ providerSlugValue;
2380
3083
  constructor(options) {
2381
3084
  super({
2382
- ...createProviderOptions(options, "unofficial"),
2383
- providerKey: options.providerKey
3085
+ ...options,
3086
+ providerKey: options.providerKey,
3087
+ webhookSignatureHeader: "x-unofficial-signature"
3088
+ });
3089
+ this.providerSlugValue = LIMITED_PROVIDER_SLUG[options.providerKey];
3090
+ }
3091
+ async listActivities(params) {
3092
+ return this.fetchActivities(params, {
3093
+ mcpTool: `${this.providerSlugValue}_list_activities`,
3094
+ buildQuery: buildSharedQuery2,
3095
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
2384
3096
  });
2385
3097
  }
3098
+ async listWorkouts(params) {
3099
+ return this.fetchWorkouts(params, {
3100
+ mcpTool: `${this.providerSlugValue}_list_workouts`,
3101
+ buildQuery: buildSharedQuery2,
3102
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3103
+ });
3104
+ }
3105
+ async listSleep(params) {
3106
+ return this.fetchSleep(params, {
3107
+ mcpTool: `${this.providerSlugValue}_list_sleep`,
3108
+ buildQuery: buildSharedQuery2,
3109
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
3110
+ });
3111
+ }
3112
+ async listBiometrics(params) {
3113
+ return this.fetchBiometrics(params, {
3114
+ mcpTool: `${this.providerSlugValue}_list_biometrics`,
3115
+ buildQuery: (input) => ({
3116
+ ...buildSharedQuery2(input),
3117
+ metricTypes: input.metricTypes
3118
+ }),
3119
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input))
3120
+ });
3121
+ }
3122
+ async listNutrition(params) {
3123
+ return this.fetchNutrition(params, {
3124
+ mcpTool: `${this.providerSlugValue}_list_nutrition`,
3125
+ buildQuery: buildSharedQuery2,
3126
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
3127
+ });
3128
+ }
3129
+ async getConnectionStatus(params) {
3130
+ return this.fetchConnectionStatus(params, {
3131
+ mcpTool: `${this.providerSlugValue}_connection_status`
3132
+ });
3133
+ }
3134
+ context(params) {
3135
+ return {
3136
+ tenantId: params.tenantId,
3137
+ connectionId: params.connectionId,
3138
+ providerKey: this.providerKey
3139
+ };
3140
+ }
2386
3141
  }
2387
-
2388
3142
  // src/impls/health-provider-factory.ts
2389
3143
  import {
2390
3144
  isUnofficialHealthProviderAllowed,
2391
3145
  resolveHealthStrategyOrder
2392
3146
  } from "@contractspec/integration.runtime/runtime";
3147
+ var OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER = {
3148
+ "health.openwearables": false,
3149
+ "health.whoop": true,
3150
+ "health.apple-health": false,
3151
+ "health.oura": true,
3152
+ "health.strava": true,
3153
+ "health.garmin": false,
3154
+ "health.fitbit": true,
3155
+ "health.myfitnesspal": false,
3156
+ "health.eightsleep": false,
3157
+ "health.peloton": false
3158
+ };
3159
+ var UNOFFICIAL_SUPPORTED_BY_PROVIDER = {
3160
+ "health.openwearables": false,
3161
+ "health.whoop": false,
3162
+ "health.apple-health": false,
3163
+ "health.oura": false,
3164
+ "health.strava": false,
3165
+ "health.garmin": true,
3166
+ "health.fitbit": false,
3167
+ "health.myfitnesspal": true,
3168
+ "health.eightsleep": true,
3169
+ "health.peloton": true
3170
+ };
2393
3171
  function createHealthProviderFromContext(context, secrets) {
2394
3172
  const providerKey = context.spec.meta.key;
2395
3173
  const config = toFactoryConfig(context.config);
2396
3174
  const strategyOrder = buildStrategyOrder(config);
2397
- const errors = [];
2398
- for (const strategy of strategyOrder) {
2399
- const provider = createHealthProviderForStrategy(providerKey, strategy, config, secrets);
3175
+ const attemptLogs = [];
3176
+ for (let index = 0;index < strategyOrder.length; index += 1) {
3177
+ const strategy = strategyOrder[index];
3178
+ if (!strategy)
3179
+ continue;
3180
+ const route = index === 0 ? "primary" : "fallback";
3181
+ if (!supportsStrategy(providerKey, strategy)) {
3182
+ attemptLogs.push(`${strategy}: unsupported by ${providerKey}`);
3183
+ continue;
3184
+ }
3185
+ if (!hasCredentialsForStrategy(strategy, config, secrets)) {
3186
+ attemptLogs.push(`${strategy}: missing credentials`);
3187
+ continue;
3188
+ }
3189
+ const provider = createHealthProviderForStrategy(providerKey, strategy, route, config, secrets);
2400
3190
  if (provider) {
2401
3191
  return provider;
2402
3192
  }
2403
- errors.push(`${strategy}: not available`);
3193
+ attemptLogs.push(`${strategy}: not available`);
2404
3194
  }
2405
- throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${errors.join(", ")}.`);
3195
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
2406
3196
  }
2407
- function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
3197
+ function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
2408
3198
  const options = {
2409
3199
  transport: strategy,
2410
3200
  apiBaseUrl: config.apiBaseUrl,
@@ -2412,10 +3202,21 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
2412
3202
  apiKey: getSecretString(secrets, "apiKey"),
2413
3203
  accessToken: getSecretString(secrets, "accessToken"),
2414
3204
  mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
2415
- webhookSecret: getSecretString(secrets, "webhookSecret")
3205
+ webhookSecret: getSecretString(secrets, "webhookSecret"),
3206
+ route,
3207
+ oauth: {
3208
+ tokenUrl: config.oauthTokenUrl,
3209
+ refreshToken: getSecretString(secrets, "refreshToken"),
3210
+ clientId: getSecretString(secrets, "clientId"),
3211
+ clientSecret: getSecretString(secrets, "clientSecret"),
3212
+ tokenExpiresAt: getSecretString(secrets, "tokenExpiresAt")
3213
+ }
2416
3214
  };
2417
3215
  if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
2418
- return new OpenWearablesHealthProvider(options);
3216
+ return createAggregatorProvider(providerKey, {
3217
+ ...options,
3218
+ aggregatorKey: "health.openwearables"
3219
+ });
2419
3220
  }
2420
3221
  if (strategy === "unofficial") {
2421
3222
  if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
@@ -2437,6 +3238,31 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
2437
3238
  }
2438
3239
  return createOfficialProvider(providerKey, options);
2439
3240
  }
3241
+ function createAggregatorProvider(providerKey, options) {
3242
+ if (providerKey === "health.apple-health") {
3243
+ return new AppleHealthBridgeProvider(options);
3244
+ }
3245
+ if (providerKey === "health.garmin") {
3246
+ return new GarminHealthProvider(options);
3247
+ }
3248
+ if (providerKey === "health.myfitnesspal") {
3249
+ return new MyFitnessPalHealthProvider(options);
3250
+ }
3251
+ if (providerKey === "health.eightsleep") {
3252
+ return new EightSleepHealthProvider(options);
3253
+ }
3254
+ if (providerKey === "health.peloton") {
3255
+ return new PelotonHealthProvider(options);
3256
+ }
3257
+ if (providerKey === "health.openwearables") {
3258
+ return new OpenWearablesHealthProvider(options);
3259
+ }
3260
+ return new OpenWearablesHealthProvider({
3261
+ ...options,
3262
+ providerKey,
3263
+ upstreamProvider: providerKey.replace("health.", "")
3264
+ });
3265
+ }
2440
3266
  function createOfficialProvider(providerKey, options) {
2441
3267
  switch (providerKey) {
2442
3268
  case "health.openwearables":
@@ -2454,11 +3280,20 @@ function createOfficialProvider(providerKey, options) {
2454
3280
  case "health.fitbit":
2455
3281
  return new FitbitHealthProvider(options);
2456
3282
  case "health.myfitnesspal":
2457
- return new MyFitnessPalHealthProvider(options);
3283
+ return new MyFitnessPalHealthProvider({
3284
+ ...options,
3285
+ transport: "aggregator-api"
3286
+ });
2458
3287
  case "health.eightsleep":
2459
- return new EightSleepHealthProvider(options);
3288
+ return new EightSleepHealthProvider({
3289
+ ...options,
3290
+ transport: "aggregator-api"
3291
+ });
2460
3292
  case "health.peloton":
2461
- return new PelotonHealthProvider(options);
3293
+ return new PelotonHealthProvider({
3294
+ ...options,
3295
+ transport: "aggregator-api"
3296
+ });
2462
3297
  default:
2463
3298
  throw new Error(`Unsupported health provider key: ${providerKey}`);
2464
3299
  }
@@ -2471,6 +3306,7 @@ function toFactoryConfig(config) {
2471
3306
  return {
2472
3307
  apiBaseUrl: asString(record.apiBaseUrl),
2473
3308
  mcpUrl: asString(record.mcpUrl),
3309
+ oauthTokenUrl: asString(record.oauthTokenUrl),
2474
3310
  defaultTransport: normalizeTransport(record.defaultTransport),
2475
3311
  strategyOrder: normalizeTransportArray(record.strategyOrder),
2476
3312
  allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
@@ -2499,6 +3335,27 @@ function normalizeTransportArray(value) {
2499
3335
  const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
2500
3336
  return transports.length > 0 ? transports : undefined;
2501
3337
  }
3338
+ function supportsStrategy(providerKey, strategy) {
3339
+ if (strategy === "official-api" || strategy === "official-mcp") {
3340
+ return OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER[providerKey];
3341
+ }
3342
+ if (strategy === "unofficial") {
3343
+ return UNOFFICIAL_SUPPORTED_BY_PROVIDER[providerKey];
3344
+ }
3345
+ return true;
3346
+ }
3347
+ function hasCredentialsForStrategy(strategy, config, secrets) {
3348
+ const hasApiCredential = Boolean(getSecretString(secrets, "accessToken")) || Boolean(getSecretString(secrets, "apiKey"));
3349
+ const hasMcpCredential = Boolean(getSecretString(secrets, "mcpAccessToken")) || hasApiCredential;
3350
+ if (strategy === "official-api" || strategy === "aggregator-api") {
3351
+ return hasApiCredential;
3352
+ }
3353
+ if (strategy === "official-mcp" || strategy === "aggregator-mcp") {
3354
+ return Boolean(config.mcpUrl) && hasMcpCredential;
3355
+ }
3356
+ const hasAutomationCredential = hasMcpCredential || Boolean(getSecretString(secrets, "username")) && Boolean(getSecretString(secrets, "password"));
3357
+ return Boolean(config.mcpUrl) && hasAutomationCredential;
3358
+ }
2502
3359
  function getSecretString(secrets, key) {
2503
3360
  const value = secrets[key];
2504
3361
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
@@ -2742,77 +3599,506 @@ class MistralLLMProvider {
2742
3599
  return null;
2743
3600
  return textParts.join("");
2744
3601
  }
2745
- extractToolCalls(message) {
2746
- const toolCallParts = message.content.filter((part) => part.type === "tool-call");
2747
- return toolCallParts.map((call, index) => ({
2748
- id: call.id ?? `call_${index}`,
2749
- type: "function",
2750
- index,
2751
- function: {
2752
- name: call.name,
2753
- arguments: call.arguments
2754
- }
2755
- }));
3602
+ extractToolCalls(message) {
3603
+ const toolCallParts = message.content.filter((part) => part.type === "tool-call");
3604
+ return toolCallParts.map((call, index) => ({
3605
+ id: call.id ?? `call_${index}`,
3606
+ type: "function",
3607
+ index,
3608
+ function: {
3609
+ name: call.name,
3610
+ arguments: call.arguments
3611
+ }
3612
+ }));
3613
+ }
3614
+ }
3615
+ function mapFinishReason(reason) {
3616
+ if (!reason)
3617
+ return;
3618
+ const normalized = reason.toLowerCase();
3619
+ switch (normalized) {
3620
+ case "stop":
3621
+ return "stop";
3622
+ case "length":
3623
+ return "length";
3624
+ case "tool_call":
3625
+ case "tool_calls":
3626
+ return "tool_call";
3627
+ case "content_filter":
3628
+ return "content_filter";
3629
+ default:
3630
+ return;
3631
+ }
3632
+ }
3633
+
3634
+ // src/impls/mistral-embedding.ts
3635
+ import { Mistral as Mistral2 } from "@mistralai/mistralai";
3636
+
3637
+ class MistralEmbeddingProvider {
3638
+ client;
3639
+ defaultModel;
3640
+ constructor(options) {
3641
+ if (!options.apiKey) {
3642
+ throw new Error("MistralEmbeddingProvider requires an apiKey");
3643
+ }
3644
+ this.client = options.client ?? new Mistral2({
3645
+ apiKey: options.apiKey,
3646
+ serverURL: options.serverURL
3647
+ });
3648
+ this.defaultModel = options.defaultModel ?? "mistral-embed";
3649
+ }
3650
+ async embedDocuments(documents, options) {
3651
+ if (documents.length === 0)
3652
+ return [];
3653
+ const model = options?.model ?? this.defaultModel;
3654
+ const response = await this.client.embeddings.create({
3655
+ model,
3656
+ inputs: documents.map((doc) => doc.text)
3657
+ });
3658
+ return response.data.map((item, index) => ({
3659
+ id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
3660
+ vector: item.embedding ?? [],
3661
+ dimensions: item.embedding?.length ?? 0,
3662
+ model: response.model,
3663
+ metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
3664
+ }));
3665
+ }
3666
+ async embedQuery(query, options) {
3667
+ const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
3668
+ if (!result) {
3669
+ throw new Error("Failed to compute embedding for query");
3670
+ }
3671
+ return result;
3672
+ }
3673
+ }
3674
+
3675
+ // src/impls/mistral-stt.ts
3676
+ var DEFAULT_BASE_URL4 = "https://api.mistral.ai/v1";
3677
+ var DEFAULT_MODEL = "voxtral-mini-latest";
3678
+ var AUDIO_MIME_BY_FORMAT = {
3679
+ mp3: "audio/mpeg",
3680
+ wav: "audio/wav",
3681
+ ogg: "audio/ogg",
3682
+ pcm: "audio/pcm",
3683
+ opus: "audio/opus"
3684
+ };
3685
+
3686
+ class MistralSttProvider {
3687
+ apiKey;
3688
+ defaultModel;
3689
+ defaultLanguage;
3690
+ baseUrl;
3691
+ fetchImpl;
3692
+ constructor(options) {
3693
+ if (!options.apiKey) {
3694
+ throw new Error("MistralSttProvider requires an apiKey");
3695
+ }
3696
+ this.apiKey = options.apiKey;
3697
+ this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
3698
+ this.defaultLanguage = options.defaultLanguage;
3699
+ this.baseUrl = normalizeBaseUrl(options.serverURL ?? DEFAULT_BASE_URL4);
3700
+ this.fetchImpl = options.fetchImpl ?? fetch;
3701
+ }
3702
+ async transcribe(input) {
3703
+ const formData = new FormData;
3704
+ const model = input.model ?? this.defaultModel;
3705
+ const mimeType = AUDIO_MIME_BY_FORMAT[input.audio.format] ?? "audio/wav";
3706
+ const fileName = `audio.${input.audio.format}`;
3707
+ const audioBytes = new Uint8Array(input.audio.data);
3708
+ const blob = new Blob([audioBytes], { type: mimeType });
3709
+ formData.append("file", blob, fileName);
3710
+ formData.append("model", model);
3711
+ formData.append("response_format", "verbose_json");
3712
+ const language = input.language ?? this.defaultLanguage;
3713
+ if (language) {
3714
+ formData.append("language", language);
3715
+ }
3716
+ const response = await this.fetchImpl(`${this.baseUrl}/audio/transcriptions`, {
3717
+ method: "POST",
3718
+ headers: {
3719
+ Authorization: `Bearer ${this.apiKey}`
3720
+ },
3721
+ body: formData
3722
+ });
3723
+ if (!response.ok) {
3724
+ const body = await response.text();
3725
+ throw new Error(`Mistral transcription request failed (${response.status}): ${body}`);
3726
+ }
3727
+ const payload = await response.json();
3728
+ return toTranscriptionResult(payload, input);
3729
+ }
3730
+ }
3731
+ function toTranscriptionResult(payload, input) {
3732
+ const record = asRecord2(payload);
3733
+ const text = readString3(record, "text") ?? "";
3734
+ const language = readString3(record, "language") ?? input.language ?? "unknown";
3735
+ const segments = parseSegments(record);
3736
+ if (segments.length === 0 && text.length > 0) {
3737
+ segments.push({
3738
+ text,
3739
+ startMs: 0,
3740
+ endMs: input.audio.durationMs ?? 0
3741
+ });
3742
+ }
3743
+ const durationMs = input.audio.durationMs ?? segments.reduce((max, segment) => Math.max(max, segment.endMs), 0);
3744
+ const topLevelWords = parseWordTimings(record.words);
3745
+ const flattenedWords = segments.flatMap((segment) => segment.wordTimings ?? []);
3746
+ const wordTimings = topLevelWords.length > 0 ? topLevelWords : flattenedWords.length > 0 ? flattenedWords : undefined;
3747
+ const speakers = dedupeSpeakers(segments);
3748
+ return {
3749
+ text,
3750
+ segments,
3751
+ language,
3752
+ durationMs,
3753
+ speakers: speakers.length > 0 ? speakers : undefined,
3754
+ wordTimings
3755
+ };
3756
+ }
3757
+ function parseSegments(record) {
3758
+ if (!Array.isArray(record.segments)) {
3759
+ return [];
3760
+ }
3761
+ const parsed = [];
3762
+ for (const entry of record.segments) {
3763
+ const segmentRecord = asRecord2(entry);
3764
+ const text = readString3(segmentRecord, "text");
3765
+ if (!text) {
3766
+ continue;
3767
+ }
3768
+ const startSeconds = readNumber2(segmentRecord, "start") ?? 0;
3769
+ const endSeconds = readNumber2(segmentRecord, "end") ?? startSeconds;
3770
+ parsed.push({
3771
+ text,
3772
+ startMs: secondsToMs(startSeconds),
3773
+ endMs: secondsToMs(endSeconds),
3774
+ speakerId: readString3(segmentRecord, "speaker") ?? undefined,
3775
+ confidence: readNumber2(segmentRecord, "confidence"),
3776
+ wordTimings: parseWordTimings(segmentRecord.words)
3777
+ });
3778
+ }
3779
+ return parsed;
3780
+ }
3781
+ function parseWordTimings(value) {
3782
+ if (!Array.isArray(value)) {
3783
+ return [];
3784
+ }
3785
+ const words = [];
3786
+ for (const entry of value) {
3787
+ const wordRecord = asRecord2(entry);
3788
+ const word = readString3(wordRecord, "word");
3789
+ const startSeconds = readNumber2(wordRecord, "start");
3790
+ const endSeconds = readNumber2(wordRecord, "end");
3791
+ if (!word || startSeconds == null || endSeconds == null) {
3792
+ continue;
3793
+ }
3794
+ words.push({
3795
+ word,
3796
+ startMs: secondsToMs(startSeconds),
3797
+ endMs: secondsToMs(endSeconds),
3798
+ confidence: readNumber2(wordRecord, "confidence")
3799
+ });
3800
+ }
3801
+ return words;
3802
+ }
3803
+ function dedupeSpeakers(segments) {
3804
+ const seen = new Set;
3805
+ const speakers = [];
3806
+ for (const segment of segments) {
3807
+ if (!segment.speakerId || seen.has(segment.speakerId)) {
3808
+ continue;
3809
+ }
3810
+ seen.add(segment.speakerId);
3811
+ speakers.push({
3812
+ id: segment.speakerId,
3813
+ name: segment.speakerName
3814
+ });
3815
+ }
3816
+ return speakers;
3817
+ }
3818
+ function normalizeBaseUrl(url) {
3819
+ return url.endsWith("/") ? url.slice(0, -1) : url;
3820
+ }
3821
+ function asRecord2(value) {
3822
+ if (value && typeof value === "object") {
3823
+ return value;
3824
+ }
3825
+ return {};
3826
+ }
3827
+ function readString3(record, key) {
3828
+ const value = record[key];
3829
+ return typeof value === "string" ? value : undefined;
3830
+ }
3831
+ function readNumber2(record, key) {
3832
+ const value = record[key];
3833
+ return typeof value === "number" ? value : undefined;
3834
+ }
3835
+ function secondsToMs(value) {
3836
+ return Math.round(value * 1000);
3837
+ }
3838
+
3839
+ // src/impls/mistral-conversational.session.ts
3840
+ class MistralConversationSession {
3841
+ events;
3842
+ queue = new AsyncEventQueue;
3843
+ turns = [];
3844
+ history = [];
3845
+ sessionId = crypto.randomUUID();
3846
+ startedAt = Date.now();
3847
+ sessionConfig;
3848
+ defaultModel;
3849
+ complete;
3850
+ sttProvider;
3851
+ pending = Promise.resolve();
3852
+ closed = false;
3853
+ closedSummary;
3854
+ constructor(options) {
3855
+ this.sessionConfig = options.sessionConfig;
3856
+ this.defaultModel = options.defaultModel;
3857
+ this.complete = options.complete;
3858
+ this.sttProvider = options.sttProvider;
3859
+ this.events = this.queue;
3860
+ this.queue.push({
3861
+ type: "session_started",
3862
+ sessionId: this.sessionId
3863
+ });
3864
+ }
3865
+ sendAudio(chunk) {
3866
+ if (this.closed) {
3867
+ return;
3868
+ }
3869
+ this.pending = this.pending.then(async () => {
3870
+ const transcription = await this.sttProvider.transcribe({
3871
+ audio: {
3872
+ data: chunk,
3873
+ format: this.sessionConfig.inputFormat ?? "pcm",
3874
+ sampleRateHz: 16000
3875
+ },
3876
+ language: this.sessionConfig.language
3877
+ });
3878
+ const transcriptText = transcription.text.trim();
3879
+ if (transcriptText.length > 0) {
3880
+ await this.handleUserText(transcriptText);
3881
+ }
3882
+ }).catch((error) => {
3883
+ this.emitError(error);
3884
+ });
3885
+ }
3886
+ sendText(text) {
3887
+ if (this.closed) {
3888
+ return;
3889
+ }
3890
+ const normalized = text.trim();
3891
+ if (normalized.length === 0) {
3892
+ return;
3893
+ }
3894
+ this.pending = this.pending.then(() => this.handleUserText(normalized)).catch((error) => {
3895
+ this.emitError(error);
3896
+ });
3897
+ }
3898
+ interrupt() {
3899
+ if (this.closed) {
3900
+ return;
3901
+ }
3902
+ this.queue.push({
3903
+ type: "error",
3904
+ error: new Error("Interrupt is not supported for non-streaming sessions.")
3905
+ });
3906
+ }
3907
+ async close() {
3908
+ if (this.closedSummary) {
3909
+ return this.closedSummary;
3910
+ }
3911
+ this.closed = true;
3912
+ await this.pending;
3913
+ const durationMs = Date.now() - this.startedAt;
3914
+ const summary = {
3915
+ sessionId: this.sessionId,
3916
+ durationMs,
3917
+ turns: this.turns.map((turn) => ({
3918
+ role: turn.role === "assistant" ? "agent" : turn.role,
3919
+ text: turn.text,
3920
+ startMs: turn.startMs,
3921
+ endMs: turn.endMs
3922
+ })),
3923
+ transcript: this.turns.map((turn) => `${turn.role}: ${turn.text}`).join(`
3924
+ `)
3925
+ };
3926
+ this.closedSummary = summary;
3927
+ this.queue.push({
3928
+ type: "session_ended",
3929
+ reason: "closed_by_client",
3930
+ durationMs
3931
+ });
3932
+ this.queue.close();
3933
+ return summary;
3934
+ }
3935
+ async handleUserText(text) {
3936
+ if (this.closed) {
3937
+ return;
3938
+ }
3939
+ const userStart = Date.now();
3940
+ this.queue.push({ type: "user_speech_started" });
3941
+ this.queue.push({ type: "user_speech_ended", transcript: text });
3942
+ this.queue.push({
3943
+ type: "transcript",
3944
+ role: "user",
3945
+ text,
3946
+ timestamp: userStart
3947
+ });
3948
+ this.turns.push({
3949
+ role: "user",
3950
+ text,
3951
+ startMs: userStart,
3952
+ endMs: Date.now()
3953
+ });
3954
+ this.history.push({ role: "user", content: text });
3955
+ const assistantStart = Date.now();
3956
+ const assistantText = await this.complete(this.history, {
3957
+ ...this.sessionConfig,
3958
+ llmModel: this.sessionConfig.llmModel ?? this.defaultModel
3959
+ });
3960
+ if (this.closed) {
3961
+ return;
3962
+ }
3963
+ const normalizedAssistantText = assistantText.trim();
3964
+ const finalAssistantText = normalizedAssistantText.length > 0 ? normalizedAssistantText : "I was unable to produce a response.";
3965
+ this.queue.push({
3966
+ type: "agent_speech_started",
3967
+ text: finalAssistantText
3968
+ });
3969
+ this.queue.push({
3970
+ type: "transcript",
3971
+ role: "agent",
3972
+ text: finalAssistantText,
3973
+ timestamp: assistantStart
3974
+ });
3975
+ this.queue.push({ type: "agent_speech_ended" });
3976
+ this.turns.push({
3977
+ role: "assistant",
3978
+ text: finalAssistantText,
3979
+ startMs: assistantStart,
3980
+ endMs: Date.now()
3981
+ });
3982
+ this.history.push({ role: "assistant", content: finalAssistantText });
3983
+ }
3984
+ emitError(error) {
3985
+ if (this.closed) {
3986
+ return;
3987
+ }
3988
+ this.queue.push({ type: "error", error: toError(error) });
2756
3989
  }
2757
3990
  }
2758
- function mapFinishReason(reason) {
2759
- if (!reason)
2760
- return;
2761
- const normalized = reason.toLowerCase();
2762
- switch (normalized) {
2763
- case "stop":
2764
- return "stop";
2765
- case "length":
2766
- return "length";
2767
- case "tool_call":
2768
- case "tool_calls":
2769
- return "tool_call";
2770
- case "content_filter":
2771
- return "content_filter";
2772
- default:
2773
- return;
3991
+ function toError(error) {
3992
+ if (error instanceof Error) {
3993
+ return error;
2774
3994
  }
3995
+ return new Error(String(error));
2775
3996
  }
2776
3997
 
2777
- // src/impls/mistral-embedding.ts
2778
- import { Mistral as Mistral2 } from "@mistralai/mistralai";
3998
+ // src/impls/mistral-conversational.ts
3999
+ var DEFAULT_BASE_URL5 = "https://api.mistral.ai/v1";
4000
+ var DEFAULT_MODEL2 = "mistral-small-latest";
4001
+ var DEFAULT_VOICE = "default";
2779
4002
 
2780
- class MistralEmbeddingProvider {
2781
- client;
4003
+ class MistralConversationalProvider {
4004
+ apiKey;
2782
4005
  defaultModel;
4006
+ defaultVoiceId;
4007
+ baseUrl;
4008
+ fetchImpl;
4009
+ sttProvider;
2783
4010
  constructor(options) {
2784
4011
  if (!options.apiKey) {
2785
- throw new Error("MistralEmbeddingProvider requires an apiKey");
4012
+ throw new Error("MistralConversationalProvider requires an apiKey");
2786
4013
  }
2787
- this.client = options.client ?? new Mistral2({
4014
+ this.apiKey = options.apiKey;
4015
+ this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
4016
+ this.defaultVoiceId = options.defaultVoiceId ?? DEFAULT_VOICE;
4017
+ this.baseUrl = normalizeBaseUrl2(options.serverURL ?? DEFAULT_BASE_URL5);
4018
+ this.fetchImpl = options.fetchImpl ?? fetch;
4019
+ this.sttProvider = options.sttProvider ?? new MistralSttProvider({
2788
4020
  apiKey: options.apiKey,
2789
- serverURL: options.serverURL
4021
+ defaultModel: options.sttOptions?.defaultModel,
4022
+ defaultLanguage: options.sttOptions?.defaultLanguage,
4023
+ serverURL: options.sttOptions?.serverURL ?? options.serverURL,
4024
+ fetchImpl: this.fetchImpl
2790
4025
  });
2791
- this.defaultModel = options.defaultModel ?? "mistral-embed";
2792
4026
  }
2793
- async embedDocuments(documents, options) {
2794
- if (documents.length === 0)
2795
- return [];
2796
- const model = options?.model ?? this.defaultModel;
2797
- const response = await this.client.embeddings.create({
2798
- model,
2799
- inputs: documents.map((doc) => doc.text)
4027
+ async startSession(config) {
4028
+ return new MistralConversationSession({
4029
+ sessionConfig: {
4030
+ ...config,
4031
+ voiceId: config.voiceId || this.defaultVoiceId
4032
+ },
4033
+ defaultModel: this.defaultModel,
4034
+ complete: (history, sessionConfig) => this.completeConversation(history, sessionConfig),
4035
+ sttProvider: this.sttProvider
2800
4036
  });
2801
- return response.data.map((item, index) => ({
2802
- id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
2803
- vector: item.embedding ?? [],
2804
- dimensions: item.embedding?.length ?? 0,
2805
- model: response.model,
2806
- metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
2807
- }));
2808
4037
  }
2809
- async embedQuery(query, options) {
2810
- const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
2811
- if (!result) {
2812
- throw new Error("Failed to compute embedding for query");
4038
+ async listVoices() {
4039
+ return [
4040
+ {
4041
+ id: this.defaultVoiceId,
4042
+ name: "Mistral Default Voice",
4043
+ description: "Default conversational voice profile.",
4044
+ capabilities: ["conversational"]
4045
+ }
4046
+ ];
4047
+ }
4048
+ async completeConversation(history, sessionConfig) {
4049
+ const model = sessionConfig.llmModel ?? this.defaultModel;
4050
+ const messages = [];
4051
+ if (sessionConfig.systemPrompt) {
4052
+ messages.push({ role: "system", content: sessionConfig.systemPrompt });
2813
4053
  }
2814
- return result;
4054
+ for (const item of history) {
4055
+ messages.push({ role: item.role, content: item.content });
4056
+ }
4057
+ const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
4058
+ method: "POST",
4059
+ headers: {
4060
+ Authorization: `Bearer ${this.apiKey}`,
4061
+ "Content-Type": "application/json"
4062
+ },
4063
+ body: JSON.stringify({
4064
+ model,
4065
+ messages
4066
+ })
4067
+ });
4068
+ if (!response.ok) {
4069
+ const body = await response.text();
4070
+ throw new Error(`Mistral conversational request failed (${response.status}): ${body}`);
4071
+ }
4072
+ const payload = await response.json();
4073
+ return readAssistantText(payload);
4074
+ }
4075
+ }
4076
+ function normalizeBaseUrl2(url) {
4077
+ return url.endsWith("/") ? url.slice(0, -1) : url;
4078
+ }
4079
+ function readAssistantText(payload) {
4080
+ const record = asRecord3(payload);
4081
+ const choices = Array.isArray(record.choices) ? record.choices : [];
4082
+ const firstChoice = asRecord3(choices[0]);
4083
+ const message = asRecord3(firstChoice.message);
4084
+ if (typeof message.content === "string") {
4085
+ return message.content;
4086
+ }
4087
+ if (Array.isArray(message.content)) {
4088
+ const textParts = message.content.map((part) => {
4089
+ const entry = asRecord3(part);
4090
+ const text = entry.text;
4091
+ return typeof text === "string" ? text : "";
4092
+ }).filter((text) => text.length > 0);
4093
+ return textParts.join("");
4094
+ }
4095
+ return "";
4096
+ }
4097
+ function asRecord3(value) {
4098
+ if (value && typeof value === "object") {
4099
+ return value;
2815
4100
  }
4101
+ return {};
2816
4102
  }
2817
4103
 
2818
4104
  // src/impls/qdrant-vector.ts
@@ -3214,7 +4500,7 @@ function distanceToScore(distance, metric) {
3214
4500
 
3215
4501
  // src/impls/stripe-payments.ts
3216
4502
  import Stripe from "stripe";
3217
- var API_VERSION = "2026-01-28.clover";
4503
+ var API_VERSION = "2026-02-25.clover";
3218
4504
 
3219
4505
  class StripePaymentsProvider {
3220
4506
  stripe;
@@ -3879,6 +5165,318 @@ function mapStatus(status) {
3879
5165
  }
3880
5166
  }
3881
5167
 
5168
+ // src/impls/messaging-slack.ts
5169
+ class SlackMessagingProvider {
5170
+ botToken;
5171
+ defaultChannelId;
5172
+ apiBaseUrl;
5173
+ constructor(options) {
5174
+ this.botToken = options.botToken;
5175
+ this.defaultChannelId = options.defaultChannelId;
5176
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://slack.com/api";
5177
+ }
5178
+ async sendMessage(input) {
5179
+ const channel = input.channelId ?? input.recipientId ?? this.defaultChannelId;
5180
+ if (!channel) {
5181
+ throw new Error("Slack sendMessage requires channelId, recipientId, or defaultChannelId.");
5182
+ }
5183
+ const payload = {
5184
+ channel,
5185
+ text: input.text,
5186
+ mrkdwn: input.markdown ?? true,
5187
+ thread_ts: input.threadId
5188
+ };
5189
+ const response = await fetch(`${this.apiBaseUrl}/chat.postMessage`, {
5190
+ method: "POST",
5191
+ headers: {
5192
+ authorization: `Bearer ${this.botToken}`,
5193
+ "content-type": "application/json"
5194
+ },
5195
+ body: JSON.stringify(payload)
5196
+ });
5197
+ const body = await response.json();
5198
+ if (!response.ok || !body.ok || !body.ts) {
5199
+ throw new Error(`Slack sendMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
5200
+ }
5201
+ return {
5202
+ id: `slack:${body.channel ?? channel}:${body.ts}`,
5203
+ providerMessageId: body.ts,
5204
+ status: "sent",
5205
+ sentAt: new Date,
5206
+ metadata: {
5207
+ channelId: body.channel ?? channel
5208
+ }
5209
+ };
5210
+ }
5211
+ async updateMessage(messageId, input) {
5212
+ const channel = input.channelId ?? this.defaultChannelId;
5213
+ if (!channel) {
5214
+ throw new Error("Slack updateMessage requires channelId or defaultChannelId.");
5215
+ }
5216
+ const response = await fetch(`${this.apiBaseUrl}/chat.update`, {
5217
+ method: "POST",
5218
+ headers: {
5219
+ authorization: `Bearer ${this.botToken}`,
5220
+ "content-type": "application/json"
5221
+ },
5222
+ body: JSON.stringify({
5223
+ channel,
5224
+ ts: messageId,
5225
+ text: input.text,
5226
+ mrkdwn: input.markdown ?? true
5227
+ })
5228
+ });
5229
+ const body = await response.json();
5230
+ if (!response.ok || !body.ok || !body.ts) {
5231
+ throw new Error(`Slack updateMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
5232
+ }
5233
+ return {
5234
+ id: `slack:${body.channel ?? channel}:${body.ts}`,
5235
+ providerMessageId: body.ts,
5236
+ status: "sent",
5237
+ sentAt: new Date,
5238
+ metadata: {
5239
+ channelId: body.channel ?? channel
5240
+ }
5241
+ };
5242
+ }
5243
+ }
5244
+
5245
+ // src/impls/messaging-github.ts
5246
+ class GithubMessagingProvider {
5247
+ token;
5248
+ defaultOwner;
5249
+ defaultRepo;
5250
+ apiBaseUrl;
5251
+ constructor(options) {
5252
+ this.token = options.token;
5253
+ this.defaultOwner = options.defaultOwner;
5254
+ this.defaultRepo = options.defaultRepo;
5255
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://api.github.com";
5256
+ }
5257
+ async sendMessage(input) {
5258
+ const target = this.resolveTarget(input);
5259
+ const response = await fetch(`${this.apiBaseUrl}/repos/${target.owner}/${target.repo}/issues/${target.issueNumber}/comments`, {
5260
+ method: "POST",
5261
+ headers: {
5262
+ authorization: `Bearer ${this.token}`,
5263
+ accept: "application/vnd.github+json",
5264
+ "content-type": "application/json"
5265
+ },
5266
+ body: JSON.stringify({ body: input.text })
5267
+ });
5268
+ const body = await response.json();
5269
+ if (!response.ok || !body.id) {
5270
+ throw new Error(`GitHub sendMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
5271
+ }
5272
+ return {
5273
+ id: String(body.id),
5274
+ providerMessageId: body.node_id,
5275
+ status: "sent",
5276
+ sentAt: new Date,
5277
+ metadata: {
5278
+ url: body.html_url ?? "",
5279
+ owner: target.owner,
5280
+ repo: target.repo,
5281
+ issueNumber: String(target.issueNumber)
5282
+ }
5283
+ };
5284
+ }
5285
+ async updateMessage(messageId, input) {
5286
+ const owner = input.metadata?.owner ?? this.defaultOwner;
5287
+ const repo = input.metadata?.repo ?? this.defaultRepo;
5288
+ if (!owner || !repo) {
5289
+ throw new Error("GitHub updateMessage requires owner and repo metadata.");
5290
+ }
5291
+ const response = await fetch(`${this.apiBaseUrl}/repos/${owner}/${repo}/issues/comments/${messageId}`, {
5292
+ method: "PATCH",
5293
+ headers: {
5294
+ authorization: `Bearer ${this.token}`,
5295
+ accept: "application/vnd.github+json",
5296
+ "content-type": "application/json"
5297
+ },
5298
+ body: JSON.stringify({ body: input.text })
5299
+ });
5300
+ const body = await response.json();
5301
+ if (!response.ok || !body.id) {
5302
+ throw new Error(`GitHub updateMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
5303
+ }
5304
+ return {
5305
+ id: String(body.id),
5306
+ providerMessageId: body.node_id,
5307
+ status: "sent",
5308
+ sentAt: new Date,
5309
+ metadata: {
5310
+ url: body.html_url ?? "",
5311
+ owner,
5312
+ repo
5313
+ }
5314
+ };
5315
+ }
5316
+ resolveTarget(input) {
5317
+ const parsedRecipient = parseRecipient(input.recipientId);
5318
+ const owner = parsedRecipient?.owner ?? this.defaultOwner;
5319
+ const repo = parsedRecipient?.repo ?? this.defaultRepo;
5320
+ const issueNumber = parsedRecipient?.issueNumber ?? parseIssueNumber(input.threadId);
5321
+ if (!owner || !repo || issueNumber == null) {
5322
+ throw new Error("GitHub sendMessage requires owner/repo and issueNumber (use recipientId like owner/repo#123 or provide defaults + threadId).");
5323
+ }
5324
+ return {
5325
+ owner,
5326
+ repo,
5327
+ issueNumber
5328
+ };
5329
+ }
5330
+ }
5331
+ function parseRecipient(value) {
5332
+ if (!value)
5333
+ return null;
5334
+ const match = value.trim().match(/^([^/]+)\/([^#]+)#(\d+)$/);
5335
+ if (!match)
5336
+ return null;
5337
+ const owner = match[1];
5338
+ const repo = match[2];
5339
+ const issueNumber = Number(match[3]);
5340
+ if (!owner || !repo || !Number.isInteger(issueNumber)) {
5341
+ return null;
5342
+ }
5343
+ return { owner, repo, issueNumber };
5344
+ }
5345
+ function parseIssueNumber(value) {
5346
+ if (!value)
5347
+ return null;
5348
+ const numeric = Number(value);
5349
+ return Number.isInteger(numeric) ? numeric : null;
5350
+ }
5351
+
5352
+ // src/impls/messaging-whatsapp-meta.ts
5353
+ class MetaWhatsappMessagingProvider {
5354
+ accessToken;
5355
+ phoneNumberId;
5356
+ apiVersion;
5357
+ constructor(options) {
5358
+ this.accessToken = options.accessToken;
5359
+ this.phoneNumberId = options.phoneNumberId;
5360
+ this.apiVersion = options.apiVersion ?? "v22.0";
5361
+ }
5362
+ async sendMessage(input) {
5363
+ const to = input.recipientId;
5364
+ if (!to) {
5365
+ throw new Error("Meta WhatsApp sendMessage requires recipientId.");
5366
+ }
5367
+ const response = await fetch(`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`, {
5368
+ method: "POST",
5369
+ headers: {
5370
+ authorization: `Bearer ${this.accessToken}`,
5371
+ "content-type": "application/json"
5372
+ },
5373
+ body: JSON.stringify({
5374
+ messaging_product: "whatsapp",
5375
+ to,
5376
+ type: "text",
5377
+ text: {
5378
+ body: input.text,
5379
+ preview_url: false
5380
+ }
5381
+ })
5382
+ });
5383
+ const body = await response.json();
5384
+ const messageId = body.messages?.[0]?.id;
5385
+ if (!response.ok || !messageId) {
5386
+ const errorCode = body.error?.code != null ? String(body.error.code) : "";
5387
+ throw new Error(`Meta WhatsApp sendMessage failed: ${body.error?.message ?? `HTTP_${response.status}`}${errorCode ? ` (${errorCode})` : ""}`);
5388
+ }
5389
+ return {
5390
+ id: messageId,
5391
+ providerMessageId: messageId,
5392
+ status: "sent",
5393
+ sentAt: new Date,
5394
+ metadata: {
5395
+ phoneNumberId: this.phoneNumberId
5396
+ }
5397
+ };
5398
+ }
5399
+ }
5400
+
5401
+ // src/impls/messaging-whatsapp-twilio.ts
5402
+ import { Buffer as Buffer4 } from "node:buffer";
5403
+
5404
+ class TwilioWhatsappMessagingProvider {
5405
+ accountSid;
5406
+ authToken;
5407
+ fromNumber;
5408
+ constructor(options) {
5409
+ this.accountSid = options.accountSid;
5410
+ this.authToken = options.authToken;
5411
+ this.fromNumber = options.fromNumber;
5412
+ }
5413
+ async sendMessage(input) {
5414
+ const to = normalizeWhatsappAddress(input.recipientId);
5415
+ const from = normalizeWhatsappAddress(input.channelId ?? this.fromNumber);
5416
+ if (!to) {
5417
+ throw new Error("Twilio WhatsApp sendMessage requires recipientId.");
5418
+ }
5419
+ if (!from) {
5420
+ throw new Error("Twilio WhatsApp sendMessage requires channelId or configured fromNumber.");
5421
+ }
5422
+ const params = new URLSearchParams;
5423
+ params.set("To", to);
5424
+ params.set("From", from);
5425
+ params.set("Body", input.text);
5426
+ const auth = Buffer4.from(`${this.accountSid}:${this.authToken}`).toString("base64");
5427
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`, {
5428
+ method: "POST",
5429
+ headers: {
5430
+ authorization: `Basic ${auth}`,
5431
+ "content-type": "application/x-www-form-urlencoded"
5432
+ },
5433
+ body: params.toString()
5434
+ });
5435
+ const body = await response.json();
5436
+ if (!response.ok || !body.sid) {
5437
+ throw new Error(`Twilio WhatsApp sendMessage failed: ${body.error_message ?? `HTTP_${response.status}`}`);
5438
+ }
5439
+ return {
5440
+ id: body.sid,
5441
+ providerMessageId: body.sid,
5442
+ status: mapTwilioStatus(body.status),
5443
+ sentAt: new Date,
5444
+ errorCode: body.error_code != null ? String(body.error_code) : undefined,
5445
+ errorMessage: body.error_message ?? undefined,
5446
+ metadata: {
5447
+ from,
5448
+ to
5449
+ }
5450
+ };
5451
+ }
5452
+ }
5453
+ function normalizeWhatsappAddress(value) {
5454
+ if (!value)
5455
+ return null;
5456
+ if (value.startsWith("whatsapp:"))
5457
+ return value;
5458
+ return `whatsapp:${value}`;
5459
+ }
5460
+ function mapTwilioStatus(status) {
5461
+ switch (status) {
5462
+ case "queued":
5463
+ case "accepted":
5464
+ case "scheduled":
5465
+ return "queued";
5466
+ case "sending":
5467
+ return "sending";
5468
+ case "delivered":
5469
+ return "delivered";
5470
+ case "failed":
5471
+ case "undelivered":
5472
+ case "canceled":
5473
+ return "failed";
5474
+ case "sent":
5475
+ default:
5476
+ return "sent";
5477
+ }
5478
+ }
5479
+
3882
5480
  // src/impls/powens-client.ts
3883
5481
  import { URL as URL2 } from "node:url";
3884
5482
  var POWENS_BASE_URL = {
@@ -4385,7 +5983,7 @@ function resolveLabelIds(defaults, tags) {
4385
5983
  }
4386
5984
 
4387
5985
  // src/impls/jira.ts
4388
- import { Buffer as Buffer4 } from "node:buffer";
5986
+ import { Buffer as Buffer5 } from "node:buffer";
4389
5987
 
4390
5988
  class JiraProjectManagementProvider {
4391
5989
  siteUrl;
@@ -4462,7 +6060,7 @@ function normalizeSiteUrl(siteUrl) {
4462
6060
  return siteUrl.replace(/\/$/, "");
4463
6061
  }
4464
6062
  function buildAuthHeader(email, apiToken) {
4465
- const token = Buffer4.from(`${email}:${apiToken}`).toString("base64");
6063
+ const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
4466
6064
  return `Basic ${token}`;
4467
6065
  }
4468
6066
  function resolveIssueType(type, defaults) {
@@ -4665,7 +6263,7 @@ function buildParagraphBlocks(text) {
4665
6263
  }
4666
6264
 
4667
6265
  // src/impls/tldv-meeting-recorder.ts
4668
- var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
6266
+ var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
4669
6267
 
4670
6268
  class TldvMeetingRecorderProvider {
4671
6269
  apiKey;
@@ -4673,7 +6271,7 @@ class TldvMeetingRecorderProvider {
4673
6271
  defaultPageSize;
4674
6272
  constructor(options) {
4675
6273
  this.apiKey = options.apiKey;
4676
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
6274
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
4677
6275
  this.defaultPageSize = options.pageSize;
4678
6276
  }
4679
6277
  async listMeetings(params) {
@@ -4808,7 +6406,7 @@ async function safeReadError4(response) {
4808
6406
  }
4809
6407
 
4810
6408
  // src/impls/provider-factory.ts
4811
- import { Buffer as Buffer5 } from "node:buffer";
6409
+ import { Buffer as Buffer6 } from "node:buffer";
4812
6410
  var SECRET_CACHE = new Map;
4813
6411
 
4814
6412
  class IntegrationProviderFactory {
@@ -4849,6 +6447,39 @@ class IntegrationProviderFactory {
4849
6447
  throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
4850
6448
  }
4851
6449
  }
6450
+ async createMessagingProvider(context) {
6451
+ const secrets = await this.loadSecrets(context);
6452
+ const config = context.config;
6453
+ switch (context.spec.meta.key) {
6454
+ case "messaging.slack":
6455
+ return new SlackMessagingProvider({
6456
+ botToken: requireSecret(secrets, "botToken", "Slack bot token is required"),
6457
+ defaultChannelId: config?.defaultChannelId,
6458
+ apiBaseUrl: config?.apiBaseUrl
6459
+ });
6460
+ case "messaging.github":
6461
+ return new GithubMessagingProvider({
6462
+ token: requireSecret(secrets, "token", "GitHub token is required"),
6463
+ defaultOwner: config?.defaultOwner,
6464
+ defaultRepo: config?.defaultRepo,
6465
+ apiBaseUrl: config?.apiBaseUrl
6466
+ });
6467
+ case "messaging.whatsapp.meta":
6468
+ return new MetaWhatsappMessagingProvider({
6469
+ accessToken: requireSecret(secrets, "accessToken", "Meta WhatsApp access token is required"),
6470
+ phoneNumberId: requireConfig(context, "phoneNumberId", "Meta WhatsApp phoneNumberId is required"),
6471
+ apiVersion: config?.apiVersion
6472
+ });
6473
+ case "messaging.whatsapp.twilio":
6474
+ return new TwilioWhatsappMessagingProvider({
6475
+ accountSid: requireSecret(secrets, "accountSid", "Twilio account SID is required"),
6476
+ authToken: requireSecret(secrets, "authToken", "Twilio auth token is required"),
6477
+ fromNumber: config?.fromNumber
6478
+ });
6479
+ default:
6480
+ throw new Error(`Unsupported messaging integration: ${context.spec.meta.key}`);
6481
+ }
6482
+ }
4852
6483
  async createVectorStoreProvider(context) {
4853
6484
  const secrets = await this.loadSecrets(context);
4854
6485
  const config = context.config;
@@ -4947,6 +6578,41 @@ class IntegrationProviderFactory {
4947
6578
  throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
4948
6579
  }
4949
6580
  }
6581
+ async createSttProvider(context) {
6582
+ const secrets = await this.loadSecrets(context);
6583
+ const config = context.config;
6584
+ switch (context.spec.meta.key) {
6585
+ case "ai-voice-stt.mistral":
6586
+ return new MistralSttProvider({
6587
+ apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
6588
+ defaultModel: config?.model,
6589
+ defaultLanguage: config?.language,
6590
+ serverURL: config?.serverURL
6591
+ });
6592
+ default:
6593
+ throw new Error(`Unsupported STT integration: ${context.spec.meta.key}`);
6594
+ }
6595
+ }
6596
+ async createConversationalProvider(context) {
6597
+ const secrets = await this.loadSecrets(context);
6598
+ const config = context.config;
6599
+ switch (context.spec.meta.key) {
6600
+ case "ai-voice-conv.mistral":
6601
+ return new MistralConversationalProvider({
6602
+ apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
6603
+ defaultModel: config?.model,
6604
+ defaultVoiceId: config?.defaultVoice,
6605
+ serverURL: config?.serverURL,
6606
+ sttOptions: {
6607
+ defaultModel: config?.model,
6608
+ defaultLanguage: config?.language,
6609
+ serverURL: config?.serverURL
6610
+ }
6611
+ });
6612
+ default:
6613
+ throw new Error(`Unsupported conversational integration: ${context.spec.meta.key}`);
6614
+ }
6615
+ }
4950
6616
  async createProjectManagementProvider(context) {
4951
6617
  const secrets = await this.loadSecrets(context);
4952
6618
  const config = context.config;
@@ -5097,7 +6763,7 @@ class IntegrationProviderFactory {
5097
6763
  }
5098
6764
  }
5099
6765
  function parseSecret(secret) {
5100
- const text = Buffer5.from(secret.data).toString("utf-8").trim();
6766
+ const text = Buffer6.from(secret.data).toString("utf-8").trim();
5101
6767
  if (!text)
5102
6768
  return {};
5103
6769
  try {
@@ -5132,12 +6798,14 @@ export {
5132
6798
  createHealthProviderFromContext,
5133
6799
  WhoopHealthProvider,
5134
6800
  UnofficialHealthAutomationProvider,
6801
+ TwilioWhatsappMessagingProvider,
5135
6802
  TwilioSmsProvider,
5136
6803
  TldvMeetingRecorderProvider,
5137
6804
  SupabaseVectorProvider,
5138
6805
  SupabasePostgresProvider,
5139
6806
  StripePaymentsProvider,
5140
6807
  StravaHealthProvider,
6808
+ SlackMessagingProvider,
5141
6809
  QdrantVectorProvider,
5142
6810
  PowensOpenBankingProvider,
5143
6811
  PowensClientError,
@@ -5150,8 +6818,11 @@ export {
5150
6818
  OpenWearablesHealthProvider,
5151
6819
  NotionProjectManagementProvider,
5152
6820
  MyFitnessPalHealthProvider,
6821
+ MistralSttProvider,
5153
6822
  MistralLLMProvider,
5154
6823
  MistralEmbeddingProvider,
6824
+ MistralConversationalProvider,
6825
+ MetaWhatsappMessagingProvider,
5155
6826
  LinearProjectManagementProvider,
5156
6827
  JiraProjectManagementProvider,
5157
6828
  IntegrationProviderFactory,
@@ -5161,6 +6832,7 @@ export {
5161
6832
  GoogleCalendarProvider,
5162
6833
  GmailOutboundProvider,
5163
6834
  GmailInboundProvider,
6835
+ GithubMessagingProvider,
5164
6836
  GarminHealthProvider,
5165
6837
  FitbitHealthProvider,
5166
6838
  FirefliesMeetingRecorderProvider,