@contractspec/integration.providers-impls 2.9.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 (61) hide show
  1. package/README.md +59 -0
  2. package/dist/health.d.ts +1 -0
  3. package/dist/health.js +3 -0
  4. package/dist/impls/async-event-queue.d.ts +8 -0
  5. package/dist/impls/async-event-queue.js +47 -0
  6. package/dist/impls/health/base-health-provider.d.ts +98 -0
  7. package/dist/impls/health/base-health-provider.js +616 -0
  8. package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
  9. package/dist/impls/health/hybrid-health-providers.js +1088 -0
  10. package/dist/impls/health/official-health-providers.d.ts +78 -0
  11. package/dist/impls/health/official-health-providers.js +968 -0
  12. package/dist/impls/health/provider-normalizers.d.ts +28 -0
  13. package/dist/impls/health/provider-normalizers.js +287 -0
  14. package/dist/impls/health/providers.d.ts +2 -0
  15. package/dist/impls/health/providers.js +1094 -0
  16. package/dist/impls/health-provider-factory.d.ts +3 -0
  17. package/dist/impls/health-provider-factory.js +1308 -0
  18. package/dist/impls/index.d.ts +8 -0
  19. package/dist/impls/index.js +2356 -176
  20. package/dist/impls/messaging-github.d.ts +17 -0
  21. package/dist/impls/messaging-github.js +110 -0
  22. package/dist/impls/messaging-slack.d.ts +14 -0
  23. package/dist/impls/messaging-slack.js +80 -0
  24. package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
  25. package/dist/impls/messaging-whatsapp-meta.js +52 -0
  26. package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
  27. package/dist/impls/messaging-whatsapp-twilio.js +82 -0
  28. package/dist/impls/mistral-conversational.d.ts +23 -0
  29. package/dist/impls/mistral-conversational.js +476 -0
  30. package/dist/impls/mistral-conversational.session.d.ts +32 -0
  31. package/dist/impls/mistral-conversational.session.js +206 -0
  32. package/dist/impls/mistral-stt.d.ts +17 -0
  33. package/dist/impls/mistral-stt.js +167 -0
  34. package/dist/impls/provider-factory.d.ts +7 -1
  35. package/dist/impls/provider-factory.js +2338 -176
  36. package/dist/impls/stripe-payments.js +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +2360 -174
  39. package/dist/messaging.d.ts +1 -0
  40. package/dist/messaging.js +3 -0
  41. package/dist/node/health.js +2 -0
  42. package/dist/node/impls/async-event-queue.js +46 -0
  43. package/dist/node/impls/health/base-health-provider.js +615 -0
  44. package/dist/node/impls/health/hybrid-health-providers.js +1087 -0
  45. package/dist/node/impls/health/official-health-providers.js +967 -0
  46. package/dist/node/impls/health/provider-normalizers.js +286 -0
  47. package/dist/node/impls/health/providers.js +1093 -0
  48. package/dist/node/impls/health-provider-factory.js +1307 -0
  49. package/dist/node/impls/index.js +2356 -176
  50. package/dist/node/impls/messaging-github.js +109 -0
  51. package/dist/node/impls/messaging-slack.js +79 -0
  52. package/dist/node/impls/messaging-whatsapp-meta.js +51 -0
  53. package/dist/node/impls/messaging-whatsapp-twilio.js +81 -0
  54. package/dist/node/impls/mistral-conversational.js +475 -0
  55. package/dist/node/impls/mistral-conversational.session.js +205 -0
  56. package/dist/node/impls/mistral-stt.js +166 -0
  57. package/dist/node/impls/provider-factory.js +2338 -176
  58. package/dist/node/impls/stripe-payments.js +1 -1
  59. package/dist/node/index.js +2360 -174
  60. package/dist/node/messaging.js +2 -0
  61. package/package.json +204 -12
@@ -13,6 +13,53 @@ export * from "@contractspec/lib.contracts-integrations";
13
13
  // src/embedding.ts
14
14
  export * from "@contractspec/lib.contracts-integrations";
15
15
 
16
+ // src/health.ts
17
+ export * from "@contractspec/lib.contracts-integrations";
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
+
16
63
  // src/impls/elevenlabs-voice.ts
17
64
  import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
18
65
  var FORMAT_MAP = {
@@ -2030,128 +2077,1433 @@ async function safeReadError3(response) {
2030
2077
  }
2031
2078
  }
2032
2079
 
2033
- // src/impls/mistral-llm.ts
2034
- import { Mistral } from "@mistralai/mistralai";
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
+ }
2035
2350
 
2036
- class MistralLLMProvider {
2037
- client;
2038
- defaultModel;
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
+
2360
+ class BaseHealthProvider {
2361
+ providerKey;
2362
+ transport;
2363
+ apiBaseUrl;
2364
+ mcpUrl;
2365
+ apiKey;
2366
+ accessToken;
2367
+ refreshToken;
2368
+ mcpAccessToken;
2369
+ webhookSecret;
2370
+ webhookSignatureHeader;
2371
+ route;
2372
+ aggregatorKey;
2373
+ oauth;
2374
+ fetchFn;
2375
+ mcpRequestId = 0;
2039
2376
  constructor(options) {
2040
- if (!options.apiKey) {
2041
- throw new Error("MistralLLMProvider requires an apiKey");
2042
- }
2043
- this.client = options.client ?? new Mistral({
2044
- apiKey: options.apiKey,
2045
- serverURL: options.serverURL,
2046
- userAgent: options.userAgentSuffix ? `${options.userAgentSuffix}` : undefined
2377
+ this.providerKey = options.providerKey;
2378
+ this.transport = options.transport;
2379
+ this.apiBaseUrl = options.apiBaseUrl;
2380
+ this.mcpUrl = options.mcpUrl;
2381
+ this.apiKey = options.apiKey;
2382
+ this.accessToken = options.accessToken;
2383
+ this.refreshToken = options.oauth?.refreshToken;
2384
+ this.mcpAccessToken = options.mcpAccessToken;
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 ?? {};
2390
+ this.fetchFn = options.fetchFn ?? fetch;
2391
+ }
2392
+ async listActivities(_params) {
2393
+ throw this.unsupported("activities");
2394
+ }
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`
2047
2410
  });
2048
- this.defaultModel = options.defaultModel ?? "mistral-large-latest";
2049
2411
  }
2050
- async chat(messages, options = {}) {
2051
- const request = this.buildChatRequest(messages, options);
2052
- const response = await this.client.chat.complete(request);
2053
- return this.buildLLMResponse(response);
2412
+ async syncActivities(params) {
2413
+ return this.syncFromList(() => this.listActivities(params));
2054
2414
  }
2055
- async* stream(messages, options = {}) {
2056
- const request = this.buildChatRequest(messages, options);
2057
- request.stream = true;
2058
- const stream = await this.client.chat.stream(request);
2059
- const aggregatedParts = [];
2060
- const aggregatedToolCalls = [];
2061
- let usage;
2062
- let finishReason;
2063
- for await (const event of stream) {
2064
- for (const choice of event.data.choices) {
2065
- const delta = choice.delta;
2066
- if (typeof delta.content === "string") {
2067
- if (delta.content.length > 0) {
2068
- aggregatedParts.push({ type: "text", text: delta.content });
2069
- yield {
2070
- type: "message_delta",
2071
- delta: { type: "text", text: delta.content },
2072
- index: choice.index
2073
- };
2074
- }
2075
- } else if (Array.isArray(delta.content)) {
2076
- for (const chunk of delta.content) {
2077
- if (chunk.type === "text" && "text" in chunk) {
2078
- aggregatedParts.push({ type: "text", text: chunk.text });
2079
- yield {
2080
- type: "message_delta",
2081
- delta: { type: "text", text: chunk.text },
2082
- index: choice.index
2083
- };
2084
- }
2085
- }
2086
- }
2087
- if (delta.toolCalls) {
2088
- let localIndex = 0;
2089
- for (const call of delta.toolCalls) {
2090
- const toolCall = this.fromMistralToolCall(call, localIndex);
2091
- aggregatedToolCalls.push(toolCall);
2092
- yield {
2093
- type: "tool_call",
2094
- call: toolCall,
2095
- index: choice.index
2096
- };
2097
- localIndex += 1;
2098
- }
2099
- }
2100
- if (choice.finishReason && choice.finishReason !== "null") {
2101
- finishReason = choice.finishReason;
2102
- }
2103
- }
2104
- if (event.data.usage) {
2105
- const usageEntry = this.fromUsage(event.data.usage);
2106
- if (usageEntry) {
2107
- usage = usageEntry;
2108
- yield { type: "usage", usage: usageEntry };
2109
- }
2110
- }
2111
- }
2112
- const message = {
2113
- role: "assistant",
2114
- content: aggregatedParts.length ? aggregatedParts : [{ type: "text", text: "" }]
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);
2440
+ return {
2441
+ activities: response.items,
2442
+ nextCursor: response.nextCursor,
2443
+ hasMore: response.hasMore,
2444
+ source: this.currentSource()
2115
2445
  };
2116
- if (aggregatedToolCalls.length > 0) {
2117
- message.content = [
2118
- ...aggregatedToolCalls,
2119
- ...aggregatedParts.length ? aggregatedParts : []
2120
- ];
2121
- }
2122
- yield {
2123
- type: "end",
2124
- response: {
2125
- message,
2126
- usage,
2127
- finishReason: mapFinishReason(finishReason)
2128
- }
2446
+ }
2447
+ async fetchWorkouts(params, config) {
2448
+ const response = await this.fetchList(params, config);
2449
+ return {
2450
+ workouts: response.items,
2451
+ nextCursor: response.nextCursor,
2452
+ hasMore: response.hasMore,
2453
+ source: this.currentSource()
2129
2454
  };
2130
2455
  }
2131
- async countTokens(_messages) {
2132
- throw new Error("Mistral API does not currently support token counting");
2456
+ async fetchSleep(params, config) {
2457
+ const response = await this.fetchList(params, config);
2458
+ return {
2459
+ sleep: response.items,
2460
+ nextCursor: response.nextCursor,
2461
+ hasMore: response.hasMore,
2462
+ source: this.currentSource()
2463
+ };
2133
2464
  }
2134
- buildChatRequest(messages, options) {
2135
- const model = options.model ?? this.defaultModel;
2136
- const mappedMessages = messages.map((message) => this.toMistralMessage(message));
2137
- const request = {
2138
- model,
2139
- messages: mappedMessages
2465
+ async fetchBiometrics(params, config) {
2466
+ const response = await this.fetchList(params, config);
2467
+ return {
2468
+ biometrics: response.items,
2469
+ nextCursor: response.nextCursor,
2470
+ hasMore: response.hasMore,
2471
+ source: this.currentSource()
2140
2472
  };
2141
- if (options.temperature != null) {
2142
- request.temperature = options.temperature;
2143
- }
2144
- if (options.topP != null) {
2145
- request.topP = options.topP;
2473
+ }
2474
+ async fetchNutrition(params, config) {
2475
+ const response = await this.fetchList(params, config);
2476
+ return {
2477
+ nutrition: response.items,
2478
+ nextCursor: response.nextCursor,
2479
+ hasMore: response.hasMore,
2480
+ source: this.currentSource()
2481
+ };
2482
+ }
2483
+ async fetchConnectionStatus(params, config) {
2484
+ const payload = await this.fetchPayload(config, params);
2485
+ return toHealthConnectionStatus(payload, params, this.currentSource());
2486
+ }
2487
+ currentSource() {
2488
+ return {
2489
+ providerKey: this.providerKey,
2490
+ transport: this.transport,
2491
+ route: this.route,
2492
+ aggregatorKey: this.aggregatorKey
2493
+ };
2494
+ }
2495
+ providerSlug() {
2496
+ return this.providerKey.replace("health.", "").replace(/-/g, "_");
2497
+ }
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);
2504
+ return {
2505
+ synced: records,
2506
+ failed: 0,
2507
+ nextCursor: undefined,
2508
+ source: result.source
2509
+ };
2510
+ }
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);
2515
+ return {
2516
+ items,
2517
+ nextCursor: pagination.nextCursor,
2518
+ hasMore: pagination.hasMore
2519
+ };
2520
+ }
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
+ });
2146
2530
  }
2147
- if (options.maxOutputTokens != null) {
2148
- request.maxTokens = options.maxOutputTokens;
2531
+ if (!config.apiPath || !this.apiBaseUrl) {
2532
+ throw new Error(`${this.providerKey} transport is missing an API path.`);
2149
2533
  }
2150
- if (options.stopSequences?.length) {
2151
- request.stop = options.stopSequences.length === 1 ? options.stopSequences[0] : options.stopSequences;
2534
+ if (method === "POST") {
2535
+ return this.requestApi(config.apiPath, "POST", undefined, body);
2152
2536
  }
2153
- if (options.tools?.length) {
2154
- request.tools = options.tools.map((tool) => ({
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)) {
2546
+ if (value == null)
2547
+ continue;
2548
+ if (Array.isArray(value)) {
2549
+ value.forEach((entry) => {
2550
+ if (entry != null)
2551
+ url.searchParams.append(key, String(entry));
2552
+ });
2553
+ continue;
2554
+ }
2555
+ url.searchParams.set(key, String(value));
2556
+ }
2557
+ }
2558
+ const response = await this.fetchFn(url, {
2559
+ method,
2560
+ headers: this.authorizationHeaders(),
2561
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
2562
+ });
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);
2570
+ }
2571
+ return this.readResponsePayload(response, path);
2572
+ }
2573
+ async callMcpTool(toolName, args) {
2574
+ if (!this.mcpUrl) {
2575
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
2576
+ }
2577
+ const response = await this.fetchFn(this.mcpUrl, {
2578
+ method: "POST",
2579
+ headers: {
2580
+ "Content-Type": "application/json",
2581
+ ...this.mcpAccessToken ? { Authorization: `Bearer ${this.mcpAccessToken}` } : {}
2582
+ },
2583
+ body: JSON.stringify({
2584
+ jsonrpc: "2.0",
2585
+ id: ++this.mcpRequestId,
2586
+ method: "tools/call",
2587
+ params: {
2588
+ name: toolName,
2589
+ arguments: args
2590
+ }
2591
+ })
2592
+ });
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;
2605
+ return {
2606
+ "Content-Type": "application/json",
2607
+ ...token ? { Authorization: `Bearer ${token}` } : {}
2608
+ };
2609
+ }
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();
2648
+ }
2649
+ }
2650
+ function readHeader(headers, key) {
2651
+ const target = key.toLowerCase();
2652
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
2653
+ if (!entry)
2654
+ return;
2655
+ const value = entry[1];
2656
+ return Array.isArray(value) ? value[0] : value;
2657
+ }
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
+ }
2671
+ }
2672
+ return 0;
2673
+ }
2674
+ function ensureTrailingSlash(value) {
2675
+ return value.endsWith("/") ? value : `${value}/`;
2676
+ }
2677
+ function safeJsonParse(raw) {
2678
+ try {
2679
+ return JSON.parse(raw);
2680
+ } catch {
2681
+ return { rawBody: raw };
2682
+ }
2683
+ }
2684
+ async function safeReadText2(response) {
2685
+ try {
2686
+ return await response.text();
2687
+ } catch {
2688
+ return response.statusText;
2689
+ }
2690
+ }
2691
+
2692
+ // src/impls/health/official-health-providers.ts
2693
+ function buildSharedQuery(params) {
2694
+ return {
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
2708
+ };
2709
+ }
2710
+
2711
+ class OpenWearablesHealthProvider extends BaseHealthProvider {
2712
+ upstreamProvider;
2713
+ constructor(options) {
2714
+ super({
2715
+ providerKey: options.providerKey ?? "health.openwearables",
2716
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
2717
+ webhookSignatureHeader: "x-openwearables-signature",
2718
+ ...options
2719
+ });
2720
+ this.upstreamProvider = options.upstreamProvider;
2721
+ }
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")
2728
+ });
2729
+ }
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))
2736
+ });
2737
+ }
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))
2744
+ });
2745
+ }
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))
2752
+ });
2753
+ }
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
+ }
2782
+
2783
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
2784
+ constructor(options) {
2785
+ super({
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")
2899
+ });
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
+ }
2970
+ }
2971
+
2972
+ class FitbitHealthProvider extends BaseHealthProvider {
2973
+ constructor(options) {
2974
+ super({
2975
+ providerKey: "health.fitbit",
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"
3029
+ });
3030
+ }
3031
+ context(params) {
3032
+ return {
3033
+ tenantId: params.tenantId,
3034
+ connectionId: params.connectionId,
3035
+ providerKey: this.providerKey
3036
+ };
3037
+ }
3038
+ }
3039
+
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 {
3060
+ constructor(options) {
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,
3073
+ providerKey: "health.myfitnesspal",
3074
+ upstreamProvider: "myfitnesspal"
3075
+ });
3076
+ }
3077
+ }
3078
+
3079
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
3080
+ constructor(options) {
3081
+ super({
3082
+ ...options,
3083
+ providerKey: "health.eightsleep",
3084
+ upstreamProvider: "eightsleep"
3085
+ });
3086
+ }
3087
+ }
3088
+
3089
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
3090
+ constructor(options) {
3091
+ super({
3092
+ ...options,
3093
+ providerKey: "health.peloton",
3094
+ upstreamProvider: "peloton"
3095
+ });
3096
+ }
3097
+ }
3098
+
3099
+ class UnofficialHealthAutomationProvider extends BaseHealthProvider {
3100
+ providerSlugValue;
3101
+ constructor(options) {
3102
+ super({
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))
3145
+ });
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
+ }
3159
+ }
3160
+ // src/impls/health-provider-factory.ts
3161
+ import {
3162
+ isUnofficialHealthProviderAllowed,
3163
+ resolveHealthStrategyOrder
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
+ };
3189
+ function createHealthProviderFromContext(context, secrets) {
3190
+ const providerKey = context.spec.meta.key;
3191
+ const config = toFactoryConfig(context.config);
3192
+ const strategyOrder = buildStrategyOrder(config);
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);
3208
+ if (provider) {
3209
+ return provider;
3210
+ }
3211
+ attemptLogs.push(`${strategy}: not available`);
3212
+ }
3213
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
3214
+ }
3215
+ function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
3216
+ const options = {
3217
+ transport: strategy,
3218
+ apiBaseUrl: config.apiBaseUrl,
3219
+ mcpUrl: config.mcpUrl,
3220
+ apiKey: getSecretString(secrets, "apiKey"),
3221
+ accessToken: getSecretString(secrets, "accessToken"),
3222
+ mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
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
+ }
3232
+ };
3233
+ if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
3234
+ return createAggregatorProvider(providerKey, {
3235
+ ...options,
3236
+ aggregatorKey: "health.openwearables"
3237
+ });
3238
+ }
3239
+ if (strategy === "unofficial") {
3240
+ if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
3241
+ return;
3242
+ }
3243
+ if (providerKey !== "health.myfitnesspal" && providerKey !== "health.eightsleep" && providerKey !== "health.peloton" && providerKey !== "health.garmin") {
3244
+ return;
3245
+ }
3246
+ return new UnofficialHealthAutomationProvider({
3247
+ ...options,
3248
+ providerKey
3249
+ });
3250
+ }
3251
+ if (strategy === "official-mcp") {
3252
+ return createOfficialProvider(providerKey, {
3253
+ ...options,
3254
+ transport: "official-mcp"
3255
+ });
3256
+ }
3257
+ return createOfficialProvider(providerKey, options);
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
+ }
3284
+ function createOfficialProvider(providerKey, options) {
3285
+ switch (providerKey) {
3286
+ case "health.openwearables":
3287
+ return new OpenWearablesHealthProvider(options);
3288
+ case "health.whoop":
3289
+ return new WhoopHealthProvider(options);
3290
+ case "health.apple-health":
3291
+ return new AppleHealthBridgeProvider(options);
3292
+ case "health.oura":
3293
+ return new OuraHealthProvider(options);
3294
+ case "health.strava":
3295
+ return new StravaHealthProvider(options);
3296
+ case "health.garmin":
3297
+ return new GarminHealthProvider(options);
3298
+ case "health.fitbit":
3299
+ return new FitbitHealthProvider(options);
3300
+ case "health.myfitnesspal":
3301
+ return new MyFitnessPalHealthProvider({
3302
+ ...options,
3303
+ transport: "aggregator-api"
3304
+ });
3305
+ case "health.eightsleep":
3306
+ return new EightSleepHealthProvider({
3307
+ ...options,
3308
+ transport: "aggregator-api"
3309
+ });
3310
+ case "health.peloton":
3311
+ return new PelotonHealthProvider({
3312
+ ...options,
3313
+ transport: "aggregator-api"
3314
+ });
3315
+ default:
3316
+ throw new Error(`Unsupported health provider key: ${providerKey}`);
3317
+ }
3318
+ }
3319
+ function toFactoryConfig(config) {
3320
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
3321
+ return {};
3322
+ }
3323
+ const record = config;
3324
+ return {
3325
+ apiBaseUrl: asString(record.apiBaseUrl),
3326
+ mcpUrl: asString(record.mcpUrl),
3327
+ oauthTokenUrl: asString(record.oauthTokenUrl),
3328
+ defaultTransport: normalizeTransport(record.defaultTransport),
3329
+ strategyOrder: normalizeTransportArray(record.strategyOrder),
3330
+ allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
3331
+ unofficialAllowList: Array.isArray(record.unofficialAllowList) ? record.unofficialAllowList.map((item) => typeof item === "string" ? item : undefined).filter((item) => Boolean(item)) : undefined
3332
+ };
3333
+ }
3334
+ function buildStrategyOrder(config) {
3335
+ const order = resolveHealthStrategyOrder(config);
3336
+ if (!config.defaultTransport) {
3337
+ return order;
3338
+ }
3339
+ const withoutDefault = order.filter((item) => item !== config.defaultTransport);
3340
+ return [config.defaultTransport, ...withoutDefault];
3341
+ }
3342
+ function normalizeTransport(value) {
3343
+ if (typeof value !== "string")
3344
+ return;
3345
+ if (value === "official-api" || value === "official-mcp" || value === "aggregator-api" || value === "aggregator-mcp" || value === "unofficial") {
3346
+ return value;
3347
+ }
3348
+ return;
3349
+ }
3350
+ function normalizeTransportArray(value) {
3351
+ if (!Array.isArray(value))
3352
+ return;
3353
+ const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
3354
+ return transports.length > 0 ? transports : undefined;
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
+ }
3377
+ function getSecretString(secrets, key) {
3378
+ const value = secrets[key];
3379
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
3380
+ }
3381
+ function asString(value) {
3382
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
3383
+ }
3384
+
3385
+ // src/impls/mistral-llm.ts
3386
+ import { Mistral } from "@mistralai/mistralai";
3387
+
3388
+ class MistralLLMProvider {
3389
+ client;
3390
+ defaultModel;
3391
+ constructor(options) {
3392
+ if (!options.apiKey) {
3393
+ throw new Error("MistralLLMProvider requires an apiKey");
3394
+ }
3395
+ this.client = options.client ?? new Mistral({
3396
+ apiKey: options.apiKey,
3397
+ serverURL: options.serverURL,
3398
+ userAgent: options.userAgentSuffix ? `${options.userAgentSuffix}` : undefined
3399
+ });
3400
+ this.defaultModel = options.defaultModel ?? "mistral-large-latest";
3401
+ }
3402
+ async chat(messages, options = {}) {
3403
+ const request = this.buildChatRequest(messages, options);
3404
+ const response = await this.client.chat.complete(request);
3405
+ return this.buildLLMResponse(response);
3406
+ }
3407
+ async* stream(messages, options = {}) {
3408
+ const request = this.buildChatRequest(messages, options);
3409
+ request.stream = true;
3410
+ const stream = await this.client.chat.stream(request);
3411
+ const aggregatedParts = [];
3412
+ const aggregatedToolCalls = [];
3413
+ let usage;
3414
+ let finishReason;
3415
+ for await (const event of stream) {
3416
+ for (const choice of event.data.choices) {
3417
+ const delta = choice.delta;
3418
+ if (typeof delta.content === "string") {
3419
+ if (delta.content.length > 0) {
3420
+ aggregatedParts.push({ type: "text", text: delta.content });
3421
+ yield {
3422
+ type: "message_delta",
3423
+ delta: { type: "text", text: delta.content },
3424
+ index: choice.index
3425
+ };
3426
+ }
3427
+ } else if (Array.isArray(delta.content)) {
3428
+ for (const chunk of delta.content) {
3429
+ if (chunk.type === "text" && "text" in chunk) {
3430
+ aggregatedParts.push({ type: "text", text: chunk.text });
3431
+ yield {
3432
+ type: "message_delta",
3433
+ delta: { type: "text", text: chunk.text },
3434
+ index: choice.index
3435
+ };
3436
+ }
3437
+ }
3438
+ }
3439
+ if (delta.toolCalls) {
3440
+ let localIndex = 0;
3441
+ for (const call of delta.toolCalls) {
3442
+ const toolCall = this.fromMistralToolCall(call, localIndex);
3443
+ aggregatedToolCalls.push(toolCall);
3444
+ yield {
3445
+ type: "tool_call",
3446
+ call: toolCall,
3447
+ index: choice.index
3448
+ };
3449
+ localIndex += 1;
3450
+ }
3451
+ }
3452
+ if (choice.finishReason && choice.finishReason !== "null") {
3453
+ finishReason = choice.finishReason;
3454
+ }
3455
+ }
3456
+ if (event.data.usage) {
3457
+ const usageEntry = this.fromUsage(event.data.usage);
3458
+ if (usageEntry) {
3459
+ usage = usageEntry;
3460
+ yield { type: "usage", usage: usageEntry };
3461
+ }
3462
+ }
3463
+ }
3464
+ const message = {
3465
+ role: "assistant",
3466
+ content: aggregatedParts.length ? aggregatedParts : [{ type: "text", text: "" }]
3467
+ };
3468
+ if (aggregatedToolCalls.length > 0) {
3469
+ message.content = [
3470
+ ...aggregatedToolCalls,
3471
+ ...aggregatedParts.length ? aggregatedParts : []
3472
+ ];
3473
+ }
3474
+ yield {
3475
+ type: "end",
3476
+ response: {
3477
+ message,
3478
+ usage,
3479
+ finishReason: mapFinishReason(finishReason)
3480
+ }
3481
+ };
3482
+ }
3483
+ async countTokens(_messages) {
3484
+ throw new Error("Mistral API does not currently support token counting");
3485
+ }
3486
+ buildChatRequest(messages, options) {
3487
+ const model = options.model ?? this.defaultModel;
3488
+ const mappedMessages = messages.map((message) => this.toMistralMessage(message));
3489
+ const request = {
3490
+ model,
3491
+ messages: mappedMessages
3492
+ };
3493
+ if (options.temperature != null) {
3494
+ request.temperature = options.temperature;
3495
+ }
3496
+ if (options.topP != null) {
3497
+ request.topP = options.topP;
3498
+ }
3499
+ if (options.maxOutputTokens != null) {
3500
+ request.maxTokens = options.maxOutputTokens;
3501
+ }
3502
+ if (options.stopSequences?.length) {
3503
+ request.stop = options.stopSequences.length === 1 ? options.stopSequences[0] : options.stopSequences;
3504
+ }
3505
+ if (options.tools?.length) {
3506
+ request.tools = options.tools.map((tool) => ({
2155
3507
  type: "function",
2156
3508
  function: {
2157
3509
  name: tool.name,
@@ -2265,77 +3617,506 @@ class MistralLLMProvider {
2265
3617
  return null;
2266
3618
  return textParts.join("");
2267
3619
  }
2268
- extractToolCalls(message) {
2269
- const toolCallParts = message.content.filter((part) => part.type === "tool-call");
2270
- return toolCallParts.map((call, index) => ({
2271
- id: call.id ?? `call_${index}`,
2272
- type: "function",
2273
- index,
2274
- function: {
2275
- name: call.name,
2276
- arguments: call.arguments
2277
- }
2278
- }));
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) });
2279
4007
  }
2280
4008
  }
2281
- function mapFinishReason(reason) {
2282
- if (!reason)
2283
- return;
2284
- const normalized = reason.toLowerCase();
2285
- switch (normalized) {
2286
- case "stop":
2287
- return "stop";
2288
- case "length":
2289
- return "length";
2290
- case "tool_call":
2291
- case "tool_calls":
2292
- return "tool_call";
2293
- case "content_filter":
2294
- return "content_filter";
2295
- default:
2296
- return;
4009
+ function toError(error) {
4010
+ if (error instanceof Error) {
4011
+ return error;
2297
4012
  }
4013
+ return new Error(String(error));
2298
4014
  }
2299
4015
 
2300
- // src/impls/mistral-embedding.ts
2301
- 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";
2302
4020
 
2303
- class MistralEmbeddingProvider {
2304
- client;
4021
+ class MistralConversationalProvider {
4022
+ apiKey;
2305
4023
  defaultModel;
4024
+ defaultVoiceId;
4025
+ baseUrl;
4026
+ fetchImpl;
4027
+ sttProvider;
2306
4028
  constructor(options) {
2307
4029
  if (!options.apiKey) {
2308
- throw new Error("MistralEmbeddingProvider requires an apiKey");
4030
+ throw new Error("MistralConversationalProvider requires an apiKey");
2309
4031
  }
2310
- 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({
2311
4038
  apiKey: options.apiKey,
2312
- serverURL: options.serverURL
4039
+ defaultModel: options.sttOptions?.defaultModel,
4040
+ defaultLanguage: options.sttOptions?.defaultLanguage,
4041
+ serverURL: options.sttOptions?.serverURL ?? options.serverURL,
4042
+ fetchImpl: this.fetchImpl
2313
4043
  });
2314
- this.defaultModel = options.defaultModel ?? "mistral-embed";
2315
4044
  }
2316
- async embedDocuments(documents, options) {
2317
- if (documents.length === 0)
2318
- return [];
2319
- const model = options?.model ?? this.defaultModel;
2320
- const response = await this.client.embeddings.create({
2321
- model,
2322
- 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
2323
4054
  });
2324
- return response.data.map((item, index) => ({
2325
- id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
2326
- vector: item.embedding ?? [],
2327
- dimensions: item.embedding?.length ?? 0,
2328
- model: response.model,
2329
- metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
2330
- }));
2331
4055
  }
2332
- async embedQuery(query, options) {
2333
- const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
2334
- if (!result) {
2335
- 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 });
2336
4071
  }
2337
- 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("");
2338
4112
  }
4113
+ return "";
4114
+ }
4115
+ function asRecord3(value) {
4116
+ if (value && typeof value === "object") {
4117
+ return value;
4118
+ }
4119
+ return {};
2339
4120
  }
2340
4121
 
2341
4122
  // src/impls/qdrant-vector.ts
@@ -2737,7 +4518,7 @@ function distanceToScore(distance, metric) {
2737
4518
 
2738
4519
  // src/impls/stripe-payments.ts
2739
4520
  import Stripe from "stripe";
2740
- var API_VERSION = "2026-01-28.clover";
4521
+ var API_VERSION = "2026-02-25.clover";
2741
4522
 
2742
4523
  class StripePaymentsProvider {
2743
4524
  stripe;
@@ -3402,8 +5183,320 @@ function mapStatus(status) {
3402
5183
  }
3403
5184
  }
3404
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
+
3405
5498
  // src/impls/powens-client.ts
3406
- import { URL } from "node:url";
5499
+ import { URL as URL2 } from "node:url";
3407
5500
  var POWENS_BASE_URL = {
3408
5501
  sandbox: "https://api-sandbox.powens.com/v2",
3409
5502
  production: "https://api.powens.com/v2"
@@ -3489,7 +5582,7 @@ class PowensClient {
3489
5582
  });
3490
5583
  }
3491
5584
  async request(options) {
3492
- const url = new URL(options.path, this.baseUrl);
5585
+ const url = new URL2(options.path, this.baseUrl);
3493
5586
  if (options.searchParams) {
3494
5587
  for (const [key, value] of Object.entries(options.searchParams)) {
3495
5588
  if (value === undefined || value === null)
@@ -3559,7 +5652,7 @@ class PowensClient {
3559
5652
  return this.token.accessToken;
3560
5653
  }
3561
5654
  async fetchAccessToken() {
3562
- const url = new URL("/oauth/token", this.baseUrl);
5655
+ const url = new URL2("/oauth/token", this.baseUrl);
3563
5656
  const basicAuth = Buffer.from(`${this.clientId}:${this.clientSecret}`, "utf-8").toString("base64");
3564
5657
  const response = await this.fetchImpl(url, {
3565
5658
  method: "POST",
@@ -3908,7 +6001,7 @@ function resolveLabelIds(defaults, tags) {
3908
6001
  }
3909
6002
 
3910
6003
  // src/impls/jira.ts
3911
- import { Buffer as Buffer4 } from "node:buffer";
6004
+ import { Buffer as Buffer5 } from "node:buffer";
3912
6005
 
3913
6006
  class JiraProjectManagementProvider {
3914
6007
  siteUrl;
@@ -3985,7 +6078,7 @@ function normalizeSiteUrl(siteUrl) {
3985
6078
  return siteUrl.replace(/\/$/, "");
3986
6079
  }
3987
6080
  function buildAuthHeader(email, apiToken) {
3988
- const token = Buffer4.from(`${email}:${apiToken}`).toString("base64");
6081
+ const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
3989
6082
  return `Basic ${token}`;
3990
6083
  }
3991
6084
  function resolveIssueType(type, defaults) {
@@ -4188,7 +6281,7 @@ function buildParagraphBlocks(text) {
4188
6281
  }
4189
6282
 
4190
6283
  // src/impls/tldv-meeting-recorder.ts
4191
- var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
6284
+ var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
4192
6285
 
4193
6286
  class TldvMeetingRecorderProvider {
4194
6287
  apiKey;
@@ -4196,7 +6289,7 @@ class TldvMeetingRecorderProvider {
4196
6289
  defaultPageSize;
4197
6290
  constructor(options) {
4198
6291
  this.apiKey = options.apiKey;
4199
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
6292
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
4200
6293
  this.defaultPageSize = options.pageSize;
4201
6294
  }
4202
6295
  async listMeetings(params) {
@@ -4331,7 +6424,7 @@ async function safeReadError4(response) {
4331
6424
  }
4332
6425
 
4333
6426
  // src/impls/provider-factory.ts
4334
- import { Buffer as Buffer5 } from "node:buffer";
6427
+ import { Buffer as Buffer6 } from "node:buffer";
4335
6428
  var SECRET_CACHE = new Map;
4336
6429
 
4337
6430
  class IntegrationProviderFactory {
@@ -4372,6 +6465,39 @@ class IntegrationProviderFactory {
4372
6465
  throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
4373
6466
  }
4374
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
+ }
4375
6501
  async createVectorStoreProvider(context) {
4376
6502
  const secrets = await this.loadSecrets(context);
4377
6503
  const config = context.config;
@@ -4470,6 +6596,41 @@ class IntegrationProviderFactory {
4470
6596
  throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
4471
6597
  }
4472
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
+ }
4473
6634
  async createProjectManagementProvider(context) {
4474
6635
  const secrets = await this.loadSecrets(context);
4475
6636
  const config = context.config;
@@ -4603,6 +6764,10 @@ class IntegrationProviderFactory {
4603
6764
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
4604
6765
  }
4605
6766
  }
6767
+ async createHealthProvider(context) {
6768
+ const secrets = await this.loadSecrets(context);
6769
+ return createHealthProviderFromContext(context, secrets);
6770
+ }
4606
6771
  async loadSecrets(context) {
4607
6772
  const cacheKey = context.connection.meta.id;
4608
6773
  if (SECRET_CACHE.has(cacheKey)) {
@@ -4616,7 +6781,7 @@ class IntegrationProviderFactory {
4616
6781
  }
4617
6782
  }
4618
6783
  function parseSecret(secret) {
4619
- const text = Buffer5.from(secret.data).toString("utf-8").trim();
6784
+ const text = Buffer6.from(secret.data).toString("utf-8").trim();
4620
6785
  if (!text)
4621
6786
  return {};
4622
6787
  try {
@@ -4662,6 +6827,9 @@ export * from "@contractspec/lib.contracts-integrations";
4662
6827
  // src/sms.ts
4663
6828
  export * from "@contractspec/lib.contracts-integrations";
4664
6829
 
6830
+ // src/messaging.ts
6831
+ export * from "@contractspec/lib.contracts-integrations";
6832
+
4665
6833
  // src/payments.ts
4666
6834
  export * from "@contractspec/lib.contracts-integrations";
4667
6835
 
@@ -4674,11 +6842,17 @@ export * from "@contractspec/lib.contracts-integrations";
4674
6842
  // src/meeting-recorder.ts
4675
6843
  export * from "@contractspec/lib.contracts-integrations";
4676
6844
  export {
6845
+ createHealthProviderFromContext,
6846
+ WhoopHealthProvider,
6847
+ UnofficialHealthAutomationProvider,
6848
+ TwilioWhatsappMessagingProvider,
4677
6849
  TwilioSmsProvider,
4678
6850
  TldvMeetingRecorderProvider,
4679
6851
  SupabaseVectorProvider,
4680
6852
  SupabasePostgresProvider,
4681
6853
  StripePaymentsProvider,
6854
+ StravaHealthProvider,
6855
+ SlackMessagingProvider,
4682
6856
  QdrantVectorProvider,
4683
6857
  PowensOpenBankingProvider,
4684
6858
  PowensClientError,
@@ -4686,9 +6860,16 @@ export {
4686
6860
  PostmarkEmailProvider,
4687
6861
  PosthogAnalyticsReader,
4688
6862
  PosthogAnalyticsProvider,
6863
+ PelotonHealthProvider,
6864
+ OuraHealthProvider,
6865
+ OpenWearablesHealthProvider,
4689
6866
  NotionProjectManagementProvider,
6867
+ MyFitnessPalHealthProvider,
6868
+ MistralSttProvider,
4690
6869
  MistralLLMProvider,
4691
6870
  MistralEmbeddingProvider,
6871
+ MistralConversationalProvider,
6872
+ MetaWhatsappMessagingProvider,
4692
6873
  LinearProjectManagementProvider,
4693
6874
  JiraProjectManagementProvider,
4694
6875
  IntegrationProviderFactory,
@@ -4698,8 +6879,13 @@ export {
4698
6879
  GoogleCalendarProvider,
4699
6880
  GmailOutboundProvider,
4700
6881
  GmailInboundProvider,
6882
+ GithubMessagingProvider,
6883
+ GarminHealthProvider,
6884
+ FitbitHealthProvider,
4701
6885
  FirefliesMeetingRecorderProvider,
4702
6886
  FathomMeetingRecorderProvider,
4703
6887
  FalVoiceProvider,
4704
- ElevenLabsVoiceProvider
6888
+ ElevenLabsVoiceProvider,
6889
+ EightSleepHealthProvider,
6890
+ AppleHealthBridgeProvider
4705
6891
  };