@customclaw/composio 0.0.1

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/dist/client.js ADDED
@@ -0,0 +1,386 @@
1
+ import { Composio } from "@composio/core";
2
+ /**
3
+ * Composio client wrapper using Tool Router pattern
4
+ */
5
+ export class ComposioClient {
6
+ client;
7
+ config;
8
+ sessionCache = new Map();
9
+ constructor(config) {
10
+ if (!config.apiKey) {
11
+ throw new Error("Composio API key required. Set COMPOSIO_API_KEY env var or plugins.composio.apiKey in config.");
12
+ }
13
+ this.config = config;
14
+ this.client = new Composio({ apiKey: config.apiKey });
15
+ }
16
+ /**
17
+ * Get the user ID to use for API calls
18
+ */
19
+ getUserId(overrideUserId) {
20
+ return overrideUserId || this.config.defaultUserId || "default";
21
+ }
22
+ /**
23
+ * Get or create a Tool Router session for a user
24
+ */
25
+ async getSession(userId) {
26
+ if (this.sessionCache.has(userId)) {
27
+ return this.sessionCache.get(userId);
28
+ }
29
+ const session = await this.client.toolRouter.create(userId);
30
+ this.sessionCache.set(userId, session);
31
+ return session;
32
+ }
33
+ /**
34
+ * Check if a toolkit is allowed based on config
35
+ */
36
+ isToolkitAllowed(toolkit) {
37
+ const { allowedToolkits, blockedToolkits } = this.config;
38
+ if (blockedToolkits?.includes(toolkit.toLowerCase())) {
39
+ return false;
40
+ }
41
+ if (allowedToolkits && allowedToolkits.length > 0) {
42
+ return allowedToolkits.includes(toolkit.toLowerCase());
43
+ }
44
+ return true;
45
+ }
46
+ /**
47
+ * Execute a Tool Router meta-tool
48
+ */
49
+ async executeMetaTool(toolName, args) {
50
+ const response = await this.client.client.tools.execute(toolName, {
51
+ arguments: args,
52
+ });
53
+ return response;
54
+ }
55
+ /**
56
+ * Search for tools matching a query using COMPOSIO_SEARCH_TOOLS
57
+ */
58
+ async searchTools(query, options) {
59
+ const userId = this.getUserId(options?.userId);
60
+ const session = await this.getSession(userId);
61
+ try {
62
+ const response = await this.executeMetaTool("COMPOSIO_SEARCH_TOOLS", {
63
+ queries: [{ use_case: query }],
64
+ session: { id: session.sessionId },
65
+ });
66
+ if (!response.successful || !response.data) {
67
+ throw new Error(response.error || "Search failed");
68
+ }
69
+ const data = response.data;
70
+ const searchResults = data.results || [];
71
+ const toolSchemas = data.tool_schemas || {};
72
+ const results = [];
73
+ const seenSlugs = new Set();
74
+ for (const result of searchResults) {
75
+ const allSlugs = [
76
+ ...(result.primary_tool_slugs || []),
77
+ ...(result.related_tool_slugs || []),
78
+ ];
79
+ for (const slug of allSlugs) {
80
+ if (seenSlugs.has(slug))
81
+ continue;
82
+ seenSlugs.add(slug);
83
+ const schema = toolSchemas[slug];
84
+ const toolkit = schema?.toolkit || slug.split("_")[0] || "";
85
+ if (!this.isToolkitAllowed(toolkit))
86
+ continue;
87
+ if (options?.toolkits && options.toolkits.length > 0) {
88
+ if (!options.toolkits.some(t => t.toLowerCase() === toolkit.toLowerCase())) {
89
+ continue;
90
+ }
91
+ }
92
+ results.push({
93
+ name: slug,
94
+ slug: slug,
95
+ description: schema?.description || "",
96
+ toolkit: toolkit,
97
+ parameters: schema?.input_schema || {},
98
+ });
99
+ if (options?.limit && results.length >= options.limit)
100
+ break;
101
+ }
102
+ if (options?.limit && results.length >= options.limit)
103
+ break;
104
+ }
105
+ return results;
106
+ }
107
+ catch (err) {
108
+ throw new Error(`Failed to search tools: ${err instanceof Error ? err.message : String(err)}`);
109
+ }
110
+ }
111
+ /**
112
+ * Execute a single tool using COMPOSIO_MULTI_EXECUTE_TOOL
113
+ */
114
+ async executeTool(toolSlug, args, userId) {
115
+ const uid = this.getUserId(userId);
116
+ const session = await this.getSession(uid);
117
+ const toolkit = toolSlug.split("_")[0]?.toLowerCase() || "";
118
+ if (!this.isToolkitAllowed(toolkit)) {
119
+ return {
120
+ success: false,
121
+ error: `Toolkit '${toolkit}' is not allowed by plugin configuration`,
122
+ };
123
+ }
124
+ try {
125
+ const response = await this.executeMetaTool("COMPOSIO_MULTI_EXECUTE_TOOL", {
126
+ tools: [{ tool_slug: toolSlug, arguments: args }],
127
+ session: { id: session.sessionId },
128
+ sync_response_to_workbench: false,
129
+ });
130
+ if (!response.successful) {
131
+ return { success: false, error: response.error || "Execution failed" };
132
+ }
133
+ const results = response.data?.results || [];
134
+ const result = results[0];
135
+ if (!result) {
136
+ return { success: false, error: "No result returned" };
137
+ }
138
+ // Response data is nested under result.response
139
+ const toolResponse = result.response;
140
+ return {
141
+ success: toolResponse.successful,
142
+ data: toolResponse.data,
143
+ error: toolResponse.error ?? undefined,
144
+ };
145
+ }
146
+ catch (err) {
147
+ return {
148
+ success: false,
149
+ error: err instanceof Error ? err.message : String(err),
150
+ };
151
+ }
152
+ }
153
+ /**
154
+ * Execute multiple tools in parallel using COMPOSIO_MULTI_EXECUTE_TOOL
155
+ */
156
+ async multiExecute(executions, userId) {
157
+ const uid = this.getUserId(userId);
158
+ const session = await this.getSession(uid);
159
+ // Filter out blocked toolkits and limit to 50
160
+ const allowedExecutions = executions
161
+ .filter(exec => {
162
+ const toolkit = exec.tool_slug.split("_")[0]?.toLowerCase() || "";
163
+ return this.isToolkitAllowed(toolkit);
164
+ })
165
+ .slice(0, 50);
166
+ if (allowedExecutions.length === 0) {
167
+ return { results: [] };
168
+ }
169
+ try {
170
+ const response = await this.executeMetaTool("COMPOSIO_MULTI_EXECUTE_TOOL", {
171
+ tools: allowedExecutions.map(exec => ({
172
+ tool_slug: exec.tool_slug,
173
+ arguments: exec.arguments,
174
+ })),
175
+ session: { id: session.sessionId },
176
+ sync_response_to_workbench: false,
177
+ });
178
+ if (!response.successful) {
179
+ return {
180
+ results: allowedExecutions.map(exec => ({
181
+ tool_slug: exec.tool_slug,
182
+ success: false,
183
+ error: response.error || "Execution failed",
184
+ })),
185
+ };
186
+ }
187
+ const apiResults = response.data?.results || [];
188
+ return {
189
+ results: apiResults.map(r => ({
190
+ tool_slug: r.tool_slug,
191
+ success: r.response.successful,
192
+ data: r.response.data,
193
+ error: r.response.error ?? undefined,
194
+ })),
195
+ };
196
+ }
197
+ catch (err) {
198
+ return {
199
+ results: allowedExecutions.map(exec => ({
200
+ tool_slug: exec.tool_slug,
201
+ success: false,
202
+ error: err instanceof Error ? err.message : String(err),
203
+ })),
204
+ };
205
+ }
206
+ }
207
+ /**
208
+ * Get connection status for toolkits using session.toolkits()
209
+ */
210
+ async getConnectionStatus(toolkits, userId) {
211
+ const uid = this.getUserId(userId);
212
+ const session = await this.getSession(uid);
213
+ try {
214
+ const response = await session.toolkits();
215
+ const allToolkits = response.items || [];
216
+ const statuses = [];
217
+ if (toolkits && toolkits.length > 0) {
218
+ // Check specific toolkits
219
+ for (const toolkit of toolkits) {
220
+ if (!this.isToolkitAllowed(toolkit))
221
+ continue;
222
+ const found = allToolkits.find(t => t.slug.toLowerCase() === toolkit.toLowerCase());
223
+ statuses.push({
224
+ toolkit,
225
+ connected: found?.connection?.isActive ?? false,
226
+ userId: uid,
227
+ });
228
+ }
229
+ }
230
+ else {
231
+ // Return all connected toolkits
232
+ for (const tk of allToolkits) {
233
+ if (!this.isToolkitAllowed(tk.slug))
234
+ continue;
235
+ if (!tk.connection?.isActive)
236
+ continue;
237
+ statuses.push({
238
+ toolkit: tk.slug,
239
+ connected: true,
240
+ userId: uid,
241
+ });
242
+ }
243
+ }
244
+ return statuses;
245
+ }
246
+ catch (err) {
247
+ throw new Error(`Failed to get connection status: ${err instanceof Error ? err.message : String(err)}`);
248
+ }
249
+ }
250
+ /**
251
+ * Create an auth connection for a toolkit using session.authorize()
252
+ */
253
+ async createConnection(toolkit, userId) {
254
+ const uid = this.getUserId(userId);
255
+ if (!this.isToolkitAllowed(toolkit)) {
256
+ return { error: `Toolkit '${toolkit}' is not allowed by plugin configuration` };
257
+ }
258
+ try {
259
+ const session = await this.getSession(uid);
260
+ const result = await session.authorize(toolkit);
261
+ return { authUrl: result.redirectUrl || result.url || "" };
262
+ }
263
+ catch (err) {
264
+ return {
265
+ error: err instanceof Error ? err.message : String(err),
266
+ };
267
+ }
268
+ }
269
+ /**
270
+ * List available toolkits
271
+ */
272
+ async listToolkits(userId) {
273
+ const uid = this.getUserId(userId);
274
+ try {
275
+ const session = await this.getSession(uid);
276
+ const response = await session.toolkits();
277
+ const allToolkits = response.items || [];
278
+ return allToolkits
279
+ .map(tk => tk.slug)
280
+ .filter(slug => this.isToolkitAllowed(slug));
281
+ }
282
+ catch (err) {
283
+ const errObj = err;
284
+ if (errObj?.status === 401) {
285
+ throw new Error("Invalid Composio API key. Get a valid key from platform.composio.dev/settings");
286
+ }
287
+ const apiMsg = errObj?.error?.error?.message;
288
+ throw new Error(`Failed to list toolkits: ${apiMsg || (err instanceof Error ? err.message : String(err))}`);
289
+ }
290
+ }
291
+ /**
292
+ * Disconnect a toolkit
293
+ */
294
+ async disconnectToolkit(toolkit, userId) {
295
+ const uid = this.getUserId(userId);
296
+ try {
297
+ const response = await this.client.connectedAccounts.list({ userId: uid });
298
+ const connections = (Array.isArray(response)
299
+ ? response
300
+ : response?.items || []);
301
+ const conn = connections.find(c => c.toolkit?.slug?.toLowerCase() === toolkit.toLowerCase());
302
+ if (!conn) {
303
+ return { success: false, error: `No connection found for toolkit '${toolkit}'` };
304
+ }
305
+ await this.client.connectedAccounts.delete({ connectedAccountId: conn.id });
306
+ // Clear session cache to refresh connection status
307
+ this.sessionCache.delete(uid);
308
+ return { success: true };
309
+ }
310
+ catch (err) {
311
+ return {
312
+ success: false,
313
+ error: err instanceof Error ? err.message : String(err),
314
+ };
315
+ }
316
+ }
317
+ /**
318
+ * Get the assistive prompt for the agent
319
+ */
320
+ async getAssistivePrompt(userId) {
321
+ const uid = this.getUserId(userId);
322
+ const session = await this.getSession(uid);
323
+ return session.experimental.assistivePrompt;
324
+ }
325
+ /**
326
+ * Execute Python code in the remote workbench using COMPOSIO_REMOTE_WORKBENCH
327
+ */
328
+ async executeWorkbench(code, options) {
329
+ const uid = this.getUserId(options?.userId);
330
+ const session = await this.getSession(uid);
331
+ try {
332
+ const response = await this.executeMetaTool("COMPOSIO_REMOTE_WORKBENCH", {
333
+ code_to_execute: code,
334
+ session_id: session.sessionId,
335
+ ...(options?.thought ? { thought: options.thought } : {}),
336
+ ...(options?.currentStep ? { current_step: options.currentStep } : {}),
337
+ ...(options?.currentStepMetric ? { current_step_metric: options.currentStepMetric } : {}),
338
+ });
339
+ if (!response.successful) {
340
+ return { success: false, error: response.error || "Workbench execution failed" };
341
+ }
342
+ return {
343
+ success: true,
344
+ output: response.data,
345
+ };
346
+ }
347
+ catch (err) {
348
+ return {
349
+ success: false,
350
+ error: err instanceof Error ? err.message : String(err),
351
+ };
352
+ }
353
+ }
354
+ /**
355
+ * Execute bash commands in the remote sandbox using COMPOSIO_REMOTE_BASH_TOOL
356
+ */
357
+ async executeBash(command, userId) {
358
+ const uid = this.getUserId(userId);
359
+ const session = await this.getSession(uid);
360
+ try {
361
+ const response = await this.executeMetaTool("COMPOSIO_REMOTE_BASH_TOOL", {
362
+ command,
363
+ session_id: session.sessionId,
364
+ });
365
+ if (!response.successful) {
366
+ return { success: false, error: response.error || "Bash execution failed" };
367
+ }
368
+ return {
369
+ success: true,
370
+ output: response.data,
371
+ };
372
+ }
373
+ catch (err) {
374
+ return {
375
+ success: false,
376
+ error: err instanceof Error ? err.message : String(err),
377
+ };
378
+ }
379
+ }
380
+ }
381
+ /**
382
+ * Create a Composio client instance
383
+ */
384
+ export function createComposioClient(config) {
385
+ return new ComposioClient(config);
386
+ }
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import type { ComposioConfig } from "./types.js";
3
+ /**
4
+ * Zod schema for Composio plugin configuration
5
+ */
6
+ export declare const ComposioConfigSchema: z.ZodObject<{
7
+ enabled: z.ZodDefault<z.ZodBoolean>;
8
+ apiKey: z.ZodOptional<z.ZodString>;
9
+ defaultUserId: z.ZodOptional<z.ZodString>;
10
+ allowedToolkits: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ blockedToolkits: z.ZodOptional<z.ZodArray<z.ZodString>>;
12
+ }, z.core.$strip>;
13
+ /**
14
+ * Parse and validate plugin config with environment fallbacks
15
+ */
16
+ export declare function parseComposioConfig(value: unknown): ComposioConfig;
17
+ /**
18
+ * UI hints for configuration fields
19
+ */
20
+ export declare const composioConfigUiHints: {
21
+ enabled: {
22
+ label: string;
23
+ help: string;
24
+ };
25
+ apiKey: {
26
+ label: string;
27
+ help: string;
28
+ sensitive: boolean;
29
+ };
30
+ defaultUserId: {
31
+ label: string;
32
+ help: string;
33
+ };
34
+ allowedToolkits: {
35
+ label: string;
36
+ help: string;
37
+ advanced: boolean;
38
+ };
39
+ blockedToolkits: {
40
+ label: string;
41
+ help: string;
42
+ advanced: boolean;
43
+ };
44
+ };
45
+ /**
46
+ * Plugin config schema object for openclaw
47
+ */
48
+ export declare const composioPluginConfigSchema: {
49
+ parse: typeof parseComposioConfig;
50
+ uiHints: {
51
+ enabled: {
52
+ label: string;
53
+ help: string;
54
+ };
55
+ apiKey: {
56
+ label: string;
57
+ help: string;
58
+ sensitive: boolean;
59
+ };
60
+ defaultUserId: {
61
+ label: string;
62
+ help: string;
63
+ };
64
+ allowedToolkits: {
65
+ label: string;
66
+ help: string;
67
+ advanced: boolean;
68
+ };
69
+ blockedToolkits: {
70
+ label: string;
71
+ help: string;
72
+ advanced: boolean;
73
+ };
74
+ };
75
+ };
package/dist/config.js ADDED
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Zod schema for Composio plugin configuration
4
+ */
5
+ export const ComposioConfigSchema = z.object({
6
+ enabled: z.boolean().default(true),
7
+ apiKey: z.string().optional(),
8
+ defaultUserId: z.string().optional(),
9
+ allowedToolkits: z.array(z.string()).optional(),
10
+ blockedToolkits: z.array(z.string()).optional(),
11
+ });
12
+ /**
13
+ * Parse and validate plugin config with environment fallbacks
14
+ */
15
+ export function parseComposioConfig(value) {
16
+ const raw = value && typeof value === "object" && !Array.isArray(value)
17
+ ? value
18
+ : {};
19
+ // Allow API key from config.apiKey, top-level apiKey, or environment
20
+ const configObj = raw.config;
21
+ const apiKey = (typeof configObj?.apiKey === "string" && configObj.apiKey.trim()) ||
22
+ (typeof raw.apiKey === "string" && raw.apiKey.trim()) ||
23
+ process.env.COMPOSIO_API_KEY ||
24
+ "";
25
+ return ComposioConfigSchema.parse({
26
+ ...raw,
27
+ apiKey,
28
+ });
29
+ }
30
+ /**
31
+ * UI hints for configuration fields
32
+ */
33
+ export const composioConfigUiHints = {
34
+ enabled: {
35
+ label: "Enable Composio",
36
+ help: "Enable or disable the Composio Tool Router integration",
37
+ },
38
+ apiKey: {
39
+ label: "API Key",
40
+ help: "Composio API key from platform.composio.dev/settings",
41
+ sensitive: true,
42
+ },
43
+ defaultUserId: {
44
+ label: "Default User ID",
45
+ help: "Default user ID for session scoping (optional)",
46
+ },
47
+ allowedToolkits: {
48
+ label: "Allowed Toolkits",
49
+ help: "Restrict to specific toolkits (e.g., github, gmail)",
50
+ advanced: true,
51
+ },
52
+ blockedToolkits: {
53
+ label: "Blocked Toolkits",
54
+ help: "Block specific toolkits from being used",
55
+ advanced: true,
56
+ },
57
+ };
58
+ /**
59
+ * Plugin config schema object for openclaw
60
+ */
61
+ export const composioPluginConfigSchema = {
62
+ parse: parseComposioConfig,
63
+ uiHints: composioConfigUiHints,
64
+ };
@@ -0,0 +1,56 @@
1
+ import { parseComposioConfig } from "./config.js";
2
+ /**
3
+ * Composio Tool Router Plugin for OpenClaw
4
+ *
5
+ * Provides access to 1000+ third-party tools through Composio's unified interface.
6
+ * Tools include: Gmail, Slack, GitHub, Notion, Linear, Jira, and many more.
7
+ *
8
+ * Configuration (in openclaw config):
9
+ * ```json
10
+ * {
11
+ * "plugins": {
12
+ * "composio": {
13
+ * "enabled": true,
14
+ * "apiKey": "your-composio-api-key"
15
+ * }
16
+ * }
17
+ * }
18
+ * ```
19
+ *
20
+ * Or set COMPOSIO_API_KEY environment variable.
21
+ */
22
+ declare const composioPlugin: {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ configSchema: {
27
+ parse: typeof parseComposioConfig;
28
+ uiHints: {
29
+ enabled: {
30
+ label: string;
31
+ help: string;
32
+ };
33
+ apiKey: {
34
+ label: string;
35
+ help: string;
36
+ sensitive: boolean;
37
+ };
38
+ defaultUserId: {
39
+ label: string;
40
+ help: string;
41
+ };
42
+ allowedToolkits: {
43
+ label: string;
44
+ help: string;
45
+ advanced: boolean;
46
+ };
47
+ blockedToolkits: {
48
+ label: string;
49
+ help: string;
50
+ advanced: boolean;
51
+ };
52
+ };
53
+ };
54
+ register(api: any): void;
55
+ };
56
+ export default composioPlugin;