@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/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.
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
openclaw composio
|
|
51
|
-
openclaw composio
|
|
52
|
-
openclaw composio
|
|
53
|
-
openclaw composio
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
268
|
+
const composioClient = requireClient();
|
|
269
|
+
if (!composioClient)
|
|
36
270
|
return;
|
|
37
|
-
}
|
|
38
271
|
try {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
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
|
-
|
|
66
|
-
|
|
345
|
+
const composioClient = requireClient();
|
|
346
|
+
if (!composioClient)
|
|
67
347
|
return;
|
|
68
|
-
}
|
|
69
348
|
try {
|
|
70
|
-
|
|
71
|
-
const
|
|
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(
|
|
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
|
-
|
|
107
|
-
|
|
388
|
+
const composioClient = requireClient();
|
|
389
|
+
if (!composioClient)
|
|
108
390
|
return;
|
|
109
|
-
}
|
|
110
391
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
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 ${
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
*/
|