@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 CHANGED
@@ -12,7 +12,13 @@ openclaw plugins install @customclaw/composio
12
12
 
13
13
  1. Get an API key from [platform.composio.dev/settings](https://platform.composio.dev/settings)
14
14
 
15
- 2. Add to `~/.openclaw/openclaw.json`:
15
+ 2. Run the guided setup:
16
+
17
+ ```bash
18
+ openclaw composio setup
19
+ ```
20
+
21
+ Or add manually to `~/.openclaw/openclaw.json`:
16
22
 
17
23
  ```json
18
24
  {
@@ -22,6 +28,7 @@ openclaw plugins install @customclaw/composio
22
28
  "enabled": true,
23
29
  "config": {
24
30
  "apiKey": "your-api-key",
31
+ "defaultUserId": "my-app-user-123",
25
32
  "allowedToolkits": ["gmail", "sentry"]
26
33
  }
27
34
  }
@@ -30,27 +37,41 @@ openclaw plugins install @customclaw/composio
30
37
  }
31
38
  ```
32
39
 
33
- Or set `COMPOSIO_API_KEY` as an environment variable.
40
+ You can also set `COMPOSIO_API_KEY` as an environment variable.
34
41
 
35
42
  3. Restart the gateway.
36
43
 
44
+ If you upgraded from an older config shape where keys like `defaultUserId` or `allowedToolkits`
45
+ were placed directly under `plugins.entries.composio`, rerun:
46
+
47
+ ```bash
48
+ openclaw composio setup
49
+ ```
50
+
51
+ Legacy mixed entry-level config keys are now rejected at runtime.
52
+
37
53
  ## What it does
38
54
 
39
- The plugin gives your agent two tools:
55
+ The plugin gives your agent three tools:
40
56
 
57
+ - `composio_search_tools` — discovers relevant Composio actions from natural-language intent
41
58
  - `composio_execute_tool` — runs a Composio action (e.g. `GMAIL_FETCH_EMAILS`, `SENTRY_LIST_ISSUES`)
42
59
  - `composio_manage_connections` — checks connection status and generates OAuth links when a toolkit isn't connected yet
43
60
 
61
+ `composio_execute_tool` also accepts optional `user_id` and `connected_account_id` fields for deterministic account selection when multiple accounts exist.
62
+
44
63
  The agent handles the rest. Ask it to "check my latest emails" and it will call the right tool, prompt you to connect Gmail if needed, and fetch the results.
45
64
 
46
65
  ## CLI
47
66
 
48
67
  ```bash
49
- openclaw composio list # list available toolkits
50
- openclaw composio status # check what's connected
51
- openclaw composio connect gmail # open OAuth link
52
- openclaw composio disconnect gmail # remove a connection
53
- openclaw composio search "send email" # find tool slugs
68
+ openclaw composio setup # interactive setup for ~/.openclaw/openclaw.json
69
+ openclaw composio list --user-id user-123 # list available toolkits for a user scope
70
+ openclaw composio status [toolkit] --user-id user-123 # check connection status in a user scope
71
+ openclaw composio accounts [toolkit] # inspect connected accounts (id/user_id/status)
72
+ openclaw composio connect gmail --user-id user-123 # open OAuth link for a specific user scope
73
+ openclaw composio disconnect gmail --user-id user-123 # remove a connection in that user scope
74
+ openclaw composio search "send email" --user-id user-123
54
75
  ```
55
76
 
56
77
  ## Config options
@@ -58,8 +79,25 @@ openclaw composio search "send email" # find tool slugs
58
79
  | Key | Description |
59
80
  |-----|-------------|
60
81
  | `apiKey` | Composio API key (required) |
82
+ | `defaultUserId` | Default Composio `user_id` scope when `--user-id` is not provided |
61
83
  | `allowedToolkits` | Only allow these toolkits (e.g. `["gmail", "sentry"]`) |
62
84
  | `blockedToolkits` | Block specific toolkits |
85
+ | `readOnlyMode` | Blocks likely-destructive actions by token matching (delete/update/create/send/etc.); use `allowedToolSlugs` to override safe exceptions |
86
+ | `sessionTags` | Optional Tool Router tags (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) |
87
+ | `allowedToolSlugs` | Optional explicit allowlist of UPPERCASE tool slugs |
88
+ | `blockedToolSlugs` | Explicit denylist of UPPERCASE tool slugs |
89
+
90
+ ## User ID Scope (Important)
91
+
92
+ Composio connections are scoped by `user_id`. If a toolkit is connected in the dashboard
93
+ under one user ID but OpenClaw checks another (for example `default`), status and execution
94
+ may look disconnected.
95
+
96
+ Tips:
97
+
98
+ - Set `defaultUserId` in plugin config for your app's primary identity.
99
+ - Use `--user-id` explicitly when checking status/connect/disconnect.
100
+ - Use `openclaw composio accounts <toolkit>` to discover which `user_id` owns active connections.
63
101
 
64
102
  ## Updating
65
103
 
@@ -78,6 +116,24 @@ npm run build
78
116
  npm test
79
117
  ```
80
118
 
119
+ ### Live Integration Tests (Optional)
120
+
121
+ ```bash
122
+ COMPOSIO_LIVE_TEST=1 \
123
+ COMPOSIO_API_KEY=your_key \
124
+ npm run test:live
125
+ ```
126
+
127
+ Optional env vars for execution/disconnect paths:
128
+
129
+ - `COMPOSIO_LIVE_USER_ID`
130
+ - `COMPOSIO_LIVE_TOOLKIT` (default: `gmail`)
131
+ - `COMPOSIO_LIVE_TOOL_SLUG`
132
+ - `COMPOSIO_LIVE_TOOL_ARGS` (JSON)
133
+ - `COMPOSIO_LIVE_CONNECTED_ACCOUNT_ID`
134
+ - `COMPOSIO_LIVE_EXPECT_EXECUTE_SUCCESS=1`
135
+ - `COMPOSIO_LIVE_ALLOW_DISCONNECT=1` (destructive, opt-in)
136
+
81
137
  ## Acknowledgments
82
138
 
83
139
  Based on the Composio plugin from [openclaw-composio](https://github.com/ComposioHQ/openclaw-composio) by ComposioHQ. See [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES).
package/dist/cli.d.ts CHANGED
@@ -7,12 +7,12 @@ interface PluginLogger {
7
7
  }
8
8
  interface RegisterCliOptions {
9
9
  program: any;
10
- client: ComposioClient;
10
+ getClient?: () => ComposioClient;
11
11
  config: ComposioConfig;
12
12
  logger: PluginLogger;
13
13
  }
14
14
  /**
15
15
  * Register Composio CLI commands
16
16
  */
17
- export declare function registerComposioCli({ program, client, config, logger }: RegisterCliOptions): void;
17
+ export declare function registerComposioCli({ program, getClient, config, logger }: RegisterCliOptions): void;
18
18
  export {};
package/dist/cli.js CHANGED
@@ -1,19 +1,253 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { isRecord, normalizeToolkitList, normalizeToolkitSlug, stripLegacyFlatConfigKeys, } from "./utils.js";
7
+ const DEFAULT_OPENCLAW_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
8
+ const COMPOSIO_PLUGIN_ID = "composio";
9
+ function parseCsvToolkits(value) {
10
+ if (!value)
11
+ return undefined;
12
+ return normalizeToolkitList(value.split(","));
13
+ }
14
+ function parseBooleanLike(value) {
15
+ const raw = value.trim().toLowerCase();
16
+ if (!raw)
17
+ return undefined;
18
+ if (["1", "true", "yes", "y"].includes(raw))
19
+ return true;
20
+ if (["0", "false", "no", "n"].includes(raw))
21
+ return false;
22
+ return undefined;
23
+ }
24
+ function normalizePluginIdList(value) {
25
+ if (!Array.isArray(value))
26
+ return undefined;
27
+ const normalized = value
28
+ .filter((item) => typeof item === "string")
29
+ .map((item) => item.trim())
30
+ .filter(Boolean);
31
+ return Array.from(new Set(normalized));
32
+ }
33
+ async function readOpenClawConfig(configPath) {
34
+ try {
35
+ const raw = await readFile(configPath, "utf8");
36
+ const parsed = JSON.parse(raw);
37
+ return isRecord(parsed) ? parsed : {};
38
+ }
39
+ catch (err) {
40
+ const nodeErr = err;
41
+ if (nodeErr?.code === "ENOENT")
42
+ return {};
43
+ throw err;
44
+ }
45
+ }
1
46
  /**
2
47
  * Register Composio CLI commands
3
48
  */
4
- export function registerComposioCli({ program, client, config, logger }) {
49
+ export function registerComposioCli({ program, getClient, config, logger }) {
5
50
  const composio = program.command("composio").description("Manage Composio Tool Router connections");
51
+ const requireClient = () => {
52
+ if (!config.enabled) {
53
+ logger.error("Composio plugin is disabled");
54
+ return null;
55
+ }
56
+ if (!getClient) {
57
+ logger.error("Composio API key is not configured. Run 'openclaw composio setup' first.");
58
+ return null;
59
+ }
60
+ try {
61
+ return getClient();
62
+ }
63
+ catch (err) {
64
+ logger.error(`Failed to initialize Composio client: ${err instanceof Error ? err.message : String(err)}`);
65
+ return null;
66
+ }
67
+ };
68
+ // openclaw composio setup
69
+ composio
70
+ .command("setup")
71
+ .description("Create or update Composio config in ~/.openclaw/openclaw.json")
72
+ .option("-c, --config-path <path>", "OpenClaw config file path", DEFAULT_OPENCLAW_CONFIG_PATH)
73
+ .option("--api-key <apiKey>", "Composio API key")
74
+ .option("--default-user-id <userId>", "Default user ID for Composio user scoping")
75
+ .option("--allowed-toolkits <toolkits>", "Comma-separated allowed toolkit slugs")
76
+ .option("--blocked-toolkits <toolkits>", "Comma-separated blocked toolkit slugs")
77
+ .option("--read-only <enabled>", "Enable read-only mode (true/false)")
78
+ .option("-y, --yes", "Skip prompts and use defaults/provided values")
79
+ .action(async (options) => {
80
+ const configPath = path.resolve(options.configPath || DEFAULT_OPENCLAW_CONFIG_PATH);
81
+ try {
82
+ const openClawConfig = await readOpenClawConfig(configPath);
83
+ const plugins = isRecord(openClawConfig.plugins) ? { ...openClawConfig.plugins } : {};
84
+ let updatedPluginSystemEnabled = false;
85
+ let addedToAllowlist = false;
86
+ let removedFromDenylist = false;
87
+ if (plugins.enabled === false) {
88
+ plugins.enabled = true;
89
+ updatedPluginSystemEnabled = true;
90
+ }
91
+ const allow = normalizePluginIdList(plugins.allow);
92
+ if (allow && allow.length > 0) {
93
+ const hasExactComposio = allow.includes(COMPOSIO_PLUGIN_ID);
94
+ const normalizedAllow = allow.filter((id) => id.toLowerCase() !== COMPOSIO_PLUGIN_ID);
95
+ normalizedAllow.push(COMPOSIO_PLUGIN_ID);
96
+ plugins.allow = Array.from(new Set(normalizedAllow));
97
+ if (!hasExactComposio) {
98
+ addedToAllowlist = true;
99
+ }
100
+ }
101
+ const deny = normalizePluginIdList(plugins.deny);
102
+ if (deny && deny.length > 0) {
103
+ const filteredDeny = deny.filter((id) => id.toLowerCase() !== COMPOSIO_PLUGIN_ID);
104
+ if (filteredDeny.length !== deny.length) {
105
+ removedFromDenylist = true;
106
+ }
107
+ if (filteredDeny.length > 0) {
108
+ plugins.deny = filteredDeny;
109
+ }
110
+ else {
111
+ delete plugins.deny;
112
+ }
113
+ }
114
+ const entries = isRecord(plugins.entries) ? { ...plugins.entries } : {};
115
+ const existingComposioEntry = isRecord(entries.composio) ? { ...entries.composio } : {};
116
+ const existingComposioConfig = isRecord(existingComposioEntry.config)
117
+ ? { ...existingComposioEntry.config }
118
+ : {};
119
+ let apiKey = String(options.apiKey || "").trim() ||
120
+ String(existingComposioConfig.apiKey || "").trim() ||
121
+ String(config.apiKey || "").trim() ||
122
+ String(process.env.COMPOSIO_API_KEY || "").trim();
123
+ let defaultUserId = String(options.defaultUserId || "").trim() ||
124
+ String(existingComposioConfig.defaultUserId || "").trim() ||
125
+ String(config.defaultUserId || "").trim();
126
+ let allowedToolkits = parseCsvToolkits(options.allowedToolkits) ||
127
+ (Array.isArray(existingComposioConfig.allowedToolkits)
128
+ ? normalizeToolkitList(existingComposioConfig.allowedToolkits)
129
+ : normalizeToolkitList(config.allowedToolkits));
130
+ let blockedToolkits = parseCsvToolkits(options.blockedToolkits) ||
131
+ (Array.isArray(existingComposioConfig.blockedToolkits)
132
+ ? normalizeToolkitList(existingComposioConfig.blockedToolkits)
133
+ : normalizeToolkitList(config.blockedToolkits));
134
+ let readOnlyMode = typeof existingComposioConfig.readOnlyMode === "boolean"
135
+ ? existingComposioConfig.readOnlyMode
136
+ : Boolean(config.readOnlyMode);
137
+ if (options.readOnly !== undefined) {
138
+ const parsedReadOnly = parseBooleanLike(options.readOnly);
139
+ if (parsedReadOnly === undefined) {
140
+ logger.error("Invalid value for --read-only. Expected one of: true/false/yes/no/1/0.");
141
+ return;
142
+ }
143
+ readOnlyMode = parsedReadOnly;
144
+ }
145
+ if (!options.yes) {
146
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
147
+ try {
148
+ const apiKeyPrompt = await rl.question(`Composio API key${apiKey ? " [configured]" : ""}: `);
149
+ if (apiKeyPrompt.trim())
150
+ apiKey = apiKeyPrompt.trim();
151
+ const defaultUserPrompt = await rl.question(`Default user ID${defaultUserId ? ` [${defaultUserId}]` : " (optional)"}: `);
152
+ if (defaultUserPrompt.trim())
153
+ defaultUserId = defaultUserPrompt.trim();
154
+ const allowedDefault = allowedToolkits && allowedToolkits.length > 0
155
+ ? ` [${allowedToolkits.join(",")}]`
156
+ : " (optional)";
157
+ const allowedPrompt = await rl.question(`Allowed toolkits${allowedDefault}: `);
158
+ if (allowedPrompt.trim()) {
159
+ allowedToolkits = parseCsvToolkits(allowedPrompt) || [];
160
+ }
161
+ const blockedDefault = blockedToolkits && blockedToolkits.length > 0
162
+ ? ` [${blockedToolkits.join(",")}]`
163
+ : " (optional)";
164
+ const blockedPrompt = await rl.question(`Blocked toolkits${blockedDefault}: `);
165
+ if (blockedPrompt.trim()) {
166
+ blockedToolkits = parseCsvToolkits(blockedPrompt) || [];
167
+ }
168
+ const readOnlyPrompt = await rl.question(`Enable read-only safety mode? (y/N) [${readOnlyMode ? "Y" : "N"}]: `);
169
+ const parsedReadOnlyPrompt = parseBooleanLike(readOnlyPrompt);
170
+ if (parsedReadOnlyPrompt !== undefined) {
171
+ readOnlyMode = parsedReadOnlyPrompt;
172
+ }
173
+ else if (readOnlyPrompt.trim()) {
174
+ logger.warn("Invalid read-only input. Keeping existing readOnlyMode value.");
175
+ }
176
+ }
177
+ finally {
178
+ rl.close();
179
+ }
180
+ }
181
+ if (!apiKey) {
182
+ logger.error("Composio API key is required. Provide --api-key or set COMPOSIO_API_KEY.");
183
+ return;
184
+ }
185
+ const mergedComposioConfig = {
186
+ ...existingComposioConfig,
187
+ apiKey,
188
+ readOnlyMode,
189
+ };
190
+ if (defaultUserId) {
191
+ mergedComposioConfig.defaultUserId = defaultUserId;
192
+ }
193
+ else {
194
+ delete mergedComposioConfig.defaultUserId;
195
+ }
196
+ if (allowedToolkits && allowedToolkits.length > 0) {
197
+ mergedComposioConfig.allowedToolkits = allowedToolkits;
198
+ }
199
+ else {
200
+ delete mergedComposioConfig.allowedToolkits;
201
+ }
202
+ if (blockedToolkits && blockedToolkits.length > 0) {
203
+ mergedComposioConfig.blockedToolkits = blockedToolkits;
204
+ }
205
+ else {
206
+ delete mergedComposioConfig.blockedToolkits;
207
+ }
208
+ entries.composio = {
209
+ ...stripLegacyFlatConfigKeys(existingComposioEntry),
210
+ enabled: true,
211
+ config: mergedComposioConfig,
212
+ };
213
+ plugins.entries = entries;
214
+ openClawConfig.plugins = plugins;
215
+ await mkdir(path.dirname(configPath), { recursive: true });
216
+ await writeFile(configPath, `${JSON.stringify(openClawConfig, null, 2)}\n`, "utf8");
217
+ console.log("\nComposio setup saved.");
218
+ console.log("─".repeat(40));
219
+ console.log(`Config: ${configPath}`);
220
+ console.log(`defaultUserId: ${defaultUserId || "default"}`);
221
+ console.log(`readOnlyMode: ${readOnlyMode ? "enabled" : "disabled"}`);
222
+ if (updatedPluginSystemEnabled) {
223
+ console.log("plugins.enabled: set to true");
224
+ }
225
+ if (addedToAllowlist) {
226
+ console.log("plugins.allow: added 'composio'");
227
+ }
228
+ if (removedFromDenylist) {
229
+ console.log("plugins.deny: removed 'composio'");
230
+ }
231
+ console.log("\nNext steps:");
232
+ console.log(" 1) openclaw gateway restart");
233
+ console.log(" 2) openclaw composio status --user-id <user-id>");
234
+ console.log();
235
+ }
236
+ catch (err) {
237
+ logger.error(`Failed to run setup: ${err instanceof Error ? err.message : String(err)}`);
238
+ }
239
+ });
6
240
  // openclaw composio list
7
241
  composio
8
242
  .command("list")
9
243
  .description("List available Composio toolkits")
10
- .action(async () => {
11
- if (!config.enabled) {
12
- logger.error("Composio plugin is disabled");
244
+ .option("-u, --user-id <userId>", "User ID for session scoping")
245
+ .action(async (options) => {
246
+ const composioClient = requireClient();
247
+ if (!composioClient)
13
248
  return;
14
- }
15
249
  try {
16
- const toolkits = await client.listToolkits();
250
+ const toolkits = await composioClient.listToolkits(options.userId);
17
251
  console.log("\nAvailable Composio Toolkits:");
18
252
  console.log("─".repeat(40));
19
253
  for (const toolkit of toolkits.sort()) {
@@ -31,13 +265,13 @@ export function registerComposioCli({ program, client, config, logger }) {
31
265
  .description("Check connection status for toolkits")
32
266
  .option("-u, --user-id <userId>", "User ID for session scoping")
33
267
  .action(async (toolkit, options) => {
34
- if (!config.enabled) {
35
- logger.error("Composio plugin is disabled");
268
+ const composioClient = requireClient();
269
+ if (!composioClient)
36
270
  return;
37
- }
38
271
  try {
39
- const toolkits = toolkit ? [toolkit] : undefined;
40
- const statuses = await client.getConnectionStatus(toolkits, options.userId);
272
+ const toolkitSlug = toolkit ? normalizeToolkitSlug(toolkit) : undefined;
273
+ const toolkits = toolkitSlug ? [toolkitSlug] : undefined;
274
+ const statuses = await composioClient.getConnectionStatus(toolkits, options.userId);
41
275
  console.log("\nComposio Connection Status:");
42
276
  console.log("─".repeat(40));
43
277
  if (statuses.length === 0) {
@@ -50,25 +284,73 @@ export function registerComposioCli({ program, client, config, logger }) {
50
284
  console.log(` ${icon} ${status.toolkit}: ${state}`);
51
285
  }
52
286
  }
287
+ if (toolkitSlug && statuses.length === 1 && !statuses[0]?.connected) {
288
+ const currentUserId = options.userId || config.defaultUserId || "default";
289
+ const activeUserIds = await composioClient.findActiveUserIdsForToolkit(toolkitSlug);
290
+ const otherUserIds = activeUserIds.filter((uid) => uid !== currentUserId);
291
+ if (otherUserIds.length > 0) {
292
+ console.log(`\n Hint: '${toolkitSlug}' is connected under other user_id(s): ${otherUserIds.join(", ")}`);
293
+ console.log(` Try: openclaw composio status ${toolkitSlug} --user-id <user-id>`);
294
+ console.log(` Discover accounts: openclaw composio accounts ${toolkitSlug}`);
295
+ }
296
+ }
53
297
  console.log();
54
298
  }
55
299
  catch (err) {
56
300
  logger.error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
57
301
  }
58
302
  });
303
+ // openclaw composio accounts [toolkit]
304
+ composio
305
+ .command("accounts [toolkit]")
306
+ .description("List connected accounts (account IDs, user IDs, and statuses)")
307
+ .option("-u, --user-id <userId>", "Filter by user ID")
308
+ .option("-s, --statuses <statuses>", "Comma-separated statuses (default: ACTIVE)", "ACTIVE")
309
+ .action(async (toolkit, options) => {
310
+ const composioClient = requireClient();
311
+ if (!composioClient)
312
+ return;
313
+ try {
314
+ const statuses = String(options.statuses || "")
315
+ .split(",")
316
+ .map(s => s.trim())
317
+ .filter(Boolean);
318
+ const accounts = await composioClient.listConnectedAccounts({
319
+ toolkits: toolkit ? [normalizeToolkitSlug(toolkit)] : undefined,
320
+ userIds: options.userId ? [options.userId] : undefined,
321
+ statuses: statuses.length > 0 ? statuses : ["ACTIVE"],
322
+ });
323
+ console.log("\nComposio Connected Accounts:");
324
+ console.log("─".repeat(80));
325
+ if (accounts.length === 0) {
326
+ console.log(" No connected accounts found");
327
+ console.log();
328
+ return;
329
+ }
330
+ for (const account of accounts) {
331
+ console.log(` ${account.id} | toolkit=${account.toolkit} | status=${account.status || "unknown"} | user_id=${account.userId || "hidden"}`);
332
+ }
333
+ console.log(`\nTotal: ${accounts.length} connected accounts\n`);
334
+ }
335
+ catch (err) {
336
+ logger.error(`Failed to list connected accounts: ${err instanceof Error ? err.message : String(err)}`);
337
+ }
338
+ });
59
339
  // openclaw composio connect <toolkit>
60
340
  composio
61
341
  .command("connect <toolkit>")
62
342
  .description("Connect to a Composio toolkit (opens auth URL)")
63
343
  .option("-u, --user-id <userId>", "User ID for session scoping")
64
344
  .action(async (toolkit, options) => {
65
- if (!config.enabled) {
66
- logger.error("Composio plugin is disabled");
345
+ const composioClient = requireClient();
346
+ if (!composioClient)
67
347
  return;
68
- }
69
348
  try {
70
- console.log(`\nInitiating connection to ${toolkit}...`);
71
- const result = await client.createConnection(toolkit, options.userId);
349
+ const toolkitSlug = normalizeToolkitSlug(toolkit);
350
+ const currentUserId = options.userId || config.defaultUserId || "default";
351
+ console.log(`\nInitiating connection to ${toolkitSlug}...`);
352
+ console.log(`Using user_id: ${currentUserId}`);
353
+ const result = await composioClient.createConnection(toolkitSlug, options.userId);
72
354
  if ("error" in result) {
73
355
  logger.error(`Failed to create connection: ${result.error}`);
74
356
  return;
@@ -77,7 +359,7 @@ export function registerComposioCli({ program, client, config, logger }) {
77
359
  console.log("─".repeat(40));
78
360
  console.log(result.authUrl);
79
361
  console.log("\nOpen this URL in your browser to authenticate.");
80
- console.log("After authentication, run 'openclaw composio status' to verify.\n");
362
+ console.log(`After authentication, run 'openclaw composio status ${toolkitSlug} --user-id ${currentUserId}' to verify.\n`);
81
363
  // Try to open URL in browser
82
364
  try {
83
365
  const { exec } = await import("node:child_process");
@@ -103,15 +385,15 @@ export function registerComposioCli({ program, client, config, logger }) {
103
385
  .description("Disconnect from a Composio toolkit")
104
386
  .option("-u, --user-id <userId>", "User ID for session scoping")
105
387
  .action(async (toolkit, options) => {
106
- if (!config.enabled) {
107
- logger.error("Composio plugin is disabled");
388
+ const composioClient = requireClient();
389
+ if (!composioClient)
108
390
  return;
109
- }
110
391
  try {
111
- console.log(`\nDisconnecting from ${toolkit}...`);
112
- const result = await client.disconnectToolkit(toolkit, options.userId);
392
+ const toolkitSlug = normalizeToolkitSlug(toolkit);
393
+ console.log(`\nDisconnecting from ${toolkitSlug}...`);
394
+ const result = await composioClient.disconnectToolkit(toolkitSlug, options.userId);
113
395
  if (result.success) {
114
- console.log(`Successfully disconnected from ${toolkit}\n`);
396
+ console.log(`Successfully disconnected from ${toolkitSlug}\n`);
115
397
  }
116
398
  else {
117
399
  logger.error(`Failed to disconnect: ${result.error}`);
@@ -129,14 +411,13 @@ export function registerComposioCli({ program, client, config, logger }) {
129
411
  .option("-l, --limit <limit>", "Maximum results", "10")
130
412
  .option("-u, --user-id <userId>", "User ID for session scoping")
131
413
  .action(async (query, options) => {
132
- if (!config.enabled) {
133
- logger.error("Composio plugin is disabled");
414
+ const composioClient = requireClient();
415
+ if (!composioClient)
134
416
  return;
135
- }
136
417
  try {
137
418
  const limit = parseInt(options.limit, 10) || 10;
138
- const toolkits = options.toolkit ? [options.toolkit] : undefined;
139
- const results = await client.searchTools(query, {
419
+ const toolkits = options.toolkit ? [normalizeToolkitSlug(options.toolkit)] : undefined;
420
+ const results = await composioClient.searchTools(query, {
140
421
  toolkits,
141
422
  limit,
142
423
  userId: options.userId,
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ComposioConfig, ToolSearchResult, ToolExecutionResult, ConnectionStatus } from "./types.js";
1
+ import type { ComposioConfig, ToolSearchResult, ToolExecutionResult, ConnectionStatus, ConnectedAccountSummary } from "./types.js";
2
2
  /**
3
3
  * Composio client wrapper using Tool Router pattern
4
4
  */
@@ -14,11 +14,19 @@ export declare class ComposioClient {
14
14
  /**
15
15
  * Get or create a Tool Router session for a user
16
16
  */
17
+ private makeSessionCacheKey;
18
+ private normalizeConnectedAccountsOverride;
19
+ private buildToolRouterBlockedToolsConfig;
20
+ private buildSessionConfig;
17
21
  private getSession;
22
+ private shouldRetrySessionWithoutToolkitFilters;
23
+ private clearUserSessionCache;
18
24
  /**
19
25
  * Check if a toolkit is allowed based on config
20
26
  */
21
27
  private isToolkitAllowed;
28
+ private isLikelyDestructiveToolSlug;
29
+ private getToolSlugRestrictionError;
22
30
  /**
23
31
  * Execute a Tool Router meta-tool
24
32
  */
@@ -34,11 +42,41 @@ export declare class ComposioClient {
34
42
  /**
35
43
  * Execute a single tool using COMPOSIO_MULTI_EXECUTE_TOOL
36
44
  */
37
- executeTool(toolSlug: string, args: Record<string, unknown>, userId?: string): Promise<ToolExecutionResult>;
45
+ executeTool(toolSlug: string, args: Record<string, unknown>, userId?: string, connectedAccountId?: string): Promise<ToolExecutionResult>;
46
+ private tryExecutionRecovery;
47
+ private tryDirectExecutionFallback;
48
+ private executeDirectTool;
49
+ private tryHintedIdentifierRetry;
50
+ private shouldFallbackToDirectExecution;
51
+ private shouldRetryFromServerHint;
52
+ private extractServerHintLiteral;
53
+ private buildRetryArgsFromHint;
54
+ private extractSingleMissingField;
55
+ private buildCombinedErrorText;
56
+ private extractNestedMetaError;
57
+ private resolveConnectedAccountForExecution;
38
58
  /**
39
59
  * Get connection status for toolkits using session.toolkits()
40
60
  */
41
61
  getConnectionStatus(toolkits?: string[], userId?: string): Promise<ConnectionStatus[]>;
62
+ private getToolkitStateMap;
63
+ private getActiveConnectedAccountToolkits;
64
+ private normalizeStatuses;
65
+ /**
66
+ * List connected accounts with optional filters.
67
+ * Uses raw API first to preserve user_id in responses, then falls back to SDK-normalized output.
68
+ */
69
+ listConnectedAccounts(options?: {
70
+ toolkits?: string[];
71
+ userIds?: string[];
72
+ statuses?: string[];
73
+ }): Promise<ConnectedAccountSummary[]>;
74
+ /**
75
+ * Find user IDs that have an active connected account for a toolkit.
76
+ */
77
+ findActiveUserIdsForToolkit(toolkit: string): Promise<string[]>;
78
+ private listConnectedAccountsRaw;
79
+ private listConnectedAccountsFallback;
42
80
  /**
43
81
  * Create an auth connection for a toolkit using session.authorize()
44
82
  */