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