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