@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,3 +1,50 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
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
|
+
|
|
1
48
|
// src/impls/elevenlabs-voice.ts
|
|
2
49
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
|
3
50
|
var FORMAT_MAP = {
|
|
@@ -1464,7 +1511,286 @@ async function safeReadError3(response) {
|
|
|
1464
1511
|
}
|
|
1465
1512
|
}
|
|
1466
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
|
+
|
|
1467
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
|
+
|
|
1468
1794
|
class BaseHealthProvider {
|
|
1469
1795
|
providerKey;
|
|
1470
1796
|
transport;
|
|
@@ -1472,146 +1798,191 @@ class BaseHealthProvider {
|
|
|
1472
1798
|
mcpUrl;
|
|
1473
1799
|
apiKey;
|
|
1474
1800
|
accessToken;
|
|
1801
|
+
refreshToken;
|
|
1475
1802
|
mcpAccessToken;
|
|
1476
1803
|
webhookSecret;
|
|
1804
|
+
webhookSignatureHeader;
|
|
1805
|
+
route;
|
|
1806
|
+
aggregatorKey;
|
|
1807
|
+
oauth;
|
|
1477
1808
|
fetchFn;
|
|
1478
1809
|
mcpRequestId = 0;
|
|
1479
1810
|
constructor(options) {
|
|
1480
1811
|
this.providerKey = options.providerKey;
|
|
1481
1812
|
this.transport = options.transport;
|
|
1482
|
-
this.apiBaseUrl = options.apiBaseUrl
|
|
1813
|
+
this.apiBaseUrl = options.apiBaseUrl;
|
|
1483
1814
|
this.mcpUrl = options.mcpUrl;
|
|
1484
1815
|
this.apiKey = options.apiKey;
|
|
1485
1816
|
this.accessToken = options.accessToken;
|
|
1817
|
+
this.refreshToken = options.oauth?.refreshToken;
|
|
1486
1818
|
this.mcpAccessToken = options.mcpAccessToken;
|
|
1487
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 ?? {};
|
|
1488
1824
|
this.fetchFn = options.fetchFn ?? fetch;
|
|
1489
1825
|
}
|
|
1490
|
-
async listActivities(
|
|
1491
|
-
|
|
1492
|
-
return {
|
|
1493
|
-
activities: result.items,
|
|
1494
|
-
nextCursor: result.nextCursor,
|
|
1495
|
-
hasMore: result.hasMore,
|
|
1496
|
-
source: this.currentSource()
|
|
1497
|
-
};
|
|
1826
|
+
async listActivities(_params) {
|
|
1827
|
+
throw this.unsupported("activities");
|
|
1498
1828
|
}
|
|
1499
|
-
async listWorkouts(
|
|
1500
|
-
|
|
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);
|
|
1501
1874
|
return {
|
|
1502
|
-
|
|
1503
|
-
nextCursor:
|
|
1504
|
-
hasMore:
|
|
1875
|
+
activities: response.items,
|
|
1876
|
+
nextCursor: response.nextCursor,
|
|
1877
|
+
hasMore: response.hasMore,
|
|
1505
1878
|
source: this.currentSource()
|
|
1506
1879
|
};
|
|
1507
1880
|
}
|
|
1508
|
-
async
|
|
1509
|
-
const
|
|
1881
|
+
async fetchWorkouts(params, config) {
|
|
1882
|
+
const response = await this.fetchList(params, config);
|
|
1510
1883
|
return {
|
|
1511
|
-
|
|
1512
|
-
nextCursor:
|
|
1513
|
-
hasMore:
|
|
1884
|
+
workouts: response.items,
|
|
1885
|
+
nextCursor: response.nextCursor,
|
|
1886
|
+
hasMore: response.hasMore,
|
|
1514
1887
|
source: this.currentSource()
|
|
1515
1888
|
};
|
|
1516
1889
|
}
|
|
1517
|
-
async
|
|
1518
|
-
const
|
|
1890
|
+
async fetchSleep(params, config) {
|
|
1891
|
+
const response = await this.fetchList(params, config);
|
|
1519
1892
|
return {
|
|
1520
|
-
|
|
1521
|
-
nextCursor:
|
|
1522
|
-
hasMore:
|
|
1893
|
+
sleep: response.items,
|
|
1894
|
+
nextCursor: response.nextCursor,
|
|
1895
|
+
hasMore: response.hasMore,
|
|
1523
1896
|
source: this.currentSource()
|
|
1524
1897
|
};
|
|
1525
1898
|
}
|
|
1526
|
-
async
|
|
1527
|
-
const
|
|
1899
|
+
async fetchBiometrics(params, config) {
|
|
1900
|
+
const response = await this.fetchList(params, config);
|
|
1528
1901
|
return {
|
|
1529
|
-
|
|
1530
|
-
nextCursor:
|
|
1531
|
-
hasMore:
|
|
1902
|
+
biometrics: response.items,
|
|
1903
|
+
nextCursor: response.nextCursor,
|
|
1904
|
+
hasMore: response.hasMore,
|
|
1532
1905
|
source: this.currentSource()
|
|
1533
1906
|
};
|
|
1534
1907
|
}
|
|
1535
|
-
async
|
|
1536
|
-
const
|
|
1537
|
-
const status = readString2(payload, "status") ?? "healthy";
|
|
1908
|
+
async fetchNutrition(params, config) {
|
|
1909
|
+
const response = await this.fetchList(params, config);
|
|
1538
1910
|
return {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
source: this.currentSource()
|
|
1543
|
-
lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
|
|
1544
|
-
errorCode: readString2(payload, "errorCode"),
|
|
1545
|
-
errorMessage: readString2(payload, "errorMessage"),
|
|
1546
|
-
metadata: asRecord(payload.metadata)
|
|
1911
|
+
nutrition: response.items,
|
|
1912
|
+
nextCursor: response.nextCursor,
|
|
1913
|
+
hasMore: response.hasMore,
|
|
1914
|
+
source: this.currentSource()
|
|
1547
1915
|
};
|
|
1548
1916
|
}
|
|
1549
|
-
async
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
async syncWorkouts(params) {
|
|
1553
|
-
return this.sync("workouts", params);
|
|
1554
|
-
}
|
|
1555
|
-
async syncSleep(params) {
|
|
1556
|
-
return this.sync("sleep", params);
|
|
1557
|
-
}
|
|
1558
|
-
async syncBiometrics(params) {
|
|
1559
|
-
return this.sync("biometrics", params);
|
|
1560
|
-
}
|
|
1561
|
-
async syncNutrition(params) {
|
|
1562
|
-
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());
|
|
1563
1920
|
}
|
|
1564
|
-
|
|
1565
|
-
const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
|
|
1566
|
-
const body = asRecord(payload);
|
|
1921
|
+
currentSource() {
|
|
1567
1922
|
return {
|
|
1568
1923
|
providerKey: this.providerKey,
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
receivedAt: new Date().toISOString(),
|
|
1573
|
-
verified: await this.verifyWebhook(request),
|
|
1574
|
-
payload
|
|
1924
|
+
transport: this.transport,
|
|
1925
|
+
route: this.route,
|
|
1926
|
+
aggregatorKey: this.aggregatorKey
|
|
1575
1927
|
};
|
|
1576
1928
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
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}`);
|
|
1583
1934
|
}
|
|
1584
|
-
async
|
|
1585
|
-
const
|
|
1586
|
-
const
|
|
1935
|
+
async syncFromList(executor) {
|
|
1936
|
+
const result = await executor();
|
|
1937
|
+
const records = countResultRecords(result);
|
|
1587
1938
|
return {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1939
|
+
synced: records,
|
|
1940
|
+
failed: 0,
|
|
1941
|
+
nextCursor: undefined,
|
|
1942
|
+
source: result.source
|
|
1591
1943
|
};
|
|
1592
1944
|
}
|
|
1593
|
-
async
|
|
1594
|
-
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);
|
|
1595
1949
|
return {
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
errors: asArray2(payload.errors)?.map((item) => String(item)),
|
|
1600
|
-
source: this.currentSource()
|
|
1950
|
+
items,
|
|
1951
|
+
nextCursor: pagination.nextCursor,
|
|
1952
|
+
hasMore: pagination.hasMore
|
|
1601
1953
|
};
|
|
1602
1954
|
}
|
|
1603
|
-
async
|
|
1604
|
-
|
|
1605
|
-
|
|
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);
|
|
1606
1970
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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)) {
|
|
1610
1980
|
if (value == null)
|
|
1611
1981
|
continue;
|
|
1612
1982
|
if (Array.isArray(value)) {
|
|
1613
|
-
value.forEach((
|
|
1614
|
-
|
|
1983
|
+
value.forEach((entry) => {
|
|
1984
|
+
if (entry != null)
|
|
1985
|
+
url.searchParams.append(key, String(entry));
|
|
1615
1986
|
});
|
|
1616
1987
|
continue;
|
|
1617
1988
|
}
|
|
@@ -1620,22 +1991,22 @@ class BaseHealthProvider {
|
|
|
1620
1991
|
}
|
|
1621
1992
|
const response = await this.fetchFn(url, {
|
|
1622
1993
|
method,
|
|
1623
|
-
headers:
|
|
1624
|
-
|
|
1625
|
-
...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
|
|
1626
|
-
},
|
|
1627
|
-
body: method === "POST" ? JSON.stringify(params) : undefined
|
|
1994
|
+
headers: this.authorizationHeaders(),
|
|
1995
|
+
body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
|
|
1628
1996
|
});
|
|
1629
|
-
if (
|
|
1630
|
-
const
|
|
1631
|
-
|
|
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);
|
|
1632
2004
|
}
|
|
1633
|
-
|
|
1634
|
-
return asRecord(data) ?? {};
|
|
2005
|
+
return this.readResponsePayload(response, path);
|
|
1635
2006
|
}
|
|
1636
|
-
async callMcpTool(
|
|
2007
|
+
async callMcpTool(toolName, args) {
|
|
1637
2008
|
if (!this.mcpUrl) {
|
|
1638
|
-
|
|
2009
|
+
throw new Error(`${this.providerKey} MCP URL is not configured.`);
|
|
1639
2010
|
}
|
|
1640
2011
|
const response = await this.fetchFn(this.mcpUrl, {
|
|
1641
2012
|
method: "POST",
|
|
@@ -1648,78 +2019,103 @@ class BaseHealthProvider {
|
|
|
1648
2019
|
id: ++this.mcpRequestId,
|
|
1649
2020
|
method: "tools/call",
|
|
1650
2021
|
params: {
|
|
1651
|
-
name:
|
|
1652
|
-
arguments:
|
|
2022
|
+
name: toolName,
|
|
2023
|
+
arguments: args
|
|
1653
2024
|
}
|
|
1654
2025
|
})
|
|
1655
2026
|
});
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
return structured;
|
|
1666
|
-
const data = asRecord(result.data);
|
|
1667
|
-
if (data)
|
|
1668
|
-
return data;
|
|
1669
|
-
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;
|
|
1670
2036
|
}
|
|
1671
|
-
|
|
2037
|
+
authorizationHeaders() {
|
|
2038
|
+
const token = this.accessToken ?? this.apiKey;
|
|
1672
2039
|
return {
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
route: "primary"
|
|
2040
|
+
"Content-Type": "application/json",
|
|
2041
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
1676
2042
|
};
|
|
1677
2043
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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();
|
|
1684
2082
|
}
|
|
1685
2083
|
}
|
|
1686
2084
|
function readHeader(headers, key) {
|
|
1687
|
-
const
|
|
1688
|
-
|
|
2085
|
+
const target = key.toLowerCase();
|
|
2086
|
+
const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
|
|
2087
|
+
if (!entry)
|
|
1689
2088
|
return;
|
|
1690
|
-
const value =
|
|
2089
|
+
const value = entry[1];
|
|
1691
2090
|
return Array.isArray(value) ? value[0] : value;
|
|
1692
2091
|
}
|
|
1693
|
-
function
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
+
}
|
|
1704
2105
|
}
|
|
1705
|
-
return
|
|
1706
|
-
}
|
|
1707
|
-
function asArray2(value) {
|
|
1708
|
-
return Array.isArray(value) ? value : undefined;
|
|
2106
|
+
return 0;
|
|
1709
2107
|
}
|
|
1710
|
-
function
|
|
1711
|
-
|
|
1712
|
-
return typeof value === "string" ? value : undefined;
|
|
1713
|
-
}
|
|
1714
|
-
function readBoolean2(record, key) {
|
|
1715
|
-
const value = record?.[key];
|
|
1716
|
-
return typeof value === "boolean" ? value : undefined;
|
|
2108
|
+
function ensureTrailingSlash(value) {
|
|
2109
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
1717
2110
|
}
|
|
1718
|
-
function
|
|
1719
|
-
|
|
1720
|
-
|
|
2111
|
+
function safeJsonParse(raw) {
|
|
2112
|
+
try {
|
|
2113
|
+
return JSON.parse(raw);
|
|
2114
|
+
} catch {
|
|
2115
|
+
return { rawBody: raw };
|
|
2116
|
+
}
|
|
1721
2117
|
}
|
|
1722
|
-
async function
|
|
2118
|
+
async function safeReadText2(response) {
|
|
1723
2119
|
try {
|
|
1724
2120
|
return await response.text();
|
|
1725
2121
|
} catch {
|
|
@@ -1727,133 +2123,530 @@ async function safeResponseText(response) {
|
|
|
1727
2123
|
}
|
|
1728
2124
|
}
|
|
1729
2125
|
|
|
1730
|
-
// src/impls/health/providers.ts
|
|
1731
|
-
function
|
|
2126
|
+
// src/impls/health/official-health-providers.ts
|
|
2127
|
+
function buildSharedQuery(params) {
|
|
1732
2128
|
return {
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
|
1735
2142
|
};
|
|
1736
2143
|
}
|
|
1737
2144
|
|
|
1738
2145
|
class OpenWearablesHealthProvider extends BaseHealthProvider {
|
|
2146
|
+
upstreamProvider;
|
|
1739
2147
|
constructor(options) {
|
|
1740
2148
|
super({
|
|
1741
|
-
providerKey: "health.openwearables",
|
|
1742
|
-
|
|
2149
|
+
providerKey: options.providerKey ?? "health.openwearables",
|
|
2150
|
+
apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
|
|
2151
|
+
webhookSignatureHeader: "x-openwearables-signature",
|
|
2152
|
+
...options
|
|
1743
2153
|
});
|
|
2154
|
+
this.upstreamProvider = options.upstreamProvider;
|
|
1744
2155
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
...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")
|
|
1752
2162
|
});
|
|
1753
2163
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
...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))
|
|
1761
2170
|
});
|
|
1762
2171
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
...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))
|
|
1770
2178
|
});
|
|
1771
2179
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
...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))
|
|
1779
2186
|
});
|
|
1780
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
|
+
}
|
|
1781
2215
|
}
|
|
1782
2216
|
|
|
1783
|
-
class
|
|
2217
|
+
class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
|
|
1784
2218
|
constructor(options) {
|
|
1785
2219
|
super({
|
|
1786
|
-
|
|
1787
|
-
|
|
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"
|
|
1788
2279
|
});
|
|
1789
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
|
+
}
|
|
1790
2404
|
}
|
|
1791
2405
|
|
|
1792
2406
|
class FitbitHealthProvider extends BaseHealthProvider {
|
|
1793
2407
|
constructor(options) {
|
|
1794
2408
|
super({
|
|
1795
2409
|
providerKey: "health.fitbit",
|
|
1796
|
-
|
|
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")
|
|
1797
2449
|
});
|
|
1798
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
|
+
}
|
|
1799
2472
|
}
|
|
1800
2473
|
|
|
1801
|
-
|
|
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 {
|
|
1802
2494
|
constructor(options) {
|
|
1803
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,
|
|
1804
2507
|
providerKey: "health.myfitnesspal",
|
|
1805
|
-
|
|
2508
|
+
upstreamProvider: "myfitnesspal"
|
|
1806
2509
|
});
|
|
1807
2510
|
}
|
|
1808
2511
|
}
|
|
1809
2512
|
|
|
1810
|
-
class EightSleepHealthProvider extends
|
|
2513
|
+
class EightSleepHealthProvider extends OpenWearablesHealthProvider {
|
|
1811
2514
|
constructor(options) {
|
|
1812
2515
|
super({
|
|
2516
|
+
...options,
|
|
1813
2517
|
providerKey: "health.eightsleep",
|
|
1814
|
-
|
|
2518
|
+
upstreamProvider: "eightsleep"
|
|
1815
2519
|
});
|
|
1816
2520
|
}
|
|
1817
2521
|
}
|
|
1818
2522
|
|
|
1819
|
-
class PelotonHealthProvider extends
|
|
2523
|
+
class PelotonHealthProvider extends OpenWearablesHealthProvider {
|
|
1820
2524
|
constructor(options) {
|
|
1821
2525
|
super({
|
|
2526
|
+
...options,
|
|
1822
2527
|
providerKey: "health.peloton",
|
|
1823
|
-
|
|
2528
|
+
upstreamProvider: "peloton"
|
|
1824
2529
|
});
|
|
1825
2530
|
}
|
|
1826
2531
|
}
|
|
1827
2532
|
|
|
1828
2533
|
class UnofficialHealthAutomationProvider extends BaseHealthProvider {
|
|
2534
|
+
providerSlugValue;
|
|
1829
2535
|
constructor(options) {
|
|
1830
2536
|
super({
|
|
1831
|
-
...
|
|
1832
|
-
providerKey: options.providerKey
|
|
2537
|
+
...options,
|
|
2538
|
+
providerKey: options.providerKey,
|
|
2539
|
+
webhookSignatureHeader: "x-unofficial-signature"
|
|
1833
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
|
+
};
|
|
1834
2592
|
}
|
|
1835
2593
|
}
|
|
1836
|
-
|
|
1837
2594
|
// src/impls/health-provider-factory.ts
|
|
1838
2595
|
import {
|
|
1839
2596
|
isUnofficialHealthProviderAllowed,
|
|
1840
2597
|
resolveHealthStrategyOrder
|
|
1841
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
|
+
};
|
|
1842
2623
|
function createHealthProviderFromContext(context, secrets) {
|
|
1843
2624
|
const providerKey = context.spec.meta.key;
|
|
1844
2625
|
const config = toFactoryConfig(context.config);
|
|
1845
2626
|
const strategyOrder = buildStrategyOrder(config);
|
|
1846
|
-
const
|
|
1847
|
-
for (
|
|
1848
|
-
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);
|
|
1849
2642
|
if (provider) {
|
|
1850
2643
|
return provider;
|
|
1851
2644
|
}
|
|
1852
|
-
|
|
2645
|
+
attemptLogs.push(`${strategy}: not available`);
|
|
1853
2646
|
}
|
|
1854
|
-
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(", ")}.`);
|
|
1855
2648
|
}
|
|
1856
|
-
function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
|
|
2649
|
+
function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
|
|
1857
2650
|
const options = {
|
|
1858
2651
|
transport: strategy,
|
|
1859
2652
|
apiBaseUrl: config.apiBaseUrl,
|
|
@@ -1861,10 +2654,21 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
|
|
|
1861
2654
|
apiKey: getSecretString(secrets, "apiKey"),
|
|
1862
2655
|
accessToken: getSecretString(secrets, "accessToken"),
|
|
1863
2656
|
mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
|
|
1864
|
-
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
|
+
}
|
|
1865
2666
|
};
|
|
1866
2667
|
if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
|
|
1867
|
-
return
|
|
2668
|
+
return createAggregatorProvider(providerKey, {
|
|
2669
|
+
...options,
|
|
2670
|
+
aggregatorKey: "health.openwearables"
|
|
2671
|
+
});
|
|
1868
2672
|
}
|
|
1869
2673
|
if (strategy === "unofficial") {
|
|
1870
2674
|
if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
|
|
@@ -1886,6 +2690,31 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
|
|
|
1886
2690
|
}
|
|
1887
2691
|
return createOfficialProvider(providerKey, options);
|
|
1888
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
|
+
}
|
|
1889
2718
|
function createOfficialProvider(providerKey, options) {
|
|
1890
2719
|
switch (providerKey) {
|
|
1891
2720
|
case "health.openwearables":
|
|
@@ -1903,11 +2732,20 @@ function createOfficialProvider(providerKey, options) {
|
|
|
1903
2732
|
case "health.fitbit":
|
|
1904
2733
|
return new FitbitHealthProvider(options);
|
|
1905
2734
|
case "health.myfitnesspal":
|
|
1906
|
-
return new MyFitnessPalHealthProvider(
|
|
2735
|
+
return new MyFitnessPalHealthProvider({
|
|
2736
|
+
...options,
|
|
2737
|
+
transport: "aggregator-api"
|
|
2738
|
+
});
|
|
1907
2739
|
case "health.eightsleep":
|
|
1908
|
-
return new EightSleepHealthProvider(
|
|
2740
|
+
return new EightSleepHealthProvider({
|
|
2741
|
+
...options,
|
|
2742
|
+
transport: "aggregator-api"
|
|
2743
|
+
});
|
|
1909
2744
|
case "health.peloton":
|
|
1910
|
-
return new PelotonHealthProvider(
|
|
2745
|
+
return new PelotonHealthProvider({
|
|
2746
|
+
...options,
|
|
2747
|
+
transport: "aggregator-api"
|
|
2748
|
+
});
|
|
1911
2749
|
default:
|
|
1912
2750
|
throw new Error(`Unsupported health provider key: ${providerKey}`);
|
|
1913
2751
|
}
|
|
@@ -1920,6 +2758,7 @@ function toFactoryConfig(config) {
|
|
|
1920
2758
|
return {
|
|
1921
2759
|
apiBaseUrl: asString(record.apiBaseUrl),
|
|
1922
2760
|
mcpUrl: asString(record.mcpUrl),
|
|
2761
|
+
oauthTokenUrl: asString(record.oauthTokenUrl),
|
|
1923
2762
|
defaultTransport: normalizeTransport(record.defaultTransport),
|
|
1924
2763
|
strategyOrder: normalizeTransportArray(record.strategyOrder),
|
|
1925
2764
|
allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
|
|
@@ -1948,6 +2787,27 @@ function normalizeTransportArray(value) {
|
|
|
1948
2787
|
const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
|
|
1949
2788
|
return transports.length > 0 ? transports : undefined;
|
|
1950
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
|
+
}
|
|
1951
2811
|
function getSecretString(secrets, key) {
|
|
1952
2812
|
const value = secrets[key];
|
|
1953
2813
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
@@ -2223,45 +3083,474 @@ function mapFinishReason(reason) {
|
|
|
2223
3083
|
}
|
|
2224
3084
|
}
|
|
2225
3085
|
|
|
2226
|
-
// src/impls/mistral-embedding.ts
|
|
2227
|
-
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";
|
|
2228
3454
|
|
|
2229
|
-
class
|
|
2230
|
-
|
|
3455
|
+
class MistralConversationalProvider {
|
|
3456
|
+
apiKey;
|
|
2231
3457
|
defaultModel;
|
|
3458
|
+
defaultVoiceId;
|
|
3459
|
+
baseUrl;
|
|
3460
|
+
fetchImpl;
|
|
3461
|
+
sttProvider;
|
|
2232
3462
|
constructor(options) {
|
|
2233
3463
|
if (!options.apiKey) {
|
|
2234
|
-
throw new Error("
|
|
3464
|
+
throw new Error("MistralConversationalProvider requires an apiKey");
|
|
2235
3465
|
}
|
|
2236
|
-
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({
|
|
2237
3472
|
apiKey: options.apiKey,
|
|
2238
|
-
|
|
3473
|
+
defaultModel: options.sttOptions?.defaultModel,
|
|
3474
|
+
defaultLanguage: options.sttOptions?.defaultLanguage,
|
|
3475
|
+
serverURL: options.sttOptions?.serverURL ?? options.serverURL,
|
|
3476
|
+
fetchImpl: this.fetchImpl
|
|
2239
3477
|
});
|
|
2240
|
-
this.defaultModel = options.defaultModel ?? "mistral-embed";
|
|
2241
3478
|
}
|
|
2242
|
-
async
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
|
2249
3488
|
});
|
|
2250
|
-
return response.data.map((item, index) => ({
|
|
2251
|
-
id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
|
|
2252
|
-
vector: item.embedding ?? [],
|
|
2253
|
-
dimensions: item.embedding?.length ?? 0,
|
|
2254
|
-
model: response.model,
|
|
2255
|
-
metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
|
|
2256
|
-
}));
|
|
2257
3489
|
}
|
|
2258
|
-
async
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
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 });
|
|
2262
3505
|
}
|
|
2263
|
-
|
|
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("");
|
|
2264
3546
|
}
|
|
3547
|
+
return "";
|
|
3548
|
+
}
|
|
3549
|
+
function asRecord3(value) {
|
|
3550
|
+
if (value && typeof value === "object") {
|
|
3551
|
+
return value;
|
|
3552
|
+
}
|
|
3553
|
+
return {};
|
|
2265
3554
|
}
|
|
2266
3555
|
|
|
2267
3556
|
// src/impls/qdrant-vector.ts
|
|
@@ -2663,7 +3952,7 @@ function distanceToScore(distance, metric) {
|
|
|
2663
3952
|
|
|
2664
3953
|
// src/impls/stripe-payments.ts
|
|
2665
3954
|
import Stripe from "stripe";
|
|
2666
|
-
var API_VERSION = "2026-
|
|
3955
|
+
var API_VERSION = "2026-02-25.clover";
|
|
2667
3956
|
|
|
2668
3957
|
class StripePaymentsProvider {
|
|
2669
3958
|
stripe;
|
|
@@ -3328,6 +4617,318 @@ function mapStatus(status) {
|
|
|
3328
4617
|
}
|
|
3329
4618
|
}
|
|
3330
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 "node: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
|
+
|
|
3331
4932
|
// src/impls/powens-client.ts
|
|
3332
4933
|
import { URL as URL2 } from "node:url";
|
|
3333
4934
|
var POWENS_BASE_URL = {
|
|
@@ -3834,7 +5435,7 @@ function resolveLabelIds(defaults, tags) {
|
|
|
3834
5435
|
}
|
|
3835
5436
|
|
|
3836
5437
|
// src/impls/jira.ts
|
|
3837
|
-
import { Buffer as
|
|
5438
|
+
import { Buffer as Buffer5 } from "node:buffer";
|
|
3838
5439
|
|
|
3839
5440
|
class JiraProjectManagementProvider {
|
|
3840
5441
|
siteUrl;
|
|
@@ -3911,7 +5512,7 @@ function normalizeSiteUrl(siteUrl) {
|
|
|
3911
5512
|
return siteUrl.replace(/\/$/, "");
|
|
3912
5513
|
}
|
|
3913
5514
|
function buildAuthHeader(email, apiToken) {
|
|
3914
|
-
const token =
|
|
5515
|
+
const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
|
|
3915
5516
|
return `Basic ${token}`;
|
|
3916
5517
|
}
|
|
3917
5518
|
function resolveIssueType(type, defaults) {
|
|
@@ -4114,7 +5715,7 @@ function buildParagraphBlocks(text) {
|
|
|
4114
5715
|
}
|
|
4115
5716
|
|
|
4116
5717
|
// src/impls/tldv-meeting-recorder.ts
|
|
4117
|
-
var
|
|
5718
|
+
var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
|
|
4118
5719
|
|
|
4119
5720
|
class TldvMeetingRecorderProvider {
|
|
4120
5721
|
apiKey;
|
|
@@ -4122,7 +5723,7 @@ class TldvMeetingRecorderProvider {
|
|
|
4122
5723
|
defaultPageSize;
|
|
4123
5724
|
constructor(options) {
|
|
4124
5725
|
this.apiKey = options.apiKey;
|
|
4125
|
-
this.baseUrl = options.baseUrl ??
|
|
5726
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
|
|
4126
5727
|
this.defaultPageSize = options.pageSize;
|
|
4127
5728
|
}
|
|
4128
5729
|
async listMeetings(params) {
|
|
@@ -4257,10 +5858,30 @@ async function safeReadError4(response) {
|
|
|
4257
5858
|
}
|
|
4258
5859
|
|
|
4259
5860
|
// src/impls/provider-factory.ts
|
|
4260
|
-
import { Buffer as
|
|
5861
|
+
import { Buffer as Buffer6 } from "node: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";
|
|
4261
5865
|
var SECRET_CACHE = new Map;
|
|
4262
5866
|
|
|
4263
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
|
+
}
|
|
4264
5885
|
async createPaymentsProvider(context) {
|
|
4265
5886
|
const secrets = await this.loadSecrets(context);
|
|
4266
5887
|
switch (context.spec.meta.key) {
|
|
@@ -4269,6 +5890,9 @@ class IntegrationProviderFactory {
|
|
|
4269
5890
|
apiKey: requireSecret(secrets, "apiKey", "Stripe API key is required")
|
|
4270
5891
|
});
|
|
4271
5892
|
default:
|
|
5893
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5894
|
+
return this.composioFallback.createPaymentsProxy(context);
|
|
5895
|
+
}
|
|
4272
5896
|
throw new Error(`Unsupported payments integration: ${context.spec.meta.key}`);
|
|
4273
5897
|
}
|
|
4274
5898
|
}
|
|
@@ -4282,6 +5906,9 @@ class IntegrationProviderFactory {
|
|
|
4282
5906
|
messageStream: context.config.messageStream
|
|
4283
5907
|
});
|
|
4284
5908
|
default:
|
|
5909
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5910
|
+
return this.composioFallback.createEmailProxy(context);
|
|
5911
|
+
}
|
|
4285
5912
|
throw new Error(`Unsupported email integration: ${context.spec.meta.key}`);
|
|
4286
5913
|
}
|
|
4287
5914
|
}
|
|
@@ -4295,9 +5922,48 @@ class IntegrationProviderFactory {
|
|
|
4295
5922
|
fromNumber: context.config.fromNumber
|
|
4296
5923
|
});
|
|
4297
5924
|
default:
|
|
5925
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5926
|
+
return this.composioFallback.createMessagingProxy(context);
|
|
5927
|
+
}
|
|
4298
5928
|
throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
|
|
4299
5929
|
}
|
|
4300
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
|
+
}
|
|
4301
5967
|
async createVectorStoreProvider(context) {
|
|
4302
5968
|
const secrets = await this.loadSecrets(context);
|
|
4303
5969
|
const config = context.config;
|
|
@@ -4318,6 +5984,9 @@ class IntegrationProviderFactory {
|
|
|
4318
5984
|
sslMode: config?.sslMode
|
|
4319
5985
|
});
|
|
4320
5986
|
default:
|
|
5987
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
5988
|
+
return this.composioFallback.createGenericProxy(context);
|
|
5989
|
+
}
|
|
4321
5990
|
throw new Error(`Unsupported vector store integration: ${context.spec.meta.key}`);
|
|
4322
5991
|
}
|
|
4323
5992
|
}
|
|
@@ -4334,6 +6003,9 @@ class IntegrationProviderFactory {
|
|
|
4334
6003
|
personalApiKey: requireSecret(secrets, "personalApiKey", "PostHog personalApiKey is required")
|
|
4335
6004
|
});
|
|
4336
6005
|
default:
|
|
6006
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6007
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6008
|
+
}
|
|
4337
6009
|
throw new Error(`Unsupported analytics integration: ${context.spec.meta.key}`);
|
|
4338
6010
|
}
|
|
4339
6011
|
}
|
|
@@ -4348,6 +6020,9 @@ class IntegrationProviderFactory {
|
|
|
4348
6020
|
sslMode: config?.sslMode
|
|
4349
6021
|
});
|
|
4350
6022
|
default:
|
|
6023
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6024
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6025
|
+
}
|
|
4351
6026
|
throw new Error(`Unsupported database integration: ${context.spec.meta.key}`);
|
|
4352
6027
|
}
|
|
4353
6028
|
}
|
|
@@ -4361,6 +6036,9 @@ class IntegrationProviderFactory {
|
|
|
4361
6036
|
clientOptions: secrets.type === "service_account" ? { credentials: secrets } : undefined
|
|
4362
6037
|
});
|
|
4363
6038
|
default:
|
|
6039
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6040
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6041
|
+
}
|
|
4364
6042
|
throw new Error(`Unsupported storage integration: ${context.spec.meta.key}`);
|
|
4365
6043
|
}
|
|
4366
6044
|
}
|
|
@@ -4393,9 +6071,53 @@ class IntegrationProviderFactory {
|
|
|
4393
6071
|
pollIntervalMs: config?.pollIntervalMs
|
|
4394
6072
|
});
|
|
4395
6073
|
default:
|
|
6074
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6075
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6076
|
+
}
|
|
4396
6077
|
throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
|
|
4397
6078
|
}
|
|
4398
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
|
+
}
|
|
4399
6121
|
async createProjectManagementProvider(context) {
|
|
4400
6122
|
const secrets = await this.loadSecrets(context);
|
|
4401
6123
|
const config = context.config;
|
|
@@ -4433,6 +6155,9 @@ class IntegrationProviderFactory {
|
|
|
4433
6155
|
descriptionProperty: config?.descriptionProperty
|
|
4434
6156
|
});
|
|
4435
6157
|
default:
|
|
6158
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6159
|
+
return this.composioFallback.createProjectManagementProxy(context);
|
|
6160
|
+
}
|
|
4436
6161
|
throw new Error(`Unsupported project management integration: ${context.spec.meta.key}`);
|
|
4437
6162
|
}
|
|
4438
6163
|
}
|
|
@@ -4481,6 +6206,9 @@ class IntegrationProviderFactory {
|
|
|
4481
6206
|
webhookSecret: secrets.webhookSecret
|
|
4482
6207
|
});
|
|
4483
6208
|
default:
|
|
6209
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6210
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6211
|
+
}
|
|
4484
6212
|
throw new Error(`Unsupported meeting recorder integration: ${context.spec.meta.key}`);
|
|
4485
6213
|
}
|
|
4486
6214
|
}
|
|
@@ -4493,6 +6221,9 @@ class IntegrationProviderFactory {
|
|
|
4493
6221
|
defaultModel: context.config.model
|
|
4494
6222
|
});
|
|
4495
6223
|
default:
|
|
6224
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6225
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6226
|
+
}
|
|
4496
6227
|
throw new Error(`Unsupported LLM integration: ${context.spec.meta.key}`);
|
|
4497
6228
|
}
|
|
4498
6229
|
}
|
|
@@ -4505,6 +6236,9 @@ class IntegrationProviderFactory {
|
|
|
4505
6236
|
defaultModel: context.config.embeddingModel
|
|
4506
6237
|
});
|
|
4507
6238
|
default:
|
|
6239
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6240
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6241
|
+
}
|
|
4508
6242
|
throw new Error(`Unsupported embeddings integration: ${context.spec.meta.key}`);
|
|
4509
6243
|
}
|
|
4510
6244
|
}
|
|
@@ -4526,6 +6260,9 @@ class IntegrationProviderFactory {
|
|
|
4526
6260
|
});
|
|
4527
6261
|
}
|
|
4528
6262
|
default:
|
|
6263
|
+
if (this.composioFallback?.canHandle(context.spec.meta.key)) {
|
|
6264
|
+
return this.composioFallback.createGenericProxy(context);
|
|
6265
|
+
}
|
|
4529
6266
|
throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
|
|
4530
6267
|
}
|
|
4531
6268
|
}
|
|
@@ -4546,7 +6283,7 @@ class IntegrationProviderFactory {
|
|
|
4546
6283
|
}
|
|
4547
6284
|
}
|
|
4548
6285
|
function parseSecret(secret) {
|
|
4549
|
-
const text =
|
|
6286
|
+
const text = Buffer6.from(secret.data).toString("utf-8").trim();
|
|
4550
6287
|
if (!text)
|
|
4551
6288
|
return {};
|
|
4552
6289
|
try {
|