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