@customclaw/composio 0.0.6 → 0.0.8
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 +64 -8
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +309 -28
- package/dist/client.d.ts +40 -2
- package/dist/client.js +687 -66
- package/dist/config.d.ts +49 -0
- package/dist/config.js +45 -8
- package/dist/index.d.ts +20 -0
- package/dist/index.js +21 -14
- 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 +15 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +76 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +3 -2
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -226
package/dist/client.js
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import { Composio } from "@composio/core";
|
|
2
|
+
import { normalizeSessionTags, normalizeToolkitList, normalizeToolkitSlug, normalizeToolSlug, normalizeToolSlugList, } from "./utils.js";
|
|
3
|
+
// Heuristic only: token matching may block some benign tools.
|
|
4
|
+
// Use `allowedToolSlugs` to explicitly override specific slugs.
|
|
5
|
+
const DESTRUCTIVE_TOOL_VERBS = new Set([
|
|
6
|
+
"CREATE",
|
|
7
|
+
"DELETE",
|
|
8
|
+
"DESTROY",
|
|
9
|
+
"DISABLE",
|
|
10
|
+
"DISCONNECT",
|
|
11
|
+
"ERASE",
|
|
12
|
+
"MODIFY",
|
|
13
|
+
"PATCH",
|
|
14
|
+
"POST",
|
|
15
|
+
"PUT",
|
|
16
|
+
"REMOVE",
|
|
17
|
+
"RENAME",
|
|
18
|
+
"REPLACE",
|
|
19
|
+
"REVOKE",
|
|
20
|
+
"SEND",
|
|
21
|
+
"SET",
|
|
22
|
+
"TRUNCATE",
|
|
23
|
+
"UNSUBSCRIBE",
|
|
24
|
+
"UPDATE",
|
|
25
|
+
"UPSERT",
|
|
26
|
+
"WRITE",
|
|
27
|
+
]);
|
|
28
|
+
function isConnectedAccountStatusFilter(value) {
|
|
29
|
+
return [
|
|
30
|
+
"INITIALIZING",
|
|
31
|
+
"INITIATED",
|
|
32
|
+
"ACTIVE",
|
|
33
|
+
"FAILED",
|
|
34
|
+
"EXPIRED",
|
|
35
|
+
"INACTIVE",
|
|
36
|
+
].includes(value);
|
|
37
|
+
}
|
|
2
38
|
/**
|
|
3
39
|
* Composio client wrapper using Tool Router pattern
|
|
4
40
|
*/
|
|
@@ -10,7 +46,15 @@ export class ComposioClient {
|
|
|
10
46
|
if (!config.apiKey) {
|
|
11
47
|
throw new Error("Composio API key required. Set COMPOSIO_API_KEY env var or plugins.composio.apiKey in config.");
|
|
12
48
|
}
|
|
13
|
-
this.config =
|
|
49
|
+
this.config = {
|
|
50
|
+
...config,
|
|
51
|
+
allowedToolkits: normalizeToolkitList(config.allowedToolkits),
|
|
52
|
+
blockedToolkits: normalizeToolkitList(config.blockedToolkits),
|
|
53
|
+
sessionTags: normalizeSessionTags(config.sessionTags),
|
|
54
|
+
allowedToolSlugs: normalizeToolSlugList(config.allowedToolSlugs),
|
|
55
|
+
blockedToolSlugs: normalizeToolSlugList(config.blockedToolSlugs),
|
|
56
|
+
readOnlyMode: Boolean(config.readOnlyMode),
|
|
57
|
+
};
|
|
14
58
|
this.client = new Composio({ apiKey: config.apiKey });
|
|
15
59
|
}
|
|
16
60
|
/**
|
|
@@ -22,35 +66,169 @@ export class ComposioClient {
|
|
|
22
66
|
/**
|
|
23
67
|
* Get or create a Tool Router session for a user
|
|
24
68
|
*/
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
69
|
+
makeSessionCacheKey(userId, connectedAccounts) {
|
|
70
|
+
if (!connectedAccounts || Object.keys(connectedAccounts).length === 0) {
|
|
71
|
+
return `uid:${userId}`;
|
|
72
|
+
}
|
|
73
|
+
const normalized = Object.entries(connectedAccounts)
|
|
74
|
+
.map(([toolkit, accountId]) => `${normalizeToolkitSlug(toolkit)}=${accountId}`)
|
|
75
|
+
.sort()
|
|
76
|
+
.join(",");
|
|
77
|
+
return `uid:${userId}::ca:${normalized}`;
|
|
78
|
+
}
|
|
79
|
+
normalizeConnectedAccountsOverride(connectedAccounts) {
|
|
80
|
+
if (!connectedAccounts)
|
|
81
|
+
return undefined;
|
|
82
|
+
const normalized = Object.entries(connectedAccounts)
|
|
83
|
+
.map(([toolkit, accountId]) => [normalizeToolkitSlug(toolkit), String(accountId || "").trim()])
|
|
84
|
+
.filter(([toolkit, accountId]) => toolkit.length > 0 && accountId.length > 0);
|
|
85
|
+
if (normalized.length === 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
return Object.fromEntries(normalized);
|
|
88
|
+
}
|
|
89
|
+
buildToolRouterBlockedToolsConfig() {
|
|
90
|
+
const blocked = this.config.blockedToolSlugs;
|
|
91
|
+
if (!blocked || blocked.length === 0)
|
|
92
|
+
return undefined;
|
|
93
|
+
const byToolkit = new Map();
|
|
94
|
+
for (const slug of blocked) {
|
|
95
|
+
const normalizedSlug = normalizeToolSlug(slug);
|
|
96
|
+
const toolkit = normalizeToolkitSlug(normalizedSlug.split("_")[0] || "");
|
|
97
|
+
if (!toolkit)
|
|
98
|
+
continue;
|
|
99
|
+
if (!this.isToolkitAllowed(toolkit))
|
|
100
|
+
continue;
|
|
101
|
+
if (!byToolkit.has(toolkit))
|
|
102
|
+
byToolkit.set(toolkit, new Set());
|
|
103
|
+
byToolkit.get(toolkit).add(normalizedSlug);
|
|
104
|
+
}
|
|
105
|
+
if (byToolkit.size === 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
const tools = {};
|
|
108
|
+
for (const [toolkit, slugs] of byToolkit.entries()) {
|
|
109
|
+
tools[toolkit] = { disable: Array.from(slugs) };
|
|
110
|
+
}
|
|
111
|
+
return tools;
|
|
112
|
+
}
|
|
113
|
+
buildSessionConfig(connectedAccounts) {
|
|
114
|
+
const sessionConfig = {};
|
|
115
|
+
const normalizedConnectedAccounts = this.normalizeConnectedAccountsOverride(connectedAccounts);
|
|
116
|
+
if (normalizedConnectedAccounts) {
|
|
117
|
+
sessionConfig.connectedAccounts = normalizedConnectedAccounts;
|
|
118
|
+
}
|
|
119
|
+
if (this.config.allowedToolkits && this.config.allowedToolkits.length > 0) {
|
|
120
|
+
sessionConfig.toolkits = { enable: this.config.allowedToolkits };
|
|
121
|
+
}
|
|
122
|
+
else if (this.config.blockedToolkits && this.config.blockedToolkits.length > 0) {
|
|
123
|
+
sessionConfig.toolkits = { disable: this.config.blockedToolkits };
|
|
124
|
+
}
|
|
125
|
+
const tags = new Set(this.config.sessionTags || []);
|
|
126
|
+
if (this.config.readOnlyMode)
|
|
127
|
+
tags.add("readOnlyHint");
|
|
128
|
+
if (tags.size > 0) {
|
|
129
|
+
sessionConfig.tags = Array.from(tags);
|
|
130
|
+
}
|
|
131
|
+
const blockedToolsConfig = this.buildToolRouterBlockedToolsConfig();
|
|
132
|
+
if (blockedToolsConfig) {
|
|
133
|
+
sessionConfig.tools = blockedToolsConfig;
|
|
134
|
+
}
|
|
135
|
+
return Object.keys(sessionConfig).length > 0 ? sessionConfig : undefined;
|
|
136
|
+
}
|
|
137
|
+
async getSession(userId, connectedAccounts) {
|
|
138
|
+
const normalizedConnectedAccounts = this.normalizeConnectedAccountsOverride(connectedAccounts);
|
|
139
|
+
const key = this.makeSessionCacheKey(userId, normalizedConnectedAccounts);
|
|
140
|
+
if (this.sessionCache.has(key)) {
|
|
141
|
+
return this.sessionCache.get(key);
|
|
142
|
+
}
|
|
143
|
+
const sessionConfig = this.buildSessionConfig(normalizedConnectedAccounts);
|
|
144
|
+
let session;
|
|
145
|
+
try {
|
|
146
|
+
session = await this.client.toolRouter.create(userId, sessionConfig);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!this.shouldRetrySessionWithoutToolkitFilters(err, sessionConfig)) {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
const { toolkits: _removedToolkits, ...retryWithoutToolkits } = sessionConfig ?? {};
|
|
153
|
+
const retryConfig = Object.keys(retryWithoutToolkits).length > 0
|
|
154
|
+
? retryWithoutToolkits
|
|
155
|
+
: undefined;
|
|
156
|
+
session = await this.client.toolRouter.create(userId, retryConfig);
|
|
28
157
|
}
|
|
29
|
-
|
|
30
|
-
this.sessionCache.set(userId, session);
|
|
158
|
+
this.sessionCache.set(key, session);
|
|
31
159
|
return session;
|
|
32
160
|
}
|
|
161
|
+
shouldRetrySessionWithoutToolkitFilters(err, sessionConfig) {
|
|
162
|
+
const enabledToolkits = sessionConfig?.toolkits?.enable;
|
|
163
|
+
if (!Array.isArray(enabledToolkits) || enabledToolkits.length === 0) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const message = String(err instanceof Error ? err.message : err || "").toLowerCase();
|
|
167
|
+
return (message.includes("require auth configs but none exist") &&
|
|
168
|
+
message.includes("please specify them in auth_configs"));
|
|
169
|
+
}
|
|
170
|
+
clearUserSessionCache(userId) {
|
|
171
|
+
const prefix = `uid:${userId}`;
|
|
172
|
+
for (const key of this.sessionCache.keys()) {
|
|
173
|
+
if (!key.startsWith(prefix))
|
|
174
|
+
continue;
|
|
175
|
+
this.sessionCache.delete(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
33
178
|
/**
|
|
34
179
|
* Check if a toolkit is allowed based on config
|
|
35
180
|
*/
|
|
36
181
|
isToolkitAllowed(toolkit) {
|
|
182
|
+
const normalizedToolkit = normalizeToolkitSlug(toolkit);
|
|
183
|
+
if (!normalizedToolkit)
|
|
184
|
+
return false;
|
|
37
185
|
const { allowedToolkits, blockedToolkits } = this.config;
|
|
38
|
-
if (blockedToolkits?.includes(
|
|
186
|
+
if (blockedToolkits?.includes(normalizedToolkit)) {
|
|
39
187
|
return false;
|
|
40
188
|
}
|
|
41
189
|
if (allowedToolkits && allowedToolkits.length > 0) {
|
|
42
|
-
return allowedToolkits.includes(
|
|
190
|
+
return allowedToolkits.includes(normalizedToolkit);
|
|
43
191
|
}
|
|
44
192
|
return true;
|
|
45
193
|
}
|
|
194
|
+
isLikelyDestructiveToolSlug(toolSlug) {
|
|
195
|
+
const tokens = normalizeToolSlug(toolSlug)
|
|
196
|
+
.split("_")
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
return tokens.some((token) => DESTRUCTIVE_TOOL_VERBS.has(token));
|
|
199
|
+
}
|
|
200
|
+
getToolSlugRestrictionError(toolSlug) {
|
|
201
|
+
const normalizedToolSlug = normalizeToolSlug(toolSlug);
|
|
202
|
+
if (!normalizedToolSlug)
|
|
203
|
+
return "tool_slug is required";
|
|
204
|
+
const isExplicitlyAllowed = this.config.allowedToolSlugs?.includes(normalizedToolSlug) ?? false;
|
|
205
|
+
if (this.config.allowedToolSlugs && this.config.allowedToolSlugs.length > 0) {
|
|
206
|
+
if (!isExplicitlyAllowed) {
|
|
207
|
+
return `Tool '${normalizedToolSlug}' is not in allowedToolSlugs`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (this.config.blockedToolSlugs?.includes(normalizedToolSlug)) {
|
|
211
|
+
return `Tool '${normalizedToolSlug}' is blocked by plugin configuration`;
|
|
212
|
+
}
|
|
213
|
+
if (this.config.readOnlyMode && !isExplicitlyAllowed && this.isLikelyDestructiveToolSlug(normalizedToolSlug)) {
|
|
214
|
+
return (`Tool '${normalizedToolSlug}' was blocked by readOnlyMode because it appears to modify data. ` +
|
|
215
|
+
"Disable readOnlyMode or add this slug to allowedToolSlugs if execution is intentional.");
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
46
219
|
/**
|
|
47
220
|
* Execute a Tool Router meta-tool
|
|
48
221
|
*/
|
|
49
|
-
async executeMetaTool(toolName, args) {
|
|
50
|
-
const response = await this.client.
|
|
222
|
+
async executeMetaTool(sessionId, toolName, args) {
|
|
223
|
+
const response = await this.client.tools.executeMetaTool(toolName, {
|
|
224
|
+
sessionId,
|
|
51
225
|
arguments: args,
|
|
52
226
|
});
|
|
53
|
-
return
|
|
227
|
+
return {
|
|
228
|
+
successful: Boolean(response.successful),
|
|
229
|
+
data: response.data,
|
|
230
|
+
error: response.error ?? undefined,
|
|
231
|
+
};
|
|
54
232
|
}
|
|
55
233
|
/**
|
|
56
234
|
* Search for tools matching a query using COMPOSIO_SEARCH_TOOLS
|
|
@@ -58,10 +236,10 @@ export class ComposioClient {
|
|
|
58
236
|
async searchTools(query, options) {
|
|
59
237
|
const userId = this.getUserId(options?.userId);
|
|
60
238
|
const session = await this.getSession(userId);
|
|
239
|
+
const requestedToolkits = normalizeToolkitList(options?.toolkits);
|
|
61
240
|
try {
|
|
62
|
-
const response = await this.executeMetaTool("COMPOSIO_SEARCH_TOOLS", {
|
|
241
|
+
const response = await this.executeMetaTool(session.sessionId, "COMPOSIO_SEARCH_TOOLS", {
|
|
63
242
|
queries: [{ use_case: query }],
|
|
64
|
-
session: { id: session.sessionId },
|
|
65
243
|
});
|
|
66
244
|
if (!response.successful || !response.data) {
|
|
67
245
|
throw new Error(response.error || "Search failed");
|
|
@@ -81,11 +259,11 @@ export class ComposioClient {
|
|
|
81
259
|
continue;
|
|
82
260
|
seenSlugs.add(slug);
|
|
83
261
|
const schema = toolSchemas[slug];
|
|
84
|
-
const toolkit = schema?.toolkit || slug.split("_")[0] || "";
|
|
262
|
+
const toolkit = normalizeToolkitSlug(schema?.toolkit || slug.split("_")[0] || "");
|
|
85
263
|
if (!this.isToolkitAllowed(toolkit))
|
|
86
264
|
continue;
|
|
87
|
-
if (
|
|
88
|
-
if (!
|
|
265
|
+
if (requestedToolkits && requestedToolkits.length > 0) {
|
|
266
|
+
if (!requestedToolkits.includes(toolkit)) {
|
|
89
267
|
continue;
|
|
90
268
|
}
|
|
91
269
|
}
|
|
@@ -111,23 +289,47 @@ export class ComposioClient {
|
|
|
111
289
|
/**
|
|
112
290
|
* Execute a single tool using COMPOSIO_MULTI_EXECUTE_TOOL
|
|
113
291
|
*/
|
|
114
|
-
async executeTool(toolSlug, args, userId) {
|
|
292
|
+
async executeTool(toolSlug, args, userId, connectedAccountId) {
|
|
115
293
|
const uid = this.getUserId(userId);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
294
|
+
const normalizedToolSlug = normalizeToolSlug(toolSlug);
|
|
295
|
+
const toolRestrictionError = this.getToolSlugRestrictionError(normalizedToolSlug);
|
|
296
|
+
if (toolRestrictionError) {
|
|
297
|
+
return { success: false, error: toolRestrictionError };
|
|
298
|
+
}
|
|
299
|
+
const toolkit = normalizeToolkitSlug(normalizedToolSlug.split("_")[0] || "");
|
|
118
300
|
if (!this.isToolkitAllowed(toolkit)) {
|
|
119
301
|
return {
|
|
120
302
|
success: false,
|
|
121
303
|
error: `Toolkit '${toolkit}' is not allowed by plugin configuration`,
|
|
122
304
|
};
|
|
123
305
|
}
|
|
306
|
+
const accountResolution = await this.resolveConnectedAccountForExecution({
|
|
307
|
+
toolkit,
|
|
308
|
+
userId: uid,
|
|
309
|
+
connectedAccountId,
|
|
310
|
+
});
|
|
311
|
+
if ("error" in accountResolution) {
|
|
312
|
+
return { success: false, error: accountResolution.error };
|
|
313
|
+
}
|
|
314
|
+
const session = await this.getSession(uid, accountResolution.connectedAccountId
|
|
315
|
+
? { [toolkit]: accountResolution.connectedAccountId }
|
|
316
|
+
: undefined);
|
|
124
317
|
try {
|
|
125
|
-
const response = await this.executeMetaTool("COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
126
|
-
tools: [{ tool_slug:
|
|
127
|
-
session: { id: session.sessionId },
|
|
318
|
+
const response = await this.executeMetaTool(session.sessionId, "COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
319
|
+
tools: [{ tool_slug: normalizedToolSlug, arguments: args }],
|
|
128
320
|
sync_response_to_workbench: false,
|
|
129
321
|
});
|
|
130
322
|
if (!response.successful) {
|
|
323
|
+
const recovered = await this.tryExecutionRecovery({
|
|
324
|
+
uid,
|
|
325
|
+
toolSlug: normalizedToolSlug,
|
|
326
|
+
args,
|
|
327
|
+
connectedAccountId: accountResolution.connectedAccountId,
|
|
328
|
+
metaError: response.error,
|
|
329
|
+
metaData: response.data,
|
|
330
|
+
});
|
|
331
|
+
if (recovered)
|
|
332
|
+
return recovered;
|
|
131
333
|
return { success: false, error: response.error || "Execution failed" };
|
|
132
334
|
}
|
|
133
335
|
const results = response.data?.results || [];
|
|
@@ -137,6 +339,18 @@ export class ComposioClient {
|
|
|
137
339
|
}
|
|
138
340
|
// Response data is nested under result.response
|
|
139
341
|
const toolResponse = result.response;
|
|
342
|
+
if (!toolResponse.successful) {
|
|
343
|
+
const recovered = await this.tryExecutionRecovery({
|
|
344
|
+
uid,
|
|
345
|
+
toolSlug: normalizedToolSlug,
|
|
346
|
+
args,
|
|
347
|
+
connectedAccountId: accountResolution.connectedAccountId,
|
|
348
|
+
metaError: toolResponse.error ?? undefined,
|
|
349
|
+
metaData: response.data,
|
|
350
|
+
});
|
|
351
|
+
if (recovered)
|
|
352
|
+
return recovered;
|
|
353
|
+
}
|
|
140
354
|
return {
|
|
141
355
|
success: toolResponse.successful,
|
|
142
356
|
data: toolResponse.data,
|
|
@@ -150,6 +364,160 @@ export class ComposioClient {
|
|
|
150
364
|
};
|
|
151
365
|
}
|
|
152
366
|
}
|
|
367
|
+
async tryExecutionRecovery(params) {
|
|
368
|
+
const directFallback = await this.tryDirectExecutionFallback(params);
|
|
369
|
+
if (directFallback?.success)
|
|
370
|
+
return directFallback;
|
|
371
|
+
const hintedRetry = await this.tryHintedIdentifierRetry({
|
|
372
|
+
...params,
|
|
373
|
+
additionalError: directFallback?.error,
|
|
374
|
+
});
|
|
375
|
+
if (hintedRetry)
|
|
376
|
+
return hintedRetry;
|
|
377
|
+
return directFallback;
|
|
378
|
+
}
|
|
379
|
+
async tryDirectExecutionFallback(params) {
|
|
380
|
+
if (!this.shouldFallbackToDirectExecution(params.uid, params.metaError, params.metaData)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return this.executeDirectTool(params.toolSlug, params.uid, params.args, params.connectedAccountId);
|
|
384
|
+
}
|
|
385
|
+
async executeDirectTool(toolSlug, userId, args, connectedAccountId) {
|
|
386
|
+
try {
|
|
387
|
+
const response = await this.client.tools.execute(toolSlug, {
|
|
388
|
+
userId,
|
|
389
|
+
connectedAccountId,
|
|
390
|
+
arguments: args,
|
|
391
|
+
dangerouslySkipVersionCheck: true,
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
success: Boolean(response?.successful),
|
|
395
|
+
data: response?.data,
|
|
396
|
+
error: response?.error ?? undefined,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
error: err instanceof Error ? err.message : String(err),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async tryHintedIdentifierRetry(params) {
|
|
407
|
+
const combined = this.buildCombinedErrorText(params.metaError, params.metaData, params.additionalError);
|
|
408
|
+
if (!this.shouldRetryFromServerHint(combined))
|
|
409
|
+
return null;
|
|
410
|
+
const hint = this.extractServerHintLiteral(combined);
|
|
411
|
+
if (!hint)
|
|
412
|
+
return null;
|
|
413
|
+
const retryArgs = this.buildRetryArgsFromHint(params.args, combined, hint);
|
|
414
|
+
if (!retryArgs)
|
|
415
|
+
return null;
|
|
416
|
+
return this.executeDirectTool(params.toolSlug, params.uid, retryArgs, params.connectedAccountId);
|
|
417
|
+
}
|
|
418
|
+
shouldFallbackToDirectExecution(uid, metaError, metaData) {
|
|
419
|
+
if (uid === "default")
|
|
420
|
+
return false;
|
|
421
|
+
const combined = this.buildCombinedErrorText(metaError, metaData).toLowerCase();
|
|
422
|
+
return combined.includes("no connected account found for entity id default");
|
|
423
|
+
}
|
|
424
|
+
shouldRetryFromServerHint(errorText) {
|
|
425
|
+
const lower = errorText.toLowerCase();
|
|
426
|
+
return (lower.includes("only allowed to access") ||
|
|
427
|
+
lower.includes("allowed to access the"));
|
|
428
|
+
}
|
|
429
|
+
extractServerHintLiteral(errorText) {
|
|
430
|
+
const matches = errorText.match(/`([^`]+)`/);
|
|
431
|
+
if (!matches?.[1])
|
|
432
|
+
return undefined;
|
|
433
|
+
const literal = matches[1].trim();
|
|
434
|
+
if (!literal)
|
|
435
|
+
return undefined;
|
|
436
|
+
if (literal.length > 64)
|
|
437
|
+
return undefined;
|
|
438
|
+
if (/\s/.test(literal))
|
|
439
|
+
return undefined;
|
|
440
|
+
return literal;
|
|
441
|
+
}
|
|
442
|
+
buildRetryArgsFromHint(args, errorText, hint) {
|
|
443
|
+
const stringEntries = Object.entries(args).filter(([, value]) => typeof value === "string");
|
|
444
|
+
if (stringEntries.length === 1) {
|
|
445
|
+
const [field, current] = stringEntries[0];
|
|
446
|
+
if (current === hint)
|
|
447
|
+
return null;
|
|
448
|
+
return { ...args, [field]: hint };
|
|
449
|
+
}
|
|
450
|
+
if (stringEntries.length === 0) {
|
|
451
|
+
const missing = this.extractSingleMissingField(errorText);
|
|
452
|
+
if (!missing)
|
|
453
|
+
return null;
|
|
454
|
+
return { ...args, [missing]: hint };
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
extractSingleMissingField(errorText) {
|
|
459
|
+
const match = errorText.match(/following fields are missing:\s*\{([^}]+)\}/i);
|
|
460
|
+
const raw = match?.[1];
|
|
461
|
+
if (!raw)
|
|
462
|
+
return undefined;
|
|
463
|
+
const fields = raw
|
|
464
|
+
.split(",")
|
|
465
|
+
.map(part => part.trim().replace(/^['"]|['"]$/g, ""))
|
|
466
|
+
.filter(Boolean);
|
|
467
|
+
return fields.length === 1 ? fields[0] : undefined;
|
|
468
|
+
}
|
|
469
|
+
buildCombinedErrorText(metaError, metaData, additionalError) {
|
|
470
|
+
return [metaError, this.extractNestedMetaError(metaData), additionalError]
|
|
471
|
+
.map(v => String(v || "").trim())
|
|
472
|
+
.filter(Boolean)
|
|
473
|
+
.join("\n");
|
|
474
|
+
}
|
|
475
|
+
extractNestedMetaError(metaData) {
|
|
476
|
+
const results = metaData?.results || [];
|
|
477
|
+
const first = results[0];
|
|
478
|
+
return String(first?.error || "");
|
|
479
|
+
}
|
|
480
|
+
async resolveConnectedAccountForExecution(params) {
|
|
481
|
+
const toolkit = normalizeToolkitSlug(params.toolkit);
|
|
482
|
+
const { userId } = params;
|
|
483
|
+
const explicitId = params.connectedAccountId?.trim();
|
|
484
|
+
if (explicitId) {
|
|
485
|
+
try {
|
|
486
|
+
const account = await this.client.connectedAccounts.get(explicitId);
|
|
487
|
+
const accountToolkit = normalizeToolkitSlug(String(account?.toolkit?.slug || ""));
|
|
488
|
+
const accountStatus = String(account?.status || "").toUpperCase();
|
|
489
|
+
if (accountToolkit && accountToolkit !== toolkit) {
|
|
490
|
+
return {
|
|
491
|
+
error: `Connected account '${explicitId}' belongs to toolkit '${accountToolkit}', but tool '${toolkit}' was requested.`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (accountStatus && accountStatus !== "ACTIVE") {
|
|
495
|
+
return {
|
|
496
|
+
error: `Connected account '${explicitId}' is '${accountStatus}', not ACTIVE.`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return { connectedAccountId: explicitId };
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
return {
|
|
503
|
+
error: `Invalid connected_account_id '${explicitId}': ${err instanceof Error ? err.message : String(err)}`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const activeAccounts = await this.listConnectedAccounts({
|
|
508
|
+
toolkits: [toolkit],
|
|
509
|
+
userIds: [userId],
|
|
510
|
+
statuses: ["ACTIVE"],
|
|
511
|
+
});
|
|
512
|
+
if (activeAccounts.length <= 1) {
|
|
513
|
+
return { connectedAccountId: activeAccounts[0]?.id };
|
|
514
|
+
}
|
|
515
|
+
const ids = activeAccounts.map(a => a.id).join(", ");
|
|
516
|
+
return {
|
|
517
|
+
error: `Multiple ACTIVE '${toolkit}' accounts found for user_id '${userId}': ${ids}. ` +
|
|
518
|
+
"Please provide connected_account_id to choose one explicitly.",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
153
521
|
/**
|
|
154
522
|
* Get connection status for toolkits using session.toolkits()
|
|
155
523
|
*/
|
|
@@ -157,53 +525,270 @@ export class ComposioClient {
|
|
|
157
525
|
const uid = this.getUserId(userId);
|
|
158
526
|
const session = await this.getSession(uid);
|
|
159
527
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
528
|
+
const normalizedToolkits = normalizeToolkitList(toolkits);
|
|
529
|
+
if (normalizedToolkits && normalizedToolkits.length > 0) {
|
|
530
|
+
const requestedToolkits = normalizedToolkits.filter((t) => this.isToolkitAllowed(t));
|
|
531
|
+
if (requestedToolkits.length === 0)
|
|
532
|
+
return [];
|
|
533
|
+
const toolkitStateMap = await this.getToolkitStateMap(session, requestedToolkits);
|
|
534
|
+
const activeAccountToolkits = await this.getActiveConnectedAccountToolkits(uid, requestedToolkits);
|
|
535
|
+
return requestedToolkits.map((toolkit) => {
|
|
536
|
+
const key = normalizeToolkitSlug(toolkit);
|
|
537
|
+
return {
|
|
170
538
|
toolkit,
|
|
171
|
-
connected:
|
|
539
|
+
connected: (toolkitStateMap.get(key) ?? false) || activeAccountToolkits.has(key),
|
|
172
540
|
userId: uid,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
541
|
+
};
|
|
542
|
+
});
|
|
175
543
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
connected: true,
|
|
186
|
-
userId: uid,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
544
|
+
const toolkitStateMap = await this.getToolkitStateMap(session);
|
|
545
|
+
const activeAccountToolkits = await this.getActiveConnectedAccountToolkits(uid);
|
|
546
|
+
const connected = new Set();
|
|
547
|
+
for (const [slug, isActive] of toolkitStateMap.entries()) {
|
|
548
|
+
if (!isActive)
|
|
549
|
+
continue;
|
|
550
|
+
if (!this.isToolkitAllowed(slug))
|
|
551
|
+
continue;
|
|
552
|
+
connected.add(slug);
|
|
189
553
|
}
|
|
190
|
-
|
|
554
|
+
for (const slug of activeAccountToolkits) {
|
|
555
|
+
if (!this.isToolkitAllowed(slug))
|
|
556
|
+
continue;
|
|
557
|
+
connected.add(slug);
|
|
558
|
+
}
|
|
559
|
+
return Array.from(connected).map((toolkit) => ({
|
|
560
|
+
toolkit,
|
|
561
|
+
connected: true,
|
|
562
|
+
userId: uid,
|
|
563
|
+
}));
|
|
191
564
|
}
|
|
192
565
|
catch (err) {
|
|
193
566
|
throw new Error(`Failed to get connection status: ${err instanceof Error ? err.message : String(err)}`);
|
|
194
567
|
}
|
|
195
568
|
}
|
|
569
|
+
async getToolkitStateMap(session, toolkits) {
|
|
570
|
+
const map = new Map();
|
|
571
|
+
let nextCursor;
|
|
572
|
+
const seenCursors = new Set();
|
|
573
|
+
do {
|
|
574
|
+
const response = await session.toolkits({
|
|
575
|
+
nextCursor,
|
|
576
|
+
limit: 100,
|
|
577
|
+
...(toolkits && toolkits.length > 0 ? { toolkits } : { isConnected: true }),
|
|
578
|
+
});
|
|
579
|
+
const items = response.items || [];
|
|
580
|
+
for (const tk of items) {
|
|
581
|
+
const key = normalizeToolkitSlug(tk.slug);
|
|
582
|
+
const isActive = tk.connection?.isActive ?? false;
|
|
583
|
+
map.set(key, (map.get(key) ?? false) || isActive);
|
|
584
|
+
}
|
|
585
|
+
nextCursor = response.nextCursor;
|
|
586
|
+
if (!nextCursor)
|
|
587
|
+
break;
|
|
588
|
+
if (seenCursors.has(nextCursor))
|
|
589
|
+
break;
|
|
590
|
+
seenCursors.add(nextCursor);
|
|
591
|
+
} while (true);
|
|
592
|
+
return map;
|
|
593
|
+
}
|
|
594
|
+
async getActiveConnectedAccountToolkits(userId, toolkits) {
|
|
595
|
+
const connected = new Set();
|
|
596
|
+
const normalizedToolkits = normalizeToolkitList(toolkits);
|
|
597
|
+
let cursor;
|
|
598
|
+
const seenCursors = new Set();
|
|
599
|
+
try {
|
|
600
|
+
do {
|
|
601
|
+
const response = await this.client.connectedAccounts.list({
|
|
602
|
+
userIds: [userId],
|
|
603
|
+
statuses: ["ACTIVE"],
|
|
604
|
+
...(normalizedToolkits && normalizedToolkits.length > 0 ? { toolkitSlugs: normalizedToolkits } : {}),
|
|
605
|
+
limit: 100,
|
|
606
|
+
...(cursor ? { cursor } : {}),
|
|
607
|
+
});
|
|
608
|
+
const items = (Array.isArray(response)
|
|
609
|
+
? response
|
|
610
|
+
: response?.items || []);
|
|
611
|
+
for (const item of items) {
|
|
612
|
+
const slug = normalizeToolkitSlug(item.toolkit?.slug || "");
|
|
613
|
+
if (!slug)
|
|
614
|
+
continue;
|
|
615
|
+
if (item.status && String(item.status).toUpperCase() !== "ACTIVE")
|
|
616
|
+
continue;
|
|
617
|
+
connected.add(slug);
|
|
618
|
+
}
|
|
619
|
+
cursor = Array.isArray(response)
|
|
620
|
+
? null
|
|
621
|
+
: (response?.nextCursor ?? null);
|
|
622
|
+
if (!cursor)
|
|
623
|
+
break;
|
|
624
|
+
if (seenCursors.has(cursor))
|
|
625
|
+
break;
|
|
626
|
+
seenCursors.add(cursor);
|
|
627
|
+
} while (true);
|
|
628
|
+
return connected;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// Best-effort fallback: preserve status checks based on session.toolkits only.
|
|
632
|
+
return connected;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
normalizeStatuses(statuses) {
|
|
636
|
+
if (!statuses || statuses.length === 0)
|
|
637
|
+
return undefined;
|
|
638
|
+
const normalized = statuses
|
|
639
|
+
.map(s => String(s || "").trim().toUpperCase())
|
|
640
|
+
.filter(isConnectedAccountStatusFilter);
|
|
641
|
+
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* List connected accounts with optional filters.
|
|
645
|
+
* Uses raw API first to preserve user_id in responses, then falls back to SDK-normalized output.
|
|
646
|
+
*/
|
|
647
|
+
async listConnectedAccounts(options) {
|
|
648
|
+
const toolkits = normalizeToolkitList(options?.toolkits)?.filter((t) => this.isToolkitAllowed(t));
|
|
649
|
+
const userIds = options?.userIds
|
|
650
|
+
?.map(u => String(u || "").trim())
|
|
651
|
+
.filter(Boolean);
|
|
652
|
+
const statuses = this.normalizeStatuses(options?.statuses);
|
|
653
|
+
if (options?.toolkits && (!toolkits || toolkits.length === 0))
|
|
654
|
+
return [];
|
|
655
|
+
try {
|
|
656
|
+
return await this.listConnectedAccountsRaw({
|
|
657
|
+
toolkits,
|
|
658
|
+
userIds,
|
|
659
|
+
statuses,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return this.listConnectedAccountsFallback({
|
|
664
|
+
toolkits,
|
|
665
|
+
userIds,
|
|
666
|
+
statuses,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Find user IDs that have an active connected account for a toolkit.
|
|
672
|
+
*/
|
|
673
|
+
async findActiveUserIdsForToolkit(toolkit) {
|
|
674
|
+
const normalizedToolkit = normalizeToolkitSlug(toolkit);
|
|
675
|
+
if (!this.isToolkitAllowed(normalizedToolkit))
|
|
676
|
+
return [];
|
|
677
|
+
const accounts = await this.listConnectedAccounts({
|
|
678
|
+
toolkits: [normalizedToolkit],
|
|
679
|
+
statuses: ["ACTIVE"],
|
|
680
|
+
});
|
|
681
|
+
const userIds = new Set();
|
|
682
|
+
for (const account of accounts) {
|
|
683
|
+
if (account.userId)
|
|
684
|
+
userIds.add(account.userId);
|
|
685
|
+
}
|
|
686
|
+
return Array.from(userIds).sort();
|
|
687
|
+
}
|
|
688
|
+
async listConnectedAccountsRaw(options) {
|
|
689
|
+
const accounts = [];
|
|
690
|
+
let cursor;
|
|
691
|
+
const seenCursors = new Set();
|
|
692
|
+
do {
|
|
693
|
+
const response = await this.client.client.connectedAccounts.list({
|
|
694
|
+
...(options?.toolkits && options.toolkits.length > 0 ? { toolkit_slugs: options.toolkits } : {}),
|
|
695
|
+
...(options?.userIds && options.userIds.length > 0 ? { user_ids: options.userIds } : {}),
|
|
696
|
+
...(options?.statuses && options.statuses.length > 0 ? { statuses: options.statuses } : {}),
|
|
697
|
+
limit: 100,
|
|
698
|
+
...(cursor ? { cursor } : {}),
|
|
699
|
+
});
|
|
700
|
+
const items = (Array.isArray(response)
|
|
701
|
+
? response
|
|
702
|
+
: response?.items || []);
|
|
703
|
+
for (const item of items) {
|
|
704
|
+
const toolkitSlug = normalizeToolkitSlug((item.toolkit?.slug || "").toString());
|
|
705
|
+
if (!toolkitSlug)
|
|
706
|
+
continue;
|
|
707
|
+
if (!this.isToolkitAllowed(toolkitSlug))
|
|
708
|
+
continue;
|
|
709
|
+
accounts.push({
|
|
710
|
+
id: String(item.id || ""),
|
|
711
|
+
toolkit: toolkitSlug,
|
|
712
|
+
userId: typeof item.user_id === "string" ? item.user_id : undefined,
|
|
713
|
+
status: typeof item.status === "string" ? item.status : undefined,
|
|
714
|
+
authConfigId: typeof item.auth_config?.id === "string"
|
|
715
|
+
? item.auth_config.id
|
|
716
|
+
: undefined,
|
|
717
|
+
isDisabled: typeof item.is_disabled === "boolean" ? item.is_disabled : undefined,
|
|
718
|
+
createdAt: typeof item.created_at === "string" ? item.created_at : undefined,
|
|
719
|
+
updatedAt: typeof item.updated_at === "string" ? item.updated_at : undefined,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
cursor = Array.isArray(response)
|
|
723
|
+
? null
|
|
724
|
+
: (response?.next_cursor ?? null);
|
|
725
|
+
if (!cursor)
|
|
726
|
+
break;
|
|
727
|
+
if (seenCursors.has(cursor))
|
|
728
|
+
break;
|
|
729
|
+
seenCursors.add(cursor);
|
|
730
|
+
} while (true);
|
|
731
|
+
return accounts;
|
|
732
|
+
}
|
|
733
|
+
async listConnectedAccountsFallback(options) {
|
|
734
|
+
const accounts = [];
|
|
735
|
+
let cursor;
|
|
736
|
+
const seenCursors = new Set();
|
|
737
|
+
do {
|
|
738
|
+
const response = await this.client.connectedAccounts.list({
|
|
739
|
+
...(options?.toolkits && options.toolkits.length > 0 ? { toolkitSlugs: options.toolkits } : {}),
|
|
740
|
+
...(options?.userIds && options.userIds.length > 0 ? { userIds: options.userIds } : {}),
|
|
741
|
+
...(options?.statuses && options.statuses.length > 0 ? { statuses: options.statuses } : {}),
|
|
742
|
+
limit: 100,
|
|
743
|
+
...(cursor ? { cursor } : {}),
|
|
744
|
+
});
|
|
745
|
+
const items = (Array.isArray(response)
|
|
746
|
+
? response
|
|
747
|
+
: response?.items || []);
|
|
748
|
+
for (const item of items) {
|
|
749
|
+
const toolkitSlug = normalizeToolkitSlug((item.toolkit?.slug || "").toString());
|
|
750
|
+
if (!toolkitSlug)
|
|
751
|
+
continue;
|
|
752
|
+
if (!this.isToolkitAllowed(toolkitSlug))
|
|
753
|
+
continue;
|
|
754
|
+
accounts.push({
|
|
755
|
+
id: String(item.id || ""),
|
|
756
|
+
toolkit: toolkitSlug,
|
|
757
|
+
status: typeof item.status === "string" ? item.status : undefined,
|
|
758
|
+
authConfigId: typeof item.authConfig?.id === "string"
|
|
759
|
+
? item.authConfig.id
|
|
760
|
+
: undefined,
|
|
761
|
+
isDisabled: typeof item.isDisabled === "boolean" ? item.isDisabled : undefined,
|
|
762
|
+
createdAt: typeof item.createdAt === "string" ? item.createdAt : undefined,
|
|
763
|
+
updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : undefined,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
cursor = Array.isArray(response)
|
|
767
|
+
? null
|
|
768
|
+
: (response?.nextCursor ?? null);
|
|
769
|
+
if (!cursor)
|
|
770
|
+
break;
|
|
771
|
+
if (seenCursors.has(cursor))
|
|
772
|
+
break;
|
|
773
|
+
seenCursors.add(cursor);
|
|
774
|
+
} while (true);
|
|
775
|
+
return accounts;
|
|
776
|
+
}
|
|
196
777
|
/**
|
|
197
778
|
* Create an auth connection for a toolkit using session.authorize()
|
|
198
779
|
*/
|
|
199
780
|
async createConnection(toolkit, userId) {
|
|
200
781
|
const uid = this.getUserId(userId);
|
|
201
|
-
|
|
202
|
-
|
|
782
|
+
const toolkitSlug = normalizeToolkitSlug(toolkit);
|
|
783
|
+
if (!toolkitSlug) {
|
|
784
|
+
return { error: "Toolkit is required" };
|
|
785
|
+
}
|
|
786
|
+
if (!this.isToolkitAllowed(toolkitSlug)) {
|
|
787
|
+
return { error: `Toolkit '${toolkitSlug}' is not allowed by plugin configuration` };
|
|
203
788
|
}
|
|
204
789
|
try {
|
|
205
790
|
const session = await this.getSession(uid);
|
|
206
|
-
const result = await session.authorize(
|
|
791
|
+
const result = await session.authorize(toolkitSlug);
|
|
207
792
|
return { authUrl: result.redirectUrl || result.url || "" };
|
|
208
793
|
}
|
|
209
794
|
catch (err) {
|
|
@@ -219,11 +804,29 @@ export class ComposioClient {
|
|
|
219
804
|
const uid = this.getUserId(userId);
|
|
220
805
|
try {
|
|
221
806
|
const session = await this.getSession(uid);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
807
|
+
const seen = new Set();
|
|
808
|
+
let nextCursor;
|
|
809
|
+
const seenCursors = new Set();
|
|
810
|
+
do {
|
|
811
|
+
const response = await session.toolkits({
|
|
812
|
+
nextCursor,
|
|
813
|
+
limit: 100,
|
|
814
|
+
});
|
|
815
|
+
const allToolkits = response.items || [];
|
|
816
|
+
for (const tk of allToolkits) {
|
|
817
|
+
const slug = normalizeToolkitSlug(tk.slug);
|
|
818
|
+
if (!this.isToolkitAllowed(slug))
|
|
819
|
+
continue;
|
|
820
|
+
seen.add(slug);
|
|
821
|
+
}
|
|
822
|
+
nextCursor = response.nextCursor;
|
|
823
|
+
if (!nextCursor)
|
|
824
|
+
break;
|
|
825
|
+
if (seenCursors.has(nextCursor))
|
|
826
|
+
break;
|
|
827
|
+
seenCursors.add(nextCursor);
|
|
828
|
+
} while (true);
|
|
829
|
+
return Array.from(seen);
|
|
227
830
|
}
|
|
228
831
|
catch (err) {
|
|
229
832
|
const errObj = err;
|
|
@@ -239,18 +842,36 @@ export class ComposioClient {
|
|
|
239
842
|
*/
|
|
240
843
|
async disconnectToolkit(toolkit, userId) {
|
|
241
844
|
const uid = this.getUserId(userId);
|
|
845
|
+
const toolkitSlug = normalizeToolkitSlug(toolkit);
|
|
846
|
+
if (!toolkitSlug) {
|
|
847
|
+
return { success: false, error: "Toolkit is required" };
|
|
848
|
+
}
|
|
849
|
+
if (this.config.readOnlyMode) {
|
|
850
|
+
return {
|
|
851
|
+
success: false,
|
|
852
|
+
error: "Disconnect is blocked by readOnlyMode.",
|
|
853
|
+
};
|
|
854
|
+
}
|
|
242
855
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
:
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
return { success: false, error: `No connection found for toolkit '${
|
|
856
|
+
const activeAccounts = await this.listConnectedAccounts({
|
|
857
|
+
toolkits: [toolkitSlug],
|
|
858
|
+
userIds: [uid],
|
|
859
|
+
statuses: ["ACTIVE"],
|
|
860
|
+
});
|
|
861
|
+
if (activeAccounts.length === 0) {
|
|
862
|
+
return { success: false, error: `No connection found for toolkit '${toolkitSlug}'` };
|
|
863
|
+
}
|
|
864
|
+
if (activeAccounts.length > 1) {
|
|
865
|
+
const ids = activeAccounts.map(a => a.id).join(", ");
|
|
866
|
+
return {
|
|
867
|
+
success: false,
|
|
868
|
+
error: `Multiple ACTIVE '${toolkitSlug}' accounts found for user_id '${uid}': ${ids}. ` +
|
|
869
|
+
"Use the dashboard to disconnect a specific account.",
|
|
870
|
+
};
|
|
250
871
|
}
|
|
251
|
-
await this.client.connectedAccounts.delete(
|
|
872
|
+
await this.client.connectedAccounts.delete(activeAccounts[0].id);
|
|
252
873
|
// Clear session cache to refresh connection status
|
|
253
|
-
this.
|
|
874
|
+
this.clearUserSessionCache(uid);
|
|
254
875
|
return { success: true };
|
|
255
876
|
}
|
|
256
877
|
catch (err) {
|