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