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