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