/** * Sync Vercel AI Registry Tools * * Fetches tools from Vercel's AI SDK registry and adds new ones to manual-tools.ts * Uses OpenAI to intelligently convert Vercel's format to our ManualTool format. */ import fs from 'node:fs/promises'; import path from 'node:path'; import OpenAI from 'openai'; import { manualTools } from './manual-tools.js'; import type { ManualTool } from './manual-tools.js'; // Vercel registry structure (based on their TypeScript interface) interface VercelTool { slug: string; name: string; description: string; packageName: string; tags?: string[]; apiKeyEnvName?: string; installCommand?: { pnpm?: string; npm?: string; yarn?: string; bun?: string; }; codeExample?: string; docsUrl?: string; apiKeyUrl?: string; websiteUrl?: string; npmUrl?: string; } const VERCEL_REGISTRY_URL = 'https://raw.githubusercontent.com/vercel/ai/refs/heads/main/content/tools-registry/registry.ts'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); async function fetchVercelRegistry(): Promise { console.log('šŸ“„ Fetching Vercel AI registry...'); console.log(` URL: ${VERCEL_REGISTRY_URL}\n`); const response = await fetch(VERCEL_REGISTRY_URL); if (!response.ok) { throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`); } const content = await response.text(); console.log(`āœ… Fetched registry (${content.length} bytes)\n`); // Parse TypeScript content to extract tools array // The registry.ts file exports: export const tools: Tool[] = [...] console.log('šŸ” Parsing TypeScript registry...'); // Extract the tools array using regex const toolsMatch = content.match(/export const tools[^=]*=\s*(\[[\s\S]*?\n\]);/); if (!toolsMatch) { throw new Error('Could not find tools array in registry'); } const toolsArrayString = toolsMatch[1]; console.log(` Found tools array (${toolsArrayString.length} chars)\n`); console.log('šŸ”§ Converting TypeScript to JavaScript...'); // Use eval to parse the TypeScript array (safe since it's from Vercel's official repo) // Wrap in try-catch to handle any parsing errors try { // biome-ignore lint/security/noGlobalEval: Parsing trusted source (Vercel's official registry) const tools = eval(toolsArrayString) as VercelTool[]; console.log(`āœ… Parsed ${tools.length} tools from registry\n`); return tools; } catch (error) { console.error('āŒ Failed to parse tools array. Error:', error); console.error('Problematic code preview:'); console.error(toolsArrayString.substring(0, 500)); throw error; } } function findNewTools(vercelTools: VercelTool[]): VercelTool[] { console.log('šŸ” Checking for new tools...'); console.log(` Existing manual tools: ${manualTools.length}`); console.log(` Vercel registry tools: ${vercelTools.length}\n`); const existingPackages = new Set(manualTools.map((t) => t.npmPackageName)); const newTools = vercelTools.filter((tool) => !existingPackages.has(tool.packageName)); console.log(`✨ Found ${newTools.length} new tools:\n`); newTools.forEach((tool, idx) => { console.log(` ${idx + 1}. ${tool.name} (${tool.packageName})`); }); console.log(); return newTools; } async function convertToolWithAI(vercelTool: VercelTool): Promise { console.log(`šŸ¤– Using OpenAI to convert: ${vercelTool.name}`); console.log(` Package: ${vercelTool.packageName}`); const prompt = `You are converting a tool from Vercel's AI registry to TPMJS manual-tools format. VERCEL TOOL DATA: ${JSON.stringify(vercelTool, null, 2)} YOUR TASK: Convert this Vercel tool to TPMJS ManualTool format(s). Important: 1. A single npm package can have MULTIPLE exports (tools). Each export needs its own ManualTool entry. 2. Analyze the codeExample to identify ALL exported tools/functions. 3. For EACH tool/function, create a separate ManualTool object. MANUALTOOLS INTERFACE: { npmPackageName: string; // Use vercelTool.packageName npmVersion?: string; // Omit - will fetch latest category: string; // Choose from: 'text-analysis', 'code-generation', 'data-processing', 'image-generation', 'audio-processing', 'search', 'integration', 'other' frameworks: Array<'vercel-ai' | 'langchain' | 'llamaindex' | 'other'>; name: string; // The actual export name (e.g., 'webSearch', 'executeCode') description: string; // Tool-specific description parameters?: Array<{ // Extract from codeExample if possible name: string; type: string; description: string; required: boolean; default?: string; }>; returns?: { type: string; description: string; }; aiAgent?: { useCase: string; // When should AI agents use this? limitations?: string; examples?: string[]; }; env?: Array<{ // Convert apiKeyEnvName to this format name: string; description: string; required: boolean; }>; tags?: string[]; // Use vercelTool.tags docsUrl?: string; apiKeyUrl?: string; websiteUrl?: string; } EXAMPLES: If codeExample shows: \`\`\`ts import { webSearch, financeSearch } from '@valyu/ai-sdk'; \`\`\` Create TWO ManualTool entries: 1. { name: 'webSearch', ... } 2. { name: 'financeSearch', ... } RESPONSE FORMAT: Return ONLY a valid JSON array of ManualTool objects. No markdown, no explanation, just the JSON array. Example response: [ { "npmPackageName": "@example/sdk", "category": "search", "frameworks": ["vercel-ai"], "name": "webSearch", "description": "...", "env": [{"name": "EXAMPLE_API_KEY", "description": "...", "required": true}], "tags": ["search"], "docsUrl": "...", "apiKeyUrl": "...", "websiteUrl": "..." } ]`; try { const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: 'You are a tool metadata converter. You convert tool definitions from one format to another. Always return valid JSON arrays.', }, { role: 'user', content: prompt }, ], temperature: 0.3, response_format: { type: 'json_object' }, }); const responseText = completion.choices[0]?.message?.content; if (!responseText) { throw new Error('Empty response from OpenAI'); } console.log(` āœ… Received OpenAI response (${responseText.length} chars)`); // Parse the response let parsed: unknown; try { parsed = JSON.parse(responseText); } catch (error) { console.error(' āŒ Failed to parse OpenAI response as JSON'); console.error(' Response:', responseText); throw error; } // Handle both array and object with array property let tools: ManualTool[]; if (Array.isArray(parsed)) { tools = parsed; } else if (parsed.tools && Array.isArray(parsed.tools)) { tools = parsed.tools; } else if (parsed.manualTools && Array.isArray(parsed.manualTools)) { tools = parsed.manualTools; } else { // Assume single tool, wrap in array tools = [parsed]; } console.log(` ✨ Converted to ${tools.length} ManualTool(s):`); tools.forEach((tool, idx) => { console.log(` ${idx + 1}. ${tool.name} - ${tool.description.substring(0, 60)}...`); }); console.log(); return tools; } catch (error) { console.error(' āŒ OpenAI conversion failed:', error); throw error; } } async function appendToManualTools(newTools: ManualTool[]): Promise { if (newTools.length === 0) { console.log('āœ… No new tools to add\n'); return; } console.log(`šŸ“ Adding ${newTools.length} new tools to manual-tools.ts...\n`); const filePath = path.join(process.cwd(), 'manual-tools.ts'); // Read the current file const currentContent = await fs.readFile(filePath, 'utf-8'); // Find the closing bracket of the manualTools array const lastBracketIndex = currentContent.lastIndexOf('];'); if (lastBracketIndex === -1) { throw new Error('Could not find closing bracket of manualTools array'); } // Generate TypeScript code for new tools const newToolsCode = newTools .map((tool) => { // Convert to properly formatted TypeScript const lines: string[] = [' {']; // Required fields lines.push(` npmPackageName: '${tool.npmPackageName}',`); lines.push(` category: '${tool.category}',`); lines.push(` frameworks: [${tool.frameworks.map((f) => `'${f}'`).join(', ')}],`); lines.push(` name: '${tool.name}',`); lines.push(` description: '${tool.description.replace(/'/g, "\\'")}',`); // Optional fields if (tool.tags && tool.tags.length > 0) { lines.push(` tags: [${tool.tags.map((t) => `'${t}'`).join(', ')}],`); } if (tool.env && tool.env.length > 0) { lines.push(' env: ['); tool.env.forEach((e, idx) => { lines.push(' {'); lines.push(` name: '${e.name}',`); lines.push(` description: '${e.description.replace(/'/g, "\\'")}',`); lines.push(` required: ${e.required},`); lines.push(` }${idx < tool.env?.length - 1 ? ',' : ''}`); }); lines.push(' ],'); } if (tool.parameters && tool.parameters.length > 0) { lines.push(' parameters: ['); tool.parameters.forEach((p, idx) => { lines.push(' {'); lines.push(` name: '${p.name}',`); lines.push(` type: '${p.type}',`); lines.push(` description: '${p.description.replace(/'/g, "\\'")}',`); lines.push(` required: ${p.required},`); if (p.default) { lines.push(` default: '${p.default}',`); } lines.push(` }${idx < tool.parameters?.length - 1 ? ',' : ''}`); }); lines.push(' ],'); } if (tool.returns) { lines.push(' returns: {'); lines.push(` type: '${tool.returns.type}',`); lines.push(` description: '${tool.returns.description.replace(/'/g, "\\'")}',`); lines.push(' },'); } if (tool.aiAgent) { lines.push(' aiAgent: {'); lines.push(` useCase: '${tool.aiAgent.useCase.replace(/'/g, "\\'")}',`); if (tool.aiAgent.limitations) { lines.push(` limitations: '${tool.aiAgent.limitations.replace(/'/g, "\\'")}',`); } if (tool.aiAgent.examples && tool.aiAgent.examples.length > 0) { lines.push( ` examples: [${tool.aiAgent.examples.map((e) => `'${e.replace(/'/g, "\\'")}'`).join(', ')}],` ); } lines.push(' },'); } if (tool.docsUrl) { lines.push(` docsUrl: '${tool.docsUrl}',`); } if (tool.apiKeyUrl) { lines.push(` apiKeyUrl: '${tool.apiKeyUrl}',`); } if (tool.websiteUrl) { lines.push(` websiteUrl: '${tool.websiteUrl}',`); } lines.push(' },'); return lines.join('\n'); }) .join('\n'); // Insert new tools before the closing bracket const updatedContent = `${ currentContent.slice(0, lastBracketIndex) + newToolsCode }\n${currentContent.slice(lastBracketIndex)}`; // Write back to file await fs.writeFile(filePath, updatedContent, 'utf-8'); console.log('āœ… Successfully updated manual-tools.ts\n'); } async function main() { console.log('\nšŸš€ Starting Vercel AI Registry Sync\n'); console.log('═══════════════════════════════════════\n'); try { // Step 1: Fetch Vercel registry const vercelTools = await fetchVercelRegistry(); // Step 2: Find new tools const newVercelTools = findNewTools(vercelTools); if (newVercelTools.length === 0) { console.log('āœ… No new tools found. Manual tools are up to date!\n'); return { processed: 0, skipped: vercelTools.length, errors: 0, total: vercelTools.length, }; } // Step 3: Convert new tools using AI console.log('šŸ¤– Converting new tools with OpenAI...\n'); const convertedTools: ManualTool[] = []; let errors = 0; for (const vercelTool of newVercelTools) { try { const tools = await convertToolWithAI(vercelTool); convertedTools.push(...tools); } catch (error) { console.error(`āŒ Failed to convert ${vercelTool.name}:`, error); errors++; } } console.log('\nšŸ“Š Conversion Summary:'); console.log(` New Vercel tools: ${newVercelTools.length}`); console.log(` Converted ManualTools: ${convertedTools.length}`); console.log(` Errors: ${errors}\n`); // Step 4: Append to manual-tools.ts if (convertedTools.length > 0) { await appendToManualTools(convertedTools); } console.log('═══════════════════════════════════════\n'); console.log('āœ… Vercel AI Registry Sync Complete!\n'); return { processed: convertedTools.length, skipped: vercelTools.length - newVercelTools.length, errors, total: vercelTools.length, }; } catch (error) { console.error('\nāŒ Sync failed:', error); throw error; } } // Run if called directly if (import.meta.url === `file://${process.argv[1]}`) { main() .then((result) => { console.log('\nšŸ“Š Final Results:'); console.log(` Processed: ${result.processed}`); console.log(` Skipped: ${result.skipped}`); console.log(` Errors: ${result.errors}`); console.log(` Total: ${result.total}\n`); process.exit(0); }) .catch((error) => { console.error('Fatal error:', error); process.exit(1); }); } export { main as syncVercelRegistry };