@contractspec/integration.providers-impls 2.9.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 (61) hide show
  1. package/README.md +59 -0
  2. package/dist/health.d.ts +1 -0
  3. package/dist/health.js +3 -0
  4. package/dist/impls/async-event-queue.d.ts +8 -0
  5. package/dist/impls/async-event-queue.js +47 -0
  6. package/dist/impls/health/base-health-provider.d.ts +98 -0
  7. package/dist/impls/health/base-health-provider.js +616 -0
  8. package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
  9. package/dist/impls/health/hybrid-health-providers.js +1088 -0
  10. package/dist/impls/health/official-health-providers.d.ts +78 -0
  11. package/dist/impls/health/official-health-providers.js +968 -0
  12. package/dist/impls/health/provider-normalizers.d.ts +28 -0
  13. package/dist/impls/health/provider-normalizers.js +287 -0
  14. package/dist/impls/health/providers.d.ts +2 -0
  15. package/dist/impls/health/providers.js +1094 -0
  16. package/dist/impls/health-provider-factory.d.ts +3 -0
  17. package/dist/impls/health-provider-factory.js +1308 -0
  18. package/dist/impls/index.d.ts +8 -0
  19. package/dist/impls/index.js +2356 -176
  20. package/dist/impls/messaging-github.d.ts +17 -0
  21. package/dist/impls/messaging-github.js +110 -0
  22. package/dist/impls/messaging-slack.d.ts +14 -0
  23. package/dist/impls/messaging-slack.js +80 -0
  24. package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
  25. package/dist/impls/messaging-whatsapp-meta.js +52 -0
  26. package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
  27. package/dist/impls/messaging-whatsapp-twilio.js +82 -0
  28. package/dist/impls/mistral-conversational.d.ts +23 -0
  29. package/dist/impls/mistral-conversational.js +476 -0
  30. package/dist/impls/mistral-conversational.session.d.ts +32 -0
  31. package/dist/impls/mistral-conversational.session.js +206 -0
  32. package/dist/impls/mistral-stt.d.ts +17 -0
  33. package/dist/impls/mistral-stt.js +167 -0
  34. package/dist/impls/provider-factory.d.ts +7 -1
  35. package/dist/impls/provider-factory.js +2338 -176
  36. package/dist/impls/stripe-payments.js +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +2360 -174
  39. package/dist/messaging.d.ts +1 -0
  40. package/dist/messaging.js +3 -0
  41. package/dist/node/health.js +2 -0
  42. package/dist/node/impls/async-event-queue.js +46 -0
  43. package/dist/node/impls/health/base-health-provider.js +615 -0
  44. package/dist/node/impls/health/hybrid-health-providers.js +1087 -0
  45. package/dist/node/impls/health/official-health-providers.js +967 -0
  46. package/dist/node/impls/health/provider-normalizers.js +286 -0
  47. package/dist/node/impls/health/providers.js +1093 -0
  48. package/dist/node/impls/health-provider-factory.js +1307 -0
  49. package/dist/node/impls/index.js +2356 -176
  50. package/dist/node/impls/messaging-github.js +109 -0
  51. package/dist/node/impls/messaging-slack.js +79 -0
  52. package/dist/node/impls/messaging-whatsapp-meta.js +51 -0
  53. package/dist/node/impls/messaging-whatsapp-twilio.js +81 -0
  54. package/dist/node/impls/mistral-conversational.js +475 -0
  55. package/dist/node/impls/mistral-conversational.session.js +205 -0
  56. package/dist/node/impls/mistral-stt.js +166 -0
  57. package/dist/node/impls/provider-factory.js +2338 -176
  58. package/dist/node/impls/stripe-payments.js +1 -1
  59. package/dist/node/index.js +2360 -174
  60. package/dist/node/messaging.js +2 -0
  61. package/package.json +204 -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,131 +1508,1436 @@ async function safeReadError3(response) {
1464
1508
  }
1465
1509
  }
1466
1510
 
1467
- // src/impls/mistral-llm.ts
1468
- import { Mistral } from "@mistralai/mistralai";
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
+ }
1469
1781
 
1470
- class MistralLLMProvider {
1471
- client;
1472
- defaultModel;
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
+
1791
+ class BaseHealthProvider {
1792
+ providerKey;
1793
+ transport;
1794
+ apiBaseUrl;
1795
+ mcpUrl;
1796
+ apiKey;
1797
+ accessToken;
1798
+ refreshToken;
1799
+ mcpAccessToken;
1800
+ webhookSecret;
1801
+ webhookSignatureHeader;
1802
+ route;
1803
+ aggregatorKey;
1804
+ oauth;
1805
+ fetchFn;
1806
+ mcpRequestId = 0;
1473
1807
  constructor(options) {
1474
- if (!options.apiKey) {
1475
- throw new Error("MistralLLMProvider requires an apiKey");
1476
- }
1477
- this.client = options.client ?? new Mistral({
1478
- apiKey: options.apiKey,
1479
- serverURL: options.serverURL,
1480
- userAgent: options.userAgentSuffix ? `${options.userAgentSuffix}` : undefined
1808
+ this.providerKey = options.providerKey;
1809
+ this.transport = options.transport;
1810
+ this.apiBaseUrl = options.apiBaseUrl;
1811
+ this.mcpUrl = options.mcpUrl;
1812
+ this.apiKey = options.apiKey;
1813
+ this.accessToken = options.accessToken;
1814
+ this.refreshToken = options.oauth?.refreshToken;
1815
+ this.mcpAccessToken = options.mcpAccessToken;
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 ?? {};
1821
+ this.fetchFn = options.fetchFn ?? fetch;
1822
+ }
1823
+ async listActivities(_params) {
1824
+ throw this.unsupported("activities");
1825
+ }
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`
1481
1841
  });
1482
- this.defaultModel = options.defaultModel ?? "mistral-large-latest";
1483
1842
  }
1484
- async chat(messages, options = {}) {
1485
- const request = this.buildChatRequest(messages, options);
1486
- const response = await this.client.chat.complete(request);
1487
- return this.buildLLMResponse(response);
1843
+ async syncActivities(params) {
1844
+ return this.syncFromList(() => this.listActivities(params));
1488
1845
  }
1489
- async* stream(messages, options = {}) {
1490
- const request = this.buildChatRequest(messages, options);
1491
- request.stream = true;
1492
- const stream = await this.client.chat.stream(request);
1493
- const aggregatedParts = [];
1494
- const aggregatedToolCalls = [];
1495
- let usage;
1496
- let finishReason;
1497
- for await (const event of stream) {
1498
- for (const choice of event.data.choices) {
1499
- const delta = choice.delta;
1500
- if (typeof delta.content === "string") {
1501
- if (delta.content.length > 0) {
1502
- aggregatedParts.push({ type: "text", text: delta.content });
1503
- yield {
1504
- type: "message_delta",
1505
- delta: { type: "text", text: delta.content },
1506
- index: choice.index
1507
- };
1508
- }
1509
- } else if (Array.isArray(delta.content)) {
1510
- for (const chunk of delta.content) {
1511
- if (chunk.type === "text" && "text" in chunk) {
1512
- aggregatedParts.push({ type: "text", text: chunk.text });
1513
- yield {
1514
- type: "message_delta",
1515
- delta: { type: "text", text: chunk.text },
1516
- index: choice.index
1517
- };
1518
- }
1519
- }
1520
- }
1521
- if (delta.toolCalls) {
1522
- let localIndex = 0;
1523
- for (const call of delta.toolCalls) {
1524
- const toolCall = this.fromMistralToolCall(call, localIndex);
1525
- aggregatedToolCalls.push(toolCall);
1526
- yield {
1527
- type: "tool_call",
1528
- call: toolCall,
1529
- index: choice.index
1530
- };
1531
- localIndex += 1;
1532
- }
1533
- }
1534
- if (choice.finishReason && choice.finishReason !== "null") {
1535
- finishReason = choice.finishReason;
1536
- }
1537
- }
1538
- if (event.data.usage) {
1539
- const usageEntry = this.fromUsage(event.data.usage);
1540
- if (usageEntry) {
1541
- usage = usageEntry;
1542
- yield { type: "usage", usage: usageEntry };
1543
- }
1544
- }
1545
- }
1546
- const message = {
1547
- role: "assistant",
1548
- content: aggregatedParts.length ? aggregatedParts : [{ type: "text", text: "" }]
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);
1871
+ return {
1872
+ activities: response.items,
1873
+ nextCursor: response.nextCursor,
1874
+ hasMore: response.hasMore,
1875
+ source: this.currentSource()
1549
1876
  };
1550
- if (aggregatedToolCalls.length > 0) {
1551
- message.content = [
1552
- ...aggregatedToolCalls,
1553
- ...aggregatedParts.length ? aggregatedParts : []
1554
- ];
1555
- }
1556
- yield {
1557
- type: "end",
1558
- response: {
1559
- message,
1560
- usage,
1561
- finishReason: mapFinishReason(finishReason)
1562
- }
1877
+ }
1878
+ async fetchWorkouts(params, config) {
1879
+ const response = await this.fetchList(params, config);
1880
+ return {
1881
+ workouts: response.items,
1882
+ nextCursor: response.nextCursor,
1883
+ hasMore: response.hasMore,
1884
+ source: this.currentSource()
1563
1885
  };
1564
1886
  }
1565
- async countTokens(_messages) {
1566
- throw new Error("Mistral API does not currently support token counting");
1887
+ async fetchSleep(params, config) {
1888
+ const response = await this.fetchList(params, config);
1889
+ return {
1890
+ sleep: response.items,
1891
+ nextCursor: response.nextCursor,
1892
+ hasMore: response.hasMore,
1893
+ source: this.currentSource()
1894
+ };
1567
1895
  }
1568
- buildChatRequest(messages, options) {
1569
- const model = options.model ?? this.defaultModel;
1570
- const mappedMessages = messages.map((message) => this.toMistralMessage(message));
1571
- const request = {
1572
- model,
1573
- messages: mappedMessages
1896
+ async fetchBiometrics(params, config) {
1897
+ const response = await this.fetchList(params, config);
1898
+ return {
1899
+ biometrics: response.items,
1900
+ nextCursor: response.nextCursor,
1901
+ hasMore: response.hasMore,
1902
+ source: this.currentSource()
1574
1903
  };
1575
- if (options.temperature != null) {
1576
- request.temperature = options.temperature;
1577
- }
1578
- if (options.topP != null) {
1579
- request.topP = options.topP;
1904
+ }
1905
+ async fetchNutrition(params, config) {
1906
+ const response = await this.fetchList(params, config);
1907
+ return {
1908
+ nutrition: response.items,
1909
+ nextCursor: response.nextCursor,
1910
+ hasMore: response.hasMore,
1911
+ source: this.currentSource()
1912
+ };
1913
+ }
1914
+ async fetchConnectionStatus(params, config) {
1915
+ const payload = await this.fetchPayload(config, params);
1916
+ return toHealthConnectionStatus(payload, params, this.currentSource());
1917
+ }
1918
+ currentSource() {
1919
+ return {
1920
+ providerKey: this.providerKey,
1921
+ transport: this.transport,
1922
+ route: this.route,
1923
+ aggregatorKey: this.aggregatorKey
1924
+ };
1925
+ }
1926
+ providerSlug() {
1927
+ return this.providerKey.replace("health.", "").replace(/-/g, "_");
1928
+ }
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);
1935
+ return {
1936
+ synced: records,
1937
+ failed: 0,
1938
+ nextCursor: undefined,
1939
+ source: result.source
1940
+ };
1941
+ }
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);
1946
+ return {
1947
+ items,
1948
+ nextCursor: pagination.nextCursor,
1949
+ hasMore: pagination.hasMore
1950
+ };
1951
+ }
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
+ });
1580
1961
  }
1581
- if (options.maxOutputTokens != null) {
1582
- request.maxTokens = options.maxOutputTokens;
1962
+ if (!config.apiPath || !this.apiBaseUrl) {
1963
+ throw new Error(`${this.providerKey} transport is missing an API path.`);
1583
1964
  }
1584
- if (options.stopSequences?.length) {
1585
- request.stop = options.stopSequences.length === 1 ? options.stopSequences[0] : options.stopSequences;
1965
+ if (method === "POST") {
1966
+ return this.requestApi(config.apiPath, "POST", undefined, body);
1586
1967
  }
1587
- if (options.tools?.length) {
1588
- request.tools = options.tools.map((tool) => ({
1589
- type: "function",
1590
- function: {
1591
- name: tool.name,
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)) {
1977
+ if (value == null)
1978
+ continue;
1979
+ if (Array.isArray(value)) {
1980
+ value.forEach((entry) => {
1981
+ if (entry != null)
1982
+ url.searchParams.append(key, String(entry));
1983
+ });
1984
+ continue;
1985
+ }
1986
+ url.searchParams.set(key, String(value));
1987
+ }
1988
+ }
1989
+ const response = await this.fetchFn(url, {
1990
+ method,
1991
+ headers: this.authorizationHeaders(),
1992
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
1993
+ });
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);
2001
+ }
2002
+ return this.readResponsePayload(response, path);
2003
+ }
2004
+ async callMcpTool(toolName, args) {
2005
+ if (!this.mcpUrl) {
2006
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
2007
+ }
2008
+ const response = await this.fetchFn(this.mcpUrl, {
2009
+ method: "POST",
2010
+ headers: {
2011
+ "Content-Type": "application/json",
2012
+ ...this.mcpAccessToken ? { Authorization: `Bearer ${this.mcpAccessToken}` } : {}
2013
+ },
2014
+ body: JSON.stringify({
2015
+ jsonrpc: "2.0",
2016
+ id: ++this.mcpRequestId,
2017
+ method: "tools/call",
2018
+ params: {
2019
+ name: toolName,
2020
+ arguments: args
2021
+ }
2022
+ })
2023
+ });
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;
2033
+ }
2034
+ authorizationHeaders() {
2035
+ const token = this.accessToken ?? this.apiKey;
2036
+ return {
2037
+ "Content-Type": "application/json",
2038
+ ...token ? { Authorization: `Bearer ${token}` } : {}
2039
+ };
2040
+ }
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();
2079
+ }
2080
+ }
2081
+ function readHeader(headers, key) {
2082
+ const target = key.toLowerCase();
2083
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
2084
+ if (!entry)
2085
+ return;
2086
+ const value = entry[1];
2087
+ return Array.isArray(value) ? value[0] : value;
2088
+ }
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
+ }
2102
+ }
2103
+ return 0;
2104
+ }
2105
+ function ensureTrailingSlash(value) {
2106
+ return value.endsWith("/") ? value : `${value}/`;
2107
+ }
2108
+ function safeJsonParse(raw) {
2109
+ try {
2110
+ return JSON.parse(raw);
2111
+ } catch {
2112
+ return { rawBody: raw };
2113
+ }
2114
+ }
2115
+ async function safeReadText2(response) {
2116
+ try {
2117
+ return await response.text();
2118
+ } catch {
2119
+ return response.statusText;
2120
+ }
2121
+ }
2122
+
2123
+ // src/impls/health/official-health-providers.ts
2124
+ function buildSharedQuery(params) {
2125
+ return {
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
2139
+ };
2140
+ }
2141
+
2142
+ class OpenWearablesHealthProvider extends BaseHealthProvider {
2143
+ upstreamProvider;
2144
+ constructor(options) {
2145
+ super({
2146
+ providerKey: options.providerKey ?? "health.openwearables",
2147
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
2148
+ webhookSignatureHeader: "x-openwearables-signature",
2149
+ ...options
2150
+ });
2151
+ this.upstreamProvider = options.upstreamProvider;
2152
+ }
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")
2159
+ });
2160
+ }
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))
2167
+ });
2168
+ }
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))
2175
+ });
2176
+ }
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))
2183
+ });
2184
+ }
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
+ }
2213
+
2214
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
2215
+ constructor(options) {
2216
+ super({
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")
2330
+ });
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
+ }
2401
+ }
2402
+
2403
+ class FitbitHealthProvider extends BaseHealthProvider {
2404
+ constructor(options) {
2405
+ super({
2406
+ providerKey: "health.fitbit",
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"
2460
+ });
2461
+ }
2462
+ context(params) {
2463
+ return {
2464
+ tenantId: params.tenantId,
2465
+ connectionId: params.connectionId,
2466
+ providerKey: this.providerKey
2467
+ };
2468
+ }
2469
+ }
2470
+
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 {
2491
+ constructor(options) {
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,
2504
+ providerKey: "health.myfitnesspal",
2505
+ upstreamProvider: "myfitnesspal"
2506
+ });
2507
+ }
2508
+ }
2509
+
2510
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
2511
+ constructor(options) {
2512
+ super({
2513
+ ...options,
2514
+ providerKey: "health.eightsleep",
2515
+ upstreamProvider: "eightsleep"
2516
+ });
2517
+ }
2518
+ }
2519
+
2520
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
2521
+ constructor(options) {
2522
+ super({
2523
+ ...options,
2524
+ providerKey: "health.peloton",
2525
+ upstreamProvider: "peloton"
2526
+ });
2527
+ }
2528
+ }
2529
+
2530
+ class UnofficialHealthAutomationProvider extends BaseHealthProvider {
2531
+ providerSlugValue;
2532
+ constructor(options) {
2533
+ super({
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))
2576
+ });
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
+ }
2590
+ }
2591
+ // src/impls/health-provider-factory.ts
2592
+ import {
2593
+ isUnofficialHealthProviderAllowed,
2594
+ resolveHealthStrategyOrder
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
+ };
2620
+ function createHealthProviderFromContext(context, secrets) {
2621
+ const providerKey = context.spec.meta.key;
2622
+ const config = toFactoryConfig(context.config);
2623
+ const strategyOrder = buildStrategyOrder(config);
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);
2639
+ if (provider) {
2640
+ return provider;
2641
+ }
2642
+ attemptLogs.push(`${strategy}: not available`);
2643
+ }
2644
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
2645
+ }
2646
+ function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
2647
+ const options = {
2648
+ transport: strategy,
2649
+ apiBaseUrl: config.apiBaseUrl,
2650
+ mcpUrl: config.mcpUrl,
2651
+ apiKey: getSecretString(secrets, "apiKey"),
2652
+ accessToken: getSecretString(secrets, "accessToken"),
2653
+ mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
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
+ }
2663
+ };
2664
+ if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
2665
+ return createAggregatorProvider(providerKey, {
2666
+ ...options,
2667
+ aggregatorKey: "health.openwearables"
2668
+ });
2669
+ }
2670
+ if (strategy === "unofficial") {
2671
+ if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
2672
+ return;
2673
+ }
2674
+ if (providerKey !== "health.myfitnesspal" && providerKey !== "health.eightsleep" && providerKey !== "health.peloton" && providerKey !== "health.garmin") {
2675
+ return;
2676
+ }
2677
+ return new UnofficialHealthAutomationProvider({
2678
+ ...options,
2679
+ providerKey
2680
+ });
2681
+ }
2682
+ if (strategy === "official-mcp") {
2683
+ return createOfficialProvider(providerKey, {
2684
+ ...options,
2685
+ transport: "official-mcp"
2686
+ });
2687
+ }
2688
+ return createOfficialProvider(providerKey, options);
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
+ }
2715
+ function createOfficialProvider(providerKey, options) {
2716
+ switch (providerKey) {
2717
+ case "health.openwearables":
2718
+ return new OpenWearablesHealthProvider(options);
2719
+ case "health.whoop":
2720
+ return new WhoopHealthProvider(options);
2721
+ case "health.apple-health":
2722
+ return new AppleHealthBridgeProvider(options);
2723
+ case "health.oura":
2724
+ return new OuraHealthProvider(options);
2725
+ case "health.strava":
2726
+ return new StravaHealthProvider(options);
2727
+ case "health.garmin":
2728
+ return new GarminHealthProvider(options);
2729
+ case "health.fitbit":
2730
+ return new FitbitHealthProvider(options);
2731
+ case "health.myfitnesspal":
2732
+ return new MyFitnessPalHealthProvider({
2733
+ ...options,
2734
+ transport: "aggregator-api"
2735
+ });
2736
+ case "health.eightsleep":
2737
+ return new EightSleepHealthProvider({
2738
+ ...options,
2739
+ transport: "aggregator-api"
2740
+ });
2741
+ case "health.peloton":
2742
+ return new PelotonHealthProvider({
2743
+ ...options,
2744
+ transport: "aggregator-api"
2745
+ });
2746
+ default:
2747
+ throw new Error(`Unsupported health provider key: ${providerKey}`);
2748
+ }
2749
+ }
2750
+ function toFactoryConfig(config) {
2751
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
2752
+ return {};
2753
+ }
2754
+ const record = config;
2755
+ return {
2756
+ apiBaseUrl: asString(record.apiBaseUrl),
2757
+ mcpUrl: asString(record.mcpUrl),
2758
+ oauthTokenUrl: asString(record.oauthTokenUrl),
2759
+ defaultTransport: normalizeTransport(record.defaultTransport),
2760
+ strategyOrder: normalizeTransportArray(record.strategyOrder),
2761
+ allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
2762
+ unofficialAllowList: Array.isArray(record.unofficialAllowList) ? record.unofficialAllowList.map((item) => typeof item === "string" ? item : undefined).filter((item) => Boolean(item)) : undefined
2763
+ };
2764
+ }
2765
+ function buildStrategyOrder(config) {
2766
+ const order = resolveHealthStrategyOrder(config);
2767
+ if (!config.defaultTransport) {
2768
+ return order;
2769
+ }
2770
+ const withoutDefault = order.filter((item) => item !== config.defaultTransport);
2771
+ return [config.defaultTransport, ...withoutDefault];
2772
+ }
2773
+ function normalizeTransport(value) {
2774
+ if (typeof value !== "string")
2775
+ return;
2776
+ if (value === "official-api" || value === "official-mcp" || value === "aggregator-api" || value === "aggregator-mcp" || value === "unofficial") {
2777
+ return value;
2778
+ }
2779
+ return;
2780
+ }
2781
+ function normalizeTransportArray(value) {
2782
+ if (!Array.isArray(value))
2783
+ return;
2784
+ const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
2785
+ return transports.length > 0 ? transports : undefined;
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
+ }
2808
+ function getSecretString(secrets, key) {
2809
+ const value = secrets[key];
2810
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2811
+ }
2812
+ function asString(value) {
2813
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2814
+ }
2815
+
2816
+ // src/impls/mistral-llm.ts
2817
+ import { Mistral } from "@mistralai/mistralai";
2818
+
2819
+ class MistralLLMProvider {
2820
+ client;
2821
+ defaultModel;
2822
+ constructor(options) {
2823
+ if (!options.apiKey) {
2824
+ throw new Error("MistralLLMProvider requires an apiKey");
2825
+ }
2826
+ this.client = options.client ?? new Mistral({
2827
+ apiKey: options.apiKey,
2828
+ serverURL: options.serverURL,
2829
+ userAgent: options.userAgentSuffix ? `${options.userAgentSuffix}` : undefined
2830
+ });
2831
+ this.defaultModel = options.defaultModel ?? "mistral-large-latest";
2832
+ }
2833
+ async chat(messages, options = {}) {
2834
+ const request = this.buildChatRequest(messages, options);
2835
+ const response = await this.client.chat.complete(request);
2836
+ return this.buildLLMResponse(response);
2837
+ }
2838
+ async* stream(messages, options = {}) {
2839
+ const request = this.buildChatRequest(messages, options);
2840
+ request.stream = true;
2841
+ const stream = await this.client.chat.stream(request);
2842
+ const aggregatedParts = [];
2843
+ const aggregatedToolCalls = [];
2844
+ let usage;
2845
+ let finishReason;
2846
+ for await (const event of stream) {
2847
+ for (const choice of event.data.choices) {
2848
+ const delta = choice.delta;
2849
+ if (typeof delta.content === "string") {
2850
+ if (delta.content.length > 0) {
2851
+ aggregatedParts.push({ type: "text", text: delta.content });
2852
+ yield {
2853
+ type: "message_delta",
2854
+ delta: { type: "text", text: delta.content },
2855
+ index: choice.index
2856
+ };
2857
+ }
2858
+ } else if (Array.isArray(delta.content)) {
2859
+ for (const chunk of delta.content) {
2860
+ if (chunk.type === "text" && "text" in chunk) {
2861
+ aggregatedParts.push({ type: "text", text: chunk.text });
2862
+ yield {
2863
+ type: "message_delta",
2864
+ delta: { type: "text", text: chunk.text },
2865
+ index: choice.index
2866
+ };
2867
+ }
2868
+ }
2869
+ }
2870
+ if (delta.toolCalls) {
2871
+ let localIndex = 0;
2872
+ for (const call of delta.toolCalls) {
2873
+ const toolCall = this.fromMistralToolCall(call, localIndex);
2874
+ aggregatedToolCalls.push(toolCall);
2875
+ yield {
2876
+ type: "tool_call",
2877
+ call: toolCall,
2878
+ index: choice.index
2879
+ };
2880
+ localIndex += 1;
2881
+ }
2882
+ }
2883
+ if (choice.finishReason && choice.finishReason !== "null") {
2884
+ finishReason = choice.finishReason;
2885
+ }
2886
+ }
2887
+ if (event.data.usage) {
2888
+ const usageEntry = this.fromUsage(event.data.usage);
2889
+ if (usageEntry) {
2890
+ usage = usageEntry;
2891
+ yield { type: "usage", usage: usageEntry };
2892
+ }
2893
+ }
2894
+ }
2895
+ const message = {
2896
+ role: "assistant",
2897
+ content: aggregatedParts.length ? aggregatedParts : [{ type: "text", text: "" }]
2898
+ };
2899
+ if (aggregatedToolCalls.length > 0) {
2900
+ message.content = [
2901
+ ...aggregatedToolCalls,
2902
+ ...aggregatedParts.length ? aggregatedParts : []
2903
+ ];
2904
+ }
2905
+ yield {
2906
+ type: "end",
2907
+ response: {
2908
+ message,
2909
+ usage,
2910
+ finishReason: mapFinishReason(finishReason)
2911
+ }
2912
+ };
2913
+ }
2914
+ async countTokens(_messages) {
2915
+ throw new Error("Mistral API does not currently support token counting");
2916
+ }
2917
+ buildChatRequest(messages, options) {
2918
+ const model = options.model ?? this.defaultModel;
2919
+ const mappedMessages = messages.map((message) => this.toMistralMessage(message));
2920
+ const request = {
2921
+ model,
2922
+ messages: mappedMessages
2923
+ };
2924
+ if (options.temperature != null) {
2925
+ request.temperature = options.temperature;
2926
+ }
2927
+ if (options.topP != null) {
2928
+ request.topP = options.topP;
2929
+ }
2930
+ if (options.maxOutputTokens != null) {
2931
+ request.maxTokens = options.maxOutputTokens;
2932
+ }
2933
+ if (options.stopSequences?.length) {
2934
+ request.stop = options.stopSequences.length === 1 ? options.stopSequences[0] : options.stopSequences;
2935
+ }
2936
+ if (options.tools?.length) {
2937
+ request.tools = options.tools.map((tool) => ({
2938
+ type: "function",
2939
+ function: {
2940
+ name: tool.name,
1592
2941
  description: tool.description,
1593
2942
  parameters: typeof tool.inputSchema === "object" && tool.inputSchema !== null ? tool.inputSchema : {}
1594
2943
  }
@@ -1699,77 +3048,506 @@ class MistralLLMProvider {
1699
3048
  return null;
1700
3049
  return textParts.join("");
1701
3050
  }
1702
- extractToolCalls(message) {
1703
- const toolCallParts = message.content.filter((part) => part.type === "tool-call");
1704
- return toolCallParts.map((call, index) => ({
1705
- id: call.id ?? `call_${index}`,
1706
- type: "function",
1707
- index,
1708
- function: {
1709
- name: call.name,
1710
- arguments: call.arguments
1711
- }
1712
- }));
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) });
1713
3438
  }
1714
3439
  }
1715
- function mapFinishReason(reason) {
1716
- if (!reason)
1717
- return;
1718
- const normalized = reason.toLowerCase();
1719
- switch (normalized) {
1720
- case "stop":
1721
- return "stop";
1722
- case "length":
1723
- return "length";
1724
- case "tool_call":
1725
- case "tool_calls":
1726
- return "tool_call";
1727
- case "content_filter":
1728
- return "content_filter";
1729
- default:
1730
- return;
3440
+ function toError(error) {
3441
+ if (error instanceof Error) {
3442
+ return error;
1731
3443
  }
3444
+ return new Error(String(error));
1732
3445
  }
1733
3446
 
1734
- // src/impls/mistral-embedding.ts
1735
- 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";
1736
3451
 
1737
- class MistralEmbeddingProvider {
1738
- client;
3452
+ class MistralConversationalProvider {
3453
+ apiKey;
1739
3454
  defaultModel;
3455
+ defaultVoiceId;
3456
+ baseUrl;
3457
+ fetchImpl;
3458
+ sttProvider;
1740
3459
  constructor(options) {
1741
3460
  if (!options.apiKey) {
1742
- throw new Error("MistralEmbeddingProvider requires an apiKey");
3461
+ throw new Error("MistralConversationalProvider requires an apiKey");
1743
3462
  }
1744
- 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({
1745
3469
  apiKey: options.apiKey,
1746
- serverURL: options.serverURL
3470
+ defaultModel: options.sttOptions?.defaultModel,
3471
+ defaultLanguage: options.sttOptions?.defaultLanguage,
3472
+ serverURL: options.sttOptions?.serverURL ?? options.serverURL,
3473
+ fetchImpl: this.fetchImpl
1747
3474
  });
1748
- this.defaultModel = options.defaultModel ?? "mistral-embed";
1749
3475
  }
1750
- async embedDocuments(documents, options) {
1751
- if (documents.length === 0)
1752
- return [];
1753
- const model = options?.model ?? this.defaultModel;
1754
- const response = await this.client.embeddings.create({
1755
- model,
1756
- 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
1757
3485
  });
1758
- return response.data.map((item, index) => ({
1759
- id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
1760
- vector: item.embedding ?? [],
1761
- dimensions: item.embedding?.length ?? 0,
1762
- model: response.model,
1763
- metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
1764
- }));
1765
3486
  }
1766
- async embedQuery(query, options) {
1767
- const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
1768
- if (!result) {
1769
- 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 });
1770
3502
  }
1771
- 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;
1772
3549
  }
3550
+ return {};
1773
3551
  }
1774
3552
 
1775
3553
  // src/impls/qdrant-vector.ts
@@ -2171,7 +3949,7 @@ function distanceToScore(distance, metric) {
2171
3949
 
2172
3950
  // src/impls/stripe-payments.ts
2173
3951
  import Stripe from "stripe";
2174
- var API_VERSION = "2026-01-28.clover";
3952
+ var API_VERSION = "2026-02-25.clover";
2175
3953
 
2176
3954
  class StripePaymentsProvider {
2177
3955
  stripe;
@@ -2836,8 +4614,320 @@ function mapStatus(status) {
2836
4614
  }
2837
4615
  }
2838
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
+
2839
4929
  // src/impls/powens-client.ts
2840
- import { URL } from "node:url";
4930
+ import { URL as URL2 } from "node:url";
2841
4931
  var POWENS_BASE_URL = {
2842
4932
  sandbox: "https://api-sandbox.powens.com/v2",
2843
4933
  production: "https://api.powens.com/v2"
@@ -2923,7 +5013,7 @@ class PowensClient {
2923
5013
  });
2924
5014
  }
2925
5015
  async request(options) {
2926
- const url = new URL(options.path, this.baseUrl);
5016
+ const url = new URL2(options.path, this.baseUrl);
2927
5017
  if (options.searchParams) {
2928
5018
  for (const [key, value] of Object.entries(options.searchParams)) {
2929
5019
  if (value === undefined || value === null)
@@ -2993,7 +5083,7 @@ class PowensClient {
2993
5083
  return this.token.accessToken;
2994
5084
  }
2995
5085
  async fetchAccessToken() {
2996
- const url = new URL("/oauth/token", this.baseUrl);
5086
+ const url = new URL2("/oauth/token", this.baseUrl);
2997
5087
  const basicAuth = Buffer.from(`${this.clientId}:${this.clientSecret}`, "utf-8").toString("base64");
2998
5088
  const response = await this.fetchImpl(url, {
2999
5089
  method: "POST",
@@ -3342,7 +5432,7 @@ function resolveLabelIds(defaults, tags) {
3342
5432
  }
3343
5433
 
3344
5434
  // src/impls/jira.ts
3345
- import { Buffer as Buffer4 } from "node:buffer";
5435
+ import { Buffer as Buffer5 } from "node:buffer";
3346
5436
 
3347
5437
  class JiraProjectManagementProvider {
3348
5438
  siteUrl;
@@ -3419,7 +5509,7 @@ function normalizeSiteUrl(siteUrl) {
3419
5509
  return siteUrl.replace(/\/$/, "");
3420
5510
  }
3421
5511
  function buildAuthHeader(email, apiToken) {
3422
- const token = Buffer4.from(`${email}:${apiToken}`).toString("base64");
5512
+ const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
3423
5513
  return `Basic ${token}`;
3424
5514
  }
3425
5515
  function resolveIssueType(type, defaults) {
@@ -3622,7 +5712,7 @@ function buildParagraphBlocks(text) {
3622
5712
  }
3623
5713
 
3624
5714
  // src/impls/tldv-meeting-recorder.ts
3625
- var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
5715
+ var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
3626
5716
 
3627
5717
  class TldvMeetingRecorderProvider {
3628
5718
  apiKey;
@@ -3630,7 +5720,7 @@ class TldvMeetingRecorderProvider {
3630
5720
  defaultPageSize;
3631
5721
  constructor(options) {
3632
5722
  this.apiKey = options.apiKey;
3633
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
5723
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
3634
5724
  this.defaultPageSize = options.pageSize;
3635
5725
  }
3636
5726
  async listMeetings(params) {
@@ -3765,7 +5855,7 @@ async function safeReadError4(response) {
3765
5855
  }
3766
5856
 
3767
5857
  // src/impls/provider-factory.ts
3768
- import { Buffer as Buffer5 } from "node:buffer";
5858
+ import { Buffer as Buffer6 } from "node:buffer";
3769
5859
  var SECRET_CACHE = new Map;
3770
5860
 
3771
5861
  class IntegrationProviderFactory {
@@ -3806,6 +5896,39 @@ class IntegrationProviderFactory {
3806
5896
  throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
3807
5897
  }
3808
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
+ }
3809
5932
  async createVectorStoreProvider(context) {
3810
5933
  const secrets = await this.loadSecrets(context);
3811
5934
  const config = context.config;
@@ -3904,6 +6027,41 @@ class IntegrationProviderFactory {
3904
6027
  throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
3905
6028
  }
3906
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
+ }
3907
6065
  async createProjectManagementProvider(context) {
3908
6066
  const secrets = await this.loadSecrets(context);
3909
6067
  const config = context.config;
@@ -4037,6 +6195,10 @@ class IntegrationProviderFactory {
4037
6195
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
4038
6196
  }
4039
6197
  }
6198
+ async createHealthProvider(context) {
6199
+ const secrets = await this.loadSecrets(context);
6200
+ return createHealthProviderFromContext(context, secrets);
6201
+ }
4040
6202
  async loadSecrets(context) {
4041
6203
  const cacheKey = context.connection.meta.id;
4042
6204
  if (SECRET_CACHE.has(cacheKey)) {
@@ -4050,7 +6212,7 @@ class IntegrationProviderFactory {
4050
6212
  }
4051
6213
  }
4052
6214
  function parseSecret(secret) {
4053
- const text = Buffer5.from(secret.data).toString("utf-8").trim();
6215
+ const text = Buffer6.from(secret.data).toString("utf-8").trim();
4054
6216
  if (!text)
4055
6217
  return {};
4056
6218
  try {