@contractspec/integration.providers-impls 2.10.0 → 3.1.1
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 +66 -10
- package/dist/impls/async-event-queue.d.ts +8 -0
- package/dist/impls/async-event-queue.js +49 -0
- package/dist/impls/composio-fallback-resolver.d.ts +34 -0
- package/dist/impls/composio-fallback-resolver.js +580 -0
- package/dist/impls/composio-mcp.d.ts +22 -0
- package/dist/impls/composio-mcp.js +164 -0
- package/dist/impls/composio-proxies.d.ts +60 -0
- package/dist/impls/composio-proxies.js +311 -0
- package/dist/impls/composio-sdk.d.ts +25 -0
- package/dist/impls/composio-sdk.js +78 -0
- package/dist/impls/composio-types.d.ts +43 -0
- package/dist/impls/composio-types.js +54 -0
- package/dist/impls/elevenlabs-voice.js +2 -0
- package/dist/impls/fal-voice.js +2 -0
- package/dist/impls/fathom-meeting-recorder.js +2 -0
- package/dist/impls/fathom-meeting-recorder.mapper.js +2 -0
- package/dist/impls/fathom-meeting-recorder.utils.js +2 -0
- package/dist/impls/fathom-meeting-recorder.webhooks.js +2 -0
- package/dist/impls/fireflies-meeting-recorder.js +2 -0
- package/dist/impls/fireflies-meeting-recorder.queries.js +2 -0
- package/dist/impls/fireflies-meeting-recorder.utils.js +2 -0
- package/dist/impls/gcs-storage.js +2 -0
- package/dist/impls/gmail-inbound.js +2 -0
- package/dist/impls/gmail-outbound.js +2 -0
- package/dist/impls/google-calendar.js +2 -0
- package/dist/impls/gradium-voice.js +2 -0
- package/dist/impls/granola-meeting-recorder.js +2 -0
- package/dist/impls/granola-meeting-recorder.mcp.js +2 -0
- package/dist/impls/health/base-health-provider.d.ts +64 -13
- package/dist/impls/health/base-health-provider.js +508 -156
- package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
- package/dist/impls/health/hybrid-health-providers.js +1090 -0
- package/dist/impls/health/official-health-providers.d.ts +78 -0
- package/dist/impls/health/official-health-providers.js +970 -0
- package/dist/impls/health/provider-normalizers.d.ts +28 -0
- package/dist/impls/health/provider-normalizers.js +289 -0
- package/dist/impls/health/providers.d.ts +2 -39
- package/dist/impls/health/providers.js +897 -184
- package/dist/impls/health-provider-factory.js +1011 -196
- package/dist/impls/index.d.ts +11 -0
- package/dist/impls/index.js +2588 -259
- package/dist/impls/jira.js +2 -0
- package/dist/impls/linear.js +2 -0
- package/dist/impls/messaging-github.d.ts +17 -0
- package/dist/impls/messaging-github.js +112 -0
- package/dist/impls/messaging-slack.d.ts +14 -0
- package/dist/impls/messaging-slack.js +82 -0
- package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
- package/dist/impls/messaging-whatsapp-meta.js +54 -0
- package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
- package/dist/impls/messaging-whatsapp-twilio.js +84 -0
- package/dist/impls/mistral-conversational.d.ts +23 -0
- package/dist/impls/mistral-conversational.js +478 -0
- package/dist/impls/mistral-conversational.session.d.ts +32 -0
- package/dist/impls/mistral-conversational.session.js +208 -0
- package/dist/impls/mistral-embedding.js +2 -0
- package/dist/impls/mistral-llm.js +2 -0
- package/dist/impls/mistral-stt.d.ts +17 -0
- package/dist/impls/mistral-stt.js +169 -0
- package/dist/impls/notion.js +2 -0
- package/dist/impls/posthog-reader.js +2 -0
- package/dist/impls/posthog-utils.js +2 -0
- package/dist/impls/posthog.js +2 -0
- package/dist/impls/postmark-email.js +2 -0
- package/dist/impls/powens-client.js +2 -0
- package/dist/impls/powens-openbanking.js +2 -0
- package/dist/impls/provider-factory.d.ts +29 -1
- package/dist/impls/provider-factory.js +1985 -249
- package/dist/impls/qdrant-vector.js +2 -0
- package/dist/impls/stripe-payments.js +3 -1
- package/dist/impls/supabase-psql.js +2 -0
- package/dist/impls/supabase-vector.js +2 -0
- package/dist/impls/tldv-meeting-recorder.js +2 -0
- package/dist/impls/twilio-sms.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2615 -283
- package/dist/messaging.d.ts +1 -0
- package/dist/messaging.js +3 -0
- package/dist/node/impls/async-event-queue.js +49 -0
- package/dist/node/impls/composio-fallback-resolver.js +580 -0
- package/dist/node/impls/composio-mcp.js +164 -0
- package/dist/node/impls/composio-proxies.js +311 -0
- package/dist/node/impls/composio-sdk.js +78 -0
- package/dist/node/impls/composio-types.js +54 -0
- package/dist/node/impls/elevenlabs-voice.js +3 -0
- package/dist/node/impls/fal-voice.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.mapper.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.utils.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.webhooks.js +3 -0
- package/dist/node/impls/fireflies-meeting-recorder.js +3 -0
- package/dist/node/impls/fireflies-meeting-recorder.queries.js +3 -0
- package/dist/node/impls/fireflies-meeting-recorder.utils.js +3 -0
- package/dist/node/impls/gcs-storage.js +3 -0
- package/dist/node/impls/gmail-inbound.js +3 -0
- package/dist/node/impls/gmail-outbound.js +3 -0
- package/dist/node/impls/google-calendar.js +3 -0
- package/dist/node/impls/gradium-voice.js +3 -0
- package/dist/node/impls/granola-meeting-recorder.js +3 -0
- package/dist/node/impls/granola-meeting-recorder.mcp.js +3 -0
- package/dist/node/impls/health/base-health-provider.js +509 -156
- package/dist/node/impls/health/hybrid-health-providers.js +1090 -0
- package/dist/node/impls/health/official-health-providers.js +970 -0
- package/dist/node/impls/health/provider-normalizers.js +289 -0
- package/dist/node/impls/health/providers.js +898 -184
- package/dist/node/impls/health-provider-factory.js +1012 -196
- package/dist/node/impls/index.js +2589 -259
- package/dist/node/impls/jira.js +3 -0
- package/dist/node/impls/linear.js +3 -0
- package/dist/node/impls/messaging-github.js +112 -0
- package/dist/node/impls/messaging-slack.js +82 -0
- package/dist/node/impls/messaging-whatsapp-meta.js +54 -0
- package/dist/node/impls/messaging-whatsapp-twilio.js +84 -0
- package/dist/node/impls/mistral-conversational.js +478 -0
- package/dist/node/impls/mistral-conversational.session.js +208 -0
- package/dist/node/impls/mistral-embedding.js +3 -0
- package/dist/node/impls/mistral-llm.js +3 -0
- package/dist/node/impls/mistral-stt.js +169 -0
- package/dist/node/impls/notion.js +3 -0
- package/dist/node/impls/posthog-reader.js +3 -0
- package/dist/node/impls/posthog-utils.js +3 -0
- package/dist/node/impls/posthog.js +3 -0
- package/dist/node/impls/postmark-email.js +3 -0
- package/dist/node/impls/powens-client.js +3 -0
- package/dist/node/impls/powens-openbanking.js +3 -0
- package/dist/node/impls/provider-factory.js +1986 -249
- package/dist/node/impls/qdrant-vector.js +3 -0
- package/dist/node/impls/stripe-payments.js +4 -1
- package/dist/node/impls/supabase-psql.js +3 -0
- package/dist/node/impls/supabase-vector.js +3 -0
- package/dist/node/impls/tldv-meeting-recorder.js +3 -0
- package/dist/node/impls/twilio-sms.js +3 -0
- package/dist/node/index.js +2616 -283
- package/dist/node/messaging.js +2 -0
- package/dist/node/secrets/provider.js +3 -0
- package/dist/secrets/provider.js +2 -0
- package/package.json +219 -14
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/impls/async-event-queue.ts
|
|
5
|
+
class AsyncEventQueue {
|
|
6
|
+
values = [];
|
|
7
|
+
waiters = [];
|
|
8
|
+
done = false;
|
|
9
|
+
push(value) {
|
|
10
|
+
if (this.done) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const waiter = this.waiters.shift();
|
|
14
|
+
if (waiter) {
|
|
15
|
+
waiter({ value, done: false });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this.values.push(value);
|
|
19
|
+
}
|
|
20
|
+
close() {
|
|
21
|
+
if (this.done) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.done = true;
|
|
25
|
+
for (const waiter of this.waiters) {
|
|
26
|
+
waiter({ value: undefined, done: true });
|
|
27
|
+
}
|
|
28
|
+
this.waiters.length = 0;
|
|
29
|
+
}
|
|
30
|
+
[Symbol.asyncIterator]() {
|
|
31
|
+
return {
|
|
32
|
+
next: async () => {
|
|
33
|
+
const value = this.values.shift();
|
|
34
|
+
if (value != null) {
|
|
35
|
+
return { value, done: false };
|
|
36
|
+
}
|
|
37
|
+
if (this.done) {
|
|
38
|
+
return { value: undefined, done: true };
|
|
39
|
+
}
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
this.waiters.push(resolve);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
2
48
|
// src/impls/elevenlabs-voice.ts
|
|
3
49
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
|
4
50
|
var FORMAT_MAP = {
|
|
@@ -1465,7 +1511,286 @@ async function safeReadError3(response) {
|
|
|
1465
1511
|
}
|
|
1466
1512
|
}
|
|
1467
1513
|
|
|
1514
|
+
// src/impls/health/provider-normalizers.ts
|
|
1515
|
+
var DEFAULT_LIST_KEYS = [
|
|
1516
|
+
"items",
|
|
1517
|
+
"data",
|
|
1518
|
+
"records",
|
|
1519
|
+
"activities",
|
|
1520
|
+
"workouts",
|
|
1521
|
+
"sleep",
|
|
1522
|
+
"biometrics",
|
|
1523
|
+
"nutrition"
|
|
1524
|
+
];
|
|
1525
|
+
function asRecord(value) {
|
|
1526
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
return value;
|
|
1530
|
+
}
|
|
1531
|
+
function asArray2(value) {
|
|
1532
|
+
return Array.isArray(value) ? value : undefined;
|
|
1533
|
+
}
|
|
1534
|
+
function readString2(record, keys) {
|
|
1535
|
+
if (!record)
|
|
1536
|
+
return;
|
|
1537
|
+
for (const key of keys) {
|
|
1538
|
+
const value = record[key];
|
|
1539
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1540
|
+
return value;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
function readNumber(record, keys) {
|
|
1546
|
+
if (!record)
|
|
1547
|
+
return;
|
|
1548
|
+
for (const key of keys) {
|
|
1549
|
+
const value = record[key];
|
|
1550
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1551
|
+
return value;
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1554
|
+
const parsed = Number(value);
|
|
1555
|
+
if (Number.isFinite(parsed)) {
|
|
1556
|
+
return parsed;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
function readBoolean2(record, keys) {
|
|
1563
|
+
if (!record)
|
|
1564
|
+
return;
|
|
1565
|
+
for (const key of keys) {
|
|
1566
|
+
const value = record[key];
|
|
1567
|
+
if (typeof value === "boolean") {
|
|
1568
|
+
return value;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
function extractList(payload, listKeys = DEFAULT_LIST_KEYS) {
|
|
1574
|
+
const root = asRecord(payload);
|
|
1575
|
+
if (!root) {
|
|
1576
|
+
return asArray2(payload)?.map((item) => asRecord(item)).filter((item) => Boolean(item)) ?? [];
|
|
1577
|
+
}
|
|
1578
|
+
for (const key of listKeys) {
|
|
1579
|
+
const arrayValue = asArray2(root[key]);
|
|
1580
|
+
if (!arrayValue)
|
|
1581
|
+
continue;
|
|
1582
|
+
return arrayValue.map((item) => asRecord(item)).filter((item) => Boolean(item));
|
|
1583
|
+
}
|
|
1584
|
+
return [];
|
|
1585
|
+
}
|
|
1586
|
+
function extractPagination(payload) {
|
|
1587
|
+
const root = asRecord(payload);
|
|
1588
|
+
const nestedPagination = asRecord(root?.pagination);
|
|
1589
|
+
const nextCursor = readString2(nestedPagination, ["nextCursor", "next_cursor"]) ?? readString2(root, [
|
|
1590
|
+
"nextCursor",
|
|
1591
|
+
"next_cursor",
|
|
1592
|
+
"cursor",
|
|
1593
|
+
"next_page_token"
|
|
1594
|
+
]);
|
|
1595
|
+
const hasMore = readBoolean2(nestedPagination, ["hasMore", "has_more"]) ?? readBoolean2(root, ["hasMore", "has_more"]);
|
|
1596
|
+
return {
|
|
1597
|
+
nextCursor,
|
|
1598
|
+
hasMore: hasMore ?? Boolean(nextCursor)
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
function toHealthActivity(item, context, fallbackType = "activity") {
|
|
1602
|
+
const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:${fallbackType}`;
|
|
1603
|
+
const id = readString2(item, ["id", "uuid", "workout_id"]) ?? `${context.providerKey}:activity:${externalId}`;
|
|
1604
|
+
return {
|
|
1605
|
+
id,
|
|
1606
|
+
externalId,
|
|
1607
|
+
tenantId: context.tenantId,
|
|
1608
|
+
connectionId: context.connectionId ?? "unknown",
|
|
1609
|
+
userId: readString2(item, ["user_id", "userId", "athlete_id"]),
|
|
1610
|
+
providerKey: context.providerKey,
|
|
1611
|
+
activityType: readString2(item, ["activity_type", "type", "sport_type", "sport"]) ?? fallbackType,
|
|
1612
|
+
startedAt: readIsoDate(item, [
|
|
1613
|
+
"started_at",
|
|
1614
|
+
"start_time",
|
|
1615
|
+
"start_date",
|
|
1616
|
+
"created_at"
|
|
1617
|
+
]),
|
|
1618
|
+
endedAt: readIsoDate(item, ["ended_at", "end_time"]),
|
|
1619
|
+
durationSeconds: readNumber(item, [
|
|
1620
|
+
"duration_seconds",
|
|
1621
|
+
"duration",
|
|
1622
|
+
"elapsed_time"
|
|
1623
|
+
]),
|
|
1624
|
+
distanceMeters: readNumber(item, ["distance_meters", "distance"]),
|
|
1625
|
+
caloriesKcal: readNumber(item, [
|
|
1626
|
+
"calories_kcal",
|
|
1627
|
+
"calories",
|
|
1628
|
+
"active_kilocalories"
|
|
1629
|
+
]),
|
|
1630
|
+
steps: readNumber(item, ["steps"])?.valueOf(),
|
|
1631
|
+
metadata: item
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function toHealthWorkout(item, context, fallbackType = "workout") {
|
|
1635
|
+
const activity = toHealthActivity(item, context, fallbackType);
|
|
1636
|
+
return {
|
|
1637
|
+
id: activity.id,
|
|
1638
|
+
externalId: activity.externalId,
|
|
1639
|
+
tenantId: activity.tenantId,
|
|
1640
|
+
connectionId: activity.connectionId,
|
|
1641
|
+
userId: activity.userId,
|
|
1642
|
+
providerKey: activity.providerKey,
|
|
1643
|
+
workoutType: readString2(item, [
|
|
1644
|
+
"workout_type",
|
|
1645
|
+
"sport_type",
|
|
1646
|
+
"type",
|
|
1647
|
+
"activity_type"
|
|
1648
|
+
]) ?? fallbackType,
|
|
1649
|
+
startedAt: activity.startedAt,
|
|
1650
|
+
endedAt: activity.endedAt,
|
|
1651
|
+
durationSeconds: activity.durationSeconds,
|
|
1652
|
+
distanceMeters: activity.distanceMeters,
|
|
1653
|
+
caloriesKcal: activity.caloriesKcal,
|
|
1654
|
+
averageHeartRateBpm: readNumber(item, [
|
|
1655
|
+
"average_heart_rate",
|
|
1656
|
+
"avg_hr",
|
|
1657
|
+
"average_heart_rate_bpm"
|
|
1658
|
+
]),
|
|
1659
|
+
maxHeartRateBpm: readNumber(item, [
|
|
1660
|
+
"max_heart_rate",
|
|
1661
|
+
"max_hr",
|
|
1662
|
+
"max_heart_rate_bpm"
|
|
1663
|
+
]),
|
|
1664
|
+
metadata: item
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function toHealthSleep(item, context) {
|
|
1668
|
+
const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:sleep`;
|
|
1669
|
+
const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:sleep:${externalId}`;
|
|
1670
|
+
const startedAt = readIsoDate(item, ["started_at", "start_time", "bedtime_start", "start"]) ?? new Date(0).toISOString();
|
|
1671
|
+
const endedAt = readIsoDate(item, ["ended_at", "end_time", "bedtime_end", "end"]) ?? startedAt;
|
|
1672
|
+
return {
|
|
1673
|
+
id,
|
|
1674
|
+
externalId,
|
|
1675
|
+
tenantId: context.tenantId,
|
|
1676
|
+
connectionId: context.connectionId ?? "unknown",
|
|
1677
|
+
userId: readString2(item, ["user_id", "userId"]),
|
|
1678
|
+
providerKey: context.providerKey,
|
|
1679
|
+
startedAt,
|
|
1680
|
+
endedAt,
|
|
1681
|
+
durationSeconds: readNumber(item, [
|
|
1682
|
+
"duration_seconds",
|
|
1683
|
+
"duration",
|
|
1684
|
+
"total_sleep_duration"
|
|
1685
|
+
]),
|
|
1686
|
+
deepSleepSeconds: readNumber(item, [
|
|
1687
|
+
"deep_sleep_seconds",
|
|
1688
|
+
"deep_sleep_duration"
|
|
1689
|
+
]),
|
|
1690
|
+
lightSleepSeconds: readNumber(item, [
|
|
1691
|
+
"light_sleep_seconds",
|
|
1692
|
+
"light_sleep_duration"
|
|
1693
|
+
]),
|
|
1694
|
+
remSleepSeconds: readNumber(item, [
|
|
1695
|
+
"rem_sleep_seconds",
|
|
1696
|
+
"rem_sleep_duration"
|
|
1697
|
+
]),
|
|
1698
|
+
awakeSeconds: readNumber(item, ["awake_seconds", "awake_time"]),
|
|
1699
|
+
sleepScore: readNumber(item, ["sleep_score", "score"]),
|
|
1700
|
+
metadata: item
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function toHealthBiometric(item, context, metricTypeFallback = "metric") {
|
|
1704
|
+
const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:biometric`;
|
|
1705
|
+
const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:biometric:${externalId}`;
|
|
1706
|
+
return {
|
|
1707
|
+
id,
|
|
1708
|
+
externalId,
|
|
1709
|
+
tenantId: context.tenantId,
|
|
1710
|
+
connectionId: context.connectionId ?? "unknown",
|
|
1711
|
+
userId: readString2(item, ["user_id", "userId"]),
|
|
1712
|
+
providerKey: context.providerKey,
|
|
1713
|
+
metricType: readString2(item, ["metric_type", "metric", "type", "name"]) ?? metricTypeFallback,
|
|
1714
|
+
value: readNumber(item, ["value", "score", "measurement"]) ?? 0,
|
|
1715
|
+
unit: readString2(item, ["unit"]),
|
|
1716
|
+
measuredAt: readIsoDate(item, ["measured_at", "timestamp", "created_at"]) ?? new Date().toISOString(),
|
|
1717
|
+
metadata: item
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function toHealthNutrition(item, context) {
|
|
1721
|
+
const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:nutrition`;
|
|
1722
|
+
const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:nutrition:${externalId}`;
|
|
1723
|
+
return {
|
|
1724
|
+
id,
|
|
1725
|
+
externalId,
|
|
1726
|
+
tenantId: context.tenantId,
|
|
1727
|
+
connectionId: context.connectionId ?? "unknown",
|
|
1728
|
+
userId: readString2(item, ["user_id", "userId"]),
|
|
1729
|
+
providerKey: context.providerKey,
|
|
1730
|
+
loggedAt: readIsoDate(item, ["logged_at", "created_at", "date", "timestamp"]) ?? new Date().toISOString(),
|
|
1731
|
+
caloriesKcal: readNumber(item, ["calories_kcal", "calories"]),
|
|
1732
|
+
proteinGrams: readNumber(item, ["protein_grams", "protein"]),
|
|
1733
|
+
carbsGrams: readNumber(item, ["carbs_grams", "carbs"]),
|
|
1734
|
+
fatGrams: readNumber(item, ["fat_grams", "fat"]),
|
|
1735
|
+
fiberGrams: readNumber(item, ["fiber_grams", "fiber"]),
|
|
1736
|
+
hydrationMl: readNumber(item, ["hydration_ml", "water_ml", "water"]),
|
|
1737
|
+
metadata: item
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
function toHealthConnectionStatus(payload, params, source) {
|
|
1741
|
+
const record = asRecord(payload);
|
|
1742
|
+
const rawStatus = readString2(record, ["status", "connection_status", "health"]) ?? "healthy";
|
|
1743
|
+
return {
|
|
1744
|
+
tenantId: params.tenantId,
|
|
1745
|
+
connectionId: params.connectionId,
|
|
1746
|
+
status: rawStatus === "healthy" || rawStatus === "degraded" || rawStatus === "error" || rawStatus === "disconnected" ? rawStatus : "healthy",
|
|
1747
|
+
source,
|
|
1748
|
+
lastCheckedAt: readIsoDate(record, ["last_checked_at", "lastCheckedAt"]) ?? new Date().toISOString(),
|
|
1749
|
+
errorCode: readString2(record, ["error_code", "errorCode"]),
|
|
1750
|
+
errorMessage: readString2(record, ["error_message", "errorMessage"]),
|
|
1751
|
+
metadata: asRecord(record?.metadata)
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function toHealthWebhookEvent(payload, providerKey, verified) {
|
|
1755
|
+
const record = asRecord(payload);
|
|
1756
|
+
const entityType = readString2(record, ["entity_type", "entityType", "type"]);
|
|
1757
|
+
const normalizedEntityType = entityType === "activity" || entityType === "workout" || entityType === "sleep" || entityType === "biometric" || entityType === "nutrition" ? entityType : undefined;
|
|
1758
|
+
return {
|
|
1759
|
+
providerKey,
|
|
1760
|
+
eventType: readString2(record, ["event_type", "eventType", "event"]),
|
|
1761
|
+
externalEntityId: readString2(record, [
|
|
1762
|
+
"external_entity_id",
|
|
1763
|
+
"externalEntityId",
|
|
1764
|
+
"entity_id",
|
|
1765
|
+
"entityId",
|
|
1766
|
+
"id"
|
|
1767
|
+
]),
|
|
1768
|
+
entityType: normalizedEntityType,
|
|
1769
|
+
receivedAt: new Date().toISOString(),
|
|
1770
|
+
verified,
|
|
1771
|
+
payload,
|
|
1772
|
+
metadata: asRecord(record?.metadata)
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function readIsoDate(record, keys) {
|
|
1776
|
+
const value = readString2(record, keys);
|
|
1777
|
+
if (!value)
|
|
1778
|
+
return;
|
|
1779
|
+
const parsed = new Date(value);
|
|
1780
|
+
if (Number.isNaN(parsed.getTime()))
|
|
1781
|
+
return;
|
|
1782
|
+
return parsed.toISOString();
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1468
1785
|
// src/impls/health/base-health-provider.ts
|
|
1786
|
+
class HealthProviderCapabilityError extends Error {
|
|
1787
|
+
code = "NOT_SUPPORTED";
|
|
1788
|
+
constructor(message) {
|
|
1789
|
+
super(message);
|
|
1790
|
+
this.name = "HealthProviderCapabilityError";
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1469
1794
|
class BaseHealthProvider {
|
|
1470
1795
|
providerKey;
|
|
1471
1796
|
transport;
|
|
@@ -1473,146 +1798,191 @@ class BaseHealthProvider {
|
|
|
1473
1798
|
mcpUrl;
|
|
1474
1799
|
apiKey;
|
|
1475
1800
|
accessToken;
|
|
1801
|
+
refreshToken;
|
|
1476
1802
|
mcpAccessToken;
|
|
1477
1803
|
webhookSecret;
|
|
1804
|
+
webhookSignatureHeader;
|
|
1805
|
+
route;
|
|
1806
|
+
aggregatorKey;
|
|
1807
|
+
oauth;
|
|
1478
1808
|
fetchFn;
|
|
1479
1809
|
mcpRequestId = 0;
|
|
1480
1810
|
constructor(options) {
|
|
1481
1811
|
this.providerKey = options.providerKey;
|
|
1482
1812
|
this.transport = options.transport;
|
|
1483
|
-
this.apiBaseUrl = options.apiBaseUrl
|
|
1813
|
+
this.apiBaseUrl = options.apiBaseUrl;
|
|
1484
1814
|
this.mcpUrl = options.mcpUrl;
|
|
1485
1815
|
this.apiKey = options.apiKey;
|
|
1486
1816
|
this.accessToken = options.accessToken;
|
|
1817
|
+
this.refreshToken = options.oauth?.refreshToken;
|
|
1487
1818
|
this.mcpAccessToken = options.mcpAccessToken;
|
|
1488
1819
|
this.webhookSecret = options.webhookSecret;
|
|
1820
|
+
this.webhookSignatureHeader = options.webhookSignatureHeader ?? "x-webhook-signature";
|
|
1821
|
+
this.route = options.route ?? "primary";
|
|
1822
|
+
this.aggregatorKey = options.aggregatorKey;
|
|
1823
|
+
this.oauth = options.oauth ?? {};
|
|
1489
1824
|
this.fetchFn = options.fetchFn ?? fetch;
|
|
1490
1825
|
}
|
|
1491
|
-
async listActivities(
|
|
1492
|
-
|
|
1493
|
-
return {
|
|
1494
|
-
activities: result.items,
|
|
1495
|
-
nextCursor: result.nextCursor,
|
|
1496
|
-
hasMore: result.hasMore,
|
|
1497
|
-
source: this.currentSource()
|
|
1498
|
-
};
|
|
1826
|
+
async listActivities(_params) {
|
|
1827
|
+
throw this.unsupported("activities");
|
|
1499
1828
|
}
|
|
1500
|
-
async listWorkouts(
|
|
1501
|
-
|
|
1829
|
+
async listWorkouts(_params) {
|
|
1830
|
+
throw this.unsupported("workouts");
|
|
1831
|
+
}
|
|
1832
|
+
async listSleep(_params) {
|
|
1833
|
+
throw this.unsupported("sleep");
|
|
1834
|
+
}
|
|
1835
|
+
async listBiometrics(_params) {
|
|
1836
|
+
throw this.unsupported("biometrics");
|
|
1837
|
+
}
|
|
1838
|
+
async listNutrition(_params) {
|
|
1839
|
+
throw this.unsupported("nutrition");
|
|
1840
|
+
}
|
|
1841
|
+
async getConnectionStatus(params) {
|
|
1842
|
+
return this.fetchConnectionStatus(params, {
|
|
1843
|
+
mcpTool: `${this.providerSlug()}_connection_status`
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
async syncActivities(params) {
|
|
1847
|
+
return this.syncFromList(() => this.listActivities(params));
|
|
1848
|
+
}
|
|
1849
|
+
async syncWorkouts(params) {
|
|
1850
|
+
return this.syncFromList(() => this.listWorkouts(params));
|
|
1851
|
+
}
|
|
1852
|
+
async syncSleep(params) {
|
|
1853
|
+
return this.syncFromList(() => this.listSleep(params));
|
|
1854
|
+
}
|
|
1855
|
+
async syncBiometrics(params) {
|
|
1856
|
+
return this.syncFromList(() => this.listBiometrics(params));
|
|
1857
|
+
}
|
|
1858
|
+
async syncNutrition(params) {
|
|
1859
|
+
return this.syncFromList(() => this.listNutrition(params));
|
|
1860
|
+
}
|
|
1861
|
+
async parseWebhook(request) {
|
|
1862
|
+
const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
|
|
1863
|
+
const verified = await this.verifyWebhook(request);
|
|
1864
|
+
return toHealthWebhookEvent(payload, this.providerKey, verified);
|
|
1865
|
+
}
|
|
1866
|
+
async verifyWebhook(request) {
|
|
1867
|
+
if (!this.webhookSecret)
|
|
1868
|
+
return true;
|
|
1869
|
+
const signature = readHeader(request.headers, this.webhookSignatureHeader);
|
|
1870
|
+
return signature === this.webhookSecret;
|
|
1871
|
+
}
|
|
1872
|
+
async fetchActivities(params, config) {
|
|
1873
|
+
const response = await this.fetchList(params, config);
|
|
1502
1874
|
return {
|
|
1503
|
-
|
|
1504
|
-
nextCursor:
|
|
1505
|
-
hasMore:
|
|
1875
|
+
activities: response.items,
|
|
1876
|
+
nextCursor: response.nextCursor,
|
|
1877
|
+
hasMore: response.hasMore,
|
|
1506
1878
|
source: this.currentSource()
|
|
1507
1879
|
};
|
|
1508
1880
|
}
|
|
1509
|
-
async
|
|
1510
|
-
const
|
|
1881
|
+
async fetchWorkouts(params, config) {
|
|
1882
|
+
const response = await this.fetchList(params, config);
|
|
1511
1883
|
return {
|
|
1512
|
-
|
|
1513
|
-
nextCursor:
|
|
1514
|
-
hasMore:
|
|
1884
|
+
workouts: response.items,
|
|
1885
|
+
nextCursor: response.nextCursor,
|
|
1886
|
+
hasMore: response.hasMore,
|
|
1515
1887
|
source: this.currentSource()
|
|
1516
1888
|
};
|
|
1517
1889
|
}
|
|
1518
|
-
async
|
|
1519
|
-
const
|
|
1890
|
+
async fetchSleep(params, config) {
|
|
1891
|
+
const response = await this.fetchList(params, config);
|
|
1520
1892
|
return {
|
|
1521
|
-
|
|
1522
|
-
nextCursor:
|
|
1523
|
-
hasMore:
|
|
1893
|
+
sleep: response.items,
|
|
1894
|
+
nextCursor: response.nextCursor,
|
|
1895
|
+
hasMore: response.hasMore,
|
|
1524
1896
|
source: this.currentSource()
|
|
1525
1897
|
};
|
|
1526
1898
|
}
|
|
1527
|
-
async
|
|
1528
|
-
const
|
|
1899
|
+
async fetchBiometrics(params, config) {
|
|
1900
|
+
const response = await this.fetchList(params, config);
|
|
1529
1901
|
return {
|
|
1530
|
-
|
|
1531
|
-
nextCursor:
|
|
1532
|
-
hasMore:
|
|
1902
|
+
biometrics: response.items,
|
|
1903
|
+
nextCursor: response.nextCursor,
|
|
1904
|
+
hasMore: response.hasMore,
|
|
1533
1905
|
source: this.currentSource()
|
|
1534
1906
|
};
|
|
1535
1907
|
}
|
|
1536
|
-
async
|
|
1537
|
-
const
|
|
1538
|
-
const status = readString2(payload, "status") ?? "healthy";
|
|
1908
|
+
async fetchNutrition(params, config) {
|
|
1909
|
+
const response = await this.fetchList(params, config);
|
|
1539
1910
|
return {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
source: this.currentSource()
|
|
1544
|
-
lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
|
|
1545
|
-
errorCode: readString2(payload, "errorCode"),
|
|
1546
|
-
errorMessage: readString2(payload, "errorMessage"),
|
|
1547
|
-
metadata: asRecord(payload.metadata)
|
|
1911
|
+
nutrition: response.items,
|
|
1912
|
+
nextCursor: response.nextCursor,
|
|
1913
|
+
hasMore: response.hasMore,
|
|
1914
|
+
source: this.currentSource()
|
|
1548
1915
|
};
|
|
1549
1916
|
}
|
|
1550
|
-
async
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
async syncWorkouts(params) {
|
|
1554
|
-
return this.sync("workouts", params);
|
|
1555
|
-
}
|
|
1556
|
-
async syncSleep(params) {
|
|
1557
|
-
return this.sync("sleep", params);
|
|
1558
|
-
}
|
|
1559
|
-
async syncBiometrics(params) {
|
|
1560
|
-
return this.sync("biometrics", params);
|
|
1561
|
-
}
|
|
1562
|
-
async syncNutrition(params) {
|
|
1563
|
-
return this.sync("nutrition", params);
|
|
1917
|
+
async fetchConnectionStatus(params, config) {
|
|
1918
|
+
const payload = await this.fetchPayload(config, params);
|
|
1919
|
+
return toHealthConnectionStatus(payload, params, this.currentSource());
|
|
1564
1920
|
}
|
|
1565
|
-
|
|
1566
|
-
const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
|
|
1567
|
-
const body = asRecord(payload);
|
|
1921
|
+
currentSource() {
|
|
1568
1922
|
return {
|
|
1569
1923
|
providerKey: this.providerKey,
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
receivedAt: new Date().toISOString(),
|
|
1574
|
-
verified: await this.verifyWebhook(request),
|
|
1575
|
-
payload
|
|
1924
|
+
transport: this.transport,
|
|
1925
|
+
route: this.route,
|
|
1926
|
+
aggregatorKey: this.aggregatorKey
|
|
1576
1927
|
};
|
|
1577
1928
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
return signature === this.webhookSecret;
|
|
1929
|
+
providerSlug() {
|
|
1930
|
+
return this.providerKey.replace("health.", "").replace(/-/g, "_");
|
|
1931
|
+
}
|
|
1932
|
+
unsupported(capability) {
|
|
1933
|
+
return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
|
|
1584
1934
|
}
|
|
1585
|
-
async
|
|
1586
|
-
const
|
|
1587
|
-
const
|
|
1935
|
+
async syncFromList(executor) {
|
|
1936
|
+
const result = await executor();
|
|
1937
|
+
const records = countResultRecords(result);
|
|
1588
1938
|
return {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1939
|
+
synced: records,
|
|
1940
|
+
failed: 0,
|
|
1941
|
+
nextCursor: undefined,
|
|
1942
|
+
source: result.source
|
|
1592
1943
|
};
|
|
1593
1944
|
}
|
|
1594
|
-
async
|
|
1595
|
-
const payload = await this.
|
|
1945
|
+
async fetchList(params, config) {
|
|
1946
|
+
const payload = await this.fetchPayload(config, params);
|
|
1947
|
+
const items = extractList(payload, config.listKeys).map((item) => config.mapItem(item, params)).filter((item) => Boolean(item));
|
|
1948
|
+
const pagination = extractPagination(payload);
|
|
1596
1949
|
return {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
errors: asArray2(payload.errors)?.map((item) => String(item)),
|
|
1601
|
-
source: this.currentSource()
|
|
1950
|
+
items,
|
|
1951
|
+
nextCursor: pagination.nextCursor,
|
|
1952
|
+
hasMore: pagination.hasMore
|
|
1602
1953
|
};
|
|
1603
1954
|
}
|
|
1604
|
-
async
|
|
1605
|
-
|
|
1606
|
-
|
|
1955
|
+
async fetchPayload(config, params) {
|
|
1956
|
+
const method = config.method ?? "GET";
|
|
1957
|
+
const query = config.buildQuery?.(params);
|
|
1958
|
+
const body = config.buildBody?.(params);
|
|
1959
|
+
if (this.isMcpTransport()) {
|
|
1960
|
+
return this.callMcpTool(config.mcpTool, {
|
|
1961
|
+
...query ?? {},
|
|
1962
|
+
...body ?? {}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
if (!config.apiPath || !this.apiBaseUrl) {
|
|
1966
|
+
throw new Error(`${this.providerKey} transport is missing an API path.`);
|
|
1967
|
+
}
|
|
1968
|
+
if (method === "POST") {
|
|
1969
|
+
return this.requestApi(config.apiPath, "POST", undefined, body);
|
|
1607
1970
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1971
|
+
return this.requestApi(config.apiPath, "GET", query, undefined);
|
|
1972
|
+
}
|
|
1973
|
+
isMcpTransport() {
|
|
1974
|
+
return this.transport.endsWith("mcp") || this.transport === "unofficial";
|
|
1975
|
+
}
|
|
1976
|
+
async requestApi(path, method, query, body) {
|
|
1977
|
+
const url = new URL(path, ensureTrailingSlash(this.apiBaseUrl ?? ""));
|
|
1978
|
+
if (query) {
|
|
1979
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1611
1980
|
if (value == null)
|
|
1612
1981
|
continue;
|
|
1613
1982
|
if (Array.isArray(value)) {
|
|
1614
|
-
value.forEach((
|
|
1615
|
-
|
|
1983
|
+
value.forEach((entry) => {
|
|
1984
|
+
if (entry != null)
|
|
1985
|
+
url.searchParams.append(key, String(entry));
|
|
1616
1986
|
});
|
|
1617
1987
|
continue;
|
|
1618
1988
|
}
|
|
@@ -1621,22 +1991,22 @@ class BaseHealthProvider {
|
|
|
1621
1991
|
}
|
|
1622
1992
|
const response = await this.fetchFn(url, {
|
|
1623
1993
|
method,
|
|
1624
|
-
headers:
|
|
1625
|
-
|
|
1626
|
-
...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
|
|
1627
|
-
},
|
|
1628
|
-
body: method === "POST" ? JSON.stringify(params) : undefined
|
|
1994
|
+
headers: this.authorizationHeaders(),
|
|
1995
|
+
body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
|
|
1629
1996
|
});
|
|
1630
|
-
if (
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1997
|
+
if (response.status === 401 && await this.refreshAccessToken()) {
|
|
1998
|
+
const retryResponse = await this.fetchFn(url, {
|
|
1999
|
+
method,
|
|
2000
|
+
headers: this.authorizationHeaders(),
|
|
2001
|
+
body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
|
|
2002
|
+
});
|
|
2003
|
+
return this.readResponsePayload(retryResponse, path);
|
|
1633
2004
|
}
|
|
1634
|
-
|
|
1635
|
-
return asRecord(data) ?? {};
|
|
2005
|
+
return this.readResponsePayload(response, path);
|
|
1636
2006
|
}
|
|
1637
|
-
async callMcpTool(
|
|
2007
|
+
async callMcpTool(toolName, args) {
|
|
1638
2008
|
if (!this.mcpUrl) {
|
|
1639
|
-
|
|
2009
|
+
throw new Error(`${this.providerKey} MCP URL is not configured.`);
|
|
1640
2010
|
}
|
|
1641
2011
|
const response = await this.fetchFn(this.mcpUrl, {
|
|
1642
2012
|
method: "POST",
|
|
@@ -1649,78 +2019,103 @@ class BaseHealthProvider {
|
|
|
1649
2019
|
id: ++this.mcpRequestId,
|
|
1650
2020
|
method: "tools/call",
|
|
1651
2021
|
params: {
|
|
1652
|
-
name:
|
|
1653
|
-
arguments:
|
|
2022
|
+
name: toolName,
|
|
2023
|
+
arguments: args
|
|
1654
2024
|
}
|
|
1655
2025
|
})
|
|
1656
2026
|
});
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
const
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
return structured;
|
|
1667
|
-
const data = asRecord(result.data);
|
|
1668
|
-
if (data)
|
|
1669
|
-
return data;
|
|
1670
|
-
return result;
|
|
2027
|
+
const payload = await this.readResponsePayload(response, toolName);
|
|
2028
|
+
const rpcEnvelope = asRecord(payload);
|
|
2029
|
+
if (!rpcEnvelope)
|
|
2030
|
+
return payload;
|
|
2031
|
+
const rpcResult = asRecord(rpcEnvelope.result);
|
|
2032
|
+
if (rpcResult) {
|
|
2033
|
+
return rpcResult.structuredContent ?? rpcResult.data ?? rpcResult;
|
|
2034
|
+
}
|
|
2035
|
+
return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
|
|
1671
2036
|
}
|
|
1672
|
-
|
|
2037
|
+
authorizationHeaders() {
|
|
2038
|
+
const token = this.accessToken ?? this.apiKey;
|
|
1673
2039
|
return {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
route: "primary"
|
|
2040
|
+
"Content-Type": "application/json",
|
|
2041
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
1677
2042
|
};
|
|
1678
2043
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
2044
|
+
async refreshAccessToken() {
|
|
2045
|
+
if (!this.oauth.tokenUrl || !this.refreshToken) {
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
const tokenUrl = new URL(this.oauth.tokenUrl);
|
|
2049
|
+
const body = new URLSearchParams({
|
|
2050
|
+
grant_type: "refresh_token",
|
|
2051
|
+
refresh_token: this.refreshToken,
|
|
2052
|
+
...this.oauth.clientId ? { client_id: this.oauth.clientId } : {},
|
|
2053
|
+
...this.oauth.clientSecret ? { client_secret: this.oauth.clientSecret } : {}
|
|
2054
|
+
});
|
|
2055
|
+
const response = await this.fetchFn(tokenUrl, {
|
|
2056
|
+
method: "POST",
|
|
2057
|
+
headers: {
|
|
2058
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2059
|
+
},
|
|
2060
|
+
body: body.toString()
|
|
2061
|
+
});
|
|
2062
|
+
if (!response.ok) {
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
const payload = await response.json();
|
|
2066
|
+
this.accessToken = payload.access_token;
|
|
2067
|
+
this.refreshToken = payload.refresh_token ?? this.refreshToken;
|
|
2068
|
+
if (typeof payload.expires_in === "number") {
|
|
2069
|
+
this.oauth.tokenExpiresAt = new Date(Date.now() + payload.expires_in * 1000).toISOString();
|
|
2070
|
+
}
|
|
2071
|
+
return Boolean(this.accessToken);
|
|
2072
|
+
}
|
|
2073
|
+
async readResponsePayload(response, context) {
|
|
2074
|
+
if (!response.ok) {
|
|
2075
|
+
const message = await safeReadText2(response);
|
|
2076
|
+
throw new Error(`${this.providerKey} request ${context} failed (${response.status}): ${message}`);
|
|
2077
|
+
}
|
|
2078
|
+
if (response.status === 204) {
|
|
2079
|
+
return {};
|
|
2080
|
+
}
|
|
2081
|
+
return response.json();
|
|
1685
2082
|
}
|
|
1686
2083
|
}
|
|
1687
2084
|
function readHeader(headers, key) {
|
|
1688
|
-
const
|
|
1689
|
-
|
|
2085
|
+
const target = key.toLowerCase();
|
|
2086
|
+
const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
|
|
2087
|
+
if (!entry)
|
|
1690
2088
|
return;
|
|
1691
|
-
const value =
|
|
2089
|
+
const value = entry[1];
|
|
1692
2090
|
return Array.isArray(value) ? value[0] : value;
|
|
1693
2091
|
}
|
|
1694
|
-
function
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2092
|
+
function countResultRecords(result) {
|
|
2093
|
+
const listKeys = [
|
|
2094
|
+
"activities",
|
|
2095
|
+
"workouts",
|
|
2096
|
+
"sleep",
|
|
2097
|
+
"biometrics",
|
|
2098
|
+
"nutrition"
|
|
2099
|
+
];
|
|
2100
|
+
for (const key of listKeys) {
|
|
2101
|
+
const value = result[key];
|
|
2102
|
+
if (Array.isArray(value)) {
|
|
2103
|
+
return value.length;
|
|
2104
|
+
}
|
|
1705
2105
|
}
|
|
1706
|
-
return
|
|
1707
|
-
}
|
|
1708
|
-
function asArray2(value) {
|
|
1709
|
-
return Array.isArray(value) ? value : undefined;
|
|
2106
|
+
return 0;
|
|
1710
2107
|
}
|
|
1711
|
-
function
|
|
1712
|
-
|
|
1713
|
-
return typeof value === "string" ? value : undefined;
|
|
1714
|
-
}
|
|
1715
|
-
function readBoolean2(record, key) {
|
|
1716
|
-
const value = record?.[key];
|
|
1717
|
-
return typeof value === "boolean" ? value : undefined;
|
|
2108
|
+
function ensureTrailingSlash(value) {
|
|
2109
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
1718
2110
|
}
|
|
1719
|
-
function
|
|
1720
|
-
|
|
1721
|
-
|
|
2111
|
+
function safeJsonParse(raw) {
|
|
2112
|
+
try {
|
|
2113
|
+
return JSON.parse(raw);
|
|
2114
|
+
} catch {
|
|
2115
|
+
return { rawBody: raw };
|
|
2116
|
+
}
|
|
1722
2117
|
}
|
|
1723
|
-
async function
|
|
2118
|
+
async function safeReadText2(response) {
|
|
1724
2119
|
try {
|
|
1725
2120
|
return await response.text();
|
|
1726
2121
|
} catch {
|
|
@@ -1728,133 +2123,530 @@ async function safeResponseText(response) {
|
|
|
1728
2123
|
}
|
|
1729
2124
|
}
|
|
1730
2125
|
|
|
1731
|
-
// src/impls/health/providers.ts
|
|
1732
|
-
function
|
|
2126
|
+
// src/impls/health/official-health-providers.ts
|
|
2127
|
+
function buildSharedQuery(params) {
|
|
1733
2128
|
return {
|
|
1734
|
-
|
|
1735
|
-
|
|
2129
|
+
tenantId: params.tenantId,
|
|
2130
|
+
connectionId: params.connectionId,
|
|
2131
|
+
userId: params.userId,
|
|
2132
|
+
from: params.from,
|
|
2133
|
+
to: params.to,
|
|
2134
|
+
cursor: params.cursor,
|
|
2135
|
+
pageSize: params.pageSize
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
function withMetricTypes(params) {
|
|
2139
|
+
return {
|
|
2140
|
+
...buildSharedQuery(params),
|
|
2141
|
+
metricTypes: params.metricTypes
|
|
1736
2142
|
};
|
|
1737
2143
|
}
|
|
1738
2144
|
|
|
1739
2145
|
class OpenWearablesHealthProvider extends BaseHealthProvider {
|
|
2146
|
+
upstreamProvider;
|
|
1740
2147
|
constructor(options) {
|
|
1741
2148
|
super({
|
|
1742
|
-
providerKey: "health.openwearables",
|
|
1743
|
-
|
|
2149
|
+
providerKey: options.providerKey ?? "health.openwearables",
|
|
2150
|
+
apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
|
|
2151
|
+
webhookSignatureHeader: "x-openwearables-signature",
|
|
2152
|
+
...options
|
|
1744
2153
|
});
|
|
2154
|
+
this.upstreamProvider = options.upstreamProvider;
|
|
1745
2155
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
...createProviderOptions(options, "official-api")
|
|
2156
|
+
async listActivities(params) {
|
|
2157
|
+
return this.fetchActivities(params, {
|
|
2158
|
+
apiPath: "/v1/activities",
|
|
2159
|
+
mcpTool: "openwearables_list_activities",
|
|
2160
|
+
buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
|
|
2161
|
+
mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
|
|
1753
2162
|
});
|
|
1754
2163
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
...createProviderOptions(options, "aggregator-api")
|
|
2164
|
+
async listWorkouts(params) {
|
|
2165
|
+
return this.fetchWorkouts(params, {
|
|
2166
|
+
apiPath: "/v1/workouts",
|
|
2167
|
+
mcpTool: "openwearables_list_workouts",
|
|
2168
|
+
buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
|
|
2169
|
+
mapItem: (item, input) => toHealthWorkout(item, this.context(input))
|
|
1762
2170
|
});
|
|
1763
2171
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
...createProviderOptions(options, "official-api")
|
|
2172
|
+
async listSleep(params) {
|
|
2173
|
+
return this.fetchSleep(params, {
|
|
2174
|
+
apiPath: "/v1/sleep",
|
|
2175
|
+
mcpTool: "openwearables_list_sleep",
|
|
2176
|
+
buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
|
|
2177
|
+
mapItem: (item, input) => toHealthSleep(item, this.context(input))
|
|
1771
2178
|
});
|
|
1772
2179
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
...createProviderOptions(options, "official-api")
|
|
2180
|
+
async listBiometrics(params) {
|
|
2181
|
+
return this.fetchBiometrics(params, {
|
|
2182
|
+
apiPath: "/v1/biometrics",
|
|
2183
|
+
mcpTool: "openwearables_list_biometrics",
|
|
2184
|
+
buildQuery: (input) => this.withUpstreamProvider(withMetricTypes(input)),
|
|
2185
|
+
mapItem: (item, input) => toHealthBiometric(item, this.context(input))
|
|
1780
2186
|
});
|
|
1781
2187
|
}
|
|
2188
|
+
async listNutrition(params) {
|
|
2189
|
+
return this.fetchNutrition(params, {
|
|
2190
|
+
apiPath: "/v1/nutrition",
|
|
2191
|
+
mcpTool: "openwearables_list_nutrition",
|
|
2192
|
+
buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
|
|
2193
|
+
mapItem: (item, input) => toHealthNutrition(item, this.context(input))
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
async getConnectionStatus(params) {
|
|
2197
|
+
return this.fetchConnectionStatus(params, {
|
|
2198
|
+
apiPath: `/v1/connections/${encodeURIComponent(params.connectionId)}/status`,
|
|
2199
|
+
mcpTool: "openwearables_connection_status"
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
withUpstreamProvider(query) {
|
|
2203
|
+
return {
|
|
2204
|
+
...query,
|
|
2205
|
+
...this.upstreamProvider ? { upstreamProvider: this.upstreamProvider } : {}
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
context(params) {
|
|
2209
|
+
return {
|
|
2210
|
+
tenantId: params.tenantId,
|
|
2211
|
+
connectionId: params.connectionId,
|
|
2212
|
+
providerKey: this.providerKey
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
1782
2215
|
}
|
|
1783
2216
|
|
|
1784
|
-
class
|
|
2217
|
+
class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
|
|
1785
2218
|
constructor(options) {
|
|
1786
2219
|
super({
|
|
1787
|
-
|
|
1788
|
-
|
|
2220
|
+
...options,
|
|
2221
|
+
providerKey: "health.apple-health",
|
|
2222
|
+
upstreamProvider: "apple-health"
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
class WhoopHealthProvider extends BaseHealthProvider {
|
|
2228
|
+
constructor(options) {
|
|
2229
|
+
super({
|
|
2230
|
+
providerKey: "health.whoop",
|
|
2231
|
+
apiBaseUrl: options.apiBaseUrl ?? "https://api.prod.whoop.com",
|
|
2232
|
+
webhookSignatureHeader: "x-whoop-signature",
|
|
2233
|
+
oauth: {
|
|
2234
|
+
tokenUrl: options.oauth?.tokenUrl ?? "https://api.prod.whoop.com/oauth/oauth2/token",
|
|
2235
|
+
...options.oauth
|
|
2236
|
+
},
|
|
2237
|
+
...options
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
async listActivities(params) {
|
|
2241
|
+
return this.fetchActivities(params, {
|
|
2242
|
+
apiPath: "/v2/activity/workout",
|
|
2243
|
+
mcpTool: "whoop_list_activities",
|
|
2244
|
+
buildQuery: buildSharedQuery,
|
|
2245
|
+
mapItem: (item, input) => toHealthActivity(item, this.context(input), "workout")
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
async listWorkouts(params) {
|
|
2249
|
+
return this.fetchWorkouts(params, {
|
|
2250
|
+
apiPath: "/v2/activity/workout",
|
|
2251
|
+
mcpTool: "whoop_list_workouts",
|
|
2252
|
+
buildQuery: buildSharedQuery,
|
|
2253
|
+
mapItem: (item, input) => toHealthWorkout(item, this.context(input))
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
async listSleep(params) {
|
|
2257
|
+
return this.fetchSleep(params, {
|
|
2258
|
+
apiPath: "/v2/activity/sleep",
|
|
2259
|
+
mcpTool: "whoop_list_sleep",
|
|
2260
|
+
buildQuery: buildSharedQuery,
|
|
2261
|
+
mapItem: (item, input) => toHealthSleep(item, this.context(input))
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
async listBiometrics(params) {
|
|
2265
|
+
return this.fetchBiometrics(params, {
|
|
2266
|
+
apiPath: "/v2/recovery",
|
|
2267
|
+
mcpTool: "whoop_list_biometrics",
|
|
2268
|
+
buildQuery: withMetricTypes,
|
|
2269
|
+
mapItem: (item, input) => toHealthBiometric(item, this.context(input), "recovery_score")
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
async listNutrition(_params) {
|
|
2273
|
+
throw this.unsupported("nutrition");
|
|
2274
|
+
}
|
|
2275
|
+
async getConnectionStatus(params) {
|
|
2276
|
+
return this.fetchConnectionStatus(params, {
|
|
2277
|
+
apiPath: "/v2/user/profile/basic",
|
|
2278
|
+
mcpTool: "whoop_connection_status"
|
|
1789
2279
|
});
|
|
1790
2280
|
}
|
|
2281
|
+
context(params) {
|
|
2282
|
+
return {
|
|
2283
|
+
tenantId: params.tenantId,
|
|
2284
|
+
connectionId: params.connectionId,
|
|
2285
|
+
providerKey: this.providerKey
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
class OuraHealthProvider extends BaseHealthProvider {
|
|
2291
|
+
constructor(options) {
|
|
2292
|
+
super({
|
|
2293
|
+
providerKey: "health.oura",
|
|
2294
|
+
apiBaseUrl: options.apiBaseUrl ?? "https://api.ouraring.com",
|
|
2295
|
+
webhookSignatureHeader: "x-oura-signature",
|
|
2296
|
+
oauth: {
|
|
2297
|
+
tokenUrl: options.oauth?.tokenUrl ?? "https://api.ouraring.com/oauth/token",
|
|
2298
|
+
...options.oauth
|
|
2299
|
+
},
|
|
2300
|
+
...options
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
async listActivities(params) {
|
|
2304
|
+
return this.fetchActivities(params, {
|
|
2305
|
+
apiPath: "/v2/usercollection/daily_activity",
|
|
2306
|
+
mcpTool: "oura_list_activities",
|
|
2307
|
+
buildQuery: buildSharedQuery,
|
|
2308
|
+
mapItem: (item, input) => toHealthActivity(item, this.context(input))
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
async listWorkouts(params) {
|
|
2312
|
+
return this.fetchWorkouts(params, {
|
|
2313
|
+
apiPath: "/v2/usercollection/workout",
|
|
2314
|
+
mcpTool: "oura_list_workouts",
|
|
2315
|
+
buildQuery: buildSharedQuery,
|
|
2316
|
+
mapItem: (item, input) => toHealthWorkout(item, this.context(input))
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
async listSleep(params) {
|
|
2320
|
+
return this.fetchSleep(params, {
|
|
2321
|
+
apiPath: "/v2/usercollection/sleep",
|
|
2322
|
+
mcpTool: "oura_list_sleep",
|
|
2323
|
+
buildQuery: buildSharedQuery,
|
|
2324
|
+
mapItem: (item, input) => toHealthSleep(item, this.context(input))
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
async listBiometrics(params) {
|
|
2328
|
+
return this.fetchBiometrics(params, {
|
|
2329
|
+
apiPath: "/v2/usercollection/daily_readiness",
|
|
2330
|
+
mcpTool: "oura_list_biometrics",
|
|
2331
|
+
buildQuery: withMetricTypes,
|
|
2332
|
+
mapItem: (item, input) => toHealthBiometric(item, this.context(input), "readiness_score")
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
async listNutrition(_params) {
|
|
2336
|
+
throw this.unsupported("nutrition");
|
|
2337
|
+
}
|
|
2338
|
+
async getConnectionStatus(params) {
|
|
2339
|
+
return this.fetchConnectionStatus(params, {
|
|
2340
|
+
apiPath: "/v2/usercollection/personal_info",
|
|
2341
|
+
mcpTool: "oura_connection_status"
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
context(params) {
|
|
2345
|
+
return {
|
|
2346
|
+
tenantId: params.tenantId,
|
|
2347
|
+
connectionId: params.connectionId,
|
|
2348
|
+
providerKey: this.providerKey
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
class StravaHealthProvider extends BaseHealthProvider {
|
|
2354
|
+
constructor(options) {
|
|
2355
|
+
super({
|
|
2356
|
+
providerKey: "health.strava",
|
|
2357
|
+
apiBaseUrl: options.apiBaseUrl ?? "https://www.strava.com",
|
|
2358
|
+
webhookSignatureHeader: "x-strava-signature",
|
|
2359
|
+
oauth: {
|
|
2360
|
+
tokenUrl: options.oauth?.tokenUrl ?? "https://www.strava.com/oauth/token",
|
|
2361
|
+
...options.oauth
|
|
2362
|
+
},
|
|
2363
|
+
...options
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
async listActivities(params) {
|
|
2367
|
+
return this.fetchActivities(params, {
|
|
2368
|
+
apiPath: "/api/v3/athlete/activities",
|
|
2369
|
+
mcpTool: "strava_list_activities",
|
|
2370
|
+
buildQuery: buildSharedQuery,
|
|
2371
|
+
mapItem: (item, input) => toHealthActivity(item, this.context(input))
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
async listWorkouts(params) {
|
|
2375
|
+
return this.fetchWorkouts(params, {
|
|
2376
|
+
apiPath: "/api/v3/athlete/activities",
|
|
2377
|
+
mcpTool: "strava_list_workouts",
|
|
2378
|
+
buildQuery: buildSharedQuery,
|
|
2379
|
+
mapItem: (item, input) => toHealthWorkout(item, this.context(input))
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
async listSleep(_params) {
|
|
2383
|
+
throw this.unsupported("sleep");
|
|
2384
|
+
}
|
|
2385
|
+
async listBiometrics(_params) {
|
|
2386
|
+
throw this.unsupported("biometrics");
|
|
2387
|
+
}
|
|
2388
|
+
async listNutrition(_params) {
|
|
2389
|
+
throw this.unsupported("nutrition");
|
|
2390
|
+
}
|
|
2391
|
+
async getConnectionStatus(params) {
|
|
2392
|
+
return this.fetchConnectionStatus(params, {
|
|
2393
|
+
apiPath: "/api/v3/athlete",
|
|
2394
|
+
mcpTool: "strava_connection_status"
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
context(params) {
|
|
2398
|
+
return {
|
|
2399
|
+
tenantId: params.tenantId,
|
|
2400
|
+
connectionId: params.connectionId,
|
|
2401
|
+
providerKey: this.providerKey
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
1791
2404
|
}
|
|
1792
2405
|
|
|
1793
2406
|
class FitbitHealthProvider extends BaseHealthProvider {
|
|
1794
2407
|
constructor(options) {
|
|
1795
2408
|
super({
|
|
1796
2409
|
providerKey: "health.fitbit",
|
|
1797
|
-
|
|
2410
|
+
apiBaseUrl: options.apiBaseUrl ?? "https://api.fitbit.com",
|
|
2411
|
+
webhookSignatureHeader: "x-fitbit-signature",
|
|
2412
|
+
oauth: {
|
|
2413
|
+
tokenUrl: options.oauth?.tokenUrl ?? "https://api.fitbit.com/oauth2/token",
|
|
2414
|
+
...options.oauth
|
|
2415
|
+
},
|
|
2416
|
+
...options
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
async listActivities(params) {
|
|
2420
|
+
return this.fetchActivities(params, {
|
|
2421
|
+
apiPath: "/1/user/-/activities/list.json",
|
|
2422
|
+
mcpTool: "fitbit_list_activities",
|
|
2423
|
+
buildQuery: buildSharedQuery,
|
|
2424
|
+
mapItem: (item, input) => toHealthActivity(item, this.context(input))
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
async listWorkouts(params) {
|
|
2428
|
+
return this.fetchWorkouts(params, {
|
|
2429
|
+
apiPath: "/1/user/-/activities/list.json",
|
|
2430
|
+
mcpTool: "fitbit_list_workouts",
|
|
2431
|
+
buildQuery: buildSharedQuery,
|
|
2432
|
+
mapItem: (item, input) => toHealthWorkout(item, this.context(input))
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
async listSleep(params) {
|
|
2436
|
+
return this.fetchSleep(params, {
|
|
2437
|
+
apiPath: "/1.2/user/-/sleep/list.json",
|
|
2438
|
+
mcpTool: "fitbit_list_sleep",
|
|
2439
|
+
buildQuery: buildSharedQuery,
|
|
2440
|
+
mapItem: (item, input) => toHealthSleep(item, this.context(input))
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
async listBiometrics(params) {
|
|
2444
|
+
return this.fetchBiometrics(params, {
|
|
2445
|
+
apiPath: "/1/user/-/body/log/weight/date/today/1m.json",
|
|
2446
|
+
mcpTool: "fitbit_list_biometrics",
|
|
2447
|
+
buildQuery: withMetricTypes,
|
|
2448
|
+
mapItem: (item, input) => toHealthBiometric(item, this.context(input), "weight")
|
|
1798
2449
|
});
|
|
1799
2450
|
}
|
|
2451
|
+
async listNutrition(params) {
|
|
2452
|
+
return this.fetchNutrition(params, {
|
|
2453
|
+
apiPath: "/1/user/-/foods/log/date/today.json",
|
|
2454
|
+
mcpTool: "fitbit_list_nutrition",
|
|
2455
|
+
buildQuery: buildSharedQuery,
|
|
2456
|
+
mapItem: (item, input) => toHealthNutrition(item, this.context(input))
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
async getConnectionStatus(params) {
|
|
2460
|
+
return this.fetchConnectionStatus(params, {
|
|
2461
|
+
apiPath: "/1/user/-/profile.json",
|
|
2462
|
+
mcpTool: "fitbit_connection_status"
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
context(params) {
|
|
2466
|
+
return {
|
|
2467
|
+
tenantId: params.tenantId,
|
|
2468
|
+
connectionId: params.connectionId,
|
|
2469
|
+
providerKey: this.providerKey
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
1800
2472
|
}
|
|
1801
2473
|
|
|
1802
|
-
|
|
2474
|
+
// src/impls/health/hybrid-health-providers.ts
|
|
2475
|
+
var LIMITED_PROVIDER_SLUG = {
|
|
2476
|
+
"health.garmin": "garmin",
|
|
2477
|
+
"health.myfitnesspal": "myfitnesspal",
|
|
2478
|
+
"health.eightsleep": "eightsleep",
|
|
2479
|
+
"health.peloton": "peloton"
|
|
2480
|
+
};
|
|
2481
|
+
function buildSharedQuery2(params) {
|
|
2482
|
+
return {
|
|
2483
|
+
tenantId: params.tenantId,
|
|
2484
|
+
connectionId: params.connectionId,
|
|
2485
|
+
userId: params.userId,
|
|
2486
|
+
from: params.from,
|
|
2487
|
+
to: params.to,
|
|
2488
|
+
cursor: params.cursor,
|
|
2489
|
+
pageSize: params.pageSize
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
class GarminHealthProvider extends OpenWearablesHealthProvider {
|
|
1803
2494
|
constructor(options) {
|
|
1804
2495
|
super({
|
|
2496
|
+
...options,
|
|
2497
|
+
providerKey: "health.garmin",
|
|
2498
|
+
upstreamProvider: "garmin"
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
|
|
2504
|
+
constructor(options) {
|
|
2505
|
+
super({
|
|
2506
|
+
...options,
|
|
1805
2507
|
providerKey: "health.myfitnesspal",
|
|
1806
|
-
|
|
2508
|
+
upstreamProvider: "myfitnesspal"
|
|
1807
2509
|
});
|
|
1808
2510
|
}
|
|
1809
2511
|
}
|
|
1810
2512
|
|
|
1811
|
-
class EightSleepHealthProvider extends
|
|
2513
|
+
class EightSleepHealthProvider extends OpenWearablesHealthProvider {
|
|
1812
2514
|
constructor(options) {
|
|
1813
2515
|
super({
|
|
2516
|
+
...options,
|
|
1814
2517
|
providerKey: "health.eightsleep",
|
|
1815
|
-
|
|
2518
|
+
upstreamProvider: "eightsleep"
|
|
1816
2519
|
});
|
|
1817
2520
|
}
|
|
1818
2521
|
}
|
|
1819
2522
|
|
|
1820
|
-
class PelotonHealthProvider extends
|
|
2523
|
+
class PelotonHealthProvider extends OpenWearablesHealthProvider {
|
|
1821
2524
|
constructor(options) {
|
|
1822
2525
|
super({
|
|
2526
|
+
...options,
|
|
1823
2527
|
providerKey: "health.peloton",
|
|
1824
|
-
|
|
2528
|
+
upstreamProvider: "peloton"
|
|
1825
2529
|
});
|
|
1826
2530
|
}
|
|
1827
2531
|
}
|
|
1828
2532
|
|
|
1829
2533
|
class UnofficialHealthAutomationProvider extends BaseHealthProvider {
|
|
2534
|
+
providerSlugValue;
|
|
1830
2535
|
constructor(options) {
|
|
1831
2536
|
super({
|
|
1832
|
-
...
|
|
1833
|
-
providerKey: options.providerKey
|
|
2537
|
+
...options,
|
|
2538
|
+
providerKey: options.providerKey,
|
|
2539
|
+
webhookSignatureHeader: "x-unofficial-signature"
|
|
1834
2540
|
});
|
|
2541
|
+
this.providerSlugValue = LIMITED_PROVIDER_SLUG[options.providerKey];
|
|
2542
|
+
}
|
|
2543
|
+
async listActivities(params) {
|
|
2544
|
+
return this.fetchActivities(params, {
|
|
2545
|
+
mcpTool: `${this.providerSlugValue}_list_activities`,
|
|
2546
|
+
buildQuery: buildSharedQuery2,
|
|
2547
|
+
mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
async listWorkouts(params) {
|
|
2551
|
+
return this.fetchWorkouts(params, {
|
|
2552
|
+
mcpTool: `${this.providerSlugValue}_list_workouts`,
|
|
2553
|
+
buildQuery: buildSharedQuery2,
|
|
2554
|
+
mapItem: (item, input) => toHealthWorkout(item, this.context(input))
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
async listSleep(params) {
|
|
2558
|
+
return this.fetchSleep(params, {
|
|
2559
|
+
mcpTool: `${this.providerSlugValue}_list_sleep`,
|
|
2560
|
+
buildQuery: buildSharedQuery2,
|
|
2561
|
+
mapItem: (item, input) => toHealthSleep(item, this.context(input))
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
async listBiometrics(params) {
|
|
2565
|
+
return this.fetchBiometrics(params, {
|
|
2566
|
+
mcpTool: `${this.providerSlugValue}_list_biometrics`,
|
|
2567
|
+
buildQuery: (input) => ({
|
|
2568
|
+
...buildSharedQuery2(input),
|
|
2569
|
+
metricTypes: input.metricTypes
|
|
2570
|
+
}),
|
|
2571
|
+
mapItem: (item, input) => toHealthBiometric(item, this.context(input))
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
async listNutrition(params) {
|
|
2575
|
+
return this.fetchNutrition(params, {
|
|
2576
|
+
mcpTool: `${this.providerSlugValue}_list_nutrition`,
|
|
2577
|
+
buildQuery: buildSharedQuery2,
|
|
2578
|
+
mapItem: (item, input) => toHealthNutrition(item, this.context(input))
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
async getConnectionStatus(params) {
|
|
2582
|
+
return this.fetchConnectionStatus(params, {
|
|
2583
|
+
mcpTool: `${this.providerSlugValue}_connection_status`
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
context(params) {
|
|
2587
|
+
return {
|
|
2588
|
+
tenantId: params.tenantId,
|
|
2589
|
+
connectionId: params.connectionId,
|
|
2590
|
+
providerKey: this.providerKey
|
|
2591
|
+
};
|
|
1835
2592
|
}
|
|
1836
2593
|
}
|
|
1837
|
-
|
|
1838
2594
|
// src/impls/health-provider-factory.ts
|
|
1839
2595
|
import {
|
|
1840
2596
|
isUnofficialHealthProviderAllowed,
|
|
1841
2597
|
resolveHealthStrategyOrder
|
|
1842
2598
|
} from "@contractspec/integration.runtime/runtime";
|
|
2599
|
+
var OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER = {
|
|
2600
|
+
"health.openwearables": false,
|
|
2601
|
+
"health.whoop": true,
|
|
2602
|
+
"health.apple-health": false,
|
|
2603
|
+
"health.oura": true,
|
|
2604
|
+
"health.strava": true,
|
|
2605
|
+
"health.garmin": false,
|
|
2606
|
+
"health.fitbit": true,
|
|
2607
|
+
"health.myfitnesspal": false,
|
|
2608
|
+
"health.eightsleep": false,
|
|
2609
|
+
"health.peloton": false
|
|
2610
|
+
};
|
|
2611
|
+
var UNOFFICIAL_SUPPORTED_BY_PROVIDER = {
|
|
2612
|
+
"health.openwearables": false,
|
|
2613
|
+
"health.whoop": false,
|
|
2614
|
+
"health.apple-health": false,
|
|
2615
|
+
"health.oura": false,
|
|
2616
|
+
"health.strava": false,
|
|
2617
|
+
"health.garmin": true,
|
|
2618
|
+
"health.fitbit": false,
|
|
2619
|
+
"health.myfitnesspal": true,
|
|
2620
|
+
"health.eightsleep": true,
|
|
2621
|
+
"health.peloton": true
|
|
2622
|
+
};
|
|
1843
2623
|
function createHealthProviderFromContext(context, secrets) {
|
|
1844
2624
|
const providerKey = context.spec.meta.key;
|
|
1845
2625
|
const config = toFactoryConfig(context.config);
|
|
1846
2626
|
const strategyOrder = buildStrategyOrder(config);
|
|
1847
|
-
const
|
|
1848
|
-
for (
|
|
1849
|
-
const
|
|
2627
|
+
const attemptLogs = [];
|
|
2628
|
+
for (let index = 0;index < strategyOrder.length; index += 1) {
|
|
2629
|
+
const strategy = strategyOrder[index];
|
|
2630
|
+
if (!strategy)
|
|
2631
|
+
continue;
|
|
2632
|
+
const route = index === 0 ? "primary" : "fallback";
|
|
2633
|
+
if (!supportsStrategy(providerKey, strategy)) {
|
|
2634
|
+
attemptLogs.push(`${strategy}: unsupported by ${providerKey}`);
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
if (!hasCredentialsForStrategy(strategy, config, secrets)) {
|
|
2638
|
+
attemptLogs.push(`${strategy}: missing credentials`);
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
const provider = createHealthProviderForStrategy(providerKey, strategy, route, config, secrets);
|
|
1850
2642
|
if (provider) {
|
|
1851
2643
|
return provider;
|
|
1852
2644
|
}
|
|
1853
|
-
|
|
2645
|
+
attemptLogs.push(`${strategy}: not available`);
|
|
1854
2646
|
}
|
|
1855
|
-
throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${
|
|
2647
|
+
throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
|
|
1856
2648
|
}
|
|
1857
|
-
function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
|
|
2649
|
+
function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
|
|
1858
2650
|
const options = {
|
|
1859
2651
|
transport: strategy,
|
|
1860
2652
|
apiBaseUrl: config.apiBaseUrl,
|
|
@@ -1862,10 +2654,21 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
|
|
|
1862
2654
|
apiKey: getSecretString(secrets, "apiKey"),
|
|
1863
2655
|
accessToken: getSecretString(secrets, "accessToken"),
|
|
1864
2656
|
mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
|
|
1865
|
-
webhookSecret: getSecretString(secrets, "webhookSecret")
|
|
2657
|
+
webhookSecret: getSecretString(secrets, "webhookSecret"),
|
|
2658
|
+
route,
|
|
2659
|
+
oauth: {
|
|
2660
|
+
tokenUrl: config.oauthTokenUrl,
|
|
2661
|
+
refreshToken: getSecretString(secrets, "refreshToken"),
|
|
2662
|
+
clientId: getSecretString(secrets, "clientId"),
|
|
2663
|
+
clientSecret: getSecretString(secrets, "clientSecret"),
|
|
2664
|
+
tokenExpiresAt: getSecretString(secrets, "tokenExpiresAt")
|
|
2665
|
+
}
|
|
1866
2666
|
};
|
|
1867
2667
|
if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
|
|
1868
|
-
return
|
|
2668
|
+
return createAggregatorProvider(providerKey, {
|
|
2669
|
+
...options,
|
|
2670
|
+
aggregatorKey: "health.openwearables"
|
|
2671
|
+
});
|
|
1869
2672
|
}
|
|
1870
2673
|
if (strategy === "unofficial") {
|
|
1871
2674
|
if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
|
|
@@ -1887,6 +2690,31 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
|
|
|
1887
2690
|
}
|
|
1888
2691
|
return createOfficialProvider(providerKey, options);
|
|
1889
2692
|
}
|
|
2693
|
+
function createAggregatorProvider(providerKey, options) {
|
|
2694
|
+
if (providerKey === "health.apple-health") {
|
|
2695
|
+
return new AppleHealthBridgeProvider(options);
|
|
2696
|
+
}
|
|
2697
|
+
if (providerKey === "health.garmin") {
|
|
2698
|
+
return new GarminHealthProvider(options);
|
|
2699
|
+
}
|
|
2700
|
+
if (providerKey === "health.myfitnesspal") {
|
|
2701
|
+
return new MyFitnessPalHealthProvider(options);
|
|
2702
|
+
}
|
|
2703
|
+
if (providerKey === "health.eightsleep") {
|
|
2704
|
+
return new EightSleepHealthProvider(options);
|
|
2705
|
+
}
|
|
2706
|
+
if (providerKey === "health.peloton") {
|
|
2707
|
+
return new PelotonHealthProvider(options);
|
|
2708
|
+
}
|
|
2709
|
+
if (providerKey === "health.openwearables") {
|
|
2710
|
+
return new OpenWearablesHealthProvider(options);
|
|
2711
|
+
}
|
|
2712
|
+
return new OpenWearablesHealthProvider({
|
|
2713
|
+
...options,
|
|
2714
|
+
providerKey,
|
|
2715
|
+
upstreamProvider: providerKey.replace("health.", "")
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
1890
2718
|
function createOfficialProvider(providerKey, options) {
|
|
1891
2719
|
switch (providerKey) {
|
|
1892
2720
|
case "health.openwearables":
|
|
@@ -1904,11 +2732,20 @@ function createOfficialProvider(providerKey, options) {
|
|
|
1904
2732
|
case "health.fitbit":
|
|
1905
2733
|
return new FitbitHealthProvider(options);
|
|
1906
2734
|
case "health.myfitnesspal":
|
|
1907
|
-
return new MyFitnessPalHealthProvider(
|
|
2735
|
+
return new MyFitnessPalHealthProvider({
|
|
2736
|
+
...options,
|
|
2737
|
+
transport: "aggregator-api"
|
|
2738
|
+
});
|
|
1908
2739
|
case "health.eightsleep":
|
|
1909
|
-
return new EightSleepHealthProvider(
|
|
2740
|
+
return new EightSleepHealthProvider({
|
|
2741
|
+
...options,
|
|
2742
|
+
transport: "aggregator-api"
|
|
2743
|
+
});
|
|
1910
2744
|
case "health.peloton":
|
|
1911
|
-
return new PelotonHealthProvider(
|
|
2745
|
+
return new PelotonHealthProvider({
|
|
2746
|
+
...options,
|
|
2747
|
+
transport: "aggregator-api"
|
|
2748
|
+
});
|
|
1912
2749
|
default:
|
|
1913
2750
|
throw new Error(`Unsupported health provider key: ${providerKey}`);
|
|
1914
2751
|
}
|
|
@@ -1921,6 +2758,7 @@ function toFactoryConfig(config) {
|
|
|
1921
2758
|
return {
|
|
1922
2759
|
apiBaseUrl: asString(record.apiBaseUrl),
|
|
1923
2760
|
mcpUrl: asString(record.mcpUrl),
|
|
2761
|
+
oauthTokenUrl: asString(record.oauthTokenUrl),
|
|
1924
2762
|
defaultTransport: normalizeTransport(record.defaultTransport),
|
|
1925
2763
|
strategyOrder: normalizeTransportArray(record.strategyOrder),
|
|
1926
2764
|
allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
|
|
@@ -1949,6 +2787,27 @@ function normalizeTransportArray(value) {
|
|
|
1949
2787
|
const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
|
|
1950
2788
|
return transports.length > 0 ? transports : undefined;
|
|
1951
2789
|
}
|
|
2790
|
+
function supportsStrategy(providerKey, strategy) {
|
|
2791
|
+
if (strategy === "official-api" || strategy === "official-mcp") {
|
|
2792
|
+
return OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER[providerKey];
|
|
2793
|
+
}
|
|
2794
|
+
if (strategy === "unofficial") {
|
|
2795
|
+
return UNOFFICIAL_SUPPORTED_BY_PROVIDER[providerKey];
|
|
2796
|
+
}
|
|
2797
|
+
return true;
|
|
2798
|
+
}
|
|
2799
|
+
function hasCredentialsForStrategy(strategy, config, secrets) {
|
|
2800
|
+
const hasApiCredential = Boolean(getSecretString(secrets, "accessToken")) || Boolean(getSecretString(secrets, "apiKey"));
|
|
2801
|
+
const hasMcpCredential = Boolean(getSecretString(secrets, "mcpAccessToken")) || hasApiCredential;
|
|
2802
|
+
if (strategy === "official-api" || strategy === "aggregator-api") {
|
|
2803
|
+
return hasApiCredential;
|
|
2804
|
+
}
|
|
2805
|
+
if (strategy === "official-mcp" || strategy === "aggregator-mcp") {
|
|
2806
|
+
return Boolean(config.mcpUrl) && hasMcpCredential;
|
|
2807
|
+
}
|
|
2808
|
+
const hasAutomationCredential = hasMcpCredential || Boolean(getSecretString(secrets, "username")) && Boolean(getSecretString(secrets, "password"));
|
|
2809
|
+
return Boolean(config.mcpUrl) && hasAutomationCredential;
|
|
2810
|
+
}
|
|
1952
2811
|
function getSecretString(secrets, key) {
|
|
1953
2812
|
const value = secrets[key];
|
|
1954
2813
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
@@ -2224,45 +3083,474 @@ function mapFinishReason(reason) {
|
|
|
2224
3083
|
}
|
|
2225
3084
|
}
|
|
2226
3085
|
|
|
2227
|
-
// src/impls/mistral-embedding.ts
|
|
2228
|
-
import { Mistral as Mistral2 } from "@mistralai/mistralai";
|
|
3086
|
+
// src/impls/mistral-embedding.ts
|
|
3087
|
+
import { Mistral as Mistral2 } from "@mistralai/mistralai";
|
|
3088
|
+
|
|
3089
|
+
class MistralEmbeddingProvider {
|
|
3090
|
+
client;
|
|
3091
|
+
defaultModel;
|
|
3092
|
+
constructor(options) {
|
|
3093
|
+
if (!options.apiKey) {
|
|
3094
|
+
throw new Error("MistralEmbeddingProvider requires an apiKey");
|
|
3095
|
+
}
|
|
3096
|
+
this.client = options.client ?? new Mistral2({
|
|
3097
|
+
apiKey: options.apiKey,
|
|
3098
|
+
serverURL: options.serverURL
|
|
3099
|
+
});
|
|
3100
|
+
this.defaultModel = options.defaultModel ?? "mistral-embed";
|
|
3101
|
+
}
|
|
3102
|
+
async embedDocuments(documents, options) {
|
|
3103
|
+
if (documents.length === 0)
|
|
3104
|
+
return [];
|
|
3105
|
+
const model = options?.model ?? this.defaultModel;
|
|
3106
|
+
const response = await this.client.embeddings.create({
|
|
3107
|
+
model,
|
|
3108
|
+
inputs: documents.map((doc) => doc.text)
|
|
3109
|
+
});
|
|
3110
|
+
return response.data.map((item, index) => ({
|
|
3111
|
+
id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
|
|
3112
|
+
vector: item.embedding ?? [],
|
|
3113
|
+
dimensions: item.embedding?.length ?? 0,
|
|
3114
|
+
model: response.model,
|
|
3115
|
+
metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
3118
|
+
async embedQuery(query, options) {
|
|
3119
|
+
const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
|
|
3120
|
+
if (!result) {
|
|
3121
|
+
throw new Error("Failed to compute embedding for query");
|
|
3122
|
+
}
|
|
3123
|
+
return result;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// src/impls/mistral-stt.ts
|
|
3128
|
+
var DEFAULT_BASE_URL4 = "https://api.mistral.ai/v1";
|
|
3129
|
+
var DEFAULT_MODEL = "voxtral-mini-latest";
|
|
3130
|
+
var AUDIO_MIME_BY_FORMAT = {
|
|
3131
|
+
mp3: "audio/mpeg",
|
|
3132
|
+
wav: "audio/wav",
|
|
3133
|
+
ogg: "audio/ogg",
|
|
3134
|
+
pcm: "audio/pcm",
|
|
3135
|
+
opus: "audio/opus"
|
|
3136
|
+
};
|
|
3137
|
+
|
|
3138
|
+
class MistralSttProvider {
|
|
3139
|
+
apiKey;
|
|
3140
|
+
defaultModel;
|
|
3141
|
+
defaultLanguage;
|
|
3142
|
+
baseUrl;
|
|
3143
|
+
fetchImpl;
|
|
3144
|
+
constructor(options) {
|
|
3145
|
+
if (!options.apiKey) {
|
|
3146
|
+
throw new Error("MistralSttProvider requires an apiKey");
|
|
3147
|
+
}
|
|
3148
|
+
this.apiKey = options.apiKey;
|
|
3149
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
|
|
3150
|
+
this.defaultLanguage = options.defaultLanguage;
|
|
3151
|
+
this.baseUrl = normalizeBaseUrl(options.serverURL ?? DEFAULT_BASE_URL4);
|
|
3152
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
3153
|
+
}
|
|
3154
|
+
async transcribe(input) {
|
|
3155
|
+
const formData = new FormData;
|
|
3156
|
+
const model = input.model ?? this.defaultModel;
|
|
3157
|
+
const mimeType = AUDIO_MIME_BY_FORMAT[input.audio.format] ?? "audio/wav";
|
|
3158
|
+
const fileName = `audio.${input.audio.format}`;
|
|
3159
|
+
const audioBytes = new Uint8Array(input.audio.data);
|
|
3160
|
+
const blob = new Blob([audioBytes], { type: mimeType });
|
|
3161
|
+
formData.append("file", blob, fileName);
|
|
3162
|
+
formData.append("model", model);
|
|
3163
|
+
formData.append("response_format", "verbose_json");
|
|
3164
|
+
const language = input.language ?? this.defaultLanguage;
|
|
3165
|
+
if (language) {
|
|
3166
|
+
formData.append("language", language);
|
|
3167
|
+
}
|
|
3168
|
+
const response = await this.fetchImpl(`${this.baseUrl}/audio/transcriptions`, {
|
|
3169
|
+
method: "POST",
|
|
3170
|
+
headers: {
|
|
3171
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
3172
|
+
},
|
|
3173
|
+
body: formData
|
|
3174
|
+
});
|
|
3175
|
+
if (!response.ok) {
|
|
3176
|
+
const body = await response.text();
|
|
3177
|
+
throw new Error(`Mistral transcription request failed (${response.status}): ${body}`);
|
|
3178
|
+
}
|
|
3179
|
+
const payload = await response.json();
|
|
3180
|
+
return toTranscriptionResult(payload, input);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function toTranscriptionResult(payload, input) {
|
|
3184
|
+
const record = asRecord2(payload);
|
|
3185
|
+
const text = readString3(record, "text") ?? "";
|
|
3186
|
+
const language = readString3(record, "language") ?? input.language ?? "unknown";
|
|
3187
|
+
const segments = parseSegments(record);
|
|
3188
|
+
if (segments.length === 0 && text.length > 0) {
|
|
3189
|
+
segments.push({
|
|
3190
|
+
text,
|
|
3191
|
+
startMs: 0,
|
|
3192
|
+
endMs: input.audio.durationMs ?? 0
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
const durationMs = input.audio.durationMs ?? segments.reduce((max, segment) => Math.max(max, segment.endMs), 0);
|
|
3196
|
+
const topLevelWords = parseWordTimings(record.words);
|
|
3197
|
+
const flattenedWords = segments.flatMap((segment) => segment.wordTimings ?? []);
|
|
3198
|
+
const wordTimings = topLevelWords.length > 0 ? topLevelWords : flattenedWords.length > 0 ? flattenedWords : undefined;
|
|
3199
|
+
const speakers = dedupeSpeakers(segments);
|
|
3200
|
+
return {
|
|
3201
|
+
text,
|
|
3202
|
+
segments,
|
|
3203
|
+
language,
|
|
3204
|
+
durationMs,
|
|
3205
|
+
speakers: speakers.length > 0 ? speakers : undefined,
|
|
3206
|
+
wordTimings
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
function parseSegments(record) {
|
|
3210
|
+
if (!Array.isArray(record.segments)) {
|
|
3211
|
+
return [];
|
|
3212
|
+
}
|
|
3213
|
+
const parsed = [];
|
|
3214
|
+
for (const entry of record.segments) {
|
|
3215
|
+
const segmentRecord = asRecord2(entry);
|
|
3216
|
+
const text = readString3(segmentRecord, "text");
|
|
3217
|
+
if (!text) {
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
const startSeconds = readNumber2(segmentRecord, "start") ?? 0;
|
|
3221
|
+
const endSeconds = readNumber2(segmentRecord, "end") ?? startSeconds;
|
|
3222
|
+
parsed.push({
|
|
3223
|
+
text,
|
|
3224
|
+
startMs: secondsToMs(startSeconds),
|
|
3225
|
+
endMs: secondsToMs(endSeconds),
|
|
3226
|
+
speakerId: readString3(segmentRecord, "speaker") ?? undefined,
|
|
3227
|
+
confidence: readNumber2(segmentRecord, "confidence"),
|
|
3228
|
+
wordTimings: parseWordTimings(segmentRecord.words)
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
return parsed;
|
|
3232
|
+
}
|
|
3233
|
+
function parseWordTimings(value) {
|
|
3234
|
+
if (!Array.isArray(value)) {
|
|
3235
|
+
return [];
|
|
3236
|
+
}
|
|
3237
|
+
const words = [];
|
|
3238
|
+
for (const entry of value) {
|
|
3239
|
+
const wordRecord = asRecord2(entry);
|
|
3240
|
+
const word = readString3(wordRecord, "word");
|
|
3241
|
+
const startSeconds = readNumber2(wordRecord, "start");
|
|
3242
|
+
const endSeconds = readNumber2(wordRecord, "end");
|
|
3243
|
+
if (!word || startSeconds == null || endSeconds == null) {
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
words.push({
|
|
3247
|
+
word,
|
|
3248
|
+
startMs: secondsToMs(startSeconds),
|
|
3249
|
+
endMs: secondsToMs(endSeconds),
|
|
3250
|
+
confidence: readNumber2(wordRecord, "confidence")
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
return words;
|
|
3254
|
+
}
|
|
3255
|
+
function dedupeSpeakers(segments) {
|
|
3256
|
+
const seen = new Set;
|
|
3257
|
+
const speakers = [];
|
|
3258
|
+
for (const segment of segments) {
|
|
3259
|
+
if (!segment.speakerId || seen.has(segment.speakerId)) {
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
seen.add(segment.speakerId);
|
|
3263
|
+
speakers.push({
|
|
3264
|
+
id: segment.speakerId,
|
|
3265
|
+
name: segment.speakerName
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
return speakers;
|
|
3269
|
+
}
|
|
3270
|
+
function normalizeBaseUrl(url) {
|
|
3271
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
3272
|
+
}
|
|
3273
|
+
function asRecord2(value) {
|
|
3274
|
+
if (value && typeof value === "object") {
|
|
3275
|
+
return value;
|
|
3276
|
+
}
|
|
3277
|
+
return {};
|
|
3278
|
+
}
|
|
3279
|
+
function readString3(record, key) {
|
|
3280
|
+
const value = record[key];
|
|
3281
|
+
return typeof value === "string" ? value : undefined;
|
|
3282
|
+
}
|
|
3283
|
+
function readNumber2(record, key) {
|
|
3284
|
+
const value = record[key];
|
|
3285
|
+
return typeof value === "number" ? value : undefined;
|
|
3286
|
+
}
|
|
3287
|
+
function secondsToMs(value) {
|
|
3288
|
+
return Math.round(value * 1000);
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
// src/impls/mistral-conversational.session.ts
|
|
3292
|
+
class MistralConversationSession {
|
|
3293
|
+
events;
|
|
3294
|
+
queue = new AsyncEventQueue;
|
|
3295
|
+
turns = [];
|
|
3296
|
+
history = [];
|
|
3297
|
+
sessionId = crypto.randomUUID();
|
|
3298
|
+
startedAt = Date.now();
|
|
3299
|
+
sessionConfig;
|
|
3300
|
+
defaultModel;
|
|
3301
|
+
complete;
|
|
3302
|
+
sttProvider;
|
|
3303
|
+
pending = Promise.resolve();
|
|
3304
|
+
closed = false;
|
|
3305
|
+
closedSummary;
|
|
3306
|
+
constructor(options) {
|
|
3307
|
+
this.sessionConfig = options.sessionConfig;
|
|
3308
|
+
this.defaultModel = options.defaultModel;
|
|
3309
|
+
this.complete = options.complete;
|
|
3310
|
+
this.sttProvider = options.sttProvider;
|
|
3311
|
+
this.events = this.queue;
|
|
3312
|
+
this.queue.push({
|
|
3313
|
+
type: "session_started",
|
|
3314
|
+
sessionId: this.sessionId
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
sendAudio(chunk) {
|
|
3318
|
+
if (this.closed) {
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
this.pending = this.pending.then(async () => {
|
|
3322
|
+
const transcription = await this.sttProvider.transcribe({
|
|
3323
|
+
audio: {
|
|
3324
|
+
data: chunk,
|
|
3325
|
+
format: this.sessionConfig.inputFormat ?? "pcm",
|
|
3326
|
+
sampleRateHz: 16000
|
|
3327
|
+
},
|
|
3328
|
+
language: this.sessionConfig.language
|
|
3329
|
+
});
|
|
3330
|
+
const transcriptText = transcription.text.trim();
|
|
3331
|
+
if (transcriptText.length > 0) {
|
|
3332
|
+
await this.handleUserText(transcriptText);
|
|
3333
|
+
}
|
|
3334
|
+
}).catch((error) => {
|
|
3335
|
+
this.emitError(error);
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
sendText(text) {
|
|
3339
|
+
if (this.closed) {
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
const normalized = text.trim();
|
|
3343
|
+
if (normalized.length === 0) {
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
this.pending = this.pending.then(() => this.handleUserText(normalized)).catch((error) => {
|
|
3347
|
+
this.emitError(error);
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
interrupt() {
|
|
3351
|
+
if (this.closed) {
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
this.queue.push({
|
|
3355
|
+
type: "error",
|
|
3356
|
+
error: new Error("Interrupt is not supported for non-streaming sessions.")
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
async close() {
|
|
3360
|
+
if (this.closedSummary) {
|
|
3361
|
+
return this.closedSummary;
|
|
3362
|
+
}
|
|
3363
|
+
this.closed = true;
|
|
3364
|
+
await this.pending;
|
|
3365
|
+
const durationMs = Date.now() - this.startedAt;
|
|
3366
|
+
const summary = {
|
|
3367
|
+
sessionId: this.sessionId,
|
|
3368
|
+
durationMs,
|
|
3369
|
+
turns: this.turns.map((turn) => ({
|
|
3370
|
+
role: turn.role === "assistant" ? "agent" : turn.role,
|
|
3371
|
+
text: turn.text,
|
|
3372
|
+
startMs: turn.startMs,
|
|
3373
|
+
endMs: turn.endMs
|
|
3374
|
+
})),
|
|
3375
|
+
transcript: this.turns.map((turn) => `${turn.role}: ${turn.text}`).join(`
|
|
3376
|
+
`)
|
|
3377
|
+
};
|
|
3378
|
+
this.closedSummary = summary;
|
|
3379
|
+
this.queue.push({
|
|
3380
|
+
type: "session_ended",
|
|
3381
|
+
reason: "closed_by_client",
|
|
3382
|
+
durationMs
|
|
3383
|
+
});
|
|
3384
|
+
this.queue.close();
|
|
3385
|
+
return summary;
|
|
3386
|
+
}
|
|
3387
|
+
async handleUserText(text) {
|
|
3388
|
+
if (this.closed) {
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
const userStart = Date.now();
|
|
3392
|
+
this.queue.push({ type: "user_speech_started" });
|
|
3393
|
+
this.queue.push({ type: "user_speech_ended", transcript: text });
|
|
3394
|
+
this.queue.push({
|
|
3395
|
+
type: "transcript",
|
|
3396
|
+
role: "user",
|
|
3397
|
+
text,
|
|
3398
|
+
timestamp: userStart
|
|
3399
|
+
});
|
|
3400
|
+
this.turns.push({
|
|
3401
|
+
role: "user",
|
|
3402
|
+
text,
|
|
3403
|
+
startMs: userStart,
|
|
3404
|
+
endMs: Date.now()
|
|
3405
|
+
});
|
|
3406
|
+
this.history.push({ role: "user", content: text });
|
|
3407
|
+
const assistantStart = Date.now();
|
|
3408
|
+
const assistantText = await this.complete(this.history, {
|
|
3409
|
+
...this.sessionConfig,
|
|
3410
|
+
llmModel: this.sessionConfig.llmModel ?? this.defaultModel
|
|
3411
|
+
});
|
|
3412
|
+
if (this.closed) {
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
const normalizedAssistantText = assistantText.trim();
|
|
3416
|
+
const finalAssistantText = normalizedAssistantText.length > 0 ? normalizedAssistantText : "I was unable to produce a response.";
|
|
3417
|
+
this.queue.push({
|
|
3418
|
+
type: "agent_speech_started",
|
|
3419
|
+
text: finalAssistantText
|
|
3420
|
+
});
|
|
3421
|
+
this.queue.push({
|
|
3422
|
+
type: "transcript",
|
|
3423
|
+
role: "agent",
|
|
3424
|
+
text: finalAssistantText,
|
|
3425
|
+
timestamp: assistantStart
|
|
3426
|
+
});
|
|
3427
|
+
this.queue.push({ type: "agent_speech_ended" });
|
|
3428
|
+
this.turns.push({
|
|
3429
|
+
role: "assistant",
|
|
3430
|
+
text: finalAssistantText,
|
|
3431
|
+
startMs: assistantStart,
|
|
3432
|
+
endMs: Date.now()
|
|
3433
|
+
});
|
|
3434
|
+
this.history.push({ role: "assistant", content: finalAssistantText });
|
|
3435
|
+
}
|
|
3436
|
+
emitError(error) {
|
|
3437
|
+
if (this.closed) {
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
this.queue.push({ type: "error", error: toError(error) });
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
function toError(error) {
|
|
3444
|
+
if (error instanceof Error) {
|
|
3445
|
+
return error;
|
|
3446
|
+
}
|
|
3447
|
+
return new Error(String(error));
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
// src/impls/mistral-conversational.ts
|
|
3451
|
+
var DEFAULT_BASE_URL5 = "https://api.mistral.ai/v1";
|
|
3452
|
+
var DEFAULT_MODEL2 = "mistral-small-latest";
|
|
3453
|
+
var DEFAULT_VOICE = "default";
|
|
2229
3454
|
|
|
2230
|
-
class
|
|
2231
|
-
|
|
3455
|
+
class MistralConversationalProvider {
|
|
3456
|
+
apiKey;
|
|
2232
3457
|
defaultModel;
|
|
3458
|
+
defaultVoiceId;
|
|
3459
|
+
baseUrl;
|
|
3460
|
+
fetchImpl;
|
|
3461
|
+
sttProvider;
|
|
2233
3462
|
constructor(options) {
|
|
2234
3463
|
if (!options.apiKey) {
|
|
2235
|
-
throw new Error("
|
|
3464
|
+
throw new Error("MistralConversationalProvider requires an apiKey");
|
|
2236
3465
|
}
|
|
2237
|
-
this.
|
|
3466
|
+
this.apiKey = options.apiKey;
|
|
3467
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
|
|
3468
|
+
this.defaultVoiceId = options.defaultVoiceId ?? DEFAULT_VOICE;
|
|
3469
|
+
this.baseUrl = normalizeBaseUrl2(options.serverURL ?? DEFAULT_BASE_URL5);
|
|
3470
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
3471
|
+
this.sttProvider = options.sttProvider ?? new MistralSttProvider({
|
|
2238
3472
|
apiKey: options.apiKey,
|
|
2239
|
-
|
|
3473
|
+
defaultModel: options.sttOptions?.defaultModel,
|
|
3474
|
+
defaultLanguage: options.sttOptions?.defaultLanguage,
|
|
3475
|
+
serverURL: options.sttOptions?.serverURL ?? options.serverURL,
|
|
3476
|
+
fetchImpl: this.fetchImpl
|
|
2240
3477
|
});
|
|
2241
|
-
this.defaultModel = options.defaultModel ?? "mistral-embed";
|
|
2242
3478
|
}
|
|
2243
|
-
async
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
3479
|
+
async startSession(config) {
|
|
3480
|
+
return new MistralConversationSession({
|
|
3481
|
+
sessionConfig: {
|
|
3482
|
+
...config,
|
|
3483
|
+
voiceId: config.voiceId || this.defaultVoiceId
|
|
3484
|
+
},
|
|
3485
|
+
defaultModel: this.defaultModel,
|
|
3486
|
+
complete: (history, sessionConfig) => this.completeConversation(history, sessionConfig),
|
|
3487
|
+
sttProvider: this.sttProvider
|
|
2250
3488
|
});
|
|
2251
|
-
return response.data.map((item, index) => ({
|
|
2252
|
-
id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
|
|
2253
|
-
vector: item.embedding ?? [],
|
|
2254
|
-
dimensions: item.embedding?.length ?? 0,
|
|
2255
|
-
model: response.model,
|
|
2256
|
-
metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
|
|
2257
|
-
}));
|
|
2258
3489
|
}
|
|
2259
|
-
async
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
3490
|
+
async listVoices() {
|
|
3491
|
+
return [
|
|
3492
|
+
{
|
|
3493
|
+
id: this.defaultVoiceId,
|
|
3494
|
+
name: "Mistral Default Voice",
|
|
3495
|
+
description: "Default conversational voice profile.",
|
|
3496
|
+
capabilities: ["conversational"]
|
|
3497
|
+
}
|
|
3498
|
+
];
|
|
3499
|
+
}
|
|
3500
|
+
async completeConversation(history, sessionConfig) {
|
|
3501
|
+
const model = sessionConfig.llmModel ?? this.defaultModel;
|
|
3502
|
+
const messages = [];
|
|
3503
|
+
if (sessionConfig.systemPrompt) {
|
|
3504
|
+
messages.push({ role: "system", content: sessionConfig.systemPrompt });
|
|
2263
3505
|
}
|
|
2264
|
-
|
|
3506
|
+
for (const item of history) {
|
|
3507
|
+
messages.push({ role: item.role, content: item.content });
|
|
3508
|
+
}
|
|
3509
|
+
const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
|
|
3510
|
+
method: "POST",
|
|
3511
|
+
headers: {
|
|
3512
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
3513
|
+
"Content-Type": "application/json"
|
|
3514
|
+
},
|
|
3515
|
+
body: JSON.stringify({
|
|
3516
|
+
model,
|
|
3517
|
+
messages
|
|
3518
|
+
})
|
|
3519
|
+
});
|
|
3520
|
+
if (!response.ok) {
|
|
3521
|
+
const body = await response.text();
|
|
3522
|
+
throw new Error(`Mistral conversational request failed (${response.status}): ${body}`);
|
|
3523
|
+
}
|
|
3524
|
+
const payload = await response.json();
|
|
3525
|
+
return readAssistantText(payload);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
function normalizeBaseUrl2(url) {
|
|
3529
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
3530
|
+
}
|
|
3531
|
+
function readAssistantText(payload) {
|
|
3532
|
+
const record = asRecord3(payload);
|
|
3533
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
3534
|
+
const firstChoice = asRecord3(choices[0]);
|
|
3535
|
+
const message = asRecord3(firstChoice.message);
|
|
3536
|
+
if (typeof message.content === "string") {
|
|
3537
|
+
return message.content;
|
|
3538
|
+
}
|
|
3539
|
+
if (Array.isArray(message.content)) {
|
|
3540
|
+
const textParts = message.content.map((part) => {
|
|
3541
|
+
const entry = asRecord3(part);
|
|
3542
|
+
const text = entry.text;
|
|
3543
|
+
return typeof text === "string" ? text : "";
|
|
3544
|
+
}).filter((text) => text.length > 0);
|
|
3545
|
+
return textParts.join("");
|
|
2265
3546
|
}
|
|
3547
|
+
return "";
|
|
3548
|
+
}
|
|
3549
|
+
function asRecord3(value) {
|
|
3550
|
+
if (value && typeof value === "object") {
|
|
3551
|
+
return value;
|
|
3552
|
+
}
|
|
3553
|
+
return {};
|
|
2266
3554
|
}
|
|
2267
3555
|
|
|
2268
3556
|
// src/impls/qdrant-vector.ts
|
|
@@ -2664,7 +3952,7 @@ function distanceToScore(distance, metric) {
|
|
|
2664
3952
|
|
|
2665
3953
|
// src/impls/stripe-payments.ts
|
|
2666
3954
|
import Stripe from "stripe";
|
|
2667
|
-
var API_VERSION = "2026-
|
|
3955
|
+
var API_VERSION = "2026-02-25.clover";
|
|
2668
3956
|
|
|
2669
3957
|
class StripePaymentsProvider {
|
|
2670
3958
|
stripe;
|
|
@@ -3329,6 +4617,318 @@ function mapStatus(status) {
|
|
|
3329
4617
|
}
|
|
3330
4618
|
}
|
|
3331
4619
|
|
|
4620
|
+
// src/impls/messaging-slack.ts
|
|
4621
|
+
class SlackMessagingProvider {
|
|
4622
|
+
botToken;
|
|
4623
|
+
defaultChannelId;
|
|
4624
|
+
apiBaseUrl;
|
|
4625
|
+
constructor(options) {
|
|
4626
|
+
this.botToken = options.botToken;
|
|
4627
|
+
this.defaultChannelId = options.defaultChannelId;
|
|
4628
|
+
this.apiBaseUrl = options.apiBaseUrl ?? "https://slack.com/api";
|
|
4629
|
+
}
|
|
4630
|
+
async sendMessage(input) {
|
|
4631
|
+
const channel = input.channelId ?? input.recipientId ?? this.defaultChannelId;
|
|
4632
|
+
if (!channel) {
|
|
4633
|
+
throw new Error("Slack sendMessage requires channelId, recipientId, or defaultChannelId.");
|
|
4634
|
+
}
|
|
4635
|
+
const payload = {
|
|
4636
|
+
channel,
|
|
4637
|
+
text: input.text,
|
|
4638
|
+
mrkdwn: input.markdown ?? true,
|
|
4639
|
+
thread_ts: input.threadId
|
|
4640
|
+
};
|
|
4641
|
+
const response = await fetch(`${this.apiBaseUrl}/chat.postMessage`, {
|
|
4642
|
+
method: "POST",
|
|
4643
|
+
headers: {
|
|
4644
|
+
authorization: `Bearer ${this.botToken}`,
|
|
4645
|
+
"content-type": "application/json"
|
|
4646
|
+
},
|
|
4647
|
+
body: JSON.stringify(payload)
|
|
4648
|
+
});
|
|
4649
|
+
const body = await response.json();
|
|
4650
|
+
if (!response.ok || !body.ok || !body.ts) {
|
|
4651
|
+
throw new Error(`Slack sendMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
|
|
4652
|
+
}
|
|
4653
|
+
return {
|
|
4654
|
+
id: `slack:${body.channel ?? channel}:${body.ts}`,
|
|
4655
|
+
providerMessageId: body.ts,
|
|
4656
|
+
status: "sent",
|
|
4657
|
+
sentAt: new Date,
|
|
4658
|
+
metadata: {
|
|
4659
|
+
channelId: body.channel ?? channel
|
|
4660
|
+
}
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
async updateMessage(messageId, input) {
|
|
4664
|
+
const channel = input.channelId ?? this.defaultChannelId;
|
|
4665
|
+
if (!channel) {
|
|
4666
|
+
throw new Error("Slack updateMessage requires channelId or defaultChannelId.");
|
|
4667
|
+
}
|
|
4668
|
+
const response = await fetch(`${this.apiBaseUrl}/chat.update`, {
|
|
4669
|
+
method: "POST",
|
|
4670
|
+
headers: {
|
|
4671
|
+
authorization: `Bearer ${this.botToken}`,
|
|
4672
|
+
"content-type": "application/json"
|
|
4673
|
+
},
|
|
4674
|
+
body: JSON.stringify({
|
|
4675
|
+
channel,
|
|
4676
|
+
ts: messageId,
|
|
4677
|
+
text: input.text,
|
|
4678
|
+
mrkdwn: input.markdown ?? true
|
|
4679
|
+
})
|
|
4680
|
+
});
|
|
4681
|
+
const body = await response.json();
|
|
4682
|
+
if (!response.ok || !body.ok || !body.ts) {
|
|
4683
|
+
throw new Error(`Slack updateMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
|
|
4684
|
+
}
|
|
4685
|
+
return {
|
|
4686
|
+
id: `slack:${body.channel ?? channel}:${body.ts}`,
|
|
4687
|
+
providerMessageId: body.ts,
|
|
4688
|
+
status: "sent",
|
|
4689
|
+
sentAt: new Date,
|
|
4690
|
+
metadata: {
|
|
4691
|
+
channelId: body.channel ?? channel
|
|
4692
|
+
}
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
// src/impls/messaging-github.ts
|
|
4698
|
+
class GithubMessagingProvider {
|
|
4699
|
+
token;
|
|
4700
|
+
defaultOwner;
|
|
4701
|
+
defaultRepo;
|
|
4702
|
+
apiBaseUrl;
|
|
4703
|
+
constructor(options) {
|
|
4704
|
+
this.token = options.token;
|
|
4705
|
+
this.defaultOwner = options.defaultOwner;
|
|
4706
|
+
this.defaultRepo = options.defaultRepo;
|
|
4707
|
+
this.apiBaseUrl = options.apiBaseUrl ?? "https://api.github.com";
|
|
4708
|
+
}
|
|
4709
|
+
async sendMessage(input) {
|
|
4710
|
+
const target = this.resolveTarget(input);
|
|
4711
|
+
const response = await fetch(`${this.apiBaseUrl}/repos/${target.owner}/${target.repo}/issues/${target.issueNumber}/comments`, {
|
|
4712
|
+
method: "POST",
|
|
4713
|
+
headers: {
|
|
4714
|
+
authorization: `Bearer ${this.token}`,
|
|
4715
|
+
accept: "application/vnd.github+json",
|
|
4716
|
+
"content-type": "application/json"
|
|
4717
|
+
},
|
|
4718
|
+
body: JSON.stringify({ body: input.text })
|
|
4719
|
+
});
|
|
4720
|
+
const body = await response.json();
|
|
4721
|
+
if (!response.ok || !body.id) {
|
|
4722
|
+
throw new Error(`GitHub sendMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
|
|
4723
|
+
}
|
|
4724
|
+
return {
|
|
4725
|
+
id: String(body.id),
|
|
4726
|
+
providerMessageId: body.node_id,
|
|
4727
|
+
status: "sent",
|
|
4728
|
+
sentAt: new Date,
|
|
4729
|
+
metadata: {
|
|
4730
|
+
url: body.html_url ?? "",
|
|
4731
|
+
owner: target.owner,
|
|
4732
|
+
repo: target.repo,
|
|
4733
|
+
issueNumber: String(target.issueNumber)
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
}
|
|
4737
|
+
async updateMessage(messageId, input) {
|
|
4738
|
+
const owner = input.metadata?.owner ?? this.defaultOwner;
|
|
4739
|
+
const repo = input.metadata?.repo ?? this.defaultRepo;
|
|
4740
|
+
if (!owner || !repo) {
|
|
4741
|
+
throw new Error("GitHub updateMessage requires owner and repo metadata.");
|
|
4742
|
+
}
|
|
4743
|
+
const response = await fetch(`${this.apiBaseUrl}/repos/${owner}/${repo}/issues/comments/${messageId}`, {
|
|
4744
|
+
method: "PATCH",
|
|
4745
|
+
headers: {
|
|
4746
|
+
authorization: `Bearer ${this.token}`,
|
|
4747
|
+
accept: "application/vnd.github+json",
|
|
4748
|
+
"content-type": "application/json"
|
|
4749
|
+
},
|
|
4750
|
+
body: JSON.stringify({ body: input.text })
|
|
4751
|
+
});
|
|
4752
|
+
const body = await response.json();
|
|
4753
|
+
if (!response.ok || !body.id) {
|
|
4754
|
+
throw new Error(`GitHub updateMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
|
|
4755
|
+
}
|
|
4756
|
+
return {
|
|
4757
|
+
id: String(body.id),
|
|
4758
|
+
providerMessageId: body.node_id,
|
|
4759
|
+
status: "sent",
|
|
4760
|
+
sentAt: new Date,
|
|
4761
|
+
metadata: {
|
|
4762
|
+
url: body.html_url ?? "",
|
|
4763
|
+
owner,
|
|
4764
|
+
repo
|
|
4765
|
+
}
|
|
4766
|
+
};
|
|
4767
|
+
}
|
|
4768
|
+
resolveTarget(input) {
|
|
4769
|
+
const parsedRecipient = parseRecipient(input.recipientId);
|
|
4770
|
+
const owner = parsedRecipient?.owner ?? this.defaultOwner;
|
|
4771
|
+
const repo = parsedRecipient?.repo ?? this.defaultRepo;
|
|
4772
|
+
const issueNumber = parsedRecipient?.issueNumber ?? parseIssueNumber(input.threadId);
|
|
4773
|
+
if (!owner || !repo || issueNumber == null) {
|
|
4774
|
+
throw new Error("GitHub sendMessage requires owner/repo and issueNumber (use recipientId like owner/repo#123 or provide defaults + threadId).");
|
|
4775
|
+
}
|
|
4776
|
+
return {
|
|
4777
|
+
owner,
|
|
4778
|
+
repo,
|
|
4779
|
+
issueNumber
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
function parseRecipient(value) {
|
|
4784
|
+
if (!value)
|
|
4785
|
+
return null;
|
|
4786
|
+
const match = value.trim().match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
4787
|
+
if (!match)
|
|
4788
|
+
return null;
|
|
4789
|
+
const owner = match[1];
|
|
4790
|
+
const repo = match[2];
|
|
4791
|
+
const issueNumber = Number(match[3]);
|
|
4792
|
+
if (!owner || !repo || !Number.isInteger(issueNumber)) {
|
|
4793
|
+
return null;
|
|
4794
|
+
}
|
|
4795
|
+
return { owner, repo, issueNumber };
|
|
4796
|
+
}
|
|
4797
|
+
function parseIssueNumber(value) {
|
|
4798
|
+
if (!value)
|
|
4799
|
+
return null;
|
|
4800
|
+
const numeric = Number(value);
|
|
4801
|
+
return Number.isInteger(numeric) ? numeric : null;
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
// src/impls/messaging-whatsapp-meta.ts
|
|
4805
|
+
class MetaWhatsappMessagingProvider {
|
|
4806
|
+
accessToken;
|
|
4807
|
+
phoneNumberId;
|
|
4808
|
+
apiVersion;
|
|
4809
|
+
constructor(options) {
|
|
4810
|
+
this.accessToken = options.accessToken;
|
|
4811
|
+
this.phoneNumberId = options.phoneNumberId;
|
|
4812
|
+
this.apiVersion = options.apiVersion ?? "v22.0";
|
|
4813
|
+
}
|
|
4814
|
+
async sendMessage(input) {
|
|
4815
|
+
const to = input.recipientId;
|
|
4816
|
+
if (!to) {
|
|
4817
|
+
throw new Error("Meta WhatsApp sendMessage requires recipientId.");
|
|
4818
|
+
}
|
|
4819
|
+
const response = await fetch(`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`, {
|
|
4820
|
+
method: "POST",
|
|
4821
|
+
headers: {
|
|
4822
|
+
authorization: `Bearer ${this.accessToken}`,
|
|
4823
|
+
"content-type": "application/json"
|
|
4824
|
+
},
|
|
4825
|
+
body: JSON.stringify({
|
|
4826
|
+
messaging_product: "whatsapp",
|
|
4827
|
+
to,
|
|
4828
|
+
type: "text",
|
|
4829
|
+
text: {
|
|
4830
|
+
body: input.text,
|
|
4831
|
+
preview_url: false
|
|
4832
|
+
}
|
|
4833
|
+
})
|
|
4834
|
+
});
|
|
4835
|
+
const body = await response.json();
|
|
4836
|
+
const messageId = body.messages?.[0]?.id;
|
|
4837
|
+
if (!response.ok || !messageId) {
|
|
4838
|
+
const errorCode = body.error?.code != null ? String(body.error.code) : "";
|
|
4839
|
+
throw new Error(`Meta WhatsApp sendMessage failed: ${body.error?.message ?? `HTTP_${response.status}`}${errorCode ? ` (${errorCode})` : ""}`);
|
|
4840
|
+
}
|
|
4841
|
+
return {
|
|
4842
|
+
id: messageId,
|
|
4843
|
+
providerMessageId: messageId,
|
|
4844
|
+
status: "sent",
|
|
4845
|
+
sentAt: new Date,
|
|
4846
|
+
metadata: {
|
|
4847
|
+
phoneNumberId: this.phoneNumberId
|
|
4848
|
+
}
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
|
|
4853
|
+
// src/impls/messaging-whatsapp-twilio.ts
|
|
4854
|
+
import { Buffer as Buffer4 } from "buffer";
|
|
4855
|
+
|
|
4856
|
+
class TwilioWhatsappMessagingProvider {
|
|
4857
|
+
accountSid;
|
|
4858
|
+
authToken;
|
|
4859
|
+
fromNumber;
|
|
4860
|
+
constructor(options) {
|
|
4861
|
+
this.accountSid = options.accountSid;
|
|
4862
|
+
this.authToken = options.authToken;
|
|
4863
|
+
this.fromNumber = options.fromNumber;
|
|
4864
|
+
}
|
|
4865
|
+
async sendMessage(input) {
|
|
4866
|
+
const to = normalizeWhatsappAddress(input.recipientId);
|
|
4867
|
+
const from = normalizeWhatsappAddress(input.channelId ?? this.fromNumber);
|
|
4868
|
+
if (!to) {
|
|
4869
|
+
throw new Error("Twilio WhatsApp sendMessage requires recipientId.");
|
|
4870
|
+
}
|
|
4871
|
+
if (!from) {
|
|
4872
|
+
throw new Error("Twilio WhatsApp sendMessage requires channelId or configured fromNumber.");
|
|
4873
|
+
}
|
|
4874
|
+
const params = new URLSearchParams;
|
|
4875
|
+
params.set("To", to);
|
|
4876
|
+
params.set("From", from);
|
|
4877
|
+
params.set("Body", input.text);
|
|
4878
|
+
const auth = Buffer4.from(`${this.accountSid}:${this.authToken}`).toString("base64");
|
|
4879
|
+
const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`, {
|
|
4880
|
+
method: "POST",
|
|
4881
|
+
headers: {
|
|
4882
|
+
authorization: `Basic ${auth}`,
|
|
4883
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
4884
|
+
},
|
|
4885
|
+
body: params.toString()
|
|
4886
|
+
});
|
|
4887
|
+
const body = await response.json();
|
|
4888
|
+
if (!response.ok || !body.sid) {
|
|
4889
|
+
throw new Error(`Twilio WhatsApp sendMessage failed: ${body.error_message ?? `HTTP_${response.status}`}`);
|
|
4890
|
+
}
|
|
4891
|
+
return {
|
|
4892
|
+
id: body.sid,
|
|
4893
|
+
providerMessageId: body.sid,
|
|
4894
|
+
status: mapTwilioStatus(body.status),
|
|
4895
|
+
sentAt: new Date,
|
|
4896
|
+
errorCode: body.error_code != null ? String(body.error_code) : undefined,
|
|
4897
|
+
errorMessage: body.error_message ?? undefined,
|
|
4898
|
+
metadata: {
|
|
4899
|
+
from,
|
|
4900
|
+
to
|
|
4901
|
+
}
|
|
4902
|
+
};
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
function normalizeWhatsappAddress(value) {
|
|
4906
|
+
if (!value)
|
|
4907
|
+
return null;
|
|
4908
|
+
if (value.startsWith("whatsapp:"))
|
|
4909
|
+
return value;
|
|
4910
|
+
return `whatsapp:${value}`;
|
|
4911
|
+
}
|
|
4912
|
+
function mapTwilioStatus(status) {
|
|
4913
|
+
switch (status) {
|
|
4914
|
+
case "queued":
|
|
4915
|
+
case "accepted":
|
|
4916
|
+
case "scheduled":
|
|
4917
|
+
return "queued";
|
|
4918
|
+
case "sending":
|
|
4919
|
+
return "sending";
|
|
4920
|
+
case "delivered":
|
|
4921
|
+
return "delivered";
|
|
4922
|
+
case "failed":
|
|
4923
|
+
case "undelivered":
|
|
4924
|
+
case "canceled":
|
|
4925
|
+
return "failed";
|
|
4926
|
+
case "sent":
|
|
4927
|
+
default:
|
|
4928
|
+
return "sent";
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
|
|
3332
4932
|
// src/impls/powens-client.ts
|
|
3333
4933
|
import { URL as URL2 } from "url";
|
|
3334
4934
|
var POWENS_BASE_URL = {
|
|
@@ -3835,7 +5435,7 @@ function resolveLabelIds(defaults, tags) {
|
|
|
3835
5435
|
}
|
|
3836
5436
|
|
|
3837
5437
|
// src/impls/jira.ts
|
|
3838
|
-
import { Buffer as
|
|
5438
|
+
import { Buffer as Buffer5 } from "buffer";
|
|
3839
5439
|
|
|
3840
5440
|
class JiraProjectManagementProvider {
|
|
3841
5441
|
siteUrl;
|
|
@@ -3912,7 +5512,7 @@ function normalizeSiteUrl(siteUrl) {
|
|
|
3912
5512
|
return siteUrl.replace(/\/$/, "");
|
|
3913
5513
|
}
|
|
3914
5514
|
function buildAuthHeader(email, apiToken) {
|
|
3915
|
-
const token =
|
|
5515
|
+
const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
|
|
3916
5516
|
return `Basic ${token}`;
|
|
3917
5517
|
}
|
|
3918
5518
|
function resolveIssueType(type, defaults) {
|
|
@@ -4115,7 +5715,7 @@ function buildParagraphBlocks(text) {
|
|
|
4115
5715
|
}
|
|
4116
5716
|
|
|
4117
5717
|
// src/impls/tldv-meeting-recorder.ts
|
|
4118
|
-
var
|
|
5718
|
+
var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
|
|
4119
5719
|
|
|
4120
5720
|
class TldvMeetingRecorderProvider {
|
|
4121
5721
|
apiKey;
|
|
@@ -4123,7 +5723,7 @@ class TldvMeetingRecorderProvider {
|
|
|
4123
5723
|
defaultPageSize;
|
|
4124
5724
|
constructor(options) {
|
|
4125
5725
|
this.apiKey = options.apiKey;
|
|
4126
|
-
this.baseUrl = options.baseUrl ??
|
|
5726
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
|
|
4127
5727
|
this.defaultPageSize = options.pageSize;
|
|
4128
5728
|
}
|
|
4129
5729
|
async listMeetings(params) {
|
|
@@ -4258,10 +5858,30 @@ async function safeReadError4(response) {
|
|
|
4258
5858
|
}
|
|
4259
5859
|
|
|
4260
5860
|
// src/impls/provider-factory.ts
|
|
4261
|
-
import { Buffer as
|
|
5861
|
+
import { Buffer as Buffer6 } from "buffer";
|
|
5862
|
+
import { resolveIntegrationRequestContext } from "@contractspec/lib.contracts-integrations/integrations/runtime";
|
|
5863
|
+
import { buildAuthHeaders } from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
|
|
5864
|
+
import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
|
|
4262
5865
|
var SECRET_CACHE = new Map;
|
|
4263
5866
|
|
|
4264
5867
|
class IntegrationProviderFactory {
|
|
5868
|
+
composioFallback;
|
|
5869
|
+
constructor(options) {
|
|
5870
|
+
this.composioFallback = options?.composioFallback;
|
|
5871
|
+
}
|
|
5872
|
+
async resolveProviderContext(context) {
|
|
5873
|
+
const secrets = await this.loadSecrets(context);
|
|
5874
|
+
const { transport, authMethod, apiVersion } = resolveIntegrationRequestContext(context.spec, context.connection);
|
|
5875
|
+
let authHeaders = {};
|
|
5876
|
+
if (authMethod && context.spec.supportedAuthMethods) {
|
|
5877
|
+
const authConfig = findAuthConfig(context.spec.supportedAuthMethods, authMethod);
|
|
5878
|
+
if (authConfig) {
|
|
5879
|
+
const stringSecrets = Object.fromEntries(Object.entries(secrets).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v]));
|
|
5880
|
+
authHeaders = buildAuthHeaders(authConfig, stringSecrets);
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
return { transport, authMethod, apiVersion, authHeaders, secrets };
|
|
5884
|
+
}
|
|
4265
5885
|
async createPaymentsProvider(context) {
|
|
4266
5886
|
const secrets = await this.loadSecrets(context);
|
|
4267
5887
|
switch (context.spec.meta.key) {
|
|
@@ -4270,6 +5890,9 @@ class IntegrationProviderFactory {
|
|
|
4270
5890
|
apiKey: requireSecret(secrets, "apiKey", "Stripe API key is required")
|
|
4271
5891
|
});
|
|
4272
5892
|
default:
|
|
5893
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5894
|
+
return this.composioFallback.createPaymentsProxy(context);
|
|
5895
|
+
}
|
|
4273
5896
|
throw new Error(`Unsupported payments integration: ${context.spec.meta.key}`);
|
|
4274
5897
|
}
|
|
4275
5898
|
}
|
|
@@ -4283,6 +5906,9 @@ class IntegrationProviderFactory {
|
|
|
4283
5906
|
messageStream: context.config.messageStream
|
|
4284
5907
|
});
|
|
4285
5908
|
default:
|
|
5909
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5910
|
+
return this.composioFallback.createEmailProxy(context);
|
|
5911
|
+
}
|
|
4286
5912
|
throw new Error(`Unsupported email integration: ${context.spec.meta.key}`);
|
|
4287
5913
|
}
|
|
4288
5914
|
}
|
|
@@ -4296,9 +5922,48 @@ class IntegrationProviderFactory {
|
|
|
4296
5922
|
fromNumber: context.config.fromNumber
|
|
4297
5923
|
});
|
|
4298
5924
|
default:
|
|
5925
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5926
|
+
return this.composioFallback.createMessagingProxy(context);
|
|
5927
|
+
}
|
|
4299
5928
|
throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
|
|
4300
5929
|
}
|
|
4301
5930
|
}
|
|
5931
|
+
async createMessagingProvider(context) {
|
|
5932
|
+
const secrets = await this.loadSecrets(context);
|
|
5933
|
+
const config = context.config;
|
|
5934
|
+
switch (context.spec.meta.key) {
|
|
5935
|
+
case "messaging.slack":
|
|
5936
|
+
return new SlackMessagingProvider({
|
|
5937
|
+
botToken: requireSecret(secrets, "botToken", "Slack bot token is required"),
|
|
5938
|
+
defaultChannelId: config?.defaultChannelId,
|
|
5939
|
+
apiBaseUrl: config?.apiBaseUrl
|
|
5940
|
+
});
|
|
5941
|
+
case "messaging.github":
|
|
5942
|
+
return new GithubMessagingProvider({
|
|
5943
|
+
token: requireSecret(secrets, "token", "GitHub token is required"),
|
|
5944
|
+
defaultOwner: config?.defaultOwner,
|
|
5945
|
+
defaultRepo: config?.defaultRepo,
|
|
5946
|
+
apiBaseUrl: config?.apiBaseUrl
|
|
5947
|
+
});
|
|
5948
|
+
case "messaging.whatsapp.meta":
|
|
5949
|
+
return new MetaWhatsappMessagingProvider({
|
|
5950
|
+
accessToken: requireSecret(secrets, "accessToken", "Meta WhatsApp access token is required"),
|
|
5951
|
+
phoneNumberId: requireConfig(context, "phoneNumberId", "Meta WhatsApp phoneNumberId is required"),
|
|
5952
|
+
apiVersion: config?.apiVersion
|
|
5953
|
+
});
|
|
5954
|
+
case "messaging.whatsapp.twilio":
|
|
5955
|
+
return new TwilioWhatsappMessagingProvider({
|
|
5956
|
+
accountSid: requireSecret(secrets, "accountSid", "Twilio account SID is required"),
|
|
5957
|
+
authToken: requireSecret(secrets, "authToken", "Twilio auth token is required"),
|
|
5958
|
+
fromNumber: config?.fromNumber
|
|
5959
|
+
});
|
|
5960
|
+
default:
|
|
5961
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5962
|
+
return this.composioFallback.createMessagingProxy(context);
|
|
5963
|
+
}
|
|
5964
|
+
throw new Error(`Unsupported messaging integration: ${context.spec.meta.key}`);
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
4302
5967
|
async createVectorStoreProvider(context) {
|
|
4303
5968
|
const secrets = await this.loadSecrets(context);
|
|
4304
5969
|
const config = context.config;
|
|
@@ -4319,6 +5984,9 @@ class IntegrationProviderFactory {
|
|
|
4319
5984
|
sslMode: config?.sslMode
|
|
4320
5985
|
});
|
|
4321
5986
|
default:
|
|
5987
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5988
|
+
return this.composioFallback.createGenericProxy(context);
|
|
5989
|
+
}
|
|
4322
5990
|
throw new Error(`Unsupported vector store integration: ${context.spec.meta.key}`);
|
|
4323
5991
|
}
|
|
4324
5992
|
}
|
|
@@ -4335,6 +6003,9 @@ class IntegrationProviderFactory {
|
|
|
4335
6003
|
personalApiKey: requireSecret(secrets, "personalApiKey", "PostHog personalApiKey is required")
|
|
4336
6004
|
});
|
|
4337
6005
|
default:
|
|
6006
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6007
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6008
|
+
}
|
|
4338
6009
|
throw new Error(`Unsupported analytics integration: ${context.spec.meta.key}`);
|
|
4339
6010
|
}
|
|
4340
6011
|
}
|
|
@@ -4349,6 +6020,9 @@ class IntegrationProviderFactory {
|
|
|
4349
6020
|
sslMode: config?.sslMode
|
|
4350
6021
|
});
|
|
4351
6022
|
default:
|
|
6023
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6024
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6025
|
+
}
|
|
4352
6026
|
throw new Error(`Unsupported database integration: ${context.spec.meta.key}`);
|
|
4353
6027
|
}
|
|
4354
6028
|
}
|
|
@@ -4362,6 +6036,9 @@ class IntegrationProviderFactory {
|
|
|
4362
6036
|
clientOptions: secrets.type === "service_account" ? { credentials: secrets } : undefined
|
|
4363
6037
|
});
|
|
4364
6038
|
default:
|
|
6039
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6040
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6041
|
+
}
|
|
4365
6042
|
throw new Error(`Unsupported storage integration: ${context.spec.meta.key}`);
|
|
4366
6043
|
}
|
|
4367
6044
|
}
|
|
@@ -4394,9 +6071,53 @@ class IntegrationProviderFactory {
|
|
|
4394
6071
|
pollIntervalMs: config?.pollIntervalMs
|
|
4395
6072
|
});
|
|
4396
6073
|
default:
|
|
6074
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6075
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6076
|
+
}
|
|
4397
6077
|
throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
|
|
4398
6078
|
}
|
|
4399
6079
|
}
|
|
6080
|
+
async createSttProvider(context) {
|
|
6081
|
+
const secrets = await this.loadSecrets(context);
|
|
6082
|
+
const config = context.config;
|
|
6083
|
+
switch (context.spec.meta.key) {
|
|
6084
|
+
case "ai-voice-stt.mistral":
|
|
6085
|
+
return new MistralSttProvider({
|
|
6086
|
+
apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
|
|
6087
|
+
defaultModel: config?.model,
|
|
6088
|
+
defaultLanguage: config?.language,
|
|
6089
|
+
serverURL: config?.serverURL
|
|
6090
|
+
});
|
|
6091
|
+
default:
|
|
6092
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6093
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6094
|
+
}
|
|
6095
|
+
throw new Error(`Unsupported STT integration: ${context.spec.meta.key}`);
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
async createConversationalProvider(context) {
|
|
6099
|
+
const secrets = await this.loadSecrets(context);
|
|
6100
|
+
const config = context.config;
|
|
6101
|
+
switch (context.spec.meta.key) {
|
|
6102
|
+
case "ai-voice-conv.mistral":
|
|
6103
|
+
return new MistralConversationalProvider({
|
|
6104
|
+
apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
|
|
6105
|
+
defaultModel: config?.model,
|
|
6106
|
+
defaultVoiceId: config?.defaultVoice,
|
|
6107
|
+
serverURL: config?.serverURL,
|
|
6108
|
+
sttOptions: {
|
|
6109
|
+
defaultModel: config?.model,
|
|
6110
|
+
defaultLanguage: config?.language,
|
|
6111
|
+
serverURL: config?.serverURL
|
|
6112
|
+
}
|
|
6113
|
+
});
|
|
6114
|
+
default:
|
|
6115
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6116
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6117
|
+
}
|
|
6118
|
+
throw new Error(`Unsupported conversational integration: ${context.spec.meta.key}`);
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
4400
6121
|
async createProjectManagementProvider(context) {
|
|
4401
6122
|
const secrets = await this.loadSecrets(context);
|
|
4402
6123
|
const config = context.config;
|
|
@@ -4434,6 +6155,9 @@ class IntegrationProviderFactory {
|
|
|
4434
6155
|
descriptionProperty: config?.descriptionProperty
|
|
4435
6156
|
});
|
|
4436
6157
|
default:
|
|
6158
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6159
|
+
return this.composioFallback.createProjectManagementProxy(context);
|
|
6160
|
+
}
|
|
4437
6161
|
throw new Error(`Unsupported project management integration: ${context.spec.meta.key}`);
|
|
4438
6162
|
}
|
|
4439
6163
|
}
|
|
@@ -4482,6 +6206,9 @@ class IntegrationProviderFactory {
|
|
|
4482
6206
|
webhookSecret: secrets.webhookSecret
|
|
4483
6207
|
});
|
|
4484
6208
|
default:
|
|
6209
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6210
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6211
|
+
}
|
|
4485
6212
|
throw new Error(`Unsupported meeting recorder integration: ${context.spec.meta.key}`);
|
|
4486
6213
|
}
|
|
4487
6214
|
}
|
|
@@ -4494,6 +6221,9 @@ class IntegrationProviderFactory {
|
|
|
4494
6221
|
defaultModel: context.config.model
|
|
4495
6222
|
});
|
|
4496
6223
|
default:
|
|
6224
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6225
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6226
|
+
}
|
|
4497
6227
|
throw new Error(`Unsupported LLM integration: ${context.spec.meta.key}`);
|
|
4498
6228
|
}
|
|
4499
6229
|
}
|
|
@@ -4506,6 +6236,9 @@ class IntegrationProviderFactory {
|
|
|
4506
6236
|
defaultModel: context.config.embeddingModel
|
|
4507
6237
|
});
|
|
4508
6238
|
default:
|
|
6239
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6240
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6241
|
+
}
|
|
4509
6242
|
throw new Error(`Unsupported embeddings integration: ${context.spec.meta.key}`);
|
|
4510
6243
|
}
|
|
4511
6244
|
}
|
|
@@ -4527,6 +6260,9 @@ class IntegrationProviderFactory {
|
|
|
4527
6260
|
});
|
|
4528
6261
|
}
|
|
4529
6262
|
default:
|
|
6263
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6264
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6265
|
+
}
|
|
4530
6266
|
throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
|
|
4531
6267
|
}
|
|
4532
6268
|
}
|
|
@@ -4547,7 +6283,7 @@ class IntegrationProviderFactory {
|
|
|
4547
6283
|
}
|
|
4548
6284
|
}
|
|
4549
6285
|
function parseSecret(secret) {
|
|
4550
|
-
const text =
|
|
6286
|
+
const text = Buffer6.from(secret.data).toString("utf-8").trim();
|
|
4551
6287
|
if (!text)
|
|
4552
6288
|
return {};
|
|
4553
6289
|
try {
|