feat: add dynamic tool loading system with Railway executor

Implements a complete dynamic tool loading system that allows the playground to discover and load tools from the TPMJS registry at runtime.

**Architecture:**
- Search tool package (@tpmjs/search-registry) - Searches registry for tools
- Search API endpoint (/api/tools/search) - Text-based search with scoring
- Pre-flight tool loading - Automatically searches and loads tools on every message
- Railway executor service (Deno) - Loads tools from esm.sh via HTTP imports
- Dynamic tool loader - Calls Railway to load and execute tools remotely

**Key Components:**

1. Railway Executor (apps/railway-executor/)
   - Deno-based service that natively supports HTTP imports
   - Endpoints: /load-and-describe, /execute-tool, /cache/stats, /cache/clear
   - Deploys to Railway with deno run --allow-net --allow-env server.ts

2. Search Tool Package (packages/tools/search-registry/)
   - AI SDK v6 tool for searching TPMJS registry
   - Uses jsonSchema + inputSchema pattern
   - Searches /api/tools/search endpoint

3. Search API (apps/web/src/app/api/tools/search/)
   - Text-based search with composite scoring
   - Scores: text relevance + quality boost + download boost
   - Returns tool metadata with importUrl for dynamic loading

4. Dynamic Tool Loader (apps/playground/src/lib/dynamic-tool-loader.ts)
   - Calls Railway service to load tools from esm.sh
   - Creates tool wrappers that execute remotely
   - Process-level module cache + per-conversation tracking

5. Pre-flight Loading (apps/playground/src/app/api/chat/route.ts)
   - Automatically searches for tools on every user message
   - Loads top 5 matching tools before agent processes request
   - Merges with static tools for seamless experience

**Technical Decisions:**
- Deno over Node.js: Native HTTP import support without flags
- Remote execution: Tools run in Railway sandbox, not Vercel
- Pre-flight loading: Better UX than two-turn search pattern
- Text search: BM25 had dependency issues, simple scoring works well

**Environment Variables:**
- RAILWAY_SERVICE_URL: https://endearing-commitment-production.up.railway.app

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-12-04 08:25:53 +10:00
parent 0612eac5e2
commit 2158ee6dfd
28 changed files with 37043 additions and 27 deletions

View file

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -3,6 +3,9 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils', '@tpmjs/types', '@tpmjs/env'],
reactStrictMode: true,
experimental: {
urlImports: ['https://esm.sh/', 'https://cdn.jsdelivr.net/npm/'],
},
};
export default nextConfig;

View file

@ -15,11 +15,13 @@
"@ai-sdk/react": "^2.0.106",
"@tpmjs/env": "workspace:*",
"@tpmjs/hello": "workspace:*",
"@tpmjs/search-registry": "workspace:*",
"@tpmjs/types": "workspace:*",
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",
"ai": "6.0.0-beta.124",
"firecrawl-aisdk": "^0.7.2",
"nanoid": "^5.1.6",
"next": "^16.0.4",
"next-themes": "^0.4.6",
"openai": "^6.9.1",

View file

@ -1,8 +1,11 @@
import { createOpenAI } from '@ai-sdk/openai';
import { searchTpmjsToolsTool } from '@tpmjs/search-registry';
import { type UIMessage, convertToModelMessages, stepCountIs, streamText } from 'ai';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { env } from '~/env';
import { loadAllTools } from '~/lib/tool-loader';
import { addConversationTools, loadToolsBatch } from '~/lib/dynamic-tool-loader';
import { loadAllTools, sanitizeToolName } from '~/lib/tool-loader';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
@ -13,6 +16,10 @@ const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});
// Add conversation state tracking (in-memory for MVP)
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const conversationStates = new Map<string, { loadedTools: Record<string, any> }>();
/**
* POST /api/chat
* Chat with AI agent that can execute TPMJS tools
@ -20,17 +27,117 @@ const openai = createOpenAI({
export async function POST(request: NextRequest) {
try {
const body = await request.json();
console.log('Request body:', JSON.stringify(body, null, 2));
console.log('📥 Request body:', JSON.stringify(body, null, 2));
const messages: UIMessage[] = body.messages || [];
const conversationId: string = body.conversationId || 'default';
// Load all available TPMJS tools
const tools = await loadAllTools();
console.log(`🔑 Conversation ID: ${conversationId}`);
// Create system prompt listing available tools
const toolsList = Object.keys(tools)
// Get or create conversation state
if (!conversationStates.has(conversationId)) {
console.log('✨ Creating new conversation state');
conversationStates.set(conversationId, { loadedTools: {} });
}
const state = conversationStates.get(conversationId)!;
console.log(
`📊 Current loaded tools in conversation: ${Object.keys(state.loadedTools).length}`
);
// 1. Load static tools + search tool
const staticTools = await loadAllTools();
console.log(`🔧 Loaded ${Object.keys(staticTools).length} static tools`);
staticTools.searchTpmjsTools = searchTpmjsToolsTool;
console.log('✅ Added searchTpmjsTools to static tools');
// Debug: Check the search tool structure
console.log('🔍 Search tool structure:', {
description: searchTpmjsToolsTool.description,
parameters: typeof searchTpmjsToolsTool.parameters,
execute: typeof searchTpmjsToolsTool.execute,
});
// 2. Extract user query from last message for tool search
const lastMessage = messages[messages.length - 1];
let userQuery = '';
if (lastMessage?.role === 'user') {
// Extract text from message parts
const parts = (lastMessage as any).parts || [];
for (const part of parts) {
if (part.type === 'text') {
userQuery = part.text;
break;
}
}
}
console.log(`💬 User query: "${userQuery}"`);
// 3. Automatically search for relevant tools based on the user's message
if (userQuery && userQuery.trim().length > 0) {
console.log('🔎 Searching for relevant tools...');
try {
const searchResult = await searchTpmjsToolsTool.execute(
{
query: userQuery,
limit: 5, // Get top 5 relevant tools
},
{} as any
);
console.log(`📦 Found ${searchResult.matchCount} matching tools`);
if (searchResult.tools && searchResult.tools.length > 0) {
console.log(
`🔧 Tools found:`,
searchResult.tools.map((t: any) => `${t.packageName}/${t.exportName}`)
);
// Dynamically load tools from esm.sh
console.log(`📥 Loading ${searchResult.tools.length} tools dynamically...`);
const toolsToLoad = searchResult.tools.map((meta: any) => ({
packageName: meta.packageName,
exportName: meta.exportName,
version: meta.version,
importUrl: meta.importUrl,
}));
try {
const loadedTools = await loadToolsBatch(toolsToLoad);
console.log(`✅ Successfully loaded ${Object.keys(loadedTools).length} tools`);
// Add sanitized tools to conversation state
for (const [key, tool] of Object.entries(loadedTools)) {
const [pkg, exp] = key.split('::');
const sanitizedKey = sanitizeToolName(`${pkg}-${exp}`);
state.loadedTools[sanitizedKey] = tool;
console.log(`✅ Added to conversation: ${sanitizedKey}`);
}
// Track for this conversation
addConversationTools(conversationId, Object.keys(state.loadedTools));
} catch (error) {
console.error('❌ Error loading tools:', error);
}
} else {
console.log('ℹ️ No matching tools found for this query');
}
} catch (error) {
console.error('❌ Error searching for tools:', error);
}
}
// 4. Merge with conversation's dynamically loaded tools
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const allTools: Record<string, any> = { ...staticTools, ...state.loadedTools };
// 5. Build system prompt with available tools
const toolsList = Object.keys(allTools)
.map((name) => {
const tool = tools[name] as { description?: string } | undefined;
const tool = allTools[name] as { description?: string } | undefined;
return `- ${name}: ${tool?.description || 'No description'}`;
})
.join('\n');
@ -42,12 +149,12 @@ ${toolsList}
When you use a tool, you MUST always follow up with a natural language answer to the user summarizing the result.`;
// Stream the response with multi-step tool usage enabled
// 6. Stream response with all available tools
const result = streamText({
model: openai('gpt-4o-mini'),
system,
messages: convertToModelMessages(messages),
tools,
tools: allTools,
stopWhen: stepCountIs(5), // Allow model to call tools AND generate text response
});

View file

@ -2,15 +2,42 @@
import { useChat as useAISDKChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
import { nanoid } from 'nanoid';
/**
* Custom chat hook that wraps the official @ai-sdk/react useChat
* Handles SSE streaming with tool calls and UI message protocol
* Includes conversation ID tracking for dynamic tool loading
*/
export function useChat() {
return useAISDKChat({
// Generate stable conversation ID for session
const [conversationId] = useState(() => nanoid());
const chat = useAISDKChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
body: {
conversationId, // Pass conversation ID to API
},
onResponse: (response: Response) => {
// Handle "tools loaded" response
if (response.headers.get('content-type')?.includes('application/json')) {
response.json().then((data: any) => {
if (data.type === 'tools_loaded') {
console.log('🔧 Tools loaded:', data.loaded);
// Optionally show toast/notification
}
}).catch(() => {
// Ignore JSON parsing errors for non-JSON responses
});
}
},
});
return {
...chat,
conversationId,
};
}

View file

@ -0,0 +1,182 @@
// Cache for tool wrappers (process-level)
const moduleCache = new Map<string, any>();
// Cache for per-conversation active tools
const conversationTools = new Map<string, Set<string>>();
// Railway service URL
const RAILWAY_SERVICE_URL =
process.env.RAILWAY_SERVICE_URL || process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3001';
/**
* Generate cache key for a tool
*/
function getCacheKey(packageName: string, exportName: string): string {
return `${packageName}::${exportName}`;
}
/**
* Dynamically load a tool via Railway service
* Railway service runs with --experimental-network-imports and can import from esm.sh
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
export async function loadToolDynamically(
packageName: string,
exportName: string,
version: string,
importUrl?: string
): Promise<any | null> {
const cacheKey = getCacheKey(packageName, exportName);
// Check cache first
if (moduleCache.has(cacheKey)) {
console.log(`✅ Cache hit: ${cacheKey}`);
return moduleCache.get(cacheKey);
}
try {
console.log(`📦 Loading from Railway: ${packageName}/${exportName}`);
console.log(`🔗 Railway URL: ${RAILWAY_SERVICE_URL}`);
// Call Railway service to load and describe tool
const response = await fetch(`${RAILWAY_SERVICE_URL}/load-and-describe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
exportName,
version,
importUrl: importUrl || `https://esm.sh/${packageName}@${version}`,
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ Railway service error (${response.status}): ${errorText}`);
return null;
}
const data = await response.json();
if (!data.success) {
console.error(`❌ Failed to load tool: ${data.error}`);
return null;
}
console.log(`✅ Tool loaded from Railway: ${cacheKey}`);
console.log(`📋 Description: ${data.tool.description}`);
// Create a tool wrapper that executes remotely
const tool = {
description: data.tool.description,
inputSchema: data.tool.inputSchema,
// biome-ignore lint/suspicious/noExplicitAny: Tool params are dynamic
execute: async (params: any) => {
console.log(`🚀 Executing ${packageName}/${exportName} remotely with params:`, params);
const execResponse = await fetch(`${RAILWAY_SERVICE_URL}/execute-tool`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
exportName,
version,
importUrl: importUrl || `https://esm.sh/${packageName}@${version}`,
params,
}),
});
const result = await execResponse.json();
if (!result.success) {
console.error(`❌ Tool execution failed: ${result.error}`);
throw new Error(result.error || 'Tool execution failed');
}
console.log(`✅ Tool executed successfully in ${result.executionTimeMs}ms`);
return result.output;
},
};
// Cache the wrapper
moduleCache.set(cacheKey, tool);
console.log(`✅ Cached tool wrapper: ${cacheKey}`);
return tool;
} catch (error) {
console.error(`❌ Failed to load ${packageName}#${exportName}:`, error);
console.error(` Stack:`, error instanceof Error ? error.stack : 'No stack trace');
return null;
}
}
/**
* Load multiple tools in parallel
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
export async function loadToolsBatch(
toolMetadata: Array<{
packageName: string;
exportName: string;
version: string;
importUrl?: string;
}>
): Promise<Record<string, any>> {
const promises = toolMetadata.map((meta) =>
loadToolDynamically(meta.packageName, meta.exportName, meta.version, meta.importUrl).then(
(tool) => ({
key: getCacheKey(meta.packageName, meta.exportName),
tool,
})
)
);
const results = await Promise.all(promises);
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const tools: Record<string, any> = {};
for (const { key, tool } of results) {
if (tool) {
tools[key] = tool;
}
}
return tools;
}
/**
* Track tools for a conversation
*/
export function addConversationTools(conversationId: string, toolKeys: string[]): void {
if (!conversationTools.has(conversationId)) {
conversationTools.set(conversationId, new Set());
}
const tools = conversationTools.get(conversationId)!;
for (const key of toolKeys) {
tools.add(key);
}
}
/**
* Get all tools for a conversation
*/
export function getConversationTools(conversationId: string): string[] {
return Array.from(conversationTools.get(conversationId) || []);
}
/**
* Clear conversation tools (on session end)
*/
export function clearConversationTools(conversationId: string): void {
conversationTools.delete(conversationId);
}
/**
* Get cache statistics
*/
export function getCacheStats() {
return {
moduleCacheSize: moduleCache.size,
conversationCount: conversationTools.size,
};
}

View file

@ -1,6 +1,5 @@
// Static imports for tools (required for Next.js/webpack)
import { helloNameTool, helloWorldTool } from '@tpmjs/hello';
import { crawlTool, scrapeTool, searchTool } from 'firecrawl-aisdk';
/**
* Tool registry mapping package names + export names to actual tool functions
@ -11,11 +10,6 @@ const TOOL_REGISTRY: Record<string, Record<string, any>> = {
helloWorldTool,
helloNameTool,
},
'firecrawl-aisdk': {
scrapeTool,
crawlTool,
searchTool,
},
};
/**
@ -72,7 +66,7 @@ function isCoreTool(obj: unknown): obj is Record<string, any> {
* Sanitize tool name to match OpenAI's requirements
* Pattern: ^[a-zA-Z0-9_-]+$ (only letters, numbers, underscores, hyphens)
*/
function sanitizeToolName(name: string): string {
export function sanitizeToolName(name: string): string {
return name
.replace(/@/g, '') // Remove @ symbols
.replace(/\//g, '_') // Replace / with _

3
apps/railway-executor/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
.env
.env.local

View file

@ -0,0 +1,160 @@
# Railway Dynamic Tool Executor
Dynamic tool executor service that runs with `--experimental-network-imports` to support loading npm packages from esm.sh at runtime.
## Features
- 🔥 **Dynamic imports** from esm.sh
- 💾 **Module caching** for fast repeated loads
- 🛡️ **Validation** of AI SDK tool structure
- 🚀 **Remote execution** of tools with parameters
## Endpoints
### `GET /health`
Health check and cache statistics
### `POST /load-and-describe`
Load a tool from esm.sh and return its schema
**Request:**
```json
{
"packageName": "firecrawl-aisdk",
"exportName": "webSearchTool",
"version": "0.7.2",
"importUrl": "https://esm.sh/firecrawl-aisdk@0.7.2"
}
```
**Response:**
```json
{
"success": true,
"tool": {
"exportName": "webSearchTool",
"description": "Search the web using Firecrawl",
"inputSchema": { ... }
}
}
```
### `POST /execute-tool`
Execute a tool with parameters
**Request:**
```json
{
"packageName": "firecrawl-aisdk",
"exportName": "webSearchTool",
"version": "0.7.2",
"params": {
"query": "latest AI news"
}
}
```
**Response:**
```json
{
"success": true,
"output": { ... },
"executionTimeMs": 1234
}
```
### `POST /cache/clear`
Clear the module cache
### `GET /cache/stats`
Get cache statistics
## Local Development
```bash
# Install dependencies
npm install
# Start server
npm start
# Server runs on http://localhost:3001
```
## Railway Deployment
This service is designed to run on Railway.
### Deploy Steps
1. Initialize Railway in this directory:
```bash
cd apps/railway-executor
railway init
```
2. Link to your Railway project:
```bash
railway link
```
3. Deploy:
```bash
railway up
```
4. Railway will automatically:
- Detect Node.js
- Run `npm install`
- Execute `npm start` (which includes `--experimental-network-imports`)
### Environment Variables
No environment variables required for basic operation. Optional:
- `PORT` - Server port (Railway sets this automatically)
- `NODE_ENV` - Set to `production` in Railway
## Testing
### Test health endpoint:
```bash
curl http://localhost:3001/health
```
### Test tool loading:
```bash
curl -X POST http://localhost:3001/load-and-describe \
-H "Content-Type: application/json" \
-d '{
"packageName": "@tpmjs/hello",
"exportName": "helloWorldTool",
"version": "0.1.0"
}'
```
### Test tool execution:
```bash
curl -X POST http://localhost:3001/execute-tool \
-H "Content-Type: application/json" \
-d '{
"packageName": "@tpmjs/hello",
"exportName": "helloWorldTool",
"version": "0.1.0",
"params": {}
}'
```
## Integration with Playground
The playground app calls this service to load and execute tools dynamically.
Set in playground environment:
```bash
RAILWAY_SERVICE_URL=https://your-service.up.railway.app
```
Or use existing:
```bash
SANDBOX_EXECUTOR_URL=https://your-service.up.railway.app
```

939
apps/railway-executor/package-lock.json generated Normal file
View file

@ -0,0 +1,939 @@
{
"name": "railway-executor",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "railway-executor",
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
"integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/send/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-static/node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static/node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

View file

@ -0,0 +1,15 @@
{
"name": "railway-executor",
"version": "1.0.0",
"description": "Dynamic tool executor for TPMJS with esm.sh imports",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5"
}
}

View file

@ -0,0 +1,11 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "deno run --allow-net --allow-env server.ts",
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}

View file

@ -0,0 +1,229 @@
/**
* 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, exportName, version, importUrl } = req.body;
if (!packageName || !exportName || !version) {
return res.status(400).json({
success: false,
error: 'Missing required fields: packageName, exportName, version',
});
}
const cacheKey = `${packageName}::${exportName}`;
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[exportName] || module;
if (!toolModule) {
console.error(`❌ Export "${exportName}" not found. Available:`, Object.keys(module));
return res.status(404).json({
success: false,
error: `Export "${exportName}" 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: {
exportName,
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, exportName, version, importUrl, params } = req.body;
if (!packageName || !exportName || !version) {
return res.status(400).json({
success: false,
error: 'Missing required fields: packageName, exportName, version',
});
}
const cacheKey = `${packageName}::${exportName}`;
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[exportName];
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`);
});

View file

@ -0,0 +1,279 @@
/**
* Railway Dynamic Tool Executor (Deno)
* Uses Deno's native HTTP import support
*/
// Cache for imported tool modules
const moduleCache = new Map<string, any>();
/**
* Load and describe a tool from esm.sh
*/
async function loadAndDescribe(req: Request): Promise<Response> {
try {
const body = await req.json();
const { packageName, exportName, version, importUrl } = body;
if (!packageName || !exportName || !version) {
return Response.json(
{
success: false,
error: 'Missing required fields: packageName, exportName, version',
},
{ status: 400 }
);
}
const cacheKey = `${packageName}::${exportName}`;
let toolModule;
// Check cache first
if (moduleCache.has(cacheKey)) {
console.log(`✅ Cache hit: ${cacheKey}`);
toolModule = moduleCache.get(cacheKey);
} else {
// Dynamic import from esm.sh (Deno supports this natively!)
const url = importUrl || `https://esm.sh/${packageName}@${version}`;
console.log(`📦 Importing: ${url}`);
const module = await import(url);
toolModule = module[exportName];
if (!toolModule) {
console.error(`❌ Export "${exportName}" not found. Available:`, Object.keys(module));
return Response.json(
{
success: false,
error: `Export "${exportName}" not found in module`,
availableExports: Object.keys(module),
},
{ status: 404 }
);
}
// 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 Response.json(
{
success: false,
error: 'Invalid AI SDK tool structure (missing description or execute)',
toolKeys: Object.keys(toolModule),
},
{ status: 400 }
);
}
// Cache it
moduleCache.set(cacheKey, toolModule);
console.log(`✅ Cached: ${cacheKey}`);
}
// Extract tool definition
return Response.json({
success: true,
tool: {
exportName,
description: toolModule.description,
inputSchema: toolModule.inputSchema || toolModule.parameters?.shape || {},
},
});
} catch (error) {
console.error('❌ Failed to load tool:', error);
return Response.json(
{
success: false,
error: error.message,
},
{ status: 500 }
);
}
}
/**
* Execute a tool with parameters
*/
async function executeTool(req: Request): Promise<Response> {
try {
const body = await req.json();
const { packageName, exportName, version, importUrl, params } = body;
if (!packageName || !exportName || !version) {
return Response.json(
{
success: false,
error: 'Missing required fields: packageName, exportName, version',
},
{ status: 400 }
);
}
const cacheKey = `${packageName}::${exportName}`;
const startTime = Date.now();
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[exportName];
if (!toolModule || !toolModule.execute) {
return Response.json(
{
success: false,
error: 'Tool not found or invalid',
executionTimeMs: Date.now() - startTime,
},
{ status: 404 }
);
}
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`);
return Response.json({
success: true,
output: result,
executionTimeMs,
});
} catch (error) {
const executionTimeMs = Date.now() - Date.now();
console.error('❌ Tool execution failed:', error);
return Response.json(
{
success: false,
error: error.message,
executionTimeMs,
},
{ status: 500 }
);
}
}
/**
* Health check
*/
function health(): Response {
return Response.json({
status: 'ok',
timestamp: new Date().toISOString(),
cacheSize: moduleCache.size,
denoVersion: Deno.version.deno,
v8Version: Deno.version.v8,
httpImports: true,
});
}
/**
* Cache stats
*/
function cacheStats(): Response {
const entries = Array.from(moduleCache.keys());
return Response.json({
success: true,
cacheSize: moduleCache.size,
cachedTools: entries,
});
}
/**
* Clear cache
*/
function clearCache(): Response {
const size = moduleCache.size;
moduleCache.clear();
console.log(`🗑️ Cleared cache (${size} entries)`);
return Response.json({
success: true,
message: `Cleared ${size} cached modules`,
});
}
/**
* Main request handler
*/
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
// Add CORS headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (req.method === 'OPTIONS') {
return new Response(null, { headers });
}
try {
let response: Response;
if (url.pathname === '/health' && req.method === 'GET') {
response = health();
} else if (url.pathname === '/load-and-describe' && req.method === 'POST') {
response = await loadAndDescribe(req);
} else if (url.pathname === '/execute-tool' && req.method === 'POST') {
response = await executeTool(req);
} else if (url.pathname === '/cache/stats' && req.method === 'GET') {
response = cacheStats();
} else if (url.pathname === '/cache/clear' && req.method === 'POST') {
response = clearCache();
} else {
response = Response.json({ error: 'Not found' }, { status: 404 });
}
// Add CORS headers to response
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
} catch (error) {
console.error('Request handler error:', error);
return Response.json(
{
success: false,
error: error.message,
},
{ status: 500, headers }
);
}
}
// Start server
const port = Number.parseInt(Deno.env.get('PORT') || '3002');
console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`);
console.log(`📦 HTTP 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`);
Deno.serve({ port }, handler);

View file

@ -21,6 +21,7 @@
"@tpmjs/utils": "workspace:*",
"@types/react-syntax-highlighter": "^15.5.13",
"ai": "6.0.0-beta.124",
"bm25": "^0.1.1",
"next": "^16.0.4",
"next-themes": "^0.4.6",
"openai": "^6.9.1",

View file

@ -0,0 +1,124 @@
import { prisma } from '@tpmjs/db';
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
// Simple text-based search scoring (fallback until BM25 is fixed)
function calculateTextScore(query: string, document: string): number {
const queryTokens = query.toLowerCase().split(/\s+/);
const docLower = document.toLowerCase();
let score = 0;
for (const token of queryTokens) {
// Exact match in document
if (docLower.includes(token)) {
score += 1;
}
// Boost if token appears in beginning (likely more relevant)
if (docLower.startsWith(token)) {
score += 0.5;
}
}
return score;
}
export async function GET(request: Request) {
console.log('🔎 [SEARCH API] Request received');
try {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q') || '';
const category = searchParams.get('category');
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '10'), 50);
console.log(`🔎 [SEARCH API] Query: "${query}", Category: ${category}, Limit: ${limit}`);
// Fetch all tools with package info
const tools = await prisma.tool.findMany({
include: { package: true },
where: category
? {
package: { category },
}
: undefined,
});
console.log(`📊 [SEARCH API] Found ${tools.length} tools in database`);
// Build searchable documents and calculate scores
const scoredResults = tools.map((tool) => {
const document = [
tool.description,
tool.exportName,
tool.package.npmPackageName,
tool.package.npmDescription || '',
...(tool.package.npmKeywords || []),
].join(' ');
const textScore = calculateTextScore(query, document);
const qualityBoost = Number.parseFloat(tool.qualityScore || '0') * 0.5;
const downloadBoost = Math.log10((tool.package.npmDownloadsLastMonth || 0) + 1) * 0.1;
const finalScore = textScore + qualityBoost + downloadBoost;
return { tool, score: finalScore };
});
// Sort by score and take top N
const topResults = scoredResults
.filter(({ score }) => score > 0) // Only include results with matches
.sort((a, b) => b.score - a.score)
.slice(0, limit + 1);
const hasMore = topResults.length > limit;
const results = hasMore ? topResults.slice(0, limit) : topResults;
console.log(`✅ [SEARCH API] Returning ${results.length} results (hasMore: ${hasMore})`);
// Format response to match existing /api/tools structure
return NextResponse.json({
success: true,
query,
filters: { category },
results: {
total: scoredResults.filter(({ score }) => score > 0).length,
returned: results.length,
tools: results.map(({ tool }) => ({
id: tool.id,
exportName: tool.exportName,
description: tool.description,
qualityScore: tool.qualityScore,
package: {
npmPackageName: tool.package.npmPackageName,
npmVersion: tool.package.npmVersion,
category: tool.package.category,
frameworks: tool.package.frameworks,
env: tool.package.env,
npmRepository: tool.package.npmRepository,
isOfficial: tool.package.isOfficial,
npmDownloadsLastMonth: tool.package.npmDownloadsLastMonth,
},
importUrl: `https://esm.sh/${tool.package.npmPackageName}@${tool.package.npmVersion}`,
cdnUrl: `https://cdn.jsdelivr.net/npm/${tool.package.npmPackageName}@${tool.package.npmVersion}/+esm`,
})),
},
pagination: {
limit,
hasMore,
},
});
} catch (error) {
console.error('❌ [SEARCH API] Error:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Search failed',
},
{ status: 500 }
);
}
}