@customclaw/composio 0.0.6 → 0.0.7
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 +22 -5
- package/dist/cli.js +53 -3
- package/dist/client.d.ts +34 -2
- package/dist/client.js +489 -46
- package/dist/client.test.js +295 -15
- package/dist/config.js +15 -2
- package/dist/tools/connections.d.ts +20 -2
- package/dist/tools/connections.js +39 -28
- package/dist/tools/execute.d.ts +2 -0
- package/dist/tools/execute.js +5 -1
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -22,14 +22,33 @@ export class ComposioClient {
|
|
|
22
22
|
/**
|
|
23
23
|
* Get or create a Tool Router session for a user
|
|
24
24
|
*/
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
25
|
+
makeSessionCacheKey(userId, connectedAccounts) {
|
|
26
|
+
if (!connectedAccounts || Object.keys(connectedAccounts).length === 0) {
|
|
27
|
+
return `uid:${userId}`;
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const normalized = Object.entries(connectedAccounts)
|
|
30
|
+
.map(([toolkit, accountId]) => `${toolkit.toLowerCase()}=${accountId}`)
|
|
31
|
+
.sort()
|
|
32
|
+
.join(",");
|
|
33
|
+
return `uid:${userId}::ca:${normalized}`;
|
|
34
|
+
}
|
|
35
|
+
async getSession(userId, connectedAccounts) {
|
|
36
|
+
const key = this.makeSessionCacheKey(userId, connectedAccounts);
|
|
37
|
+
if (this.sessionCache.has(key)) {
|
|
38
|
+
return this.sessionCache.get(key);
|
|
39
|
+
}
|
|
40
|
+
const session = await this.client.toolRouter.create(userId, connectedAccounts ? { connectedAccounts } : undefined);
|
|
41
|
+
this.sessionCache.set(key, session);
|
|
31
42
|
return session;
|
|
32
43
|
}
|
|
44
|
+
clearUserSessionCache(userId) {
|
|
45
|
+
const prefix = `uid:${userId}`;
|
|
46
|
+
for (const key of this.sessionCache.keys()) {
|
|
47
|
+
if (!key.startsWith(prefix))
|
|
48
|
+
continue;
|
|
49
|
+
this.sessionCache.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
/**
|
|
34
53
|
* Check if a toolkit is allowed based on config
|
|
35
54
|
*/
|
|
@@ -111,9 +130,8 @@ export class ComposioClient {
|
|
|
111
130
|
/**
|
|
112
131
|
* Execute a single tool using COMPOSIO_MULTI_EXECUTE_TOOL
|
|
113
132
|
*/
|
|
114
|
-
async executeTool(toolSlug, args, userId) {
|
|
133
|
+
async executeTool(toolSlug, args, userId, connectedAccountId) {
|
|
115
134
|
const uid = this.getUserId(userId);
|
|
116
|
-
const session = await this.getSession(uid);
|
|
117
135
|
const toolkit = toolSlug.split("_")[0]?.toLowerCase() || "";
|
|
118
136
|
if (!this.isToolkitAllowed(toolkit)) {
|
|
119
137
|
return {
|
|
@@ -121,6 +139,17 @@ export class ComposioClient {
|
|
|
121
139
|
error: `Toolkit '${toolkit}' is not allowed by plugin configuration`,
|
|
122
140
|
};
|
|
123
141
|
}
|
|
142
|
+
const accountResolution = await this.resolveConnectedAccountForExecution({
|
|
143
|
+
toolkit,
|
|
144
|
+
userId: uid,
|
|
145
|
+
connectedAccountId,
|
|
146
|
+
});
|
|
147
|
+
if ("error" in accountResolution) {
|
|
148
|
+
return { success: false, error: accountResolution.error };
|
|
149
|
+
}
|
|
150
|
+
const session = await this.getSession(uid, accountResolution.connectedAccountId
|
|
151
|
+
? { [toolkit]: accountResolution.connectedAccountId }
|
|
152
|
+
: undefined);
|
|
124
153
|
try {
|
|
125
154
|
const response = await this.executeMetaTool("COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
126
155
|
tools: [{ tool_slug: toolSlug, arguments: args }],
|
|
@@ -128,6 +157,16 @@ export class ComposioClient {
|
|
|
128
157
|
sync_response_to_workbench: false,
|
|
129
158
|
});
|
|
130
159
|
if (!response.successful) {
|
|
160
|
+
const recovered = await this.tryExecutionRecovery({
|
|
161
|
+
uid,
|
|
162
|
+
toolSlug,
|
|
163
|
+
args,
|
|
164
|
+
connectedAccountId: accountResolution.connectedAccountId,
|
|
165
|
+
metaError: response.error,
|
|
166
|
+
metaData: response.data,
|
|
167
|
+
});
|
|
168
|
+
if (recovered)
|
|
169
|
+
return recovered;
|
|
131
170
|
return { success: false, error: response.error || "Execution failed" };
|
|
132
171
|
}
|
|
133
172
|
const results = response.data?.results || [];
|
|
@@ -137,6 +176,18 @@ export class ComposioClient {
|
|
|
137
176
|
}
|
|
138
177
|
// Response data is nested under result.response
|
|
139
178
|
const toolResponse = result.response;
|
|
179
|
+
if (!toolResponse.successful) {
|
|
180
|
+
const recovered = await this.tryExecutionRecovery({
|
|
181
|
+
uid,
|
|
182
|
+
toolSlug,
|
|
183
|
+
args,
|
|
184
|
+
connectedAccountId: accountResolution.connectedAccountId,
|
|
185
|
+
metaError: toolResponse.error ?? undefined,
|
|
186
|
+
metaData: response.data,
|
|
187
|
+
});
|
|
188
|
+
if (recovered)
|
|
189
|
+
return recovered;
|
|
190
|
+
}
|
|
140
191
|
return {
|
|
141
192
|
success: toolResponse.successful,
|
|
142
193
|
data: toolResponse.data,
|
|
@@ -150,6 +201,159 @@ export class ComposioClient {
|
|
|
150
201
|
};
|
|
151
202
|
}
|
|
152
203
|
}
|
|
204
|
+
async tryExecutionRecovery(params) {
|
|
205
|
+
const directFallback = await this.tryDirectExecutionFallback(params);
|
|
206
|
+
if (directFallback?.success)
|
|
207
|
+
return directFallback;
|
|
208
|
+
const hintedRetry = await this.tryHintedIdentifierRetry({
|
|
209
|
+
...params,
|
|
210
|
+
additionalError: directFallback?.error,
|
|
211
|
+
});
|
|
212
|
+
if (hintedRetry)
|
|
213
|
+
return hintedRetry;
|
|
214
|
+
return directFallback;
|
|
215
|
+
}
|
|
216
|
+
async tryDirectExecutionFallback(params) {
|
|
217
|
+
if (!this.shouldFallbackToDirectExecution(params.uid, params.metaError, params.metaData)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return this.executeDirectTool(params.toolSlug, params.uid, params.args, params.connectedAccountId);
|
|
221
|
+
}
|
|
222
|
+
async executeDirectTool(toolSlug, userId, args, connectedAccountId) {
|
|
223
|
+
try {
|
|
224
|
+
const response = await this.client.tools.execute(toolSlug, {
|
|
225
|
+
userId,
|
|
226
|
+
connectedAccountId,
|
|
227
|
+
arguments: args,
|
|
228
|
+
dangerouslySkipVersionCheck: true,
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
success: Boolean(response?.successful),
|
|
232
|
+
data: response?.data,
|
|
233
|
+
error: response?.error ?? undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
error: err instanceof Error ? err.message : String(err),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async tryHintedIdentifierRetry(params) {
|
|
244
|
+
const combined = this.buildCombinedErrorText(params.metaError, params.metaData, params.additionalError);
|
|
245
|
+
if (!this.shouldRetryFromServerHint(combined))
|
|
246
|
+
return null;
|
|
247
|
+
const hint = this.extractServerHintLiteral(combined);
|
|
248
|
+
if (!hint)
|
|
249
|
+
return null;
|
|
250
|
+
const retryArgs = this.buildRetryArgsFromHint(params.args, combined, hint);
|
|
251
|
+
if (!retryArgs)
|
|
252
|
+
return null;
|
|
253
|
+
return this.executeDirectTool(params.toolSlug, params.uid, retryArgs, params.connectedAccountId);
|
|
254
|
+
}
|
|
255
|
+
shouldFallbackToDirectExecution(uid, metaError, metaData) {
|
|
256
|
+
if (uid === "default")
|
|
257
|
+
return false;
|
|
258
|
+
const combined = this.buildCombinedErrorText(metaError, metaData).toLowerCase();
|
|
259
|
+
return combined.includes("no connected account found for entity id default");
|
|
260
|
+
}
|
|
261
|
+
shouldRetryFromServerHint(errorText) {
|
|
262
|
+
const lower = errorText.toLowerCase();
|
|
263
|
+
return (lower.includes("only allowed to access") ||
|
|
264
|
+
lower.includes("allowed to access the"));
|
|
265
|
+
}
|
|
266
|
+
extractServerHintLiteral(errorText) {
|
|
267
|
+
const matches = errorText.match(/`([^`]+)`/);
|
|
268
|
+
if (!matches?.[1])
|
|
269
|
+
return undefined;
|
|
270
|
+
const literal = matches[1].trim();
|
|
271
|
+
if (!literal)
|
|
272
|
+
return undefined;
|
|
273
|
+
if (literal.length > 64)
|
|
274
|
+
return undefined;
|
|
275
|
+
if (/\s/.test(literal))
|
|
276
|
+
return undefined;
|
|
277
|
+
return literal;
|
|
278
|
+
}
|
|
279
|
+
buildRetryArgsFromHint(args, errorText, hint) {
|
|
280
|
+
const stringEntries = Object.entries(args).filter(([, value]) => typeof value === "string");
|
|
281
|
+
if (stringEntries.length === 1) {
|
|
282
|
+
const [field, current] = stringEntries[0];
|
|
283
|
+
if (current === hint)
|
|
284
|
+
return null;
|
|
285
|
+
return { ...args, [field]: hint };
|
|
286
|
+
}
|
|
287
|
+
if (stringEntries.length === 0) {
|
|
288
|
+
const missing = this.extractSingleMissingField(errorText);
|
|
289
|
+
if (!missing)
|
|
290
|
+
return null;
|
|
291
|
+
return { ...args, [missing]: hint };
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
extractSingleMissingField(errorText) {
|
|
296
|
+
const match = errorText.match(/following fields are missing:\s*\{([^}]+)\}/i);
|
|
297
|
+
const raw = match?.[1];
|
|
298
|
+
if (!raw)
|
|
299
|
+
return undefined;
|
|
300
|
+
const fields = raw
|
|
301
|
+
.split(",")
|
|
302
|
+
.map(part => part.trim().replace(/^['"]|['"]$/g, ""))
|
|
303
|
+
.filter(Boolean);
|
|
304
|
+
return fields.length === 1 ? fields[0] : undefined;
|
|
305
|
+
}
|
|
306
|
+
buildCombinedErrorText(metaError, metaData, additionalError) {
|
|
307
|
+
return [metaError, this.extractNestedMetaError(metaData), additionalError]
|
|
308
|
+
.map(v => String(v || "").trim())
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join("\n");
|
|
311
|
+
}
|
|
312
|
+
extractNestedMetaError(metaData) {
|
|
313
|
+
const results = metaData?.results || [];
|
|
314
|
+
const first = results[0];
|
|
315
|
+
return String(first?.error || "");
|
|
316
|
+
}
|
|
317
|
+
async resolveConnectedAccountForExecution(params) {
|
|
318
|
+
const { toolkit, userId } = params;
|
|
319
|
+
const explicitId = params.connectedAccountId?.trim();
|
|
320
|
+
if (explicitId) {
|
|
321
|
+
try {
|
|
322
|
+
const account = await this.client.connectedAccounts.get(explicitId);
|
|
323
|
+
const accountToolkit = String(account?.toolkit?.slug || "").toLowerCase();
|
|
324
|
+
const accountStatus = String(account?.status || "").toUpperCase();
|
|
325
|
+
if (accountToolkit && accountToolkit !== toolkit) {
|
|
326
|
+
return {
|
|
327
|
+
error: `Connected account '${explicitId}' belongs to toolkit '${accountToolkit}', but tool '${toolkit}' was requested.`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (accountStatus && accountStatus !== "ACTIVE") {
|
|
331
|
+
return {
|
|
332
|
+
error: `Connected account '${explicitId}' is '${accountStatus}', not ACTIVE.`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return { connectedAccountId: explicitId };
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return {
|
|
339
|
+
error: `Invalid connected_account_id '${explicitId}': ${err instanceof Error ? err.message : String(err)}`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const activeAccounts = await this.listConnectedAccounts({
|
|
344
|
+
toolkits: [toolkit],
|
|
345
|
+
userIds: [userId],
|
|
346
|
+
statuses: ["ACTIVE"],
|
|
347
|
+
});
|
|
348
|
+
if (activeAccounts.length <= 1) {
|
|
349
|
+
return { connectedAccountId: activeAccounts[0]?.id };
|
|
350
|
+
}
|
|
351
|
+
const ids = activeAccounts.map(a => a.id).join(", ");
|
|
352
|
+
return {
|
|
353
|
+
error: `Multiple ACTIVE '${toolkit}' accounts found for user_id '${userId}': ${ids}. ` +
|
|
354
|
+
"Please provide connected_account_id to choose one explicitly.",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
153
357
|
/**
|
|
154
358
|
* Get connection status for toolkits using session.toolkits()
|
|
155
359
|
*/
|
|
@@ -157,42 +361,255 @@ export class ComposioClient {
|
|
|
157
361
|
const uid = this.getUserId(userId);
|
|
158
362
|
const session = await this.getSession(uid);
|
|
159
363
|
try {
|
|
160
|
-
const response = await session.toolkits();
|
|
161
|
-
const allToolkits = response.items || [];
|
|
162
|
-
const statuses = [];
|
|
163
364
|
if (toolkits && toolkits.length > 0) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
365
|
+
const requestedToolkits = toolkits.filter(t => this.isToolkitAllowed(t));
|
|
366
|
+
if (requestedToolkits.length === 0)
|
|
367
|
+
return [];
|
|
368
|
+
const toolkitStateMap = await this.getToolkitStateMap(session, requestedToolkits);
|
|
369
|
+
const activeAccountToolkits = await this.getActiveConnectedAccountToolkits(uid, requestedToolkits);
|
|
370
|
+
return requestedToolkits.map((toolkit) => {
|
|
371
|
+
const key = toolkit.toLowerCase();
|
|
372
|
+
return {
|
|
170
373
|
toolkit,
|
|
171
|
-
connected:
|
|
374
|
+
connected: (toolkitStateMap.get(key) ?? false) || activeAccountToolkits.has(key),
|
|
172
375
|
userId: uid,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
376
|
+
};
|
|
377
|
+
});
|
|
175
378
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
379
|
+
const toolkitStateMap = await this.getToolkitStateMap(session);
|
|
380
|
+
const activeAccountToolkits = await this.getActiveConnectedAccountToolkits(uid);
|
|
381
|
+
const connected = new Set();
|
|
382
|
+
for (const [slug, isActive] of toolkitStateMap.entries()) {
|
|
383
|
+
if (!isActive)
|
|
384
|
+
continue;
|
|
385
|
+
if (!this.isToolkitAllowed(slug))
|
|
386
|
+
continue;
|
|
387
|
+
connected.add(slug);
|
|
388
|
+
}
|
|
389
|
+
for (const slug of activeAccountToolkits) {
|
|
390
|
+
if (!this.isToolkitAllowed(slug))
|
|
391
|
+
continue;
|
|
392
|
+
connected.add(slug);
|
|
189
393
|
}
|
|
190
|
-
return
|
|
394
|
+
return Array.from(connected).map((toolkit) => ({
|
|
395
|
+
toolkit,
|
|
396
|
+
connected: true,
|
|
397
|
+
userId: uid,
|
|
398
|
+
}));
|
|
191
399
|
}
|
|
192
400
|
catch (err) {
|
|
193
401
|
throw new Error(`Failed to get connection status: ${err instanceof Error ? err.message : String(err)}`);
|
|
194
402
|
}
|
|
195
403
|
}
|
|
404
|
+
async getToolkitStateMap(session, toolkits) {
|
|
405
|
+
const map = new Map();
|
|
406
|
+
let nextCursor;
|
|
407
|
+
const seenCursors = new Set();
|
|
408
|
+
do {
|
|
409
|
+
const response = await session.toolkits({
|
|
410
|
+
nextCursor,
|
|
411
|
+
limit: 100,
|
|
412
|
+
...(toolkits && toolkits.length > 0 ? { toolkits } : { isConnected: true }),
|
|
413
|
+
});
|
|
414
|
+
const items = response.items || [];
|
|
415
|
+
for (const tk of items) {
|
|
416
|
+
const key = tk.slug.toLowerCase();
|
|
417
|
+
const isActive = tk.connection?.isActive ?? false;
|
|
418
|
+
map.set(key, (map.get(key) ?? false) || isActive);
|
|
419
|
+
}
|
|
420
|
+
nextCursor = response.nextCursor;
|
|
421
|
+
if (!nextCursor)
|
|
422
|
+
break;
|
|
423
|
+
if (seenCursors.has(nextCursor))
|
|
424
|
+
break;
|
|
425
|
+
seenCursors.add(nextCursor);
|
|
426
|
+
} while (true);
|
|
427
|
+
return map;
|
|
428
|
+
}
|
|
429
|
+
async getActiveConnectedAccountToolkits(userId, toolkits) {
|
|
430
|
+
const connected = new Set();
|
|
431
|
+
let cursor;
|
|
432
|
+
const seenCursors = new Set();
|
|
433
|
+
try {
|
|
434
|
+
do {
|
|
435
|
+
const response = await this.client.connectedAccounts.list({
|
|
436
|
+
userIds: [userId],
|
|
437
|
+
statuses: ["ACTIVE"],
|
|
438
|
+
...(toolkits && toolkits.length > 0 ? { toolkitSlugs: toolkits } : {}),
|
|
439
|
+
limit: 100,
|
|
440
|
+
...(cursor ? { cursor } : {}),
|
|
441
|
+
});
|
|
442
|
+
const items = (Array.isArray(response)
|
|
443
|
+
? response
|
|
444
|
+
: response?.items || []);
|
|
445
|
+
for (const item of items) {
|
|
446
|
+
const slug = item.toolkit?.slug;
|
|
447
|
+
if (!slug)
|
|
448
|
+
continue;
|
|
449
|
+
if (item.status && String(item.status).toUpperCase() !== "ACTIVE")
|
|
450
|
+
continue;
|
|
451
|
+
connected.add(slug.toLowerCase());
|
|
452
|
+
}
|
|
453
|
+
cursor = Array.isArray(response)
|
|
454
|
+
? null
|
|
455
|
+
: (response?.nextCursor ?? null);
|
|
456
|
+
if (!cursor)
|
|
457
|
+
break;
|
|
458
|
+
if (seenCursors.has(cursor))
|
|
459
|
+
break;
|
|
460
|
+
seenCursors.add(cursor);
|
|
461
|
+
} while (true);
|
|
462
|
+
return connected;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Best-effort fallback: preserve status checks based on session.toolkits only.
|
|
466
|
+
return connected;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
normalizeStatuses(statuses) {
|
|
470
|
+
if (!statuses || statuses.length === 0)
|
|
471
|
+
return undefined;
|
|
472
|
+
const allowed = new Set(["INITIALIZING", "INITIATED", "ACTIVE", "FAILED", "EXPIRED", "INACTIVE"]);
|
|
473
|
+
const normalized = statuses
|
|
474
|
+
.map(s => String(s || "").trim().toUpperCase())
|
|
475
|
+
.filter(s => allowed.has(s));
|
|
476
|
+
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* List connected accounts with optional filters.
|
|
480
|
+
* Uses raw API first to preserve user_id in responses, then falls back to SDK-normalized output.
|
|
481
|
+
*/
|
|
482
|
+
async listConnectedAccounts(options) {
|
|
483
|
+
const toolkits = options?.toolkits
|
|
484
|
+
?.map(t => String(t || "").trim())
|
|
485
|
+
.filter(t => t.length > 0 && this.isToolkitAllowed(t));
|
|
486
|
+
const userIds = options?.userIds
|
|
487
|
+
?.map(u => String(u || "").trim())
|
|
488
|
+
.filter(Boolean);
|
|
489
|
+
const statuses = this.normalizeStatuses(options?.statuses);
|
|
490
|
+
if (options?.toolkits && (!toolkits || toolkits.length === 0))
|
|
491
|
+
return [];
|
|
492
|
+
try {
|
|
493
|
+
return await this.listConnectedAccountsRaw({
|
|
494
|
+
toolkits,
|
|
495
|
+
userIds,
|
|
496
|
+
statuses,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return this.listConnectedAccountsFallback({
|
|
501
|
+
toolkits,
|
|
502
|
+
userIds,
|
|
503
|
+
statuses,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Find user IDs that have an active connected account for a toolkit.
|
|
509
|
+
*/
|
|
510
|
+
async findActiveUserIdsForToolkit(toolkit) {
|
|
511
|
+
if (!this.isToolkitAllowed(toolkit))
|
|
512
|
+
return [];
|
|
513
|
+
const accounts = await this.listConnectedAccounts({
|
|
514
|
+
toolkits: [toolkit],
|
|
515
|
+
statuses: ["ACTIVE"],
|
|
516
|
+
});
|
|
517
|
+
const userIds = new Set();
|
|
518
|
+
for (const account of accounts) {
|
|
519
|
+
if (account.userId)
|
|
520
|
+
userIds.add(account.userId);
|
|
521
|
+
}
|
|
522
|
+
return Array.from(userIds).sort();
|
|
523
|
+
}
|
|
524
|
+
async listConnectedAccountsRaw(options) {
|
|
525
|
+
const accounts = [];
|
|
526
|
+
let cursor;
|
|
527
|
+
const seenCursors = new Set();
|
|
528
|
+
do {
|
|
529
|
+
const response = await this.client.client.connectedAccounts.list({
|
|
530
|
+
...(options?.toolkits && options.toolkits.length > 0 ? { toolkit_slugs: options.toolkits } : {}),
|
|
531
|
+
...(options?.userIds && options.userIds.length > 0 ? { user_ids: options.userIds } : {}),
|
|
532
|
+
...(options?.statuses && options.statuses.length > 0 ? { statuses: options.statuses } : {}),
|
|
533
|
+
limit: 100,
|
|
534
|
+
...(cursor ? { cursor } : {}),
|
|
535
|
+
});
|
|
536
|
+
const items = (Array.isArray(response)
|
|
537
|
+
? response
|
|
538
|
+
: response?.items || []);
|
|
539
|
+
for (const item of items) {
|
|
540
|
+
const toolkitSlug = (item.toolkit?.slug || "").toString().toLowerCase();
|
|
541
|
+
if (!toolkitSlug)
|
|
542
|
+
continue;
|
|
543
|
+
if (!this.isToolkitAllowed(toolkitSlug))
|
|
544
|
+
continue;
|
|
545
|
+
accounts.push({
|
|
546
|
+
id: String(item.id || ""),
|
|
547
|
+
toolkit: toolkitSlug,
|
|
548
|
+
userId: typeof item.user_id === "string" ? item.user_id : undefined,
|
|
549
|
+
status: typeof item.status === "string" ? item.status : undefined,
|
|
550
|
+
authConfigId: typeof item.auth_config?.id === "string"
|
|
551
|
+
? item.auth_config.id
|
|
552
|
+
: undefined,
|
|
553
|
+
isDisabled: typeof item.is_disabled === "boolean" ? item.is_disabled : undefined,
|
|
554
|
+
createdAt: typeof item.created_at === "string" ? item.created_at : undefined,
|
|
555
|
+
updatedAt: typeof item.updated_at === "string" ? item.updated_at : undefined,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
cursor = Array.isArray(response)
|
|
559
|
+
? null
|
|
560
|
+
: (response?.next_cursor ?? null);
|
|
561
|
+
if (!cursor)
|
|
562
|
+
break;
|
|
563
|
+
if (seenCursors.has(cursor))
|
|
564
|
+
break;
|
|
565
|
+
seenCursors.add(cursor);
|
|
566
|
+
} while (true);
|
|
567
|
+
return accounts;
|
|
568
|
+
}
|
|
569
|
+
async listConnectedAccountsFallback(options) {
|
|
570
|
+
const accounts = [];
|
|
571
|
+
let cursor;
|
|
572
|
+
const seenCursors = new Set();
|
|
573
|
+
do {
|
|
574
|
+
const response = await this.client.connectedAccounts.list({
|
|
575
|
+
...(options?.toolkits && options.toolkits.length > 0 ? { toolkitSlugs: options.toolkits } : {}),
|
|
576
|
+
...(options?.userIds && options.userIds.length > 0 ? { userIds: options.userIds } : {}),
|
|
577
|
+
...(options?.statuses && options.statuses.length > 0 ? { statuses: options.statuses } : {}),
|
|
578
|
+
limit: 100,
|
|
579
|
+
...(cursor ? { cursor } : {}),
|
|
580
|
+
});
|
|
581
|
+
const items = (Array.isArray(response)
|
|
582
|
+
? response
|
|
583
|
+
: response?.items || []);
|
|
584
|
+
for (const item of items) {
|
|
585
|
+
const toolkitSlug = (item.toolkit?.slug || "").toString().toLowerCase();
|
|
586
|
+
if (!toolkitSlug)
|
|
587
|
+
continue;
|
|
588
|
+
if (!this.isToolkitAllowed(toolkitSlug))
|
|
589
|
+
continue;
|
|
590
|
+
accounts.push({
|
|
591
|
+
id: String(item.id || ""),
|
|
592
|
+
toolkit: toolkitSlug,
|
|
593
|
+
status: typeof item.status === "string" ? item.status : undefined,
|
|
594
|
+
authConfigId: typeof item.authConfig?.id === "string"
|
|
595
|
+
? item.authConfig.id
|
|
596
|
+
: undefined,
|
|
597
|
+
isDisabled: typeof item.isDisabled === "boolean" ? item.isDisabled : undefined,
|
|
598
|
+
createdAt: typeof item.createdAt === "string" ? item.createdAt : undefined,
|
|
599
|
+
updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : undefined,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
cursor = Array.isArray(response)
|
|
603
|
+
? null
|
|
604
|
+
: (response?.nextCursor ?? null);
|
|
605
|
+
if (!cursor)
|
|
606
|
+
break;
|
|
607
|
+
if (seenCursors.has(cursor))
|
|
608
|
+
break;
|
|
609
|
+
seenCursors.add(cursor);
|
|
610
|
+
} while (true);
|
|
611
|
+
return accounts;
|
|
612
|
+
}
|
|
196
613
|
/**
|
|
197
614
|
* Create an auth connection for a toolkit using session.authorize()
|
|
198
615
|
*/
|
|
@@ -219,11 +636,29 @@ export class ComposioClient {
|
|
|
219
636
|
const uid = this.getUserId(userId);
|
|
220
637
|
try {
|
|
221
638
|
const session = await this.getSession(uid);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
639
|
+
const seen = new Set();
|
|
640
|
+
let nextCursor;
|
|
641
|
+
const seenCursors = new Set();
|
|
642
|
+
do {
|
|
643
|
+
const response = await session.toolkits({
|
|
644
|
+
nextCursor,
|
|
645
|
+
limit: 100,
|
|
646
|
+
});
|
|
647
|
+
const allToolkits = response.items || [];
|
|
648
|
+
for (const tk of allToolkits) {
|
|
649
|
+
const slug = tk.slug.toLowerCase();
|
|
650
|
+
if (!this.isToolkitAllowed(slug))
|
|
651
|
+
continue;
|
|
652
|
+
seen.add(slug);
|
|
653
|
+
}
|
|
654
|
+
nextCursor = response.nextCursor;
|
|
655
|
+
if (!nextCursor)
|
|
656
|
+
break;
|
|
657
|
+
if (seenCursors.has(nextCursor))
|
|
658
|
+
break;
|
|
659
|
+
seenCursors.add(nextCursor);
|
|
660
|
+
} while (true);
|
|
661
|
+
return Array.from(seen);
|
|
227
662
|
}
|
|
228
663
|
catch (err) {
|
|
229
664
|
const errObj = err;
|
|
@@ -240,17 +675,25 @@ export class ComposioClient {
|
|
|
240
675
|
async disconnectToolkit(toolkit, userId) {
|
|
241
676
|
const uid = this.getUserId(userId);
|
|
242
677
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
:
|
|
247
|
-
|
|
248
|
-
if (
|
|
678
|
+
const activeAccounts = await this.listConnectedAccounts({
|
|
679
|
+
toolkits: [toolkit],
|
|
680
|
+
userIds: [uid],
|
|
681
|
+
statuses: ["ACTIVE"],
|
|
682
|
+
});
|
|
683
|
+
if (activeAccounts.length === 0) {
|
|
249
684
|
return { success: false, error: `No connection found for toolkit '${toolkit}'` };
|
|
250
685
|
}
|
|
251
|
-
|
|
686
|
+
if (activeAccounts.length > 1) {
|
|
687
|
+
const ids = activeAccounts.map(a => a.id).join(", ");
|
|
688
|
+
return {
|
|
689
|
+
success: false,
|
|
690
|
+
error: `Multiple ACTIVE '${toolkit}' accounts found for user_id '${uid}': ${ids}. ` +
|
|
691
|
+
"Use the dashboard to disconnect a specific account.",
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
await this.client.connectedAccounts.delete({ connectedAccountId: activeAccounts[0].id });
|
|
252
695
|
// Clear session cache to refresh connection status
|
|
253
|
-
this.
|
|
696
|
+
this.clearUserSessionCache(uid);
|
|
254
697
|
return { success: true };
|
|
255
698
|
}
|
|
256
699
|
catch (err) {
|