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