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