#!/usr/bin/env node import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { parseArgs } from 'node:util'; import { parseComposioManifest } from '../src/connections/catalog/composio-manifests.ts'; import { MANIFEST_DIRECTORY, SNAPSHOT_DIRECTORY, checkComposioManifestDirectory, draftComposioManifest, fetchComposioToolkit, readSnapshot, renderDraftReport, reviewedTools, snapshotFromComposio, writeComposioCatalogModules, } from './lib/composio-manifests.ts'; const USAGE = `Usage: npm run connectors:composio -- snapshot --from npm run connectors:composio -- snapshot --fetch (reads COMPOSIO_API_KEY; GET requests only) npm run connectors:composio -- draft [--report ] [--validation-arguments ] npm run connectors:composio -- generate (lists every manifest but the held ones in the catalog) npm run connectors:composio -- check`; const { positionals, values } = parseArgs({ allowPositionals: true, options: { from: { type: 'string' }, fetch: { type: 'boolean' }, report: { type: 'string' }, 'validation-arguments': { type: 'string' }, }, }); const [command, toolkit] = positionals; if (command === 'generate') { await writeComposioCatalogModules(); console.log('Wrote src/connections/catalog/composio-headers.ts and composio-details.ts.'); } else if (command === 'check') { const problems = await checkComposioManifestDirectory(); for (const problem of problems) console.error(problem); if (problems.length > 0) process.exit(1); console.log('Composio manifests match their pinned snapshots.'); } else if (command === 'snapshot' && toolkit && (values.fetch || values.from)) { const previous = await readManifest(toolkit); const [detail, tools] = values.fetch ? await fetchComposio(toolkit) : await readComposioExport(values.from, toolkit); const snapshot = snapshotFromComposio(detail, tools, new Set(previous ? reviewedTools(previous) : [])); const directory = path.join(SNAPSHOT_DIRECTORY, toolkit); await rm(directory, { recursive: true, force: true }); await mkdir(directory, { recursive: true }); const file = path.join(directory, `${snapshot.toolkit.version}.json`); await writeFile(file, `${JSON.stringify(snapshot, null, 2)}\n`); console.log(`Wrote ${path.relative(process.cwd(), file)} with ${snapshot.tools.length} tools.`); } else if (command === 'draft' && toolkit) { const versions = (await readdir(path.join(SNAPSHOT_DIRECTORY, toolkit)).catch(() => [])) .filter((file) => file.endsWith('.json')) .sort(); if (versions.length !== 1) { throw new Error(`Expected exactly one snapshot for ${toolkit}; run the snapshot command first`); } const snapshot = await readSnapshot(toolkit, versions[0].slice(0, -'.json'.length)); const validationArguments = values['validation-arguments'] === undefined ? undefined : JSON.parse(values['validation-arguments']); const { manifest, report } = draftComposioManifest(snapshot, await readManifest(toolkit), { validationArguments }); await writeFile( path.join(MANIFEST_DIRECTORY, `${toolkit}.json`), `${JSON.stringify(manifest, null, 2)}\n`, ); const text = renderDraftReport(report); if (values.report) await writeFile(values.report, text); else process.stdout.write(text); } else { console.error(USAGE); process.exit(2); } async function readManifest(name) { const raw = await readFile(path.join(MANIFEST_DIRECTORY, `${name}.json`), 'utf8').catch((error) => { if (error?.code === 'ENOENT') return undefined; throw error; }); return raw === undefined ? undefined : parseComposioManifest(JSON.parse(raw)); } async function readComposioExport(directory, name) { return Promise.all([ readFile(path.join(directory, 'composio-details', `${name}.json`), 'utf8').then(JSON.parse), readFile(path.join(directory, 'composio-tools', `${name}.json`), 'utf8').then(JSON.parse), ]); } async function fetchComposio(name) { const apiKey = process.env.COMPOSIO_API_KEY?.trim(); if (!apiKey) throw new Error('COMPOSIO_API_KEY is not set'); return fetchComposioToolkit(name, apiKey); }