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