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