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
229 lines
6.5 KiB
JavaScript
229 lines
6.5 KiB
JavaScript
/**
|
|
* Railway Dynamic Tool Executor
|
|
* Runs with --experimental-network-imports to support esm.sh imports
|
|
*/
|
|
|
|
import cors from 'cors';
|
|
import express from 'express';
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Cache for imported tool modules
|
|
const moduleCache = new Map();
|
|
|
|
/**
|
|
* Health check endpoint
|
|
*/
|
|
app.get('/health', (_req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
cacheSize: moduleCache.size,
|
|
nodeVersion: process.version,
|
|
experimentalNetworkImports: true,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Load and describe a tool from esm.sh
|
|
* Returns tool metadata (description, schema) without executing
|
|
*/
|
|
app.post('/load-and-describe', async (req, res) => {
|
|
const { packageName, name, version, importUrl } = req.body;
|
|
|
|
if (!packageName || !name || !version) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Missing required fields: packageName, name, version',
|
|
});
|
|
}
|
|
|
|
const cacheKey = `${packageName}::${name}`;
|
|
|
|
try {
|
|
let toolModule;
|
|
|
|
// Check cache first
|
|
if (moduleCache.has(cacheKey)) {
|
|
console.log(`✅ Cache hit: ${cacheKey}`);
|
|
toolModule = moduleCache.get(cacheKey);
|
|
} else {
|
|
// Dynamic import from esm.sh using fetch + eval
|
|
// Note: import() doesn't support HTTPS URLs without custom loader in Node.js
|
|
const url = importUrl || `https://esm.sh/${packageName}@${version}`;
|
|
console.log(`📦 Importing: ${url}`);
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch module: ${response.statusText}`);
|
|
}
|
|
|
|
const moduleCode = await response.text();
|
|
|
|
// Create a module wrapper that captures exports
|
|
const moduleExports = {};
|
|
const moduleWrapper = new Function('exports', 'module', 'require', moduleCode);
|
|
moduleWrapper(moduleExports, { exports: moduleExports }, require);
|
|
|
|
// esm.sh returns ES modules, try to get default or named export
|
|
const module = moduleExports.default || moduleExports;
|
|
toolModule = module[name] || module;
|
|
|
|
if (!toolModule) {
|
|
console.error(`❌ Export "${name}" not found. Available:`, Object.keys(module));
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: `Export "${name}" not found in module`,
|
|
availableExports: Object.keys(module),
|
|
});
|
|
}
|
|
|
|
// Validate it's an AI SDK tool
|
|
if (!toolModule.description || !toolModule.execute) {
|
|
console.error(`❌ Invalid AI SDK tool structure:`, {
|
|
hasDescription: !!toolModule.description,
|
|
hasExecute: !!toolModule.execute,
|
|
hasInputSchema: !!toolModule.inputSchema,
|
|
keys: Object.keys(toolModule),
|
|
});
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Invalid AI SDK tool structure (missing description or execute)',
|
|
toolKeys: Object.keys(toolModule),
|
|
});
|
|
}
|
|
|
|
// Cache it
|
|
moduleCache.set(cacheKey, toolModule);
|
|
console.log(`✅ Cached: ${cacheKey}`);
|
|
}
|
|
|
|
// Extract tool definition (description + schema)
|
|
// AI SDK v6 tools have: description, inputSchema, execute
|
|
res.json({
|
|
success: true,
|
|
tool: {
|
|
name,
|
|
description: toolModule.description,
|
|
inputSchema: toolModule.inputSchema || toolModule.parameters?.shape || {},
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Failed to load tool:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Execute a dynamically loaded tool with parameters
|
|
*/
|
|
app.post('/execute-tool', async (req, res) => {
|
|
const { packageName, name, version, importUrl, params } = req.body;
|
|
|
|
if (!packageName || !name || !version) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Missing required fields: packageName, name, version',
|
|
});
|
|
}
|
|
|
|
const cacheKey = `${packageName}::${name}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
let toolModule;
|
|
|
|
// Check cache or import
|
|
if (moduleCache.has(cacheKey)) {
|
|
console.log(`✅ Using cached tool: ${cacheKey}`);
|
|
toolModule = moduleCache.get(cacheKey);
|
|
} else {
|
|
const url = importUrl || `https://esm.sh/${packageName}@${version}`;
|
|
console.log(`📦 Importing for execution: ${url}`);
|
|
|
|
const module = await import(url);
|
|
toolModule = module[name];
|
|
|
|
if (!toolModule || !toolModule.execute) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Tool not found or invalid',
|
|
executionTimeMs: Date.now() - startTime,
|
|
});
|
|
}
|
|
|
|
moduleCache.set(cacheKey, toolModule);
|
|
}
|
|
|
|
// Execute the tool
|
|
console.log(`🚀 Executing ${cacheKey} with params:`, params);
|
|
const result = await toolModule.execute(params || {});
|
|
|
|
const executionTimeMs = Date.now() - startTime;
|
|
console.log(`✅ Execution complete in ${executionTimeMs}ms`);
|
|
|
|
res.json({
|
|
success: true,
|
|
output: result,
|
|
executionTimeMs,
|
|
});
|
|
} catch (error) {
|
|
const executionTimeMs = Date.now() - startTime;
|
|
console.error('❌ Tool execution failed:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
executionTimeMs,
|
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Clear module cache (for debugging)
|
|
*/
|
|
app.post('/cache/clear', (_req, res) => {
|
|
const size = moduleCache.size;
|
|
moduleCache.clear();
|
|
console.log(`🗑️ Cleared cache (${size} entries)`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Cleared ${size} cached modules`,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Get cache statistics
|
|
*/
|
|
app.get('/cache/stats', (_req, res) => {
|
|
const entries = Array.from(moduleCache.keys());
|
|
|
|
res.json({
|
|
success: true,
|
|
cacheSize: moduleCache.size,
|
|
cachedTools: entries,
|
|
});
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 Railway Tool Executor running on port ${PORT}`);
|
|
console.log(`📦 Experimental network imports: ENABLED`);
|
|
console.log(`🔗 Health check: http://localhost:${PORT}/health`);
|
|
console.log(`🛠️ Endpoints:`);
|
|
console.log(` POST /load-and-describe - Load tool and get schema`);
|
|
console.log(` POST /execute-tool - Execute a tool with params`);
|
|
console.log(` POST /cache/clear - Clear module cache`);
|
|
console.log(` GET /cache/stats - Get cache statistics`);
|
|
});
|