mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-30 20:39:46 +02:00
support managed composio
This commit is contained in:
parent
d0a48d7f51
commit
d2bb11f104
4 changed files with 78 additions and 136 deletions
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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': {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue