@contractspec/integration.providers-impls 2.10.0 → 3.1.1

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 (138) hide show
  1. package/README.md +66 -10
  2. package/dist/impls/async-event-queue.d.ts +8 -0
  3. package/dist/impls/async-event-queue.js +49 -0
  4. package/dist/impls/composio-fallback-resolver.d.ts +34 -0
  5. package/dist/impls/composio-fallback-resolver.js +580 -0
  6. package/dist/impls/composio-mcp.d.ts +22 -0
  7. package/dist/impls/composio-mcp.js +164 -0
  8. package/dist/impls/composio-proxies.d.ts +60 -0
  9. package/dist/impls/composio-proxies.js +311 -0
  10. package/dist/impls/composio-sdk.d.ts +25 -0
  11. package/dist/impls/composio-sdk.js +78 -0
  12. package/dist/impls/composio-types.d.ts +43 -0
  13. package/dist/impls/composio-types.js +54 -0
  14. package/dist/impls/elevenlabs-voice.js +2 -0
  15. package/dist/impls/fal-voice.js +2 -0
  16. package/dist/impls/fathom-meeting-recorder.js +2 -0
  17. package/dist/impls/fathom-meeting-recorder.mapper.js +2 -0
  18. package/dist/impls/fathom-meeting-recorder.utils.js +2 -0
  19. package/dist/impls/fathom-meeting-recorder.webhooks.js +2 -0
  20. package/dist/impls/fireflies-meeting-recorder.js +2 -0
  21. package/dist/impls/fireflies-meeting-recorder.queries.js +2 -0
  22. package/dist/impls/fireflies-meeting-recorder.utils.js +2 -0
  23. package/dist/impls/gcs-storage.js +2 -0
  24. package/dist/impls/gmail-inbound.js +2 -0
  25. package/dist/impls/gmail-outbound.js +2 -0
  26. package/dist/impls/google-calendar.js +2 -0
  27. package/dist/impls/gradium-voice.js +2 -0
  28. package/dist/impls/granola-meeting-recorder.js +2 -0
  29. package/dist/impls/granola-meeting-recorder.mcp.js +2 -0
  30. package/dist/impls/health/base-health-provider.d.ts +64 -13
  31. package/dist/impls/health/base-health-provider.js +508 -156
  32. package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
  33. package/dist/impls/health/hybrid-health-providers.js +1090 -0
  34. package/dist/impls/health/official-health-providers.d.ts +78 -0
  35. package/dist/impls/health/official-health-providers.js +970 -0
  36. package/dist/impls/health/provider-normalizers.d.ts +28 -0
  37. package/dist/impls/health/provider-normalizers.js +289 -0
  38. package/dist/impls/health/providers.d.ts +2 -39
  39. package/dist/impls/health/providers.js +897 -184
  40. package/dist/impls/health-provider-factory.js +1011 -196
  41. package/dist/impls/index.d.ts +11 -0
  42. package/dist/impls/index.js +2588 -259
  43. package/dist/impls/jira.js +2 -0
  44. package/dist/impls/linear.js +2 -0
  45. package/dist/impls/messaging-github.d.ts +17 -0
  46. package/dist/impls/messaging-github.js +112 -0
  47. package/dist/impls/messaging-slack.d.ts +14 -0
  48. package/dist/impls/messaging-slack.js +82 -0
  49. package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
  50. package/dist/impls/messaging-whatsapp-meta.js +54 -0
  51. package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
  52. package/dist/impls/messaging-whatsapp-twilio.js +84 -0
  53. package/dist/impls/mistral-conversational.d.ts +23 -0
  54. package/dist/impls/mistral-conversational.js +478 -0
  55. package/dist/impls/mistral-conversational.session.d.ts +32 -0
  56. package/dist/impls/mistral-conversational.session.js +208 -0
  57. package/dist/impls/mistral-embedding.js +2 -0
  58. package/dist/impls/mistral-llm.js +2 -0
  59. package/dist/impls/mistral-stt.d.ts +17 -0
  60. package/dist/impls/mistral-stt.js +169 -0
  61. package/dist/impls/notion.js +2 -0
  62. package/dist/impls/posthog-reader.js +2 -0
  63. package/dist/impls/posthog-utils.js +2 -0
  64. package/dist/impls/posthog.js +2 -0
  65. package/dist/impls/postmark-email.js +2 -0
  66. package/dist/impls/powens-client.js +2 -0
  67. package/dist/impls/powens-openbanking.js +2 -0
  68. package/dist/impls/provider-factory.d.ts +29 -1
  69. package/dist/impls/provider-factory.js +1985 -249
  70. package/dist/impls/qdrant-vector.js +2 -0
  71. package/dist/impls/stripe-payments.js +3 -1
  72. package/dist/impls/supabase-psql.js +2 -0
  73. package/dist/impls/supabase-vector.js +2 -0
  74. package/dist/impls/tldv-meeting-recorder.js +2 -0
  75. package/dist/impls/twilio-sms.js +2 -0
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.js +2615 -283
  78. package/dist/messaging.d.ts +1 -0
  79. package/dist/messaging.js +3 -0
  80. package/dist/node/impls/async-event-queue.js +49 -0
  81. package/dist/node/impls/composio-fallback-resolver.js +580 -0
  82. package/dist/node/impls/composio-mcp.js +164 -0
  83. package/dist/node/impls/composio-proxies.js +311 -0
  84. package/dist/node/impls/composio-sdk.js +78 -0
  85. package/dist/node/impls/composio-types.js +54 -0
  86. package/dist/node/impls/elevenlabs-voice.js +3 -0
  87. package/dist/node/impls/fal-voice.js +3 -0
  88. package/dist/node/impls/fathom-meeting-recorder.js +3 -0
  89. package/dist/node/impls/fathom-meeting-recorder.mapper.js +3 -0
  90. package/dist/node/impls/fathom-meeting-recorder.utils.js +3 -0
  91. package/dist/node/impls/fathom-meeting-recorder.webhooks.js +3 -0
  92. package/dist/node/impls/fireflies-meeting-recorder.js +3 -0
  93. package/dist/node/impls/fireflies-meeting-recorder.queries.js +3 -0
  94. package/dist/node/impls/fireflies-meeting-recorder.utils.js +3 -0
  95. package/dist/node/impls/gcs-storage.js +3 -0
  96. package/dist/node/impls/gmail-inbound.js +3 -0
  97. package/dist/node/impls/gmail-outbound.js +3 -0
  98. package/dist/node/impls/google-calendar.js +3 -0
  99. package/dist/node/impls/gradium-voice.js +3 -0
  100. package/dist/node/impls/granola-meeting-recorder.js +3 -0
  101. package/dist/node/impls/granola-meeting-recorder.mcp.js +3 -0
  102. package/dist/node/impls/health/base-health-provider.js +509 -156
  103. package/dist/node/impls/health/hybrid-health-providers.js +1090 -0
  104. package/dist/node/impls/health/official-health-providers.js +970 -0
  105. package/dist/node/impls/health/provider-normalizers.js +289 -0
  106. package/dist/node/impls/health/providers.js +898 -184
  107. package/dist/node/impls/health-provider-factory.js +1012 -196
  108. package/dist/node/impls/index.js +2589 -259
  109. package/dist/node/impls/jira.js +3 -0
  110. package/dist/node/impls/linear.js +3 -0
  111. package/dist/node/impls/messaging-github.js +112 -0
  112. package/dist/node/impls/messaging-slack.js +82 -0
  113. package/dist/node/impls/messaging-whatsapp-meta.js +54 -0
  114. package/dist/node/impls/messaging-whatsapp-twilio.js +84 -0
  115. package/dist/node/impls/mistral-conversational.js +478 -0
  116. package/dist/node/impls/mistral-conversational.session.js +208 -0
  117. package/dist/node/impls/mistral-embedding.js +3 -0
  118. package/dist/node/impls/mistral-llm.js +3 -0
  119. package/dist/node/impls/mistral-stt.js +169 -0
  120. package/dist/node/impls/notion.js +3 -0
  121. package/dist/node/impls/posthog-reader.js +3 -0
  122. package/dist/node/impls/posthog-utils.js +3 -0
  123. package/dist/node/impls/posthog.js +3 -0
  124. package/dist/node/impls/postmark-email.js +3 -0
  125. package/dist/node/impls/powens-client.js +3 -0
  126. package/dist/node/impls/powens-openbanking.js +3 -0
  127. package/dist/node/impls/provider-factory.js +1986 -249
  128. package/dist/node/impls/qdrant-vector.js +3 -0
  129. package/dist/node/impls/stripe-payments.js +4 -1
  130. package/dist/node/impls/supabase-psql.js +3 -0
  131. package/dist/node/impls/supabase-vector.js +3 -0
  132. package/dist/node/impls/tldv-meeting-recorder.js +3 -0
  133. package/dist/node/impls/twilio-sms.js +3 -0
  134. package/dist/node/index.js +2616 -283
  135. package/dist/node/messaging.js +2 -0
  136. package/dist/node/secrets/provider.js +3 -0
  137. package/dist/secrets/provider.js +2 -0
  138. package/package.json +219 -14
@@ -1,4 +1,625 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/impls/async-event-queue.ts
5
+ class AsyncEventQueue {
6
+ values = [];
7
+ waiters = [];
8
+ done = false;
9
+ push(value) {
10
+ if (this.done) {
11
+ return;
12
+ }
13
+ const waiter = this.waiters.shift();
14
+ if (waiter) {
15
+ waiter({ value, done: false });
16
+ return;
17
+ }
18
+ this.values.push(value);
19
+ }
20
+ close() {
21
+ if (this.done) {
22
+ return;
23
+ }
24
+ this.done = true;
25
+ for (const waiter of this.waiters) {
26
+ waiter({ value: undefined, done: true });
27
+ }
28
+ this.waiters.length = 0;
29
+ }
30
+ [Symbol.asyncIterator]() {
31
+ return {
32
+ next: async () => {
33
+ const value = this.values.shift();
34
+ if (value != null) {
35
+ return { value, done: false };
36
+ }
37
+ if (this.done) {
38
+ return { value: undefined, done: true };
39
+ }
40
+ return new Promise((resolve) => {
41
+ this.waiters.push(resolve);
42
+ });
43
+ }
44
+ };
45
+ }
46
+ }
47
+
48
+ // src/impls/composio-types.ts
49
+ var INTEGRATION_KEY_TO_TOOLKIT = {
50
+ payments: "stripe",
51
+ email: "gmail",
52
+ calendar: "googlecalendar",
53
+ sms: "twilio",
54
+ "messaging.slack": "slack",
55
+ "messaging.github": "github",
56
+ "messaging.discord": "discord",
57
+ "messaging.teams": "microsoft_teams",
58
+ "analytics.posthog": "posthog",
59
+ "project-management.linear": "linear",
60
+ "project-management.jira": "jira",
61
+ "project-management.notion": "notion",
62
+ "project-management.asana": "asana",
63
+ "project-management.trello": "trello",
64
+ "project-management.monday": "monday",
65
+ "storage.s3": "aws",
66
+ "storage.gcs": "google_cloud",
67
+ "storage.gdrive": "googledrive",
68
+ "storage.dropbox": "dropbox",
69
+ "storage.onedrive": "onedrive",
70
+ "crm.salesforce": "salesforce",
71
+ "crm.hubspot": "hubspot",
72
+ "crm.pipedrive": "pipedrive",
73
+ "database.supabase": "supabase",
74
+ "vectordb.supabase": "supabase",
75
+ "ai-llm": "openai"
76
+ };
77
+ var SESSION_TTL_MS = 30 * 60 * 1000;
78
+ function isSessionExpired(session) {
79
+ return Date.now() - session.createdAt > SESSION_TTL_MS;
80
+ }
81
+ function resolveToolkit(integrationKey) {
82
+ if (INTEGRATION_KEY_TO_TOOLKIT[integrationKey]) {
83
+ return INTEGRATION_KEY_TO_TOOLKIT[integrationKey];
84
+ }
85
+ for (const [prefix, toolkit] of Object.entries(INTEGRATION_KEY_TO_TOOLKIT)) {
86
+ if (integrationKey.startsWith(prefix)) {
87
+ return toolkit;
88
+ }
89
+ }
90
+ const parts = integrationKey.split(".");
91
+ const last = parts[parts.length - 1];
92
+ return parts.length > 1 && last ? last : integrationKey;
93
+ }
94
+
95
+ // src/impls/composio-mcp.ts
96
+ class ComposioMcpProvider {
97
+ sessions = new Map;
98
+ config;
99
+ composioInstance;
100
+ constructor(config) {
101
+ this.config = config;
102
+ }
103
+ async executeTool(toolName, args) {
104
+ const userId = args._userId ?? "default";
105
+ const session = await this.getOrCreateSession(userId);
106
+ try {
107
+ const response = await fetch(session.mcpUrl, {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ ...session.mcpHeaders
112
+ },
113
+ body: JSON.stringify({
114
+ jsonrpc: "2.0",
115
+ id: crypto.randomUUID(),
116
+ method: "tools/call",
117
+ params: { name: toolName, arguments: args }
118
+ })
119
+ });
120
+ if (!response.ok) {
121
+ return {
122
+ success: false,
123
+ error: `Composio MCP call failed: ${response.status} ${response.statusText}`
124
+ };
125
+ }
126
+ const result = await response.json();
127
+ if (result.error) {
128
+ return {
129
+ success: false,
130
+ error: result.error.message ?? "Unknown MCP error"
131
+ };
132
+ }
133
+ return { success: true, data: result.result };
134
+ } catch (error) {
135
+ return {
136
+ success: false,
137
+ error: error instanceof Error ? error.message : String(error)
138
+ };
139
+ }
140
+ }
141
+ async searchTools(query) {
142
+ const session = await this.getOrCreateSession("default");
143
+ try {
144
+ const response = await fetch(session.mcpUrl, {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ ...session.mcpHeaders
149
+ },
150
+ body: JSON.stringify({
151
+ jsonrpc: "2.0",
152
+ id: crypto.randomUUID(),
153
+ method: "tools/list",
154
+ params: {}
155
+ })
156
+ });
157
+ if (!response.ok)
158
+ return [];
159
+ const result = await response.json();
160
+ const tools = result.result?.tools ?? [];
161
+ return tools.filter((t) => t.name.toLowerCase().includes(query.toLowerCase()) || (t.description ?? "").toLowerCase().includes(query.toLowerCase())).map((t) => ({
162
+ name: t.name,
163
+ description: t.description ?? "",
164
+ toolkit: resolveToolkit(t.name.split("_")[0]?.toLowerCase() ?? ""),
165
+ parameters: t.inputSchema ?? {}
166
+ }));
167
+ } catch {
168
+ return [];
169
+ }
170
+ }
171
+ getMcpConfig(userId) {
172
+ const session = this.sessions.get(userId);
173
+ if (!session || isSessionExpired(session))
174
+ return;
175
+ return { url: session.mcpUrl, headers: session.mcpHeaders };
176
+ }
177
+ async getOrCreateSession(userId) {
178
+ const existing = this.sessions.get(userId);
179
+ if (existing && !isSessionExpired(existing)) {
180
+ return existing;
181
+ }
182
+ const client = await this.getClient();
183
+ const entity = await client.getEntity(userId);
184
+ const mcpUrl = entity.getMcpUrl();
185
+ const mcpHeaders = entity.getMcpHeaders();
186
+ const session = {
187
+ userId,
188
+ mcpUrl,
189
+ mcpHeaders,
190
+ createdAt: Date.now()
191
+ };
192
+ this.sessions.set(userId, session);
193
+ return session;
194
+ }
195
+ async getClient() {
196
+ if (this.composioInstance)
197
+ return this.composioInstance;
198
+ const { Composio } = await import("@composio/core");
199
+ this.composioInstance = new Composio({
200
+ apiKey: this.config.apiKey,
201
+ ...this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}
202
+ });
203
+ return this.composioInstance;
204
+ }
205
+ }
206
+
207
+ // src/impls/composio-sdk.ts
208
+ class ComposioSdkProvider {
209
+ config;
210
+ client;
211
+ constructor(config) {
212
+ this.config = config;
213
+ }
214
+ async executeTool(toolName, args) {
215
+ const client = await this.getClient();
216
+ const userId = args._userId ?? "default";
217
+ try {
218
+ const entity = await client.getEntity(userId);
219
+ const result = await entity.execute(toolName, args);
220
+ return { success: true, data: result };
221
+ } catch (error) {
222
+ return {
223
+ success: false,
224
+ error: error instanceof Error ? error.message : String(error)
225
+ };
226
+ }
227
+ }
228
+ async searchTools(query) {
229
+ const client = await this.getClient();
230
+ try {
231
+ const tools = await client.actions.list({ query, limit: 20 });
232
+ return tools.map((t) => ({
233
+ name: t.name,
234
+ description: t.description ?? "",
235
+ toolkit: t.appName ?? "",
236
+ parameters: t.parameters ?? {}
237
+ }));
238
+ } catch {
239
+ return [];
240
+ }
241
+ }
242
+ async getConnectedAccounts(userId) {
243
+ const client = await this.getClient();
244
+ try {
245
+ const entity = await client.getEntity(userId);
246
+ const connections = await entity.getConnections();
247
+ return connections.map((c) => ({
248
+ id: c.id,
249
+ appName: c.appName,
250
+ status: c.status
251
+ }));
252
+ } catch {
253
+ return [];
254
+ }
255
+ }
256
+ async getMcpConfig(userId) {
257
+ const client = await this.getClient();
258
+ try {
259
+ const entity = await client.getEntity(userId);
260
+ return {
261
+ url: entity.getMcpUrl(),
262
+ headers: entity.getMcpHeaders()
263
+ };
264
+ } catch {
265
+ return;
266
+ }
267
+ }
268
+ async getClient() {
269
+ if (this.client)
270
+ return this.client;
271
+ const { Composio } = await import("@composio/core");
272
+ this.client = new Composio({
273
+ apiKey: this.config.apiKey,
274
+ ...this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}
275
+ });
276
+ return this.client;
277
+ }
278
+ }
279
+
280
+ // src/impls/composio-proxies.ts
281
+ function composioToolName(toolkit, action) {
282
+ return `${toolkit.toUpperCase()}_${action.toUpperCase()}`;
283
+ }
284
+ function unwrapResult(result, fallback) {
285
+ if (!result.success) {
286
+ throw new Error(`Composio tool execution failed: ${result.error}`);
287
+ }
288
+ return result.data ?? fallback;
289
+ }
290
+
291
+ class ComposioMessagingProxy {
292
+ proxy;
293
+ toolkit;
294
+ constructor(proxy, toolkit) {
295
+ this.proxy = proxy;
296
+ this.toolkit = toolkit;
297
+ }
298
+ async sendMessage(input) {
299
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "SEND_MESSAGE"), {
300
+ channel: input.channelId,
301
+ thread_ts: input.threadId,
302
+ text: input.text,
303
+ recipient: input.recipientId
304
+ });
305
+ const data = unwrapResult(result, {});
306
+ return {
307
+ id: String(data.id ?? data.ts ?? crypto.randomUUID()),
308
+ providerMessageId: data.ts,
309
+ status: "sent",
310
+ sentAt: new Date
311
+ };
312
+ }
313
+ async updateMessage(messageId, input) {
314
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "UPDATE_MESSAGE"), {
315
+ message_id: messageId,
316
+ channel: input.channelId,
317
+ text: input.text
318
+ });
319
+ const data = unwrapResult(result, {});
320
+ return {
321
+ id: String(data.id ?? messageId),
322
+ status: "sent",
323
+ sentAt: new Date
324
+ };
325
+ }
326
+ }
327
+
328
+ class ComposioEmailProxy {
329
+ proxy;
330
+ toolkit;
331
+ constructor(proxy, toolkit) {
332
+ this.proxy = proxy;
333
+ this.toolkit = toolkit;
334
+ }
335
+ async sendEmail(message) {
336
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "SEND_EMAIL"), {
337
+ to: message.to.map((a) => a.email).join(","),
338
+ cc: message.cc?.map((a) => a.email).join(","),
339
+ bcc: message.bcc?.map((a) => a.email).join(","),
340
+ subject: message.subject,
341
+ body: message.htmlBody ?? message.textBody ?? "",
342
+ from: message.from.email
343
+ });
344
+ const data = unwrapResult(result, {});
345
+ return {
346
+ id: String(data.id ?? data.messageId ?? crypto.randomUUID()),
347
+ providerMessageId: data.messageId,
348
+ queuedAt: new Date
349
+ };
350
+ }
351
+ }
352
+
353
+ class ComposioPaymentsProxy {
354
+ proxy;
355
+ toolkit;
356
+ constructor(proxy, toolkit) {
357
+ this.proxy = proxy;
358
+ this.toolkit = toolkit;
359
+ }
360
+ async createCustomer(input) {
361
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "CREATE_CUSTOMER"), { email: input.email, name: input.name, description: input.description });
362
+ const data = unwrapResult(result, {});
363
+ return {
364
+ id: String(data.id ?? crypto.randomUUID()),
365
+ email: data.email ?? input.email,
366
+ name: data.name ?? input.name
367
+ };
368
+ }
369
+ async getCustomer(customerId) {
370
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "GET_CUSTOMER"), { customer_id: customerId });
371
+ if (!result.success)
372
+ return null;
373
+ const data = result.data;
374
+ if (!data)
375
+ return null;
376
+ return {
377
+ id: String(data.id ?? customerId),
378
+ email: data.email,
379
+ name: data.name
380
+ };
381
+ }
382
+ async createPaymentIntent(input) {
383
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "CREATE_PAYMENT_INTENT"), {
384
+ amount: input.amount.amount,
385
+ currency: input.amount.currency,
386
+ customer_id: input.customerId,
387
+ description: input.description
388
+ });
389
+ const data = unwrapResult(result, {});
390
+ return {
391
+ id: String(data.id ?? crypto.randomUUID()),
392
+ amount: input.amount,
393
+ status: data.status ?? "requires_payment_method",
394
+ customerId: input.customerId,
395
+ clientSecret: data.client_secret
396
+ };
397
+ }
398
+ async capturePayment(paymentIntentId, input) {
399
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "CAPTURE_PAYMENT"), { payment_intent_id: paymentIntentId, amount: input?.amount?.amount });
400
+ const data = unwrapResult(result, {});
401
+ return {
402
+ id: paymentIntentId,
403
+ amount: input?.amount ?? { amount: 0, currency: "usd" },
404
+ status: data.status ?? "succeeded"
405
+ };
406
+ }
407
+ async cancelPaymentIntent(paymentIntentId) {
408
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "CANCEL_PAYMENT_INTENT"), { payment_intent_id: paymentIntentId });
409
+ const data = unwrapResult(result, {});
410
+ return {
411
+ id: paymentIntentId,
412
+ amount: { amount: 0, currency: "usd" },
413
+ status: data.status ?? "canceled"
414
+ };
415
+ }
416
+ async refundPayment(input) {
417
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "REFUND_PAYMENT"), {
418
+ payment_intent_id: input.paymentIntentId,
419
+ amount: input.amount?.amount,
420
+ reason: input.reason
421
+ });
422
+ const data = unwrapResult(result, {});
423
+ return {
424
+ id: String(data.id ?? crypto.randomUUID()),
425
+ paymentIntentId: input.paymentIntentId,
426
+ amount: input.amount ?? { amount: 0, currency: "usd" },
427
+ status: "succeeded"
428
+ };
429
+ }
430
+ async listInvoices(_query) {
431
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "LIST_INVOICES"), { customer_id: _query?.customerId, limit: _query?.limit });
432
+ if (!result.success)
433
+ return [];
434
+ const items = result.data ?? [];
435
+ return items.map((i) => ({
436
+ id: String(i.id),
437
+ status: i.status ?? "open",
438
+ amountDue: {
439
+ amount: Number(i.amount_due ?? 0),
440
+ currency: String(i.currency ?? "usd")
441
+ }
442
+ }));
443
+ }
444
+ async listTransactions(_query) {
445
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "LIST_TRANSACTIONS"), { customer_id: _query?.customerId, limit: _query?.limit });
446
+ if (!result.success)
447
+ return [];
448
+ const items = result.data ?? [];
449
+ return items.map((i) => ({
450
+ id: String(i.id),
451
+ amount: {
452
+ amount: Number(i.amount ?? 0),
453
+ currency: String(i.currency ?? "usd")
454
+ },
455
+ type: "capture",
456
+ status: "succeeded",
457
+ createdAt: new Date
458
+ }));
459
+ }
460
+ }
461
+
462
+ class ComposioProjectManagementProxy {
463
+ proxy;
464
+ toolkit;
465
+ constructor(proxy, toolkit) {
466
+ this.proxy = proxy;
467
+ this.toolkit = toolkit;
468
+ }
469
+ async createWorkItem(input) {
470
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "CREATE_ISSUE"), {
471
+ title: input.title,
472
+ description: input.description,
473
+ priority: input.priority,
474
+ assignee_id: input.assigneeId,
475
+ project_id: input.projectId,
476
+ labels: input.tags
477
+ });
478
+ const data = unwrapResult(result, {});
479
+ return {
480
+ id: String(data.id ?? data.key ?? crypto.randomUUID()),
481
+ title: input.title,
482
+ url: data.url,
483
+ status: data.status,
484
+ priority: input.priority,
485
+ tags: input.tags,
486
+ projectId: input.projectId
487
+ };
488
+ }
489
+ async createWorkItems(items) {
490
+ return Promise.all(items.map((item) => this.createWorkItem(item)));
491
+ }
492
+ }
493
+
494
+ class ComposioCalendarProxy {
495
+ proxy;
496
+ toolkit;
497
+ constructor(proxy, toolkit) {
498
+ this.proxy = proxy;
499
+ this.toolkit = toolkit;
500
+ }
501
+ async listEvents(query) {
502
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "LIST_EVENTS"), {
503
+ calendar_id: query.calendarId,
504
+ time_min: query.timeMin?.toISOString(),
505
+ time_max: query.timeMax?.toISOString(),
506
+ max_results: query.maxResults
507
+ });
508
+ if (!result.success)
509
+ return { events: [] };
510
+ const items = result.data ?? [];
511
+ return {
512
+ events: items.map((e) => ({
513
+ id: String(e.id),
514
+ calendarId: query.calendarId,
515
+ title: String(e.summary ?? e.title ?? ""),
516
+ start: new Date(String(e.start ?? Date.now())),
517
+ end: new Date(String(e.end ?? Date.now()))
518
+ }))
519
+ };
520
+ }
521
+ async createEvent(input) {
522
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "CREATE_EVENT"), {
523
+ calendar_id: input.calendarId,
524
+ summary: input.title,
525
+ description: input.description,
526
+ location: input.location,
527
+ start: input.start.toISOString(),
528
+ end: input.end.toISOString(),
529
+ attendees: input.attendees?.map((a) => a.email)
530
+ });
531
+ const data = unwrapResult(result, {});
532
+ return {
533
+ id: String(data.id ?? crypto.randomUUID()),
534
+ calendarId: input.calendarId,
535
+ title: input.title,
536
+ start: input.start,
537
+ end: input.end
538
+ };
539
+ }
540
+ async updateEvent(calendarId, eventId, input) {
541
+ const result = await this.proxy.executeTool(composioToolName(this.toolkit, "UPDATE_EVENT"), {
542
+ calendar_id: calendarId,
543
+ event_id: eventId,
544
+ summary: input.title,
545
+ description: input.description,
546
+ start: input.start?.toISOString(),
547
+ end: input.end?.toISOString()
548
+ });
549
+ const data = unwrapResult(result, {});
550
+ return {
551
+ id: eventId,
552
+ calendarId,
553
+ title: String(data.summary ?? input.title ?? ""),
554
+ start: input.start ?? new Date,
555
+ end: input.end ?? new Date
556
+ };
557
+ }
558
+ async deleteEvent(calendarId, eventId) {
559
+ await this.proxy.executeTool(composioToolName(this.toolkit, "DELETE_EVENT"), { calendar_id: calendarId, event_id: eventId });
560
+ }
561
+ }
562
+
563
+ class ComposioGenericProxy {
564
+ proxy;
565
+ toolkit;
566
+ constructor(proxy, toolkit) {
567
+ this.proxy = proxy;
568
+ this.toolkit = toolkit;
569
+ }
570
+ async executeTool(action, args) {
571
+ return this.proxy.executeTool(composioToolName(this.toolkit, action), args);
572
+ }
573
+ async searchTools(query) {
574
+ return this.proxy.searchTools(query);
575
+ }
576
+ getToolkit() {
577
+ return this.toolkit;
578
+ }
579
+ }
580
+
581
+ // src/impls/composio-fallback-resolver.ts
582
+ class ComposioFallbackResolver {
583
+ mcpProvider;
584
+ sdkProvider;
585
+ preferredTransport;
586
+ constructor(config) {
587
+ this.mcpProvider = new ComposioMcpProvider(config);
588
+ this.sdkProvider = new ComposioSdkProvider(config);
589
+ this.preferredTransport = config.preferredTransport;
590
+ }
591
+ canHandle(_integrationKey) {
592
+ return true;
593
+ }
594
+ createMessagingProxy(context) {
595
+ const toolkit = resolveToolkit(context.spec.meta.key);
596
+ return new ComposioMessagingProxy(this.getProxy(), toolkit);
597
+ }
598
+ createEmailProxy(context) {
599
+ const toolkit = resolveToolkit(context.spec.meta.key);
600
+ return new ComposioEmailProxy(this.getProxy(), toolkit);
601
+ }
602
+ createPaymentsProxy(context) {
603
+ const toolkit = resolveToolkit(context.spec.meta.key);
604
+ return new ComposioPaymentsProxy(this.getProxy(), toolkit);
605
+ }
606
+ createProjectManagementProxy(context) {
607
+ const toolkit = resolveToolkit(context.spec.meta.key);
608
+ return new ComposioProjectManagementProxy(this.getProxy(), toolkit);
609
+ }
610
+ createCalendarProxy(context) {
611
+ const toolkit = resolveToolkit(context.spec.meta.key);
612
+ return new ComposioCalendarProxy(this.getProxy(), toolkit);
613
+ }
614
+ createGenericProxy(context) {
615
+ const toolkit = resolveToolkit(context.spec.meta.key);
616
+ return new ComposioGenericProxy(this.getProxy(), toolkit);
617
+ }
618
+ getProxy() {
619
+ return this.preferredTransport === "sdk" ? this.sdkProvider : this.mcpProvider;
620
+ }
621
+ }
622
+
2
623
  // src/impls/elevenlabs-voice.ts
3
624
  import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
4
625
  var FORMAT_MAP = {
@@ -2016,154 +2637,478 @@ async function safeReadError3(response) {
2016
2637
  }
2017
2638
  }
2018
2639
 
2019
- // src/impls/health/base-health-provider.ts
2020
- class BaseHealthProvider {
2021
- providerKey;
2022
- transport;
2023
- apiBaseUrl;
2024
- mcpUrl;
2025
- apiKey;
2026
- accessToken;
2027
- mcpAccessToken;
2028
- webhookSecret;
2029
- fetchFn;
2030
- mcpRequestId = 0;
2031
- constructor(options) {
2032
- this.providerKey = options.providerKey;
2033
- this.transport = options.transport;
2034
- this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
2035
- this.mcpUrl = options.mcpUrl;
2036
- this.apiKey = options.apiKey;
2037
- this.accessToken = options.accessToken;
2038
- this.mcpAccessToken = options.mcpAccessToken;
2039
- this.webhookSecret = options.webhookSecret;
2040
- this.fetchFn = options.fetchFn ?? fetch;
2041
- }
2042
- async listActivities(params) {
2043
- const result = await this.fetchList("activities", params);
2044
- return {
2045
- activities: result.items,
2046
- nextCursor: result.nextCursor,
2047
- hasMore: result.hasMore,
2048
- source: this.currentSource()
2049
- };
2640
+ // src/impls/health/provider-normalizers.ts
2641
+ var DEFAULT_LIST_KEYS = [
2642
+ "items",
2643
+ "data",
2644
+ "records",
2645
+ "activities",
2646
+ "workouts",
2647
+ "sleep",
2648
+ "biometrics",
2649
+ "nutrition"
2650
+ ];
2651
+ function asRecord(value) {
2652
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2653
+ return;
2050
2654
  }
2051
- async listWorkouts(params) {
2052
- const result = await this.fetchList("workouts", params);
2053
- return {
2054
- workouts: result.items,
2055
- nextCursor: result.nextCursor,
2056
- hasMore: result.hasMore,
2057
- source: this.currentSource()
2058
- };
2655
+ return value;
2656
+ }
2657
+ function asArray2(value) {
2658
+ return Array.isArray(value) ? value : undefined;
2659
+ }
2660
+ function readString2(record, keys) {
2661
+ if (!record)
2662
+ return;
2663
+ for (const key of keys) {
2664
+ const value = record[key];
2665
+ if (typeof value === "string" && value.trim().length > 0) {
2666
+ return value;
2667
+ }
2059
2668
  }
2060
- async listSleep(params) {
2061
- const result = await this.fetchList("sleep", params);
2062
- return {
2063
- sleep: result.items,
2064
- nextCursor: result.nextCursor,
2065
- hasMore: result.hasMore,
2066
- source: this.currentSource()
2067
- };
2669
+ return;
2670
+ }
2671
+ function readNumber(record, keys) {
2672
+ if (!record)
2673
+ return;
2674
+ for (const key of keys) {
2675
+ const value = record[key];
2676
+ if (typeof value === "number" && Number.isFinite(value)) {
2677
+ return value;
2678
+ }
2679
+ if (typeof value === "string" && value.trim().length > 0) {
2680
+ const parsed = Number(value);
2681
+ if (Number.isFinite(parsed)) {
2682
+ return parsed;
2683
+ }
2684
+ }
2068
2685
  }
2069
- async listBiometrics(params) {
2070
- const result = await this.fetchList("biometrics", params);
2071
- return {
2072
- biometrics: result.items,
2073
- nextCursor: result.nextCursor,
2074
- hasMore: result.hasMore,
2075
- source: this.currentSource()
2076
- };
2686
+ return;
2687
+ }
2688
+ function readBoolean2(record, keys) {
2689
+ if (!record)
2690
+ return;
2691
+ for (const key of keys) {
2692
+ const value = record[key];
2693
+ if (typeof value === "boolean") {
2694
+ return value;
2695
+ }
2077
2696
  }
2078
- async listNutrition(params) {
2079
- const result = await this.fetchList("nutrition", params);
2080
- return {
2081
- nutrition: result.items,
2082
- nextCursor: result.nextCursor,
2083
- hasMore: result.hasMore,
2084
- source: this.currentSource()
2085
- };
2697
+ return;
2698
+ }
2699
+ function extractList(payload, listKeys = DEFAULT_LIST_KEYS) {
2700
+ const root = asRecord(payload);
2701
+ if (!root) {
2702
+ return asArray2(payload)?.map((item) => asRecord(item)).filter((item) => Boolean(item)) ?? [];
2703
+ }
2704
+ for (const key of listKeys) {
2705
+ const arrayValue = asArray2(root[key]);
2706
+ if (!arrayValue)
2707
+ continue;
2708
+ return arrayValue.map((item) => asRecord(item)).filter((item) => Boolean(item));
2709
+ }
2710
+ return [];
2711
+ }
2712
+ function extractPagination(payload) {
2713
+ const root = asRecord(payload);
2714
+ const nestedPagination = asRecord(root?.pagination);
2715
+ const nextCursor = readString2(nestedPagination, ["nextCursor", "next_cursor"]) ?? readString2(root, [
2716
+ "nextCursor",
2717
+ "next_cursor",
2718
+ "cursor",
2719
+ "next_page_token"
2720
+ ]);
2721
+ const hasMore = readBoolean2(nestedPagination, ["hasMore", "has_more"]) ?? readBoolean2(root, ["hasMore", "has_more"]);
2722
+ return {
2723
+ nextCursor,
2724
+ hasMore: hasMore ?? Boolean(nextCursor)
2725
+ };
2726
+ }
2727
+ function toHealthActivity(item, context, fallbackType = "activity") {
2728
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:${fallbackType}`;
2729
+ const id = readString2(item, ["id", "uuid", "workout_id"]) ?? `${context.providerKey}:activity:${externalId}`;
2730
+ return {
2731
+ id,
2732
+ externalId,
2733
+ tenantId: context.tenantId,
2734
+ connectionId: context.connectionId ?? "unknown",
2735
+ userId: readString2(item, ["user_id", "userId", "athlete_id"]),
2736
+ providerKey: context.providerKey,
2737
+ activityType: readString2(item, ["activity_type", "type", "sport_type", "sport"]) ?? fallbackType,
2738
+ startedAt: readIsoDate(item, [
2739
+ "started_at",
2740
+ "start_time",
2741
+ "start_date",
2742
+ "created_at"
2743
+ ]),
2744
+ endedAt: readIsoDate(item, ["ended_at", "end_time"]),
2745
+ durationSeconds: readNumber(item, [
2746
+ "duration_seconds",
2747
+ "duration",
2748
+ "elapsed_time"
2749
+ ]),
2750
+ distanceMeters: readNumber(item, ["distance_meters", "distance"]),
2751
+ caloriesKcal: readNumber(item, [
2752
+ "calories_kcal",
2753
+ "calories",
2754
+ "active_kilocalories"
2755
+ ]),
2756
+ steps: readNumber(item, ["steps"])?.valueOf(),
2757
+ metadata: item
2758
+ };
2759
+ }
2760
+ function toHealthWorkout(item, context, fallbackType = "workout") {
2761
+ const activity = toHealthActivity(item, context, fallbackType);
2762
+ return {
2763
+ id: activity.id,
2764
+ externalId: activity.externalId,
2765
+ tenantId: activity.tenantId,
2766
+ connectionId: activity.connectionId,
2767
+ userId: activity.userId,
2768
+ providerKey: activity.providerKey,
2769
+ workoutType: readString2(item, [
2770
+ "workout_type",
2771
+ "sport_type",
2772
+ "type",
2773
+ "activity_type"
2774
+ ]) ?? fallbackType,
2775
+ startedAt: activity.startedAt,
2776
+ endedAt: activity.endedAt,
2777
+ durationSeconds: activity.durationSeconds,
2778
+ distanceMeters: activity.distanceMeters,
2779
+ caloriesKcal: activity.caloriesKcal,
2780
+ averageHeartRateBpm: readNumber(item, [
2781
+ "average_heart_rate",
2782
+ "avg_hr",
2783
+ "average_heart_rate_bpm"
2784
+ ]),
2785
+ maxHeartRateBpm: readNumber(item, [
2786
+ "max_heart_rate",
2787
+ "max_hr",
2788
+ "max_heart_rate_bpm"
2789
+ ]),
2790
+ metadata: item
2791
+ };
2792
+ }
2793
+ function toHealthSleep(item, context) {
2794
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:sleep`;
2795
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:sleep:${externalId}`;
2796
+ const startedAt = readIsoDate(item, ["started_at", "start_time", "bedtime_start", "start"]) ?? new Date(0).toISOString();
2797
+ const endedAt = readIsoDate(item, ["ended_at", "end_time", "bedtime_end", "end"]) ?? startedAt;
2798
+ return {
2799
+ id,
2800
+ externalId,
2801
+ tenantId: context.tenantId,
2802
+ connectionId: context.connectionId ?? "unknown",
2803
+ userId: readString2(item, ["user_id", "userId"]),
2804
+ providerKey: context.providerKey,
2805
+ startedAt,
2806
+ endedAt,
2807
+ durationSeconds: readNumber(item, [
2808
+ "duration_seconds",
2809
+ "duration",
2810
+ "total_sleep_duration"
2811
+ ]),
2812
+ deepSleepSeconds: readNumber(item, [
2813
+ "deep_sleep_seconds",
2814
+ "deep_sleep_duration"
2815
+ ]),
2816
+ lightSleepSeconds: readNumber(item, [
2817
+ "light_sleep_seconds",
2818
+ "light_sleep_duration"
2819
+ ]),
2820
+ remSleepSeconds: readNumber(item, [
2821
+ "rem_sleep_seconds",
2822
+ "rem_sleep_duration"
2823
+ ]),
2824
+ awakeSeconds: readNumber(item, ["awake_seconds", "awake_time"]),
2825
+ sleepScore: readNumber(item, ["sleep_score", "score"]),
2826
+ metadata: item
2827
+ };
2828
+ }
2829
+ function toHealthBiometric(item, context, metricTypeFallback = "metric") {
2830
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:biometric`;
2831
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:biometric:${externalId}`;
2832
+ return {
2833
+ id,
2834
+ externalId,
2835
+ tenantId: context.tenantId,
2836
+ connectionId: context.connectionId ?? "unknown",
2837
+ userId: readString2(item, ["user_id", "userId"]),
2838
+ providerKey: context.providerKey,
2839
+ metricType: readString2(item, ["metric_type", "metric", "type", "name"]) ?? metricTypeFallback,
2840
+ value: readNumber(item, ["value", "score", "measurement"]) ?? 0,
2841
+ unit: readString2(item, ["unit"]),
2842
+ measuredAt: readIsoDate(item, ["measured_at", "timestamp", "created_at"]) ?? new Date().toISOString(),
2843
+ metadata: item
2844
+ };
2845
+ }
2846
+ function toHealthNutrition(item, context) {
2847
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:nutrition`;
2848
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:nutrition:${externalId}`;
2849
+ return {
2850
+ id,
2851
+ externalId,
2852
+ tenantId: context.tenantId,
2853
+ connectionId: context.connectionId ?? "unknown",
2854
+ userId: readString2(item, ["user_id", "userId"]),
2855
+ providerKey: context.providerKey,
2856
+ loggedAt: readIsoDate(item, ["logged_at", "created_at", "date", "timestamp"]) ?? new Date().toISOString(),
2857
+ caloriesKcal: readNumber(item, ["calories_kcal", "calories"]),
2858
+ proteinGrams: readNumber(item, ["protein_grams", "protein"]),
2859
+ carbsGrams: readNumber(item, ["carbs_grams", "carbs"]),
2860
+ fatGrams: readNumber(item, ["fat_grams", "fat"]),
2861
+ fiberGrams: readNumber(item, ["fiber_grams", "fiber"]),
2862
+ hydrationMl: readNumber(item, ["hydration_ml", "water_ml", "water"]),
2863
+ metadata: item
2864
+ };
2865
+ }
2866
+ function toHealthConnectionStatus(payload, params, source) {
2867
+ const record = asRecord(payload);
2868
+ const rawStatus = readString2(record, ["status", "connection_status", "health"]) ?? "healthy";
2869
+ return {
2870
+ tenantId: params.tenantId,
2871
+ connectionId: params.connectionId,
2872
+ status: rawStatus === "healthy" || rawStatus === "degraded" || rawStatus === "error" || rawStatus === "disconnected" ? rawStatus : "healthy",
2873
+ source,
2874
+ lastCheckedAt: readIsoDate(record, ["last_checked_at", "lastCheckedAt"]) ?? new Date().toISOString(),
2875
+ errorCode: readString2(record, ["error_code", "errorCode"]),
2876
+ errorMessage: readString2(record, ["error_message", "errorMessage"]),
2877
+ metadata: asRecord(record?.metadata)
2878
+ };
2879
+ }
2880
+ function toHealthWebhookEvent(payload, providerKey, verified) {
2881
+ const record = asRecord(payload);
2882
+ const entityType = readString2(record, ["entity_type", "entityType", "type"]);
2883
+ const normalizedEntityType = entityType === "activity" || entityType === "workout" || entityType === "sleep" || entityType === "biometric" || entityType === "nutrition" ? entityType : undefined;
2884
+ return {
2885
+ providerKey,
2886
+ eventType: readString2(record, ["event_type", "eventType", "event"]),
2887
+ externalEntityId: readString2(record, [
2888
+ "external_entity_id",
2889
+ "externalEntityId",
2890
+ "entity_id",
2891
+ "entityId",
2892
+ "id"
2893
+ ]),
2894
+ entityType: normalizedEntityType,
2895
+ receivedAt: new Date().toISOString(),
2896
+ verified,
2897
+ payload,
2898
+ metadata: asRecord(record?.metadata)
2899
+ };
2900
+ }
2901
+ function readIsoDate(record, keys) {
2902
+ const value = readString2(record, keys);
2903
+ if (!value)
2904
+ return;
2905
+ const parsed = new Date(value);
2906
+ if (Number.isNaN(parsed.getTime()))
2907
+ return;
2908
+ return parsed.toISOString();
2909
+ }
2910
+
2911
+ // src/impls/health/base-health-provider.ts
2912
+ class HealthProviderCapabilityError extends Error {
2913
+ code = "NOT_SUPPORTED";
2914
+ constructor(message) {
2915
+ super(message);
2916
+ this.name = "HealthProviderCapabilityError";
2917
+ }
2918
+ }
2919
+
2920
+ class BaseHealthProvider {
2921
+ providerKey;
2922
+ transport;
2923
+ apiBaseUrl;
2924
+ mcpUrl;
2925
+ apiKey;
2926
+ accessToken;
2927
+ refreshToken;
2928
+ mcpAccessToken;
2929
+ webhookSecret;
2930
+ webhookSignatureHeader;
2931
+ route;
2932
+ aggregatorKey;
2933
+ oauth;
2934
+ fetchFn;
2935
+ mcpRequestId = 0;
2936
+ constructor(options) {
2937
+ this.providerKey = options.providerKey;
2938
+ this.transport = options.transport;
2939
+ this.apiBaseUrl = options.apiBaseUrl;
2940
+ this.mcpUrl = options.mcpUrl;
2941
+ this.apiKey = options.apiKey;
2942
+ this.accessToken = options.accessToken;
2943
+ this.refreshToken = options.oauth?.refreshToken;
2944
+ this.mcpAccessToken = options.mcpAccessToken;
2945
+ this.webhookSecret = options.webhookSecret;
2946
+ this.webhookSignatureHeader = options.webhookSignatureHeader ?? "x-webhook-signature";
2947
+ this.route = options.route ?? "primary";
2948
+ this.aggregatorKey = options.aggregatorKey;
2949
+ this.oauth = options.oauth ?? {};
2950
+ this.fetchFn = options.fetchFn ?? fetch;
2951
+ }
2952
+ async listActivities(_params) {
2953
+ throw this.unsupported("activities");
2954
+ }
2955
+ async listWorkouts(_params) {
2956
+ throw this.unsupported("workouts");
2957
+ }
2958
+ async listSleep(_params) {
2959
+ throw this.unsupported("sleep");
2960
+ }
2961
+ async listBiometrics(_params) {
2962
+ throw this.unsupported("biometrics");
2963
+ }
2964
+ async listNutrition(_params) {
2965
+ throw this.unsupported("nutrition");
2086
2966
  }
2087
2967
  async getConnectionStatus(params) {
2088
- const payload = await this.fetchRecord("connection/status", params);
2089
- const status = readString2(payload, "status") ?? "healthy";
2090
- return {
2091
- tenantId: params.tenantId,
2092
- connectionId: params.connectionId,
2093
- status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
2094
- source: this.currentSource(),
2095
- lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
2096
- errorCode: readString2(payload, "errorCode"),
2097
- errorMessage: readString2(payload, "errorMessage"),
2098
- metadata: asRecord(payload.metadata)
2099
- };
2968
+ return this.fetchConnectionStatus(params, {
2969
+ mcpTool: `${this.providerSlug()}_connection_status`
2970
+ });
2100
2971
  }
2101
2972
  async syncActivities(params) {
2102
- return this.sync("activities", params);
2973
+ return this.syncFromList(() => this.listActivities(params));
2103
2974
  }
2104
2975
  async syncWorkouts(params) {
2105
- return this.sync("workouts", params);
2976
+ return this.syncFromList(() => this.listWorkouts(params));
2106
2977
  }
2107
2978
  async syncSleep(params) {
2108
- return this.sync("sleep", params);
2979
+ return this.syncFromList(() => this.listSleep(params));
2109
2980
  }
2110
2981
  async syncBiometrics(params) {
2111
- return this.sync("biometrics", params);
2982
+ return this.syncFromList(() => this.listBiometrics(params));
2112
2983
  }
2113
2984
  async syncNutrition(params) {
2114
- return this.sync("nutrition", params);
2985
+ return this.syncFromList(() => this.listNutrition(params));
2115
2986
  }
2116
2987
  async parseWebhook(request) {
2117
2988
  const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
2118
- const body = asRecord(payload);
2119
- return {
2120
- providerKey: this.providerKey,
2121
- eventType: readString2(body, "eventType") ?? readString2(body, "event"),
2122
- externalEntityId: readString2(body, "externalEntityId") ?? readString2(body, "entityId"),
2123
- entityType: normalizeEntityType(readString2(body, "entityType") ?? readString2(body, "type")),
2124
- receivedAt: new Date().toISOString(),
2125
- verified: await this.verifyWebhook(request),
2126
- payload
2127
- };
2989
+ const verified = await this.verifyWebhook(request);
2990
+ return toHealthWebhookEvent(payload, this.providerKey, verified);
2128
2991
  }
2129
2992
  async verifyWebhook(request) {
2130
- if (!this.webhookSecret) {
2993
+ if (!this.webhookSecret)
2131
2994
  return true;
2132
- }
2133
- const signature = readHeader(request.headers, "x-webhook-signature");
2995
+ const signature = readHeader(request.headers, this.webhookSignatureHeader);
2134
2996
  return signature === this.webhookSecret;
2135
2997
  }
2136
- async fetchList(resource, params) {
2137
- const payload = await this.fetchRecord(resource, params);
2138
- const items = asArray2(payload.items) ?? asArray2(payload[resource]) ?? asArray2(payload.records) ?? [];
2998
+ async fetchActivities(params, config) {
2999
+ const response = await this.fetchList(params, config);
2139
3000
  return {
2140
- items,
2141
- nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
2142
- hasMore: readBoolean2(payload, "hasMore")
3001
+ activities: response.items,
3002
+ nextCursor: response.nextCursor,
3003
+ hasMore: response.hasMore,
3004
+ source: this.currentSource()
3005
+ };
3006
+ }
3007
+ async fetchWorkouts(params, config) {
3008
+ const response = await this.fetchList(params, config);
3009
+ return {
3010
+ workouts: response.items,
3011
+ nextCursor: response.nextCursor,
3012
+ hasMore: response.hasMore,
3013
+ source: this.currentSource()
2143
3014
  };
2144
3015
  }
2145
- async sync(resource, params) {
2146
- const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
3016
+ async fetchSleep(params, config) {
3017
+ const response = await this.fetchList(params, config);
2147
3018
  return {
2148
- synced: readNumber(payload, "synced") ?? 0,
2149
- failed: readNumber(payload, "failed") ?? 0,
2150
- nextCursor: readString2(payload, "nextCursor"),
2151
- errors: asArray2(payload.errors)?.map((item) => String(item)),
3019
+ sleep: response.items,
3020
+ nextCursor: response.nextCursor,
3021
+ hasMore: response.hasMore,
2152
3022
  source: this.currentSource()
2153
3023
  };
2154
3024
  }
2155
- async fetchRecord(resource, params, method = "GET") {
2156
- if (this.transport.endsWith("mcp")) {
2157
- return this.callMcpTool(resource, params);
3025
+ async fetchBiometrics(params, config) {
3026
+ const response = await this.fetchList(params, config);
3027
+ return {
3028
+ biometrics: response.items,
3029
+ nextCursor: response.nextCursor,
3030
+ hasMore: response.hasMore,
3031
+ source: this.currentSource()
3032
+ };
3033
+ }
3034
+ async fetchNutrition(params, config) {
3035
+ const response = await this.fetchList(params, config);
3036
+ return {
3037
+ nutrition: response.items,
3038
+ nextCursor: response.nextCursor,
3039
+ hasMore: response.hasMore,
3040
+ source: this.currentSource()
3041
+ };
3042
+ }
3043
+ async fetchConnectionStatus(params, config) {
3044
+ const payload = await this.fetchPayload(config, params);
3045
+ return toHealthConnectionStatus(payload, params, this.currentSource());
3046
+ }
3047
+ currentSource() {
3048
+ return {
3049
+ providerKey: this.providerKey,
3050
+ transport: this.transport,
3051
+ route: this.route,
3052
+ aggregatorKey: this.aggregatorKey
3053
+ };
3054
+ }
3055
+ providerSlug() {
3056
+ return this.providerKey.replace("health.", "").replace(/-/g, "_");
3057
+ }
3058
+ unsupported(capability) {
3059
+ return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
3060
+ }
3061
+ async syncFromList(executor) {
3062
+ const result = await executor();
3063
+ const records = countResultRecords(result);
3064
+ return {
3065
+ synced: records,
3066
+ failed: 0,
3067
+ nextCursor: undefined,
3068
+ source: result.source
3069
+ };
3070
+ }
3071
+ async fetchList(params, config) {
3072
+ const payload = await this.fetchPayload(config, params);
3073
+ const items = extractList(payload, config.listKeys).map((item) => config.mapItem(item, params)).filter((item) => Boolean(item));
3074
+ const pagination = extractPagination(payload);
3075
+ return {
3076
+ items,
3077
+ nextCursor: pagination.nextCursor,
3078
+ hasMore: pagination.hasMore
3079
+ };
3080
+ }
3081
+ async fetchPayload(config, params) {
3082
+ const method = config.method ?? "GET";
3083
+ const query = config.buildQuery?.(params);
3084
+ const body = config.buildBody?.(params);
3085
+ if (this.isMcpTransport()) {
3086
+ return this.callMcpTool(config.mcpTool, {
3087
+ ...query ?? {},
3088
+ ...body ?? {}
3089
+ });
3090
+ }
3091
+ if (!config.apiPath || !this.apiBaseUrl) {
3092
+ throw new Error(`${this.providerKey} transport is missing an API path.`);
2158
3093
  }
2159
- const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
2160
- if (method === "GET") {
2161
- for (const [key, value] of Object.entries(params)) {
3094
+ if (method === "POST") {
3095
+ return this.requestApi(config.apiPath, "POST", undefined, body);
3096
+ }
3097
+ return this.requestApi(config.apiPath, "GET", query, undefined);
3098
+ }
3099
+ isMcpTransport() {
3100
+ return this.transport.endsWith("mcp") || this.transport === "unofficial";
3101
+ }
3102
+ async requestApi(path, method, query, body) {
3103
+ const url = new URL(path, ensureTrailingSlash(this.apiBaseUrl ?? ""));
3104
+ if (query) {
3105
+ for (const [key, value] of Object.entries(query)) {
2162
3106
  if (value == null)
2163
3107
  continue;
2164
3108
  if (Array.isArray(value)) {
2165
- value.forEach((item) => {
2166
- url.searchParams.append(key, String(item));
3109
+ value.forEach((entry) => {
3110
+ if (entry != null)
3111
+ url.searchParams.append(key, String(entry));
2167
3112
  });
2168
3113
  continue;
2169
3114
  }
@@ -2172,22 +3117,22 @@ class BaseHealthProvider {
2172
3117
  }
2173
3118
  const response = await this.fetchFn(url, {
2174
3119
  method,
2175
- headers: {
2176
- "Content-Type": "application/json",
2177
- ...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
2178
- },
2179
- body: method === "POST" ? JSON.stringify(params) : undefined
3120
+ headers: this.authorizationHeaders(),
3121
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
2180
3122
  });
2181
- if (!response.ok) {
2182
- const errorBody = await safeResponseText(response);
2183
- throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
3123
+ if (response.status === 401 && await this.refreshAccessToken()) {
3124
+ const retryResponse = await this.fetchFn(url, {
3125
+ method,
3126
+ headers: this.authorizationHeaders(),
3127
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
3128
+ });
3129
+ return this.readResponsePayload(retryResponse, path);
2184
3130
  }
2185
- const data = await response.json();
2186
- return asRecord(data) ?? {};
3131
+ return this.readResponsePayload(response, path);
2187
3132
  }
2188
- async callMcpTool(resource, params) {
3133
+ async callMcpTool(toolName, args) {
2189
3134
  if (!this.mcpUrl) {
2190
- return {};
3135
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
2191
3136
  }
2192
3137
  const response = await this.fetchFn(this.mcpUrl, {
2193
3138
  method: "POST",
@@ -2200,78 +3145,103 @@ class BaseHealthProvider {
2200
3145
  id: ++this.mcpRequestId,
2201
3146
  method: "tools/call",
2202
3147
  params: {
2203
- name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
2204
- arguments: params
3148
+ name: toolName,
3149
+ arguments: args
2205
3150
  }
2206
3151
  })
2207
3152
  });
2208
- if (!response.ok) {
2209
- const errorBody = await safeResponseText(response);
2210
- throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
2211
- }
2212
- const rpcPayload = await response.json();
2213
- const rpc = asRecord(rpcPayload);
2214
- const result = asRecord(rpc?.result) ?? {};
2215
- const structured = asRecord(result.structuredContent);
2216
- if (structured)
2217
- return structured;
2218
- const data = asRecord(result.data);
2219
- if (data)
2220
- return data;
2221
- return result;
2222
- }
2223
- currentSource() {
3153
+ const payload = await this.readResponsePayload(response, toolName);
3154
+ const rpcEnvelope = asRecord(payload);
3155
+ if (!rpcEnvelope)
3156
+ return payload;
3157
+ const rpcResult = asRecord(rpcEnvelope.result);
3158
+ if (rpcResult) {
3159
+ return rpcResult.structuredContent ?? rpcResult.data ?? rpcResult;
3160
+ }
3161
+ return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
3162
+ }
3163
+ authorizationHeaders() {
3164
+ const token = this.accessToken ?? this.apiKey;
2224
3165
  return {
2225
- providerKey: this.providerKey,
2226
- transport: this.transport,
2227
- route: "primary"
3166
+ "Content-Type": "application/json",
3167
+ ...token ? { Authorization: `Bearer ${token}` } : {}
2228
3168
  };
2229
3169
  }
2230
- }
2231
- function safeJsonParse(raw) {
2232
- try {
2233
- return JSON.parse(raw);
2234
- } catch {
2235
- return { rawBody: raw };
3170
+ async refreshAccessToken() {
3171
+ if (!this.oauth.tokenUrl || !this.refreshToken) {
3172
+ return false;
3173
+ }
3174
+ const tokenUrl = new URL(this.oauth.tokenUrl);
3175
+ const body = new URLSearchParams({
3176
+ grant_type: "refresh_token",
3177
+ refresh_token: this.refreshToken,
3178
+ ...this.oauth.clientId ? { client_id: this.oauth.clientId } : {},
3179
+ ...this.oauth.clientSecret ? { client_secret: this.oauth.clientSecret } : {}
3180
+ });
3181
+ const response = await this.fetchFn(tokenUrl, {
3182
+ method: "POST",
3183
+ headers: {
3184
+ "Content-Type": "application/x-www-form-urlencoded"
3185
+ },
3186
+ body: body.toString()
3187
+ });
3188
+ if (!response.ok) {
3189
+ return false;
3190
+ }
3191
+ const payload = await response.json();
3192
+ this.accessToken = payload.access_token;
3193
+ this.refreshToken = payload.refresh_token ?? this.refreshToken;
3194
+ if (typeof payload.expires_in === "number") {
3195
+ this.oauth.tokenExpiresAt = new Date(Date.now() + payload.expires_in * 1000).toISOString();
3196
+ }
3197
+ return Boolean(this.accessToken);
3198
+ }
3199
+ async readResponsePayload(response, context) {
3200
+ if (!response.ok) {
3201
+ const message = await safeReadText2(response);
3202
+ throw new Error(`${this.providerKey} request ${context} failed (${response.status}): ${message}`);
3203
+ }
3204
+ if (response.status === 204) {
3205
+ return {};
3206
+ }
3207
+ return response.json();
2236
3208
  }
2237
3209
  }
2238
3210
  function readHeader(headers, key) {
2239
- const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
2240
- if (!match)
3211
+ const target = key.toLowerCase();
3212
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
3213
+ if (!entry)
2241
3214
  return;
2242
- const value = match[1];
3215
+ const value = entry[1];
2243
3216
  return Array.isArray(value) ? value[0] : value;
2244
3217
  }
2245
- function normalizeEntityType(value) {
2246
- if (!value)
2247
- return;
2248
- if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
2249
- return value;
2250
- }
2251
- return;
2252
- }
2253
- function asRecord(value) {
2254
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2255
- return;
3218
+ function countResultRecords(result) {
3219
+ const listKeys = [
3220
+ "activities",
3221
+ "workouts",
3222
+ "sleep",
3223
+ "biometrics",
3224
+ "nutrition"
3225
+ ];
3226
+ for (const key of listKeys) {
3227
+ const value = result[key];
3228
+ if (Array.isArray(value)) {
3229
+ return value.length;
3230
+ }
2256
3231
  }
2257
- return value;
3232
+ return 0;
2258
3233
  }
2259
- function asArray2(value) {
2260
- return Array.isArray(value) ? value : undefined;
2261
- }
2262
- function readString2(record, key) {
2263
- const value = record?.[key];
2264
- return typeof value === "string" ? value : undefined;
2265
- }
2266
- function readBoolean2(record, key) {
2267
- const value = record?.[key];
2268
- return typeof value === "boolean" ? value : undefined;
3234
+ function ensureTrailingSlash(value) {
3235
+ return value.endsWith("/") ? value : `${value}/`;
2269
3236
  }
2270
- function readNumber(record, key) {
2271
- const value = record?.[key];
2272
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
3237
+ function safeJsonParse(raw) {
3238
+ try {
3239
+ return JSON.parse(raw);
3240
+ } catch {
3241
+ return { rawBody: raw };
3242
+ }
2273
3243
  }
2274
- async function safeResponseText(response) {
3244
+ async function safeReadText2(response) {
2275
3245
  try {
2276
3246
  return await response.text();
2277
3247
  } catch {
@@ -2279,19 +3249,103 @@ async function safeResponseText(response) {
2279
3249
  }
2280
3250
  }
2281
3251
 
2282
- // src/impls/health/providers.ts
2283
- function createProviderOptions(options, fallbackTransport) {
3252
+ // src/impls/health/official-health-providers.ts
3253
+ function buildSharedQuery(params) {
2284
3254
  return {
2285
- ...options,
2286
- transport: options.transport ?? fallbackTransport
3255
+ tenantId: params.tenantId,
3256
+ connectionId: params.connectionId,
3257
+ userId: params.userId,
3258
+ from: params.from,
3259
+ to: params.to,
3260
+ cursor: params.cursor,
3261
+ pageSize: params.pageSize
3262
+ };
3263
+ }
3264
+ function withMetricTypes(params) {
3265
+ return {
3266
+ ...buildSharedQuery(params),
3267
+ metricTypes: params.metricTypes
2287
3268
  };
2288
3269
  }
2289
3270
 
2290
3271
  class OpenWearablesHealthProvider extends BaseHealthProvider {
3272
+ upstreamProvider;
2291
3273
  constructor(options) {
2292
3274
  super({
2293
- providerKey: "health.openwearables",
2294
- ...createProviderOptions(options, "aggregator-api")
3275
+ providerKey: options.providerKey ?? "health.openwearables",
3276
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
3277
+ webhookSignatureHeader: "x-openwearables-signature",
3278
+ ...options
3279
+ });
3280
+ this.upstreamProvider = options.upstreamProvider;
3281
+ }
3282
+ async listActivities(params) {
3283
+ return this.fetchActivities(params, {
3284
+ apiPath: "/v1/activities",
3285
+ mcpTool: "openwearables_list_activities",
3286
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
3287
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
3288
+ });
3289
+ }
3290
+ async listWorkouts(params) {
3291
+ return this.fetchWorkouts(params, {
3292
+ apiPath: "/v1/workouts",
3293
+ mcpTool: "openwearables_list_workouts",
3294
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
3295
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3296
+ });
3297
+ }
3298
+ async listSleep(params) {
3299
+ return this.fetchSleep(params, {
3300
+ apiPath: "/v1/sleep",
3301
+ mcpTool: "openwearables_list_sleep",
3302
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
3303
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
3304
+ });
3305
+ }
3306
+ async listBiometrics(params) {
3307
+ return this.fetchBiometrics(params, {
3308
+ apiPath: "/v1/biometrics",
3309
+ mcpTool: "openwearables_list_biometrics",
3310
+ buildQuery: (input) => this.withUpstreamProvider(withMetricTypes(input)),
3311
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input))
3312
+ });
3313
+ }
3314
+ async listNutrition(params) {
3315
+ return this.fetchNutrition(params, {
3316
+ apiPath: "/v1/nutrition",
3317
+ mcpTool: "openwearables_list_nutrition",
3318
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
3319
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
3320
+ });
3321
+ }
3322
+ async getConnectionStatus(params) {
3323
+ return this.fetchConnectionStatus(params, {
3324
+ apiPath: `/v1/connections/${encodeURIComponent(params.connectionId)}/status`,
3325
+ mcpTool: "openwearables_connection_status"
3326
+ });
3327
+ }
3328
+ withUpstreamProvider(query) {
3329
+ return {
3330
+ ...query,
3331
+ ...this.upstreamProvider ? { upstreamProvider: this.upstreamProvider } : {}
3332
+ };
3333
+ }
3334
+ context(params) {
3335
+ return {
3336
+ tenantId: params.tenantId,
3337
+ connectionId: params.connectionId,
3338
+ providerKey: this.providerKey
3339
+ };
3340
+ }
3341
+ }
3342
+
3343
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
3344
+ constructor(options) {
3345
+ super({
3346
+ ...options,
3347
+ providerKey: "health.apple-health",
3348
+ upstreamProvider: "apple-health"
2295
3349
  });
2296
3350
  }
2297
3351
  }
@@ -2300,112 +3354,425 @@ class WhoopHealthProvider extends BaseHealthProvider {
2300
3354
  constructor(options) {
2301
3355
  super({
2302
3356
  providerKey: "health.whoop",
2303
- ...createProviderOptions(options, "official-api")
3357
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.prod.whoop.com",
3358
+ webhookSignatureHeader: "x-whoop-signature",
3359
+ oauth: {
3360
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.prod.whoop.com/oauth/oauth2/token",
3361
+ ...options.oauth
3362
+ },
3363
+ ...options
3364
+ });
3365
+ }
3366
+ async listActivities(params) {
3367
+ return this.fetchActivities(params, {
3368
+ apiPath: "/v2/activity/workout",
3369
+ mcpTool: "whoop_list_activities",
3370
+ buildQuery: buildSharedQuery,
3371
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "workout")
2304
3372
  });
2305
3373
  }
3374
+ async listWorkouts(params) {
3375
+ return this.fetchWorkouts(params, {
3376
+ apiPath: "/v2/activity/workout",
3377
+ mcpTool: "whoop_list_workouts",
3378
+ buildQuery: buildSharedQuery,
3379
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3380
+ });
3381
+ }
3382
+ async listSleep(params) {
3383
+ return this.fetchSleep(params, {
3384
+ apiPath: "/v2/activity/sleep",
3385
+ mcpTool: "whoop_list_sleep",
3386
+ buildQuery: buildSharedQuery,
3387
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
3388
+ });
3389
+ }
3390
+ async listBiometrics(params) {
3391
+ return this.fetchBiometrics(params, {
3392
+ apiPath: "/v2/recovery",
3393
+ mcpTool: "whoop_list_biometrics",
3394
+ buildQuery: withMetricTypes,
3395
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "recovery_score")
3396
+ });
3397
+ }
3398
+ async listNutrition(_params) {
3399
+ throw this.unsupported("nutrition");
3400
+ }
3401
+ async getConnectionStatus(params) {
3402
+ return this.fetchConnectionStatus(params, {
3403
+ apiPath: "/v2/user/profile/basic",
3404
+ mcpTool: "whoop_connection_status"
3405
+ });
3406
+ }
3407
+ context(params) {
3408
+ return {
3409
+ tenantId: params.tenantId,
3410
+ connectionId: params.connectionId,
3411
+ providerKey: this.providerKey
3412
+ };
3413
+ }
2306
3414
  }
2307
3415
 
2308
- class AppleHealthBridgeProvider extends BaseHealthProvider {
3416
+ class OuraHealthProvider extends BaseHealthProvider {
2309
3417
  constructor(options) {
2310
3418
  super({
2311
- providerKey: "health.apple-health",
2312
- ...createProviderOptions(options, "aggregator-api")
3419
+ providerKey: "health.oura",
3420
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.ouraring.com",
3421
+ webhookSignatureHeader: "x-oura-signature",
3422
+ oauth: {
3423
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.ouraring.com/oauth/token",
3424
+ ...options.oauth
3425
+ },
3426
+ ...options
2313
3427
  });
2314
3428
  }
2315
- }
2316
-
2317
- class OuraHealthProvider extends BaseHealthProvider {
2318
- constructor(options) {
2319
- super({
2320
- providerKey: "health.oura",
2321
- ...createProviderOptions(options, "official-api")
3429
+ async listActivities(params) {
3430
+ return this.fetchActivities(params, {
3431
+ apiPath: "/v2/usercollection/daily_activity",
3432
+ mcpTool: "oura_list_activities",
3433
+ buildQuery: buildSharedQuery,
3434
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
3435
+ });
3436
+ }
3437
+ async listWorkouts(params) {
3438
+ return this.fetchWorkouts(params, {
3439
+ apiPath: "/v2/usercollection/workout",
3440
+ mcpTool: "oura_list_workouts",
3441
+ buildQuery: buildSharedQuery,
3442
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3443
+ });
3444
+ }
3445
+ async listSleep(params) {
3446
+ return this.fetchSleep(params, {
3447
+ apiPath: "/v2/usercollection/sleep",
3448
+ mcpTool: "oura_list_sleep",
3449
+ buildQuery: buildSharedQuery,
3450
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
3451
+ });
3452
+ }
3453
+ async listBiometrics(params) {
3454
+ return this.fetchBiometrics(params, {
3455
+ apiPath: "/v2/usercollection/daily_readiness",
3456
+ mcpTool: "oura_list_biometrics",
3457
+ buildQuery: withMetricTypes,
3458
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "readiness_score")
3459
+ });
3460
+ }
3461
+ async listNutrition(_params) {
3462
+ throw this.unsupported("nutrition");
3463
+ }
3464
+ async getConnectionStatus(params) {
3465
+ return this.fetchConnectionStatus(params, {
3466
+ apiPath: "/v2/usercollection/personal_info",
3467
+ mcpTool: "oura_connection_status"
2322
3468
  });
2323
3469
  }
3470
+ context(params) {
3471
+ return {
3472
+ tenantId: params.tenantId,
3473
+ connectionId: params.connectionId,
3474
+ providerKey: this.providerKey
3475
+ };
3476
+ }
2324
3477
  }
2325
3478
 
2326
3479
  class StravaHealthProvider extends BaseHealthProvider {
2327
3480
  constructor(options) {
2328
3481
  super({
2329
3482
  providerKey: "health.strava",
2330
- ...createProviderOptions(options, "official-api")
3483
+ apiBaseUrl: options.apiBaseUrl ?? "https://www.strava.com",
3484
+ webhookSignatureHeader: "x-strava-signature",
3485
+ oauth: {
3486
+ tokenUrl: options.oauth?.tokenUrl ?? "https://www.strava.com/oauth/token",
3487
+ ...options.oauth
3488
+ },
3489
+ ...options
3490
+ });
3491
+ }
3492
+ async listActivities(params) {
3493
+ return this.fetchActivities(params, {
3494
+ apiPath: "/api/v3/athlete/activities",
3495
+ mcpTool: "strava_list_activities",
3496
+ buildQuery: buildSharedQuery,
3497
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2331
3498
  });
2332
3499
  }
3500
+ async listWorkouts(params) {
3501
+ return this.fetchWorkouts(params, {
3502
+ apiPath: "/api/v3/athlete/activities",
3503
+ mcpTool: "strava_list_workouts",
3504
+ buildQuery: buildSharedQuery,
3505
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3506
+ });
3507
+ }
3508
+ async listSleep(_params) {
3509
+ throw this.unsupported("sleep");
3510
+ }
3511
+ async listBiometrics(_params) {
3512
+ throw this.unsupported("biometrics");
3513
+ }
3514
+ async listNutrition(_params) {
3515
+ throw this.unsupported("nutrition");
3516
+ }
3517
+ async getConnectionStatus(params) {
3518
+ return this.fetchConnectionStatus(params, {
3519
+ apiPath: "/api/v3/athlete",
3520
+ mcpTool: "strava_connection_status"
3521
+ });
3522
+ }
3523
+ context(params) {
3524
+ return {
3525
+ tenantId: params.tenantId,
3526
+ connectionId: params.connectionId,
3527
+ providerKey: this.providerKey
3528
+ };
3529
+ }
2333
3530
  }
2334
3531
 
2335
- class GarminHealthProvider extends BaseHealthProvider {
3532
+ class FitbitHealthProvider extends BaseHealthProvider {
2336
3533
  constructor(options) {
2337
3534
  super({
2338
- providerKey: "health.garmin",
2339
- ...createProviderOptions(options, "official-api")
3535
+ providerKey: "health.fitbit",
3536
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.fitbit.com",
3537
+ webhookSignatureHeader: "x-fitbit-signature",
3538
+ oauth: {
3539
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.fitbit.com/oauth2/token",
3540
+ ...options.oauth
3541
+ },
3542
+ ...options
3543
+ });
3544
+ }
3545
+ async listActivities(params) {
3546
+ return this.fetchActivities(params, {
3547
+ apiPath: "/1/user/-/activities/list.json",
3548
+ mcpTool: "fitbit_list_activities",
3549
+ buildQuery: buildSharedQuery,
3550
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
3551
+ });
3552
+ }
3553
+ async listWorkouts(params) {
3554
+ return this.fetchWorkouts(params, {
3555
+ apiPath: "/1/user/-/activities/list.json",
3556
+ mcpTool: "fitbit_list_workouts",
3557
+ buildQuery: buildSharedQuery,
3558
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3559
+ });
3560
+ }
3561
+ async listSleep(params) {
3562
+ return this.fetchSleep(params, {
3563
+ apiPath: "/1.2/user/-/sleep/list.json",
3564
+ mcpTool: "fitbit_list_sleep",
3565
+ buildQuery: buildSharedQuery,
3566
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
3567
+ });
3568
+ }
3569
+ async listBiometrics(params) {
3570
+ return this.fetchBiometrics(params, {
3571
+ apiPath: "/1/user/-/body/log/weight/date/today/1m.json",
3572
+ mcpTool: "fitbit_list_biometrics",
3573
+ buildQuery: withMetricTypes,
3574
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "weight")
3575
+ });
3576
+ }
3577
+ async listNutrition(params) {
3578
+ return this.fetchNutrition(params, {
3579
+ apiPath: "/1/user/-/foods/log/date/today.json",
3580
+ mcpTool: "fitbit_list_nutrition",
3581
+ buildQuery: buildSharedQuery,
3582
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
3583
+ });
3584
+ }
3585
+ async getConnectionStatus(params) {
3586
+ return this.fetchConnectionStatus(params, {
3587
+ apiPath: "/1/user/-/profile.json",
3588
+ mcpTool: "fitbit_connection_status"
2340
3589
  });
2341
3590
  }
3591
+ context(params) {
3592
+ return {
3593
+ tenantId: params.tenantId,
3594
+ connectionId: params.connectionId,
3595
+ providerKey: this.providerKey
3596
+ };
3597
+ }
2342
3598
  }
2343
3599
 
2344
- class FitbitHealthProvider extends BaseHealthProvider {
3600
+ // src/impls/health/hybrid-health-providers.ts
3601
+ var LIMITED_PROVIDER_SLUG = {
3602
+ "health.garmin": "garmin",
3603
+ "health.myfitnesspal": "myfitnesspal",
3604
+ "health.eightsleep": "eightsleep",
3605
+ "health.peloton": "peloton"
3606
+ };
3607
+ function buildSharedQuery2(params) {
3608
+ return {
3609
+ tenantId: params.tenantId,
3610
+ connectionId: params.connectionId,
3611
+ userId: params.userId,
3612
+ from: params.from,
3613
+ to: params.to,
3614
+ cursor: params.cursor,
3615
+ pageSize: params.pageSize
3616
+ };
3617
+ }
3618
+
3619
+ class GarminHealthProvider extends OpenWearablesHealthProvider {
2345
3620
  constructor(options) {
2346
3621
  super({
2347
- providerKey: "health.fitbit",
2348
- ...createProviderOptions(options, "official-api")
3622
+ ...options,
3623
+ providerKey: "health.garmin",
3624
+ upstreamProvider: "garmin"
2349
3625
  });
2350
3626
  }
2351
3627
  }
2352
3628
 
2353
- class MyFitnessPalHealthProvider extends BaseHealthProvider {
3629
+ class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
2354
3630
  constructor(options) {
2355
3631
  super({
3632
+ ...options,
2356
3633
  providerKey: "health.myfitnesspal",
2357
- ...createProviderOptions(options, "official-api")
3634
+ upstreamProvider: "myfitnesspal"
2358
3635
  });
2359
3636
  }
2360
3637
  }
2361
3638
 
2362
- class EightSleepHealthProvider extends BaseHealthProvider {
3639
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
2363
3640
  constructor(options) {
2364
3641
  super({
3642
+ ...options,
2365
3643
  providerKey: "health.eightsleep",
2366
- ...createProviderOptions(options, "official-api")
3644
+ upstreamProvider: "eightsleep"
2367
3645
  });
2368
3646
  }
2369
3647
  }
2370
3648
 
2371
- class PelotonHealthProvider extends BaseHealthProvider {
3649
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
2372
3650
  constructor(options) {
2373
3651
  super({
3652
+ ...options,
2374
3653
  providerKey: "health.peloton",
2375
- ...createProviderOptions(options, "official-api")
3654
+ upstreamProvider: "peloton"
2376
3655
  });
2377
3656
  }
2378
3657
  }
2379
3658
 
2380
3659
  class UnofficialHealthAutomationProvider extends BaseHealthProvider {
3660
+ providerSlugValue;
2381
3661
  constructor(options) {
2382
3662
  super({
2383
- ...createProviderOptions(options, "unofficial"),
2384
- providerKey: options.providerKey
3663
+ ...options,
3664
+ providerKey: options.providerKey,
3665
+ webhookSignatureHeader: "x-unofficial-signature"
3666
+ });
3667
+ this.providerSlugValue = LIMITED_PROVIDER_SLUG[options.providerKey];
3668
+ }
3669
+ async listActivities(params) {
3670
+ return this.fetchActivities(params, {
3671
+ mcpTool: `${this.providerSlugValue}_list_activities`,
3672
+ buildQuery: buildSharedQuery2,
3673
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
3674
+ });
3675
+ }
3676
+ async listWorkouts(params) {
3677
+ return this.fetchWorkouts(params, {
3678
+ mcpTool: `${this.providerSlugValue}_list_workouts`,
3679
+ buildQuery: buildSharedQuery2,
3680
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
3681
+ });
3682
+ }
3683
+ async listSleep(params) {
3684
+ return this.fetchSleep(params, {
3685
+ mcpTool: `${this.providerSlugValue}_list_sleep`,
3686
+ buildQuery: buildSharedQuery2,
3687
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2385
3688
  });
2386
3689
  }
3690
+ async listBiometrics(params) {
3691
+ return this.fetchBiometrics(params, {
3692
+ mcpTool: `${this.providerSlugValue}_list_biometrics`,
3693
+ buildQuery: (input) => ({
3694
+ ...buildSharedQuery2(input),
3695
+ metricTypes: input.metricTypes
3696
+ }),
3697
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input))
3698
+ });
3699
+ }
3700
+ async listNutrition(params) {
3701
+ return this.fetchNutrition(params, {
3702
+ mcpTool: `${this.providerSlugValue}_list_nutrition`,
3703
+ buildQuery: buildSharedQuery2,
3704
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
3705
+ });
3706
+ }
3707
+ async getConnectionStatus(params) {
3708
+ return this.fetchConnectionStatus(params, {
3709
+ mcpTool: `${this.providerSlugValue}_connection_status`
3710
+ });
3711
+ }
3712
+ context(params) {
3713
+ return {
3714
+ tenantId: params.tenantId,
3715
+ connectionId: params.connectionId,
3716
+ providerKey: this.providerKey
3717
+ };
3718
+ }
2387
3719
  }
2388
-
2389
3720
  // src/impls/health-provider-factory.ts
2390
3721
  import {
2391
3722
  isUnofficialHealthProviderAllowed,
2392
3723
  resolveHealthStrategyOrder
2393
3724
  } from "@contractspec/integration.runtime/runtime";
3725
+ var OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER = {
3726
+ "health.openwearables": false,
3727
+ "health.whoop": true,
3728
+ "health.apple-health": false,
3729
+ "health.oura": true,
3730
+ "health.strava": true,
3731
+ "health.garmin": false,
3732
+ "health.fitbit": true,
3733
+ "health.myfitnesspal": false,
3734
+ "health.eightsleep": false,
3735
+ "health.peloton": false
3736
+ };
3737
+ var UNOFFICIAL_SUPPORTED_BY_PROVIDER = {
3738
+ "health.openwearables": false,
3739
+ "health.whoop": false,
3740
+ "health.apple-health": false,
3741
+ "health.oura": false,
3742
+ "health.strava": false,
3743
+ "health.garmin": true,
3744
+ "health.fitbit": false,
3745
+ "health.myfitnesspal": true,
3746
+ "health.eightsleep": true,
3747
+ "health.peloton": true
3748
+ };
2394
3749
  function createHealthProviderFromContext(context, secrets) {
2395
3750
  const providerKey = context.spec.meta.key;
2396
3751
  const config = toFactoryConfig(context.config);
2397
3752
  const strategyOrder = buildStrategyOrder(config);
2398
- const errors = [];
2399
- for (const strategy of strategyOrder) {
2400
- const provider = createHealthProviderForStrategy(providerKey, strategy, config, secrets);
3753
+ const attemptLogs = [];
3754
+ for (let index = 0;index < strategyOrder.length; index += 1) {
3755
+ const strategy = strategyOrder[index];
3756
+ if (!strategy)
3757
+ continue;
3758
+ const route = index === 0 ? "primary" : "fallback";
3759
+ if (!supportsStrategy(providerKey, strategy)) {
3760
+ attemptLogs.push(`${strategy}: unsupported by ${providerKey}`);
3761
+ continue;
3762
+ }
3763
+ if (!hasCredentialsForStrategy(strategy, config, secrets)) {
3764
+ attemptLogs.push(`${strategy}: missing credentials`);
3765
+ continue;
3766
+ }
3767
+ const provider = createHealthProviderForStrategy(providerKey, strategy, route, config, secrets);
2401
3768
  if (provider) {
2402
3769
  return provider;
2403
3770
  }
2404
- errors.push(`${strategy}: not available`);
3771
+ attemptLogs.push(`${strategy}: not available`);
2405
3772
  }
2406
- throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${errors.join(", ")}.`);
3773
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
2407
3774
  }
2408
- function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
3775
+ function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
2409
3776
  const options = {
2410
3777
  transport: strategy,
2411
3778
  apiBaseUrl: config.apiBaseUrl,
@@ -2413,10 +3780,21 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
2413
3780
  apiKey: getSecretString(secrets, "apiKey"),
2414
3781
  accessToken: getSecretString(secrets, "accessToken"),
2415
3782
  mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
2416
- webhookSecret: getSecretString(secrets, "webhookSecret")
3783
+ webhookSecret: getSecretString(secrets, "webhookSecret"),
3784
+ route,
3785
+ oauth: {
3786
+ tokenUrl: config.oauthTokenUrl,
3787
+ refreshToken: getSecretString(secrets, "refreshToken"),
3788
+ clientId: getSecretString(secrets, "clientId"),
3789
+ clientSecret: getSecretString(secrets, "clientSecret"),
3790
+ tokenExpiresAt: getSecretString(secrets, "tokenExpiresAt")
3791
+ }
2417
3792
  };
2418
3793
  if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
2419
- return new OpenWearablesHealthProvider(options);
3794
+ return createAggregatorProvider(providerKey, {
3795
+ ...options,
3796
+ aggregatorKey: "health.openwearables"
3797
+ });
2420
3798
  }
2421
3799
  if (strategy === "unofficial") {
2422
3800
  if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
@@ -2438,6 +3816,31 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
2438
3816
  }
2439
3817
  return createOfficialProvider(providerKey, options);
2440
3818
  }
3819
+ function createAggregatorProvider(providerKey, options) {
3820
+ if (providerKey === "health.apple-health") {
3821
+ return new AppleHealthBridgeProvider(options);
3822
+ }
3823
+ if (providerKey === "health.garmin") {
3824
+ return new GarminHealthProvider(options);
3825
+ }
3826
+ if (providerKey === "health.myfitnesspal") {
3827
+ return new MyFitnessPalHealthProvider(options);
3828
+ }
3829
+ if (providerKey === "health.eightsleep") {
3830
+ return new EightSleepHealthProvider(options);
3831
+ }
3832
+ if (providerKey === "health.peloton") {
3833
+ return new PelotonHealthProvider(options);
3834
+ }
3835
+ if (providerKey === "health.openwearables") {
3836
+ return new OpenWearablesHealthProvider(options);
3837
+ }
3838
+ return new OpenWearablesHealthProvider({
3839
+ ...options,
3840
+ providerKey,
3841
+ upstreamProvider: providerKey.replace("health.", "")
3842
+ });
3843
+ }
2441
3844
  function createOfficialProvider(providerKey, options) {
2442
3845
  switch (providerKey) {
2443
3846
  case "health.openwearables":
@@ -2455,11 +3858,20 @@ function createOfficialProvider(providerKey, options) {
2455
3858
  case "health.fitbit":
2456
3859
  return new FitbitHealthProvider(options);
2457
3860
  case "health.myfitnesspal":
2458
- return new MyFitnessPalHealthProvider(options);
3861
+ return new MyFitnessPalHealthProvider({
3862
+ ...options,
3863
+ transport: "aggregator-api"
3864
+ });
2459
3865
  case "health.eightsleep":
2460
- return new EightSleepHealthProvider(options);
3866
+ return new EightSleepHealthProvider({
3867
+ ...options,
3868
+ transport: "aggregator-api"
3869
+ });
2461
3870
  case "health.peloton":
2462
- return new PelotonHealthProvider(options);
3871
+ return new PelotonHealthProvider({
3872
+ ...options,
3873
+ transport: "aggregator-api"
3874
+ });
2463
3875
  default:
2464
3876
  throw new Error(`Unsupported health provider key: ${providerKey}`);
2465
3877
  }
@@ -2472,6 +3884,7 @@ function toFactoryConfig(config) {
2472
3884
  return {
2473
3885
  apiBaseUrl: asString(record.apiBaseUrl),
2474
3886
  mcpUrl: asString(record.mcpUrl),
3887
+ oauthTokenUrl: asString(record.oauthTokenUrl),
2475
3888
  defaultTransport: normalizeTransport(record.defaultTransport),
2476
3889
  strategyOrder: normalizeTransportArray(record.strategyOrder),
2477
3890
  allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
@@ -2500,6 +3913,27 @@ function normalizeTransportArray(value) {
2500
3913
  const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
2501
3914
  return transports.length > 0 ? transports : undefined;
2502
3915
  }
3916
+ function supportsStrategy(providerKey, strategy) {
3917
+ if (strategy === "official-api" || strategy === "official-mcp") {
3918
+ return OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER[providerKey];
3919
+ }
3920
+ if (strategy === "unofficial") {
3921
+ return UNOFFICIAL_SUPPORTED_BY_PROVIDER[providerKey];
3922
+ }
3923
+ return true;
3924
+ }
3925
+ function hasCredentialsForStrategy(strategy, config, secrets) {
3926
+ const hasApiCredential = Boolean(getSecretString(secrets, "accessToken")) || Boolean(getSecretString(secrets, "apiKey"));
3927
+ const hasMcpCredential = Boolean(getSecretString(secrets, "mcpAccessToken")) || hasApiCredential;
3928
+ if (strategy === "official-api" || strategy === "aggregator-api") {
3929
+ return hasApiCredential;
3930
+ }
3931
+ if (strategy === "official-mcp" || strategy === "aggregator-mcp") {
3932
+ return Boolean(config.mcpUrl) && hasMcpCredential;
3933
+ }
3934
+ const hasAutomationCredential = hasMcpCredential || Boolean(getSecretString(secrets, "username")) && Boolean(getSecretString(secrets, "password"));
3935
+ return Boolean(config.mcpUrl) && hasAutomationCredential;
3936
+ }
2503
3937
  function getSecretString(secrets, key) {
2504
3938
  const value = secrets[key];
2505
3939
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
@@ -2775,45 +4209,474 @@ function mapFinishReason(reason) {
2775
4209
  }
2776
4210
  }
2777
4211
 
2778
- // src/impls/mistral-embedding.ts
2779
- import { Mistral as Mistral2 } from "@mistralai/mistralai";
4212
+ // src/impls/mistral-embedding.ts
4213
+ import { Mistral as Mistral2 } from "@mistralai/mistralai";
4214
+
4215
+ class MistralEmbeddingProvider {
4216
+ client;
4217
+ defaultModel;
4218
+ constructor(options) {
4219
+ if (!options.apiKey) {
4220
+ throw new Error("MistralEmbeddingProvider requires an apiKey");
4221
+ }
4222
+ this.client = options.client ?? new Mistral2({
4223
+ apiKey: options.apiKey,
4224
+ serverURL: options.serverURL
4225
+ });
4226
+ this.defaultModel = options.defaultModel ?? "mistral-embed";
4227
+ }
4228
+ async embedDocuments(documents, options) {
4229
+ if (documents.length === 0)
4230
+ return [];
4231
+ const model = options?.model ?? this.defaultModel;
4232
+ const response = await this.client.embeddings.create({
4233
+ model,
4234
+ inputs: documents.map((doc) => doc.text)
4235
+ });
4236
+ return response.data.map((item, index) => ({
4237
+ id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
4238
+ vector: item.embedding ?? [],
4239
+ dimensions: item.embedding?.length ?? 0,
4240
+ model: response.model,
4241
+ metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
4242
+ }));
4243
+ }
4244
+ async embedQuery(query, options) {
4245
+ const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
4246
+ if (!result) {
4247
+ throw new Error("Failed to compute embedding for query");
4248
+ }
4249
+ return result;
4250
+ }
4251
+ }
4252
+
4253
+ // src/impls/mistral-stt.ts
4254
+ var DEFAULT_BASE_URL4 = "https://api.mistral.ai/v1";
4255
+ var DEFAULT_MODEL = "voxtral-mini-latest";
4256
+ var AUDIO_MIME_BY_FORMAT = {
4257
+ mp3: "audio/mpeg",
4258
+ wav: "audio/wav",
4259
+ ogg: "audio/ogg",
4260
+ pcm: "audio/pcm",
4261
+ opus: "audio/opus"
4262
+ };
4263
+
4264
+ class MistralSttProvider {
4265
+ apiKey;
4266
+ defaultModel;
4267
+ defaultLanguage;
4268
+ baseUrl;
4269
+ fetchImpl;
4270
+ constructor(options) {
4271
+ if (!options.apiKey) {
4272
+ throw new Error("MistralSttProvider requires an apiKey");
4273
+ }
4274
+ this.apiKey = options.apiKey;
4275
+ this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
4276
+ this.defaultLanguage = options.defaultLanguage;
4277
+ this.baseUrl = normalizeBaseUrl(options.serverURL ?? DEFAULT_BASE_URL4);
4278
+ this.fetchImpl = options.fetchImpl ?? fetch;
4279
+ }
4280
+ async transcribe(input) {
4281
+ const formData = new FormData;
4282
+ const model = input.model ?? this.defaultModel;
4283
+ const mimeType = AUDIO_MIME_BY_FORMAT[input.audio.format] ?? "audio/wav";
4284
+ const fileName = `audio.${input.audio.format}`;
4285
+ const audioBytes = new Uint8Array(input.audio.data);
4286
+ const blob = new Blob([audioBytes], { type: mimeType });
4287
+ formData.append("file", blob, fileName);
4288
+ formData.append("model", model);
4289
+ formData.append("response_format", "verbose_json");
4290
+ const language = input.language ?? this.defaultLanguage;
4291
+ if (language) {
4292
+ formData.append("language", language);
4293
+ }
4294
+ const response = await this.fetchImpl(`${this.baseUrl}/audio/transcriptions`, {
4295
+ method: "POST",
4296
+ headers: {
4297
+ Authorization: `Bearer ${this.apiKey}`
4298
+ },
4299
+ body: formData
4300
+ });
4301
+ if (!response.ok) {
4302
+ const body = await response.text();
4303
+ throw new Error(`Mistral transcription request failed (${response.status}): ${body}`);
4304
+ }
4305
+ const payload = await response.json();
4306
+ return toTranscriptionResult(payload, input);
4307
+ }
4308
+ }
4309
+ function toTranscriptionResult(payload, input) {
4310
+ const record = asRecord2(payload);
4311
+ const text = readString3(record, "text") ?? "";
4312
+ const language = readString3(record, "language") ?? input.language ?? "unknown";
4313
+ const segments = parseSegments(record);
4314
+ if (segments.length === 0 && text.length > 0) {
4315
+ segments.push({
4316
+ text,
4317
+ startMs: 0,
4318
+ endMs: input.audio.durationMs ?? 0
4319
+ });
4320
+ }
4321
+ const durationMs = input.audio.durationMs ?? segments.reduce((max, segment) => Math.max(max, segment.endMs), 0);
4322
+ const topLevelWords = parseWordTimings(record.words);
4323
+ const flattenedWords = segments.flatMap((segment) => segment.wordTimings ?? []);
4324
+ const wordTimings = topLevelWords.length > 0 ? topLevelWords : flattenedWords.length > 0 ? flattenedWords : undefined;
4325
+ const speakers = dedupeSpeakers(segments);
4326
+ return {
4327
+ text,
4328
+ segments,
4329
+ language,
4330
+ durationMs,
4331
+ speakers: speakers.length > 0 ? speakers : undefined,
4332
+ wordTimings
4333
+ };
4334
+ }
4335
+ function parseSegments(record) {
4336
+ if (!Array.isArray(record.segments)) {
4337
+ return [];
4338
+ }
4339
+ const parsed = [];
4340
+ for (const entry of record.segments) {
4341
+ const segmentRecord = asRecord2(entry);
4342
+ const text = readString3(segmentRecord, "text");
4343
+ if (!text) {
4344
+ continue;
4345
+ }
4346
+ const startSeconds = readNumber2(segmentRecord, "start") ?? 0;
4347
+ const endSeconds = readNumber2(segmentRecord, "end") ?? startSeconds;
4348
+ parsed.push({
4349
+ text,
4350
+ startMs: secondsToMs(startSeconds),
4351
+ endMs: secondsToMs(endSeconds),
4352
+ speakerId: readString3(segmentRecord, "speaker") ?? undefined,
4353
+ confidence: readNumber2(segmentRecord, "confidence"),
4354
+ wordTimings: parseWordTimings(segmentRecord.words)
4355
+ });
4356
+ }
4357
+ return parsed;
4358
+ }
4359
+ function parseWordTimings(value) {
4360
+ if (!Array.isArray(value)) {
4361
+ return [];
4362
+ }
4363
+ const words = [];
4364
+ for (const entry of value) {
4365
+ const wordRecord = asRecord2(entry);
4366
+ const word = readString3(wordRecord, "word");
4367
+ const startSeconds = readNumber2(wordRecord, "start");
4368
+ const endSeconds = readNumber2(wordRecord, "end");
4369
+ if (!word || startSeconds == null || endSeconds == null) {
4370
+ continue;
4371
+ }
4372
+ words.push({
4373
+ word,
4374
+ startMs: secondsToMs(startSeconds),
4375
+ endMs: secondsToMs(endSeconds),
4376
+ confidence: readNumber2(wordRecord, "confidence")
4377
+ });
4378
+ }
4379
+ return words;
4380
+ }
4381
+ function dedupeSpeakers(segments) {
4382
+ const seen = new Set;
4383
+ const speakers = [];
4384
+ for (const segment of segments) {
4385
+ if (!segment.speakerId || seen.has(segment.speakerId)) {
4386
+ continue;
4387
+ }
4388
+ seen.add(segment.speakerId);
4389
+ speakers.push({
4390
+ id: segment.speakerId,
4391
+ name: segment.speakerName
4392
+ });
4393
+ }
4394
+ return speakers;
4395
+ }
4396
+ function normalizeBaseUrl(url) {
4397
+ return url.endsWith("/") ? url.slice(0, -1) : url;
4398
+ }
4399
+ function asRecord2(value) {
4400
+ if (value && typeof value === "object") {
4401
+ return value;
4402
+ }
4403
+ return {};
4404
+ }
4405
+ function readString3(record, key) {
4406
+ const value = record[key];
4407
+ return typeof value === "string" ? value : undefined;
4408
+ }
4409
+ function readNumber2(record, key) {
4410
+ const value = record[key];
4411
+ return typeof value === "number" ? value : undefined;
4412
+ }
4413
+ function secondsToMs(value) {
4414
+ return Math.round(value * 1000);
4415
+ }
4416
+
4417
+ // src/impls/mistral-conversational.session.ts
4418
+ class MistralConversationSession {
4419
+ events;
4420
+ queue = new AsyncEventQueue;
4421
+ turns = [];
4422
+ history = [];
4423
+ sessionId = crypto.randomUUID();
4424
+ startedAt = Date.now();
4425
+ sessionConfig;
4426
+ defaultModel;
4427
+ complete;
4428
+ sttProvider;
4429
+ pending = Promise.resolve();
4430
+ closed = false;
4431
+ closedSummary;
4432
+ constructor(options) {
4433
+ this.sessionConfig = options.sessionConfig;
4434
+ this.defaultModel = options.defaultModel;
4435
+ this.complete = options.complete;
4436
+ this.sttProvider = options.sttProvider;
4437
+ this.events = this.queue;
4438
+ this.queue.push({
4439
+ type: "session_started",
4440
+ sessionId: this.sessionId
4441
+ });
4442
+ }
4443
+ sendAudio(chunk) {
4444
+ if (this.closed) {
4445
+ return;
4446
+ }
4447
+ this.pending = this.pending.then(async () => {
4448
+ const transcription = await this.sttProvider.transcribe({
4449
+ audio: {
4450
+ data: chunk,
4451
+ format: this.sessionConfig.inputFormat ?? "pcm",
4452
+ sampleRateHz: 16000
4453
+ },
4454
+ language: this.sessionConfig.language
4455
+ });
4456
+ const transcriptText = transcription.text.trim();
4457
+ if (transcriptText.length > 0) {
4458
+ await this.handleUserText(transcriptText);
4459
+ }
4460
+ }).catch((error) => {
4461
+ this.emitError(error);
4462
+ });
4463
+ }
4464
+ sendText(text) {
4465
+ if (this.closed) {
4466
+ return;
4467
+ }
4468
+ const normalized = text.trim();
4469
+ if (normalized.length === 0) {
4470
+ return;
4471
+ }
4472
+ this.pending = this.pending.then(() => this.handleUserText(normalized)).catch((error) => {
4473
+ this.emitError(error);
4474
+ });
4475
+ }
4476
+ interrupt() {
4477
+ if (this.closed) {
4478
+ return;
4479
+ }
4480
+ this.queue.push({
4481
+ type: "error",
4482
+ error: new Error("Interrupt is not supported for non-streaming sessions.")
4483
+ });
4484
+ }
4485
+ async close() {
4486
+ if (this.closedSummary) {
4487
+ return this.closedSummary;
4488
+ }
4489
+ this.closed = true;
4490
+ await this.pending;
4491
+ const durationMs = Date.now() - this.startedAt;
4492
+ const summary = {
4493
+ sessionId: this.sessionId,
4494
+ durationMs,
4495
+ turns: this.turns.map((turn) => ({
4496
+ role: turn.role === "assistant" ? "agent" : turn.role,
4497
+ text: turn.text,
4498
+ startMs: turn.startMs,
4499
+ endMs: turn.endMs
4500
+ })),
4501
+ transcript: this.turns.map((turn) => `${turn.role}: ${turn.text}`).join(`
4502
+ `)
4503
+ };
4504
+ this.closedSummary = summary;
4505
+ this.queue.push({
4506
+ type: "session_ended",
4507
+ reason: "closed_by_client",
4508
+ durationMs
4509
+ });
4510
+ this.queue.close();
4511
+ return summary;
4512
+ }
4513
+ async handleUserText(text) {
4514
+ if (this.closed) {
4515
+ return;
4516
+ }
4517
+ const userStart = Date.now();
4518
+ this.queue.push({ type: "user_speech_started" });
4519
+ this.queue.push({ type: "user_speech_ended", transcript: text });
4520
+ this.queue.push({
4521
+ type: "transcript",
4522
+ role: "user",
4523
+ text,
4524
+ timestamp: userStart
4525
+ });
4526
+ this.turns.push({
4527
+ role: "user",
4528
+ text,
4529
+ startMs: userStart,
4530
+ endMs: Date.now()
4531
+ });
4532
+ this.history.push({ role: "user", content: text });
4533
+ const assistantStart = Date.now();
4534
+ const assistantText = await this.complete(this.history, {
4535
+ ...this.sessionConfig,
4536
+ llmModel: this.sessionConfig.llmModel ?? this.defaultModel
4537
+ });
4538
+ if (this.closed) {
4539
+ return;
4540
+ }
4541
+ const normalizedAssistantText = assistantText.trim();
4542
+ const finalAssistantText = normalizedAssistantText.length > 0 ? normalizedAssistantText : "I was unable to produce a response.";
4543
+ this.queue.push({
4544
+ type: "agent_speech_started",
4545
+ text: finalAssistantText
4546
+ });
4547
+ this.queue.push({
4548
+ type: "transcript",
4549
+ role: "agent",
4550
+ text: finalAssistantText,
4551
+ timestamp: assistantStart
4552
+ });
4553
+ this.queue.push({ type: "agent_speech_ended" });
4554
+ this.turns.push({
4555
+ role: "assistant",
4556
+ text: finalAssistantText,
4557
+ startMs: assistantStart,
4558
+ endMs: Date.now()
4559
+ });
4560
+ this.history.push({ role: "assistant", content: finalAssistantText });
4561
+ }
4562
+ emitError(error) {
4563
+ if (this.closed) {
4564
+ return;
4565
+ }
4566
+ this.queue.push({ type: "error", error: toError(error) });
4567
+ }
4568
+ }
4569
+ function toError(error) {
4570
+ if (error instanceof Error) {
4571
+ return error;
4572
+ }
4573
+ return new Error(String(error));
4574
+ }
4575
+
4576
+ // src/impls/mistral-conversational.ts
4577
+ var DEFAULT_BASE_URL5 = "https://api.mistral.ai/v1";
4578
+ var DEFAULT_MODEL2 = "mistral-small-latest";
4579
+ var DEFAULT_VOICE = "default";
2780
4580
 
2781
- class MistralEmbeddingProvider {
2782
- client;
4581
+ class MistralConversationalProvider {
4582
+ apiKey;
2783
4583
  defaultModel;
4584
+ defaultVoiceId;
4585
+ baseUrl;
4586
+ fetchImpl;
4587
+ sttProvider;
2784
4588
  constructor(options) {
2785
4589
  if (!options.apiKey) {
2786
- throw new Error("MistralEmbeddingProvider requires an apiKey");
4590
+ throw new Error("MistralConversationalProvider requires an apiKey");
2787
4591
  }
2788
- this.client = options.client ?? new Mistral2({
4592
+ this.apiKey = options.apiKey;
4593
+ this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
4594
+ this.defaultVoiceId = options.defaultVoiceId ?? DEFAULT_VOICE;
4595
+ this.baseUrl = normalizeBaseUrl2(options.serverURL ?? DEFAULT_BASE_URL5);
4596
+ this.fetchImpl = options.fetchImpl ?? fetch;
4597
+ this.sttProvider = options.sttProvider ?? new MistralSttProvider({
2789
4598
  apiKey: options.apiKey,
2790
- serverURL: options.serverURL
4599
+ defaultModel: options.sttOptions?.defaultModel,
4600
+ defaultLanguage: options.sttOptions?.defaultLanguage,
4601
+ serverURL: options.sttOptions?.serverURL ?? options.serverURL,
4602
+ fetchImpl: this.fetchImpl
2791
4603
  });
2792
- this.defaultModel = options.defaultModel ?? "mistral-embed";
2793
4604
  }
2794
- async embedDocuments(documents, options) {
2795
- if (documents.length === 0)
2796
- return [];
2797
- const model = options?.model ?? this.defaultModel;
2798
- const response = await this.client.embeddings.create({
2799
- model,
2800
- inputs: documents.map((doc) => doc.text)
4605
+ async startSession(config) {
4606
+ return new MistralConversationSession({
4607
+ sessionConfig: {
4608
+ ...config,
4609
+ voiceId: config.voiceId || this.defaultVoiceId
4610
+ },
4611
+ defaultModel: this.defaultModel,
4612
+ complete: (history, sessionConfig) => this.completeConversation(history, sessionConfig),
4613
+ sttProvider: this.sttProvider
2801
4614
  });
2802
- return response.data.map((item, index) => ({
2803
- id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
2804
- vector: item.embedding ?? [],
2805
- dimensions: item.embedding?.length ?? 0,
2806
- model: response.model,
2807
- metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
2808
- }));
2809
4615
  }
2810
- async embedQuery(query, options) {
2811
- const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
2812
- if (!result) {
2813
- throw new Error("Failed to compute embedding for query");
4616
+ async listVoices() {
4617
+ return [
4618
+ {
4619
+ id: this.defaultVoiceId,
4620
+ name: "Mistral Default Voice",
4621
+ description: "Default conversational voice profile.",
4622
+ capabilities: ["conversational"]
4623
+ }
4624
+ ];
4625
+ }
4626
+ async completeConversation(history, sessionConfig) {
4627
+ const model = sessionConfig.llmModel ?? this.defaultModel;
4628
+ const messages = [];
4629
+ if (sessionConfig.systemPrompt) {
4630
+ messages.push({ role: "system", content: sessionConfig.systemPrompt });
2814
4631
  }
2815
- return result;
4632
+ for (const item of history) {
4633
+ messages.push({ role: item.role, content: item.content });
4634
+ }
4635
+ const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
4636
+ method: "POST",
4637
+ headers: {
4638
+ Authorization: `Bearer ${this.apiKey}`,
4639
+ "Content-Type": "application/json"
4640
+ },
4641
+ body: JSON.stringify({
4642
+ model,
4643
+ messages
4644
+ })
4645
+ });
4646
+ if (!response.ok) {
4647
+ const body = await response.text();
4648
+ throw new Error(`Mistral conversational request failed (${response.status}): ${body}`);
4649
+ }
4650
+ const payload = await response.json();
4651
+ return readAssistantText(payload);
4652
+ }
4653
+ }
4654
+ function normalizeBaseUrl2(url) {
4655
+ return url.endsWith("/") ? url.slice(0, -1) : url;
4656
+ }
4657
+ function readAssistantText(payload) {
4658
+ const record = asRecord3(payload);
4659
+ const choices = Array.isArray(record.choices) ? record.choices : [];
4660
+ const firstChoice = asRecord3(choices[0]);
4661
+ const message = asRecord3(firstChoice.message);
4662
+ if (typeof message.content === "string") {
4663
+ return message.content;
4664
+ }
4665
+ if (Array.isArray(message.content)) {
4666
+ const textParts = message.content.map((part) => {
4667
+ const entry = asRecord3(part);
4668
+ const text = entry.text;
4669
+ return typeof text === "string" ? text : "";
4670
+ }).filter((text) => text.length > 0);
4671
+ return textParts.join("");
4672
+ }
4673
+ return "";
4674
+ }
4675
+ function asRecord3(value) {
4676
+ if (value && typeof value === "object") {
4677
+ return value;
2816
4678
  }
4679
+ return {};
2817
4680
  }
2818
4681
 
2819
4682
  // src/impls/qdrant-vector.ts
@@ -3215,7 +5078,7 @@ function distanceToScore(distance, metric) {
3215
5078
 
3216
5079
  // src/impls/stripe-payments.ts
3217
5080
  import Stripe from "stripe";
3218
- var API_VERSION = "2026-01-28.clover";
5081
+ var API_VERSION = "2026-02-25.clover";
3219
5082
 
3220
5083
  class StripePaymentsProvider {
3221
5084
  stripe;
@@ -3880,6 +5743,318 @@ function mapStatus(status) {
3880
5743
  }
3881
5744
  }
3882
5745
 
5746
+ // src/impls/messaging-slack.ts
5747
+ class SlackMessagingProvider {
5748
+ botToken;
5749
+ defaultChannelId;
5750
+ apiBaseUrl;
5751
+ constructor(options) {
5752
+ this.botToken = options.botToken;
5753
+ this.defaultChannelId = options.defaultChannelId;
5754
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://slack.com/api";
5755
+ }
5756
+ async sendMessage(input) {
5757
+ const channel = input.channelId ?? input.recipientId ?? this.defaultChannelId;
5758
+ if (!channel) {
5759
+ throw new Error("Slack sendMessage requires channelId, recipientId, or defaultChannelId.");
5760
+ }
5761
+ const payload = {
5762
+ channel,
5763
+ text: input.text,
5764
+ mrkdwn: input.markdown ?? true,
5765
+ thread_ts: input.threadId
5766
+ };
5767
+ const response = await fetch(`${this.apiBaseUrl}/chat.postMessage`, {
5768
+ method: "POST",
5769
+ headers: {
5770
+ authorization: `Bearer ${this.botToken}`,
5771
+ "content-type": "application/json"
5772
+ },
5773
+ body: JSON.stringify(payload)
5774
+ });
5775
+ const body = await response.json();
5776
+ if (!response.ok || !body.ok || !body.ts) {
5777
+ throw new Error(`Slack sendMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
5778
+ }
5779
+ return {
5780
+ id: `slack:${body.channel ?? channel}:${body.ts}`,
5781
+ providerMessageId: body.ts,
5782
+ status: "sent",
5783
+ sentAt: new Date,
5784
+ metadata: {
5785
+ channelId: body.channel ?? channel
5786
+ }
5787
+ };
5788
+ }
5789
+ async updateMessage(messageId, input) {
5790
+ const channel = input.channelId ?? this.defaultChannelId;
5791
+ if (!channel) {
5792
+ throw new Error("Slack updateMessage requires channelId or defaultChannelId.");
5793
+ }
5794
+ const response = await fetch(`${this.apiBaseUrl}/chat.update`, {
5795
+ method: "POST",
5796
+ headers: {
5797
+ authorization: `Bearer ${this.botToken}`,
5798
+ "content-type": "application/json"
5799
+ },
5800
+ body: JSON.stringify({
5801
+ channel,
5802
+ ts: messageId,
5803
+ text: input.text,
5804
+ mrkdwn: input.markdown ?? true
5805
+ })
5806
+ });
5807
+ const body = await response.json();
5808
+ if (!response.ok || !body.ok || !body.ts) {
5809
+ throw new Error(`Slack updateMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
5810
+ }
5811
+ return {
5812
+ id: `slack:${body.channel ?? channel}:${body.ts}`,
5813
+ providerMessageId: body.ts,
5814
+ status: "sent",
5815
+ sentAt: new Date,
5816
+ metadata: {
5817
+ channelId: body.channel ?? channel
5818
+ }
5819
+ };
5820
+ }
5821
+ }
5822
+
5823
+ // src/impls/messaging-github.ts
5824
+ class GithubMessagingProvider {
5825
+ token;
5826
+ defaultOwner;
5827
+ defaultRepo;
5828
+ apiBaseUrl;
5829
+ constructor(options) {
5830
+ this.token = options.token;
5831
+ this.defaultOwner = options.defaultOwner;
5832
+ this.defaultRepo = options.defaultRepo;
5833
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://api.github.com";
5834
+ }
5835
+ async sendMessage(input) {
5836
+ const target = this.resolveTarget(input);
5837
+ const response = await fetch(`${this.apiBaseUrl}/repos/${target.owner}/${target.repo}/issues/${target.issueNumber}/comments`, {
5838
+ method: "POST",
5839
+ headers: {
5840
+ authorization: `Bearer ${this.token}`,
5841
+ accept: "application/vnd.github+json",
5842
+ "content-type": "application/json"
5843
+ },
5844
+ body: JSON.stringify({ body: input.text })
5845
+ });
5846
+ const body = await response.json();
5847
+ if (!response.ok || !body.id) {
5848
+ throw new Error(`GitHub sendMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
5849
+ }
5850
+ return {
5851
+ id: String(body.id),
5852
+ providerMessageId: body.node_id,
5853
+ status: "sent",
5854
+ sentAt: new Date,
5855
+ metadata: {
5856
+ url: body.html_url ?? "",
5857
+ owner: target.owner,
5858
+ repo: target.repo,
5859
+ issueNumber: String(target.issueNumber)
5860
+ }
5861
+ };
5862
+ }
5863
+ async updateMessage(messageId, input) {
5864
+ const owner = input.metadata?.owner ?? this.defaultOwner;
5865
+ const repo = input.metadata?.repo ?? this.defaultRepo;
5866
+ if (!owner || !repo) {
5867
+ throw new Error("GitHub updateMessage requires owner and repo metadata.");
5868
+ }
5869
+ const response = await fetch(`${this.apiBaseUrl}/repos/${owner}/${repo}/issues/comments/${messageId}`, {
5870
+ method: "PATCH",
5871
+ headers: {
5872
+ authorization: `Bearer ${this.token}`,
5873
+ accept: "application/vnd.github+json",
5874
+ "content-type": "application/json"
5875
+ },
5876
+ body: JSON.stringify({ body: input.text })
5877
+ });
5878
+ const body = await response.json();
5879
+ if (!response.ok || !body.id) {
5880
+ throw new Error(`GitHub updateMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
5881
+ }
5882
+ return {
5883
+ id: String(body.id),
5884
+ providerMessageId: body.node_id,
5885
+ status: "sent",
5886
+ sentAt: new Date,
5887
+ metadata: {
5888
+ url: body.html_url ?? "",
5889
+ owner,
5890
+ repo
5891
+ }
5892
+ };
5893
+ }
5894
+ resolveTarget(input) {
5895
+ const parsedRecipient = parseRecipient(input.recipientId);
5896
+ const owner = parsedRecipient?.owner ?? this.defaultOwner;
5897
+ const repo = parsedRecipient?.repo ?? this.defaultRepo;
5898
+ const issueNumber = parsedRecipient?.issueNumber ?? parseIssueNumber(input.threadId);
5899
+ if (!owner || !repo || issueNumber == null) {
5900
+ throw new Error("GitHub sendMessage requires owner/repo and issueNumber (use recipientId like owner/repo#123 or provide defaults + threadId).");
5901
+ }
5902
+ return {
5903
+ owner,
5904
+ repo,
5905
+ issueNumber
5906
+ };
5907
+ }
5908
+ }
5909
+ function parseRecipient(value) {
5910
+ if (!value)
5911
+ return null;
5912
+ const match = value.trim().match(/^([^/]+)\/([^#]+)#(\d+)$/);
5913
+ if (!match)
5914
+ return null;
5915
+ const owner = match[1];
5916
+ const repo = match[2];
5917
+ const issueNumber = Number(match[3]);
5918
+ if (!owner || !repo || !Number.isInteger(issueNumber)) {
5919
+ return null;
5920
+ }
5921
+ return { owner, repo, issueNumber };
5922
+ }
5923
+ function parseIssueNumber(value) {
5924
+ if (!value)
5925
+ return null;
5926
+ const numeric = Number(value);
5927
+ return Number.isInteger(numeric) ? numeric : null;
5928
+ }
5929
+
5930
+ // src/impls/messaging-whatsapp-meta.ts
5931
+ class MetaWhatsappMessagingProvider {
5932
+ accessToken;
5933
+ phoneNumberId;
5934
+ apiVersion;
5935
+ constructor(options) {
5936
+ this.accessToken = options.accessToken;
5937
+ this.phoneNumberId = options.phoneNumberId;
5938
+ this.apiVersion = options.apiVersion ?? "v22.0";
5939
+ }
5940
+ async sendMessage(input) {
5941
+ const to = input.recipientId;
5942
+ if (!to) {
5943
+ throw new Error("Meta WhatsApp sendMessage requires recipientId.");
5944
+ }
5945
+ const response = await fetch(`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`, {
5946
+ method: "POST",
5947
+ headers: {
5948
+ authorization: `Bearer ${this.accessToken}`,
5949
+ "content-type": "application/json"
5950
+ },
5951
+ body: JSON.stringify({
5952
+ messaging_product: "whatsapp",
5953
+ to,
5954
+ type: "text",
5955
+ text: {
5956
+ body: input.text,
5957
+ preview_url: false
5958
+ }
5959
+ })
5960
+ });
5961
+ const body = await response.json();
5962
+ const messageId = body.messages?.[0]?.id;
5963
+ if (!response.ok || !messageId) {
5964
+ const errorCode = body.error?.code != null ? String(body.error.code) : "";
5965
+ throw new Error(`Meta WhatsApp sendMessage failed: ${body.error?.message ?? `HTTP_${response.status}`}${errorCode ? ` (${errorCode})` : ""}`);
5966
+ }
5967
+ return {
5968
+ id: messageId,
5969
+ providerMessageId: messageId,
5970
+ status: "sent",
5971
+ sentAt: new Date,
5972
+ metadata: {
5973
+ phoneNumberId: this.phoneNumberId
5974
+ }
5975
+ };
5976
+ }
5977
+ }
5978
+
5979
+ // src/impls/messaging-whatsapp-twilio.ts
5980
+ import { Buffer as Buffer4 } from "buffer";
5981
+
5982
+ class TwilioWhatsappMessagingProvider {
5983
+ accountSid;
5984
+ authToken;
5985
+ fromNumber;
5986
+ constructor(options) {
5987
+ this.accountSid = options.accountSid;
5988
+ this.authToken = options.authToken;
5989
+ this.fromNumber = options.fromNumber;
5990
+ }
5991
+ async sendMessage(input) {
5992
+ const to = normalizeWhatsappAddress(input.recipientId);
5993
+ const from = normalizeWhatsappAddress(input.channelId ?? this.fromNumber);
5994
+ if (!to) {
5995
+ throw new Error("Twilio WhatsApp sendMessage requires recipientId.");
5996
+ }
5997
+ if (!from) {
5998
+ throw new Error("Twilio WhatsApp sendMessage requires channelId or configured fromNumber.");
5999
+ }
6000
+ const params = new URLSearchParams;
6001
+ params.set("To", to);
6002
+ params.set("From", from);
6003
+ params.set("Body", input.text);
6004
+ const auth = Buffer4.from(`${this.accountSid}:${this.authToken}`).toString("base64");
6005
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`, {
6006
+ method: "POST",
6007
+ headers: {
6008
+ authorization: `Basic ${auth}`,
6009
+ "content-type": "application/x-www-form-urlencoded"
6010
+ },
6011
+ body: params.toString()
6012
+ });
6013
+ const body = await response.json();
6014
+ if (!response.ok || !body.sid) {
6015
+ throw new Error(`Twilio WhatsApp sendMessage failed: ${body.error_message ?? `HTTP_${response.status}`}`);
6016
+ }
6017
+ return {
6018
+ id: body.sid,
6019
+ providerMessageId: body.sid,
6020
+ status: mapTwilioStatus(body.status),
6021
+ sentAt: new Date,
6022
+ errorCode: body.error_code != null ? String(body.error_code) : undefined,
6023
+ errorMessage: body.error_message ?? undefined,
6024
+ metadata: {
6025
+ from,
6026
+ to
6027
+ }
6028
+ };
6029
+ }
6030
+ }
6031
+ function normalizeWhatsappAddress(value) {
6032
+ if (!value)
6033
+ return null;
6034
+ if (value.startsWith("whatsapp:"))
6035
+ return value;
6036
+ return `whatsapp:${value}`;
6037
+ }
6038
+ function mapTwilioStatus(status) {
6039
+ switch (status) {
6040
+ case "queued":
6041
+ case "accepted":
6042
+ case "scheduled":
6043
+ return "queued";
6044
+ case "sending":
6045
+ return "sending";
6046
+ case "delivered":
6047
+ return "delivered";
6048
+ case "failed":
6049
+ case "undelivered":
6050
+ case "canceled":
6051
+ return "failed";
6052
+ case "sent":
6053
+ default:
6054
+ return "sent";
6055
+ }
6056
+ }
6057
+
3883
6058
  // src/impls/powens-client.ts
3884
6059
  import { URL as URL2 } from "url";
3885
6060
  var POWENS_BASE_URL = {
@@ -4386,7 +6561,7 @@ function resolveLabelIds(defaults, tags) {
4386
6561
  }
4387
6562
 
4388
6563
  // src/impls/jira.ts
4389
- import { Buffer as Buffer4 } from "buffer";
6564
+ import { Buffer as Buffer5 } from "buffer";
4390
6565
 
4391
6566
  class JiraProjectManagementProvider {
4392
6567
  siteUrl;
@@ -4463,7 +6638,7 @@ function normalizeSiteUrl(siteUrl) {
4463
6638
  return siteUrl.replace(/\/$/, "");
4464
6639
  }
4465
6640
  function buildAuthHeader(email, apiToken) {
4466
- const token = Buffer4.from(`${email}:${apiToken}`).toString("base64");
6641
+ const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
4467
6642
  return `Basic ${token}`;
4468
6643
  }
4469
6644
  function resolveIssueType(type, defaults) {
@@ -4666,7 +6841,7 @@ function buildParagraphBlocks(text) {
4666
6841
  }
4667
6842
 
4668
6843
  // src/impls/tldv-meeting-recorder.ts
4669
- var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
6844
+ var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
4670
6845
 
4671
6846
  class TldvMeetingRecorderProvider {
4672
6847
  apiKey;
@@ -4674,7 +6849,7 @@ class TldvMeetingRecorderProvider {
4674
6849
  defaultPageSize;
4675
6850
  constructor(options) {
4676
6851
  this.apiKey = options.apiKey;
4677
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
6852
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
4678
6853
  this.defaultPageSize = options.pageSize;
4679
6854
  }
4680
6855
  async listMeetings(params) {
@@ -4809,10 +6984,30 @@ async function safeReadError4(response) {
4809
6984
  }
4810
6985
 
4811
6986
  // src/impls/provider-factory.ts
4812
- import { Buffer as Buffer5 } from "buffer";
6987
+ import { Buffer as Buffer6 } from "buffer";
6988
+ import { resolveIntegrationRequestContext } from "@contractspec/lib.contracts-integrations/integrations/runtime";
6989
+ import { buildAuthHeaders } from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
6990
+ import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
4813
6991
  var SECRET_CACHE = new Map;
4814
6992
 
4815
6993
  class IntegrationProviderFactory {
6994
+ composioFallback;
6995
+ constructor(options) {
6996
+ this.composioFallback = options?.composioFallback;
6997
+ }
6998
+ async resolveProviderContext(context) {
6999
+ const secrets = await this.loadSecrets(context);
7000
+ const { transport, authMethod, apiVersion } = resolveIntegrationRequestContext(context.spec, context.connection);
7001
+ let authHeaders = {};
7002
+ if (authMethod && context.spec.supportedAuthMethods) {
7003
+ const authConfig = findAuthConfig(context.spec.supportedAuthMethods, authMethod);
7004
+ if (authConfig) {
7005
+ const stringSecrets = Object.fromEntries(Object.entries(secrets).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v]));
7006
+ authHeaders = buildAuthHeaders(authConfig, stringSecrets);
7007
+ }
7008
+ }
7009
+ return { transport, authMethod, apiVersion, authHeaders, secrets };
7010
+ }
4816
7011
  async createPaymentsProvider(context) {
4817
7012
  const secrets = await this.loadSecrets(context);
4818
7013
  switch (context.spec.meta.key) {
@@ -4821,6 +7016,9 @@ class IntegrationProviderFactory {
4821
7016
  apiKey: requireSecret(secrets, "apiKey", "Stripe API key is required")
4822
7017
  });
4823
7018
  default:
7019
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7020
+ return this.composioFallback.createPaymentsProxy(context);
7021
+ }
4824
7022
  throw new Error(`Unsupported payments integration: ${context.spec.meta.key}`);
4825
7023
  }
4826
7024
  }
@@ -4834,6 +7032,9 @@ class IntegrationProviderFactory {
4834
7032
  messageStream: context.config.messageStream
4835
7033
  });
4836
7034
  default:
7035
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7036
+ return this.composioFallback.createEmailProxy(context);
7037
+ }
4837
7038
  throw new Error(`Unsupported email integration: ${context.spec.meta.key}`);
4838
7039
  }
4839
7040
  }
@@ -4847,9 +7048,48 @@ class IntegrationProviderFactory {
4847
7048
  fromNumber: context.config.fromNumber
4848
7049
  });
4849
7050
  default:
7051
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7052
+ return this.composioFallback.createMessagingProxy(context);
7053
+ }
4850
7054
  throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
4851
7055
  }
4852
7056
  }
7057
+ async createMessagingProvider(context) {
7058
+ const secrets = await this.loadSecrets(context);
7059
+ const config = context.config;
7060
+ switch (context.spec.meta.key) {
7061
+ case "messaging.slack":
7062
+ return new SlackMessagingProvider({
7063
+ botToken: requireSecret(secrets, "botToken", "Slack bot token is required"),
7064
+ defaultChannelId: config?.defaultChannelId,
7065
+ apiBaseUrl: config?.apiBaseUrl
7066
+ });
7067
+ case "messaging.github":
7068
+ return new GithubMessagingProvider({
7069
+ token: requireSecret(secrets, "token", "GitHub token is required"),
7070
+ defaultOwner: config?.defaultOwner,
7071
+ defaultRepo: config?.defaultRepo,
7072
+ apiBaseUrl: config?.apiBaseUrl
7073
+ });
7074
+ case "messaging.whatsapp.meta":
7075
+ return new MetaWhatsappMessagingProvider({
7076
+ accessToken: requireSecret(secrets, "accessToken", "Meta WhatsApp access token is required"),
7077
+ phoneNumberId: requireConfig(context, "phoneNumberId", "Meta WhatsApp phoneNumberId is required"),
7078
+ apiVersion: config?.apiVersion
7079
+ });
7080
+ case "messaging.whatsapp.twilio":
7081
+ return new TwilioWhatsappMessagingProvider({
7082
+ accountSid: requireSecret(secrets, "accountSid", "Twilio account SID is required"),
7083
+ authToken: requireSecret(secrets, "authToken", "Twilio auth token is required"),
7084
+ fromNumber: config?.fromNumber
7085
+ });
7086
+ default:
7087
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7088
+ return this.composioFallback.createMessagingProxy(context);
7089
+ }
7090
+ throw new Error(`Unsupported messaging integration: ${context.spec.meta.key}`);
7091
+ }
7092
+ }
4853
7093
  async createVectorStoreProvider(context) {
4854
7094
  const secrets = await this.loadSecrets(context);
4855
7095
  const config = context.config;
@@ -4870,6 +7110,9 @@ class IntegrationProviderFactory {
4870
7110
  sslMode: config?.sslMode
4871
7111
  });
4872
7112
  default:
7113
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7114
+ return this.composioFallback.createGenericProxy(context);
7115
+ }
4873
7116
  throw new Error(`Unsupported vector store integration: ${context.spec.meta.key}`);
4874
7117
  }
4875
7118
  }
@@ -4886,6 +7129,9 @@ class IntegrationProviderFactory {
4886
7129
  personalApiKey: requireSecret(secrets, "personalApiKey", "PostHog personalApiKey is required")
4887
7130
  });
4888
7131
  default:
7132
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7133
+ return this.composioFallback.createGenericProxy(context);
7134
+ }
4889
7135
  throw new Error(`Unsupported analytics integration: ${context.spec.meta.key}`);
4890
7136
  }
4891
7137
  }
@@ -4900,6 +7146,9 @@ class IntegrationProviderFactory {
4900
7146
  sslMode: config?.sslMode
4901
7147
  });
4902
7148
  default:
7149
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7150
+ return this.composioFallback.createGenericProxy(context);
7151
+ }
4903
7152
  throw new Error(`Unsupported database integration: ${context.spec.meta.key}`);
4904
7153
  }
4905
7154
  }
@@ -4913,6 +7162,9 @@ class IntegrationProviderFactory {
4913
7162
  clientOptions: secrets.type === "service_account" ? { credentials: secrets } : undefined
4914
7163
  });
4915
7164
  default:
7165
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7166
+ return this.composioFallback.createGenericProxy(context);
7167
+ }
4916
7168
  throw new Error(`Unsupported storage integration: ${context.spec.meta.key}`);
4917
7169
  }
4918
7170
  }
@@ -4945,9 +7197,53 @@ class IntegrationProviderFactory {
4945
7197
  pollIntervalMs: config?.pollIntervalMs
4946
7198
  });
4947
7199
  default:
7200
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7201
+ return this.composioFallback.createGenericProxy(context);
7202
+ }
4948
7203
  throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
4949
7204
  }
4950
7205
  }
7206
+ async createSttProvider(context) {
7207
+ const secrets = await this.loadSecrets(context);
7208
+ const config = context.config;
7209
+ switch (context.spec.meta.key) {
7210
+ case "ai-voice-stt.mistral":
7211
+ return new MistralSttProvider({
7212
+ apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
7213
+ defaultModel: config?.model,
7214
+ defaultLanguage: config?.language,
7215
+ serverURL: config?.serverURL
7216
+ });
7217
+ default:
7218
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7219
+ return this.composioFallback.createGenericProxy(context);
7220
+ }
7221
+ throw new Error(`Unsupported STT integration: ${context.spec.meta.key}`);
7222
+ }
7223
+ }
7224
+ async createConversationalProvider(context) {
7225
+ const secrets = await this.loadSecrets(context);
7226
+ const config = context.config;
7227
+ switch (context.spec.meta.key) {
7228
+ case "ai-voice-conv.mistral":
7229
+ return new MistralConversationalProvider({
7230
+ apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
7231
+ defaultModel: config?.model,
7232
+ defaultVoiceId: config?.defaultVoice,
7233
+ serverURL: config?.serverURL,
7234
+ sttOptions: {
7235
+ defaultModel: config?.model,
7236
+ defaultLanguage: config?.language,
7237
+ serverURL: config?.serverURL
7238
+ }
7239
+ });
7240
+ default:
7241
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7242
+ return this.composioFallback.createGenericProxy(context);
7243
+ }
7244
+ throw new Error(`Unsupported conversational integration: ${context.spec.meta.key}`);
7245
+ }
7246
+ }
4951
7247
  async createProjectManagementProvider(context) {
4952
7248
  const secrets = await this.loadSecrets(context);
4953
7249
  const config = context.config;
@@ -4985,6 +7281,9 @@ class IntegrationProviderFactory {
4985
7281
  descriptionProperty: config?.descriptionProperty
4986
7282
  });
4987
7283
  default:
7284
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7285
+ return this.composioFallback.createProjectManagementProxy(context);
7286
+ }
4988
7287
  throw new Error(`Unsupported project management integration: ${context.spec.meta.key}`);
4989
7288
  }
4990
7289
  }
@@ -5033,6 +7332,9 @@ class IntegrationProviderFactory {
5033
7332
  webhookSecret: secrets.webhookSecret
5034
7333
  });
5035
7334
  default:
7335
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7336
+ return this.composioFallback.createGenericProxy(context);
7337
+ }
5036
7338
  throw new Error(`Unsupported meeting recorder integration: ${context.spec.meta.key}`);
5037
7339
  }
5038
7340
  }
@@ -5045,6 +7347,9 @@ class IntegrationProviderFactory {
5045
7347
  defaultModel: context.config.model
5046
7348
  });
5047
7349
  default:
7350
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7351
+ return this.composioFallback.createGenericProxy(context);
7352
+ }
5048
7353
  throw new Error(`Unsupported LLM integration: ${context.spec.meta.key}`);
5049
7354
  }
5050
7355
  }
@@ -5057,6 +7362,9 @@ class IntegrationProviderFactory {
5057
7362
  defaultModel: context.config.embeddingModel
5058
7363
  });
5059
7364
  default:
7365
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7366
+ return this.composioFallback.createGenericProxy(context);
7367
+ }
5060
7368
  throw new Error(`Unsupported embeddings integration: ${context.spec.meta.key}`);
5061
7369
  }
5062
7370
  }
@@ -5078,6 +7386,9 @@ class IntegrationProviderFactory {
5078
7386
  });
5079
7387
  }
5080
7388
  default:
7389
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
7390
+ return this.composioFallback.createGenericProxy(context);
7391
+ }
5081
7392
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
5082
7393
  }
5083
7394
  }
@@ -5098,7 +7409,7 @@ class IntegrationProviderFactory {
5098
7409
  }
5099
7410
  }
5100
7411
  function parseSecret(secret) {
5101
- const text = Buffer5.from(secret.data).toString("utf-8").trim();
7412
+ const text = Buffer6.from(secret.data).toString("utf-8").trim();
5102
7413
  if (!text)
5103
7414
  return {};
5104
7415
  try {
@@ -5130,15 +7441,19 @@ function requireConfig(context, key, message) {
5130
7441
  return value;
5131
7442
  }
5132
7443
  export {
7444
+ resolveToolkit,
7445
+ isSessionExpired,
5133
7446
  createHealthProviderFromContext,
5134
7447
  WhoopHealthProvider,
5135
7448
  UnofficialHealthAutomationProvider,
7449
+ TwilioWhatsappMessagingProvider,
5136
7450
  TwilioSmsProvider,
5137
7451
  TldvMeetingRecorderProvider,
5138
7452
  SupabaseVectorProvider,
5139
7453
  SupabasePostgresProvider,
5140
7454
  StripePaymentsProvider,
5141
7455
  StravaHealthProvider,
7456
+ SlackMessagingProvider,
5142
7457
  QdrantVectorProvider,
5143
7458
  PowensOpenBankingProvider,
5144
7459
  PowensClientError,
@@ -5151,17 +7466,22 @@ export {
5151
7466
  OpenWearablesHealthProvider,
5152
7467
  NotionProjectManagementProvider,
5153
7468
  MyFitnessPalHealthProvider,
7469
+ MistralSttProvider,
5154
7470
  MistralLLMProvider,
5155
7471
  MistralEmbeddingProvider,
7472
+ MistralConversationalProvider,
7473
+ MetaWhatsappMessagingProvider,
5156
7474
  LinearProjectManagementProvider,
5157
7475
  JiraProjectManagementProvider,
5158
7476
  IntegrationProviderFactory,
7477
+ INTEGRATION_KEY_TO_TOOLKIT,
5159
7478
  GranolaMeetingRecorderProvider,
5160
7479
  GradiumVoiceProvider,
5161
7480
  GoogleCloudStorageProvider,
5162
7481
  GoogleCalendarProvider,
5163
7482
  GmailOutboundProvider,
5164
7483
  GmailInboundProvider,
7484
+ GithubMessagingProvider,
5165
7485
  GarminHealthProvider,
5166
7486
  FitbitHealthProvider,
5167
7487
  FirefliesMeetingRecorderProvider,
@@ -5169,5 +7489,14 @@ export {
5169
7489
  FalVoiceProvider,
5170
7490
  ElevenLabsVoiceProvider,
5171
7491
  EightSleepHealthProvider,
7492
+ ComposioSdkProvider,
7493
+ ComposioProjectManagementProxy,
7494
+ ComposioPaymentsProxy,
7495
+ ComposioMessagingProxy,
7496
+ ComposioMcpProvider,
7497
+ ComposioGenericProxy,
7498
+ ComposioFallbackResolver,
7499
+ ComposioEmailProxy,
7500
+ ComposioCalendarProxy,
5172
7501
  AppleHealthBridgeProvider
5173
7502
  };