tpmjs/sync-vercel-registry.ts
Ajax Davis 48c41e8733 feat: add use cases marketing product with AI-generated content
Transform qualifying scenarios into marketing-ready use cases with:
- AI-generated titles, descriptions, ROI estimates, business value
- Persona/industry/category taxonomy for targeting
- Browseable feed with filtering and ranking
- SEO-optimized case study pages
- Daily cron job for generation and ranking

Database:
- Add Persona, Industry, Category lookup tables
- Add UseCase model with marketing content fields
- Add junction tables for personas/industries/categories
- Add SocialProof model for cached metrics

API:
- GET /api/use-cases - Global directory with filtering
- GET /api/use-cases/[id] - Individual use case details
- GET /api/public/users/[username]/collections/[slug]/use-cases
- POST /api/cron/use-cases - Nightly generation job

Frontend:
- /use-cases - Global feed with persona dropdown
- /use-cases/[slug] - SEO case study page
- /[username]/collections/[slug]/use-cases - Collection feed
- UseCasesFeed component - Sortable table component
- UseCaseCaseStudy component - Full case study layout
2026-01-20 16:17:35 +10:00

439 lines
14 KiB
TypeScript

/**
* 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 type { ManualTool } from './manual-tools.js';
import { manualTools } 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<VercelTool[]> {
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<ManualTool[]> {
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<void> {
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 };