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

@ -2,7 +2,8 @@ import { shell, BrowserWindow } from 'electron';
import { createAuthServer } from './auth-server.js'; import { createAuthServer } from './auth-server.js';
import * as composioClient from '@x/core/dist/composio/client.js'; import * as composioClient from '@x/core/dist/composio/client.js';
import { composioAccountsRepo } from '@x/core/dist/composio/repo.js'; import { composioAccountsRepo } from '@x/core/dist/composio/repo.js';
import type { LocalConnectedAccount } from '@x/core/dist/composio/types.js'; import type { LocalConnectedAccount, ZExecuteActionResponse } from '@x/core/dist/composio/types.js';
import { z } from 'zod';
const REDIRECT_URI = 'http://localhost:8081/oauth/callback'; const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
@ -28,8 +29,8 @@ export function emitComposioEvent(event: { toolkitSlug: string; success: boolean
/** /**
* Check if Composio is configured with an API key * Check if Composio is configured with an API key
*/ */
export function isConfigured(): { configured: boolean } { export async function isConfigured(): Promise<{ configured: boolean }> {
return { configured: composioClient.isConfigured() }; return { configured: await composioClient.isConfigured() };
} }
/** /**
@ -272,23 +273,28 @@ export async function executeAction(
actionSlug: string, actionSlug: string,
toolkitSlug: string, toolkitSlug: string,
input: Record<string, unknown> input: Record<string, unknown>
): Promise<{ success: boolean; data: unknown; error?: string }> { ): Promise<z.infer<typeof ZExecuteActionResponse>> {
try { try {
const account = composioAccountsRepo.getAccount(toolkitSlug); const account = composioAccountsRepo.getAccount(toolkitSlug);
if (!account || account.status !== 'ACTIVE') { if (!account || account.status !== 'ACTIVE') {
return { return {
success: false,
data: null, data: null,
successful: false,
error: `Toolkit ${toolkitSlug} is not connected`, error: `Toolkit ${toolkitSlug} is not connected`,
}; };
} }
const result = await composioClient.executeAction(actionSlug, account.id, input); const result = await composioClient.executeAction(actionSlug, {
connected_account_id: account.id,
user_id: 'rowboat-user',
version: 'latest',
arguments: input,
});
return result; return result;
} catch (error) { } catch (error) {
console.error('[Composio] Action execution failed:', error); console.error('[Composio] Action execution failed:', error);
return { return {
success: false, successful: false,
data: null, data: null,
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
}; };

View file

@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { Composio } from "@composio/core";
import { WorkDir } from "../config/config.js"; import { WorkDir } from "../config/config.js";
import { import {
ZAuthConfig, ZAuthConfig,
@ -12,33 +11,36 @@ import {
ZCreateConnectedAccountResponse, ZCreateConnectedAccountResponse,
ZDeleteOperationResponse, ZDeleteOperationResponse,
ZErrorResponse, ZErrorResponse,
ZExecuteActionRequest,
ZExecuteActionResponse, ZExecuteActionResponse,
ZListResponse, ZListResponse,
ZTool,
ZToolkit, ZToolkit,
} from "./types.js"; } 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'); const CONFIG_FILE = path.join(WorkDir, 'config', 'composio.json');
// Composio SDK client (lazily initialized) async function getBaseUrl(): Promise<string> {
let composioClient: Composio | null = null; if (await isSignedIn()) {
return `${API_URL}/v1/composio`;
function getComposioClient(): Composio {
if (composioClient) {
return composioClient;
} }
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(); const apiKey = getApiKey();
if (!apiKey) { if (!apiKey) {
throw new Error('Composio API key not configured'); throw new Error('Composio API key not configured');
} }
return { 'x-api-key': apiKey };
composioClient = new Composio({ apiKey });
return composioClient;
}
function resetComposioClient(): void {
composioClient = null;
} }
/** /**
@ -91,13 +93,13 @@ export function setApiKey(apiKey: string): void {
const config = loadConfig(); const config = loadConfig();
config.apiKey = apiKey; config.apiKey = apiKey;
saveConfig(config); saveConfig(config);
resetComposioClient();
} }
/** /**
* Check if Composio is configured * Check if Composio is configured
*/ */
export function isConfigured(): boolean { export async function isConfigured(): Promise<boolean> {
if (await isSignedIn()) return true;
return !!getApiKey(); return !!getApiKey();
} }
@ -106,23 +108,25 @@ export function isConfigured(): boolean {
*/ */
export async function composioApiCall<T extends z.ZodTypeAny>( export async function composioApiCall<T extends z.ZodTypeAny>(
schema: T, schema: T,
url: string, path: string,
params: Record<string, string> = {},
options: RequestInit = {}, options: RequestInit = {},
): Promise<z.infer<T>> { ): Promise<z.infer<T>> {
const apiKey = getApiKey(); const authHeaders = await getAuthHeaders();
if (!apiKey) { const baseURL = await getBaseUrl();
throw new Error('Composio API key not configured'); const url = new URL(path, baseURL);
}
console.log(`[Composio] ${options.method || 'GET'} ${url}`); console.log(`[Composio] ${options.method || 'GET'} ${url}`);
const startTime = Date.now(); const startTime = Date.now();
try { try {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, { const response = await fetch(url, {
...options, ...options,
headers: { headers: {
...options.headers, ...options.headers,
"x-api-key": apiKey, ...authHeaders,
...(options.method === 'POST' ? { "Content-Type": "application/json" } : {}), ...(options.method === 'POST' ? { "Content-Type": "application/json" } : {}),
}, },
}); });
@ -174,47 +178,20 @@ export async function composioApiCall<T extends z.ZodTypeAny>(
* List available toolkits * List available toolkits
*/ */
export async function listToolkits(cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> { export async function listToolkits(cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
const url = new URL(`${BASE_URL}/toolkits`); const params: Record<string, string> = {
url.searchParams.set("sort_by", "usage"); sort_by: "usage",
};
if (cursor) { 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 * Get a specific toolkit
*/ */
export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZToolkit>> { export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZToolkit>> {
const apiKey = getApiKey(); return composioApiCall(ZToolkit, `/toolkits/${toolkitSlug}`);
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 || [],
});
} }
/** /**
@ -225,15 +202,16 @@ export async function listAuthConfigs(
cursor: string | null = null, cursor: string | null = null,
managedOnly: boolean = false managedOnly: boolean = false
): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> { ): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> {
const url = new URL(`${BASE_URL}/auth_configs`); const params: Record<string, string> = {
url.searchParams.set("toolkit_slug", toolkitSlug); toolkit_slug: toolkitSlug,
};
if (cursor) { if (cursor) {
url.searchParams.set("cursor", cursor); params.cursor = cursor;
} }
if (managedOnly) { 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( export async function createAuthConfig(
request: z.infer<typeof ZCreateAuthConfigRequest> request: z.infer<typeof ZCreateAuthConfigRequest>
): Promise<z.infer<typeof ZCreateAuthConfigResponse>> { ): Promise<z.infer<typeof ZCreateAuthConfigResponse>> {
const url = new URL(`${BASE_URL}/auth_configs`); return composioApiCall(ZCreateAuthConfigResponse, "/auth_configs", {}, {
return composioApiCall(ZCreateAuthConfigResponse, url.toString(), {
method: 'POST', method: 'POST',
body: JSON.stringify(request), body: JSON.stringify(request),
}); });
@ -253,8 +230,7 @@ export async function createAuthConfig(
* Delete an auth config * Delete an auth config
*/ */
export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> { export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
const url = new URL(`${BASE_URL}/auth_configs/${authConfigId}`); return composioApiCall(ZDeleteOperationResponse, `/auth_configs/${authConfigId}`, {}, {
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
method: 'DELETE', method: 'DELETE',
}); });
} }
@ -265,8 +241,7 @@ export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<ty
export async function createConnectedAccount( export async function createConnectedAccount(
request: z.infer<typeof ZCreateConnectedAccountRequest> request: z.infer<typeof ZCreateConnectedAccountRequest>
): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> { ): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
const url = new URL(`${BASE_URL}/connected_accounts`); return composioApiCall(ZCreateConnectedAccountResponse, "/connected_accounts", {}, {
return composioApiCall(ZCreateConnectedAccountResponse, url.toString(), {
method: 'POST', method: 'POST',
body: JSON.stringify(request), body: JSON.stringify(request),
}); });
@ -276,16 +251,14 @@ export async function createConnectedAccount(
* Get a connected account * Get a connected account
*/ */
export async function getConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZConnectedAccount>> { export async function getConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZConnectedAccount>> {
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`); return composioApiCall(ZConnectedAccount, `/connected_accounts/${connectedAccountId}`);
return composioApiCall(ZConnectedAccount, url.toString());
} }
/** /**
* Delete a connected account * Delete a connected account
*/ */
export async function deleteConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> { export async function deleteConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`); return composioApiCall(ZDeleteOperationResponse, `/connected_accounts/${connectedAccountId}`, {}, {
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
method: 'DELETE', method: 'DELETE',
}); });
} }
@ -296,64 +269,26 @@ export async function deleteConnectedAccount(connectedAccountId: string): Promis
export async function listToolkitTools( export async function listToolkitTools(
toolkitSlug: string, toolkitSlug: string,
searchQuery: string | null = null, searchQuery: string | null = null,
): Promise<{ items: Array<{ slug: string; name: string; description: string }> }> { ): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const apiKey = getApiKey(); const params: Record<string, string> = {
if (!apiKey) { toolkit_slug: toolkitSlug,
throw new Error('Composio API key not configured'); limit: '200',
}
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 ?? ''),
})),
}; };
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( export async function executeAction(
actionSlug: string, actionSlug: string,
connectedAccountId: string, request: z.infer<typeof ZExecuteActionRequest>
input: Record<string, unknown>
): Promise<z.infer<typeof ZExecuteActionResponse>> { ): Promise<z.infer<typeof ZExecuteActionResponse>> {
console.log(`[Composio] Executing action: ${actionSlug} (account: ${connectedAccountId})`); return composioApiCall(ZExecuteActionResponse, `/tools/execute/${actionSlug}`, {}, {
method: 'POST',
try { body: JSON.stringify(request),
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 };
}
} }

View file

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

View file

@ -375,9 +375,9 @@ const ipcSchemas = {
input: z.record(z.string(), z.unknown()), input: z.record(z.string(), z.unknown()),
}), }),
res: z.object({ res: z.object({
success: z.boolean(),
data: z.unknown(), data: z.unknown(),
error: z.string().optional(), successful: z.boolean(),
error: z.string().nullable(),
}), }),
}, },
'composio:didConnect': { 'composio:didConnect': {