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