feat: add reusable ToolHealthBadge and ToolHealthBanner components

- Create ToolHealthBadge component in @tpmjs/ui for broken tool indicator
- Create ToolHealthBanner component in @tpmjs/ui for detailed health warnings
- Integrate both components into playground ToolsSidebar:
  - Badge shows in tool cards in left sidebar
  - Banner shows in tool detail modal
- Update search-registry to include health fields in API responses
- Add package.json exports for new health components

These components provide consistent UI for displaying broken tool status
across the application (tool search, tool detail pages, playground).

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-12-04 19:12:07 +10:00
parent 54ee6439cb
commit a9e91c02d1
24 changed files with 729 additions and 405 deletions

306
DENO_NODE_PACKAGE_ISSUE.md Normal file
View file

@ -0,0 +1,306 @@
# Running `ai-sdk-tool-code-execution` in Deno - Compatibility Issue
## Problem Summary
We need to run the npm package `ai-sdk-tool-code-execution` in a Deno runtime environment on Railway. The package requires Node.js built-ins (`node:sqlite`, `undici`) that don't exist in Deno, and we're looking for a solution to make it work.
## Environment
- **Runtime:** Deno 1.39.0 on Railway
- **Package:** `ai-sdk-tool-code-execution@0.0.2`
- **Import Method:** Dynamic imports via esm.sh CDN
- **Use Case:** Remote code execution for AI SDK tools
## What We're Trying to Do
We have a Deno server that dynamically imports npm packages at runtime to provide AI SDK tools. The workflow is:
1. User requests a tool (e.g., `executeCode`)
2. Deno server fetches the package from esm.sh or npm
3. Server loads the tool's schema and execution function
4. Server executes the tool with user-provided parameters
## The Package We Need
**Package:** `ai-sdk-tool-code-execution`
**Version:** `0.0.2`
**Description:** Execute Python code in a sandboxed environment using Vercel Sandbox
**npm URL:** https://www.npmjs.com/package/ai-sdk-tool-code-execution
**CDN URLs:**
- esm.sh: `https://esm.sh/ai-sdk-tool-code-execution@0.0.2`
- jsdelivr: `https://cdn.jsdelivr.net/npm/ai-sdk-tool-code-execution@0.0.2/+esm`
**Dependencies (from package.json):**
```json
{
"dependencies": {
"ai": "^4.0.18",
"better-sqlite3": "^11.8.1",
"undici": "^7.16.0"
}
}
```
**Key Issue:** The package depends on:
- `better-sqlite3` → which requires `node:sqlite` (Node.js built-in)
- `undici` → HTTP client that uses Node.js internals
## What We've Tried
### Attempt 1: Deno npm: Specifier (Node.js Compatibility Mode)
**Code:**
```typescript
const npmUrl = `npm:ai-sdk-tool-code-execution@0.0.2`;
const module = await import(npmUrl);
```
**Error:**
```
Loading unprepared module: npm:ai-sdk-tool-code-execution@0.0.2
```
**Why it failed:** Deno's npm compatibility requires the package to be "prepared" (downloaded/cached) before import. Dynamic imports of unprepared npm packages fail.
### Attempt 2: esm.sh with Node.js Target
**Code:**
```typescript
const esmUrl = `https://esm.sh/ai-sdk-tool-code-execution@0.0.2?target=esnext`;
const module = await import(esmUrl);
```
**Error:**
```
Module not found "https://esm.sh/node:sqlite?target=esnext"
at https://esm.sh/undici@^7.16.0?target=esnext:25:8
```
**Why it failed:** The package code imports `node:sqlite` which esm.sh tries to load from `https://esm.sh/node:sqlite?target=esnext`, but `node:sqlite` is a Node.js built-in, not an npm package.
### Attempt 3: Multi-Strategy with Fallback
**Code:**
```typescript
let module;
let importError;
// Strategy 1: npm: specifier
try {
const npmUrl = `npm:${packageName}@${version}`;
module = await import(npmUrl);
} catch (error) {
importError = error;
// Strategy 2: esm.sh with esnext target
try {
const esmUrl = `https://esm.sh/${packageName}@${version}?target=esnext`;
module = await import(esmUrl);
} catch (esmError) {
return { success: false, error: esmError.message };
}
}
```
**Result:** Both strategies fail with the same errors as above.
## Current Deno Configuration
**`deno.json`:**
```json
{
"compilerOptions": {
"allowJs": true,
"lib": ["deno.window"],
"strict": true
},
"nodeModulesDir": true,
"unstable": ["byonm"],
"imports": {
"zod-to-json-schema": "https://esm.sh/zod-to-json-schema@3.25.0"
}
}
```
**Key Settings:**
- `nodeModulesDir: true` - Creates `node_modules` directory for npm packages
- `unstable: ["byonm"]` - Enables "Bring Your Own Node Modules" mode
## Full Error Details
### npm: Strategy Error
```json
{
"success": false,
"error": "Failed to import package: ...",
"details": {
"npmError": "Loading unprepared module: npm:ai-sdk-tool-code-execution@0.0.2, imported from: file:///app/server.ts"
}
}
```
### esm.sh Strategy Error
```json
{
"success": false,
"error": "Failed to import package: Module not found \"https://esm.sh/node:sqlite?target=esnext\"",
"details": {
"esmError": "Module not found \"https://esm.sh/node:sqlite?target=esnext\".\n at https://esm.sh/undici@^7.16.0?target=esnext:25:8"
}
}
```
## Technical Deep Dive
### Why This Package Needs Node.js
1. **better-sqlite3** - Native Node.js addon for SQLite
- Uses `node:sqlite` built-in
- Compiled C++ bindings
- Not available in Deno without Node compatibility layer
2. **undici** - Modern HTTP client for Node.js
- Uses Node.js streams and buffer APIs
- Optimized for Node.js internals
- May work in Deno with polyfills, but blocked by sqlite dependency
### Deno's Node.js Compatibility
Deno supports many Node.js built-ins via `node:*` imports:
- `node:fs`, `node:path`, `node:http`, `node:crypto`, etc.
**BUT** it does NOT support:
- `node:sqlite` (not a standard Node.js built-in)
- Native addons (`.node` files)
- Some advanced internal APIs
### The Import Flow
1. **Deno tries to import** `npm:ai-sdk-tool-code-execution@0.0.2`
2. **Package resolves to** esm.sh or npm registry
3. **Package imports** `better-sqlite3`
4. **better-sqlite3 imports** `node:sqlite`
5. **FAILURE:** `node:sqlite` doesn't exist in Deno or esm.sh
## Questions for ChatGPT
1. **Can Deno's npm compatibility layer handle `better-sqlite3` or `node:sqlite`?**
- Is there a Deno-compatible SQLite library we could alias?
- Can we use import maps to redirect `node:sqlite` to a Deno polyfill?
2. **Can we "prepare" the npm module in Deno before dynamic import?**
- Is there a way to pre-cache npm packages in Deno?
- Can we use `deno vendor` or similar to prepare the package?
3. **Can esm.sh or other CDNs provide Node.js built-in polyfills?**
- Does esm.sh have a mode that bundles Node.js built-ins?
- Are there CDN parameters we're missing?
4. **Could we use Deno's `--node-modules-dir` flag differently?**
- Should we install the package via npm/pnpm first?
- Can we point Deno to pre-installed node_modules?
5. **Is there a way to patch/bundle the package to remove Node.js dependencies?**
- Could we create a Deno-compatible fork?
- Are there tools to transpile Node.js packages to Deno?
6. **Alternative: Different code execution package?**
- Are there Deno-native code execution tools?
- Could we use WebAssembly or browser-based sandboxing?
## What Would Success Look Like
**Ideal outcome:**
```typescript
// This should work in Deno:
const module = await import('npm:ai-sdk-tool-code-execution@0.0.2');
const { executeCode } = module;
// And this should execute:
const result = await executeCode.execute({
code: 'print(fibonacci(10))',
language: 'python'
});
```
**Acceptable outcome:**
```typescript
// Some preparation step, then:
const module = await import('https://esm.sh/ai-sdk-tool-code-execution@0.0.2');
// Works without errors
```
## Repository Context
**Project:** TPMJS - Tool Package Manager for AI SDK
**Server:** `apps/railway-executor/server.ts`
**Config:** `apps/railway-executor/deno.json`
**Deployment:** Railway with Deno runtime
**Server Code (Simplified):**
```typescript
async function loadAndDescribe(req: Request): Promise<Response> {
const { packageName, exportName, version, importUrl } = await req.json();
// Try npm: specifier first
try {
const npmUrl = `npm:${packageName}@${version}`;
const module = await import(npmUrl);
const tool = module[exportName];
return Response.json({ success: true, tool });
} catch (error) {
// Try esm.sh fallback
const esmUrl = `https://esm.sh/${packageName}@${version}?target=esnext`;
const module = await import(esmUrl);
const tool = module[exportName];
return Response.json({ success: true, tool });
}
}
Deno.serve({ port: 3001 }, handler);
```
## Live Error Logs
**Request:**
```bash
curl -X POST https://endearing-commitment-production.up.railway.app/load-and-describe \
-H "Content-Type: application/json" \
-d '{
"packageName": "ai-sdk-tool-code-execution",
"exportName": "executeCode",
"version": "0.0.2",
"importUrl": "https://esm.sh/ai-sdk-tool-code-execution@0.0.2"
}'
```
**Response:**
```json
{
"success": false,
"error": "Failed to import package: Module not found \"https://esm.sh/node:sqlite?target=esnext\"",
"details": {
"npmError": "Loading unprepared module: npm:ai-sdk-tool-code-execution@0.0.2",
"esmError": "Module not found \"https://esm.sh/node:sqlite?target=esnext\""
}
}
```
## Additional Context
- We successfully load other packages (e.g., `@tpmjs/hello`, `zod-to-json-schema`)
- Only packages with Node.js built-in dependencies fail
- Switching to Node.js would work, but we prefer Deno's security model
- This is for a production tool registry serving AI SDK tools to users
## Related Resources
- **Deno npm compatibility:** https://deno.com/manual/node/npm_specifiers
- **Deno Node built-ins:** https://deno.com/manual/node/node_specifiers
- **esm.sh documentation:** https://esm.sh/
- **Package source:** https://www.npmjs.com/package/ai-sdk-tool-code-execution
- **Deno SQLite libraries:** https://deno.land/x/sqlite@v3.8
---
**Question for ChatGPT:** Is there any way to make `ai-sdk-tool-code-execution` work in Deno, given these constraints? If not, what's the closest alternative that would work in Deno's runtime?

View file

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

View file

@ -3,6 +3,8 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
import { Input } from '@tpmjs/ui/Input/Input';
import { ToolHealthBadge } from '@tpmjs/ui/ToolHealthBadge/ToolHealthBadge';
import { ToolHealthBanner } from '@tpmjs/ui/ToolHealthBanner/ToolHealthBanner';
import { useEffect, useState } from 'react';
interface Tool {
@ -16,6 +18,10 @@ interface Tool {
frameworks?: string[];
env?: Array<{ name: string; description: string; required?: boolean; default?: string }>;
importUrl?: string;
importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
healthCheckError?: string | null;
lastHealthCheck?: string | null;
}
export function ToolsSidebar(): React.ReactElement {
@ -86,11 +92,16 @@ export function ToolsSidebar(): React.ReactElement {
<p className="text-xs text-foreground-secondary line-clamp-2">
{tool.description}
</p>
<div className="mt-2 flex items-center gap-2">
<div className="mt-2 flex items-center gap-2 flex-wrap">
<Badge variant="secondary" size="sm">
{tool.category}
</Badge>
<span className="text-xs text-foreground-tertiary">v{tool.version}</span>
<ToolHealthBadge
importHealth={tool.importHealth}
executionHealth={tool.executionHealth}
size="sm"
/>
</div>
</CardContent>
</Card>
@ -156,6 +167,15 @@ export function ToolsSidebar(): React.ReactElement {
</div>
</div>
{/* Health warning banner */}
<ToolHealthBanner
importHealth={selectedTool.importHealth}
executionHealth={selectedTool.executionHealth}
healthCheckError={selectedTool.healthCheckError}
lastHealthCheck={selectedTool.lastHealthCheck}
className="mb-6"
/>
{/* Description */}
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Description</h3>

View file

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

View file

@ -25,7 +25,7 @@ export async function GET() {
// Official tools count (isOfficial is at package level)
prisma.tool.count({
where: {
package: { isOfficial: true }
package: { isOfficial: true },
},
}),

View file

@ -5,46 +5,46 @@
@layer base {
/* Light mode (default) */
:root {
/* Backgrounds & Surfaces */
--background: 0 0% 100%; /* Pure white */
--surface: 210 20% 98%; /* Off-white */
/* Backgrounds & Surfaces - DRAMATIC CONTRAST */
--background: 220 15% 96%; /* Light blue-gray background */
--surface: 0 0% 100%; /* Pure white - cards really pop! */
--surface-elevated: 0 0% 100%; /* White (elevated) */
--surface-overlay: 0 0% 98%; /* Light gray */
--surface-overlay: 0 0% 100%; /* White overlays */
/* Foreground (Text) */
--foreground: 222 47% 11%; /* Almost black */
--foreground-secondary: 215 16% 47%; /* Medium gray */
--foreground-tertiary: 215 16% 65%; /* Light gray */
--foreground-muted: 215 16% 75%; /* Very light gray */
--foreground-secondary: 215 25% 35%; /* Much darker for readability */
--foreground-tertiary: 215 20% 50%; /* Medium gray */
--foreground-muted: 215 16% 65%; /* Light gray */
/* Borders */
--border: 214 32% 91%; /* Light gray */
--border-strong: 214 32% 80%; /* Medium gray */
--border-subtle: 214 20% 95%; /* Very light gray */
/* Borders - MUCH MORE VISIBLE */
--border: 214 25% 80%; /* Strong medium gray */
--border-strong: 214 30% 60%; /* Dark gray for emphasis */
--border-subtle: 214 20% 88%; /* Subtle but visible */
/* Interactive States */
--primary: 222 47% 11%; /* Dark for light mode */
--primary-foreground: 210 40% 98%; /* Light text */
--secondary: 210 40% 96%; /* Light secondary */
/* Interactive States - MODERN & REFINED */
--primary: 221 83% 53%; /* Sophisticated blue */
--primary-foreground: 0 0% 100%; /* White text */
--secondary: 220 15% 90%; /* Subtle gray-blue bg */
--secondary-foreground: 222 47% 11%; /* Dark text */
--accent: 210 40% 96%; /* Accent bg */
--accent-foreground: 222 47% 11%; /* Accent text */
--muted: 210 40% 96%; /* Muted bg */
--muted-foreground: 215 16% 47%; /* Muted text */
--accent: 221 75% 95%; /* Soft blue tint */
--accent-foreground: 221 70% 35%; /* Rich blue text */
--muted: 220 15% 92%; /* Subtle muted bg */
--muted-foreground: 215 25% 40%; /* Darker muted text */
/* Status Colors */
--success: 142 71% 45%;
--success-foreground: 142 76% 15%;
--error: 0 72% 51%;
--error-foreground: 0 86% 17%;
--warning: 38 92% 50%;
--warning-foreground: 48 96% 19%;
--info: 217 91% 60%;
--info-foreground: 214 95% 23%;
/* Status Colors - Modern Editorial Palette */
--success: 152 57% 45%; /* Refined emerald green */
--success-foreground: 0 0% 100%; /* White text on success */
--error: 0 65% 51%; /* Sophisticated red, less harsh */
--error-foreground: 0 0% 100%; /* White text on error */
--warning: 36 100% 50%; /* Warm sophisticated amber */
--warning-foreground: 0 0% 100%; /* White text on warning */
--info: 210 100% 56%; /* Cool modern blue */
--info-foreground: 0 0% 100%; /* White text on info */
/* Destructive (legacy) */
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--destructive: 0 65% 51%; /* Match error color */
--destructive-foreground: 0 0% 100%;
/* Form Elements */
--input: 214 32% 91%;
@ -52,11 +52,11 @@
--ring-offset: 0 0% 100%;
/* Grid/Blueprint Pattern */
--grid-color: 214 32% 95%;
--grid-color: 214 32% 90%;
--grid-size: 24px; /* Grid cell size */
/* Card */
--card: 0 0% 100%;
--card: 0 0% 100%; /* Pure white - stands out dramatically on blue-gray background */
--card-foreground: 222 47% 11%;
/* Border Radii */
@ -93,9 +93,9 @@
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
/* Brutalist Accent Colors */
--brutalist-accent: 221 83% 53%; /* #0066ff - Electric blue for light mode */
--brutalist-accent-hover: 221 83% 43%;
/* Brutalist Accent Colors - Modern Editorial */
--brutalist-accent: 221 83% 53%; /* Sophisticated blue */
--brutalist-accent-hover: 221 83% 45%; /* Slightly deeper on hover */
}
/* Dark mode (opt-in) - Vercel/Cursor/Perplexity aesthetic */
@ -127,19 +127,19 @@
--muted: 210 10% 12%; /* Muted background */
--muted-foreground: 210 8% 60%; /* Muted text */
/* Status Colors (desaturated for dark mode) */
--success: 142 71% 45%; /* #10b981 - Green */
--success-foreground: 142 76% 95%; /* Light green text */
--error: 0 72% 51%; /* #ef4444 - Red */
--error-foreground: 0 86% 97%; /* Light red text */
--warning: 38 92% 50%; /* #f59e0b - Amber */
--warning-foreground: 48 96% 89%; /* Light amber text */
--info: 217 91% 60%; /* #3b82f6 - Blue */
--info-foreground: 214 95% 93%; /* Light blue text */
/* Status Colors - Modern Editorial (Dark Mode) */
--success: 152 57% 50%; /* Refined emerald - slightly brighter for dark */
--success-foreground: 0 0% 100%; /* White text */
--error: 0 65% 58%; /* Sophisticated red - brighter for dark */
--error-foreground: 0 0% 100%; /* White text */
--warning: 36 100% 55%; /* Warm amber - brighter for dark */
--warning-foreground: 0 0% 100%; /* White text */
--info: 210 100% 60%; /* Cool blue - brighter for dark */
--info-foreground: 0 0% 100%; /* White text */
/* Destructive (legacy support) */
--destructive: 0 72% 51%;
--destructive-foreground: 0 86% 97%;
--destructive: 0 65% 58%;
--destructive-foreground: 0 0% 100%;
/* Form Elements */
--input: 210 10% 20%; /* Input border */
@ -153,9 +153,9 @@
--card: 210 10% 8%;
--card-foreground: 210 10% 90%;
/* Brutalist Accent Colors */
--brutalist-accent: 158 100% 50%; /* #00ff88 - Neon green for dark mode */
--brutalist-accent-hover: 158 100% 40%;
/* Brutalist Accent Colors - Modern Editorial */
--brutalist-accent: 210 100% 60%; /* Cool sophisticated blue for dark mode */
--brutalist-accent-hover: 210 100% 65%; /* Slightly brighter on hover */
}
/* Base element styles */

View file

@ -9,21 +9,16 @@ import { HeroSection } from '../components/home/HeroSection';
async function getHomePageData() {
try {
// Fetch stats in parallel
const [toolCount, simulationCount, featuredTools, categoryStats] = await Promise.all([
const [packageCount, toolCount, featuredTools, categoryStats] = await Promise.all([
// Total package count
prisma.package.count(),
// Total tool count
prisma.tool.count(),
// Total successful simulations (as proxy for invocations)
prisma.simulation.count({
where: { status: 'success' },
}),
// Top 6 featured tools by quality score
prisma.tool.findMany({
orderBy: [
{ qualityScore: 'desc' },
{ package: { npmDownloadsLastMonth: 'desc' } },
],
orderBy: [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }],
take: 6,
select: {
id: true,
@ -50,30 +45,10 @@ async function getHomePageData() {
}),
]);
// Calculate average latency from recent successful simulations
const recentSimulations = await prisma.simulation.findMany({
where: {
status: 'success',
executionTimeMs: { not: null },
},
select: { executionTimeMs: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
const avgLatency =
recentSimulations.length > 0
? Math.round(
recentSimulations.reduce((sum, s) => sum + (s.executionTimeMs || 0), 0) /
recentSimulations.length
)
: 0;
return {
stats: {
packageCount,
toolCount,
invocations: simulationCount,
avgLatency,
categoryCount: categoryStats.length,
},
featuredTools,
@ -86,9 +61,8 @@ async function getHomePageData() {
console.error('Failed to fetch homepage data:', error);
return {
stats: {
packageCount: 0,
toolCount: 0,
invocations: 0,
avgLatency: 0,
categoryCount: 0,
},
featuredTools: [],
@ -238,17 +212,30 @@ export default async function HomePage(): Promise<React.ReactElement> {
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-sm text-foreground-secondary">© 2025 TPMJS. All rights reserved.</p>
<div className="flex items-center gap-4 text-sm">
<button type="button" className="text-foreground-secondary hover:text-foreground">
Privacy
</button>
<span className="text-border">·</span>
<button type="button" className="text-foreground-secondary hover:text-foreground">
Terms
</button>
<span className="text-border">·</span>
<button type="button" className="text-foreground-secondary hover:text-foreground">
<a
href="mailto:hello@tpmjs.com"
className="text-foreground-secondary hover:text-foreground transition-colors"
>
Contact
</button>
</a>
<span className="text-border">·</span>
<a
href="https://github.com/your-org/tpmjs"
target="_blank"
rel="noopener noreferrer"
className="text-foreground-secondary hover:text-foreground transition-colors"
>
GitHub
</a>
<span className="text-border">·</span>
<a
href="https://twitter.com/tpmjs"
target="_blank"
rel="noopener noreferrer"
className="text-foreground-secondary hover:text-foreground transition-colors"
>
Twitter
</a>
</div>
</div>
</Container>

View file

@ -464,7 +464,7 @@ export default function ToolDetailPage({
)}
{/* Tags */}
{tool.tags.length > 0 && (
{tool.tags && tool.tags.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Tags</CardTitle>

View file

@ -52,6 +52,8 @@ export default function ToolSearchPage(): React.ReactElement {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [officialCount, setOfficialCount] = useState(0);
// Fetch tools from API
useEffect(() => {
@ -79,14 +81,32 @@ export default function ToolSearchPage(): React.ReactElement {
params.set('broken', 'true');
}
const response = await fetch(`/api/tools?${params.toString()}`);
const data = await response.json();
// Fetch tools and counts in parallel
const [toolsResponse, allCountResponse, officialCountResponse] = await Promise.all([
fetch(`/api/tools?${params.toString()}`),
fetch('/api/tools'),
fetch('/api/tools?official=true'),
]);
if (data.success) {
const fetchedTools = data.data;
const [toolsData, allCountData, officialCountData] = await Promise.all([
toolsResponse.json(),
allCountResponse.json(),
officialCountResponse.json(),
]);
if (toolsData.success) {
const fetchedTools = toolsData.data;
setTools(fetchedTools);
setError(null);
// Update counts
if (allCountData.success) {
setTotalCount(allCountData.data.length);
}
if (officialCountData.success) {
setOfficialCount(officialCountData.data.length);
}
// Extract unique categories from all tools
const categories = new Set<string>();
@ -98,7 +118,7 @@ export default function ToolSearchPage(): React.ReactElement {
setAvailableCategories(Array.from(categories).sort());
} else {
setError(data.error || 'Failed to fetch tools');
setError(toolsData.error || 'Failed to fetch tools');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
@ -186,11 +206,11 @@ export default function ToolSearchPage(): React.ReactElement {
{/* Tabs */}
<Tabs
tabs={[
{ id: 'all', label: 'All Tools', count: tools.length },
{ id: 'all', label: 'All Tools', count: totalCount },
{
id: 'featured',
label: 'Official',
count: tools.filter((t) => t.package.isOfficial).length,
count: officialCount,
},
]}
activeTab={activeTab}

View file

@ -7,9 +7,8 @@ import { useState } from 'react';
interface HeroSectionProps {
stats: {
packageCount: number;
toolCount: number;
invocations: number;
avgLatency: number;
categoryCount: number;
};
}
@ -71,23 +70,15 @@ export function HeroSection({ stats }: HeroSectionProps): React.ReactElement {
{/* Live Metrics Strip */}
<div className="mb-12 flex flex-wrap items-center gap-3 border-l-[6px] border-brutalist-accent pl-6 font-mono text-base md:text-lg font-bold uppercase tracking-wider">
<div className="flex items-center gap-2">
<span className="text-foreground">{formatNumber(stats.packageCount)}</span>
<span className="text-foreground-secondary">PACKAGES</span>
</div>
<span className="text-foreground-tertiary">/</span>
<div className="flex items-center gap-2">
<span className="text-foreground">{formatNumber(stats.toolCount)}</span>
<span className="text-foreground-secondary">TOOLS</span>
</div>
<span className="text-foreground-tertiary">/</span>
<div className="flex items-center gap-2">
<span className="text-foreground">
{formatNumber(stats.invocations)}
{stats.invocations >= 1000 ? '+' : ''}
</span>
<span className="text-foreground-secondary">INVOCATIONS</span>
</div>
<span className="text-foreground-tertiary">/</span>
<div className="flex items-center gap-2">
<span className="text-foreground">{stats.avgLatency || '--'}ms</span>
<span className="text-foreground-secondary">AVG LATENCY</span>
</div>
</div>
{/* Subheading */}

View file

@ -109,9 +109,7 @@ export function createToolDefinition(tool: Tool & { package: Package }) {
console.log('[createToolDefinition] Created Zod schema:', inputSchema);
const sanitizedName = sanitizeToolName(
`${tool.package.npmPackageName}-${tool.exportName}`
);
const sanitizedName = sanitizeToolName(`${tool.package.npmPackageName}-${tool.exportName}`);
// AI SDK v6 tool definition
return {
@ -199,9 +197,7 @@ export async function executeToolWithAgent(
onTokenUpdate?: (tokens: Partial<TokenBreakdown>) => void
) {
const toolDef = createToolDefinition(tool);
const sanitizedToolName = sanitizeToolName(
`${tool.package.npmPackageName}-${tool.exportName}`
);
const sanitizedToolName = sanitizeToolName(`${tool.package.npmPackageName}-${tool.exportName}`);
console.log('[executeToolWithAgent] Tool name:', sanitizedToolName);

View file

@ -10,17 +10,10 @@
"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",
"frameworks": [
"vercel-ai"
],
"frameworks": ["vercel-ai"],
"tools": [
{
"exportName": "helloWorldTool",
@ -70,8 +63,5 @@
"tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"files": [
"dist",
"README.md"
]
"files": ["dist", "README.md"]
}

View file

@ -118,6 +118,10 @@ export const searchTpmjsToolsTool = tool({
env: tool.package.env,
version: tool.package.npmVersion,
importUrl: `https://esm.sh/${tool.package.npmPackageName}@${tool.package.npmVersion}`,
importHealth: tool.importHealth,
executionHealth: tool.executionHealth,
healthCheckError: tool.healthCheckError,
lastHealthCheck: tool.lastHealthCheck,
})),
};
},

View file

@ -170,9 +170,10 @@ export function validateTpmjsField(tpmjs: unknown): ValidationResult {
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;
const hasRichFields =
data.tools.some((tool) => tool.parameters || tool.returns || tool.aiAgent) ||
data.env ||
data.frameworks;
return {
valid: true,
@ -203,8 +204,11 @@ export function validateTpmjsField(tpmjs: unknown): ValidationResult {
};
const hasRichFields =
legacyData.parameters || legacyData.returns || legacyData.env ||
legacyData.frameworks || legacyData.aiAgent;
legacyData.parameters ||
legacyData.returns ||
legacyData.env ||
legacyData.frameworks ||
legacyData.aiAgent;
return {
valid: true,

View file

@ -134,6 +134,14 @@
"./ActivityStream/ActivityStream": {
"types": "./dist/ActivityStream/ActivityStream.d.ts",
"default": "./dist/ActivityStream/ActivityStream.js"
},
"./ToolHealthBadge/ToolHealthBadge": {
"types": "./dist/ToolHealthBadge/ToolHealthBadge.d.ts",
"default": "./dist/ToolHealthBadge/ToolHealthBadge.js"
},
"./ToolHealthBanner/ToolHealthBanner": {
"types": "./dist/ToolHealthBanner/ToolHealthBanner.d.ts",
"default": "./dist/ToolHealthBanner/ToolHealthBanner.js"
}
},
"files": ["dist"],
@ -152,6 +160,7 @@
},
"dependencies": {
"@tpmjs/utils": "workspace:*",
"react-syntax-highlighter": "^16.1.0",
"zod": "^4.1.13"
},
"devDependencies": {
@ -163,6 +172,7 @@
"@tpmjs/tsconfig": "workspace:*",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/react-syntax-highlighter": "^15.5.13",
"eslint": "^9.39.1",
"glob": "^13.0.0",
"happy-dom": "^15.11.7",

View file

@ -1,5 +1,7 @@
import { cn } from '@tpmjs/utils/cn';
import { forwardRef, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Icon } from '../Icon/Icon';
import type { CodeBlockProps } from './types';
import {
@ -8,11 +10,75 @@ import {
codeBlockCopyButtonVariants,
} from './variants';
// Custom light theme with better contrast
const customLightTheme = {
'code[class*="language-"]': {
color: '#24292e',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
wordWrap: 'normal',
lineHeight: '1.5',
tabSize: '4',
hyphens: 'none',
},
'pre[class*="language-"]': {
color: '#24292e',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
wordWrap: 'normal',
lineHeight: '1.5',
tabSize: '4',
hyphens: 'none',
padding: '1em',
margin: '0.5em 0',
overflow: 'auto',
},
comment: { color: '#6a737d', fontStyle: 'italic' },
prolog: { color: '#6a737d' },
doctype: { color: '#6a737d' },
cdata: { color: '#6a737d' },
punctuation: { color: '#24292e' },
property: { color: '#005cc5' },
tag: { color: '#22863a' },
boolean: { color: '#005cc5' },
number: { color: '#005cc5' },
constant: { color: '#005cc5' },
symbol: { color: '#005cc5' },
deleted: { color: '#b31d28' },
selector: { color: '#22863a' },
'attr-name': { color: '#6f42c1' },
string: { color: '#032f62' },
char: { color: '#032f62' },
builtin: { color: '#005cc5' },
inserted: { color: '#22863a' },
operator: { color: '#d73a49' },
entity: { color: '#6f42c1' },
url: { color: '#032f62' },
variable: { color: '#e36209' },
atrule: { color: '#d73a49' },
'attr-value': { color: '#032f62' },
function: { color: '#6f42c1' },
'class-name': { color: '#6f42c1' },
keyword: { color: '#d73a49' },
regex: { color: '#032f62' },
important: { color: '#d73a49', fontWeight: 'bold' },
bold: { fontWeight: 'bold' },
italic: { fontStyle: 'italic' },
};
/**
* CodeBlock component
*
* Displays formatted code with optional copy functionality.
* Includes syntax-highlighted display and copy-to-clipboard button.
* Displays formatted code with syntax highlighting and optional copy functionality.
* Uses Prism syntax highlighter for beautiful code display.
* Built with React and JSX.
*
* @example
@ -32,7 +98,10 @@ import {
* ```
*/
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
({ className, code, language = 'text', size = 'md', showCopy = true, ...props }, ref) => {
(
{ className, code, language = 'text', size = 'md', showCopy = true, theme = 'light', ...props },
ref
) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@ -46,16 +115,42 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
}
};
// Map language aliases to supported languages
const languageMap: Record<string, string> = {
js: 'javascript',
ts: 'typescript',
jsx: 'jsx',
tsx: 'tsx',
py: 'python',
rb: 'ruby',
sh: 'bash',
yml: 'yaml',
};
const normalizedLanguage = languageMap[language] || language;
return (
<div ref={ref} className={cn(codeBlockContainerVariants(), className)} {...props}>
<code
className={codeBlockCodeVariants({
size,
})}
data-language={language}
>
{code}
</code>
<div className={codeBlockCodeVariants({ size })} data-language={normalizedLanguage}>
<SyntaxHighlighter
language={normalizedLanguage}
style={theme === 'dark' ? oneDark : oneLight}
customStyle={{
margin: 0,
padding: 0,
background: 'transparent',
fontSize: 'inherit',
lineHeight: 'inherit',
}}
codeTagProps={{
style: {
fontFamily: 'inherit',
},
}}
>
{code}
</SyntaxHighlighter>
</div>
{showCopy && (
<button
type="button"

View file

@ -10,7 +10,7 @@ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
code: string;
/**
* Programming language (for display/metadata only)
* Programming language for syntax highlighting
* @default 'text'
*/
language?: string;
@ -26,6 +26,12 @@ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
* @default true
*/
showCopy?: boolean;
/**
* Color theme for syntax highlighting
* @default 'light'
*/
theme?: 'light' | 'dark';
}
/**

View file

@ -12,8 +12,8 @@ export const inputVariants = createVariants({
'font-sans',
// Borders & Radius
'rounded-md border',
// Background
'bg-background',
// Background - Pure white to stand out
'bg-surface',
// Transitions
'transition-base',
// Focus

View file

@ -12,8 +12,8 @@ export const textareaVariants = createVariants({
'font-sans',
// Borders & Radius
'rounded-md border',
// Background
'bg-background',
// Background - Pure white to stand out
'bg-surface',
// Transitions
'transition-base',
// Focus

View file

@ -0,0 +1,35 @@
'use client';
import type React from 'react';
import { Badge } from '../Badge/Badge';
import { Icon } from '../Icon/Icon';
export interface ToolHealthBadgeProps {
importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
/**
* Badge component to indicate if a tool is broken (import or execution failed)
*/
export function ToolHealthBadge({
importHealth,
executionHealth,
size = 'sm',
className,
}: ToolHealthBadgeProps): React.ReactElement | null {
const isBroken = importHealth === 'BROKEN' || executionHealth === 'BROKEN';
if (!isBroken) {
return null;
}
return (
<Badge variant="error" size={size} className={className}>
<Icon icon="x" size="sm" className="mr-1" />
Broken
</Badge>
);
}

View file

@ -0,0 +1,86 @@
'use client';
import type React from 'react';
import { Badge } from '../Badge/Badge';
export interface ToolHealthBannerProps {
importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
healthCheckError?: string | null;
lastHealthCheck?: string | null;
onRecheck?: () => void;
recheckLoading?: boolean;
className?: string;
}
/**
* Banner component to display detailed health status for broken tools
*/
export function ToolHealthBanner({
importHealth,
executionHealth,
healthCheckError,
lastHealthCheck,
onRecheck,
recheckLoading = false,
className,
}: ToolHealthBannerProps): React.ReactElement | null {
const isBroken = importHealth === 'BROKEN' || executionHealth === 'BROKEN';
if (!isBroken) {
return null;
}
return (
<div
className={`p-4 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 ${className || ''}`}
>
<div className="flex items-start gap-3">
<span className="text-xl mt-0.5"></span>
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-800 dark:text-red-300 mb-1">
This tool is currently broken
</h3>
<div className="space-y-1 text-sm text-red-700 dark:text-red-400">
{importHealth === 'BROKEN' && (
<div className="flex items-center gap-2">
<Badge variant="error" size="sm">
Import Failed
</Badge>
<span className="text-xs">Cannot load from Railway service</span>
</div>
)}
{executionHealth === 'BROKEN' && (
<div className="flex items-center gap-2">
<Badge variant="error" size="sm">
Execution Failed
</Badge>
<span className="text-xs">Runtime error with test parameters</span>
</div>
)}
</div>
{healthCheckError && (
<pre className="mt-2 p-2 rounded bg-red-100 dark:bg-red-900/30 text-xs font-mono text-red-800 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
{healthCheckError}
</pre>
)}
{lastHealthCheck && (
<p className="text-xs text-red-600 dark:text-red-500 mt-2">
Last checked: {new Date(lastHealthCheck).toLocaleString()}
</p>
)}
{onRecheck && (
<button
type="button"
onClick={onRecheck}
disabled={recheckLoading}
className="mt-3 text-sm font-medium text-red-700 dark:text-red-400 hover:underline disabled:opacity-50"
>
{recheckLoading ? 'Rechecking...' : 'Recheck health →'}
</button>
)}
</div>
</div>
</div>
);
}

View file

@ -14,8 +14,8 @@ export const formInputBase = [
'font-sans text-base',
// Borders and radius
'rounded-md border border-border',
// Colors
'bg-background text-foreground',
// Colors - Pure white background to stand out
'bg-surface text-foreground',
// Placeholder
'placeholder:text-foreground-tertiary',
// Transitions

253
pnpm-lock.yaml generated
View file

@ -131,10 +131,10 @@ importers:
version: 10.4.22(postcss@8.5.6)
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@1.21.7)
version: 9.39.1(jiti@2.6.1)
eslint-config-next:
specifier: ^16.0.4
version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
postcss:
specifier: ^8.5.1
version: 8.5.6
@ -571,6 +571,9 @@ importers:
'@tpmjs/utils':
specifier: workspace:*
version: link:../utils
react-syntax-highlighter:
specifier: ^16.1.0
version: 16.1.0(react@19.2.0)
zod:
specifier: ^4.1.13
version: 4.1.13
@ -599,6 +602,9 @@ importers:
'@types/react-dom':
specifier: ^19.0.2
version: 19.2.3(@types/react@19.2.7)
'@types/react-syntax-highlighter':
specifier: ^15.5.13
version: 15.5.13
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@2.6.1)
@ -6882,11 +6888,6 @@ snapshots:
'@esbuild/win32-x64@0.27.0':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))':
dependencies:
eslint: 9.39.1(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))':
dependencies:
eslint: 9.39.1(jiti@2.6.1)
@ -7940,23 +7941,6 @@ snapshots:
'@types/uuid@9.0.8': {}
'@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.0
eslint: 9.39.1(jiti@1.21.7)
graphemer: 1.4.0
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@ -7974,18 +7958,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.0
debug: 4.4.3
eslint: 9.39.1(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.48.0
@ -8016,18 +7988,6 @@ snapshots:
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.1(jiti@1.21.7)
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.48.0
@ -8057,17 +8017,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7))
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
eslint: 9.39.1(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
@ -9246,32 +9195,12 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 16.0.4
eslint: 9.39.1(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@1.21.7))
globals: 16.4.0
typescript-eslint: 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
eslint-config-next@16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 16.0.4
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
@ -9294,22 +9223,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
eslint: 9.39.1(jiti@1.21.7)
get-tsconfig: 4.13.0
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@ -9334,23 +9248,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)):
dependencies:
debug: 3.2.7
optionalDependencies:
eslint: 9.39.1(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@ -9383,33 +9287,6 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
array.prototype.findlastindex: 1.2.6
array.prototype.flat: 1.3.3
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.39.1(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.1
semver: 6.3.1
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
@ -9421,7 +9298,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -9437,25 +9314,6 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@1.21.7)):
dependencies:
aria-query: 5.3.2
array-includes: 3.1.9
array.prototype.flatmap: 1.3.3
ast-types-flow: 0.0.8
axe-core: 4.11.0
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
eslint: 9.39.1(jiti@1.21.7)
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
minimatch: 3.1.2
object.fromentries: 2.0.8
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@2.6.1)):
dependencies:
aria-query: 5.3.2
@ -9479,17 +9337,6 @@ snapshots:
dependencies:
eslint: 9.39.1(jiti@2.6.1)
eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@1.21.7)):
dependencies:
'@babel/core': 7.28.5
'@babel/parser': 7.28.5
eslint: 9.39.1(jiti@1.21.7)
hermes-parser: 0.25.1
zod: 4.1.13
zod-validation-error: 4.0.2(zod@4.1.13)
transitivePeerDependencies:
- supports-color
eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@babel/core': 7.28.5
@ -9501,28 +9348,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)):
dependencies:
array-includes: 3.1.9
array.prototype.findlast: 1.2.5
array.prototype.flatmap: 1.3.3
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.1
eslint: 9.39.1(jiti@1.21.7)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
minimatch: 3.1.2
object.entries: 1.1.9
object.fromentries: 2.0.8
object.values: 1.2.1
prop-types: 15.8.1
resolve: 2.0.0-next.5
semver: 6.3.1
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)):
dependencies:
array-includes: 3.1.9
@ -9554,47 +9379,6 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
eslint@9.39.1(jiti@1.21.7):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.1
'@eslint/js': 9.39.1
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.7
transitivePeerDependencies:
- supports-color
eslint@9.39.1(jiti@2.6.1):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
@ -12448,17 +12232,6 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typescript-eslint@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.1(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript-eslint@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)

View file

@ -1,12 +1,12 @@
import { prisma } from './packages/db/src/index.js';
import { manualTools } from './manual-tools.js';
import { prisma } from './packages/db/src/index.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;
const skipped = 0;
let errors = 0;
for (const manualTool of manualTools) {
@ -26,7 +26,8 @@ async function syncManualTools() {
const version = manualTool.npmVersion || npmData.version;
// Get published date
const publishedAt = npmData.time?.[version] || npmData.time?.modified || new Date().toISOString();
const publishedAt =
npmData.time?.[version] || npmData.time?.modified || new Date().toISOString();
// Upsert Package record
const packageRecord = await prisma.package.upsert({
@ -119,7 +120,7 @@ async function syncManualTools() {
console.log(` Total manual tools: ${manualTools.length}\n`);
}
function calculateTier(tool: typeof manualTools[0]): 'minimal' | 'rich' {
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';