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