feat: implement multi-tool package architecture with manual tool registry
BREAKING CHANGE: Complete refactoring from single-tool to multi-tool package support Database Schema: - Split Tool model into Package (1) and Tool (many) with one-to-many relationship - Package stores npm metadata and package-level tpmjs fields (category, env, frameworks, tier) - Tool stores individual tool exports with tool-level metadata (exportName, description, parameters, returns, aiAgent) - Unique constraint on (packageId, exportName) to prevent duplicate tools - Cascade deletes when packages are removed Type System: - Updated tpmjs field schema to support tools array - Each tool has exportName, description, parameters, returns, aiAgent - Package-level fields: category, env, frameworks shared across all tools - Backward compatible with legacy single-tool format (auto-migrates to exportName: "default") API Updates: - Updated all /api/tools routes to query Tool model with Package relations - Updated /api/tools/[slug] to accept package/export path segments - Updated tool-executor-agent to use actual exportName instead of hardcoded "default" - Updated metrics sync to calculate quality scores per Tool Frontend Updates: - Updated tool search page to display exportName as primary heading - Updated tool detail pages to show package name as secondary info - Removed tag-based filtering (tags moved to package level) Manual Tool Registry: - Added manual-tools.ts with 23 curated tools from major providers - Created sync-manual-tools.ts script to sync manual tools to database - Added MANUAL_TOOLS.md documentation for manual tool system - Added GitHub workflow for automated daily sync - Includes tools from: Vercel, Exa, Firecrawl, AWS Bedrock, Perplexity, Tavily, Superagent, Valyu Playground Updates: - Updated tool loader to load multiple tools per package - Added sanitizeToolName for OpenAI API compatibility Sync System Updates: - Updated changes feed sync to handle multi-tool packages - Updated keyword sync to upsert multiple tools per package - Added orphaned tool deletion when tools removed from package.json Migration Strategy: - Database uses same Neon instance for dev and prod - Schema updated via prisma db push (no migration files yet) - All data repopulates from npm via sync system 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fdd1b2c304
commit
0612eac5e2
31 changed files with 3783 additions and 786 deletions
97
.github/workflows/sync-manual.yml
vendored
Normal file
97
.github/workflows/sync-manual.yml
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
name: Sync Manual Tools
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at midnight UTC
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
# Run on pushes to main that modify manual-tools.ts
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'manual-tools.ts'
|
||||
- 'sync-manual-tools.ts'
|
||||
|
||||
jobs:
|
||||
sync-manual:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Prisma Client
|
||||
run: pnpm --filter=@tpmjs/db db:generate
|
||||
|
||||
- name: Run manual tools sync
|
||||
id: sync
|
||||
run: |
|
||||
# Run the sync script and capture output
|
||||
output=$(pnpm tsx sync-manual-tools.ts 2>&1)
|
||||
echo "$output"
|
||||
|
||||
# Extract statistics from output
|
||||
processed=$(echo "$output" | grep "Processed:" | awk '{print $2}')
|
||||
skipped=$(echo "$output" | grep "Skipped:" | awk '{print $2}')
|
||||
errors=$(echo "$output" | grep "Errors:" | awk '{print $2}')
|
||||
total=$(echo "$output" | grep "Total manual tools:" | awk '{print $4}')
|
||||
|
||||
# Set outputs for Discord notification
|
||||
echo "processed=${processed:-0}" >> $GITHUB_OUTPUT
|
||||
echo "skipped=${skipped:-0}" >> $GITHUB_OUTPUT
|
||||
echo "errors=${errors:-0}" >> $GITHUB_OUTPUT
|
||||
echo "total=${total:-0}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "${errors:-0}" -gt 0 ]; then
|
||||
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
|
||||
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
|
||||
else
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
|
||||
fi
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
# Build Discord payload
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} Manual Tools Sync" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
color: $color,
|
||||
fields: [
|
||||
{ name: "📦 Total Tools", value: "${{ steps.sync.outputs.total }}", inline: true },
|
||||
{ name: "✨ Processed", value: "${{ steps.sync.outputs.processed }}", inline: true },
|
||||
{ name: "⏭️ Skipped", value: "${{ steps.sync.outputs.skipped }}", inline: true },
|
||||
{ name: "❌ Errors", value: "${{ steps.sync.outputs.errors }}", inline: true },
|
||||
{ name: "🔗 Run", value: "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", inline: true }
|
||||
],
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Discord
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload"
|
||||
293
MANUAL_TOOLS.md
Normal file
293
MANUAL_TOOLS.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Manual Tools Registry
|
||||
|
||||
## Overview
|
||||
|
||||
This system allows TPMJS to include high-quality tools that don't follow the standard `tpmjs` field specification in their package.json. These tools are manually curated and synced to the database.
|
||||
|
||||
## Why Manual Tools?
|
||||
|
||||
Some excellent tools (like Vercel's code execution, Exa search, Firecrawl, etc.) don't include the `tpmjs` field in their package.json. Rather than wait for these package maintainers to adopt the spec, we manually curate metadata for these tools.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Files
|
||||
|
||||
1. **`manual-tools.ts`** - The registry of manually curated tools
|
||||
2. **`sync-manual-tools.ts`** - Script to sync manual tools to database
|
||||
3. **`MANUAL_TOOLS.md`** - This documentation
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Manual Tool Registry** (`manual-tools.ts`)
|
||||
- Exports a `manualTools` array with metadata for each tool
|
||||
- Each entry includes npm package name, export name, category, description, parameters, etc.
|
||||
- Follows the same schema as the standard `tpmjs` field
|
||||
|
||||
2. **Sync Script** (`sync-manual-tools.ts`)
|
||||
- Fetches latest package metadata from npm
|
||||
- Combines npm metadata with manual metadata
|
||||
- Upserts Package + Tool records to database
|
||||
- Marks tools with `discoveryMethod: 'manual'`
|
||||
|
||||
3. **Database Storage**
|
||||
- Manual tools stored in same `packages` and `tools` tables as auto-discovered tools
|
||||
- No special handling needed in API or frontend
|
||||
- `discoveryMethod: 'manual'` field distinguishes them
|
||||
|
||||
## Adding a New Manual Tool
|
||||
|
||||
### Step 1: Add to Registry
|
||||
|
||||
Edit `manual-tools.ts` and add a new entry:
|
||||
|
||||
```typescript
|
||||
{
|
||||
npmPackageName: 'example-package',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'exampleTool',
|
||||
description: 'A clear, concise description of what this tool does',
|
||||
|
||||
// Optional but recommended for 'rich' tier
|
||||
parameters: [
|
||||
{
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
description: 'The search query',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
returns: {
|
||||
type: 'array',
|
||||
description: 'Array of search results',
|
||||
},
|
||||
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to search for X',
|
||||
limitations: 'Rate limits apply',
|
||||
examples: [
|
||||
'Search for current news',
|
||||
'Find specific information',
|
||||
],
|
||||
},
|
||||
|
||||
// Environment variables
|
||||
env: [
|
||||
{
|
||||
name: 'EXAMPLE_API_KEY',
|
||||
description: 'API key for the service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Additional metadata
|
||||
tags: ['search', 'web'],
|
||||
docsUrl: 'https://example.com/docs',
|
||||
apiKeyUrl: 'https://example.com/api-keys',
|
||||
websiteUrl: 'https://example.com',
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Run Sync Script
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
pnpm tsx sync-manual-tools.ts
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Fetch the package from npm
|
||||
2. Create/update Package record
|
||||
3. Create/update Tool record(s)
|
||||
4. Set `discoveryMethod: 'manual'`
|
||||
|
||||
### Step 3: Verify
|
||||
|
||||
Check that the tool appears on tpmjs.com:
|
||||
|
||||
```bash
|
||||
# Start dev server
|
||||
pnpm dev --filter=@tpmjs/web
|
||||
|
||||
# Visit http://localhost:3000/tool/tool-search
|
||||
# Search for your package name
|
||||
```
|
||||
|
||||
## Multi-Tool Packages
|
||||
|
||||
If a package exports multiple tools, add multiple entries with the same `npmPackageName` but different `exportName`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
exportName: 'scrapeTool',
|
||||
description: 'Scrape websites...',
|
||||
// ...
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
exportName: 'searchTool',
|
||||
description: 'Search the web...',
|
||||
// ...
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
exportName: 'crawlTool',
|
||||
description: 'Crawl entire websites...',
|
||||
// ...
|
||||
},
|
||||
```
|
||||
|
||||
## Tier Calculation
|
||||
|
||||
Tools are automatically assigned a tier:
|
||||
|
||||
- **Rich tier**: Has `parameters` OR `returns` OR `aiAgent` fields
|
||||
- **Minimal tier**: Only has basic metadata
|
||||
|
||||
Rich tier tools get 4x quality score multiplier, so add detailed metadata when possible.
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updating Manual Tools
|
||||
|
||||
1. Edit the entry in `manual-tools.ts`
|
||||
2. Run `pnpm tsx sync-manual-tools.ts`
|
||||
3. The upsert will update existing records
|
||||
|
||||
### Removing Manual Tools
|
||||
|
||||
1. Remove the entry from `manual-tools.ts`
|
||||
2. Manually delete from database OR wait for metrics sync to mark as stale
|
||||
|
||||
### Version Updates
|
||||
|
||||
The sync script automatically fetches the latest version from npm unless you specify `npmVersion` in the manual tool entry.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Option 1: Manual Sync on Deploy
|
||||
|
||||
Add to your deployment workflow:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
- name: Sync manual tools
|
||||
run: pnpm tsx sync-manual-tools.ts
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
### Option 2: Scheduled Sync
|
||||
|
||||
Create a cron job or GitHub Action to sync periodically:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/sync-manual.yml
|
||||
name: Sync Manual Tools
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly on Sunday
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm tsx sync-manual-tools.ts
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
### Option 3: API Endpoint
|
||||
|
||||
Create a sync endpoint (similar to keyword/changes sync):
|
||||
|
||||
```typescript
|
||||
// apps/web/src/app/api/sync/manual/route.ts
|
||||
import { manualTools } from '@/manual-tools';
|
||||
// ... sync logic
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Verify CRON_SECRET
|
||||
// Run manual sync
|
||||
// Return results
|
||||
}
|
||||
```
|
||||
|
||||
## Currently Included Manual Tools
|
||||
|
||||
As of this documentation:
|
||||
|
||||
- **ai-sdk-tool-code-execution** - Vercel Sandbox code execution
|
||||
- **@exalabs/ai-sdk** - Exa web search
|
||||
- **@parallel-web/ai-sdk-tools** - Parallel search and extraction (2 tools)
|
||||
- **ctx-zip** - MCP + Vercel Sandbox integration
|
||||
- **@perplexity-ai/ai-sdk** - Perplexity search
|
||||
- **@tavily/ai-sdk** - Tavily web research
|
||||
- **firecrawl-aisdk** - Firecrawl scraping, search, crawling (3 tools)
|
||||
- **bedrock-agentcore** - AWS Bedrock code interpreter and browser (2 tools)
|
||||
- **@superagent-ai/ai-sdk** - Superagent security tools (3 tools)
|
||||
- **@valyu/ai-sdk** - Valyu domain-specific search tools (8 tools)
|
||||
|
||||
**Total: 24 manually curated tools across 10 packages**
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why not just ask package maintainers to add the tpmjs field?
|
||||
|
||||
We should! But:
|
||||
1. Some packages are from large companies (Vercel, AWS, etc.) with slow adoption cycles
|
||||
2. We want these tools available on TPMJS now
|
||||
3. Manual curation lets us provide better metadata than package authors might
|
||||
|
||||
### Will manual tools be replaced by auto-discovered ones?
|
||||
|
||||
Yes! If a package adds a proper `tpmjs` field, the auto-discovery sync will update it with `discoveryMethod: 'keyword'` or `'changes-feed'`. Manual entries can then be removed from `manual-tools.ts`.
|
||||
|
||||
### Can I mix manual and auto-discovered tools from the same package?
|
||||
|
||||
Yes. If a package has some tools in the `tpmjs` field but is missing others, you can manually add the missing ones. The sync scripts will coexist peacefully.
|
||||
|
||||
### How do I know if a tool is manually curated?
|
||||
|
||||
Check the `discoveryMethod` field in the database:
|
||||
- `'manual'` = Manually curated
|
||||
- `'keyword'` = Auto-discovered via keyword search
|
||||
- `'changes-feed'` = Auto-discovered via npm changes feed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Complete Metadata** - Provide as much metadata as possible for rich tier
|
||||
2. **Accurate Descriptions** - Tool descriptions should be clear and specific
|
||||
3. **AI-Friendly** - Write `aiAgent.useCase` as guidance for LLMs
|
||||
4. **Keep Updated** - Periodically check if packages have added native `tpmjs` support
|
||||
5. **Link to Docs** - Always include `docsUrl` when available
|
||||
6. **API Key URLs** - Include `apiKeyUrl` for tools requiring authentication
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute new manual tools:
|
||||
|
||||
1. Fork the repository
|
||||
2. Add your tool to `manual-tools.ts`
|
||||
3. Test with `pnpm tsx sync-manual-tools.ts`
|
||||
4. Open a pull request with:
|
||||
- Why this tool should be included
|
||||
- Link to the npm package
|
||||
- Screenshot of it working in TPMJS
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [HOW_TO_PUBLISH_A_TOOL.md](./HOW_TO_PUBLISH_A_TOOL.md) - Standard tpmjs field spec
|
||||
- [CLAUDE.md](./CLAUDE.md) - General project documentation
|
||||
- [packages/types/src/tpmjs.ts](./packages/types/src/tpmjs.ts) - TypeScript schema definitions
|
||||
|
|
@ -20,12 +20,12 @@
|
|||
"@tpmjs/utils": "workspace:*",
|
||||
"ai": "6.0.0-beta.124",
|
||||
"firecrawl-aisdk": "^0.7.2",
|
||||
"fish-joke-generator": "^1.1.0",
|
||||
"next": "^16.0.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"openai": "^6.9.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"streamdown": "^1.6.9",
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function ChatInterface(): React.ReactElement {
|
|||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<ChatMessages messages={messages} />
|
||||
<ChatMessages messages={messages} isStreaming={isLoading} />
|
||||
<ChatInput
|
||||
input={input}
|
||||
isLoading={isLoading}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,13 @@ interface Message {
|
|||
|
||||
interface ChatMessagesProps {
|
||||
messages: Message[];
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export function ChatMessages({ messages }: ChatMessagesProps): React.ReactElement {
|
||||
export function ChatMessages({
|
||||
messages,
|
||||
isStreaming = false,
|
||||
}: ChatMessagesProps): React.ReactElement {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
|
|
@ -41,9 +45,17 @@ export function ChatMessages({ messages }: ChatMessagesProps): React.ReactElemen
|
|||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} />
|
||||
))}
|
||||
{messages.map((message, idx) => {
|
||||
// Only animate the last message if it's streaming
|
||||
const isLastMessage = idx === messages.length - 1;
|
||||
return (
|
||||
<MessageBubble
|
||||
key={message.id}
|
||||
message={message}
|
||||
isStreaming={isStreaming && isLastMessage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
|
||||
import { Streamdown } from 'streamdown';
|
||||
|
||||
interface MessagePart {
|
||||
type: string; // Can be 'text', 'tool-{toolName}', 'step-start', etc.
|
||||
|
|
@ -23,9 +24,13 @@ interface Message {
|
|||
|
||||
interface MessageBubbleProps {
|
||||
message: Message;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export function MessageBubble({ message }: MessageBubbleProps): React.ReactElement {
|
||||
export function MessageBubble({
|
||||
message,
|
||||
isStreaming = false,
|
||||
}: MessageBubbleProps): React.ReactElement {
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
// Debug: Log message structure
|
||||
|
|
@ -59,11 +64,13 @@ export function MessageBubble({ message }: MessageBubbleProps): React.ReactEleme
|
|||
return null;
|
||||
}
|
||||
|
||||
// Render text parts
|
||||
// Render text parts with markdown support
|
||||
if (part.type === 'text') {
|
||||
return (
|
||||
<div key={`text-${message.id}-${idx}`} className="whitespace-pre-wrap text-sm">
|
||||
{part.text}
|
||||
<div key={`text-${message.id}-${idx}`} className="text-sm">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>
|
||||
{part.text || ''}
|
||||
</Streamdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -116,7 +123,11 @@ export function MessageBubble({ message }: MessageBubbleProps): React.ReactEleme
|
|||
</div>
|
||||
) : (
|
||||
// Fallback to content if no parts
|
||||
<div className="whitespace-pre-wrap text-sm">{message.content || '...'}</div>
|
||||
<div className="text-sm">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>
|
||||
{message.content || '...'}
|
||||
</Streamdown>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -18,25 +18,14 @@ export const dynamic = 'force-dynamic';
|
|||
export async function GET() {
|
||||
try {
|
||||
// Run all aggregations in parallel
|
||||
const [totalTools, officialTools, categoryStats, recentCount, downloadSum] = await Promise.all([
|
||||
const [totalTools, officialTools, recentCount, packages] = await Promise.all([
|
||||
// Total tools count
|
||||
prisma.tool.count(),
|
||||
|
||||
// Official tools count
|
||||
// Official tools count (isOfficial is at package level)
|
||||
prisma.tool.count({
|
||||
where: { isOfficial: true },
|
||||
}),
|
||||
|
||||
// Group by category
|
||||
prisma.tool.groupBy({
|
||||
by: ['category'],
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
id: 'desc',
|
||||
},
|
||||
where: {
|
||||
package: { isOfficial: true }
|
||||
},
|
||||
}),
|
||||
|
||||
|
|
@ -49,21 +38,31 @@ export async function GET() {
|
|||
},
|
||||
}),
|
||||
|
||||
// Sum of all downloads
|
||||
prisma.tool.aggregate({
|
||||
_sum: {
|
||||
// Get all packages with their tool counts and download stats
|
||||
prisma.package.findMany({
|
||||
select: {
|
||||
category: true,
|
||||
npmDownloadsLastMonth: true,
|
||||
_count: {
|
||||
select: { tools: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Format category stats
|
||||
const categories = categoryStats.reduce<Record<string, number>>((acc, stat) => {
|
||||
if (stat.category) {
|
||||
acc[stat.category] = stat._count.id;
|
||||
// Calculate stats from packages
|
||||
const categories: Record<string, number> = {};
|
||||
let totalDownloads = 0;
|
||||
|
||||
for (const pkg of packages) {
|
||||
// Count tools by category
|
||||
if (pkg.category) {
|
||||
categories[pkg.category] = (categories[pkg.category] || 0) + pkg._count.tools;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Sum downloads
|
||||
totalDownloads += pkg.npmDownloadsLastMonth || 0;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -72,7 +71,7 @@ export async function GET() {
|
|||
officialTools,
|
||||
categories,
|
||||
recentTools: recentCount,
|
||||
totalDownloads: downloadSum._sum.npmDownloadsLastMonth || 0,
|
||||
totalDownloads,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -66,74 +66,119 @@ export async function POST(request: NextRequest) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Validate tpmjs field
|
||||
// Validate tpmjs field (supports both new multi-tool and legacy formats)
|
||||
const validation = validateTpmjsField(pkg.tpmjs);
|
||||
if (!validation.valid || !validation.data) {
|
||||
if (!validation.valid || !validation.packageData || !validation.tools) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Log auto-migration from legacy format
|
||||
if (validation.wasLegacyFormat) {
|
||||
console.log(`Auto-migrated legacy package: ${pkg.name}`);
|
||||
}
|
||||
|
||||
// Extract repository URL and GitHub stars
|
||||
const githubStars: number | null = null;
|
||||
|
||||
// Cast to TpmjsRich to access optional fields (they'll be undefined if not present)
|
||||
const tpmjsData = validation.data as {
|
||||
category: string;
|
||||
description: string;
|
||||
example: string;
|
||||
parameters?: unknown;
|
||||
returns?: unknown;
|
||||
authentication?: unknown;
|
||||
pricing?: unknown;
|
||||
frameworks?: string[];
|
||||
links?: unknown;
|
||||
tags?: string[];
|
||||
status?: string;
|
||||
aiAgent?: unknown;
|
||||
};
|
||||
|
||||
// Prepare data for upsert
|
||||
const toolData = {
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: pkg.publishedAt ? new Date(pkg.publishedAt) : new Date(),
|
||||
npmDescription: pkg.description ?? undefined,
|
||||
npmRepository: pkg.repository ?? undefined,
|
||||
npmHomepage: pkg.homepage ?? undefined,
|
||||
npmLicense: pkg.license ?? undefined,
|
||||
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
|
||||
npmReadme: pkg.readme ?? undefined,
|
||||
npmAuthor: pkg.author ?? undefined,
|
||||
npmMaintainers: pkg.maintainers ?? undefined,
|
||||
category: tpmjsData.category,
|
||||
description: tpmjsData.description,
|
||||
example: tpmjsData.example,
|
||||
parameters: tpmjsData.parameters ?? undefined,
|
||||
returns: tpmjsData.returns ?? undefined,
|
||||
authentication: tpmjsData.authentication ?? undefined,
|
||||
pricing: tpmjsData.pricing ?? undefined,
|
||||
frameworks: tpmjsData.frameworks || [],
|
||||
links: tpmjsData.links ?? undefined,
|
||||
tags: tpmjsData.tags || [],
|
||||
status: tpmjsData.status ?? undefined,
|
||||
aiAgent: tpmjsData.aiAgent ?? undefined,
|
||||
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false,
|
||||
tier: validation.tier || 'minimal',
|
||||
};
|
||||
|
||||
// Upsert tool to database
|
||||
await prisma.tool.upsert({
|
||||
// Upsert Package record
|
||||
const packageRecord = await prisma.package.upsert({
|
||||
where: { npmPackageName: pkg.name },
|
||||
create: {
|
||||
npmPackageName: pkg.name,
|
||||
...toolData,
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: pkg.publishedAt ? new Date(pkg.publishedAt) : new Date(),
|
||||
npmDescription: pkg.description ?? undefined,
|
||||
npmRepository: pkg.repository ?? undefined,
|
||||
npmHomepage: pkg.homepage ?? undefined,
|
||||
npmLicense: pkg.license ?? undefined,
|
||||
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
|
||||
npmReadme: pkg.readme ?? undefined,
|
||||
npmAuthor: pkg.author ?? undefined,
|
||||
npmMaintainers: pkg.maintainers ?? undefined,
|
||||
category: validation.packageData.category,
|
||||
env: validation.packageData.env ?? undefined,
|
||||
frameworks: validation.packageData.frameworks || [],
|
||||
tier: validation.tier || 'minimal',
|
||||
discoveryMethod: 'changes-feed',
|
||||
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false,
|
||||
npmDownloadsLastMonth: 0, // Will be updated by metrics sync
|
||||
githubStars: githubStars,
|
||||
qualityScore: null, // Will be calculated by metrics sync
|
||||
},
|
||||
update: toolData,
|
||||
update: {
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: pkg.publishedAt ? new Date(pkg.publishedAt) : new Date(),
|
||||
npmDescription: pkg.description ?? undefined,
|
||||
npmRepository: pkg.repository ?? undefined,
|
||||
npmHomepage: pkg.homepage ?? undefined,
|
||||
npmLicense: pkg.license ?? undefined,
|
||||
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
|
||||
npmReadme: pkg.readme ?? undefined,
|
||||
npmAuthor: pkg.author ?? undefined,
|
||||
npmMaintainers: pkg.maintainers ?? undefined,
|
||||
category: validation.packageData.category,
|
||||
env: validation.packageData.env ?? undefined,
|
||||
frameworks: validation.packageData.frameworks || [],
|
||||
tier: validation.tier || 'minimal',
|
||||
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false,
|
||||
},
|
||||
});
|
||||
|
||||
// Get existing tools for this package
|
||||
const existingTools = await prisma.tool.findMany({
|
||||
where: { packageId: packageRecord.id },
|
||||
});
|
||||
|
||||
// Upsert each tool in the tools array
|
||||
for (const toolDef of validation.tools) {
|
||||
await prisma.tool.upsert({
|
||||
where: {
|
||||
packageId_exportName: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: toolDef.exportName,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: toolDef.exportName,
|
||||
description: toolDef.description,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: toolDef.parameters ? (toolDef.parameters as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
returns: toolDef.returns ? (toolDef.returns as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
qualityScore: null, // Will be calculated by metrics sync
|
||||
},
|
||||
update: {
|
||||
description: toolDef.description,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: toolDef.parameters ? (toolDef.parameters as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
returns: toolDef.returns ? (toolDef.returns as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete orphaned tools (tools removed from package.json)
|
||||
const orphanedTools = existingTools.filter(
|
||||
(existingTool) =>
|
||||
!validation.tools?.some((toolDef) => toolDef.exportName === existingTool.exportName)
|
||||
);
|
||||
|
||||
if (orphanedTools.length > 0) {
|
||||
await prisma.tool.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedTools.map((t) => t.id) },
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`Deleted ${orphanedTools.length} orphaned tools from package: ${pkg.name}`
|
||||
);
|
||||
}
|
||||
|
||||
processed++;
|
||||
} catch (error) {
|
||||
errors++;
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ export async function POST(request: NextRequest) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Validate tpmjs field
|
||||
// Validate tpmjs field (supports both new multi-tool and legacy formats)
|
||||
const validation = validateTpmjsField(pkg.tpmjs);
|
||||
if (!validation.valid || !validation.data) {
|
||||
if (!validation.valid || !validation.packageData || !validation.tools) {
|
||||
skipped++;
|
||||
skippedPackages.push({
|
||||
name: pkg.name,
|
||||
|
|
@ -87,67 +87,112 @@ export async function POST(request: NextRequest) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Log auto-migration from legacy format
|
||||
if (validation.wasLegacyFormat) {
|
||||
console.log(`Auto-migrated legacy package: ${pkg.name}`);
|
||||
}
|
||||
|
||||
// Extract repository URL and GitHub stars
|
||||
const githubStars: number | null = null;
|
||||
|
||||
// Cast to TpmjsRich to access optional fields (they'll be undefined if not present)
|
||||
const tpmjsData = validation.data as {
|
||||
category: string;
|
||||
description: string;
|
||||
example: string;
|
||||
parameters?: unknown;
|
||||
returns?: unknown;
|
||||
authentication?: unknown;
|
||||
pricing?: unknown;
|
||||
frameworks?: string[];
|
||||
links?: unknown;
|
||||
tags?: string[];
|
||||
status?: string;
|
||||
aiAgent?: unknown;
|
||||
};
|
||||
|
||||
// Prepare data for upsert
|
||||
const toolData = {
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: pkg.publishedAt ? new Date(pkg.publishedAt) : new Date(),
|
||||
npmDescription: pkg.description ?? undefined,
|
||||
npmRepository: pkg.repository ?? undefined,
|
||||
npmHomepage: pkg.homepage ?? undefined,
|
||||
npmLicense: pkg.license ?? undefined,
|
||||
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
|
||||
npmReadme: pkg.readme ?? undefined,
|
||||
npmAuthor: pkg.author ?? undefined,
|
||||
npmMaintainers: pkg.maintainers ?? undefined,
|
||||
category: tpmjsData.category,
|
||||
description: tpmjsData.description,
|
||||
example: tpmjsData.example,
|
||||
parameters: tpmjsData.parameters ?? undefined,
|
||||
returns: tpmjsData.returns ?? undefined,
|
||||
authentication: tpmjsData.authentication ?? undefined,
|
||||
pricing: tpmjsData.pricing ?? undefined,
|
||||
frameworks: tpmjsData.frameworks || [],
|
||||
links: tpmjsData.links ?? undefined,
|
||||
tags: tpmjsData.tags || [],
|
||||
status: tpmjsData.status ?? undefined,
|
||||
aiAgent: tpmjsData.aiAgent ?? undefined,
|
||||
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false,
|
||||
tier: validation.tier || 'minimal',
|
||||
};
|
||||
|
||||
// Upsert tool to database
|
||||
await prisma.tool.upsert({
|
||||
// Upsert Package record
|
||||
const packageRecord = await prisma.package.upsert({
|
||||
where: { npmPackageName: pkg.name },
|
||||
create: {
|
||||
npmPackageName: pkg.name,
|
||||
...toolData,
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: pkg.publishedAt ? new Date(pkg.publishedAt) : new Date(),
|
||||
npmDescription: pkg.description ?? undefined,
|
||||
npmRepository: pkg.repository ?? undefined,
|
||||
npmHomepage: pkg.homepage ?? undefined,
|
||||
npmLicense: pkg.license ?? undefined,
|
||||
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
|
||||
npmReadme: pkg.readme ?? undefined,
|
||||
npmAuthor: pkg.author ?? undefined,
|
||||
npmMaintainers: pkg.maintainers ?? undefined,
|
||||
category: validation.packageData.category,
|
||||
env: validation.packageData.env ?? undefined,
|
||||
frameworks: validation.packageData.frameworks || [],
|
||||
tier: validation.tier || 'minimal',
|
||||
discoveryMethod: 'keyword',
|
||||
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false,
|
||||
npmDownloadsLastMonth: 0, // Will be updated by metrics sync
|
||||
githubStars: githubStars,
|
||||
qualityScore: null, // Will be calculated by metrics sync
|
||||
},
|
||||
update: toolData,
|
||||
update: {
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: pkg.publishedAt ? new Date(pkg.publishedAt) : new Date(),
|
||||
npmDescription: pkg.description ?? undefined,
|
||||
npmRepository: pkg.repository ?? undefined,
|
||||
npmHomepage: pkg.homepage ?? undefined,
|
||||
npmLicense: pkg.license ?? undefined,
|
||||
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
|
||||
npmReadme: pkg.readme ?? undefined,
|
||||
npmAuthor: pkg.author ?? undefined,
|
||||
npmMaintainers: pkg.maintainers ?? undefined,
|
||||
category: validation.packageData.category,
|
||||
env: validation.packageData.env ?? undefined,
|
||||
frameworks: validation.packageData.frameworks || [],
|
||||
tier: validation.tier || 'minimal',
|
||||
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false,
|
||||
},
|
||||
});
|
||||
|
||||
// Get existing tools for this package
|
||||
const existingTools = await prisma.tool.findMany({
|
||||
where: { packageId: packageRecord.id },
|
||||
});
|
||||
|
||||
// Upsert each tool in the tools array
|
||||
for (const toolDef of validation.tools) {
|
||||
await prisma.tool.upsert({
|
||||
where: {
|
||||
packageId_exportName: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: toolDef.exportName,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: toolDef.exportName,
|
||||
description: toolDef.description,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: toolDef.parameters ? (toolDef.parameters as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
returns: toolDef.returns ? (toolDef.returns as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
qualityScore: null, // Will be calculated by metrics sync
|
||||
},
|
||||
update: {
|
||||
description: toolDef.description,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: toolDef.parameters ? (toolDef.parameters as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
returns: toolDef.returns ? (toolDef.returns as any) : undefined,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete orphaned tools (tools removed from package.json)
|
||||
const orphanedTools = existingTools.filter(
|
||||
(existingTool) =>
|
||||
!validation.tools?.some((toolDef) => toolDef.exportName === existingTool.exportName)
|
||||
);
|
||||
|
||||
if (orphanedTools.length > 0) {
|
||||
await prisma.tool.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedTools.map((t) => t.id) },
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`Deleted ${orphanedTools.length} orphaned tools from package: ${pkg.name}`
|
||||
);
|
||||
}
|
||||
|
||||
processed++;
|
||||
} catch (error) {
|
||||
errors++;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const maxDuration = 300; // 5 minutes max for cron jobs
|
|||
|
||||
/**
|
||||
* POST /api/sync/metrics
|
||||
* Update download stats and quality scores for all tools
|
||||
* Update download stats and quality scores for all packages and tools
|
||||
*
|
||||
* This endpoint is called by Vercel Cron (every hour)
|
||||
* Requires Authorization: Bearer <CRON_SECRET>
|
||||
|
|
@ -31,43 +31,51 @@ export async function POST(request: NextRequest) {
|
|||
const errorMessages: string[] = [];
|
||||
|
||||
try {
|
||||
// Get all tools from database
|
||||
const tools = await prisma.tool.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
npmPackageName: true,
|
||||
tier: true,
|
||||
npmDownloadsLastMonth: true,
|
||||
githubStars: true,
|
||||
// Get all packages with their tools from database
|
||||
const packages = await prisma.package.findMany({
|
||||
include: {
|
||||
tools: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Process each tool
|
||||
for (const tool of tools) {
|
||||
// Process each package
|
||||
for (const pkg of packages) {
|
||||
try {
|
||||
// Fetch download stats from NPM
|
||||
const downloads = await fetchDownloadStats(tool.npmPackageName);
|
||||
// Fetch download stats from NPM (package-level metric)
|
||||
const downloads = await fetchDownloadStats(pkg.npmPackageName);
|
||||
|
||||
// Calculate quality score (0.00 to 1.00)
|
||||
const qualityScore = calculateQualityScore({
|
||||
tier: tool.tier,
|
||||
downloads,
|
||||
githubStars: tool.githubStars || 0,
|
||||
});
|
||||
|
||||
// Update tool metrics
|
||||
await prisma.tool.update({
|
||||
where: { id: tool.id },
|
||||
// Update package metrics
|
||||
await prisma.package.update({
|
||||
where: { id: pkg.id },
|
||||
data: {
|
||||
npmDownloadsLastMonth: downloads,
|
||||
qualityScore,
|
||||
// githubStars would be updated here if we had GitHub API integration
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate and update quality score for each tool in this package
|
||||
for (const tool of pkg.tools) {
|
||||
const qualityScore = calculateQualityScore({
|
||||
tier: pkg.tier, // Tier is at package level
|
||||
downloads, // Package downloads
|
||||
githubStars: pkg.githubStars || 0, // Package stars
|
||||
hasParameters: !!tool.parameters,
|
||||
hasReturns: !!tool.returns,
|
||||
hasAiAgent: !!tool.aiAgent,
|
||||
});
|
||||
|
||||
await prisma.tool.update({
|
||||
where: { id: tool.id },
|
||||
data: {
|
||||
qualityScore,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
processed++;
|
||||
} catch (error) {
|
||||
errors++;
|
||||
const errorMsg = `Failed to process ${tool.npmPackageName}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
const errorMsg = `Failed to process ${pkg.npmPackageName}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
errorMessages.push(errorMsg);
|
||||
console.error(errorMsg);
|
||||
}
|
||||
|
|
@ -80,13 +88,15 @@ export async function POST(request: NextRequest) {
|
|||
source: 'metrics',
|
||||
checkpoint: {
|
||||
lastRun: new Date().toISOString(),
|
||||
totalTools: tools.length,
|
||||
totalPackages: packages.length,
|
||||
totalTools: packages.reduce((sum, pkg) => sum + pkg.tools.length, 0),
|
||||
},
|
||||
},
|
||||
update: {
|
||||
checkpoint: {
|
||||
lastRun: new Date().toISOString(),
|
||||
totalTools: tools.length,
|
||||
totalPackages: packages.length,
|
||||
totalTools: packages.reduce((sum, pkg) => sum + pkg.tools.length, 0),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -102,10 +112,11 @@ export async function POST(request: NextRequest) {
|
|||
message:
|
||||
errors > 0
|
||||
? `Processed with errors: ${errorMessages.slice(0, 3).join('; ')}`
|
||||
: `Successfully updated metrics for ${processed} tools`,
|
||||
: `Successfully updated metrics for ${processed} packages`,
|
||||
metadata: {
|
||||
durationMs: Date.now() - startTime,
|
||||
totalTools: tools.length,
|
||||
totalPackages: packages.length,
|
||||
totalTools: packages.reduce((sum, pkg) => sum + pkg.tools.length, 0),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -116,7 +127,8 @@ export async function POST(request: NextRequest) {
|
|||
processed,
|
||||
skipped,
|
||||
errors,
|
||||
totalTools: tools.length,
|
||||
totalPackages: packages.length,
|
||||
totalTools: packages.reduce((sum, pkg) => sum + pkg.tools.length, 0),
|
||||
durationMs: Date.now() - startTime,
|
||||
},
|
||||
});
|
||||
|
|
@ -152,25 +164,40 @@ export async function POST(request: NextRequest) {
|
|||
/**
|
||||
* Calculate quality score based on multiple factors
|
||||
* Returns a value between 0.00 and 1.00
|
||||
*
|
||||
* Score components:
|
||||
* - Tier (0.4 minimal, 0.6 rich)
|
||||
* - Downloads (logarithmic, max 0.2)
|
||||
* - GitHub stars (logarithmic, max 0.1)
|
||||
* - Tool metadata richness (0.1 for each: parameters, returns, aiAgent)
|
||||
*/
|
||||
function calculateQualityScore(params: {
|
||||
tier: string;
|
||||
downloads: number;
|
||||
githubStars: number;
|
||||
hasParameters: boolean;
|
||||
hasReturns: boolean;
|
||||
hasAiAgent: boolean;
|
||||
}): number {
|
||||
const { tier, downloads, githubStars } = params;
|
||||
const { tier, downloads, githubStars, hasParameters, hasReturns, hasAiAgent } = params;
|
||||
|
||||
// Base score from tier
|
||||
const tierScore = tier === 'rich' ? 0.6 : 0.4;
|
||||
|
||||
// Downloads score (logarithmic scale, max 0.3)
|
||||
const downloadsScore = Math.min(0.3, Math.log10(downloads + 1) / 10);
|
||||
// Downloads score (logarithmic scale, max 0.2)
|
||||
const downloadsScore = Math.min(0.2, Math.log10(downloads + 1) / 15);
|
||||
|
||||
// GitHub stars score (logarithmic scale, max 0.1)
|
||||
const starsScore = Math.min(0.1, Math.log10(githubStars + 1) / 10);
|
||||
|
||||
// Tool metadata richness score (max 0.1)
|
||||
let richnessScore = 0;
|
||||
if (hasParameters) richnessScore += 0.04;
|
||||
if (hasReturns) richnessScore += 0.03;
|
||||
if (hasAiAgent) richnessScore += 0.03;
|
||||
|
||||
// Total score (capped at 1.00)
|
||||
const totalScore = Math.min(1.0, tierScore + downloadsScore + starsScore);
|
||||
const totalScore = Math.min(1.0, tierScore + downloadsScore + starsScore + richnessScore);
|
||||
|
||||
// Round to 2 decimal places
|
||||
return Math.round(totalScore * 100) / 100;
|
||||
|
|
|
|||
|
|
@ -18,31 +18,83 @@ export async function GET(
|
|||
try {
|
||||
const { slug } = await params;
|
||||
|
||||
// Join slug array to reconstruct package name (e.g., ['@tpmjs', 'text-transformer'] -> '@tpmjs/text-transformer')
|
||||
const packageName = slug.join('/');
|
||||
// Slug can be:
|
||||
// - ['@scope', 'package'] -> Get all tools for @scope/package
|
||||
// - ['@scope', 'package', 'exportName'] -> Get specific tool @scope/package/exportName
|
||||
// - ['package'] -> Get all tools for package
|
||||
// - ['package', 'exportName'] -> Get specific tool package/exportName
|
||||
|
||||
// Find the tool by npmPackageName
|
||||
const tool = await prisma.tool.findUnique({
|
||||
where: {
|
||||
npmPackageName: packageName,
|
||||
},
|
||||
});
|
||||
let packageName: string;
|
||||
let exportName: string | undefined;
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
if (slug.length === 1) {
|
||||
// Single slug - package name without scope
|
||||
packageName = slug[0] || '';
|
||||
} else if (slug.length === 2) {
|
||||
// Could be: @scope/package OR package/exportName
|
||||
if (slug[0]?.startsWith('@')) {
|
||||
// @scope/package
|
||||
packageName = slug.join('/');
|
||||
} else {
|
||||
// package + exportName
|
||||
packageName = slug[0] || '';
|
||||
exportName = slug[1];
|
||||
}
|
||||
} else {
|
||||
// 3+ slugs: @scope/package/exportName
|
||||
packageName = slug.slice(0, slug[0]?.startsWith('@') ? 2 : 1).join('/');
|
||||
exportName = slug[slug.length - 1];
|
||||
}
|
||||
|
||||
// Return the tool data
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: tool,
|
||||
});
|
||||
if (exportName) {
|
||||
// Find specific tool by package name and export name
|
||||
const tool = await prisma.tool.findFirst({
|
||||
where: {
|
||||
package: { npmPackageName: packageName },
|
||||
exportName: exportName,
|
||||
},
|
||||
include: { package: true },
|
||||
});
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: tool,
|
||||
});
|
||||
} else {
|
||||
// Find all tools for the package
|
||||
const pkg = await prisma.package.findUnique({
|
||||
where: { npmPackageName: packageName },
|
||||
include: { tools: true },
|
||||
});
|
||||
|
||||
if (!pkg) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Package not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
package: pkg,
|
||||
tools: pkg.tools,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tool:', error);
|
||||
return NextResponse.json(
|
||||
|
|
|
|||
|
|
@ -18,15 +18,19 @@ interface ExecuteRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* POST /api/tools/[...slug]/execute
|
||||
* POST /api/tools/execute/[...slug]
|
||||
* Executes a tool with an AI agent and streams the response via SSE
|
||||
*
|
||||
* Slug format: [toolId] or [packageName, exportName]
|
||||
* Examples:
|
||||
* /api/tools/execute/clx123abc (by tool ID)
|
||||
* /api/tools/execute/@tpmjs/hello/helloWorldTool (by package and export name)
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string[] }> }
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const packageName = decodeURIComponent(slug.join('/'));
|
||||
|
||||
try {
|
||||
// Parse request body
|
||||
|
|
@ -63,10 +67,29 @@ export async function POST(
|
|||
);
|
||||
}
|
||||
|
||||
// Fetch tool from database
|
||||
const tool = await prisma.tool.findUnique({
|
||||
where: { npmPackageName: packageName },
|
||||
});
|
||||
// Fetch tool from database with package relation
|
||||
// Support both ID-based lookup and packageName/exportName lookup
|
||||
let tool;
|
||||
|
||||
if (slug.length === 1) {
|
||||
// Single slug - treat as tool ID
|
||||
tool = await prisma.tool.findUnique({
|
||||
where: { id: slug[0] || '' },
|
||||
include: { package: true },
|
||||
});
|
||||
} else {
|
||||
// Multiple slugs - treat as packageName/exportName
|
||||
const packageName = decodeURIComponent(slug.slice(0, -1).join('/'));
|
||||
const exportName = decodeURIComponent(slug[slug.length - 1] || '');
|
||||
|
||||
tool = await prisma.tool.findFirst({
|
||||
where: {
|
||||
package: { npmPackageName: packageName },
|
||||
exportName: exportName,
|
||||
},
|
||||
include: { package: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json({ error: 'Tool not found' }, { status: 404 });
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ export const maxDuration = 60;
|
|||
* Search and list tools with filtering, sorting, and pagination
|
||||
*
|
||||
* Query params:
|
||||
* - q: Search query (searches name, description, tags)
|
||||
* - q: Search query (searches package name, tool description)
|
||||
* - category: Filter by category
|
||||
* - official: Filter by official status (true/false)
|
||||
* - limit: Results per page (default: 20, max: 100)
|
||||
* - limit: Results per page (default: 20, max: 50)
|
||||
* - offset: Pagination offset (default: 0)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
|
|
@ -27,44 +27,52 @@ export async function GET(request: NextRequest) {
|
|||
const limitParam = searchParams.get('limit');
|
||||
const offsetParam = searchParams.get('offset');
|
||||
|
||||
// Validate and set defaults (reduced max from 100 to 50 for faster queries)
|
||||
// Validate and set defaults
|
||||
const limit = Math.min(
|
||||
Number.parseInt(limitParam || '20', 10),
|
||||
50 // Reduced from 100 for better performance
|
||||
50 // Max 50 for better performance
|
||||
);
|
||||
const offset = Math.max(Number.parseInt(offsetParam || '0', 10), 0);
|
||||
|
||||
// Build where clause
|
||||
// Build where clause for Tool table
|
||||
const where: Prisma.ToolWhereInput = {};
|
||||
|
||||
// Search filter (case-insensitive partial match)
|
||||
// Build package filter separately
|
||||
const packageFilter: Prisma.PackageWhereInput = {};
|
||||
|
||||
// Category filter (category is at package level)
|
||||
if (category) {
|
||||
packageFilter.category = category;
|
||||
}
|
||||
|
||||
// Official filter (isOfficial is at package level)
|
||||
if (officialParam !== null) {
|
||||
packageFilter.isOfficial = officialParam === 'true';
|
||||
}
|
||||
|
||||
// Search filter (searches tool description and package name)
|
||||
if (query) {
|
||||
where.OR = [
|
||||
{ npmPackageName: { contains: query, mode: 'insensitive' } },
|
||||
{ description: { contains: query, mode: 'insensitive' } },
|
||||
{
|
||||
tags: {
|
||||
hasSome: [query],
|
||||
},
|
||||
},
|
||||
{ package: { npmPackageName: { contains: query, mode: 'insensitive' }, ...packageFilter } },
|
||||
];
|
||||
} else if (Object.keys(packageFilter).length > 0) {
|
||||
// Apply package filter if no search query
|
||||
where.package = packageFilter;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
// Official filter
|
||||
if (officialParam !== null) {
|
||||
where.isOfficial = officialParam === 'true';
|
||||
}
|
||||
|
||||
// Execute queries - run count separately only if needed for pagination
|
||||
// For first page, we can skip count if we don't need total pages
|
||||
// Execute query - fetch tools with package relation
|
||||
// We fetch limit+1 to check if there are more results (avoid expensive count)
|
||||
const tools = await prisma.tool.findMany({
|
||||
where,
|
||||
orderBy: [{ qualityScore: 'desc' }, { npmDownloadsLastMonth: 'desc' }, { createdAt: 'desc' }],
|
||||
include: {
|
||||
package: true, // Include package data for each tool
|
||||
},
|
||||
orderBy: [
|
||||
{ qualityScore: 'desc' }, // Tool quality score
|
||||
{ package: { npmDownloadsLastMonth: 'desc' } }, // Package downloads
|
||||
{ createdAt: 'desc' }, // Tool creation time
|
||||
],
|
||||
take: limit + 1, // Fetch one extra to check if there are more
|
||||
skip: offset,
|
||||
});
|
||||
|
|
@ -81,7 +89,6 @@ export async function GET(request: NextRequest) {
|
|||
offset,
|
||||
hasMore,
|
||||
// Note: total count omitted for performance (can be expensive)
|
||||
// Only return count if explicitly requested
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -7,22 +7,41 @@ import { prisma } from '@tpmjs/db';
|
|||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* GET /api/tools/[...slug]/simulations
|
||||
* GET /api/tools/simulations/[...slug]
|
||||
* Returns the last 10 simulations for a tool
|
||||
*
|
||||
* Slug can be:
|
||||
* - Tool ID (single slug)
|
||||
* - Package name + export name (multiple slugs)
|
||||
*/
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string[] }> }
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const packageName = decodeURIComponent(slug.join('/'));
|
||||
|
||||
try {
|
||||
// Fetch tool
|
||||
const tool = await prisma.tool.findUnique({
|
||||
where: { npmPackageName: packageName },
|
||||
select: { id: true },
|
||||
});
|
||||
let tool;
|
||||
|
||||
if (slug.length === 1) {
|
||||
// Single slug - treat as tool ID
|
||||
tool = await prisma.tool.findUnique({
|
||||
where: { id: slug[0] || '' },
|
||||
select: { id: true },
|
||||
});
|
||||
} else {
|
||||
// Multiple slugs - treat as packageName/exportName
|
||||
const packageName = decodeURIComponent(slug.slice(0, -1).join('/'));
|
||||
const exportName = decodeURIComponent(slug[slug.length - 1] || '');
|
||||
|
||||
tool = await prisma.tool.findFirst({
|
||||
where: {
|
||||
package: { npmPackageName: packageName },
|
||||
exportName: exportName,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json({ error: 'Tool not found' }, { status: 404 });
|
||||
|
|
|
|||
|
|
@ -20,24 +20,33 @@ async function getHomePageData() {
|
|||
|
||||
// Top 6 featured tools by quality score
|
||||
prisma.tool.findMany({
|
||||
orderBy: [{ qualityScore: 'desc' }, { npmDownloadsLastMonth: 'desc' }],
|
||||
orderBy: [
|
||||
{ qualityScore: 'desc' },
|
||||
{ package: { npmDownloadsLastMonth: 'desc' } },
|
||||
],
|
||||
take: 6,
|
||||
select: {
|
||||
id: true,
|
||||
npmPackageName: true,
|
||||
exportName: true,
|
||||
description: true,
|
||||
category: true,
|
||||
tags: true,
|
||||
qualityScore: true,
|
||||
npmDownloadsLastMonth: true,
|
||||
isOfficial: true,
|
||||
package: {
|
||||
select: {
|
||||
npmPackageName: true,
|
||||
category: true,
|
||||
npmDownloadsLastMonth: true,
|
||||
isOfficial: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// Category distribution for stats
|
||||
prisma.tool.groupBy({
|
||||
// Category distribution for stats (group by package category)
|
||||
prisma.package.groupBy({
|
||||
by: ['category'],
|
||||
_count: true,
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
@ -70,7 +79,7 @@ async function getHomePageData() {
|
|||
featuredTools,
|
||||
categories: categoryStats.slice(0, 5).map((c) => ({
|
||||
name: c.category,
|
||||
count: c._count,
|
||||
count: c._count._all,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
@ -113,13 +122,20 @@ export default async function HomePage(): Promise<React.ReactElement> {
|
|||
{data.featuredTools.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||
{data.featuredTools.map((tool) => (
|
||||
<Link key={tool.id} href={`/tool/${tool.npmPackageName}`} className="group">
|
||||
<Link
|
||||
key={tool.id}
|
||||
href={`/tool/${tool.package.npmPackageName}/${tool.exportName}`}
|
||||
className="group"
|
||||
>
|
||||
<div className="p-6 border border-border rounded-lg bg-surface hover:border-foreground transition-colors h-full flex flex-col">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-foreground group-hover:text-brutalist-accent transition-colors">
|
||||
{tool.npmPackageName}
|
||||
{tool.package.npmPackageName}
|
||||
<span className="text-xs text-foreground-tertiary ml-2">
|
||||
({tool.exportName})
|
||||
</span>
|
||||
</h3>
|
||||
{tool.isOfficial && (
|
||||
{tool.package.isOfficial && (
|
||||
<Badge variant="default" size="sm">
|
||||
Official
|
||||
</Badge>
|
||||
|
|
@ -132,13 +148,8 @@ export default async function HomePage(): Promise<React.ReactElement> {
|
|||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline" size="sm">
|
||||
{tool.category}
|
||||
{tool.package.category}
|
||||
</Badge>
|
||||
{tool.tags.slice(0, 2).map((tag) => (
|
||||
<Badge key={tag} variant="secondary" size="sm">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-border flex items-center justify-between text-xs text-foreground-tertiary">
|
||||
|
|
@ -147,7 +158,7 @@ export default async function HomePage(): Promise<React.ReactElement> {
|
|||
{tool.qualityScore ? Number(tool.qualityScore).toFixed(2) : 'N/A'}
|
||||
</span>
|
||||
<span>
|
||||
{tool.npmDownloadsLastMonth?.toLocaleString() || '0'} downloads/mo
|
||||
{tool.package.npmDownloadsLastMonth?.toLocaleString() || '0'} downloads/mo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,13 @@ export default function PublishPage(): React.ReactElement {
|
|||
code={`{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "A concise description of what your tool does"
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"exportName": "myTool",
|
||||
"description": "A concise description of what your tool does"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -118,26 +124,32 @@ export default function PublishPage(): React.ReactElement {
|
|||
code={`{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Analyzes sentiment in text",
|
||||
"parameters": [
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "The text to analyze",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Language code (e.g., 'en')",
|
||||
"required": false,
|
||||
"default": "en"
|
||||
"exportName": "sentimentAnalysisTool",
|
||||
"description": "Analyzes sentiment in text",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "The text to analyze",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Language code (e.g., 'en')",
|
||||
"required": false,
|
||||
"default": "en"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "SentimentResult",
|
||||
"description": "Object with score and label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "SentimentResult",
|
||||
"description": "Object with score and label"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -159,9 +171,7 @@ export default function PublishPage(): React.ReactElement {
|
|||
code={`{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Advanced sentiment analysis with emotion detection",
|
||||
"parameters": [...],
|
||||
"returns": {...},
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"env": [
|
||||
{
|
||||
"name": "SENTIMENT_API_KEY",
|
||||
|
|
@ -169,15 +179,22 @@ export default function PublishPage(): React.ReactElement {
|
|||
"required": true
|
||||
}
|
||||
],
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"aiAgent": {
|
||||
"useCase": "Use when users need to analyze sentiment or detect emotions",
|
||||
"limitations": "English and Spanish only. Max 10,000 characters",
|
||||
"examples": [
|
||||
"Analyze customer review sentiment",
|
||||
"Detect emotions in feedback"
|
||||
]
|
||||
}
|
||||
"tools": [
|
||||
{
|
||||
"exportName": "sentimentAnalysisTool",
|
||||
"description": "Advanced sentiment analysis with emotion detection",
|
||||
"parameters": [...],
|
||||
"returns": {...},
|
||||
"aiAgent": {
|
||||
"useCase": "Use when users need to analyze sentiment or detect emotions",
|
||||
"limitations": "English and Spanish only. Max 10,000 characters",
|
||||
"examples": [
|
||||
"Analyze customer review sentiment",
|
||||
"Detect emotions in feedback"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -279,26 +296,31 @@ npm publish --access public
|
|||
"keywords": ["tpmjs-tool", "blog", "content"],
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Creates structured blog posts with frontmatter and SEO metadata",
|
||||
"parameters": [
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"description": "The title of the blog post",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"description": "The main content",
|
||||
"required": true
|
||||
"exportName": "createBlogPostTool",
|
||||
"description": "Creates structured blog posts with frontmatter and SEO metadata",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"description": "The title of the blog post",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"description": "The main content",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "BlogPost",
|
||||
"description": "Structured blog post with frontmatter"
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "BlogPost",
|
||||
"description": "Structured blog post with frontmatter"
|
||||
},
|
||||
"frameworks": ["vercel-ai", "langchain"]
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -163,7 +163,13 @@ export default function SpecPage(): React.ReactElement {
|
|||
code={`{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Analyzes sentiment in text and returns positive/negative/neutral classification"
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"exportName": "sentimentAnalysisTool",
|
||||
"description": "Analyzes sentiment in text and returns positive/negative/neutral classification"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -237,26 +243,32 @@ export default function SpecPage(): React.ReactElement {
|
|||
code={`{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Analyzes sentiment in text",
|
||||
"parameters": [
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "The text to analyze",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Language code (e.g., 'en', 'es')",
|
||||
"required": false,
|
||||
"default": "en"
|
||||
"exportName": "sentimentAnalysisTool",
|
||||
"description": "Analyzes sentiment in text",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "The text to analyze",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Language code (e.g., 'en', 'es')",
|
||||
"required": false,
|
||||
"default": "en"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "SentimentResult",
|
||||
"description": "Object with score (-1 to 1) and label (positive/negative/neutral)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "SentimentResult",
|
||||
"description": "Object with score (-1 to 1) and label (positive/negative/neutral)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -361,9 +373,7 @@ export default function SpecPage(): React.ReactElement {
|
|||
code={`{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Advanced sentiment analysis with emotion detection",
|
||||
"parameters": [...],
|
||||
"returns": {...},
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"env": [
|
||||
{
|
||||
"name": "SENTIMENT_API_KEY",
|
||||
|
|
@ -371,15 +381,22 @@ export default function SpecPage(): React.ReactElement {
|
|||
"required": true
|
||||
}
|
||||
],
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"aiAgent": {
|
||||
"useCase": "Use when users need to analyze sentiment or detect emotions in text",
|
||||
"limitations": "English and Spanish only. Max 10,000 characters per request.",
|
||||
"examples": [
|
||||
"Analyze customer review sentiment",
|
||||
"Detect emotions in user feedback"
|
||||
]
|
||||
}
|
||||
"tools": [
|
||||
{
|
||||
"exportName": "sentimentAnalysisTool",
|
||||
"description": "Advanced sentiment analysis with emotion detection",
|
||||
"parameters": [...],
|
||||
"returns": {...},
|
||||
"aiAgent": {
|
||||
"useCase": "Use when users need to analyze sentiment or detect emotions in text",
|
||||
"limitations": "English and Spanish only. Max 10,000 characters per request.",
|
||||
"examples": [
|
||||
"Analyze customer review sentiment",
|
||||
"Detect emotions in user feedback"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,15 +23,17 @@ import { AppHeader } from '~/components/AppHeader';
|
|||
|
||||
interface Tool {
|
||||
id: string;
|
||||
npmPackageName: string;
|
||||
npmVersion: string;
|
||||
exportName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
npmRepository: { url: string; type: string } | null;
|
||||
qualityScore: string;
|
||||
isOfficial: boolean;
|
||||
npmDownloadsLastMonth: number;
|
||||
package: {
|
||||
npmPackageName: string;
|
||||
npmVersion: string;
|
||||
category: string;
|
||||
npmRepository: { url: string; type: string } | null;
|
||||
isOfficial: boolean;
|
||||
npmDownloadsLastMonth: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,12 +45,10 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
|
||||
const [availableTags, setAvailableTags] = useState<string[]>([]);
|
||||
|
||||
// Fetch tools from API
|
||||
useEffect(() => {
|
||||
|
|
@ -77,21 +77,16 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
setTools(fetchedTools);
|
||||
setError(null);
|
||||
|
||||
// Extract unique categories and tags from all tools
|
||||
// Extract unique categories from all tools
|
||||
const categories = new Set<string>();
|
||||
const tags = new Set<string>();
|
||||
|
||||
for (const tool of fetchedTools) {
|
||||
if (tool.category) {
|
||||
categories.add(tool.category);
|
||||
}
|
||||
for (const tag of tool.tags) {
|
||||
tags.add(tag);
|
||||
if (tool.package.category) {
|
||||
categories.add(tool.package.category);
|
||||
}
|
||||
}
|
||||
|
||||
setAvailableCategories(Array.from(categories).sort());
|
||||
setAvailableTags(Array.from(tags).sort());
|
||||
} else {
|
||||
setError(data.error || 'Failed to fetch tools');
|
||||
}
|
||||
|
|
@ -105,12 +100,6 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
fetchTools();
|
||||
}, [searchQuery, activeTab, categoryFilter]);
|
||||
|
||||
// Filter tools by selected tags (client-side)
|
||||
const displayedTools =
|
||||
selectedTags.length > 0
|
||||
? tools.filter((tool) => selectedTags.some((tag) => tool.tags.includes(tag)))
|
||||
: tools;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<AppHeader />
|
||||
|
|
@ -154,43 +143,18 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
</div>
|
||||
|
||||
{/* Clear filters button */}
|
||||
{(categoryFilter !== 'all' || selectedTags.length > 0) && (
|
||||
{categoryFilter !== 'all' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCategoryFilter('all');
|
||||
setSelectedTags([]);
|
||||
}}
|
||||
>
|
||||
Clear Filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Popular tags */}
|
||||
{availableTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-sm font-medium text-foreground-secondary mr-2">
|
||||
Filter by tag:
|
||||
</span>
|
||||
{availableTags.slice(0, 10).map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="cursor-pointer hover:bg-foreground/10 transition-colors"
|
||||
onClick={() => {
|
||||
setSelectedTags((prev) =>
|
||||
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
|
||||
);
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
|
|
@ -200,7 +164,7 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
{
|
||||
id: 'featured',
|
||||
label: 'Official',
|
||||
count: tools.filter((t) => t.isOfficial).length,
|
||||
count: tools.filter((t) => t.package.isOfficial).length,
|
||||
},
|
||||
]}
|
||||
activeTab={activeTab}
|
||||
|
|
@ -220,15 +184,22 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
{/* Tool grid */}
|
||||
{!loading && !error && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{displayedTools.length > 0 ? (
|
||||
displayedTools.map((tool) => (
|
||||
{tools.length > 0 ? (
|
||||
tools.map((tool) => (
|
||||
<Card key={tool.id} className="flex flex-col">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<CardTitle>{tool.npmPackageName}</CardTitle>
|
||||
{tool.npmRepository && (
|
||||
<div className="flex-1">
|
||||
<CardTitle>
|
||||
{tool.exportName !== 'default' ? tool.exportName : tool.package.npmPackageName}
|
||||
</CardTitle>
|
||||
<div className="text-sm text-foreground-secondary mt-1">
|
||||
{tool.package.npmPackageName}
|
||||
</div>
|
||||
</div>
|
||||
{tool.package.npmRepository && (
|
||||
<a
|
||||
href={tool.npmRepository.url.replace('git+', '').replace('.git', '')}
|
||||
href={tool.package.npmRepository.url.replace('git+', '').replace('.git', '')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground-secondary hover:text-foreground transition-colors"
|
||||
|
|
@ -244,33 +215,22 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
{/* Category badge and version */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" size="sm">
|
||||
{tool.category}
|
||||
{tool.package.category}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">v{tool.npmVersion}</span>
|
||||
{tool.isOfficial && (
|
||||
<span className="text-xs text-foreground-tertiary">v{tool.package.npmVersion}</span>
|
||||
{tool.package.isOfficial && (
|
||||
<Badge variant="default" size="sm">
|
||||
Official
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{tool.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tool.tags.slice(0, 5).map((tag) => (
|
||||
<Badge key={tag} variant="outline" size="sm">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quality score and downloads */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-foreground-secondary">Quality Score</span>
|
||||
<span className="text-foreground-tertiary">
|
||||
{tool.npmDownloadsLastMonth.toLocaleString()} downloads/mo
|
||||
{tool.package.npmDownloadsLastMonth.toLocaleString()} downloads/mo
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
|
|
@ -289,7 +249,7 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
|
||||
{/* Install command */}
|
||||
<CodeBlock
|
||||
code={`npm install ${tool.npmPackageName}`}
|
||||
code={`npm install ${tool.package.npmPackageName}`}
|
||||
language="bash"
|
||||
size="sm"
|
||||
showCopy={true}
|
||||
|
|
@ -297,7 +257,7 @@ export default function ToolSearchPage(): React.ReactElement {
|
|||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Link href={`/tool/${tool.npmPackageName}`}>
|
||||
<Link href={`/tool/${tool.package.npmPackageName}/${tool.exportName}`}>
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
View Details
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@
|
|||
*/
|
||||
|
||||
import type { TokenBreakdown as TokenData } from '@/lib/ai-agent/tool-executor-agent';
|
||||
import type { Tool } from '@tpmjs/db';
|
||||
import type { Package, Tool } from '@tpmjs/db';
|
||||
import { useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { TokenBreakdown } from './TokenBreakdown';
|
||||
|
||||
interface ToolPlaygroundProps {
|
||||
tool: Tool;
|
||||
tool: Tool & { package: Package };
|
||||
}
|
||||
|
||||
type Tab = 'input' | 'output' | 'logs' | 'tokens';
|
||||
|
|
@ -47,7 +47,7 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
|
|||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/tools/execute/${encodeURIComponent(tool.npmPackageName)}`,
|
||||
`/api/tools/execute/${encodeURIComponent(tool.package.npmPackageName)}/${encodeURIComponent(tool.exportName)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -194,7 +194,7 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
|
|||
<div>
|
||||
<h2 className="text-xl font-semibold text-foreground">Interactive Playground</h2>
|
||||
<p className="text-sm text-foreground-secondary mt-1">
|
||||
Test {tool.npmPackageName} with AI-powered execution
|
||||
Test {tool.package.npmPackageName} ({tool.exportName}) with AI-powered execution
|
||||
</p>
|
||||
</div>
|
||||
{rateLimitInfo && (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import type { Tool } from '@tpmjs/db';
|
||||
import type { Package, Tool } from '@tpmjs/db';
|
||||
import { executePackage } from '@tpmjs/package-executor';
|
||||
import { type CoreMessage, generateText } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
|
@ -90,13 +90,14 @@ export function tpmjsParamsToZodSchema(parameters: TPMJSParameter[]): z.ZodObjec
|
|||
|
||||
/**
|
||||
* Create AI SDK v6 tool definition from TPMJS Tool
|
||||
* Requires Tool with Package relation
|
||||
*/
|
||||
export function createToolDefinition(tool: Tool) {
|
||||
export function createToolDefinition(tool: Tool & { package: Package }) {
|
||||
const parameters = Array.isArray(tool.parameters)
|
||||
? (tool.parameters as unknown as TPMJSParameter[])
|
||||
: [];
|
||||
|
||||
console.log('[createToolDefinition] Tool:', tool.npmPackageName);
|
||||
console.log('[createToolDefinition] Tool:', tool.package.npmPackageName, '/', tool.exportName);
|
||||
console.log('[createToolDefinition] Parameters array:', JSON.stringify(parameters));
|
||||
console.log('[createToolDefinition] Parameters length:', parameters.length);
|
||||
|
||||
|
|
@ -108,7 +109,9 @@ export function createToolDefinition(tool: Tool) {
|
|||
|
||||
console.log('[createToolDefinition] Created Zod schema:', inputSchema);
|
||||
|
||||
const sanitizedName = sanitizeToolName(tool.npmPackageName);
|
||||
const sanitizedName = sanitizeToolName(
|
||||
`${tool.package.npmPackageName}-${tool.exportName}`
|
||||
);
|
||||
|
||||
// AI SDK v6 tool definition
|
||||
return {
|
||||
|
|
@ -118,9 +121,10 @@ export function createToolDefinition(tool: Tool) {
|
|||
console.log('[Tool execute] Running:', sanitizedName, params);
|
||||
|
||||
// Execute the actual npm package in a sandbox
|
||||
// Use the actual export name from the Tool record
|
||||
const result = await executePackage(
|
||||
tool.npmPackageName,
|
||||
'default', // Most TPMJS packages export a default function
|
||||
tool.package.npmPackageName,
|
||||
tool.exportName, // Use actual export name (e.g., "helloWorldTool", "default")
|
||||
params,
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
|
@ -186,15 +190,18 @@ function sanitizeToolName(npmPackageName: string): string {
|
|||
|
||||
/**
|
||||
* Execute tool with AI agent using AI SDK v6
|
||||
* Requires Tool with Package relation
|
||||
*/
|
||||
export async function executeToolWithAgent(
|
||||
tool: Tool,
|
||||
tool: Tool & { package: Package },
|
||||
userPrompt: string,
|
||||
onChunk?: (chunk: string) => void,
|
||||
onTokenUpdate?: (tokens: Partial<TokenBreakdown>) => void
|
||||
) {
|
||||
const toolDef = createToolDefinition(tool);
|
||||
const sanitizedToolName = sanitizeToolName(tool.npmPackageName);
|
||||
const sanitizedToolName = sanitizeToolName(
|
||||
`${tool.package.npmPackageName}-${tool.exportName}`
|
||||
);
|
||||
|
||||
console.log('[executeToolWithAgent] Tool name:', sanitizedToolName);
|
||||
|
||||
|
|
|
|||
762
manual-tools.ts
Normal file
762
manual-tools.ts
Normal file
|
|
@ -0,0 +1,762 @@
|
|||
/**
|
||||
* Manual Tools Registry
|
||||
*
|
||||
* This file contains tools that should be included in TPMJS but don't follow
|
||||
* the standard tpmjs field specification in their package.json.
|
||||
*
|
||||
* These tools are manually curated and synced to the database via the
|
||||
* manual sync script.
|
||||
*/
|
||||
|
||||
export interface ManualTool {
|
||||
// Package metadata
|
||||
npmPackageName: string;
|
||||
npmVersion?: string; // Optional - will fetch latest if not specified
|
||||
category: 'text-analysis' | 'code-generation' | 'data-processing' | 'image-generation' | 'audio-processing' | 'search' | 'integration' | 'other';
|
||||
frameworks: Array<'vercel-ai' | 'langchain' | 'llamaindex' | 'other'>;
|
||||
|
||||
// Tool definition
|
||||
exportName: string;
|
||||
description: string;
|
||||
|
||||
// Optional rich metadata
|
||||
parameters?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
default?: string;
|
||||
}>;
|
||||
|
||||
returns?: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
aiAgent?: {
|
||||
useCase: string;
|
||||
limitations?: string;
|
||||
examples?: string[];
|
||||
};
|
||||
|
||||
// Environment variables
|
||||
env?: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
}>;
|
||||
|
||||
// Additional metadata not in npm
|
||||
tags?: string[];
|
||||
docsUrl?: string;
|
||||
apiKeyUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}
|
||||
|
||||
export const manualTools: ManualTool[] = [
|
||||
{
|
||||
npmPackageName: 'ai-sdk-tool-code-execution',
|
||||
category: 'code-generation',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'executeCode',
|
||||
description: 'Execute Python code in a sandboxed environment using Vercel Sandbox. Run calculations, data processing, and other computational tasks safely in an isolated environment with Python 3.13.',
|
||||
tags: ['code-execution', 'sandbox'],
|
||||
env: [
|
||||
{
|
||||
name: 'VERCEL_OIDC_TOKEN',
|
||||
description: 'Vercel OIDC token for sandbox authentication',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
description: 'Python code to execute in the sandbox',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Execution result with stdout, stderr, and return value',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to perform calculations, data processing, or execute Python code safely',
|
||||
limitations: 'Python 3.13 only. No network access. 30 second execution timeout.',
|
||||
examples: [
|
||||
'Perform complex mathematical calculations',
|
||||
'Process data with pandas/numpy',
|
||||
'Generate plots with matplotlib',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://vercel.com/docs/vercel-sandbox',
|
||||
apiKeyUrl: 'https://vercel.com/docs/vercel-sandbox#authentication',
|
||||
websiteUrl: 'https://vercel.com/docs/vercel-sandbox',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@exalabs/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'webSearch',
|
||||
description: 'Search the web for current information using Exa\'s AI-powered search API. Returns high-quality, relevant results optimized for LLM consumption.',
|
||||
tags: ['search', 'web', 'extraction'],
|
||||
env: [
|
||||
{
|
||||
name: 'EXA_API_KEY',
|
||||
description: 'API key for Exa search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'numResults',
|
||||
type: 'number',
|
||||
description: 'Number of results to return (1-10)',
|
||||
required: false,
|
||||
default: '5',
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'array',
|
||||
description: 'Array of search results with title, URL, and content',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need current information, news, research papers, or web content',
|
||||
limitations: 'Requires API key. Rate limits apply based on plan.',
|
||||
examples: [
|
||||
'Find latest AI developments',
|
||||
'Search for technical documentation',
|
||||
'Research current events',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://docs.exa.ai/reference/vercel',
|
||||
apiKeyUrl: 'https://dashboard.exa.ai/api-keys',
|
||||
websiteUrl: 'https://exa.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@parallel-web/ai-sdk-tools',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'searchTool',
|
||||
description: 'Search and extract context from the web with token-optimized results. Parallel compresses web results for optimal inference efficiency.',
|
||||
tags: ['search', 'web', 'extraction'],
|
||||
env: [
|
||||
{
|
||||
name: 'PARALLEL_API_KEY',
|
||||
description: 'API key for Parallel search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need web search with minimal token usage',
|
||||
examples: [
|
||||
'Search for current information',
|
||||
'Extract structured data from websites',
|
||||
],
|
||||
},
|
||||
apiKeyUrl: 'https://platform.parallel.ai',
|
||||
websiteUrl: 'https://parallel.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@parallel-web/ai-sdk-tools',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'extractTool',
|
||||
description: 'Extract structured content from web pages with token-optimized compression.',
|
||||
tags: ['extraction', 'web'],
|
||||
env: [
|
||||
{
|
||||
name: 'PARALLEL_API_KEY',
|
||||
description: 'API key for Parallel search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
description: 'URL of the page to extract content from',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Extracted and compressed page content',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use to extract clean content from specific URLs',
|
||||
examples: [
|
||||
'Extract article content',
|
||||
'Parse documentation pages',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://parallel.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: 'ctx-zip',
|
||||
category: 'code-generation',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'createVercelSandboxCodeMode',
|
||||
description: 'Transform MCP tools and AI SDK tools into code, write to Vercel sandbox filesystem, and execute in isolated environment.',
|
||||
tags: ['code-execution', 'sandbox', 'mcp', 'code-mode'],
|
||||
env: [
|
||||
{
|
||||
name: 'VERCEL_OIDC_TOKEN',
|
||||
description: 'Vercel OIDC token for sandbox authentication',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to combine MCP tools with code execution in a sandbox',
|
||||
limitations: 'Requires Vercel Sandbox access',
|
||||
examples: [
|
||||
'Execute code using MCP server tools',
|
||||
'Combine multiple tool sources',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://github.com/karthikscale3/ctx-zip/blob/main/README.md',
|
||||
apiKeyUrl: 'https://vercel.com/docs/vercel-sandbox#authentication',
|
||||
websiteUrl: 'https://github.com/karthikscale3/ctx-zip',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@perplexity-ai/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'perplexitySearch',
|
||||
description: 'Search the web with real-time results and advanced filtering powered by Perplexity Search API. Supports ranked results with domain, language, date range, and recency filters.',
|
||||
tags: ['search', 'web'],
|
||||
env: [
|
||||
{
|
||||
name: 'PERPLEXITY_API_KEY',
|
||||
description: 'API key for Perplexity search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'filters',
|
||||
type: 'object',
|
||||
description: 'Optional filters for domain, language, date range, etc.',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'array',
|
||||
description: 'Ranked search results with metadata',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use for comprehensive web search with filtering capabilities',
|
||||
examples: [
|
||||
'Find latest AI developments',
|
||||
'Search with date range filters',
|
||||
'Domain-specific searches',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://docs.perplexity.ai/guides/search-quickstart',
|
||||
apiKeyUrl: 'https://www.perplexity.ai/account/api/keys',
|
||||
websiteUrl: 'https://www.perplexity.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@tavily/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'tavilySearch',
|
||||
description: 'Real-time web search optimized for AI applications. Provides comprehensive web research including search, content extraction, website crawling, and site mapping.',
|
||||
tags: ['search', 'extract', 'crawl'],
|
||||
env: [
|
||||
{
|
||||
name: 'TAVILY_API_KEY',
|
||||
description: 'API key for Tavily search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use for comprehensive web research and content extraction',
|
||||
examples: [
|
||||
'Research complex topics',
|
||||
'Extract structured information',
|
||||
'Crawl entire websites',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://docs.tavily.com/documentation/integrations/vercel',
|
||||
apiKeyUrl: 'https://app.tavily.com/home',
|
||||
websiteUrl: 'https://tavily.com',
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'scrapeTool',
|
||||
description: 'Scrape any website into clean markdown format. Convert web pages to LLM-friendly markdown with automatic cleaning and formatting.',
|
||||
tags: ['scraping', 'web', 'extraction'],
|
||||
env: [
|
||||
{
|
||||
name: 'FIRECRAWL_API_KEY',
|
||||
description: 'API key for Firecrawl service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
description: 'URL of the website to scrape',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Scraped content in clean markdown format',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to convert web pages to clean, structured markdown',
|
||||
examples: [
|
||||
'Scrape documentation pages',
|
||||
'Extract article content',
|
||||
'Convert web pages to markdown',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://docs.firecrawl.dev/integrations/ai-sdk',
|
||||
apiKeyUrl: 'https://firecrawl.dev/app/api-keys',
|
||||
websiteUrl: 'https://firecrawl.dev',
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'searchTool',
|
||||
description: 'Search the web and get results in clean markdown format optimized for AI consumption.',
|
||||
tags: ['search', 'web'],
|
||||
env: [
|
||||
{
|
||||
name: 'FIRECRAWL_API_KEY',
|
||||
description: 'API key for Firecrawl service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
websiteUrl: 'https://firecrawl.dev',
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'crawlTool',
|
||||
description: 'Crawl entire websites and extract structured data. Automatically discover and process multiple pages.',
|
||||
tags: ['crawling', 'web', 'extraction'],
|
||||
env: [
|
||||
{
|
||||
name: 'FIRECRAWL_API_KEY',
|
||||
description: 'API key for Firecrawl service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
description: 'Starting URL to begin crawling',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'maxPages',
|
||||
type: 'number',
|
||||
description: 'Maximum number of pages to crawl',
|
||||
required: false,
|
||||
default: '10',
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to crawl and extract data from entire websites',
|
||||
examples: [
|
||||
'Crawl documentation sites',
|
||||
'Extract all articles from a blog',
|
||||
'Build knowledge base from website',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://firecrawl.dev',
|
||||
},
|
||||
{
|
||||
npmPackageName: 'bedrock-agentcore',
|
||||
category: 'code-generation',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'CodeInterpreterTools',
|
||||
description: 'Isolated sandbox for executing Python, JavaScript, and TypeScript code to solve complex tasks. Fully managed by Amazon Bedrock.',
|
||||
tags: ['code-execution', 'sandbox'],
|
||||
env: [
|
||||
{
|
||||
name: 'AWS_ROLE_ARN',
|
||||
description: 'AWS IAM role ARN for Bedrock access',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use for secure code execution in AWS-managed sandbox',
|
||||
limitations: 'Requires AWS credentials and Bedrock access',
|
||||
examples: [
|
||||
'Execute Python data analysis',
|
||||
'Run JavaScript computations',
|
||||
'TypeScript code execution',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://github.com/aws/bedrock-agentcore-sdk-typescript',
|
||||
apiKeyUrl: 'https://vercel.com/docs/oidc/aws',
|
||||
websiteUrl: 'https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/built-in-tools.html',
|
||||
},
|
||||
{
|
||||
npmPackageName: 'bedrock-agentcore',
|
||||
category: 'integration',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'BrowserTools',
|
||||
description: 'Fast and secure cloud-based browser runtime for web automation. Fill forms, navigate websites, and extract information in managed environment.',
|
||||
tags: ['browser-automation', 'web'],
|
||||
env: [
|
||||
{
|
||||
name: 'AWS_ROLE_ARN',
|
||||
description: 'AWS IAM role ARN for Bedrock access',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use for browser automation and web interaction tasks',
|
||||
examples: [
|
||||
'Fill web forms',
|
||||
'Navigate websites',
|
||||
'Extract dynamic content',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/built-in-tools.html',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@superagent-ai/ai-sdk',
|
||||
category: 'other',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'guard',
|
||||
description: 'Protect AI apps from prompt injection and security threats. Detect and block malicious inputs before they reach your LLM.',
|
||||
tags: ['security', 'guardrails', 'prompt-injection'],
|
||||
env: [
|
||||
{
|
||||
name: 'SUPERAGENT_API_KEY',
|
||||
description: 'API key for Superagent security service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'input',
|
||||
type: 'string',
|
||||
description: 'User input to check for security threats',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Security analysis with threat detection results',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use to validate user inputs for security threats before processing',
|
||||
examples: [
|
||||
'Detect prompt injection attempts',
|
||||
'Block malicious inputs',
|
||||
'Security validation',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://docs.superagent.sh',
|
||||
apiKeyUrl: 'https://dashboard.superagent.sh',
|
||||
websiteUrl: 'https://superagent.sh',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@superagent-ai/ai-sdk',
|
||||
category: 'other',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'redact',
|
||||
description: 'Redact PII/PHI from text including SSNs, emails, phone numbers, and other sensitive information.',
|
||||
tags: ['security', 'pii', 'redaction'],
|
||||
env: [
|
||||
{
|
||||
name: 'SUPERAGENT_API_KEY',
|
||||
description: 'API key for Superagent security service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
description: 'Text containing potentially sensitive information',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Redacted text with PII/PHI removed or masked',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use to remove sensitive information from text before processing',
|
||||
examples: [
|
||||
'Redact SSNs from documents',
|
||||
'Remove email addresses',
|
||||
'Mask phone numbers',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://superagent.sh',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@superagent-ai/ai-sdk',
|
||||
category: 'other',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'verify',
|
||||
description: 'Verify AI-generated claims against source materials. Fact-check and validate LLM outputs for accuracy.',
|
||||
tags: ['verification', 'fact-checking'],
|
||||
env: [
|
||||
{
|
||||
name: 'SUPERAGENT_API_KEY',
|
||||
description: 'API key for Superagent security service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'claim',
|
||||
type: 'string',
|
||||
description: 'Claim to verify',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'sources',
|
||||
type: 'array',
|
||||
description: 'Source materials to verify against',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Verification result with confidence score',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use to fact-check LLM outputs against trusted sources',
|
||||
examples: [
|
||||
'Verify factual claims',
|
||||
'Check citations',
|
||||
'Validate information accuracy',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://superagent.sh',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'webSearch',
|
||||
description: 'Real-time web search for current information and news.',
|
||||
tags: ['search', 'web'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use for general web search and current information',
|
||||
examples: [
|
||||
'Find latest news',
|
||||
'Search for current events',
|
||||
],
|
||||
},
|
||||
docsUrl: 'https://docs.valyu.ai/integrations/vercel-ai-sdk',
|
||||
apiKeyUrl: 'https://platform.valyu.ai',
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'financeSearch',
|
||||
description: 'Search financial data including stock prices, earnings, income statements, cash flows, and market data.',
|
||||
tags: ['search', 'finance', 'domain-search'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use for financial data and market research',
|
||||
examples: [
|
||||
'Get stock prices',
|
||||
'Find earnings reports',
|
||||
'Search financial statements',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'paperSearch',
|
||||
description: 'Full-text search across PubMed, arXiv, bioRxiv, and medRxiv research papers.',
|
||||
tags: ['search', 'research', 'domain-search'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use to search academic and research papers',
|
||||
examples: [
|
||||
'Find medical research',
|
||||
'Search arXiv papers',
|
||||
'Look up scientific publications',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'bioSearch',
|
||||
description: 'Search biomedical information including clinical trials, FDA drug labels, PubMed, medRxiv, and bioRxiv.',
|
||||
tags: ['search', 'biomedical', 'domain-search'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use for biomedical and clinical research',
|
||||
examples: [
|
||||
'Search clinical trials',
|
||||
'Find FDA drug information',
|
||||
'Research medical studies',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'patentSearch',
|
||||
description: 'Search USPTO patent database for patents and patent applications.',
|
||||
tags: ['search', 'patents', 'domain-search'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use to search and research patents',
|
||||
examples: [
|
||||
'Find existing patents',
|
||||
'Research patent applications',
|
||||
'Prior art search',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'secSearch',
|
||||
description: 'Search SEC filings including 10-K, 10-Q, and 8-K reports.',
|
||||
tags: ['search', 'sec', 'domain-search'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use to search SEC filings and corporate reports',
|
||||
examples: [
|
||||
'Find 10-K annual reports',
|
||||
'Search quarterly filings',
|
||||
'Research corporate disclosures',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'economicsSearch',
|
||||
description: 'Search economic data from BLS, FRED, and World Bank databases.',
|
||||
tags: ['search', 'economics', 'domain-search'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
aiAgent: {
|
||||
useCase: 'Use to search economic indicators and statistics',
|
||||
examples: [
|
||||
'Find employment data',
|
||||
'Search GDP statistics',
|
||||
'Research economic indicators',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
{
|
||||
npmPackageName: '@valyu/ai-sdk',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'companyResearch',
|
||||
description: 'Generate comprehensive company research reports with financial data, news, and analysis.',
|
||||
tags: ['search', 'research', 'company-analysis'],
|
||||
env: [
|
||||
{
|
||||
name: 'VALYU_API_KEY',
|
||||
description: 'API key for Valyu search service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'company',
|
||||
type: 'string',
|
||||
description: 'Company name or ticker symbol',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Comprehensive research report with financial and market data',
|
||||
},
|
||||
aiAgent: {
|
||||
useCase: 'Use to generate detailed company research and analysis',
|
||||
examples: [
|
||||
'Company financial analysis',
|
||||
'Competitive research',
|
||||
'Market position analysis',
|
||||
],
|
||||
},
|
||||
websiteUrl: 'https://valyu.ai',
|
||||
},
|
||||
];
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
"dependency-cruiser": "^17.3.1",
|
||||
"knip": "^5.70.2",
|
||||
"lefthook": "^1.10.1",
|
||||
"tsx": "^4.21.0",
|
||||
"turbo": "^2.6.1",
|
||||
"type-coverage": "^2.29.7",
|
||||
"typescript": "^5.9.3"
|
||||
|
|
|
|||
|
|
@ -10,46 +10,68 @@ datasource db {
|
|||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
/// Main tools table - stores all discovered NPM packages with TPMJS metadata
|
||||
model Tool {
|
||||
/// Package table - stores NPM package metadata (package-level)
|
||||
model Package {
|
||||
id String @id @default(cuid())
|
||||
|
||||
// NPM Metadata
|
||||
npmPackageName String @unique @map("npm_package_name") @db.VarChar(214)
|
||||
npmVersion String @map("npm_version") @db.VarChar(50)
|
||||
npmPublishedAt DateTime @map("npm_published_at")
|
||||
npmDescription String? @map("npm_description") @db.Text
|
||||
npmRepository Json? @map("npm_repository") @db.JsonB
|
||||
npmHomepage String? @map("npm_homepage") @db.Text
|
||||
npmLicense String? @map("npm_license") @db.VarChar(50)
|
||||
npmKeywords String[] @default([]) @map("npm_keywords") @db.Text
|
||||
npmReadme String? @map("npm_readme") @db.Text
|
||||
npmAuthor Json? @map("npm_author") @db.JsonB
|
||||
npmMaintainers Json? @map("npm_maintainers") @db.JsonB
|
||||
npmPackageName String @unique @map("npm_package_name") @db.VarChar(214)
|
||||
npmVersion String @map("npm_version") @db.VarChar(50)
|
||||
npmPublishedAt DateTime @map("npm_published_at")
|
||||
npmDescription String? @map("npm_description") @db.Text
|
||||
npmRepository Json? @map("npm_repository") @db.JsonB
|
||||
npmHomepage String? @map("npm_homepage") @db.Text
|
||||
npmLicense String? @map("npm_license") @db.VarChar(50)
|
||||
npmKeywords String[] @default([]) @map("npm_keywords") @db.Text
|
||||
npmReadme String? @map("npm_readme") @db.Text
|
||||
npmAuthor Json? @map("npm_author") @db.JsonB
|
||||
npmMaintainers Json? @map("npm_maintainers") @db.JsonB
|
||||
|
||||
// TPMJS Metadata (from package.json tpmjs field)
|
||||
// TPMJS Package-Level Metadata
|
||||
category String @db.VarChar(50)
|
||||
description String @db.Text
|
||||
example String? @db.Text
|
||||
parameters Json? @db.JsonB
|
||||
returns Json? @db.JsonB
|
||||
authentication Json? @db.JsonB
|
||||
pricing Json? @db.JsonB
|
||||
env Json? @db.JsonB // Environment variables (shared across tools)
|
||||
frameworks String[] @default([]) @db.Text
|
||||
links Json? @db.JsonB
|
||||
tags String[] @default([]) @db.Text
|
||||
status String? @db.VarChar(20)
|
||||
aiAgent Json? @map("ai_agent") @db.JsonB
|
||||
|
||||
// Discovery Metadata
|
||||
tier String @db.VarChar(20) // 'minimal' | 'rich'
|
||||
discoveryMethod String @map("discovery_method") @db.VarChar(20) // 'keyword' | 'changes-feed'
|
||||
isOfficial Boolean @default(false) @map("is_official")
|
||||
tier String @db.VarChar(20) // 'minimal' | 'rich'
|
||||
isOfficial Boolean @default(false) @map("is_official")
|
||||
|
||||
// Metrics
|
||||
npmDownloadsLastMonth Int? @default(0) @map("npm_downloads_last_month")
|
||||
githubStars Int? @default(0) @map("github_stars")
|
||||
qualityScore Decimal? @map("quality_score") @db.Decimal(3, 2) // 0.00 to 1.00
|
||||
// Package Metrics
|
||||
npmDownloadsLastMonth Int? @default(0) @map("npm_downloads_last_month")
|
||||
githubStars Int? @default(0) @map("github_stars")
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
// Relations
|
||||
tools Tool[]
|
||||
|
||||
@@index([category])
|
||||
@@index([isOfficial])
|
||||
@@index([npmDownloadsLastMonth])
|
||||
@@index([createdAt])
|
||||
@@map("packages")
|
||||
}
|
||||
|
||||
/// Tool table - stores individual tools within packages
|
||||
model Tool {
|
||||
id String @id @default(cuid())
|
||||
|
||||
// Package Relation
|
||||
packageId String @map("package_id")
|
||||
package Package @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Tool Identity
|
||||
exportName String @map("export_name") @db.VarChar(100) // e.g., "helloWorldTool", "default"
|
||||
|
||||
// Tool Metadata
|
||||
description String @db.Text
|
||||
parameters Json? @db.JsonB
|
||||
returns Json? @db.JsonB
|
||||
aiAgent Json? @map("ai_agent") @db.JsonB
|
||||
|
||||
// Tool Metrics
|
||||
qualityScore Decimal? @map("quality_score") @db.Decimal(3, 2) // 0.00 to 1.00
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
|
@ -58,11 +80,8 @@ model Tool {
|
|||
// Relations
|
||||
simulations Simulation[]
|
||||
|
||||
@@index([category])
|
||||
@@index([isOfficial])
|
||||
@@unique([packageId, exportName])
|
||||
@@index([qualityScore])
|
||||
@@index([npmDownloadsLastMonth])
|
||||
@@index([createdAt])
|
||||
@@map("tools")
|
||||
}
|
||||
|
||||
|
|
@ -81,14 +100,14 @@ model SyncCheckpoint {
|
|||
model SyncLog {
|
||||
id String @id @default(cuid())
|
||||
|
||||
source String @db.VarChar(50) // 'changes-feed' | 'keyword-search' | 'metrics'
|
||||
status String @db.VarChar(20) // 'success' | 'error' | 'partial'
|
||||
processed Int @default(0) // Number of packages processed
|
||||
skipped Int @default(0) // Number of packages skipped
|
||||
errors Int @default(0) // Number of errors encountered
|
||||
message String? @db.Text // Error message or summary
|
||||
metadata Json? @db.JsonB // Additional context
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
source String @db.VarChar(50) // 'changes-feed' | 'keyword-search' | 'metrics'
|
||||
status String @db.VarChar(20) // 'success' | 'error' | 'partial'
|
||||
processed Int @default(0) // Number of packages processed
|
||||
skipped Int @default(0) // Number of packages skipped
|
||||
errors Int @default(0) // Number of errors encountered
|
||||
message String? @db.Text // Error message or summary
|
||||
metadata Json? @db.JsonB // Additional context
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([source])
|
||||
@@index([status])
|
||||
|
|
@ -121,8 +140,8 @@ model Simulation {
|
|||
model String? @db.VarChar(50)
|
||||
|
||||
// Relations
|
||||
tokenUsage TokenUsage?
|
||||
logs ExecutionLog[]
|
||||
tokenUsage TokenUsage?
|
||||
logs ExecutionLog[]
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
|
|
@ -140,12 +159,12 @@ model TokenUsage {
|
|||
simulationId String @unique @map("simulation_id")
|
||||
simulation Simulation @relation(fields: [simulationId], references: [id], onDelete: Cascade)
|
||||
|
||||
inputTokens Int @default(0) @map("input_tokens")
|
||||
toolDescTokens Int @default(0) @map("tool_desc_tokens")
|
||||
schemaTokens Int @default(0) @map("schema_tokens")
|
||||
outputTokens Int @default(0) @map("output_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
estimatedCost Decimal? @map("estimated_cost") @db.Decimal(10, 6)
|
||||
inputTokens Int @default(0) @map("input_tokens")
|
||||
toolDescTokens Int @default(0) @map("tool_desc_tokens")
|
||||
schemaTokens Int @default(0) @map("schema_tokens")
|
||||
outputTokens Int @default(0) @map("output_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
estimatedCost Decimal? @map("estimated_cost") @db.Decimal(10, 6)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@map("token_usage")
|
||||
|
|
|
|||
9
packages/tools/hello/CHANGELOG.md
Normal file
9
packages/tools/hello/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# @tpmjs/hello
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: add multi-tool example package with helloWorldTool and helloNameTool
|
||||
|
||||
This is the first TPMJS package published with the new multi-tool format, demonstrating support for multiple tool exports in a single package.
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"name": "@tpmjs/hello",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"description": "Example TPMJS tools - Hello World and Hello Name",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
@ -11,17 +10,68 @@
|
|||
"clean": "rm -rf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": ["tpmjs-tool", "ai-sdk", "hello", "example"],
|
||||
"keywords": [
|
||||
"tpmjs-tool",
|
||||
"ai-sdk",
|
||||
"hello",
|
||||
"example"
|
||||
],
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Simple greeting tools - Hello World and personalized Hello Name greetings"
|
||||
"frameworks": [
|
||||
"vercel-ai"
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"exportName": "helloWorldTool",
|
||||
"description": "Returns a simple 'Hello, World!' greeting with optional timestamp and customizable message",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "includeTimestamp",
|
||||
"type": "boolean",
|
||||
"description": "Whether to include the current timestamp in the greeting",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"description": "A greeting message, optionally with timestamp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"exportName": "helloNameTool",
|
||||
"description": "Returns a personalized greeting with the provided name",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "The name to greet",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "formal",
|
||||
"type": "boolean",
|
||||
"description": "Whether to use formal greeting (Mr./Ms.)",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"description": "A personalized greeting message"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.0-beta.124"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"files": ["dist", "README.md"]
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,23 +67,53 @@ export const TpmjsAiAgentSchema = z.object({
|
|||
export type TpmjsAiAgent = z.infer<typeof TpmjsAiAgentSchema>;
|
||||
|
||||
/**
|
||||
* Minimal tier schema - required fields only
|
||||
* This is the minimum required to publish a tool to TPMJS
|
||||
* Individual tool definition within a multi-tool package
|
||||
*/
|
||||
export const TpmjsMinimalSchema = z.object({
|
||||
export const TpmjsToolDefinitionSchema = z.object({
|
||||
exportName: z.string().min(1, 'Export name is required'),
|
||||
description: z.string().min(20, 'Description must be at least 20 characters').max(500),
|
||||
parameters: z.array(TpmjsParameterSchema).optional(),
|
||||
returns: TpmjsReturnsSchema.optional(),
|
||||
aiAgent: TpmjsAiAgentSchema.optional(),
|
||||
});
|
||||
|
||||
export type TpmjsToolDefinition = z.infer<typeof TpmjsToolDefinitionSchema>;
|
||||
|
||||
/**
|
||||
* Multi-tool format - NEW SCHEMA
|
||||
* Package-level metadata with array of tools
|
||||
*/
|
||||
export const TpmjsMultiToolSchema = z.object({
|
||||
category: z.enum(TPMJS_CATEGORIES, {
|
||||
message: `Category must be one of: ${TPMJS_CATEGORIES.join(', ')}`,
|
||||
}),
|
||||
tools: z.array(TpmjsToolDefinitionSchema).min(1, 'At least one tool is required'),
|
||||
env: z.array(TpmjsEnvSchema).optional(),
|
||||
frameworks: z
|
||||
.array(z.enum(['vercel-ai', 'langchain', 'llamaindex', 'haystack', 'semantic-kernel']))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TpmjsMultiTool = z.infer<typeof TpmjsMultiToolSchema>;
|
||||
|
||||
/**
|
||||
* Legacy minimal tier schema - DEPRECATED
|
||||
* Kept for backward compatibility with auto-migration
|
||||
*/
|
||||
export const TpmjsLegacyMinimalSchema = z.object({
|
||||
category: z.enum(TPMJS_CATEGORIES, {
|
||||
message: `Category must be one of: ${TPMJS_CATEGORIES.join(', ')}`,
|
||||
}),
|
||||
description: z.string().min(20, 'Description must be at least 20 characters').max(500),
|
||||
});
|
||||
|
||||
export type TpmjsMinimal = z.infer<typeof TpmjsMinimalSchema>;
|
||||
export type TpmjsLegacyMinimal = z.infer<typeof TpmjsLegacyMinimalSchema>;
|
||||
|
||||
/**
|
||||
* Rich tier schema - includes optional enhanced metadata
|
||||
* Tools with these fields get better visibility and quality scores
|
||||
* Legacy rich tier schema - DEPRECATED
|
||||
* Kept for backward compatibility with auto-migration
|
||||
*/
|
||||
export const TpmjsRichSchema = TpmjsMinimalSchema.extend({
|
||||
export const TpmjsLegacyRichSchema = TpmjsLegacyMinimalSchema.extend({
|
||||
parameters: z.array(TpmjsParameterSchema).optional(),
|
||||
returns: TpmjsReturnsSchema.optional(),
|
||||
env: z.array(TpmjsEnvSchema).optional(),
|
||||
|
|
@ -93,57 +123,129 @@ export const TpmjsRichSchema = TpmjsMinimalSchema.extend({
|
|||
aiAgent: TpmjsAiAgentSchema.optional(),
|
||||
});
|
||||
|
||||
export type TpmjsRich = z.infer<typeof TpmjsRichSchema>;
|
||||
export type TpmjsLegacyRich = z.infer<typeof TpmjsLegacyRichSchema>;
|
||||
|
||||
/**
|
||||
* Union type for either tier
|
||||
* Union type for legacy formats
|
||||
*/
|
||||
export type TpmjsField = TpmjsMinimal | TpmjsRich;
|
||||
export type TpmjsLegacy = TpmjsLegacyMinimal | TpmjsLegacyRich;
|
||||
|
||||
/**
|
||||
* Validation result type
|
||||
* Union type for all formats (new multi-tool + legacy)
|
||||
*/
|
||||
export type TpmjsField = TpmjsMultiTool | TpmjsLegacy;
|
||||
|
||||
// Backward compatibility aliases
|
||||
export type TpmjsMinimal = TpmjsLegacyMinimal;
|
||||
export type TpmjsRich = TpmjsLegacyRich;
|
||||
export const TpmjsMinimalSchema = TpmjsLegacyMinimalSchema;
|
||||
export const TpmjsRichSchema = TpmjsLegacyRichSchema;
|
||||
|
||||
/**
|
||||
* Extended validation result type for multi-tool support
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
tier: 'minimal' | 'rich' | null;
|
||||
data?: TpmjsField;
|
||||
errors?: z.ZodError;
|
||||
// New fields for multi-tool support
|
||||
packageData?: {
|
||||
category: TpmjsCategory;
|
||||
env?: TpmjsEnv[];
|
||||
frameworks?: string[];
|
||||
};
|
||||
tools?: TpmjsToolDefinition[];
|
||||
wasLegacyFormat?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a tpmjs field and determines its tier
|
||||
* Supports both new multi-tool format and legacy single-tool format with auto-migration
|
||||
*/
|
||||
export function validateTpmjsField(tpmjs: unknown): ValidationResult {
|
||||
// Try rich tier first
|
||||
const richResult = TpmjsRichSchema.safeParse(tpmjs);
|
||||
if (richResult.success) {
|
||||
// Check if it has any rich-tier fields
|
||||
const data = richResult.data;
|
||||
const hasRichFields =
|
||||
data.parameters || data.returns || data.env || data.frameworks || data.aiAgent;
|
||||
// Try new multi-tool format first
|
||||
const multiResult = TpmjsMultiToolSchema.safeParse(tpmjs);
|
||||
if (multiResult.success) {
|
||||
const data = multiResult.data;
|
||||
|
||||
// Determine tier based on tool richness
|
||||
const hasRichFields = data.tools.some(
|
||||
(tool) => tool.parameters || tool.returns || tool.aiAgent
|
||||
) || data.env || data.frameworks;
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tier: hasRichFields ? 'rich' : 'minimal',
|
||||
data: richResult.data,
|
||||
data: data,
|
||||
packageData: {
|
||||
category: data.category,
|
||||
env: data.env,
|
||||
frameworks: data.frameworks,
|
||||
},
|
||||
tools: data.tools,
|
||||
wasLegacyFormat: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Try minimal tier
|
||||
const minimalResult = TpmjsMinimalSchema.safeParse(tpmjs);
|
||||
// Try legacy rich tier format with auto-migration
|
||||
const richResult = TpmjsLegacyRichSchema.safeParse(tpmjs);
|
||||
if (richResult.success) {
|
||||
const legacyData = richResult.data;
|
||||
|
||||
// Auto-migrate to multi-tool format
|
||||
const tool: TpmjsToolDefinition = {
|
||||
exportName: 'default',
|
||||
description: legacyData.description,
|
||||
parameters: legacyData.parameters,
|
||||
returns: legacyData.returns,
|
||||
aiAgent: legacyData.aiAgent,
|
||||
};
|
||||
|
||||
const hasRichFields =
|
||||
legacyData.parameters || legacyData.returns || legacyData.env ||
|
||||
legacyData.frameworks || legacyData.aiAgent;
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tier: hasRichFields ? 'rich' : 'minimal',
|
||||
data: legacyData,
|
||||
packageData: {
|
||||
category: legacyData.category,
|
||||
env: legacyData.env,
|
||||
frameworks: legacyData.frameworks,
|
||||
},
|
||||
tools: [tool],
|
||||
wasLegacyFormat: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Try legacy minimal tier format with auto-migration
|
||||
const minimalResult = TpmjsLegacyMinimalSchema.safeParse(tpmjs);
|
||||
if (minimalResult.success) {
|
||||
// Auto-migrate to multi-tool format
|
||||
const tool: TpmjsToolDefinition = {
|
||||
exportName: 'default',
|
||||
description: minimalResult.data.description,
|
||||
};
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tier: 'minimal',
|
||||
data: minimalResult.data,
|
||||
packageData: {
|
||||
category: minimalResult.data.category,
|
||||
},
|
||||
tools: [tool],
|
||||
wasLegacyFormat: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Invalid
|
||||
// Invalid - return error from multi-tool schema (most informative)
|
||||
return {
|
||||
valid: false,
|
||||
tier: null,
|
||||
errors: minimalResult.error,
|
||||
errors: multiResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
1643
pnpm-lock.yaml
generated
1643
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
116
sync-hello.ts
Normal file
116
sync-hello.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { validateTpmjsField } from '@tpmjs/types/tpmjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function syncHello() {
|
||||
try {
|
||||
console.log('Fetching @tpmjs/hello from npm...');
|
||||
|
||||
const response = await fetch('https://registry.npmjs.org/@tpmjs/hello');
|
||||
const data = await response.json();
|
||||
|
||||
const latest = data['dist-tags'].latest;
|
||||
const pkg = data.versions[latest];
|
||||
|
||||
console.log(`\nPackage: ${pkg.name}@${pkg.version}`);
|
||||
console.log(`Keywords: ${pkg.keywords.join(', ')}`);
|
||||
console.log(`\ntpmjs field:`, JSON.stringify(pkg.tpmjs, null, 2));
|
||||
|
||||
// Validate
|
||||
const validation = validateTpmjsField(pkg.tpmjs);
|
||||
|
||||
if (!validation.valid) {
|
||||
console.error('\n❌ Invalid tpmjs field:', validation.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n✅ Valid tpmjs field (${validation.tier} tier)`);
|
||||
console.log(`Tools to create: ${validation.tools?.length || 0}`);
|
||||
|
||||
// Upsert Package
|
||||
const packageRecord = await prisma.package.upsert({
|
||||
where: { npmPackageName: pkg.name },
|
||||
create: {
|
||||
npmPackageName: pkg.name,
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: new Date(pkg.time[pkg.version]),
|
||||
npmDownloadsLastMonth: 0,
|
||||
category: validation.packageData!.category,
|
||||
env: validation.packageData!.env || null,
|
||||
frameworks: validation.packageData!.frameworks || [],
|
||||
tier: validation.tier!,
|
||||
discoveryMethod: 'manual',
|
||||
isOfficial: pkg.name.startsWith('@tpmjs/'),
|
||||
},
|
||||
update: {
|
||||
npmVersion: pkg.version,
|
||||
npmPublishedAt: new Date(pkg.time[pkg.version]),
|
||||
category: validation.packageData!.category,
|
||||
env: validation.packageData!.env || null,
|
||||
frameworks: validation.packageData!.frameworks || [],
|
||||
tier: validation.tier!,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`\n✅ Package upserted: ${packageRecord.id}`);
|
||||
|
||||
// Get existing tools
|
||||
const existingTools = await prisma.tool.findMany({
|
||||
where: { packageId: packageRecord.id },
|
||||
});
|
||||
|
||||
console.log(`\nExisting tools: ${existingTools.length}`);
|
||||
|
||||
// Upsert each tool
|
||||
for (const toolDef of validation.tools || []) {
|
||||
const tool = await prisma.tool.upsert({
|
||||
where: {
|
||||
packageId_exportName: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: toolDef.exportName,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: toolDef.exportName,
|
||||
description: toolDef.description,
|
||||
parameters: toolDef.parameters ? (toolDef.parameters as any) : undefined,
|
||||
returns: toolDef.returns ? (toolDef.returns as any) : undefined,
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
qualityScore: null,
|
||||
},
|
||||
update: {
|
||||
description: toolDef.description,
|
||||
parameters: toolDef.parameters ? (toolDef.parameters as any) : undefined,
|
||||
returns: toolDef.returns ? (toolDef.returns as any) : undefined,
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Tool upserted: ${tool.exportName} (${tool.id})`);
|
||||
}
|
||||
|
||||
// Delete orphaned tools
|
||||
const orphanedTools = existingTools.filter(
|
||||
(existingTool) =>
|
||||
!validation.tools?.some((toolDef) => toolDef.exportName === existingTool.exportName)
|
||||
);
|
||||
|
||||
if (orphanedTools.length > 0) {
|
||||
await prisma.tool.deleteMany({
|
||||
where: { id: { in: orphanedTools.map((t) => t.id) } },
|
||||
});
|
||||
console.log(`\n🗑️ Deleted ${orphanedTools.length} orphaned tools`);
|
||||
}
|
||||
|
||||
console.log('\n✅ Sync complete!');
|
||||
} catch (error) {
|
||||
console.error('\n❌ Sync failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
syncHello();
|
||||
138
sync-manual-tools.ts
Normal file
138
sync-manual-tools.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { prisma } from './packages/db/src/index.js';
|
||||
import { manualTools } from './manual-tools.js';
|
||||
import { fetchLatestPackageWithMetadata } from './packages/npm-client/src/package.js';
|
||||
|
||||
async function syncManualTools() {
|
||||
console.log('\n🔧 Starting manual tools sync...\n');
|
||||
|
||||
let processed = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const manualTool of manualTools) {
|
||||
try {
|
||||
console.log(`Processing: ${manualTool.npmPackageName} (${manualTool.exportName})`);
|
||||
|
||||
// Fetch package metadata from npm
|
||||
const npmData = await fetchLatestPackageWithMetadata(manualTool.npmPackageName);
|
||||
|
||||
if (!npmData) {
|
||||
console.error(` ❌ Package not found on npm: ${manualTool.npmPackageName}`);
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use manual version if specified, otherwise use latest from npm
|
||||
const version = manualTool.npmVersion || npmData.version;
|
||||
|
||||
// Get published date
|
||||
const publishedAt = npmData.time?.[version] || npmData.time?.modified || new Date().toISOString();
|
||||
|
||||
// Upsert Package record
|
||||
const packageRecord = await prisma.package.upsert({
|
||||
where: { npmPackageName: manualTool.npmPackageName },
|
||||
create: {
|
||||
npmPackageName: manualTool.npmPackageName,
|
||||
npmVersion: version,
|
||||
npmPublishedAt: new Date(publishedAt),
|
||||
npmDescription: npmData.description || null,
|
||||
npmRepository: npmData.repository || null,
|
||||
npmHomepage: npmData.homepage || manualTool.websiteUrl || null,
|
||||
npmLicense: npmData.license || null,
|
||||
npmKeywords: npmData.keywords || [],
|
||||
npmReadme: npmData.readme || null,
|
||||
npmAuthor: npmData.author || null,
|
||||
npmMaintainers: npmData.maintainers || null,
|
||||
category: manualTool.category,
|
||||
env: manualTool.env ? (manualTool.env as any) : null,
|
||||
frameworks: manualTool.frameworks,
|
||||
tier: calculateTier(manualTool),
|
||||
discoveryMethod: 'manual',
|
||||
isOfficial: manualTool.npmPackageName.startsWith('@tpmjs/'),
|
||||
npmDownloadsLastMonth: 0,
|
||||
},
|
||||
update: {
|
||||
npmVersion: version,
|
||||
npmPublishedAt: new Date(publishedAt),
|
||||
npmDescription: npmData.description || null,
|
||||
npmRepository: npmData.repository || null,
|
||||
npmHomepage: npmData.homepage || manualTool.websiteUrl || null,
|
||||
npmLicense: npmData.license || null,
|
||||
npmKeywords: npmData.keywords || [],
|
||||
npmReadme: npmData.readme || null,
|
||||
npmAuthor: npmData.author || null,
|
||||
npmMaintainers: npmData.maintainers || null,
|
||||
category: manualTool.category,
|
||||
env: manualTool.env ? (manualTool.env as any) : null,
|
||||
frameworks: manualTool.frameworks,
|
||||
tier: calculateTier(manualTool),
|
||||
isOfficial: manualTool.npmPackageName.startsWith('@tpmjs/'),
|
||||
},
|
||||
});
|
||||
|
||||
console.log(` ✅ Package upserted: ${packageRecord.id}`);
|
||||
|
||||
// Get existing tools for this package
|
||||
const existingTools = await prisma.tool.findMany({
|
||||
where: { packageId: packageRecord.id },
|
||||
});
|
||||
|
||||
// Upsert the tool
|
||||
const tool = await prisma.tool.upsert({
|
||||
where: {
|
||||
packageId_exportName: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: manualTool.exportName,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
packageId: packageRecord.id,
|
||||
exportName: manualTool.exportName,
|
||||
description: manualTool.description,
|
||||
parameters: manualTool.parameters ? (manualTool.parameters as any) : null,
|
||||
returns: manualTool.returns ? (manualTool.returns as any) : null,
|
||||
aiAgent: manualTool.aiAgent ? (manualTool.aiAgent as any) : null,
|
||||
},
|
||||
update: {
|
||||
description: manualTool.description,
|
||||
parameters: manualTool.parameters ? (manualTool.parameters as any) : null,
|
||||
returns: manualTool.returns ? (manualTool.returns as any) : null,
|
||||
aiAgent: manualTool.aiAgent ? (manualTool.aiAgent as any) : null,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(` ✅ Tool upserted: ${tool.exportName} (${tool.id})`);
|
||||
processed++;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
` ❌ Error processing ${manualTool.npmPackageName} (${manualTool.exportName}):`,
|
||||
error
|
||||
);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n📊 Manual sync complete!');
|
||||
console.log(` Processed: ${processed}`);
|
||||
console.log(` Skipped: ${skipped}`);
|
||||
console.log(` Errors: ${errors}`);
|
||||
console.log(` Total manual tools: ${manualTools.length}\n`);
|
||||
}
|
||||
|
||||
function calculateTier(tool: typeof manualTools[0]): 'minimal' | 'rich' {
|
||||
// Tier is 'rich' if tool has parameters OR returns OR aiAgent
|
||||
if (tool.parameters || tool.returns || tool.aiAgent) {
|
||||
return 'rich';
|
||||
}
|
||||
return 'minimal';
|
||||
}
|
||||
|
||||
syncManualTools()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error('❌ Manual sync failed:', error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
prisma.$disconnect();
|
||||
});
|
||||
88
test-schema.ts
Normal file
88
test-schema.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function testSchema() {
|
||||
console.log('Testing new Package + Tool schema...\n');
|
||||
|
||||
// Test 1: Create a package
|
||||
console.log('1. Creating test package...');
|
||||
const pkg = await prisma.package.create({
|
||||
data: {
|
||||
npmPackageName: '@test/hello',
|
||||
npmVersion: '1.0.0',
|
||||
npmPublishedAt: new Date(),
|
||||
category: 'text-analysis',
|
||||
tier: 'rich',
|
||||
discoveryMethod: 'test',
|
||||
isOfficial: false,
|
||||
frameworks: ['vercel-ai'],
|
||||
npmDownloadsLastMonth: 100,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Package created: ${pkg.npmPackageName} (${pkg.id})\n`);
|
||||
|
||||
// Test 2: Create multiple tools for the package
|
||||
console.log('2. Creating tools for package...');
|
||||
const tool1 = await prisma.tool.create({
|
||||
data: {
|
||||
packageId: pkg.id,
|
||||
exportName: 'helloWorldTool',
|
||||
description: 'Returns a simple Hello World greeting',
|
||||
},
|
||||
});
|
||||
console.log(`✅ Tool 1 created: ${tool1.exportName}`);
|
||||
|
||||
const tool2 = await prisma.tool.create({
|
||||
data: {
|
||||
packageId: pkg.id,
|
||||
exportName: 'helloNameTool',
|
||||
description: 'Returns a personalized greeting with name',
|
||||
},
|
||||
});
|
||||
console.log(`✅ Tool 2 created: ${tool2.exportName}\n`);
|
||||
|
||||
// Test 3: Query package with tools
|
||||
console.log('3. Querying package with tools...');
|
||||
const packageWithTools = await prisma.package.findUnique({
|
||||
where: { npmPackageName: '@test/hello' },
|
||||
include: { tools: true },
|
||||
});
|
||||
console.log(`✅ Found package with ${packageWithTools?.tools.length} tools:`);
|
||||
packageWithTools?.tools.forEach((t) => {
|
||||
console.log(` - ${t.exportName}: ${t.description}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// Test 4: Query tool with package
|
||||
console.log('4. Querying tool with package...');
|
||||
const toolWithPackage = await prisma.tool.findFirst({
|
||||
where: {
|
||||
package: { npmPackageName: '@test/hello' },
|
||||
exportName: 'helloWorldTool',
|
||||
},
|
||||
include: { package: true },
|
||||
});
|
||||
console.log(`✅ Found tool: ${toolWithPackage?.exportName}`);
|
||||
console.log(` Package: ${toolWithPackage?.package.npmPackageName}`);
|
||||
console.log(` Category: ${toolWithPackage?.package.category}\n`);
|
||||
|
||||
// Test 5: Delete package (should cascade to tools)
|
||||
console.log('5. Testing cascade delete...');
|
||||
await prisma.package.delete({
|
||||
where: { id: pkg.id },
|
||||
});
|
||||
const remainingTools = await prisma.tool.count({
|
||||
where: { packageId: pkg.id },
|
||||
});
|
||||
console.log(`✅ Package deleted, remaining tools: ${remainingTools}`);
|
||||
console.log(` (Should be 0 due to cascade delete)\n`);
|
||||
|
||||
console.log('✅ All schema tests passed!');
|
||||
}
|
||||
|
||||
testSchema()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error('❌ Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue