Promote AgentCompany-New to the repository root and retire the old app

The legacy application - its src, tests, landing site, promo assets, docs,
release tooling, CI workflows, and root configs - leaves the tree; it stays
reachable in history. Everything under AgentCompany-New moves up unchanged:
same package name, so the production userData/database path is untouched,
and e2e/build scripts already resolved paths relative to the project. The
developer cred.txt stays gitignored at its new location. Full verify:ci
passes from the new root.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-07 00:04:13 -04:00
1 parent 2350f744e3
commit 07d0b4587d
1904 files changed
+1133 -282075

No files matched your search

@@ -0,0 +1,194 @@
import { net } from "electron";
const BASE_URL = "https://backend.composio.dev/api/v3";
export interface ComposioToolSchema {
name: string;
description: string;
toolkitSlug: string | null;
parameters: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
}
export interface ComposioToolkitSummary {
slug: string;
name: string;
description: string;
logo: string | null;
categories: string[];
authSchemes: string[];
noAuth: boolean;
toolsCount: number;
}
export interface ComposioConnectedAccount {
id: string;
toolkitSlug: string;
status: string;
authConfigId: string;
createdAt: string;
}
export interface ComposioToolExecutionResult {
data: Record<string, unknown>;
error: string | null;
}
export class ComposioClient {
constructor(private readonly apiKey: string) {}
/** The toolkit catalog, most popular first. */
async listToolkits(limit: number): Promise<ComposioToolkitSummary[]> {
const toolkits: ComposioToolkitSummary[] = [];
let cursor: string | null = null;
while (toolkits.length < limit) {
const params = new URLSearchParams({ limit: String(Math.min(100, limit - toolkits.length)) });
if (cursor) params.set("cursor", cursor);
const response = await this.request("GET", `${BASE_URL}/toolkits?${params.toString()}`);
const items = Array.isArray(response.items) ? response.items as Array<Record<string, unknown>> : [];
for (const item of items) {
const meta = (item.meta ?? {}) as Record<string, unknown>;
toolkits.push({
slug: String(item.slug ?? ""),
name: String(item.name ?? ""),
description: String(meta.description ?? "").slice(0, 200),
logo: typeof meta.logo === "string" ? meta.logo : null,
categories: Array.isArray(meta.categories)
? (meta.categories as Array<Record<string, unknown>>).map((c) => String(c.name ?? c.id ?? "")).filter(Boolean)
: [],
authSchemes: Array.isArray(item.auth_schemes) ? (item.auth_schemes as unknown[]).map(String) : [],
noAuth: item.no_auth === true,
toolsCount: Number(meta.tools_count ?? 0),
});
}
cursor = typeof response.next_cursor === "string" && response.next_cursor ? response.next_cursor : null;
if (!cursor || items.length === 0) break;
}
return toolkits;
}
async getTools(toolkits: string[]): Promise<ComposioToolSchema[]> {
const all: ComposioToolSchema[] = [];
for (const toolkit of toolkits) {
const params = new URLSearchParams({ toolkit_slug: toolkit, limit: "20" });
const url = `${BASE_URL}/tools?${params.toString()}`;
const response = await this.request("GET", url);
if (Array.isArray(response.items)) {
all.push(...(response.items as Array<Record<string, unknown>>).map(normalizeToolSchema));
}
}
return all;
}
/** Creates a Composio-managed auth config for a toolkit. */
async createAuthConfig(toolkitSlug: string): Promise<string> {
const response = await this.request("POST", `${BASE_URL}/auth_configs`, {
toolkit: { slug: toolkitSlug },
});
const authConfig = (response.auth_config ?? {}) as Record<string, unknown>;
const id = String(authConfig.id ?? "");
if (!id) throw new Error("Composio auth config creation returned no id.");
return id;
}
/** Starts a connection for a user; the redirect URL completes it in the browser. */
async createConnectedAccount(input: { authConfigId: string; userId: string }): Promise<{
id: string;
status: string;
redirectUrl: string | null;
}> {
const response = await this.request("POST", `${BASE_URL}/connected_accounts`, {
auth_config: { id: input.authConfigId },
connection: { user_id: input.userId },
});
return {
id: String(response.id ?? ""),
status: String(response.status ?? ""),
redirectUrl: typeof response.redirect_url === "string" ? response.redirect_url : null,
};
}
async listConnectedAccounts(userId: string): Promise<ComposioConnectedAccount[]> {
const params = new URLSearchParams({ user_ids: userId, limit: "50" });
const response = await this.request("GET", `${BASE_URL}/connected_accounts?${params.toString()}`);
if (!Array.isArray(response.items)) return [];
return (response.items as Array<Record<string, unknown>>).map((item) => {
const toolkit = (item.toolkit ?? {}) as Record<string, unknown>;
const authConfig = (item.auth_config ?? {}) as Record<string, unknown>;
return {
id: String(item.id ?? ""),
toolkitSlug: String(toolkit.slug ?? "").toLowerCase(),
status: String(item.status ?? ""),
authConfigId: String(authConfig.id ?? ""),
createdAt: String(item.created_at ?? ""),
};
});
}
async executeTool(input: {
toolName: string;
arguments: Record<string, unknown>;
userId: string;
connectedAccountId?: string;
}): Promise<ComposioToolExecutionResult> {
const url = `${BASE_URL}/tools/execute/${encodeURIComponent(input.toolName)}`;
const body: Record<string, unknown> = {
user_id: input.userId,
arguments: input.arguments,
};
if (input.connectedAccountId) body.connected_account_id = input.connectedAccountId;
const response = await this.request("POST", url, body);
return {
data: typeof response === "object" && response !== null ? response as Record<string, unknown> : {},
error: null,
};
}
private async request(method: string, url: string, body?: Record<string, unknown>): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
const request = net.request({ method, url });
request.setHeader("x-api-key", this.apiKey);
request.setHeader("Accept", "application/json");
if (body) request.setHeader("Content-Type", "application/json");
let responseData = "";
request.on("response", (response) => {
response.on("data", (chunk) => { responseData += chunk.toString(); });
response.on("end", () => {
if (response.statusCode && response.statusCode >= 400) {
reject(new Error(`Composio API ${method} ${url} returned ${response.statusCode}: ${responseData.slice(0, 200)}`));
return;
}
try {
resolve(JSON.parse(responseData) as Record<string, unknown>);
} catch {
reject(new Error(`Composio API returned non-JSON response: ${responseData.slice(0, 200)}`));
}
});
response.on("error", reject);
});
request.on("error", reject);
if (body) request.write(JSON.stringify(body));
request.end();
});
}
}
function normalizeToolSchema(raw: Record<string, unknown>): ComposioToolSchema {
const params = raw.input_parameters ?? raw.parameters ?? raw.inputSchema ?? {};
const schema = typeof params === "object" && params !== null ? params as Record<string, unknown> : {};
const toolkit = (raw.toolkit ?? {}) as Record<string, unknown>;
return {
name: String(raw.slug ?? raw.name ?? ""),
description: String(raw.description ?? "").slice(0, 300),
toolkitSlug: typeof toolkit.slug === "string" ? toolkit.slug : null,
parameters: {
type: "object",
properties: (schema.properties ?? {}) as Record<string, unknown>,
required: Array.isArray(schema.required) ? schema.required as string[] : [],
},
};
}
@@ -0,0 +1,112 @@
import type { DatabaseSync } from "node:sqlite";
import type { ComposioToolkitSummaryDto, ConnectComposioToolkitResult } from "../../../shared/commands";
import { ComposioClient, type ComposioToolkitSummary } from "./composio-client";
import { resolveComposioApiKey } from "./composio-credentials";
import { getComposioUserId } from "./composio-tool-bridge";
import { normalizeSlug } from "./connector-grants";
const TOOLKIT_CATALOG_LIMIT = 200;
const CATALOG_TTL_MS = 10 * 60 * 1000;
let catalogCache: { fetchedAt: number; toolkits: ComposioToolkitSummary[] } | null = null;
/**
* Company-level Composio connections. One connected account per toolkit per
* organization, mirrored into connector_accounts so agent runs and the
* Connectors page read local state instead of calling out on every frame.
*/
export class ComposioConnectionService {
constructor(private readonly db: DatabaseSync, private readonly credentialFileDir: string) {}
listToolkits(orgId: string): Promise<ComposioToolkitSummaryDto[]> {
const client = this.requireClient(orgId);
return this.catalog(client);
}
/**
* Starts (or restarts) the company's connection for a toolkit: creates a
* Composio-managed auth config once per toolkit, initiates the connected
* account for the company user, and records the pending account locally.
* The returned redirect URL completes authorization in the browser.
*/
async connectToolkit(orgId: string, toolkitSlug: string, toolkitName: string): Promise<ConnectComposioToolkitResult> {
const slug = normalizeSlug(toolkitSlug);
const client = this.requireClient(orgId);
const authConfigId = await this.ensureAuthConfig(client, orgId, slug);
const account = await client.createConnectedAccount({
authConfigId,
userId: getComposioUserId(this.db, orgId),
});
if (!account.id) throw new Error("Composio did not return a connected account id.");
const now = new Date().toISOString();
this.db.prepare(`
insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, auth_config_id, status, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?)
on conflict(org_id, toolkit_slug) do update set
id = excluded.id,
toolkit_name = excluded.toolkit_name,
auth_config_id = excluded.auth_config_id,
status = excluded.status,
updated_at = excluded.updated_at
`).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, account.status || "INITIATED", now, now);
return { accountId: account.id, status: account.status, redirectUrl: account.redirectUrl };
}
/** Pulls account statuses from Composio and updates the local mirror. */
async refreshAccounts(orgId: string): Promise<void> {
const client = this.requireClient(orgId);
const accounts = await client.listConnectedAccounts(getComposioUserId(this.db, orgId));
const now = new Date().toISOString();
for (const account of accounts) {
if (!account.toolkitSlug) continue;
this.db.prepare(`
insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, auth_config_id, status, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?)
on conflict(org_id, toolkit_slug) do update set
id = excluded.id,
auth_config_id = excluded.auth_config_id,
status = excluded.status,
updated_at = excluded.updated_at
`).run(
account.id,
orgId,
account.toolkitSlug,
account.toolkitSlug,
account.authConfigId,
account.status,
account.createdAt || now,
now,
);
}
}
private requireClient(orgId: string): ComposioClient {
const apiKey = resolveComposioApiKey(this.db, orgId, this.credentialFileDir);
if (!apiKey) throw new Error("Composio API key is not configured for this company.");
return new ComposioClient(apiKey);
}
private async ensureAuthConfig(client: ComposioClient, orgId: string, slug: string): Promise<string> {
const key = `auth_config:${slug}`;
const existing = this.db.prepare(
"select value from composio_settings where org_id = ? and key = ? limit 1",
).get(orgId, key) as { value: string } | undefined;
if (existing?.value) return existing.value;
const authConfigId = await client.createAuthConfig(slug);
this.db.prepare(`
insert into composio_settings(org_id, key, value, updated_at) values (?, ?, ?, ?)
on conflict(org_id, key) do update set value = excluded.value, updated_at = excluded.updated_at
`).run(orgId, key, authConfigId, new Date().toISOString());
return authConfigId;
}
private async catalog(client: ComposioClient): Promise<ComposioToolkitSummaryDto[]> {
if (catalogCache && Date.now() - catalogCache.fetchedAt < CATALOG_TTL_MS) {
return catalogCache.toolkits;
}
const toolkits = await client.listToolkits(TOOLKIT_CATALOG_LIMIT);
catalogCache = { fetchedAt: Date.now(), toolkits };
return toolkits;
}
}
@@ -0,0 +1,32 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
/**
* The org's Composio API key. When the database has none, a developer
* credential file next to the app (cred.txt, first line "Composio apikey:
* ak_...", gitignored) seeds it once for this org — the product owner's
* local path. Packaged installs configure the key in Settings instead; a
* missing file simply yields null.
*/
export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDir: string = process.cwd()): string | null {
const stored = db.prepare(
"select value from composio_settings where org_id = ? and key = 'api_key' limit 1",
).get(orgId) as { value: string } | undefined;
if (stored?.value) return stored.value;
const path = join(credentialFileDir, "cred.txt");
if (!existsSync(path)) return null;
const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? "";
const key = firstLine.split(":").slice(1).join(":").trim();
if (!key) return null;
const now = new Date().toISOString();
const upsert = db.prepare(`
insert into composio_settings(org_id, key, value, updated_at) values (?, ?, ?, ?)
on conflict(org_id, key) do update set value = excluded.value, updated_at = excluded.updated_at
`);
upsert.run(orgId, "api_key", key, now);
upsert.run(orgId, "entity_id", orgId, now);
return key;
}
@@ -0,0 +1,126 @@
import type { DatabaseSync } from "node:sqlite";
import type { ToolDefinition, ToolCall } from "../provider-tool-turn";
import { ComposioClient, type ComposioToolSchema } from "./composio-client";
import { resolveComposioApiKey } from "./composio-credentials";
import { ConnectorGrantsService } from "./connector-grants";
const COMPOSIO_TOOL_PREFIX = "composio_";
const CACHE_TTL_MS = 5 * 60 * 1000;
interface CacheEntry {
tools: ToolDefinition[];
fetchedAt: number;
toolkitsKey: string;
}
const toolCache = new Map<string, CacheEntry>();
/** tool slug (upper case) → toolkit slug, learned from the tools listing. */
const toolkitByTool = new Map<string, string>();
export function isComposioTool(toolName: string): boolean {
return toolName.startsWith(COMPOSIO_TOOL_PREFIX);
}
export function composioToolName(composioName: string): string {
return `${COMPOSIO_TOOL_PREFIX}${composioName.toLowerCase()}`;
}
function originalToolName(prefixedName: string): string {
return prefixedName.slice(COMPOSIO_TOOL_PREFIX.length).toUpperCase();
}
/** Toolkit a tool belongs to: learned mapping first, slug prefix as contract. */
function toolkitForTool(toolSlug: string): string {
return toolkitByTool.get(toolSlug) ?? toolSlug.split("_")[0].toLowerCase();
}
export function composioSchemaToToolDefinition(schema: ComposioToolSchema): ToolDefinition {
return {
name: composioToolName(schema.name),
description: schema.description,
parameters: {
type: "object",
additionalProperties: false,
properties: schema.parameters.properties,
required: schema.parameters.required ?? [],
},
};
}
/**
* The connector tools one agent is allowed to carry into a run: the
* intersection of the company's active connections and the toolkits the CEO
* granted this agent.
*/
export async function loadComposioTools(db: DatabaseSync, orgId: string, agentId: string): Promise<ToolDefinition[]> {
const apiKey = getComposioApiKey(db, orgId);
if (!apiKey) return [];
const grants = new ConnectorGrantsService(db);
const toolkits = grants.toolkitsForAgent(orgId, agentId);
if (toolkits.length === 0) return [];
const toolkitsKey = toolkits.join(",");
const cacheKey = `${orgId}:${toolkitsKey}`;
const cached = toolCache.get(cacheKey);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return cached.tools;
}
try {
const client = new ComposioClient(apiKey);
const schemas = await client.getTools(toolkits);
for (const schema of schemas) {
if (schema.toolkitSlug) toolkitByTool.set(schema.name.toUpperCase(), schema.toolkitSlug.toLowerCase());
}
const tools = schemas.map(composioSchemaToToolDefinition);
toolCache.set(cacheKey, { tools, fetchedAt: Date.now(), toolkitsKey });
return tools;
} catch {
return cached?.tools ?? [];
}
}
export async function executeComposioTool(
db: DatabaseSync,
orgId: string,
agentId: string,
call: ToolCall,
): Promise<string> {
const apiKey = getComposioApiKey(db, orgId);
if (!apiKey) return "ERROR: Composio API key is not configured for this company.";
const toolSlug = originalToolName(call.name);
const toolkit = toolkitForTool(toolSlug);
const grants = new ConnectorGrantsService(db);
const allowed = grants.toolkitsForAgent(orgId, agentId);
if (!allowed.includes(toolkit)) {
return `ERROR: You have no grant for the "${toolkit}" connector. Ask the CEO to grant it to you (grant_connector).`;
}
const account = db.prepare(
"select id from connector_accounts where org_id = ? and toolkit_slug = ? and status = 'ACTIVE'",
).get(orgId, toolkit) as { id: string } | undefined;
if (!account) {
return `ERROR: The "${toolkit}" connector is no longer connected. Ask the board to reconnect it on the Connectors page.`;
}
const client = new ComposioClient(apiKey);
try {
const result = await client.executeTool({
toolName: toolSlug,
arguments: call.arguments,
userId: getComposioUserId(db, orgId),
connectedAccountId: account.id,
});
if (result.error) return `ERROR: ${result.error}`;
return JSON.stringify(result.data);
} catch (error) {
return `ERROR: ${error instanceof Error ? error.message : String(error)}`;
}
}
function getComposioApiKey(db: DatabaseSync, orgId: string): string | null {
return resolveComposioApiKey(db, orgId);
}
export function getComposioUserId(db: DatabaseSync, orgId: string): string {
const row = db.prepare(
"select value from composio_settings where org_id = ? and key = 'entity_id' limit 1",
).get(orgId) as { value: string } | undefined;
return row?.value ?? orgId;
}
@@ -0,0 +1,92 @@
import type { DatabaseSync } from "node:sqlite";
import { createId } from "../../../shared/ids";
export interface ConnectorAccountRow {
id: string;
orgId: string;
toolkitSlug: string;
toolkitName: string;
status: string;
}
export interface ConnectorGrantRow {
id: string;
orgId: string;
agentId: string;
toolkitSlug: string;
}
/**
* Which agents may use which connected toolkits. Connections belong to the
* company (one connected account per toolkit per org); the CEO hands
* individual toolkits to individual agents, and both the tool loadout and
* the execution guard read from these grants.
*/
export class ConnectorGrantsService {
constructor(private readonly db: DatabaseSync) {}
grant(input: { orgId: string; agentId: string; toolkitSlug: string; grantedByType: "agent" | "employee"; grantedById: string | null }): void {
const agent = this.db.prepare("select id from agents where id = ? and org_id = ? and lifecycle = 'active'")
.get(input.agentId, input.orgId);
if (!agent) throw new Error("Grant target must be an active agent in this organization.");
const slug = normalizeSlug(input.toolkitSlug);
const account = this.db.prepare(
"select id from connector_accounts where org_id = ? and toolkit_slug = ? and status = 'ACTIVE'",
).get(input.orgId, slug);
if (!account) throw new Error(`No active connection for toolkit "${slug}". Connect it on the Connectors page first.`);
this.db.prepare(`
insert into agent_connector_grants(id, org_id, agent_id, toolkit_slug, granted_by_type, granted_by_id, created_at)
values (?, ?, ?, ?, ?, ?, ?)
on conflict(org_id, agent_id, toolkit_slug) do nothing
`).run(createId("cgr"), input.orgId, input.agentId, slug, input.grantedByType, input.grantedById, new Date().toISOString());
}
revoke(input: { orgId: string; agentId: string; toolkitSlug: string }): void {
this.db.prepare("delete from agent_connector_grants where org_id = ? and agent_id = ? and toolkit_slug = ?")
.run(input.orgId, input.agentId, normalizeSlug(input.toolkitSlug));
}
/** Toolkits this agent may use: granted AND actively connected. */
toolkitsForAgent(orgId: string, agentId: string): string[] {
const rows = this.db.prepare(`
select g.toolkit_slug as slug
from agent_connector_grants g
join connector_accounts a on a.org_id = g.org_id and a.toolkit_slug = g.toolkit_slug and a.status = 'ACTIVE'
where g.org_id = ? and g.agent_id = ?
order by g.toolkit_slug asc
`).all(orgId, agentId) as Array<{ slug: string }>;
return rows.map((row) => row.slug);
}
activeAccounts(orgId: string): ConnectorAccountRow[] {
const rows = this.db.prepare(`
select id, org_id, toolkit_slug, toolkit_name, status
from connector_accounts
where org_id = ? and status = 'ACTIVE'
order by toolkit_slug asc
`).all(orgId) as Array<{ id: string; org_id: string; toolkit_slug: string; toolkit_name: string; status: string }>;
return rows.map((row) => ({
id: row.id,
orgId: row.org_id,
toolkitSlug: row.toolkit_slug,
toolkitName: row.toolkit_name,
status: row.status,
}));
}
grantsForOrg(orgId: string): ConnectorGrantRow[] {
const rows = this.db.prepare(`
select id, org_id, agent_id, toolkit_slug
from agent_connector_grants
where org_id = ?
order by toolkit_slug asc
`).all(orgId) as Array<{ id: string; org_id: string; agent_id: string; toolkit_slug: string }>;
return rows.map((row) => ({ id: row.id, orgId: row.org_id, agentId: row.agent_id, toolkitSlug: row.toolkit_slug }));
}
}
export function normalizeSlug(slug: string): string {
const normalized = slug.trim().toLowerCase();
if (!normalized) throw new Error("Toolkit slug is required.");
return normalized;
}