@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.
@@ -29,9 +29,19 @@ vi.mock("@composio/core", () => ({
29
29
  data: { results: [{ tool_slug: "GMAIL_FETCH_EMAILS", index: 0, response: { successful: true, data: { messages: [] } } }] },
30
30
  }),
31
31
  },
32
+ connectedAccounts: {
33
+ list: vi.fn().mockResolvedValue({ items: [], next_cursor: null }),
34
+ },
35
+ },
36
+ tools: {
37
+ execute: vi.fn().mockResolvedValue({
38
+ successful: true,
39
+ data: { direct: true },
40
+ }),
32
41
  },
33
42
  connectedAccounts: {
34
43
  list: vi.fn().mockResolvedValue({ items: [] }),
44
+ get: vi.fn().mockResolvedValue({ toolkit: { slug: "gmail" }, status: "ACTIVE" }),
35
45
  delete: vi.fn().mockResolvedValue({}),
36
46
  },
37
47
  })),
@@ -43,6 +53,11 @@ function makeClient(overrides) {
43
53
  ...overrides,
44
54
  });
45
55
  }
56
+ async function getLatestComposioInstance() {
57
+ const { Composio } = await import("@composio/core");
58
+ const mockResults = Composio.mock.results;
59
+ return mockResults[mockResults.length - 1].value;
60
+ }
46
61
  describe("config parsing", () => {
47
62
  it("reads apiKey from config object", () => {
48
63
  const config = parseComposioConfig({ config: { apiKey: "from-config" } });
@@ -62,6 +77,19 @@ describe("config parsing", () => {
62
77
  const config = parseComposioConfig({});
63
78
  expect(config.enabled).toBe(true);
64
79
  });
80
+ it("reads defaultUserId and toolkit filters from nested config object", () => {
81
+ const config = parseComposioConfig({
82
+ config: {
83
+ apiKey: "from-config",
84
+ defaultUserId: "app-user-123",
85
+ allowedToolkits: ["gmail", "sentry"],
86
+ blockedToolkits: ["github"],
87
+ },
88
+ });
89
+ expect(config.defaultUserId).toBe("app-user-123");
90
+ expect(config.allowedToolkits).toEqual(["gmail", "sentry"]);
91
+ expect(config.blockedToolkits).toEqual(["github"]);
92
+ });
65
93
  });
66
94
  describe("toolkit filtering", () => {
67
95
  it("allows all toolkits when no filter set", async () => {
@@ -99,6 +127,16 @@ describe("connection status", () => {
99
127
  const statuses = await client.getConnectionStatus(["sentry"]);
100
128
  expect(statuses[0].connected).toBe(false);
101
129
  });
130
+ it("reports toolkit as connected when active connected account exists", async () => {
131
+ const client = makeClient();
132
+ const instance = await getLatestComposioInstance();
133
+ instance.connectedAccounts.list.mockResolvedValueOnce({
134
+ items: [{ toolkit: { slug: "affinity" }, status: "ACTIVE" }],
135
+ nextCursor: null,
136
+ });
137
+ const statuses = await client.getConnectionStatus(["affinity"]);
138
+ expect(statuses[0].connected).toBe(true);
139
+ });
102
140
  it("returns only connected toolkits when no filter", async () => {
103
141
  const client = makeClient();
104
142
  const statuses = await client.getConnectionStatus();
@@ -119,6 +157,123 @@ describe("execute tool", () => {
119
157
  expect(result.success).toBe(false);
120
158
  expect(result.error).toContain("not allowed");
121
159
  });
160
+ it("pins execution to explicit connected_account_id", async () => {
161
+ const client = makeClient();
162
+ const instance = await getLatestComposioInstance();
163
+ instance.connectedAccounts.get.mockResolvedValueOnce({
164
+ toolkit: { slug: "gmail" },
165
+ status: "ACTIVE",
166
+ });
167
+ const result = await client.executeTool("GMAIL_FETCH_EMAILS", {}, "default", "ca_explicit");
168
+ expect(result.success).toBe(true);
169
+ expect(instance.toolRouter.create).toHaveBeenCalledWith("default", {
170
+ connectedAccounts: { gmail: "ca_explicit" },
171
+ });
172
+ });
173
+ it("auto-pins execution when one active account exists", async () => {
174
+ const client = makeClient();
175
+ const instance = await getLatestComposioInstance();
176
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
177
+ items: [
178
+ { id: "ca_single", user_id: "default", status: "ACTIVE", toolkit: { slug: "gmail" } },
179
+ ],
180
+ next_cursor: null,
181
+ });
182
+ const result = await client.executeTool("GMAIL_FETCH_EMAILS", {}, "default");
183
+ expect(result.success).toBe(true);
184
+ expect(instance.toolRouter.create).toHaveBeenCalledWith("default", {
185
+ connectedAccounts: { gmail: "ca_single" },
186
+ });
187
+ });
188
+ it("fails with clear error when multiple active accounts exist and none selected", async () => {
189
+ const client = makeClient();
190
+ const instance = await getLatestComposioInstance();
191
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
192
+ items: [
193
+ { id: "ca_1", user_id: "default", status: "ACTIVE", toolkit: { slug: "gmail" } },
194
+ { id: "ca_2", user_id: "default", status: "ACTIVE", toolkit: { slug: "gmail" } },
195
+ ],
196
+ next_cursor: null,
197
+ });
198
+ const result = await client.executeTool("GMAIL_FETCH_EMAILS", {}, "default");
199
+ expect(result.success).toBe(false);
200
+ expect(result.error).toContain("Multiple ACTIVE 'gmail' accounts");
201
+ expect(result.error).toContain("ca_1");
202
+ expect(result.error).toContain("ca_2");
203
+ });
204
+ it("falls back to direct execute when meta-tool resolves entity as default for non-default user", async () => {
205
+ const client = makeClient();
206
+ const instance = await getLatestComposioInstance();
207
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
208
+ items: [
209
+ { id: "ca_sentry", user_id: "pg-user", status: "ACTIVE", toolkit: { slug: "sentry" } },
210
+ ],
211
+ next_cursor: null,
212
+ });
213
+ instance.client.tools.execute.mockResolvedValueOnce({
214
+ successful: false,
215
+ error: "1 out of 1 tools failed",
216
+ data: {
217
+ results: [{ error: "Error: No connected account found for entity ID default for toolkit sentry" }],
218
+ },
219
+ });
220
+ instance.tools.execute.mockResolvedValueOnce({
221
+ successful: true,
222
+ data: { ok: true },
223
+ });
224
+ const result = await client.executeTool("SENTRY_GET_ORGANIZATION_DETAILS", {}, "pg-user");
225
+ expect(result.success).toBe(true);
226
+ expect(result.data).toEqual({ ok: true });
227
+ expect(instance.tools.execute).toHaveBeenCalledWith("SENTRY_GET_ORGANIZATION_DETAILS", {
228
+ userId: "pg-user",
229
+ connectedAccountId: "ca_sentry",
230
+ arguments: {},
231
+ dangerouslySkipVersionCheck: true,
232
+ });
233
+ });
234
+ it("retries once with server-hinted identifier value", async () => {
235
+ const client = makeClient();
236
+ const instance = await getLatestComposioInstance();
237
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
238
+ items: [
239
+ { id: "ca_posthog", user_id: "pg-user", status: "ACTIVE", toolkit: { slug: "posthog" } },
240
+ ],
241
+ next_cursor: null,
242
+ });
243
+ instance.client.tools.execute.mockResolvedValueOnce({
244
+ successful: true,
245
+ data: {
246
+ results: [
247
+ {
248
+ tool_slug: "POSTHOG_RETRIEVE_USER_PROFILE_AND_TEAM_DETAILS",
249
+ index: 0,
250
+ response: {
251
+ successful: false,
252
+ error: JSON.stringify({
253
+ type: "authentication_error",
254
+ code: "permission_denied",
255
+ detail: "As a non-staff user you're only allowed to access the `@me` user instance.",
256
+ attr: null,
257
+ }),
258
+ },
259
+ },
260
+ ],
261
+ },
262
+ });
263
+ instance.tools.execute.mockResolvedValueOnce({
264
+ successful: true,
265
+ data: { ok: true, retried: true },
266
+ });
267
+ const result = await client.executeTool("POSTHOG_RETRIEVE_USER_PROFILE_AND_TEAM_DETAILS", { uuid: "some-other-uuid" }, "pg-user");
268
+ expect(result.success).toBe(true);
269
+ expect(result.data).toEqual({ ok: true, retried: true });
270
+ expect(instance.tools.execute).toHaveBeenCalledWith("POSTHOG_RETRIEVE_USER_PROFILE_AND_TEAM_DETAILS", {
271
+ userId: "pg-user",
272
+ connectedAccountId: "ca_posthog",
273
+ arguments: { uuid: "@me" },
274
+ dangerouslySkipVersionCheck: true,
275
+ });
276
+ });
122
277
  });
123
278
  describe("create connection", () => {
124
279
  it("returns auth URL", async () => {
@@ -135,6 +290,111 @@ describe("create connection", () => {
135
290
  expect("error" in result).toBe(true);
136
291
  });
137
292
  });
293
+ describe("disconnect toolkit", () => {
294
+ it("disconnects single active account", async () => {
295
+ const client = makeClient();
296
+ const instance = await getLatestComposioInstance();
297
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
298
+ items: [
299
+ { id: "ca_gmail", user_id: "default", status: "ACTIVE", toolkit: { slug: "gmail" } },
300
+ ],
301
+ next_cursor: null,
302
+ });
303
+ const result = await client.disconnectToolkit("gmail", "default");
304
+ expect(result.success).toBe(true);
305
+ expect(instance.connectedAccounts.delete).toHaveBeenCalledWith({ connectedAccountId: "ca_gmail" });
306
+ });
307
+ it("fails safely when multiple active accounts exist", async () => {
308
+ const client = makeClient();
309
+ const instance = await getLatestComposioInstance();
310
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
311
+ items: [
312
+ { id: "ca_1", user_id: "default", status: "ACTIVE", toolkit: { slug: "gmail" } },
313
+ { id: "ca_2", user_id: "default", status: "ACTIVE", toolkit: { slug: "gmail" } },
314
+ ],
315
+ next_cursor: null,
316
+ });
317
+ const result = await client.disconnectToolkit("gmail", "default");
318
+ expect(result.success).toBe(false);
319
+ expect(result.error).toContain("Multiple ACTIVE 'gmail' accounts");
320
+ expect(instance.connectedAccounts.delete).not.toHaveBeenCalled();
321
+ });
322
+ });
323
+ describe("connected accounts discovery", () => {
324
+ it("lists connected accounts with user IDs from raw API", async () => {
325
+ const client = makeClient();
326
+ const instance = await getLatestComposioInstance();
327
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
328
+ items: [
329
+ {
330
+ id: "ca_1",
331
+ user_id: "user-a",
332
+ status: "ACTIVE",
333
+ toolkit: { slug: "sentry" },
334
+ auth_config: { id: "ac_1" },
335
+ },
336
+ ],
337
+ next_cursor: null,
338
+ });
339
+ const accounts = await client.listConnectedAccounts({ toolkits: ["sentry"], statuses: ["ACTIVE"] });
340
+ expect(instance.client.connectedAccounts.list).toHaveBeenCalledWith({
341
+ toolkit_slugs: ["sentry"],
342
+ statuses: ["ACTIVE"],
343
+ limit: 100,
344
+ });
345
+ expect(accounts).toEqual([
346
+ {
347
+ id: "ca_1",
348
+ toolkit: "sentry",
349
+ userId: "user-a",
350
+ status: "ACTIVE",
351
+ authConfigId: "ac_1",
352
+ isDisabled: undefined,
353
+ createdAt: undefined,
354
+ updatedAt: undefined,
355
+ },
356
+ ]);
357
+ });
358
+ it("falls back to SDK-normalized account list when raw API errors", async () => {
359
+ const client = makeClient();
360
+ const instance = await getLatestComposioInstance();
361
+ instance.client.connectedAccounts.list.mockRejectedValueOnce(new Error("raw unavailable"));
362
+ instance.connectedAccounts.list.mockResolvedValueOnce({
363
+ items: [
364
+ {
365
+ id: "ca_2",
366
+ status: "ACTIVE",
367
+ toolkit: { slug: "gmail" },
368
+ authConfig: { id: "ac_2" },
369
+ isDisabled: false,
370
+ },
371
+ ],
372
+ nextCursor: null,
373
+ });
374
+ const accounts = await client.listConnectedAccounts({ toolkits: ["gmail"], statuses: ["ACTIVE"] });
375
+ expect(accounts[0]).toMatchObject({
376
+ id: "ca_2",
377
+ toolkit: "gmail",
378
+ status: "ACTIVE",
379
+ authConfigId: "ac_2",
380
+ isDisabled: false,
381
+ });
382
+ });
383
+ it("finds active user IDs for toolkit", async () => {
384
+ const client = makeClient();
385
+ const instance = await getLatestComposioInstance();
386
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
387
+ items: [
388
+ { id: "ca_1", user_id: "default", status: "ACTIVE", toolkit: { slug: "sentry" } },
389
+ { id: "ca_2", user_id: "user-b", status: "ACTIVE", toolkit: { slug: "sentry" } },
390
+ { id: "ca_3", user_id: "default", status: "ACTIVE", toolkit: { slug: "sentry" } },
391
+ ],
392
+ next_cursor: null,
393
+ });
394
+ const userIds = await client.findActiveUserIdsForToolkit("sentry");
395
+ expect(userIds).toEqual(["default", "user-b"]);
396
+ });
397
+ });
138
398
  describe("session caching", () => {
139
399
  it("reuses session for same user", async () => {
140
400
  const client = makeClient();
@@ -192,35 +452,55 @@ describe("connections tool", () => {
192
452
  }
193
453
  it("list action passes user_id to client", async () => {
194
454
  const tool = makeConnectionsTool();
195
- const result = await tool.execute("test", { action: "list", user_id: "custom-user" });
196
- const details = result.details;
197
- expect(details).toHaveProperty("action", "list");
198
- expect(details.toolkits).toBeInstanceOf(Array);
455
+ await tool.execute("test", { action: "list", user_id: "custom-user" });
456
+ const instance = await getLatestComposioInstance();
457
+ expect(instance.toolRouter.create).toHaveBeenCalledWith("custom-user", undefined);
199
458
  });
200
- it("status probes API-key toolkit and flips to connected on success", async () => {
459
+ it("status uses active connected accounts as fallback", async () => {
201
460
  const tool = makeConnectionsTool();
461
+ const instance = await getLatestComposioInstance();
462
+ instance.connectedAccounts.list.mockResolvedValueOnce({
463
+ items: [{ toolkit: { slug: "affinity" }, status: "ACTIVE" }],
464
+ nextCursor: null,
465
+ });
202
466
  const result = await tool.execute("test", { action: "status", toolkit: "affinity" });
203
467
  const details = result.details;
204
468
  const conn = details.connections.find((c) => c.toolkit === "affinity");
205
469
  expect(conn.connected).toBe(true);
470
+ expect(instance.client.tools.execute).not.toHaveBeenCalledWith("AFFINITY_GET_METADATA_ON_ALL_LISTS", expect.anything());
206
471
  });
207
- it("status does not probe toolkits without a defined probe", async () => {
472
+ it("status keeps disconnected when no active account exists", async () => {
208
473
  const tool = makeConnectionsTool();
209
474
  const result = await tool.execute("test", { action: "status", toolkit: "sentry" });
210
475
  const details = result.details;
211
476
  const conn = details.connections.find((c) => c.toolkit === "sentry");
212
477
  expect(conn.connected).toBe(false);
213
478
  });
214
- it("status keeps disconnected when probe fails", async () => {
479
+ it("accounts action returns connected accounts", async () => {
215
480
  const tool = makeConnectionsTool();
216
- // Get the latest Composio instance (the one this tool's client is using)
217
- const { Composio } = await import("@composio/core");
218
- const mockResults = Composio.mock.results;
219
- const instance = mockResults[mockResults.length - 1].value;
220
- instance.client.tools.execute.mockRejectedValueOnce(new Error("probe failed"));
221
- const result = await tool.execute("test", { action: "status", toolkit: "affinity" });
481
+ const instance = await getLatestComposioInstance();
482
+ instance.client.connectedAccounts.list.mockResolvedValueOnce({
483
+ items: [
484
+ {
485
+ id: "ca_1",
486
+ user_id: "user-a",
487
+ status: "ACTIVE",
488
+ toolkit: { slug: "sentry" },
489
+ auth_config: { id: "ac_1" },
490
+ },
491
+ ],
492
+ next_cursor: null,
493
+ });
494
+ const result = await tool.execute("test", { action: "accounts", toolkit: "sentry" });
222
495
  const details = result.details;
223
- const conn = details.connections.find((c) => c.toolkit === "affinity");
224
- expect(conn.connected).toBe(false);
496
+ expect(details.action).toBe("accounts");
497
+ expect(details.count).toBe(1);
498
+ expect(details.accounts[0]).toMatchObject({
499
+ id: "ca_1",
500
+ toolkit: "sentry",
501
+ user_id: "user-a",
502
+ status: "ACTIVE",
503
+ auth_config_id: "ac_1",
504
+ });
225
505
  });
226
506
  });
package/dist/config.js CHANGED
@@ -16,15 +16,28 @@ export function parseComposioConfig(value) {
16
16
  const raw = value && typeof value === "object" && !Array.isArray(value)
17
17
  ? value
18
18
  : {};
19
- // Allow API key from config.apiKey, top-level apiKey, or environment
19
+ // Support both plugin entry shape ({ enabled, config: {...} }) and flat shape.
20
20
  const configObj = raw.config;
21
+ const enabled = (typeof raw.enabled === "boolean" ? raw.enabled : undefined) ??
22
+ (typeof configObj?.enabled === "boolean" ? configObj.enabled : undefined) ??
23
+ true;
24
+ const defaultUserId = (typeof raw.defaultUserId === "string" ? raw.defaultUserId : undefined) ??
25
+ (typeof configObj?.defaultUserId === "string" ? configObj.defaultUserId : undefined);
26
+ const allowedToolkits = (Array.isArray(raw.allowedToolkits) ? raw.allowedToolkits : undefined) ??
27
+ (Array.isArray(configObj?.allowedToolkits) ? configObj.allowedToolkits : undefined);
28
+ const blockedToolkits = (Array.isArray(raw.blockedToolkits) ? raw.blockedToolkits : undefined) ??
29
+ (Array.isArray(configObj?.blockedToolkits) ? configObj.blockedToolkits : undefined);
30
+ // Allow API key from config.apiKey, top-level apiKey, or environment.
21
31
  const apiKey = (typeof configObj?.apiKey === "string" && configObj.apiKey.trim()) ||
22
32
  (typeof raw.apiKey === "string" && raw.apiKey.trim()) ||
23
33
  process.env.COMPOSIO_API_KEY ||
24
34
  "";
25
35
  return ComposioConfigSchema.parse({
26
- ...raw,
36
+ enabled,
27
37
  apiKey,
38
+ defaultUserId,
39
+ allowedToolkits,
40
+ blockedToolkits,
28
41
  });
29
42
  }
30
43
  /**
@@ -4,10 +4,11 @@ import type { ComposioConfig } from "../types.js";
4
4
  * Tool parameters for composio_manage_connections
5
5
  */
6
6
  export declare const ComposioManageConnectionsToolSchema: import("@sinclair/typebox").TObject<{
7
- action: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"status">, import("@sinclair/typebox").TLiteral<"create">, import("@sinclair/typebox").TLiteral<"list">]>;
7
+ action: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"status">, import("@sinclair/typebox").TLiteral<"create">, import("@sinclair/typebox").TLiteral<"list">, import("@sinclair/typebox").TLiteral<"accounts">]>;
8
8
  toolkit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
9
9
  toolkits: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
10
10
  user_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ statuses: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
11
12
  }>;
12
13
  /**
13
14
  * Create the composio_manage_connections tool
@@ -17,10 +18,11 @@ export declare function createComposioConnectionsTool(client: ComposioClient, _c
17
18
  label: string;
18
19
  description: string;
19
20
  parameters: import("@sinclair/typebox").TObject<{
20
- action: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"status">, import("@sinclair/typebox").TLiteral<"create">, import("@sinclair/typebox").TLiteral<"list">]>;
21
+ action: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"status">, import("@sinclair/typebox").TLiteral<"create">, import("@sinclair/typebox").TLiteral<"list">, import("@sinclair/typebox").TLiteral<"accounts">]>;
21
22
  toolkit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
22
23
  toolkits: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
23
24
  user_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
25
+ statuses: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
24
26
  }>;
25
27
  execute(_toolCallId: string, params: Record<string, unknown>): Promise<{
26
28
  content: {
@@ -32,6 +34,22 @@ export declare function createComposioConnectionsTool(client: ComposioClient, _c
32
34
  count: number;
33
35
  toolkits: string[];
34
36
  };
37
+ } | {
38
+ content: {
39
+ type: string;
40
+ text: string;
41
+ }[];
42
+ details: {
43
+ action: string;
44
+ count: number;
45
+ accounts: {
46
+ id: string;
47
+ toolkit: string;
48
+ user_id: string | undefined;
49
+ status: string | undefined;
50
+ auth_config_id: string | undefined;
51
+ }[];
52
+ };
35
53
  } | {
36
54
  content: {
37
55
  type: string;
@@ -1,16 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- const CONNECTION_PROBES = {
3
- affinity: {
4
- toolSlug: "AFFINITY_GET_METADATA_ON_ALL_LISTS",
5
- args: { limit: 1 },
6
- },
7
- };
8
2
  /**
9
3
  * Tool parameters for composio_manage_connections
10
4
  */
11
5
  export const ComposioManageConnectionsToolSchema = Type.Object({
12
- action: Type.Union([Type.Literal("status"), Type.Literal("create"), Type.Literal("list")], {
13
- description: "Action to perform: 'status' to check connections, 'create' to initiate auth, 'list' to list toolkits",
6
+ action: Type.Union([Type.Literal("status"), Type.Literal("create"), Type.Literal("list"), Type.Literal("accounts")], {
7
+ description: "Action to perform: 'status' to check connections, 'create' to initiate auth, 'list' to list toolkits, 'accounts' to inspect connected accounts",
14
8
  }),
15
9
  toolkit: Type.Optional(Type.String({
16
10
  description: "Toolkit name for 'status' or 'create' actions (e.g., 'github', 'gmail')",
@@ -21,6 +15,9 @@ export const ComposioManageConnectionsToolSchema = Type.Object({
21
15
  user_id: Type.Optional(Type.String({
22
16
  description: "User ID for session scoping (uses default if not provided)",
23
17
  })),
18
+ statuses: Type.Optional(Type.Array(Type.String(), {
19
+ description: "Optional connection statuses filter for 'accounts' (e.g., ['ACTIVE'])",
20
+ })),
24
21
  });
25
22
  /**
26
23
  * Create the composio_manage_connections tool
@@ -30,7 +27,8 @@ export function createComposioConnectionsTool(client, _config) {
30
27
  name: "composio_manage_connections",
31
28
  label: "Composio Manage Connections",
32
29
  description: "Manage Composio toolkit connections. Use action='status' to check if a toolkit is connected, " +
33
- "action='create' to generate an auth URL when disconnected, or action='list' to see available toolkits. " +
30
+ "action='create' to generate an auth URL when disconnected, action='list' to see available toolkits, " +
31
+ "or action='accounts' to inspect connected accounts across user IDs. " +
34
32
  "Check connection status before executing tools with composio_execute_tool.",
35
33
  parameters: ComposioManageConnectionsToolSchema,
36
34
  async execute(_toolCallId, params) {
@@ -50,6 +48,38 @@ export function createComposioConnectionsTool(client, _config) {
50
48
  details: response,
51
49
  };
52
50
  }
51
+ case "accounts": {
52
+ let toolkits;
53
+ if (typeof params.toolkit === "string" && params.toolkit.trim()) {
54
+ toolkits = [params.toolkit.trim()];
55
+ }
56
+ else if (Array.isArray(params.toolkits)) {
57
+ toolkits = params.toolkits.filter((t) => typeof t === "string" && t.trim() !== "");
58
+ }
59
+ const statuses = Array.isArray(params.statuses)
60
+ ? params.statuses.filter((s) => typeof s === "string" && s.trim() !== "")
61
+ : ["ACTIVE"];
62
+ const accounts = await client.listConnectedAccounts({
63
+ toolkits,
64
+ userIds: userId ? [userId] : undefined,
65
+ statuses,
66
+ });
67
+ const response = {
68
+ action: "accounts",
69
+ count: accounts.length,
70
+ accounts: accounts.map((a) => ({
71
+ id: a.id,
72
+ toolkit: a.toolkit,
73
+ user_id: a.userId,
74
+ status: a.status,
75
+ auth_config_id: a.authConfigId,
76
+ })),
77
+ };
78
+ return {
79
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
80
+ details: response,
81
+ };
82
+ }
53
83
  case "create": {
54
84
  const toolkit = String(params.toolkit || "").trim();
55
85
  if (!toolkit) {
@@ -89,25 +119,6 @@ export function createComposioConnectionsTool(client, _config) {
89
119
  toolkitsToCheck = params.toolkits.filter((t) => typeof t === "string" && t.trim() !== "");
90
120
  }
91
121
  const statuses = await client.getConnectionStatus(toolkitsToCheck, userId);
92
- // Fallback probe for API-key style integrations where
93
- // connection.isActive can be false despite successful tool execution
94
- if (toolkitsToCheck && toolkitsToCheck.length > 0) {
95
- for (const status of statuses) {
96
- if (status.connected)
97
- continue;
98
- const probe = CONNECTION_PROBES[String(status.toolkit || "").toLowerCase()];
99
- if (!probe)
100
- continue;
101
- try {
102
- const probeResult = await client.executeTool(probe.toolSlug, probe.args, userId);
103
- if (probeResult?.success)
104
- status.connected = true;
105
- }
106
- catch {
107
- // keep false if probe fails
108
- }
109
- }
110
- }
111
122
  const response = {
112
123
  action: "status",
113
124
  count: statuses.length,
@@ -7,6 +7,7 @@ export declare const ComposioExecuteToolSchema: import("@sinclair/typebox").TObj
7
7
  tool_slug: import("@sinclair/typebox").TString;
8
8
  arguments: import("@sinclair/typebox").TUnknown;
9
9
  user_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
10
+ connected_account_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
10
11
  }>;
11
12
  /**
12
13
  * Create the composio_execute_tool tool
@@ -19,6 +20,7 @@ export declare function createComposioExecuteTool(client: ComposioClient, _confi
19
20
  tool_slug: import("@sinclair/typebox").TString;
20
21
  arguments: import("@sinclair/typebox").TUnknown;
21
22
  user_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
23
+ connected_account_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
22
24
  }>;
23
25
  execute(_toolCallId: string, params: Record<string, unknown>): Promise<{
24
26
  content: {
@@ -12,6 +12,9 @@ export const ComposioExecuteToolSchema = Type.Object({
12
12
  user_id: Type.Optional(Type.String({
13
13
  description: "User ID for session scoping (uses default if not provided)",
14
14
  })),
15
+ connected_account_id: Type.Optional(Type.String({
16
+ description: "Optional connected account ID to pin execution to a specific account when multiple are connected",
17
+ })),
15
18
  });
16
19
  /**
17
20
  * Create the composio_execute_tool tool
@@ -43,8 +46,9 @@ export function createComposioExecuteTool(client, _config) {
43
46
  ? rawArgs
44
47
  : {};
45
48
  const userId = typeof params.user_id === "string" ? params.user_id : undefined;
49
+ const connectedAccountId = typeof params.connected_account_id === "string" ? params.connected_account_id : undefined;
46
50
  try {
47
- const result = await client.executeTool(toolSlug, args, userId);
51
+ const result = await client.executeTool(toolSlug, args, userId, connectedAccountId);
48
52
  const response = {
49
53
  tool_slug: toolSlug,
50
54
  success: result.success,
package/dist/types.d.ts CHANGED
@@ -26,3 +26,13 @@ export interface ConnectionStatus {
26
26
  userId?: string;
27
27
  authUrl?: string;
28
28
  }
29
+ export interface ConnectedAccountSummary {
30
+ id: string;
31
+ toolkit: string;
32
+ userId?: string;
33
+ status?: string;
34
+ authConfigId?: string;
35
+ isDisabled?: boolean;
36
+ createdAt?: string;
37
+ updatedAt?: string;
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customclaw/composio",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "description": "Composio Tool Router plugin for OpenClaw — access 1000+ third-party integrations",
6
6
  "main": "dist/index.js",