support managed composio

This commit is contained in:
Ramnique Singh 2026-03-15 23:39:48 +05:30
parent d0a48d7f51
commit d2bb11f104
4 changed files with 78 additions and 136 deletions

View file

@ -1,7 +1,6 @@
import { z } from "zod";
import fs from "fs";
import path from "path";
import { Composio } from "@composio/core";
import { WorkDir } from "../config/config.js";
import {
ZAuthConfig,
@ -12,33 +11,36 @@ import {
ZCreateConnectedAccountResponse,
ZDeleteOperationResponse,
ZErrorResponse,
ZExecuteActionRequest,
ZExecuteActionResponse,
ZListResponse,
ZTool,
ZToolkit,
} from "./types.js";
import { isSignedIn } from "../account/account.js";
import { getAccessToken } from "../auth/tokens.js";
import { API_URL } from "../config/env.js";
const BASE_URL = 'https://backend.composio.dev/api/v3';
const COMPOSIO_BASE_URL = 'https://backend.composio.dev/api/v3';
const CONFIG_FILE = path.join(WorkDir, 'config', 'composio.json');
// Composio SDK client (lazily initialized)
let composioClient: Composio | null = null;
function getComposioClient(): Composio {
if (composioClient) {
return composioClient;
async function getBaseUrl(): Promise<string> {
if (await isSignedIn()) {
return `${API_URL}/v1/composio`;
}
return COMPOSIO_BASE_URL;
}
async function getAuthHeaders(): Promise<Record<string, string>> {
if (await isSignedIn()) {
const token = await getAccessToken();
return { 'Authorization': `Bearer ${token}` };
}
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('Composio API key not configured');
}
composioClient = new Composio({ apiKey });
return composioClient;
}
function resetComposioClient(): void {
composioClient = null;
return { 'x-api-key': apiKey };
}
/**
@ -91,13 +93,13 @@ export function setApiKey(apiKey: string): void {
const config = loadConfig();
config.apiKey = apiKey;
saveConfig(config);
resetComposioClient();
}
/**
* Check if Composio is configured
*/
export function isConfigured(): boolean {
export async function isConfigured(): Promise<boolean> {
if (await isSignedIn()) return true;
return !!getApiKey();
}
@ -106,23 +108,25 @@ export function isConfigured(): boolean {
*/
export async function composioApiCall<T extends z.ZodTypeAny>(
schema: T,
url: string,
path: string,
params: Record<string, string> = {},
options: RequestInit = {},
): Promise<z.infer<T>> {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('Composio API key not configured');
}
const authHeaders = await getAuthHeaders();
const baseURL = await getBaseUrl();
const url = new URL(path, baseURL);
console.log(`[Composio] ${options.method || 'GET'} ${url}`);
const startTime = Date.now();
try {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
...options,
headers: {
...options.headers,
"x-api-key": apiKey,
...authHeaders,
...(options.method === 'POST' ? { "Content-Type": "application/json" } : {}),
},
});
@ -174,47 +178,20 @@ export async function composioApiCall<T extends z.ZodTypeAny>(
* List available toolkits
*/
export async function listToolkits(cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
const url = new URL(`${BASE_URL}/toolkits`);
url.searchParams.set("sort_by", "usage");
const params: Record<string, string> = {
sort_by: "usage",
};
if (cursor) {
url.searchParams.set("cursor", cursor);
params.cursor = cursor;
}
return composioApiCall(ZListResponse(ZToolkit), url.toString());
return composioApiCall(ZListResponse(ZToolkit), "/toolkits", params);
}
/**
* Get a specific toolkit
*/
export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZToolkit>> {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('Composio API key not configured');
}
const url = `${BASE_URL}/toolkits/${toolkitSlug}`;
console.log(`[Composio] GET ${url}`);
const response = await fetch(url, {
headers: { "x-api-key": apiKey },
});
if (!response.ok) {
throw new Error(`Failed to fetch toolkit: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const no_auth = data.composio_managed_auth_schemes?.includes('NO_AUTH') ||
data.auth_config_details?.some((config: { mode: string }) => config.mode === 'NO_AUTH') ||
false;
return ZToolkit.parse({
...data,
no_auth,
meta: data.meta || { description: '', logo: '', tools_count: 0, triggers_count: 0 },
auth_schemes: data.auth_schemes || [],
composio_managed_auth_schemes: data.composio_managed_auth_schemes || [],
});
return composioApiCall(ZToolkit, `/toolkits/${toolkitSlug}`);
}
/**
@ -225,15 +202,16 @@ export async function listAuthConfigs(
cursor: string | null = null,
managedOnly: boolean = false
): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> {
const url = new URL(`${BASE_URL}/auth_configs`);
url.searchParams.set("toolkit_slug", toolkitSlug);
const params: Record<string, string> = {
toolkit_slug: toolkitSlug,
};
if (cursor) {
url.searchParams.set("cursor", cursor);
params.cursor = cursor;
}
if (managedOnly) {
url.searchParams.set("is_composio_managed", "true");
params.is_composio_managed = "true";
}
return composioApiCall(ZListResponse(ZAuthConfig), url.toString());
return composioApiCall(ZListResponse(ZAuthConfig), "/auth_configs", params);
}
/**
@ -242,8 +220,7 @@ export async function listAuthConfigs(
export async function createAuthConfig(
request: z.infer<typeof ZCreateAuthConfigRequest>
): Promise<z.infer<typeof ZCreateAuthConfigResponse>> {
const url = new URL(`${BASE_URL}/auth_configs`);
return composioApiCall(ZCreateAuthConfigResponse, url.toString(), {
return composioApiCall(ZCreateAuthConfigResponse, "/auth_configs", {}, {
method: 'POST',
body: JSON.stringify(request),
});
@ -253,8 +230,7 @@ export async function createAuthConfig(
* Delete an auth config
*/
export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
const url = new URL(`${BASE_URL}/auth_configs/${authConfigId}`);
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
return composioApiCall(ZDeleteOperationResponse, `/auth_configs/${authConfigId}`, {}, {
method: 'DELETE',
});
}
@ -265,8 +241,7 @@ export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<ty
export async function createConnectedAccount(
request: z.infer<typeof ZCreateConnectedAccountRequest>
): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
const url = new URL(`${BASE_URL}/connected_accounts`);
return composioApiCall(ZCreateConnectedAccountResponse, url.toString(), {
return composioApiCall(ZCreateConnectedAccountResponse, "/connected_accounts", {}, {
method: 'POST',
body: JSON.stringify(request),
});
@ -276,16 +251,14 @@ export async function createConnectedAccount(
* Get a connected account
*/
export async function getConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZConnectedAccount>> {
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
return composioApiCall(ZConnectedAccount, url.toString());
return composioApiCall(ZConnectedAccount, `/connected_accounts/${connectedAccountId}`);
}
/**
* Delete a connected account
*/
export async function deleteConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
return composioApiCall(ZDeleteOperationResponse, `/connected_accounts/${connectedAccountId}`, {}, {
method: 'DELETE',
});
}
@ -296,64 +269,26 @@ export async function deleteConnectedAccount(connectedAccountId: string): Promis
export async function listToolkitTools(
toolkitSlug: string,
searchQuery: string | null = null,
): Promise<{ items: Array<{ slug: string; name: string; description: string }> }> {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('Composio API key not configured');
}
const url = new URL(`${BASE_URL}/tools`);
url.searchParams.set('toolkit_slug', toolkitSlug);
url.searchParams.set('limit', '200');
if (searchQuery) {
url.searchParams.set('search', searchQuery);
}
console.log(`[Composio] Listing tools for toolkit: ${toolkitSlug}`);
const response = await fetch(url.toString(), {
headers: { "x-api-key": apiKey },
});
if (!response.ok) {
throw new Error(`Failed to list tools: ${response.status} ${response.statusText}`);
}
const data = await response.json() as { items?: Array<Record<string, unknown>> };
return {
items: (data.items || []).map((item) => ({
slug: String(item.slug ?? ''),
name: String(item.name ?? ''),
description: String(item.description ?? ''),
})),
): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const params: Record<string, string> = {
toolkit_slug: toolkitSlug,
limit: '200',
};
if (searchQuery) {
params.search = searchQuery;
}
return composioApiCall(ZListResponse(ZTool), "/tools", params);
}
/**
* Execute a tool action using Composio SDK
* Execute a tool action
*/
export async function executeAction(
actionSlug: string,
connectedAccountId: string,
input: Record<string, unknown>
request: z.infer<typeof ZExecuteActionRequest>
): Promise<z.infer<typeof ZExecuteActionResponse>> {
console.log(`[Composio] Executing action: ${actionSlug} (account: ${connectedAccountId})`);
try {
const client = getComposioClient();
const result = await client.tools.execute(actionSlug, {
userId: 'rowboat-user',
arguments: input,
connectedAccountId,
dangerouslySkipVersionCheck: true,
});
console.log(`[Composio] Action completed successfully`);
return { success: true, data: result.data };
} catch (error) {
console.error(`[Composio] Action execution failed:`, JSON.stringify(error, Object.getOwnPropertyNames(error ?? {}), 2));
const message = error instanceof Error ? error.message : (typeof error === 'object' ? JSON.stringify(error) : 'Unknown error');
return { success: false, data: null, error: message };
}
return composioApiCall(ZExecuteActionResponse, `/tools/execute/${actionSlug}`, {}, {
method: 'POST',
body: JSON.stringify(request),
});
}

View file

@ -200,18 +200,19 @@ export const ZListResponse = <T extends z.ZodTypeAny>(schema: T) => z.object({
* Execute action request
*/
export const ZExecuteActionRequest = z.object({
action: z.string(),
connected_account_id: z.string(),
input: z.record(z.string(), z.unknown()),
user_id: z.string(),
version: z.string(),
arguments: z.any().optional(),
});
/**
* Execute action response
*/
export const ZExecuteActionResponse = z.object({
success: z.boolean(),
data: z.unknown(),
error: z.string().optional(),
successful: z.boolean(),
error: z.string().nullable(),
});
/**