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