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