Compare commits

...

256 commits

Author SHA1 Message Date
Ajax Davis
b682bf0d7b chore: version packages 2025-12-31 21:07:20 +10:00
Ajax Davis
1ea9b83039 feat: fully implement 5 research tools with production-ready functionality
Research tools now have complete implementations:

- **page-brief**: Uses @mozilla/readability + jsdom for content extraction,
  sbd for sentence parsing, comprehensive error handling with timeout and
  network error detection

- **compare-pages**: Uses natural library TF-IDF for text similarity,
  detects agreements via high-similarity sentences, identifies conflicts
  using negation pattern analysis

- **source-credibility**: Uses tldts for domain parsing, cheerio for HTML
  analysis, calculates 6 credibility signals (HTTPS, domain reputation,
  author, date, citations, contact info), weighted scoring system

- **claim-checklist**: Uses sbd for sentence boundary detection, regex
  patterns for claim identification (statistics, quotes, historical,
  scientific, factual), priority levels and evidence suggestions

- **timeline-from-text**: Uses chrono-node for date parsing, sbd for
  context extraction, calculates confidence scores, identifies gaps >30 days

All tools follow AI SDK v6 pattern (tool() + jsonSchema()), include proper
TypeScript types, input validation, comprehensive error handling, and
Node.js 18+ fetch requirement verification.

Updated blocks.yml with comprehensive philosophy and domain rules for
quality validation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-31 21:04:07 +10:00
Ajax Davis
6990906c6e chore: version packages 2025-12-31 20:31:57 +10:00
Ajax Davis
072489fd50 fix: use 'tpmjs' keyword instead of deprecated 'tpmjs-tool'
- Add 'research' to valid TPMJS_CATEGORIES
- Update all tutorial slides to reference 'tpmjs' keyword
- Deprecate 'tpmjs-tool' keyword for package discovery
2025-12-31 20:28:37 +10:00
Ajax Davis
81405982ce chore: version packages 2025-12-31 19:52:33 +10:00
Ajax Davis
aaf3cdd424 feat: add 5 research tools and blocks framework setup
- Add @tpmjs/tools-page-brief for URL content extraction
- Add @tpmjs/tools-compare-pages for cross-source validation
- Add @tpmjs/tools-source-credibility for credibility scoring
- Add @tpmjs/tools-claim-checklist for factual claim extraction
- Add @tpmjs/tools-timeline-from-text for timeline generation
- Move createBlogPost to packages/tools/official/
- Add blocks.yml for Blocks framework validation
- Update pnpm-workspace.yaml to include official tools
2025-12-31 19:47:02 +10:00
Ajax Davis
7edd4f43b2 refactor: redesign spec page with clean toggle between specification and example views
- Convert to client component with useState for view toggling
- Add "Specification" view with clean schema reference and sidebar
- Add "Full Example" view with complete package.json and tool code examples
- Remove explanatory content (moved elsewhere)
- Create layout.tsx for metadata since page is client component
- Reduce page complexity from 839 to 406 lines
2025-12-31 13:15:49 +10:00
Ajax Davis
4043df2f91 docs: add feature roadmap for users, collections, and ratings 2025-12-31 12:54:46 +10:00
Ajax Davis
e9a7a689a1 perf: reduce Neon compute usage with cron + caching optimizations
- Reduce cron frequency: changes 2min→4hr, keyword 15min→6hr, metrics hourly→daily
- Add Prisma directUrl for connection pooling support
- Add Vercel KV caching to /api/tools endpoint (graceful degradation if not configured)
- Add X-Cache header to indicate cache hit/miss
- Add NEON_COMPUTE_OPTIMIZATION.md with full strategy guide

These changes should reduce Neon CU usage from 100+ to ~20-30 CU-hrs/month.
2025-12-31 12:14:00 +10:00
Ajax Davis
24bc48bcef fix: allow scrolling on tutorials index page 2025-12-31 01:35:40 +10:00
Ajax Davis
10116cd460 fix: escape JSX entities in tutorial slides 2025-12-31 01:11:08 +10:00
Ajax Davis
5d9e256660 feat: add 3 new tutorials (first-tool, agent-example, playground)
- Build Your First Tool: Step-by-step guide from npm init to published
- Real-World Agent: Complete working example with meta-tools
- Interactive Playground: How to test tools in browser

Each tutorial has 5-6 slides with code examples and animations.
Updated main page to show all 6 tutorials in grid.
2025-12-31 01:02:21 +10:00
Ajax Davis
6ef33735a7 feat: add general overview tutorial
- Add 6-slide overview tutorial covering TPMJS concepts
- Slides: Welcome, What is TPMJS, Ecosystem, Architecture, Use Cases, Explore
- Add OverviewSlideshow component and page route at /overview
- Update main tutorial page with overview as first card
2025-12-30 13:53:54 +10:00
Ajax Davis
d132da9c20 fix: use z.toJSONSchema for Zod v4 schema conversion
Import zod@4 directly and use z.toJSONSchema method with fallback
to z.default.toJSONSchema for compatibility.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 23:29:06 +10:00
Ajax Davis
bfdb4064b1 fix: use dynamic import for Zod v4 JSON schema conversion
Static import from 'zod@4/json-schema' was failing and crashing executor.
Use dynamic import inside the schema extraction logic instead.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 23:25:40 +10:00
Ajax Davis
1fd5407944 fix: add Zod v4 schema extraction support in Railway executor
- Import Zod v4's toJSONSchema from zod@4/json-schema
- Add Strategy 3: Detect Zod v4 schemas via ._zod property
- Convert Zod v4 schemas using native toJSONSchema function
- Update error message to mention both Zod v4 and v3 support

Fixes schema extraction for @tpmjs/emoji-magic and other Zod v4 tools

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 23:21:05 +10:00
Ajax Davis
957cfd5828 fix: URL-decode slug to handle scoped packages with encoded @ symbol
The @ in scoped package names was being URL-encoded to %40, causing
parseSlug to treat them as unscoped packages. Now we decode the slug
components first.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 22:16:15 +10:00
Ajax Davis
a03e7a560b fix: format CSS 2025-12-29 22:03:57 +10:00
Ajax Davis
5200f271f9 fix: remove unused error variable in cache.ts
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 21:56:48 +10:00
Ajax Davis
7e6c0194c6 fix: use two-step query to fix scoped package 404s on tool detail page
The relation filter `package: { npmPackageName }` was not working correctly.
Now first find the package, then find the tool by packageId.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 21:50:48 +10:00
Ajax Davis
364f507518 fix: add force-dynamic to tool detail page to fix scoped package 404s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 21:42:26 +10:00
Ajax Davis
f24eebc196 feat: add npm-like features to tool detail page and update keyword to tpmjs
- Add download trend sparkline chart showing 30-day download history
- Add bundle size component with minified/gzipped sizes via bundlephobia proxy
- Add more install commands: yarn, bun, deno (in addition to npm, pnpm)
- Change discovery keyword from "tpmjs-tool" to "tpmjs" across entire codebase
- Update sync endpoints to use new keyword
- Update all documentation and package.json files

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 21:30:42 +10:00
Ajax Davis
6c28f01633 feat: add two-tutorial structure with landing page
- Create landing page at / with cards linking to both tutorials
- Add /agents route for agent developers (search & execute)
- Add /authors route for package authors (publish tools)
- Create 9 author slides covering the publishing workflow
- Rename Slideshow.tsx to AgentSlideshow.tsx
- Add back navigation buttons to both slideshows
- Fix array key lint errors using unique slide IDs

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 13:36:57 +10:00
Ajax Davis
1e779007be refactor: rewrite tutorial slides with coherent AI SDK narrative
Complete rewrite of all 9 slides to create a coherent story for engineers:

1. Welcome - TPMJS: A Tool Registry for the AI SDK
2. Problem - AI SDK Tools Are Static (compiled in, bundle grows)
3. Solution - Two imports enable dynamic tools at runtime
4. HowItWorks - The two meta-tools (search + execute)
5. Discovery - searchTpmjsToolsTool schema and response
6. Integration - registryExecuteTool execution flow
7. Quality - What gets indexed (schemas, scores, health)
8. ToolDetail - Full agent conversation flow example
9. GetStarted - How to publish tools to the registry

Also adds eslint.config.mjs to ignore .next build directory.
2025-12-29 12:03:56 +10:00
Ajax Davis
bda49615fb content: update tutorial slides with grounded TPMJS content
- WelcomeSlide: "The missing layer between npm and AI agents"
- ProblemSlide: "npm has 2 million packages. Which ones work?"
- SolutionSlide: Registry that extracts schemas, scores quality, checks health
- HowItWorksSlide: Automated pipeline flow
- DiscoverySlide: Schema extraction from sandbox
- IntegrationSlide: Quality scoring algorithm
- QualitySlide: Health checks (import + execution)
- ToolDetailSlide: What we store (inputSchema, returnSchema, envKeys, tier)
- GetStartedSlide: CTAs with accurate descriptions

All content now grounded in actual codebase functionality.
Includes TPMJS_TALK.md as source of truth document.
2025-12-29 11:40:39 +10:00
Ajax Davis
20f5fcc4b0 content: update tutorial slides with pitch deck messaging
- WelcomeSlide: 'Tool Discovery for AI Agents'
- ProblemSlide: 'Tool Sprawl' with integration tax framing
- SolutionSlide: Registry that normalizes + enriches tool metadata
- HowItWorksSlide: What TPMJS stores (description, schema, env, signals)
- DiscoverySlide: Two users - engineers browsing & agents selecting
- IntegrationSlide: Immediate benefits (less spelunking, schemas, vocab)
- QualitySlide: The Multipliers (signals, health, playground, remote exec)
- ToolDetailSlide: Tool-shaped results, remove guesswork
- GetStartedSlide: Engineering-friendly CTAs
2025-12-29 01:49:47 +10:00
Ajax Davis
4f191d64c0 chore: add Vercel config for tutorial deployment 2025-12-29 00:34:00 +10:00
Ajax Davis
06029fbc60 feat: add interactive tutorial slideshow app
Create apps/tutorial - a beautiful, animated Next.js slideshow that explains
TPMJS concepts from basic to advanced:

Slides:
- Welcome: Gradient TPMJS title with animated floating orbs
- Problem: Chaotic floating icons showing fragmented AI tools
- Solution: Icons organizing into a neat grid
- How It Works: Flow diagram (npm → Discovery → Registry → AI Agent)
- Discovery: Animated counters showing real-time sync stats
- Integration: Typewriter code animation showing SDK usage
- Quality Scoring: Animated gauge and progress bars
- Tool Detail: Mock tool card with all metadata
- Get Started: CTA buttons linking to tpmjs.com

Features:
- Framer Motion animations with spring physics
- Keyboard navigation (arrows, space, enter)
- Mouse navigation (arrows, progress dots)
- Touch-friendly swipe support
- Dark theme with cyan/purple gradient accents
- Responsive design
2025-12-28 23:41:15 +10:00
Ajax Davis
fa9b2c626d feat: add OG images for all 78 tool pages
- Generate unique AI images for each tool using gpt-image-1-mini
- Update script to fetch tools from production API
- Images served from public/og/tool/{package-name}-{tool-name}.png

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-28 21:35:27 +10:00
Ajax Davis
87b200a735 feat: add build-time OG image generation with OpenAI
- Create generate-og-images.ts script for build-time image generation
- Generate 13 static page OG images using gpt-image-1-mini
- Update API route to serve pre-generated images from public/og/
- Add 30-day cache headers and fallback to default image
- Images regenerate if older than 30 days

Run with: pnpm --filter=@tpmjs/web generate-og

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-28 20:45:02 +10:00
Ajax Davis
6cb172f87b feat: upgrade to gpt-image-1 with comprehensive premium prompts
- Switch from gpt-image-1-mini to gpt-image-1 for higher quality
- Upgrade quality setting to 'high'
- Completely rewrite prompts with detailed visual concepts
- Add glassmorphism, neon accents, and premium SaaS aesthetic
- Include specific typography and composition instructions
- Add page-specific visual elements and iconography
2025-12-28 19:55:01 +10:00
Ajax Davis
0f7f17ba94 feat: improve OG images with landscape dimensions and text content
- Use 1536x1024 landscape format for proper OG aspect ratio
- Include page titles, taglines, and descriptions in generated images
- Add typography guidelines for consistent text rendering
- Upgrade quality from low to medium for better visuals
2025-12-28 19:34:08 +10:00
Ajax Davis
d5efd970c2 fix: use base64 response format for gpt-image-1-mini 2025-12-28 19:16:18 +10:00
Ajax Davis
16f8f79fbf fix: use gpt-image-1-mini model for OG image generation 2025-12-28 19:13:23 +10:00
Ajax Davis
6c5e1a84c5 chore: trigger redeploy for new env vars 2025-12-28 19:01:53 +10:00
Ajax Davis
75a386359c feat: add AI-generated OG images with OpenAI and Vercel Blob caching
- Add /api/og/[...path] endpoint for dynamic OG image generation
- Use OpenAI gpt-image-1 for image generation with page-specific prompts
- Cache images in Vercel Blob storage with 30-day TTL
- Extract page content for contextual prompts (static pages, tool details)
- Update all page metadata to use dynamic OG image URLs
- Refactor tool detail page to server component for generateMetadata support
- Fall back to static /public/og-image.png on generation errors
2025-12-28 18:21:11 +10:00
Ajax Davis
7a23181339 fix: wrap ORDER BY CASE with MIN() for PostgreSQL GROUP BY compatibility 2025-12-28 17:26:55 +10:00
Ajax Davis
cd895c10cb feat: add historical stats tracking with daily snapshots
- Add StatsSnapshot model to store daily registry metrics
- Create /api/sync/stats-snapshot endpoint for daily cron captures
- Add GET endpoint to retrieve historical snapshots (up to 365 days)
- Update stats page with Historical Trends section showing:
  - Tools & packages growth over time
  - Health status trends
  - Daily executions history
  - NPM downloads trends
- Add cron job running at midnight UTC daily
- Fix PostgreSQL GROUP BY issue in quality distribution query
2025-12-28 17:19:46 +10:00
Ajax Davis
c7c375138a fix: use explicit CASE in GROUP BY for PostgreSQL compatibility 2025-12-28 17:14:03 +10:00
Ajax Davis
b4f6eb46b8 feat: add comprehensive stats dashboard with D3 charts
- Add /stats page with animated D3 visualizations
- Create reusable chart components: AnimatedCounter, DonutChart, BarChart, AreaChart
- Expand stats API endpoints: /api/stats, /api/stats/health, /api/stats/executions, /api/stats/sync, /api/stats/tools
- Add Stats link to desktop and mobile navigation
- Add AI SDK v6 integration tests with vitest

Dashboard displays:
- Registry overview metrics with count-up animations
- Health distribution donut charts (import/execution)
- Quality score distribution
- Package tier breakdown
- Execution trends area chart with success/error series
- Token usage statistics
- Top categories bar chart
- Recent sync operations status
2025-12-28 17:08:19 +10:00
Ajax Davis
52ea479ad9 chore: upgrade AI SDK from beta to stable v6
- ai: 6.0.0-beta.124 → 6.0.3
- @ai-sdk/openai: 3.0.0-beta.74 → 3.0.1
- @ai-sdk/react: 3.0.0-beta.131 → 3.0.3

Breaking changes addressed:
- CoreMessage renamed to ModelMessage
- convertToModelMessages is now async (added await)
2025-12-28 16:17:41 +10:00
Ajax Davis
c2cc29114c fix: properly resolve CSS variables for SVG colors and move tooltip outside diagram 2025-12-28 13:07:39 +10:00
Ajax Davis
82f7d2da72 fix: improve diagram node contrast and move tooltip to top 2025-12-28 13:02:01 +10:00
Ajax Davis
e5fea299dd feat: rewrite ArchitectureDiagram with interactive D3 visualization
- Replace static SVG with D3-powered interactive diagram
- Add hover states with tooltips showing node descriptions
- Add animated flowing particles along connection paths
- Add entrance animations staggered by node index
- Add glow effects on hover
- Make diagram responsive to container width
- Match interactive style of SDK page diagram

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-28 12:53:44 +10:00
Ajax Davis
a2ea72739a fix: replace marketing speak with straightforward copy on homepage 2025-12-28 12:47:03 +10:00
Ajax Davis
a3cf662b1c feat: add mobile responsiveness across all pages
- Add hamburger menu icon and MobileMenu slide-out drawer component
- Update AppHeader with responsive nav (hidden on mobile, hamburger shown)
- Apply responsive styles to all 14 pages:
  - Responsive headings (text-2xl sm:text-3xl md:text-4xl)
  - Responsive grid gaps (gap-4 md:gap-6)
  - Explicit mobile grid columns (grid-cols-1 md:grid-cols-X)
  - Flex stacking on mobile (flex-col sm:flex-row)
- Add mobile nav dropdown to docs page
- Update README to correctly frame TPMJS as a discovery registry first,
  with agent integration as secondary/optional
2025-12-28 12:39:02 +10:00
Ajax Davis
6cc8c5bdcf revert: restore original HeroSection marketing
Reverts to "TOOL REGISTRY FOR AI AGENTS" headline and original messaging.
2025-12-28 12:12:00 +10:00
Ajax Davis
91e00afde4 feat: add Discord link to header 2025-12-28 12:08:26 +10:00
Ajax Davis
d806a51599 feat: replace ASCII architecture diagram with SVG component 2025-12-28 12:05:49 +10:00
Ajax Davis
f157393ec7 chore: remove 'What is TPMJS?' section from homepage 2025-12-28 12:02:55 +10:00
Ajax Davis
c0c805b7b1 chore: prepare for Hacker News launch
- Add MIT LICENSE file
- Fix YOUR_ORG placeholders in README and DEPLOYMENT docs
- Fix Node.js version mismatch in release workflow (21 → 22)
- Delete 17 internal debug/development docs
- Rewrite hero section for clarity (explain what TPMJS is in seconds)
- Add "What is TPMJS?" section to landing page
- Fix hardcoded emails to hello@tpmjs.com
- Fix hardcoded dates to December 2024
- Add package metadata (author, license, repository) to all published packages
- Clean up AI-sounding language throughout
- Add comprehensive LAUNCH_REVIEW.md with checklist

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-28 11:46:59 +10:00
Ajax Davis
751fc24bfd fix: add schema extraction to manual tools sync
- Try Railway executor first for dynamic schema extraction
- Fall back to converting parameters from manual-tools.ts to JSON Schema
- This ensures Vercel AI registry tools get schemas during sync
2025-12-17 20:52:19 +10:00
Ajax Davis
6f2218f88d fix: add backwards compatibility for exportName in tpmjs schema
The TpmjsToolDefinitionSchema now accepts both 'name' and 'exportName' fields,
transforming exportName to name for backward compatibility with published packages
that still use the old field name.

Also fix type errors in sync routes for auto-discovered tools.
2025-12-17 19:56:01 +10:00
Ajax Davis
ada3ffb341 refactor: rename exportName to name across entire codebase
- Database: Migrate column export_name to name in tools table
- Prisma schema: Update Tool model to use name field
- Sync routes: Update keyword and changes sync to use name
- Railway executor: Update API endpoints to use name parameter
- API routes: Update all tool routes to use name field
- Web app: Update all pages and components
- Playground: Update tool loader and sidebar
- create-basic-tools: Update types and generators
- Scripts: Update sync and test scripts

Database migration was done via direct SQL:
  ALTER TABLE tools RENAME COLUMN export_name TO name;

The unique constraint remains on (package_id, name).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 17:19:21 +10:00
Ajax Davis
1a1cc36ff0 refactor: rename exportName to name in ManualTool interface and sync scripts
- Update ManualTool interface to use `name` field instead of `exportName`
- Update all manual tool definitions to use `name:` property
- Update sync-manual-tools.ts to reference `manualTool.name`
- Update sync-vercel-registry.ts to generate `name` field
- Add biome-ignore comments for Prisma Json type casts

The database column remains `exportName` but the TypeScript spec uses `name`.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 16:43:16 +10:00
Ajax Davis
10792991ea refactor: rename exportName to name in ManualTool interface
- Update ManualTool interface to use `name` instead of `exportName`
- Update all tool definitions in manual-tools.ts
- Update sync-manual-tools.ts to use manualTool.name
- Update sync-vercel-registry.ts OpenAI prompt and output generation
- Database column `exportName` remains unchanged (stores the value)
2025-12-17 16:43:15 +10:00
github-actions[bot]
cad30ab931 chore: sync 1 new tools from Vercel AI registry
Added 1 tools from Vercel AI SDK registry:
- Total tools in registry: 11
- Already synced: 10
- Newly added: 1
- Errors: 0

🤖 Automated by GitHub Actions
Run: https://github.com/tpmjs/tpmjs/actions/runs/20293730295
2025-12-17 06:23:14 +00:00
Ajax Davis
84d1e382a1 fix: require name field in TPMJS spec, add workflow permissions
Breaking change for TPMJS spec:
- Remove `exportName` field support from TpmjsToolDefinitionSchema
- Make `name` field required (replaces deprecated `exportName`)
- Update create-basic-tools to generate `name` field
- Update sync routes to use `name` from validated schema
- Add `permissions: contents: write` to Vercel registry sync workflow

Packages using the old `exportName` field must update to use `name`.
2025-12-17 16:20:20 +10:00
Ajax Davis
704632bc75 chore: trigger Vercel redeploy for CRON_SECRET env var 2025-12-17 15:28:19 +10:00
Ajax Davis
3b77f74d4b fix(types): support both 'name' and 'exportName' for backward compatibility
The TpmjsToolDefinitionSchema now accepts either 'name' (new) or 'exportName'
(legacy) to ensure existing published packages continue to work.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 14:56:56 +10:00
Ajax Davis
aadd304ca5 feat(tpmjs-spec): add auto-discovery of tools and rename exportName to name
Major changes to the TPMJS specification:

1. Auto-Discovery: The `tools` array is now optional. If omitted, TPMJS
   automatically scans package exports and registers any export with
   `description` and `execute` properties (standard AI SDK tool format).

2. Renamed `exportName` to `name` in tool definitions for cleaner spec.

3. Added `/list-exports` endpoint to Railway executor that:
   - Lists all exports from a package
   - Identifies valid AI SDK tools
   - Extracts descriptions for auto-discovered tools

4. Added `toolDiscoverySource` field to track 'auto' vs 'manual' discovery.

5. Updated tool page UI with:
   - Auto-discovery warning banner
   - Badge showing discovery source

6. Updated all documentation pages (docs, spec, publish) to reflect:
   - Optional tools array with auto-discovery
   - Use of `name` instead of `exportName`
   - Auto-extraction of schema and description

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 14:02:26 +10:00
Ajax Davis
984936a3d9 docs: update documentation to reflect auto-extraction of inputSchema
- Update spec page to show simplified required fields (category, tools)
- Mark parameters, returns, aiAgent as deprecated (now auto-extracted)
- Add schema extraction section explaining the process
- Update publish page with simplified examples
- Update FAQ with schema extraction question
- Update how-it-works with validation & schema extraction step
- Update docs page with auto-extraction callout and simplified spec
2025-12-17 13:25:44 +10:00
Ajax Davis
0062cfee8d feat(schema-extraction): auto-extract inputSchema via executor during sync
- Add inputSchema, schemaSource, schemaExtractedAt fields to Tool model
- Create schema extraction helper that calls executor's /load-and-describe
- Update sync routes to extract schema synchronously after tool upsert
- Reduce changes feed batch size from 100 to 30 for extraction time
- Update /api/tools/update-schema to store full JSON Schema
- Add /api/tools/extract-schema endpoint for manual re-extraction
- Update tool page UI with schema source badge and re-extract button
- Add deprecation comments to parameters, returns, aiAgent in types

Authors no longer need to define inputSchema in package.json - it's now
automatically extracted from the tool at sync time. Falls back to
author-provided parameters if extraction fails.
2025-12-17 12:56:25 +10:00
Ajax Davis
65efebe692 feat(railway-executor): add 2-minute TTL cache for esm.sh modules
- Add TTL-based module cache (2 minutes) for imported esm.sh packages
- Cache non-factory tools to avoid re-downloading on each request
- Factory functions are cached but always re-imported to get fresh env vars
- Automatic cleanup of expired cache entries every minute
- Enhanced /cache/stats endpoint shows TTL info and expiration times
2025-12-17 11:42:12 +10:00
Ajax Davis
5cc223af6e Revert "feat(package-executor): add 2-minute in-memory cache for execution results"
This reverts commit fb1ed57471.
2025-12-17 11:40:04 +10:00
Ajax Davis
f7fd6ad023 docs: add comprehensive guide for building dynamic tool systems 2025-12-17 11:31:38 +10:00
Ajax Davis
fb1ed57471 feat(package-executor): add 2-minute in-memory cache for execution results
- Cache successful execution results with 2-minute TTL
- Automatic cleanup of expired entries every minute
- Cache hit returns 0ms execution time to indicate cached response
- Add getCacheStats() helper for debugging
- clearCache() now clears both local and remote sandbox cache
2025-12-17 11:29:22 +10:00
Ajax Davis
0a3b004243 docs: add PRD for AI-generated OG images 2025-12-17 11:06:57 +10:00
Ajax Davis
d2b22201aa feat: add Vercel Analytics to web and playground apps
- Install @vercel/analytics in both apps
- Add <Analytics /> component to root layouts
- Enables automatic page view tracking on Vercel
2025-12-17 11:04:02 +10:00
Ajax Davis
6ccb5fbd79 fix: add npmPublishedAt to API response and add null safety for dates 2025-12-16 12:49:53 +10:00
Ajax Davis
ad694e27e1 fix: make sitemap dynamic to avoid DB calls during CI build 2025-12-16 12:38:42 +10:00
Ajax Davis
098d25b6e0 feat: add @tpmjs/unsandbox package for secure code execution
Initial release of @tpmjs/unsandbox - AI SDK tools for secure code execution
in 42+ programming languages via unsandbox.com. Re-exports tools from
@thomasdavis/unsandbox with TPMJS registry metadata.
2025-12-16 12:07:46 +10:00
Ajax Davis
ff17182e04 chore: update dependencies and add unsandbox package 2025-12-16 12:00:22 +10:00
Ajax Davis
5ebad3cd35 refactor: implement REST API best practices for /api/tools endpoint
- Add standardized ApiResponse interface with consistent structure
- Implement proper request validation with detailed error messages
- Add response metadata (version, timestamp, requestId, processingTime)
- Include proper HTTP status codes and error handling
- Add validation for pagination parameters (limit: 1-1000, offset: >=0)
- Add health status enum validation
- Include response headers (X-Request-ID, X-Processing-Time, Cache-Control)
- Improve error logging with structured context
- Add count field to pagination response
- Follow REST API industry standards and best practices
2025-12-16 11:54:59 +10:00
Ajax Davis
3cb636a703 feat: increase tools API limit to 1000 and optimize response payload
- Increase max limit from 50 to 1000 for bulk tool fetching
- Exclude large fields (npmReadme, npmAuthor, npmMaintainers) from API response
- Reduces payload size while maintaining all necessary tool metadata
2025-12-16 11:31:18 +10:00
Ajax Davis
72bdc5e854 refactor: move schema updates to executor
- Executor now updates TPM.js database directly when loading tools
- Update /api/tools/update-schema to use packageName+exportName lookup
- Remove schema update logic from HLLM proxy (no longer needed)
2025-12-15 13:07:42 +10:00
Ajax Davis
7aabba7e8f feat: auto-update TPM.js database with tool schemas on execution
- Add /api/tools/update-schema endpoint to TPM.js to update tool parameters
- When a tool is executed, fetch its schema from the executor
- Update TPM.js database with the discovered schema (async, non-blocking)
- This ensures /api/tools returns correct inputSchema for all tools
2025-12-15 12:52:58 +10:00
Ajax Davis
5dd3a6b631 fix: disable tool caching entirely to ensure fresh env vars
Temporary fix - always re-import tools so env vars are fresh each request
2025-12-15 12:36:26 +10:00
Ajax Davis
d78025723f fix: inject env vars before factory calls and skip caching factory tools
- Move env var injection to happen BEFORE cache check and factory function calls
- This ensures process.env is set when factory functions like Valyu's webSearch() read from it
- Skip caching factory-created tools since they may read env vars at creation time
- Fixes issue where Valyu tools fail with 'VALYU_API_KEY is required' even when key is provided
2025-12-15 12:34:22 +10:00
Ajax Davis
eaefae34c3 docs: add execution API documentation to /docs page
- Document POST /api/tools/execute/[...slug] endpoint
- Show URL formats (by tool ID and by package/export name)
- Document request body parameters (prompt, parameters)
- List SSE events (chunk, tokens, complete, error)
- Add curl and JavaScript code examples
- Document rate limiting (10 requests/minute per IP)
- Add to sidebar navigation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 13:43:51 +10:00
Ajax Davis
d261c0cce0 feat(web): add changelog page displaying release history for all packages
- Parse CHANGELOG.md files from SDK and tool packages at build time
- Display version history with major/minor/patch badges
- Group by SDK packages (ui, types, utils, env) and Tool packages
- Add changelog link to navigation header
- Fix unescaped apostrophes in docs page

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 13:31:10 +10:00
Ajax Davis
9f1f93b532 feat(docs): add comprehensive documentation page with sidebar navigation
- Create /docs page with complete TPMJS documentation
- Add sidebar navigation with section tracking
- Document SDK reference (registrySearchTool, registryExecuteTool)
- Document REST API endpoints
- Document publishing guide and TPMJS specification
- Add advanced sections (override execute, custom wrappers, self-hosting)
- Include FAQ and troubleshooting sections
- Add Docs link to main navigation header

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 13:00:21 +10:00
Ajax Davis
604deb224a docs: add guide for overriding execute functions in npm tools
Focused documentation on customizing tool execution when importing
tools from npm packages. Covers 10 patterns:

- Simple override with spread
- Wrap with pre/post processing
- Transform inputs/outputs
- Add authentication/API keys
- Conditional execution
- Retry logic
- Timeout handling
- Rate limiting
- Validation layer
- Wrapper factory for reusable patterns

Includes TypeScript typing guidance and real-world examples.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:48:06 +10:00
Ajax Davis
67df5e456d docs: add comprehensive AI SDK 6 tool execution documentation
- Document override patterns (complete, conditional, schema-only)
- Document extension patterns (pre/post processing, retry, caching, validation)
- Add custom middleware implementation guide
- Add factory pattern for maximum flexibility
- Include complete examples combining TPMJS tools with custom tools
- Cover best practices for error handling, timeouts, rate limiting, security
- Add API reference for registrySearchTool and registryExecuteTool

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:39:28 +10:00
Ajax Davis
48c777425d fix: update SDK playground link to playground.tpmjs.com 2025-12-14 12:27:52 +10:00
Ajax Davis
34ae497c75 fix: standardize Twitter handle to @tpmjs_registry in schema 2025-12-14 12:24:52 +10:00
Ajax Davis
7fd6ab9dc6 feat: add SVG favicon and apple-touch-icon
- Add modern SVG favicon with TPMJS "T" logo
- Add apple-touch-icon for iOS devices
- Update metadata to reference new icons

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:18:04 +10:00
Ajax Davis
b2984d1f61 feat: add HN launch readiness features
Launch checklist implementation:
- Add Privacy Policy page (/privacy) with GDPR compliance
- Add Terms of Service page (/terms)
- Add custom 404 and error pages with helpful navigation
- Add FAQ page (/faq) covering common questions
- Add SEO meta tags with OpenGraph/Twitter cards
- Add JSON-LD structured data (Organization, WebSite, SoftwareApplication)
- Add sitemap.ts and robots.ts for search engines
- Add security headers (HSTS, CSP, X-Frame-Options) in vercel.json
- Add security.txt at /.well-known/security.txt
- Add API rate limiting (100 req/min default, 20 req/min strict)
- Add empty states in tool search for better UX
- Update AppHeader with FAQ link
- Update AppFooter with Privacy/Terms links
- Update biome.json to allow dangerouslySetInnerHTML for JSON-LD in page files

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 11:56:42 +10:00
Ajax Davis
5df484718a fix(ui): fix checkbox tick not visible in light/dark mode
The checkmark SVG was nested inside a span, making peer-checked
selectors ineffective since peer only works on siblings.

Fix by moving the checkmark and indeterminate SVGs to be direct
siblings of the hidden input element, allowing Tailwind's peer-checked
and peer-data-[indeterminate] selectors to properly toggle visibility.
2025-12-12 12:51:37 +10:00
Ajax Davis
3ce5ea9bb7 style(playground): polish UI design for better consistency and alignment
- ChatInput: Redesign with rounded container, better button alignment, helper text
- ChatHeader: Consistent bg-surface, refined typography and spacing
- ChatMessages: Improved empty state with icon badge and suggestion box
- MessageBubble: Cleaner message cards, collapsible tool input/output, status badges
- ToolsSidebar: Header/content separation, keyboard accessibility, wider width
- SettingsSidebar: Matching header style, dashed empty state, footer info section

All components now use consistent theme tokens (bg-surface, bg-background, etc.)
and follow the same visual patterns for headers, cards, and spacing.
2025-12-12 12:39:18 +10:00
Ajax Davis
a793322710 feat(playground): load all tools and add hide broken toggle
- Paginate through all tools from registry API (was limited to 20)
- Add 'Hide broken tools' checkbox (enabled by default)
- Filter out tools with BROKEN import or execution health
2025-12-12 12:28:02 +10:00
Ajax Davis
a99ea61cfe fix: point Playground link to playground.tpmjs.com 2025-12-12 12:21:04 +10:00
Ajax Davis
55bb521d77 docs(sdk): add 'Passing API Keys' section with wrapper pattern 2025-12-12 11:41:41 +10:00
Ajax Davis
25e83a1809 docs(registry-execute): recommend wrapper pattern for API keys
Replace system prompt approach with cleaner tool wrapper pattern
that auto-injects pre-configured API keys
2025-12-12 11:39:45 +10:00
Ajax Davis
0a856dda39 docs(sdk): add API keys documentation section
- Add 'Passing API Keys to Tools' section to registry-execute README
- Add 'Understanding requiredEnvVars' section to registry-search README
- Include example of pre-configuring keys for agents
- Document tools that don't require keys
- Bump both packages to 0.1.2
2025-12-12 11:35:51 +10:00
Ajax Davis
db482855d3 fix(createblogpost): convert to proper AI SDK tool
- Wrap function with tool() from 'ai' package
- Add jsonSchema() for input validation
- Rename export from createBlogPost to createBlogPostTool
- Update package.json with proper tpmjs.tools format
- Bump version to 0.3.0

Fixes executor error: 'Cannot destructure property title of t'
2025-12-12 10:38:46 +10:00
Ajax Davis
196cbacf62 chore: bump registry-search and registry-execute to 0.1.1 2025-12-12 10:09:54 +10:00
Ajax Davis
46088f09c3 docs: update READMEs to use AI SDK v6 streamText API
- Replace deprecated Agent class with streamText
- Add anthropic provider import
- Add system prompt example
2025-12-12 10:09:17 +10:00
Ajax Davis
36b84294b4 docs(sdk): improve diagram tooltips and fix GitHub button labels
- Make tooltips more verbose with detailed explanations for each node
- Add tooltip for 'Your Tools' node
- Change GitHub button labels from 'GitHub' to 'Source'
- Fix missing </p> closing tags
2025-12-12 10:04:46 +10:00
Ajax Davis
aa1001a0ee fix: rename SDK packages to lowercase for npm compatibility
- @tpmjs/registrySearch -> @tpmjs/registry-search
- @tpmjs/registryExecute -> @tpmjs/registry-execute

npm package names cannot contain capital letters
2025-12-12 09:52:48 +10:00
Ajax Davis
dcab29baa2 chore: version packages for release 2025-12-12 09:50:42 +10:00
Ajax Davis
53d30f7eca feat(sdk): replace ASCII diagram with interactive D3 visualization
- Create SDKFlowDiagram component with animated D3 graphics
- Add flowing particle animations along connection paths
- Add hover interactions with tooltips for each node
- Add entrance animations with staggered timing
- Add subtle glow effects and shadows
- Responsive design that adapts to screen width
- Sleek minimal black and white aesthetic with depth effects
2025-12-12 09:43:40 +10:00
Ajax Davis
92135cf9f0 fix(sdk): update code example to use AI SDK v6 streamText API
- Replace deprecated Agent class with streamText function
- Add @ai-sdk/anthropic import for model provider
- Change instructions to system parameter
- Add example prompt showing tool usage
2025-12-12 09:12:20 +10:00
Ajax Davis
02b9b7e484 fix(sdk): move package links to hero, separate install commands 2025-12-12 08:15:38 +10:00
Ajax Davis
be5cae3f1c feat(sdk): add npm and GitHub links to package sections 2025-12-12 07:55:30 +10:00
Ajax Davis
463889b7c4 feat: add @tpmjs/registrySearch and @tpmjs/registryExecute SDK packages
- Create @tpmjs/registrySearch package for searching tool registry
- Create @tpmjs/registryExecute package for executing tools via sandbox
- Support self-hosted registries via TPMJS_API_URL and TPMJS_EXECUTOR_URL env vars
- Add /sdk documentation page with usage examples and architecture
- Add SDK link to navigation menu
- Include design doc for registry SDK architecture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 07:29:06 +10:00
Ajax Davis
051550ee76 docs: update health system docs with packageName fix pattern 2025-12-12 06:15:12 +10:00
Ajax Davis
2a46021341 fix(executor): declare packageName/exportName before try block
Same issue as startTime - these variables were destructured inside the
try block but referenced in the catch block for health reporting. If
JSON parsing or any early error occurred, the catch block would crash
with 'packageName is not defined'.

Now declares them with 'unknown' defaults before try, then assigns
the actual values inside.
2025-12-12 06:14:08 +10:00
Ajax Davis
6c3267eb98 docs: add comprehensive tool health system documentation
Documents the health check architecture, error classification logic,
common debugging scenarios, and lessons learned from production issues
like the startTime bug.
2025-12-12 06:08:08 +10:00
Ajax Davis
c2447e8467 fix(executor): move startTime declaration before try block
The startTime variable was declared inside the try block but referenced
in the catch block, causing 'startTime is not defined' errors when
exceptions occurred before line 404 (e.g., during req.json() parsing).

Moving the declaration before the try ensures it's in scope for the
catch block's executionTimeMs calculation.
2025-12-12 05:53:47 +10:00
Ajax Davis
6b11a2f9a7 refactor(health): move health reporting to Railway executor
Health status is now reported from the executor - the single point where
all tools run. This ensures consistent health tracking regardless of
client (playground, direct API, etc).

- Add reportToolHealth() to Railway executor
- Report success/failure after every tool execution
- Remove health reporting from playground (executor handles it)
- Executor calls /api/tools/report-health which has all the logic
2025-12-12 05:23:27 +10:00
Ajax Davis
25dec1795b refactor(health): centralize health status logic in web app API
- Remove direct DB updates from playground
- Add /api/tools/report-health endpoint with all health logic
- Playground now reports results to web app API
- All env var / validation error detection is in one place
- Health status updates based on execution success and error type
- Fix type errors with proper null checks
2025-12-12 05:17:04 +10:00
Ajax Davis
572b7a081d docs(broken-tools): update resolution status after health check fix 2025-12-12 04:58:07 +10:00
Ajax Davis
970e161390 fix(health-check): treat env var errors from executor 500 responses as HEALTHY
The executor returns HTTP 500 for all tool errors, including missing env vars.
Before: 500 response = BROKEN
After: Check error message for env/validation patterns before marking BROKEN

This ensures tools that require API keys (like @parallel-web/ai-sdk-tools)
show importHealth: HEALTHY since the tool loads correctly - it just needs config.
2025-12-12 04:56:02 +10:00
Ajax Davis
0804f1bd1e fix(executor): pass execution context with abortSignal to tool execute()
Some AI SDK tools (like @parallel-web/ai-sdk-tools) expect execute(params, context)
where context contains { abortSignal, messages, toolCallId }. Previously we only
passed params which caused 'Cannot destructure abortSignal' errors.

Also:
- Improved playground system prompt for better tool execution
- Playground /api/tools now proxies to web app with response transformation
- Added broken-tools.md documenting tool failure categories

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 04:43:20 +10:00
Ajax Davis
1a2128d3ae fix(search-registry): remove health status from search results
Removes importHealth, executionHealth, healthCheckError, and lastHealthCheck
fields from tool search results. Models were refusing to call tools marked
as BROKEN, even when the issue was just a missing env var.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 03:14:36 +10:00
Ajax Davis
1050914f86 fix(health-check): only mark tools broken for infrastructure failures
Simplify execution health check logic:
- If executor responds (2xx or 4xx), tool is HEALTHY
- Only mark BROKEN for 5xx errors or network/timeout failures
- Validation errors (URL format, missing fields) mean tool IS working
- Remove brittle pattern matching for specific error messages

The previous approach tried to match specific error patterns to determine
if an error was "acceptable". This was fragile. The new approach:
- Import check: can we load and describe the tool?
- Execution check: did the tool execute at all?

If a tool throws a validation error, it executed successfully - it's
correctly rejecting invalid test input. Only infrastructure failures
(executor down, network timeout) indicate a truly broken tool.
2025-12-11 12:22:21 +10:00
Ajax Davis
38a9710474 fix(health-check): treat input validation errors as healthy
Tools that reject invalid test inputs (like invalid URLs) are actually
working correctly - they're validating input as expected. Previously
the health check marked these as BROKEN because the test used dummy
values like 'test' which failed Zod validation.

Now input validation errors (invalid URL, invalid format, type mismatch,
etc.) are treated as HEALTHY, similar to how we already treat missing
environment variables.
2025-12-11 12:13:58 +10:00
Ajax Davis
b27f377319 fix(playground): update tool health status on successful execution
When a tool is marked as BROKEN but executes successfully in the
playground, update its health status to HEALTHY. This ensures
stale health check data doesn't persist when tools are working.

Also fixes noImplicitAnyLet lint error by refactoring to const.
2025-12-11 12:00:17 +10:00
Ajax Davis
2a16218871 fix(tool-search): add published date, center copy button
- Add formatTimeAgo utility function for relative time display
- Show "Published X ago" at bottom of each tool card using npmPublishedAt
- Center CodeBlock copy button vertically for better alignment
- Update Tool interface to include npmPublishedAt from API
2025-12-11 11:37:42 +10:00
Ajax Davis
256699099b fix(security): update Next.js to 16.0.8 to address CVE-2025-66478 2025-12-11 11:14:43 +10:00
Ajax Davis
ee31201204 chore: trigger Vercel deployment 2025-12-11 11:11:49 +10:00
Ajax Davis
07b23e6ef6 fix(ui): allow deep import for react-syntax-highlighter styles 2025-12-11 10:10:58 +10:00
Ajax Davis
bdacc7142e fix: add keyboard handler and role for a11y compliance 2025-12-11 09:58:20 +10:00
Ajax Davis
5561d5c177 fix(ci): resolve lint and architecture errors blocking Vercel deploy
- Add eslint-disable and biome-ignore for a11y rules in tool-search page
- Escape quotes in how-it-works page for react/no-unescaped-entities
- Exclude Deno-based railway-executor from dependency cruiser
- Extract sortTools helper to reduce cognitive complexity
2025-12-11 09:46:27 +10:00
Ajax Davis
e9e995fd14 fix(tool-search): fix download sorting and enable text selection
- Add nullish coalescing for downloads (handle null/undefined values)
- Add select-text class to Link and Card for text selection
2025-12-11 09:33:45 +10:00
Ajax Davis
66b24dd13e feat(tool-search): add sort dropdown with Most Downloaded and Recent options
- Add sortBy state with 'downloads' as default (most downloaded)
- Add 'recent' sort option to sort by createdAt
- Sorting keeps broken tools at bottom regardless of sort order
- Add createdAt field to Tool interface
2025-12-11 09:27:16 +10:00
Ajax Davis
c962daabce fix(tool-details): correct data model and add AI SDK usage examples
- Update interface to match actual API response (Tool has package relation)
- Fix package name display in installation section (was showing undefined)
- Add step-by-step usage instructions:
  1. Install package
  2. Import the tool
  3. Use with AI SDK (generateText example)
- Update all sidebar sections to use pkg.* for package-level data
- Remove deprecated tpmjsMetadata references
- Clean up unused Tags section
2025-12-11 09:08:54 +10:00
Ajax Davis
937ffc3194 fix(playground): use explicit white background for textarea 2025-12-11 08:56:51 +10:00
Ajax Davis
8162b1366f feat(ui): reimagine Spinner as brutalist grid-based loader
- Replace orbital spinner with 3x3 grid of blocks
- Diagonal wave animation matches dithering aesthetic
- Sharp squares, no rounded corners (brutalist)
- Inline horizontal layout with monospace text
- Consistent styling across all loading states

The new loader evokes "tools being constructed" - fitting
for a tool registry. Uses staggered opacity/scale animation
creating a wave pattern across the grid.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 08:19:44 +10:00
Ajax Davis
d8b5f67a1c feat(ui): add Spinner component with orbital animation
- Create new Spinner component with three orbiting dots
- Use inline CSS keyframes for reliable animation
- Support multiple size variants (xs, sm, md, lg, xl)
- Increase spinner sizes in loading states across the app
- Add biome-ignore directives for pre-existing lint issues
- Fix accessibility: change span onClick to button element

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 07:47:32 +10:00
Ajax Davis
bb967ec527 fix(tool-search): remove tabs and show all tools by default
- Remove Tabs component and activeTab state
- Show all tools in a single grid view
- Simplify API calls by removing unnecessary count fetches
- Add missing Script import in layout.tsx
2025-12-11 06:25:04 +10:00
Ajax Davis
8d0cdc1154 feat(playground): add timing display for each message step
Shows duration for text generation and tool execution in chat messages.
Tracks when each part starts and completes, displays timing in the UI.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 03:42:09 +10:00
Ajax Davis
579bfcfbeb fix(web): disable caching on homepage with force-dynamic
Ensures fresh data is always fetched from database on homepage load.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 03:08:39 +10:00
Ajax Davis
f50f838328 fix(ui): correct CodeBlock test selectors to match component structure
Tests were checking classes on <code> element but variant classes are
applied to the wrapper div with data-language attribute.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 02:55:57 +10:00
Ajax Davis
b934cd4c64 Revert "fix(create-basic-tools): correct template path resolution for npx execution"
This reverts commit 96c53ed49a.
2025-12-06 01:48:42 +10:00
Ajax Davis
96c53ed49a fix(create-basic-tools): correct template path resolution for npx execution
Version @tpmjs/create-basic-tools@1.0.6

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 09:06:52 +10:00
Ajax Davis
79547ff52e feat: add markdown-formatter package with defensive patterns
- Create @tpmjs/markdown-formatter with 2 tools:
  - markdownToPlainText: Convert markdown to plain text
  - formatMarkdownTable: Format and align markdown tables
- Includes defensive parameter validation
- Uses AI SDK v6 beta with Zod 4 schemas
- Published v0.2.0 to npm

Testing the full end-to-end workflow:
- Package creation following generator patterns
- Changesets for version management
- npm publishing
- TPMJS registry auto-discovery
2025-12-05 01:31:03 +10:00
Ajax Davis
6765ad9f17 feat(create-basic-tools): add defensive parameter validation to generated tools
- Add defensive checks for required parameters in generated tool code
- Prevents crashes when tools are called with missing/empty params
- Returns descriptive error messages instead of undefined errors
- Update README with explanation of defensive pattern and best practices
- Bump to v1.0.5

Based on learnings from emoji-magic deployment:
- LLMs sometimes make probe calls with empty params
- Defensive checks prevent crashes and provide better error messages
- Even with Zod validation, runtime checks are valuable for robustness
2025-12-05 01:17:08 +10:00
Ajax Davis
6ec6f790d3 fix: remove nonexistent deno.json from Dockerfile 2025-12-05 01:10:42 +10:00
Ajax Davis
750ff2afbe fix: use manual volume at /data for Deno cache
Simplified Docker configuration to use manually mounted volume at /data
instead of complex pre-caching strategy. This approach:

- Uses ENV DENO_DIR=/data to point to manually created Railway volume
- Keeps Dockerfile simple and maintainable
- Adds --allow-read/--allow-write permissions for cache access
- Removes railway.toml volume configuration (manual setup instead)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 01:05:07 +10:00
Ajax Davis
227a7e2dd1 Revert "feat: add persistent Deno cache to Railway executor for faster tool loading"
This reverts commit eb8a5ff91e.
2025-12-05 01:04:39 +10:00
Ajax Davis
74811cc269 fix(emoji-magic): add defensive checks for missing required parameters
- Add validation in both textToEmoji and emojiMood to handle missing text parameter
- Return descriptive error instead of crashing with undefined error
- Bump to v0.2.1
2025-12-05 00:51:24 +10:00
Ajax Davis
eb8a5ff91e feat: add persistent Deno cache to Railway executor for faster tool loading
- Set DENO_DIR=/app/.deno_cache to persist module cache
- Pre-cache common dependencies (zod-to-json-schema, ai, zod) during build
- Add Railway volume configuration for /app/.deno_cache
- Improve logging to show cache hits vs network downloads
- Add --allow-read and --allow-write permissions for cache access

This dramatically reduces tool loading time after the first import.
Dependencies are downloaded once and reused across all subsequent requests.

Example: ctx-zip with 200+ dependencies will only download once instead
of on every chat request.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 00:50:10 +10:00
Ajax Davis
578ce944d2 fix: add 120s timeout per tool and increase route maxDuration to 300s
- Add AbortController timeout (120s) to Railway fetch requests
- Gracefully handle timeout errors and report to health check system
- Increase /api/chat maxDuration from 60s to 300s (5 minutes)
- Prevents entire chat from timing out when one tool has large dependencies
- Tools that timeout are logged and skipped, allowing others to load

Fixes issue where tools like ctx-zip with many dependencies would
cause the entire chat request to timeout after 60 seconds.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 00:43:13 +10:00
Ajax Davis
9fae5f22b2 fix: make Prisma import lazy in playground to prevent module initialization failures
- Remove top-level Prisma import from dynamic-tool-loader.ts
- Use dynamic import in reportToolFailure function instead
- Prevents entire module from failing if DATABASE_URL is missing
- Fixes API route timeout issue caused by module initialization failure

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 00:27:16 +10:00
Ajax Davis
3cfc141944 chore: release emoji-magic v0.2.0 and create-basic-tools v1.0.4 2025-12-05 00:10:18 +10:00
Ajax Davis
8b7a52ef89 feat: simplify interactive CLI to only ask for package name
- Removed all prompts except package name
- Auto-generate description from package name
- Use sensible defaults: 2 example tools, ai-ml category, MIT license
- Generate exampleTool and anotherTool that users can customize
- Much faster UX - no more 10+ prompts for basic usage
- Updated README with simplified flow example
- Bumped version to 1.0.3

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 23:55:54 +10:00
Ajax Davis
20353987e9 feat: add @tpmjs/create-basic-tools generator promotion to website
- Add prominent callout box on homepage in 'Publish Your Tool' section
- Add featured generator section on /publish page with full documentation link
- Include command example and links to GitHub README and NPM
- Highlight key features: 2-3 tools, complete setup, production-ready

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 23:48:36 +10:00
Ajax Davis
75f652e2f2 fix: remove duplicate shebang from source file
- tsup banner already adds shebang, no need in source
- bump version to 1.0.2
- fixes CLI execution errors

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 23:43:03 +10:00
Ajax Davis
051e58c23d feat: add @tpmjs/create-basic-tools CLI generator
- Interactive CLI generator for scaffolding TPMJS tool packages
- Generates packages with minimum 2 tools (ideally 2-3)
- Zod 4 schemas - uses Zod directly (not jsonSchema wrapper)
- One file per tool in src/tools/<toolName>.ts
- TPMJS validated against official schemas from @tpmjs/types
- Complete package generation ready to publish to npm
- Works both standalone and in monorepo packages/ folders

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 23:34:37 +10:00
Ajax Davis
c4161f1d35 feat: add beta experimental section for dynamic tool loading to how-it-works page
- Add comprehensive section explaining BM25 search with context awareness
- Show comparison of traditional vs dynamic tool loading approaches
- Document Deno sandboxed execution environment on Railway
- Preview future collections feature for tool organization
- Include call-to-action to try the playground
2025-12-04 20:26:38 +10:00
Ajax Davis
31b29c306f test: mock react-syntax-highlighter to fix ESM compatibility in CodeBlock tests 2025-12-04 20:21:14 +10:00
Ajax Davis
0f758aecca test: mock react-syntax-highlighter to fix ESM compatibility in CodeBlock tests 2025-12-04 20:06:41 +10:00
Ajax Davis
e384951aa8 test: update form input tests to expect bg-surface instead of bg-background 2025-12-04 20:02:55 +10:00
Ajax Davis
e97ecfaa31 feat: add How It Works documentation page
- Create comprehensive /how-it-works page explaining TPMJS architecture
- Add detailed sections on developer workflow, AI agent integration, and system internals
- Include quality scoring formula, health checks, and data flow diagrams
- Add navigation link to AppHeader between Tools and Playground
- Style consistently with existing pages (Publish, Playground)
- Fix: remove debug console.log from ToolsSidebar

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 19:58:54 +10:00
Ajax Davis
5b5cffa248 fix: don't mark tools as broken for missing environment variables
Tools that fail due to missing environment variables (API keys, etc.)
are not actually broken - they just need configuration. Added detection
for common env var error patterns and mark these tools as HEALTHY
instead of BROKEN.

Error patterns detected:
- 'is required'
- 'is not set'
- 'missing environment'
- 'API key required/not provided'
- etc.

This fixes false positives where tools like @superagent-ai/ai-sdk
were marked as broken when they just need SUPERAGENT_API_KEY configured.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 19:52:22 +10:00
Ajax Davis
722cb6af0b fix: add CSS variables for status colors to playground
Added error, warning, success, and info color CSS variables to
playground globals.css so that Badge component variants display
with correct colors. The error variant will now show red as expected.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 19:47:24 +10:00
Ajax Davis
0e907338ed fix: add health fields to /api/tools/search response
The search endpoint was missing importHealth, executionHealth,
healthCheckError, and lastHealthCheck fields in the response. This caused
the playground (which uses search-registry tool) to not receive health
data for displaying broken tool badges.

Added all four health fields to the tool mapping in the search response.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 19:41:50 +10:00
Ajax Davis
f6812b2267 fix: update footer links - correct GitHub URL and contact email
- Change GitHub URL to github.com/tpmjs/tpmjs
- Update contact email to thomasalwyndavis@gmail.com
2025-12-04 19:18:46 +10:00
Ajax Davis
9abff1f203 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>
2025-12-04 19:12:07 +10:00
Ajax Davis
5b5be16770 fix: display environment variable names correctly in tool modal
Previously env vars were stored as an array but component used Object.entries(),
causing array indices (0, 1, 2...) to appear as variable names instead of actual
names like 'EXA_API_KEY'.

Updated component to:
- Reflect correct array structure in Tool interface
- Iterate directly over array with .map() instead of Object.entries()
- Access envVar.name field for display
- Added support for displaying default values if present

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:56:31 +10:00
Ajax Davis
177f8136a0 fix: use lighter background for tool detail modal
- Change modal from bg-background to bg-white/dark:bg-gray-900
- Makes modal stand out clearly from the app background
- Provides better visual hierarchy

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:49:59 +10:00
Ajax Davis
07809784db fix: use theme-aware colors for tool detail modal
- Change modal background from bg-surface to bg-background for proper dark mode support
- Use bg-surface for nested elements (env vars, code blocks) to create subtle contrast
- Ensures modal respects the application's dark theme

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:43:44 +10:00
Ajax Davis
f50b747e45 feat: add package names and tool detail modal to playground sidebar
- Display package name as secondary text below tool name
- Add clickable tool cards that open detailed modal
- Modal shows comprehensive tool info: description, frameworks, env vars, import URL, tool ID
- Improve modal click handling to only close on backdrop clicks
- Fix accessibility: add type="button" to close button
- Full keyboard support with Escape key and proper ARIA labels

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:34:22 +10:00
Ajax Davis
addb6ba06e feat(playground): enhance tool list UI with package names and detail modal
Add two major UX improvements to the ToolsSidebar:

1. Package Name Display
   - Show package name as secondary label below tool name
   - Improves tool identification at a glance

2. Tool Detail Modal
   - Click any tool card to open detailed modal
   - Shows comprehensive information:
     * Tool name, package, version, category
     * Quality score (if available)
     * Full description
     * Supported frameworks (badges)
     * Environment variables (with required flag)
     * Import URL (for manual integration)
     * Tool ID (for debugging)
   - Modal features:
     * Backdrop blur effect
     * Click outside or press Escape to close
     * Close button (X icon) with SVG title
     * Responsive layout (max-w-2xl)
     * Scrollable content (max-h-90vh)
     * Full keyboard accessibility (ARIA labels, tabIndex, Escape key)

3. Enhanced Tool Interface
   - Added optional fields: toolId, qualityScore, frameworks, env, importUrl
   - Maintains backward compatibility with existing API

This gives users full visibility into tool metadata and helps them
understand what each tool does before using it in conversations.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:20:33 +10:00
Ajax Davis
56a6128ac6 fix: sanitize invalid JSON schemas with type 'None' to valid object schemas
Add sanitizeJsonSchema() function to fix common schema issues:
- Replaces invalid type 'None' (common in Python tools) with 'object'
- Ensures all schemas have a valid type field
- Recursively sanitizes nested schemas in properties, items, anyOf/oneOf/allOf
- Prevents OpenAI API errors from malformed tool schemas

This fixes the error with @superagent-ai/ai-sdk guard tool which
returns type: 'None' instead of a valid JSON Schema type.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:02:10 +10:00
Ajax Davis
2c2eac7992 Revert "feat: add Node.js compatibility layer to Railway executor"
This reverts commit 8562eb5b38.
2025-12-04 17:55:25 +10:00
Ajax Davis
801b0d81b7 fix: use eval instead of regex for TypeScript parsing 2025-12-04 17:50:26 +10:00
Ajax Davis
564543274d fix: use variable for multi-line commit message 2025-12-04 17:47:03 +10:00
Ajax Davis
daedf035af fix: use heredoc for multi-line commit message in workflow 2025-12-04 17:45:56 +10:00
Ajax Davis
fd04674501 fix: support AI SDK jsonSchema() with .jsonSchema property
Added Strategy 2.5 to schema extraction to handle AI SDK v6's
jsonSchema() wrapper which uses `.jsonSchema` property instead
of `.schema`.

This fixes schema validation errors for @tpmjs/hello and other
packages that use jsonSchema() wrapper.

Error was: "No valid schema found" with keys ["_type", "jsonSchema", "validate"]
Fix: Check for inputSchema.jsonSchema as an object property

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:44:26 +10:00
Ajax Davis
1642ec07d9 feat: add automated Vercel AI registry sync with OpenAI
Add hourly GitHub Action that syncs tools from Vercel's AI SDK registry:

Features:
- Fetches Vercel AI registry from their GitHub
- Uses OpenAI GPT-4 to intelligently convert tool metadata
- Handles multi-export packages (multiple tools per npm package)
- Automatically commits new tools to manual-tools.ts
- Sends Discord notifications with detailed stats
- Extensive logging at every step

Files added:
- sync-vercel-registry.ts - Main sync script with AI conversion
- .github/workflows/sync-vercel-registry.yml - Hourly GitHub Action
- docs/vercel-registry-sync.md - Complete documentation

Requires OPENAI_API_KEY secret in GitHub repository settings.
2025-12-04 17:43:17 +10:00
Ajax Davis
dc4846c7b1 fix: set env vars in both Deno.env and process.env for Node.js compat
The Node.js compatibility layer (npm: specifier) broke environment
variable passing because tools imported via npm: expect process.env,
not Deno.env.

Root cause: Recent commit added npm: specifier for Node compatibility,
but env injection code only set Deno.env.set(), not process.env.

Fix: Set environment variables in BOTH locations:
- Deno.env.set() for esm.sh imports
- globalThis.process.env for npm: imports

This restores functionality for tools like Firecrawl that require
API keys via environment variables.

Fixes regression from commit 8562eb5.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:29:42 +10:00
Ajax Davis
a39f28546a perf: only type-check changed packages in pre-commit hook
Use Turborepo's --filter='...[HEAD]' to only type-check packages
with staged changes instead of all 21 packages. This reduces
pre-commit hook time from ~30s to ~3s for single-package changes.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:16:42 +10:00
Ajax Davis
b4e22723bb fix: handle undefined npmKeywords in tool detail page
Add null check before accessing npmKeywords.length to prevent
runtime TypeError when npmKeywords is undefined.

Fixes: Cannot read properties of undefined (reading 'length')

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:14:20 +10:00
Ajax Davis
709c2c20db fix: handle undefined githubStars in tool detail page
Use loose equality (!=) instead of strict equality (!==) to check for
both null and undefined values. This prevents runtime TypeError when
githubStars is undefined.

Fixes: Cannot read properties of undefined (reading 'toLocaleString')

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:03:58 +10:00
Ajax Davis
c200a10015 feat: add collated error reporting to batch tool loading
Improve visibility when multiple tools fail during batch loading:

- Track success/failure status for each tool in batch
- Log consolidated summary with counts ( Successful: X/Y,  Failed: Y/Z)
- List all failed tools together with automatic health check confirmation
- Provide guidance to check individual error logs for detailed reasons

Also fix linting issues:
- Remove non-null assertions for safer code
- Fix template literals that don't need interpolation
- Add biome-ignore comments for AI SDK any types

This addresses error collation when multiple Railway tools fail,
making it easier to see the big picture while maintaining detailed
individual error logs and health check triggers.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:59:12 +10:00
Ajax Davis
84c6a579b2 feat: add automatic health check updates on Railway tool failures
Add real-time health status updates when tools fail to load or execute:

- Add @tpmjs/db dependency to playground package
- Create reportToolFailure() function to update health status on errors
- Trigger health check updates for:
  1. Import failures (Railway load-and-describe errors)
  2. Execution failures (Railway execute-tool errors)
- Updates are non-blocking and run in background
- Each tool failure now logs:
  - 🏥 Triggering health check for {package}/{export}
  -  Health status updated for {package}/{export}

This complements the proactive health checking (daily cron + manual recheck)
with reactive health updates from actual tool usage errors.

Handles multiple errors in batch loading - each error triggers its own
health check update independently and asynchronously.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:51:27 +10:00
Ajax Davis
bf8d27a59c feat: add GitHub Action for daily health check cron and backfill script
- Create .github/workflows/health-check.yml to run daily at 2am UTC
- Add scripts/backfill-health-checks.ts to populate health data for existing tools
- Remove health-check from vercel.json crons (now using GitHub Actions)

The GitHub Action workflow follows the same pattern as other sync operations
and calls the /api/sync/health-check endpoint with proper authentication.

The backfill script:
- Fetches all tools from database
- Runs batch health checks with concurrency control (5 tools at a time)
- Shows detailed progress and summary statistics
- Lists broken tools with error details

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:36:55 +10:00
Ajax Davis
f0ecdab824 feat: add health status UI to search and detail pages
Add comprehensive health status visibility across tool browsing:

Search Page (/tool/tool-search):
- Add health filter dropdown (All/Healthy Only/Broken Only)
- Show "Broken" badges on tool cards when import or execution fails
- Include health filter in Clear Filters button logic
- Update Tool interface with health fields

Detail Page (/tool/[...slug]):
- Add prominent warning banner for broken tools
- Display specific failure types (Import Failed / Execution Failed)
- Show health check error messages in code blocks
- Add manual "Recheck health" button with loading state
- Display last health check timestamp

Phase 3 of health check system implementation complete.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:30:04 +10:00
Ajax Davis
787410a7e0 feat: add broken tools page at /tool/broken
Create dedicated page to display all tools with failed health checks.

Features:
- Lists all tools with importHealth='BROKEN' OR executionHealth='BROKEN'
- Displays health status badges for both import and execution
- Shows error messages in code blocks for debugging
- Includes last checked timestamp
- Links to tool detail pages for manual recheck
- Shows empty state with checkmark when all tools are healthy
- Warning banner showing total broken tool count

UI Components:
- Card layout with red borders for broken tools
- Health status icons (check/x) for visual status
- Category badges and version info
- Direct links to tool detail pages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:20:47 +10:00
Ajax Davis
e5ba9fa441 feat: trigger health checks automatically when tools are synced
Add non-blocking health check calls to both sync endpoints:
- /api/sync/changes: Triggers health checks after tool upsert from changes feed
- /api/sync/keyword: Triggers health checks after tool upsert from keyword search

Health checks run asynchronously with 'sync' trigger source, ensuring:
- New/updated tools are validated immediately after sync
- Sync operations don't wait for health check completion
- Errors are logged but don't fail the sync

This completes Phase 2 of the health check system implementation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:16:30 +10:00
Ajax Davis
b00eae9fa2 feat: add manual health check endpoint and health filtering
API Endpoints (Phase 2 - Part 2):

1. Manual Health Check Endpoint
   - POST /api/tools/[...slug]
   - Added POST handler to existing tool detail route
   - Extracts slug parsing into shared parseSlug() helper
   - 5-minute rate limit per tool
   - Returns full health check results
   - Validates that export name is provided (can't check whole package)

2. Health Filtering in /api/tools
   - Add query params: ?importHealth=HEALTHY|BROKEN|UNKNOWN
   - Add query params: ?executionHealth=HEALTHY|BROKEN|UNKNOWN
   - Add shorthand: ?broken=true (at least one health check failed)
   - Health filters applied as AND conditions with search/category filters
   - Refactored to reduce complexity:
     - Extract buildHealthFilters() helper
     - Extract buildPackageFilter() helper
     - Extract buildWhereClause() helper

3. Code Quality Improvements
   - Extract parseSlug() helper to reduce duplication (DRY)
   - Remove useless else clauses (biome lint fix)
   - Reduce cognitive complexity (GET: 16->8, POST: simplified)

RESTful Design:
- GET /api/tools/@tpmjs/hello/hello -> Fetch tool data
- POST /api/tools/@tpmjs/hello/hello -> Trigger health check

Query Examples:
- /api/tools?broken=true (all broken tools)
- /api/tools?importHealth=HEALTHY&executionHealth=HEALTHY (fully healthy)
- /api/tools?q=text&broken=true (search "text" in broken tools)

Rate Limiting:
- Manual recheck cooldown: 5 minutes per tool
- Returns 429 with retryAfter seconds on rate limit

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:12:40 +10:00
Ajax Davis
148207bcaa feat: implement health check system (Phase 1 & 2)
Add comprehensive health monitoring for TPMJS tools that tracks both
import and execution health via Railway executor service.

## Database Schema

- Add HealthStatus enum (UNKNOWN, HEALTHY, BROKEN)
- Add HealthCheckType enum (IMPORT, EXECUTION, FULL)
- Add health fields to Tool model:
  - importHealth: tracks if tool can be loaded
  - executionHealth: tracks if tool can execute
  - lastHealthCheck: timestamp of last check
  - healthCheckError: stores error message
- Add HealthCheck audit table for full history

## Core Service

Create health-check-service.ts with 5 functions:
1. checkImportHealth() - Tests tool loading via /load-and-describe
2. checkExecutionHealth() - Tests execution via /execute-tool
3. generateTestParameters() - Creates minimal test params by type
4. performHealthCheck() - Full check with database updates
5. performBatchHealthCheck() - Processes tools in batches

Features:
- 30-second timeout per check
- Skips execution if import fails
- Batch processing (5 concurrent, 1s delays)
- Full audit trail in HealthCheck table

## API Endpoints

/api/sync/health-check (POST):
- Daily cron job at 2am UTC
- Checks all tools in database
- Requires CRON_SECRET auth
- Logs results to SyncLog table
- Max duration: 5 minutes

/api/tools/broken (GET):
- Lists all tools with broken health status
- Filters by importHealth='BROKEN' OR executionHealth='BROKEN'
- Includes package metadata
- Orders by lastHealthCheck DESC

## Configuration

- Add RAILWAY_EXECUTOR_URL to env.ts
- Add daily cron job to vercel.json
- Use db:push for schema changes (existing production data)

Next: Manual trigger endpoint + health filtering + UI components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:03:21 +10:00
Ajax Davis
8562eb5b38 feat: add Node.js compatibility layer to Railway executor
Add multi-strategy import system to support Node.js packages in Deno:

1. Primary: Use npm: specifier for Node.js compatibility mode
2. Fallback: Use esm.sh with explicit esnext target

This allows packages like ai-sdk-tool-code-execution that depend on
Node.js built-ins (node:sqlite, undici) to work in the Deno runtime.

Also added deno.json with nodeModulesDir and BYONM support.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:18:38 +10:00
Ajax Davis
934c8e9c0b fix: check for errorText field in tool error rendering
AI SDK streams use "errorText" field for tool errors, not "error".
Updated MessageBubble to check both errorText and error for compatibility.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:04:54 +10:00
Ajax Davis
19837ae800 feat: display tool execution errors in chat UI
Add error message rendering to MessageBubble component so users can see
detailed error messages when tools fail (e.g., missing API keys).

Previously only showed "output-error" badge without the actual error text.
Now displays the error message in a red-tinted box below the tool call.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 14:59:34 +10:00
Ajax Davis
d3f8adc0ea feat: add factory function support to Railway tool executor
Detects and handles tools exported as factory functions that require
configuration before returning the actual AI SDK tool object.

Supports multiple factory patterns:
1. No-args factory: toolName()
2. Config object: toolName({ apiKey: 'xxx' })
3. Single-arg: toolName('api-key-value')

For config objects, tries multiple key name variations:
- Raw env vars: { VALYU_API_KEY: 'xxx' }
- Normalized apiKey: { apiKey: 'xxx' }
- Normalized key: { key: 'xxx' }

This enables dynamic loading of tools like @valyu/ai-sdk paperSearch
that use factory patterns instead of direct tool exports.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 14:52:47 +10:00
Ajax Davis
d0110f990d fix: read env vars directly from localStorage to avoid React closure issue
The previous approach using useEnvVars() hook had a closure problem:
- envVars started as [] on first render
- buildEnvObject captured this empty array
- Even with function body, the transport memoized the old buildEnvObject

Solution: Read directly from localStorage inside the body function
- Bypasses React state entirely
- Gets fresh values on each request
- No closure issues

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 14:36:51 +10:00
Ajax Davis
c6b4456baf fix: use function body in DefaultChatTransport to send latest env vars
PROBLEM:
DefaultChatTransport body is cloned ONCE on mount, so env vars
were always empty {} even after localStorage loaded them.

SOLUTION:
Use a function for body instead of an object. AI SDK v6 calls
body() on each request, ensuring latest env vars are sent.

Changes:
- useChat.ts: body: { env } → body: () => ({ env: buildEnvObject() })
- buildEnvObject() is called fresh on each request
- Env vars now sent correctly to /api/chat

Credit: ChatGPT for identifying the exact AI SDK v6 pattern

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 14:25:59 +10:00
Ajax Davis
4094c661be fix: properly pass env vars to cached tools and add extensive logging
PROBLEM:
- Tool wrappers cached env vars in closure, so cached tools used stale env
- Client env vars weren't reaching Railway executor even when provided
- No visibility into env var flow through the system

SOLUTION:
1. Store env vars per conversation in conversationEnv Map
2. Tool execute functions look up latest env from Map (not closure)
3. Chat API calls setConversationEnv() on each request
4. Added logging at every step

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 14:18:32 +10:00
Ajax Davis
d339637ebf feat: implement BM25 search with context from last 3 user messages
- Implement proper BM25 scoring algorithm in /api/tools/search
  - Term frequency with saturation (k1 = 1.5)
  - Length normalization (b = 0.75)
  - Inverse document frequency (IDF)
- Accept recent messages via 'messages' query param for better context
- Update search-registry tool to:
  - Use /api/tools/search endpoint (not /api/tools)
  - Pass last 3 user messages for contextual search
  - Include recentMessages in tool input schema
- Update chat API to extract and pass last 3 user messages to search

BM25 formula: Σ IDF(qi) * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * |D| / avgdl))

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:59:22 +10:00
Ajax Davis
9f85898937 fix: use deployed /api/tools endpoint with client-side filtering and streamline git hooks
- Change search-registry to use /api/tools instead of /api/tools/search (not deployed yet)
- Add client-side filtering for search queries since deployed API doesn't support search
- Handle both deployed (/api/tools) and local dev (/api/tools/search) response formats
- Remove lint from pre-commit hooks to speed up commits (keep format + type-check)
- Fixes 404 errors when playground tries to search tools in production

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:43:19 +10:00
Ajax Davis
45b477397f fix: use production API URL for tool search in Vercel deployments
- Change default TPMJS_API_URL to https://tpmjs.com in production
- Keep localhost:3000 for local development
- Fixes "ECONNREFUSED 127.0.0.1:3000" error in Vercel
- Allows playground to search tools from production registry

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:34:29 +10:00
Ajax Davis
cc14476a36 fix: make OPENAI_API_KEY optional for playground to allow client-provided keys
- Make OPENAI_API_KEY optional in env validation
- Move OpenAI client initialization from module level to runtime
- Accept API key from client UI (Settings sidebar) or server env
- Return clear error message if no API key is provided
- Fixes Vercel build failure due to missing env var at build time

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:22:23 +10:00
Ajax Davis
8b5757c9aa fix: migrate playground to AI SDK v6 beta and resolve TypeScript errors
- Update @ai-sdk/react to v3.0.0-beta.131 for compatibility with AI SDK v6
- Fix tool execute method calls with type assertions in API routes
- Update chat components to use UIMessage types from AI SDK
- Move body option into DefaultChatTransport constructor
- Remove deprecated onResponse option from useChat hook
- Remove unused isCoreTool type guard function
- Fix Button variant from 'primary' to 'default'

Resolves all TypeScript compilation errors and build passes successfully.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:15:21 +10:00
Ajax Davis
54b504f661 fix: add non-null assertion for searchTpmjsToolsTool.execute
TypeScript was complaining that execute might be undefined, but tools created
with tool() from AI SDK always have an execute method. Added non-null assertion
with biome-ignore comment to fix the TypeScript build error.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 12:38:39 +10:00
Ajax Davis
d4ec877c6c fix: use inputSchema instead of parameters for AI SDK v6 tool
Changed debug logging to access inputSchema property which exists on AI SDK v6 tools,
instead of parameters which doesn't exist. This fixes the TypeScript build error.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 12:34:32 +10:00
Ajax Davis
e8ef703c6e fix: remove unused NextResponse import in playground chat route
Removed unused NextResponse import that was causing TypeScript build error.
The error handler already uses native Response constructor directly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 12:31:11 +10:00
Ajax Davis
f127b47f08 feat: add playground sidebars with dynamic tools and env var management
Left Sidebar:
- Create /api/tools endpoint to fetch tools from registry
- Update ToolsSidebar to fetch and display tools dynamically
- Add filter input for searching tools by name/description/category
- Fix interface to use packageName/exportName from search registry

Right Sidebar:
- Create SettingsSidebar with environment variable management
- Add localStorage persistence for env vars
- Implement password masking for values
- Export useEnvVars() hook for accessing env vars

Environment Variable Forwarding:
- Update useChat hook to read and forward env vars to API
- Update chat route to extract env vars from request body
- Update dynamic-tool-loader to accept and forward env vars
- Update Railway executor to inject env vars into Deno environment
- Complete chain: localStorage → client → chat → Railway → Deno.env

Bug Fixes:
- Fix undefined property errors in tool detail page
- Add optional chaining for npmDownloadsLastMonth and qualityScore

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 11:00:20 +10:00
Ajax Davis
c18d1b7ea6 feat: add Zod v3 schema support via zod-to-json-schema
- Import zod-to-json-schema from esm.sh
- Add Strategy 3: detect Zod schemas via _def property
- Convert Zod v3/v4 schemas to JSON Schema
- Update error message to mention Zod v3 support
- Fixes firecrawl-aisdk tool schema extraction
2025-12-04 09:49:39 +10:00
Ajax Davis
b6cc98d1e0 feat: add fallback schema extraction with Zod v4 support
- Try Zod v4 toJSONSchema() or jsonSchema() first
- Fall back to AI SDK jsonSchema.schema property
- Fail gracefully with detailed debug info
- Supports both Zod-based and jsonSchema-based tools
2025-12-04 09:42:53 +10:00
Ajax Davis
b95edd8541 debug: add logging for inputSchema structure inspection 2025-12-04 09:33:09 +10:00
Ajax Davis
1e61ba1551 fix: use AI SDK tool() function to create proper tool wrappers
CRITICAL FIX: Was creating plain objects instead of using tool() from AI SDK.

The issue:
- Creating { description, inputSchema, execute } plain objects
- OpenAI receives invalid tool format: "type: None"
- AI SDK needs tools created with tool() function

The fix:
- Import tool() and jsonSchema() from 'ai'
- Use tool() to wrap the remote execution
- Use jsonSchema() to wrap the JSON Schema received from Railway
- Matches the format used in packages/tools/hello

Example from hello tool:
```ts
tool({
  description: "...",
  inputSchema: jsonSchema({ type: 'object', properties: {...} }),
  execute: async (params) => {...}
})
```

Now the playground creates tools the same way!

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 09:13:10 +10:00
Ajax Davis
b7d1accc6f fix: extract and serialize JSON Schema from AI SDK v6 tools correctly
THE BREAKTHROUGH: AI SDK v6 tools use jsonSchema() which wraps plain JSON Schema objects, NOT Zod schemas. JSON Schema is fully serializable.

Changes:
1. Railway server: Extract raw JSON Schema from toolModule.inputSchema?.schema
2. Playground loader: Wrap received JSON Schema with { type: 'json_schema', schema: ... }
3. This matches AI SDK v6 format exactly - no Zod serialization needed

How it works:
- Tools define inputSchema: jsonSchema({ type: 'object', properties: {...} })
- AI SDK stores it as { type: 'json_schema', schema: {...} }
- Railway extracts the plain JSON Schema (.schema property)
- Sends it as plain JSON (fully serializable)
- Playground wraps it back in AI SDK format
- OpenAI receives valid JSON Schema for function calling

This fixes both errors:
 No more "def.shape is not a function" (not using Zod)
 No more "Invalid schema type None" (proper JSON Schema provided)

Note: Tools using Zod instead of jsonSchema() will need to migrate.
TPMJS standard: All tools MUST use jsonSchema() with plain JSON Schema.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 09:00:00 +10:00
Ajax Davis
09df09c4df docs: document Zod schema serialization problem for external consultation 2025-12-04 08:53:26 +10:00
Ajax Davis
d3b928c97e fix: use Dockerfile instead of Nixpacks for Railway deployment
Railway is deprecating Nixpacks, so switching to a standard Dockerfile
with the official Deno image. This provides a cleaner and more maintainable
deployment configuration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 08:35:50 +10:00
Ajax Davis
3450d7a6d5 fix: add nixpacks.toml to configure Deno for Railway deployment
Railway's Nixpacks builder needs explicit configuration to install Deno.
This adds nixpacks.toml to specify Deno as a required package.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 08:30:19 +10:00
Ajax Davis
2158ee6dfd feat: add dynamic tool loading system with Railway executor
Implements a complete dynamic tool loading system that allows the playground to discover and load tools from the TPMJS registry at runtime.

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

**Key Components:**

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 08:25:53 +10:00
Ajax Davis
0612eac5e2 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>
2025-12-04 06:39:58 +10:00
Ajax Davis
fdd1b2c304 fix: sanitize tool names for OpenAI API compatibility in playground
Add sanitizeToolName function to convert package names to OpenAI-compatible
format that matches pattern ^[a-zA-Z0-9_-]+$. Removes @ symbols, replaces
/ with _, and replaces other invalid characters with _.

This fixes the error: "Invalid 'tools[0].name': string does not match pattern"
when loading tools in the playground chat interface.

Example transformations:
- @tpmjs/hello-helloWorldTool → tpmjs_hello-helloWorldTool
- firecrawl-aisdk-scrapeTool → firecrawl-aisdk-scrapeTool

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 06:29:33 +10:00
Ajax Davis
635fc96cac feat: implement playground app with AI SDK v6 tool execution
- Create new Next.js app at apps/playground for testing TPMJS tools
- Implement AI SDK v6 patterns with DefaultChatTransport and UIMessage format
- Create template tool package at packages/tools/hello with hello-world and hello-name tools
- Use tool() and jsonSchema() helpers to avoid Zod 4 conversion issues with OpenAI
- Add static tool loading system with switch statement (Next.js/webpack compatible)
- Implement chat interface with tool call visualization showing inputs/outputs
- Support multi-step tool execution with stepCountIs(5)
- Stream responses with toUIMessageStreamResponse() for full tool support
- Add sidebar showing available tools (static list)
- Use parts-based message rendering for text and tool calls
- Integrate firecrawl-aisdk tools (scrape, crawl, search)
- Add theme toggle in header (defaults to light mode)
- Fix responsive layout with max-width for message bubbles
- Use biome-ignore comments for legitimate any types in tool loading

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 02:51:02 +10:00
Ajax Davis
fcd6667357 feat: add author name to skipped packages in Discord notifications
API Changes:
- Track author name along with package name and reason
- Extract author from pkg.author (string or object with name field)
- Default to "unknown" if author info not available

Workflow Changes:
- Display author in format: "package-name (by author) - reason"

Example Discord output:
📋 Skipped Packages
tpmjs-threejs-tool (by john-doe) - invalid tpmjs field
@scope/package-1 (by jane-smith) - missing tpmjs field

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 09:18:16 +10:00
Ajax Davis
14cd668a2b feat: add skip reasons to Discord notifications
API Changes:
- Track skip reason along with package name
- Changed skippedPackages from string[] to Array<{name, reason}>
- Reasons: "package not found", "missing tpmjs field", "invalid tpmjs field"

Workflow Changes:
- Format skipped packages as "package-name - reason"
- Display one package per line in Discord notification

Example Discord output:
📋 Skipped Packages
@scope/package-1 - missing tpmjs field
package-2 - invalid tpmjs field

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 08:59:33 +10:00
Ajax Davis
493cd96d7b feat: add skipped packages list to Discord notifications
API Changes:
- Track skipped package names in keyword sync endpoint
- Include skippedPackages array in API response

Workflow Changes:
- Extract skipped package names from sync response
- Display skipped packages in Discord notification as comma-separated list
- Only show "📋 Skipped Packages" field when packages are skipped
- Dynamic field construction using jq

Example Discord output:
📋 Skipped Packages
package-name-1, package-name-2, package-name-3

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 08:57:30 +10:00
Ajax Davis
2bf8565c1d fix: make example field optional and fix Discord webhook JSON escaping
Database Changes:
- Made `example` field optional in Prisma schema (String?)
- Added default empty arrays for `frameworks` and `tags`
- Allows tools to be synced without example field

Workflow Changes:
- Use jq to properly construct Discord webhook JSON
- Fixes "invalid JSON" error caused by unescaped special characters
- Properly escapes error messages with newlines and quotes

This fixes sync errors for packages missing the example field and
ensures Discord notifications are sent successfully.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 08:32:14 +10:00
Ajax Davis
7fab0440b3 feat: add detailed error logging to keyword sync workflow
API Changes:
- Return errorMessages array in sync response (first 5 errors)

Workflow Changes:
- Display error messages in GitHub Actions logs with formatting
- Include error details in Discord notifications (first 3 errors)
- Shows errors in both console output and Discord embed

Example output:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️  SYNC ERRORS (4 total):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  • Failed to process pkg1: Invalid tpmjs field
  • Failed to process pkg2: Network timeout
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 08:22:04 +10:00
Ajax Davis
bc34f50705 feat: add Discord webhook notifications to keyword search sync workflow
- Captures sync API response and parses JSON results
- Sends formatted Discord embed with sync metrics:
  - Packages found, processed, skipped, errors
  - Duration and link to workflow logs
- Color-coded status: green for success, yellow for errors
- Runs on every sync (manual and scheduled)

Requires DISCORD_WEBHOOK secret to be set in GitHub repository.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 08:18:15 +10:00
Ajax Davis
4febe71d76 fix: split sync workflow into separate files to fix job skipping issue
The previous combined workflow had flawed conditional logic that caused
sync-keyword and sync-metrics jobs to be skipped. GitHub Actions doesn't
populate github.event.schedule with the cron expression, so the equality
checks never matched.

Replaced with three separate workflow files:
- sync-changes.yml: Runs every 2 minutes
- sync-keyword.yml: Runs every 15 minutes
- sync-metrics.yml: Runs every hour

Each workflow can be manually triggered via workflow_dispatch.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 04:36:38 +10:00
Ajax Davis
8e8e511e5a refactor: remove links, tags, and status fields from TPMJS spec
These fields are redundant as they already exist in package.json:
- links: Use package.json repository/homepage fields
- tags: Use package.json keywords field
- status: Not needed in tool metadata

Changes:
- Remove TpmjsLinksSchema type definition
- Remove links, tags, and status from TpmjsRichSchema
- Update validation logic to not check these fields
- Update all documentation (spec page, publish page, HOW_TO_PUBLISH_A_TOOL.md)
- Update example tool package.json
- Simplify field reference tables

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 05:53:48 +10:00
Ajax Davis
054bac9a53 refactor: rename envVars to env and remove example field from TPMJS spec
- Rename TpmjsEnvVarSchema to TpmjsEnvSchema
- Rename envVars field to env throughout codebase
- Remove example field from TpmjsMinimalSchema (no longer required)
- Update all documentation (spec page, publish page, HOW_TO_PUBLISH_A_TOOL.md)
- Update example tool package.json
- Simplify minimal tier requirements to only category and description

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 05:37:28 +10:00
Ajax Davis
4146f279e6 refactor: replace authentication field with envVars in TPMJS specification
Replace the authentication field with a more general envVars array that allows tools to specify required environment variables:

Changes to type definitions:
- Remove TpmjsAuthenticationSchema and TpmjsAuthentication type
- Add TpmjsEnvVarSchema with fields: name, description, required, default
- Replace authentication field with envVars array in TpmjsRichSchema
- Update validateTpmjsField to check envVars instead of authentication

Changes to documentation:
- Update /spec page to document envVars instead of authentication
- Update /publish page examples to use envVars
- Update HOW_TO_PUBLISH_A_TOOL.md with envVars examples
- Update validation errors section
- Remove authentication from @tpmjs/createblogpost example

The envVars field is more flexible and clearer - it lists all environment variables a tool needs (API keys, endpoints, config values) rather than trying to categorize authentication types.

Example:
```json
"envVars": [
  {
    "name": "OPENAI_API_KEY",
    "description": "API key for OpenAI services",
    "required": true
  }
]
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 05:21:30 +10:00
Ajax Davis
4dc53338f2 refactor: remove pricing field from TPMJS specification
Remove pricing field from the entire project as it doesn't make sense for tool metadata:

- Remove TpmjsPricingSchema and TpmjsPricing type from types package
- Remove pricing from TpmjsRichSchema validation
- Remove pricing from validateTpmjsField check
- Remove pricing documentation from /spec page
- Remove pricing examples from /publish page
- Remove pricing from HOW_TO_PUBLISH_A_TOOL.md
- Remove pricing from @tpmjs/createblogpost example package.json

The pricing field was removed from Tier 3 (Rich) metadata as it's not relevant for tool discovery and integration. Tools can document pricing in their README or documentation links instead.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 04:57:21 +10:00
Ajax Davis
251442f1d4 feat: add comprehensive TPMJS specification page
- Create /spec page with complete technical reference for TPMJS metadata
- Document all three tiers (Minimal, Basic, Rich) with field explanations
- Include field reference table with types and requirements
- Explain quality scoring algorithm and discovery mechanisms
- Add cross-links to /publish page for complementary content
- Update AppHeader to include Spec link in navigation (Tools > Playground > Spec > GitHub > Publish)

The spec page provides a balanced technical reference while the publish page remains the practical how-to guide.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 04:47:21 +10:00
Ajax Davis
c8ccab5cb8 refactor: create shared AppHeader component for consistent navigation across all pages
**Problem:**
- Each page had different header implementations with varying navigation links
- Inconsistent user experience across homepage, tools, playground, and publish pages
- Duplicate header code throughout the application

**Solution:**
- Created `AppHeader` component (apps/web/src/components/AppHeader.tsx) with consistent navigation:
  - TPMJS logo linking to homepage
  - Tools, Playground, and Publish Tool links
  - GitHub icon link
  - Sticky header with medium size
- Updated all pages to use the shared component:
  - apps/web/src/app/page.tsx (homepage)
  - apps/web/src/app/tool/tool-search/page.tsx (tools search)
  - apps/web/src/app/playground/page.tsx (component playground)
  - apps/web/src/app/publish/page.tsx (publish guide)
  - apps/web/src/app/tool/[...slug]/page.tsx (tool detail pages)

**Benefits:**
- Consistent header across all pages
- Single source of truth for navigation
- Easier to maintain and update navigation links
- Improved user experience with predictable navigation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 21:03:33 +10:00
Ajax Davis
0b4a314bd1 docs: add comprehensive NPM package sync documentation and GitHub Actions workflow
**Documentation (CLAUDE.md):**
- Document all three sync endpoints (changes feed, keyword search, metrics)
- Explain Vercel Cron configuration and schedules
- Detail quality score calculation algorithm
- Add database schema documentation for sync tables
- Provide manual sync trigger examples with curl
- Include monitoring and debugging instructions
- Document error handling patterns (partial/complete failures)
- Add package discovery flow diagram
- List future improvements and potential enhancements

**GitHub Actions Workflow (.github/workflows/sync.yml):**
- Add backup sync automation via GitHub Actions cron
- Support manual trigger via workflow_dispatch with sync type selection
- Run changes feed every 2 minutes
- Run keyword search every 15 minutes
- Run metrics sync every hour
- Use concurrency control to prevent overlapping runs
- Call production Vercel endpoints with CRON_SECRET auth

**Key Features:**
- Dual automation strategy: Vercel Cron (primary) + GitHub Actions (backup)
- Idempotent endpoints allow both systems to run simultaneously
- Manual trigger capability for debugging and testing
- Comprehensive documentation for future maintenance

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 20:46:57 +10:00
Ajax Davis
61ac2aa5dd feat: display raw JSON output and human-readable preview in playground 2025-11-30 20:41:30 +10:00
Ajax Davis
47ddc4dea7 fix: refactor tool execution to use generateText with proper tool handling
Major changes:
- Fixed package executor URL protocol handling (add https:// if missing)
- Switched from streamText to generateText for proper tool execution
- AI now calls tool AND generates natural language response
- Tool results no longer show raw JSON metadata

Tool executor (tool-executor-agent.ts):
- Use generateText() instead of streamText() for tool execution
- Add system prompt to guide AI to summarize tool results
- Return result.text for natural language output
- Tool definition uses inputSchema (AI SDK v6 format)

Package executor:
- Add getSandboxUrl() to ensure URL has https:// protocol
- Fixes "Failed to parse URL" error in production

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 20:30:01 +10:00
Ajax Davis
45da32e8ec fix: extract tool results from messages array in AI SDK v6
- In AI SDK v6, tool results are in fullResponse.messages with role 'tool'
- Updated result extraction to iterate through messages array
- Added detailed logging to debug response structure
- Handle both text output and tool-only responses

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 20:05:41 +10:00
Ajax Davis
266a6ef74c debug: add logging to diagnose streaming issue in AI SDK v6 2025-11-30 19:50:41 +10:00
Ajax Davis
2a05e68aa3 fix: preserve streamed output and add markdown rendering to playground
**Problem:**
- Tool playground was only showing partial output
- When 'complete' event arrived, it replaced streamed text with final output
- Output was displayed as plain text instead of formatted markdown

**Changes:**
- Fixed streaming bug in ToolPlayground component
  - Removed line that overwrote accumulated text on 'complete' event
  - Now preserves all streamed chunks for full output display
- Added react-markdown with GitHub Flavored Markdown support
  - Install react-markdown and remark-gfm packages
  - Added @tailwindcss/typography plugin for prose styling
  - Replaced plain <pre> with <ReactMarkdown> component
  - Applied prose classes for proper markdown formatting
- Added type="button" to button elements for accessibility

**Files changed:**
- apps/web/src/components/ToolPlayground.tsx
  - Comment out setOutput(data.output) on complete event
  - Import ReactMarkdown and remarkGfm
  - Replace pre element with ReactMarkdown component
  - Add prose styling classes
  - Add type="button" to buttons
- apps/web/tailwind.config.ts
  - Add @tailwindcss/typography plugin

**Result:**
- Full streamed output now displays correctly
- Markdown is rendered with proper formatting (headings, lists, code blocks, etc.)
- Better UX for AI-generated tool responses

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 19:28:53 +10:00
Ajax Davis
78c72b85f3 feat: upgrade to AI SDK v6 beta and Zod v4 to fix tool schema errors
OpenAI was rejecting tool definitions with error "schema must be a JSON Schema of 'type: "object"'". This was caused by AI SDK v5 not properly converting Zod schemas to JSON Schema format.

**Changes:**
- Upgrade AI SDK from v5.0.104 to v6.0.0-beta.124
- Upgrade @ai-sdk/openai from v2.0.74 to v3.0.0-beta.22
- Upgrade Zod from v3.25.76 to v4.1.13 across all packages

**AI SDK v6 breaking changes:**
- Tool definition API: `parameters` renamed to `inputSchema`
- Removed `aiTool()` wrapper - use plain object with description, inputSchema, execute
- Streaming API: Use `textStream` async iterator instead of onChunk callback
- Zod schemas now properly converted to JSON Schema for OpenAI

**Zod v4 breaking changes:**
- `z.record()` now requires two arguments: `z.record(keySchema, valueSchema)`
- `z.enum()` params changed: `errorMap` removed, use `message` instead
- Type system improvements require explicit type parameters
- Fixed type errors in @tpmjs/env, @tpmjs/npm-client, @tpmjs/types

**Files changed:**
- apps/web/src/lib/ai-agent/tool-executor-agent.ts
  - Updated tool definition to use `inputSchema` instead of `parameters`
  - Removed `aiTool()` wrapper
  - Fixed streaming to use `textStream` iterator
- packages/env/src/index.ts
  - Updated type constraint from `z.ZodRawShape` to `Record<string, z.ZodTypeAny>`
- packages/npm-client/src/package.ts
  - Fixed `z.record()` calls to include both key and value schemas
  - Added type assertions for record indexing
- packages/types/src/tpmjs.ts
  - Changed `errorMap` to `message` in z.enum() calls

**Testing:**
-  Type-check passes
-  Production build succeeds
-  All routes compile correctly

This fixes the tool execution error where OpenAI rejected tool schemas with invalid format.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 19:14:01 +10:00
Ajax Davis
59da95ce85 feat: update homepage to use real database data
- Add server component to fetch live stats from database
  - Tool count from Tool table
  - Invocations from successful Simulation records
  - Average latency from recent executions
  - Category distribution stats

- Add featured tools section
  - Display top 6 tools by quality score
  - Show tool cards with name, description, category, tags
  - Include quality score and download metrics
  - Official badge for verified tools
  - Click-through to tool detail pages

- Update HeroSection component
  - Accept stats prop with real database metrics
  - Format large numbers (e.g., "1.2K", "5.3M")
  - Add functional search navigation
  - Enter key and button click navigate to tool-search
  - Empty search browses all tools

- Optimize database queries
  - Use Promise.all() for parallel execution
  - Calculate avg latency from last 100 simulations
  - Graceful error handling with fallback values

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 15:33:10 +10:00
Ajax Davis
1cdbf8c891 debug: add detailed logging to tool executor to diagnose OpenAI schema error
Add console.log statements to track:
- Tool parameters array and length
- Generated Zod schema details
- Tool definition structure
- Sanitized tool name
- Complete tools config sent to OpenAI

This will help diagnose why OpenAI is still receiving 'type: "None"'
for empty parameter schemas despite the fix.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 05:46:13 +10:00
Ajax Davis
c0383f5440 fix(vercel): run install command from monorepo root to access workspace packages
**Problem:**
Vercel deployments were failing with:
```
ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE: No matching version found for @tpmjs/eslint-config@* inside the workspace
```

The `installCommand` was running `pnpm install` from `apps/web`, which couldn't access workspace packages defined at the monorepo root.

**Solution:**
Change `installCommand` from `pnpm install` to `cd ../.. && pnpm install` to run from the monorepo root, matching the `buildCommand` behavior.

**Impact:**
- Vercel can now find and install all workspace packages
- Deployments should succeed
- All previous tool execution fixes can now actually deploy

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 05:07:28 +10:00
Ajax Davis
ad4fde7e79 fix(ci): skip Lefthook installation in CI/Vercel environments
**Problem:**
Vercel deployments were failing during `pnpm install` because the `prepare` script tried to run `lefthook install`, which requires a git repository. Vercel's build environment doesn't have a proper `.git` directory, causing:
```
fatal: not a git repository (or any parent up to mount point /vercel)
Error: exit status 128
```

**Solution:**
Skip Lefthook installation when running in CI or Vercel environments by checking `process.env.CI` and `process.env.VERCEL` before attempting to install git hooks.

**Impact:**
- Vercel deployments will now succeed
- Local development still gets git hooks installed
- GitHub Actions CI will skip hook installation (not needed in CI)
- All previous tool execution fixes can now actually deploy

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 05:00:00 +10:00
Ajax Davis
4705af6b1a fix: ensure tool parameters schema is always a valid JSON Schema object
Problem: OpenAI API error 'got type: None' when tool has no/invalid parameters

Solution: Add guard to explicitly create empty object schema with description when no parameters exist

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 02:45:32 +10:00
Ajax Davis
8f3c6a2910 fix: sanitize npm package names for OpenAI tool names and update lockfile
Problem: OpenAI tool names must match ^[a-zA-Z0-9_-]+ but npm package names like @tpmjs/createblogpost contain @ and /

Solution: Add sanitizeToolName() function and update pnpm-lock.yaml

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 02:28:42 +10:00
Ajax Davis
fdfc721d40 fix: remove tiktoken dependency to resolve WASM runtime error in serverless
**Problem:**
The Interactive Playground was failing with "Missing tiktoken_bg.wasm" error in production. Tiktoken requires WASM files which don't work in Vercel's serverless environment.

**Solution:**
- Remove tiktoken import from tool-executor-agent
- Replace tiktoken-based token counting with character estimation (~4 chars/token)
- Remove tiktoken from package.json dependencies
- Remove unused biome-ignore comment

**Impact:**
- Token counting is now approximate but consistent
- No more WASM-related runtime errors
- Serverless deployment works properly
- Tool execution now functional in production

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 02:19:05 +10:00
Ajax Davis
cc13e7cb09 fix: remove tiktoken dependency to resolve WASM runtime error in serverless
**Problem:**
The Interactive Playground was failing with "Missing tiktoken_bg.wasm" error in production. Tiktoken requires WASM files which don't work in Vercel's serverless environment.

**Solution:**
- Remove tiktoken import from tool-executor-agent
- Replace tiktoken-based token counting with character estimation (~4 chars/token)
- Remove tiktoken from package.json dependencies
- Remove experimental webpack WASM config (no longer needed)

**Impact:**
- Token counting is now approximate but consistent
- No more WASM-related runtime errors
- Serverless deployment works properly
- Tool execution now functional in production

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 02:15:58 +10:00
Ajax Davis
492a7221c9 fix: update dependency cruiser config to allow TypeScript path aliases and workspace packages
- Add `^@/` to pathNot to allow Next.js `@/` path alias
- Add `^@tpmjs/` to pathNot to allow workspace package imports
- Fixes architecture check failures in CI
- Apply Biome formatting to check-tool.mjs and sync-single-tool.mjs

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:55:51 +10:00
Ajax Davis
664bbdad57 fix: use dynamic import for AI SDK to resolve tiktoken WASM build error
Convert static import of executeToolWithAgent to dynamic import inside the
POST handler. This prevents the AI SDK (and its tiktoken dependency) from
being loaded at build time, which was causing WASM loading errors.

**Why this fix works:**
- Next.js 16 + Turbopack tries to analyze routes at build time
- tiktoken requires tiktoken_bg.wasm which can't load during static analysis
- Dynamic imports defer loading until runtime, avoiding build-time WASM issues

**Changes:**
- Remove: `import { executeToolWithAgent } from '@/lib/ai-agent/tool-executor-agent'`
- Add: `const { executeToolWithAgent } = await import('@/lib/ai-agent/tool-executor-agent')`
  inside the stream start() handler

Build now completes successfully. Route is properly marked as dynamic (ƒ).

Resolves: "Error: Missing tiktoken_bg.wasm" during Next.js build

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:50:49 +10:00
Ajax Davis
140a2fa218 fix: mark AI tool execution route as dynamic to prevent build-time WASM error
Add `export const dynamic = 'force-dynamic'` to `/api/tools/execute/[...slug]`
to prevent Next.js from attempting static generation at build time.

The AI SDK (used via executeToolWithAgent) requires tiktoken_bg.wasm which
cannot be loaded during static generation. Marking as dynamic ensures the
route is only executed at runtime.

Note: This partially addresses the build error but further investigation needed
for complete resolution of tiktoken WASM loading in Next.js 16 + Turbopack.

Relates to: "Error: Missing tiktoken_bg.wasm"

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:42:04 +10:00
Ajax Davis
2ce37ae2aa feat: integrate Railway sandbox microservice for secure package execution
**Replace VM2 with Railway Microservice:**
- Remove VM2 dependency (incompatible with Next.js Turbopack bundling)
- Create Express sandbox service at `/services/sandbox-executor/`
- Uses isolated-vm for V8-level isolation with 128MB memory limit
- 10-second execution timeout with proper error handling

**Package Executor Client:**
- Rewrite `@tpmjs/package-executor` to call remote sandbox via HTTP
- Add `executePackage()`, `clearCache()`, `checkHealth()` functions
- Use AbortController for timeout handling
- Proper TypeScript type assertions for API responses
- Reads `SANDBOX_EXECUTOR_URL` from environment (defaults to localhost:3000)

**Sandbox Service Features:**
- `/execute` - Execute npm packages in isolated environment
- `/health` - Health check endpoint with service info
- `/cache/clear` - Clear npm package cache
- Package caching in `/tmp/.tpmjs-cache` for faster subsequent runs
- CORS support for web app integration
- Automatic ESM/CommonJS package detection

**Deployment Configuration:**
- Dockerfile with isolated-vm native dependencies (python3, make, g++)
- Railway.json with health checks and restart policies
- Environment variables: PORT, PACKAGE_CACHE_DIR, ALLOWED_ORIGINS
- Production URL: https://tpmjs-production.up.railway.app

**Integration:**
- Add SANDBOX_EXECUTOR_URL to .env.local
- Update Next.js config to mark package-executor as external
- Maintain existing API routes at `/api/tools/execute/[...slug]`

This architectural change enables secure package execution on Vercel
by moving sandboxing to a dedicated microservice on Railway.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:30:47 +10:00
Ajax Davis
b176ae54df feat: add Railway-based sandbox executor microservice
**New Sandbox Service:**
- Separate microservice for secure npm package execution
- Uses isolated-vm for V8-level isolation
- 128MB memory limit, 10s timeout
- Package caching for performance
- Express API with /execute, /health, /cache/clear endpoints

**Why Microservice:**
- VM2 doesn't work with Next.js Turbopack (requires runtime file access)
- isolated-vm doesn't work in Vercel serverless (native bindings)
- Microservice allows full Node environment with proper sandboxing
- Industry standard approach (Replit, CodeSandbox, RunKit)

**Deployment:**
- Dockerfile with isolated-vm build dependencies
- Railway.json configuration
- Health checks and auto-restart policies
- CORS configuration for Next.js integration

**Next Steps:**
1. Deploy to Railway:
   cd services/sandbox-executor
   railway init
   railway up
2. Set SANDBOX_EXECUTOR_URL in Next.js env
3. Update API routes to call sandbox service

This provides secure, production-ready package execution outside Vercel's constraints.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:07:24 +10:00
Ajax Davis
d73db1c333 fix: remove VM2 sandboxing to resolve Next.js build errors
**VM2 Removal:**
- Remove VM2 dependency from package-executor
- Rewrite executor to use direct package execution with require()
- Add TODO comment for future sandboxing implementation

**Why this change:**
- VM2 requires runtime filesystem access to bridge.js which doesn't work with Next.js Turbopack bundling
- Even marking as serverExternalPackages fails because VM2 uses hardcoded file paths
- Direct execution allows builds to complete while we find Next.js-compatible sandboxing solution

**Next Steps:**
- Implement proper sandboxing with isolated-vm or similar Next.js-compatible solution
- Add security measures for package execution
- Consider moving package execution to separate microservice

This unblocks CI/CD while maintaining playground functionality.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:57:32 +10:00
Ajax Davis
ca57de533a fix: resolve Next.js routing and package-executor build errors
**API Route Restructuring:**
- Move execute endpoint from /api/tools/[...slug]/execute to /api/tools/execute/[...slug]
- Move simulations endpoint from /api/tools/[...slug]/simulations to /api/tools/simulations/[...slug]
- Fix Next.js App Router constraint: catch-all segments must be terminal
- Update ToolPlayground component to use new endpoint paths

**Package Executor Export Fix:**
- Remove .js extensions from exports in @tpmjs/package-executor
- Change from './types.js' to './types' for proper TypeScript resolution
- Change from './executor.js' to './executor' for proper TypeScript resolution
- Fixes "Export executePackage doesn't exist in target module" build error

**Next.js Configuration:**
- Add vm2 and @tpmjs/package-executor to serverExternalPackages
- Prevents bundling VM2 which requires filesystem access to internal files

**Code Quality:**
- Add biome-ignore for excessive complexity in SSE stream handling
- Add biome-ignore for decorative loading spinner SVGs (2 instances)

Note: VM2 sandboxing still has compatibility issues with Next.js Turbopack.
This may need to be replaced with a different sandboxing approach or disabled.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:52:56 +10:00
Ajax Davis
6eb51d1371 feat: add interactive tool playground with AI-powered execution
Implement comprehensive tool testing environment with real package execution, AI agents, and token tracking.

**New Features:**
- Interactive playground UI with 4 tabs (Input, Output, Logs, Token Usage)
- Real npm package execution in VM2 sandbox with security constraints
- AI-powered tool execution using AI SDK v5 and GPT-4 Turbo
- Server-Sent Events (SSE) streaming for real-time progress updates
- Comprehensive 4-category token tracking (Input, Tool Description, Schema, Output)
- Visual token breakdown with colored progress bars
- IP-based rate limiting (10 executions/hour per IP)
- Database persistence of all simulations with full metadata

**Database Schema:**
- New `Simulation` model for execution records
- New `TokenUsage` model for detailed token metrics
- New `ExecutionLog` model for execution event tracking
- Added simulations relation to Tool model

**Package Executor (@tpmjs/package-executor):**
- VM2 sandbox with 5-second timeout
- Blocked dangerous modules (fs, net, http, https, child_process)
- LRU file system cache in /tmp/.tpmjs-cache
- Package installation and caching strategy

**AI Agent Service:**
- TPMJS parameter to Zod schema conversion
- AI SDK tool definition generation
- Token counting using tiktoken library
- GPT-4 Turbo pricing estimation
- Streaming text execution with callbacks

**API Endpoints:**
- POST /api/tools/[...slug]/execute - SSE streaming execution
- GET /api/tools/[...slug]/simulations - Execution history
- Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining)

**Frontend Components:**
- ToolPlayground - Main playground UI with tabs
- TokenBreakdown - Visual token metrics with colored bars
- Integrated above README section on tool detail pages

**Security:**
- VM2 sandboxing prevents filesystem/network access
- Rate limiting prevents abuse
- IP tracking for usage monitoring
- Timeout protection (60s max API duration, 5s VM timeout)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:36:05 +10:00
Ajax Davis
91f950d453 feat: add syntax highlighting and improve README readability
**Syntax Highlighting:**
- Add react-syntax-highlighter with Solarized Light theme
- Proper language detection from markdown code blocks
- Beautiful syntax highlighting for all code examples
- Improved inline code styling with subtle borders

**Enhanced Readability:**
- Larger base typography with prose-lg
- Better contrast for text colors (zinc-700/zinc-300)
- Improved heading spacing and hierarchy
- Enhanced table styling with hover effects and better spacing
- Table headers with uppercase, bold styling
- Table cells with generous padding (px-6 py-4/py-3)
- Row hover effects for better interaction
- Better blockquote styling with blue accents
- Improved list spacing with leading-relaxed
- Enhanced image borders and shadows

**Table Improvements:**
- Professional header styling with background colors
- Better cell padding and spacing
- Hover effects on rows
- Improved borders and shadows
- Responsive overflow handling

All code blocks now have beautiful Solarized Light syntax highlighting, and the overall typography is more readable and professional.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 23:21:40 +10:00
Ajax Davis
48c8775acf feat: improve README markdown styling to match npm.com quality
**Enhanced Markdown Rendering:**
- Use prose-slate for better default typography
- Add proper heading hierarchy with bottom borders on h1/h2
- Improve code block styling with better backgrounds and shadows
- Style inline code with pink/red accent colors like npm
- Better table styling with proper borders and rounded corners
- Improve link colors (blue) with hover effects
- Add better spacing throughout (margins, padding, line-height)
- Enhance blockquote styling with background colors
- Better list spacing with space-y-2
- Add proper light/dark mode support with zinc color palette

**Component Updates:**
- Custom pre component with better background and border
- Custom table wrapper with overflow handling
- Improved link component with external link detection
- Better inline code styling

The README now renders beautifully like npm.com instead of looking plain.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 23:08:00 +10:00
Ajax Davis
5feac0d2db fix: update API route to use catch-all pattern for clean URLs
- Rename API route from [slug] to [...slug] for catch-all routing
- Update API handler to join slug segments for scoped packages
- Remove encodeURIComponent from frontend API call

This fixes the 404 error when accessing tool pages with scoped package names.
The sync workers will need to run to populate README data for existing tools.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 22:57:54 +10:00
Ajax Davis
c52a044b47 fix: use catch-all route for clean URLs with scoped package names
- Rename [slug] to [...slug] for catch-all routing
- Update tool detail page to join slug segments (e.g., ['@tpmjs', 'text-transformer'] -> '@tpmjs/text-transformer')
- Remove encodeURIComponent from tool search links
- URLs now display as /tool/@tpmjs/text-transformer instead of /tool/%40tpmjs%2Ftext-transformer

This makes URLs cleaner and more readable while maintaining full compatibility with scoped npm package names.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 22:49:44 +10:00
Ajax Davis
f9ba1c094e feat: add README rendering and enhanced package metadata display
**Database Schema:**
- Add npmReadme, npmKeywords, npmAuthor, npmMaintainers fields to Tool model

**NPM Client:**
- Add fetchLatestPackageWithMetadata() function to fetch README and top-level metadata
- Export new PackageVersionWithReadme type

**Sync Workers:**
- Update keyword and changes sync to fetch and store README content
- Store author, maintainers, and keywords from package.json

**UI Components:**
- Create Markdown component using react-markdown with GitHub Flavored Markdown
- Add rehype-sanitize for security and remark-gfm for tables/strikethrough support

**Tool Detail Page:**
- Convert from createElement to JSX for better maintainability
- Display README in a dedicated card with proper markdown rendering
- Show NPM keywords, author, and maintainers in sidebar
- Add ThemeToggle to header
- Improve layout with better spacing and organization

This brings the tool detail pages much closer to NPM's package pages,
providing users with comprehensive information about each tool including
the full README documentation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 22:41:50 +10:00
Ajax Davis
e38e627a07 feat(web): add comprehensive /publish page with full publishing guide
- Create new /publish page with step-by-step instructions
- Show all 3 metadata tiers (Minimal, Basic, Rich) with examples
- Include quality score explanation and real-world examples
- Add category reference and tips for success
- Update homepage with Publish navigation link
- Add Publish Your Tool promotional section to homepage

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 22:19:43 +10:00
Ajax Davis
7f5d307c0f fix(api): remove conflicting [id] route causing build error
Next.js error: "You cannot use different slug names for the same dynamic path ('id' !== 'slug')"

Removed /api/tools/[id]/route.ts to resolve conflict with /api/tools/[slug]/route.ts
The [slug] route already handles fetching tools by package name

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 19:59:21 +10:00
506 changed files with 59267 additions and 5028 deletions

View file

@ -105,8 +105,8 @@ export default {
from: {},
to: {
couldNotResolve: true,
// Allow TypeScript path aliases that are resolved by the TS compiler
pathNot: ['^~/'],
// Allow TypeScript path aliases and workspace packages that are resolved by the TS compiler
pathNot: ['^~/', '^@/', '^@tpmjs/'],
},
},
{
@ -143,6 +143,10 @@ export default {
doNotFollow: {
path: ['node_modules', '\\.next', 'dist', '\\.turbo', 'storybook-static'],
},
exclude: {
// Exclude railway-executor - it's a Deno app with HTTP imports that can't be resolved
path: '^apps/railway-executor',
},
tsPreCompilationDeps: true,
tsConfig: {
fileName: './tsconfig.json',

41
.env.vercel.production Normal file
View file

@ -0,0 +1,41 @@
# Created by Vercel CLI
CRON_SECRET="CRON_SECRET=6c806d35cf6212f489c76414d38d2b6acbc44590ac78bb08aadea28dd04a29d0\n"
DATABASE_URL="postgresql://neondb_owner:npg_euvYo4OTi1lX@ep-broad-darkness-a4lml85k-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"
DATABASE_URL_UNPOOLED="postgresql://neondb_owner:npg_euvYo4OTi1lX@ep-broad-darkness-a4lml85k.us-east-1.aws.neon.tech/neondb?sslmode=require"
NEXT_PUBLIC_STACK_PROJECT_ID="d786bd3a-a31d-4c6b-9497-5d6803dd9d86"
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY="pck_hafmpkaj047z331x5azv8bk5zggfnbgdedbj9pfqh1rn0"
NX_DAEMON="false"
PGDATABASE="neondb"
PGHOST="ep-broad-darkness-a4lml85k-pooler.us-east-1.aws.neon.tech"
PGHOST_UNPOOLED="ep-broad-darkness-a4lml85k.us-east-1.aws.neon.tech"
PGPASSWORD="npg_euvYo4OTi1lX"
PGUSER="neondb_owner"
POSTGRES_DATABASE="neondb"
POSTGRES_HOST="ep-broad-darkness-a4lml85k-pooler.us-east-1.aws.neon.tech"
POSTGRES_PASSWORD="npg_euvYo4OTi1lX"
POSTGRES_PRISMA_URL="postgresql://neondb_owner:npg_euvYo4OTi1lX@ep-broad-darkness-a4lml85k-pooler.us-east-1.aws.neon.tech/neondb?connect_timeout=15&sslmode=require"
POSTGRES_URL="postgresql://neondb_owner:npg_euvYo4OTi1lX@ep-broad-darkness-a4lml85k-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"
POSTGRES_URL_NON_POOLING="postgresql://neondb_owner:npg_euvYo4OTi1lX@ep-broad-darkness-a4lml85k.us-east-1.aws.neon.tech/neondb?sslmode=require"
POSTGRES_URL_NO_SSL="postgresql://neondb_owner:npg_euvYo4OTi1lX@ep-broad-darkness-a4lml85k-pooler.us-east-1.aws.neon.tech/neondb"
POSTGRES_USER="neondb_owner"
STACK_SECRET_SERVER_KEY="ssk_p05kwe938wx13rpera9xf1fewc816dwkbq658xcsbwj1g"
TURBO_CACHE="remote:rw"
TURBO_DOWNLOAD_LOCAL_ENABLED="true"
TURBO_REMOTE_ONLY="true"
TURBO_RUN_SUMMARY="true"
VERCEL="1"
VERCEL_ENV="production"
VERCEL_GIT_COMMIT_AUTHOR_LOGIN=""
VERCEL_GIT_COMMIT_AUTHOR_NAME=""
VERCEL_GIT_COMMIT_MESSAGE=""
VERCEL_GIT_COMMIT_REF=""
VERCEL_GIT_COMMIT_SHA=""
VERCEL_GIT_PREVIOUS_SHA=""
VERCEL_GIT_PROVIDER=""
VERCEL_GIT_PULL_REQUEST_ID=""
VERCEL_GIT_REPO_ID=""
VERCEL_GIT_REPO_OWNER=""
VERCEL_GIT_REPO_SLUG=""
VERCEL_OIDC_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1yay00MzAyZWMxYjY3MGY0OGE5OGFkNjFkYWRlNGEyM2JlNyJ9.eyJpc3MiOiJodHRwczovL29pZGMudmVyY2VsLmNvbS90cG1qcyIsInN1YiI6Im93bmVyOnRwbWpzOnByb2plY3Q6dHBtanMtd2ViOmVudmlyb25tZW50OmRldmVsb3BtZW50Iiwic2NvcGUiOiJvd25lcjp0cG1qczpwcm9qZWN0OnRwbWpzLXdlYjplbnZpcm9ubWVudDpkZXZlbG9wbWVudCIsImF1ZCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90cG1qcyIsIm93bmVyIjoidHBtanMiLCJvd25lcl9pZCI6InRlYW1femtHV0NXYjdWakhvbmk2VmJ5ZmQyc3c4IiwicHJvamVjdCI6InRwbWpzLXdlYiIsInByb2plY3RfaWQiOiJwcmpfNWd1MEkwVzFjUFhkQ3ozd1RjQ0ZIejQzNUJ0MCIsImVudmlyb25tZW50IjoiZGV2ZWxvcG1lbnQiLCJwbGFuIjoicHJvIiwidXNlcl9pZCI6IkxKZk05VzdIdlljb2gyclVCaXRWd283ViIsIm5iZiI6MTc2NDM4OTAxMiwiaWF0IjoxNzY0Mzg5MDEyLCJleHAiOjE3NjQ0MzIyMTJ9.OF4IHrcmteA2lU1tkqHO1a9ITGGrCjCo29G8jI991q8_SQjgHZHVqcBj3AYVKZJDh6BjHib4HyNKdjO8nwUblF2dCFbYDv6y4hwB6jHNpsz32BE1JDKcXEJOKPtg_tBOFUDKtzMkPk7VOPWDVYw8Tz4_HZ_MR3SNoy1Pk9AFL-hEl3E-zR3bAYMDB8tKrIm9y9K4sZF6efMU7BR_J6Bf-i3IsbbrH-Axgq5dewlpogf-xHWmWaTXoUp6UFejNKhSMXqg3sAWTnizYeSGc2Ut6zNuAYPumUPBdQ37Kk7vuRNwS1h7RJz3vtEg6aOuw0-Ld0LdF-tWkfDGsVqanR_sxw"
VERCEL_TARGET_ENV="production"
VERCEL_URL=""

18
.github/workflows/health-check.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Daily Health Check
on:
schedule:
# Run daily at 2am UTC
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- name: Trigger health check sync
run: |
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/health-check" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-f -s -S -w "\nHTTP Status: %{http_code}\n"

View file

@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'

18
.github/workflows/sync-changes.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Sync NPM Changes Feed
on:
schedule:
# Run every 2 minutes
- cron: '*/2 * * * *'
workflow_dispatch:
jobs:
sync-changes:
runs-on: ubuntu-latest
steps:
- name: Trigger changes feed sync
run: |
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/changes" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-f -s -S -w "\nHTTP Status: %{http_code}\n"

131
.github/workflows/sync-keyword.yml vendored Normal file
View file

@ -0,0 +1,131 @@
name: Sync NPM Keyword Search
on:
schedule:
# Run every 15 minutes
- cron: '*/15 * * * *'
workflow_dispatch:
jobs:
sync-keyword:
runs-on: ubuntu-latest
steps:
- name: Trigger keyword search sync
id: sync
run: |
# Call the sync API and capture response
response=$(curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/keyword" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-f -s -S)
echo "Response: $response"
# Extract data using jq
processed=$(echo "$response" | jq -r '.data.processed')
skipped=$(echo "$response" | jq -r '.data.skipped')
errors=$(echo "$response" | jq -r '.data.errors')
packagesFound=$(echo "$response" | jq -r '.data.packagesFound')
durationMs=$(echo "$response" | jq -r '.data.durationMs')
# Extract and display error messages
errorMessages=$(echo "$response" | jq -r '.data.errorMessages[]?' 2>/dev/null || echo "")
if [ -n "$errorMessages" ]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "⚠️ SYNC ERRORS ($errors total):"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "$response" | jq -r '.data.errorMessages[]?' | while IFS= read -r error; do
echo " • $error"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
fi
# Set outputs for Discord notification
echo "processed=$processed" >> $GITHUB_OUTPUT
echo "skipped=$skipped" >> $GITHUB_OUTPUT
echo "errors=$errors" >> $GITHUB_OUTPUT
echo "packagesFound=$packagesFound" >> $GITHUB_OUTPUT
echo "durationMs=$durationMs" >> $GITHUB_OUTPUT
# Store error messages for Discord (first 3, truncated)
if [ "$errors" -gt 0 ]; then
errorSummary=$(echo "$response" | jq -r '.data.errorMessages[0:3]? | join("\n• ")' 2>/dev/null || echo "")
if [ -n "$errorSummary" ]; then
# Save to file to preserve newlines
echo "• $errorSummary" > /tmp/error_summary.txt
fi
fi
# Store skipped packages for Discord
if [ "$skipped" -gt 0 ]; then
skippedList=$(echo "$response" | jq -r '.data.skippedPackages[]? | "\(.name) (by \(.author)) - \(.reason)"' 2>/dev/null | paste -sd "\n" - || echo "")
if [ -n "$skippedList" ]; then
echo "$skippedList" > /tmp/skipped_packages.txt
fi
fi
# Determine status emoji
if [ "$errors" -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
- name: Send Discord notification
if: always()
run: |
# Format duration
duration_sec=$(echo "scale=2; ${{ steps.sync.outputs.durationMs }} / 1000" | bc)
# Build Discord payload using jq for proper JSON escaping
# Read optional data
error_text=""
skipped_text=""
if [ -f /tmp/error_summary.txt ] && [ ${{ steps.sync.outputs.errors }} -gt 0 ]; then
error_text=$(cat /tmp/error_summary.txt | head -c 800)
fi
if [ -f /tmp/skipped_packages.txt ] && [ ${{ steps.sync.outputs.skipped }} -gt 0 ]; then
skipped_text=$(cat /tmp/skipped_packages.txt)
fi
# Build fields array dynamically
base_fields='[
{ "name": "📦 Packages Found", "value": "${{ steps.sync.outputs.packagesFound }}", "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": "⏱️ Duration", "value": "'"${duration_sec}s"'", "inline": true },
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
]'
# Create payload with dynamic fields
payload=$(jq -n \
--arg title "${{ steps.sync.outputs.status_emoji }} NPM Keyword Search Sync" \
--argjson color ${{ steps.sync.outputs.status_color }} \
--argjson baseFields "$base_fields" \
--arg error_text "$error_text" \
--arg skipped_text "$skipped_text" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
'
{
embeds: [{
title: $title,
color: $color,
fields: (
$baseFields +
(if $skipped_text != "" then [{ name: "📋 Skipped Packages", value: $skipped_text, inline: false }] else [] end) +
(if $error_text != "" then [{ name: "🔍 Error Details", value: ("```\n" + $error_text + "\n```"), inline: false }] else [] end)
),
timestamp: $timestamp
}]
}')
# Send to Discord
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d "$payload"

97
.github/workflows/sync-manual.yml vendored Normal file
View 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"

18
.github/workflows/sync-metrics.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Sync NPM Metrics
on:
schedule:
# Run every hour
- cron: '0 * * * *'
workflow_dispatch:
jobs:
sync-metrics:
runs-on: ubuntu-latest
steps:
- name: Trigger metrics sync
run: |
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/metrics" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-f -s -S -w "\nHTTP Status: %{http_code}\n"

View file

@ -0,0 +1,267 @@
name: Sync Vercel AI Registry
on:
schedule:
# Run every hour
- cron: '0 * * * *'
workflow_dispatch:
# Run on pushes to main that modify the sync script
push:
branches:
- main
paths:
- 'sync-vercel-registry.ts'
permissions:
contents: write
jobs:
sync-vercel:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.14.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: |
echo "📦 Installing dependencies..."
pnpm install --frozen-lockfile
echo "✅ Dependencies installed"
- name: Run Vercel registry sync
id: sync
run: |
echo "════════════════════════════════════════"
echo "🚀 Starting Vercel AI Registry Sync"
echo "════════════════════════════════════════"
echo ""
echo "📅 Time: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "🔑 OpenAI API Key: ${OPENAI_API_KEY:0:8}..."
echo ""
# Run the sync script and capture output
output=$(pnpm tsx sync-vercel-registry.ts 2>&1)
exit_code=$?
echo "$output"
echo ""
# Extract statistics from output
processed=$(echo "$output" | grep "Processed:" | tail -1 | awk '{print $2}')
skipped=$(echo "$output" | grep "Skipped:" | tail -1 | awk '{print $2}')
errors=$(echo "$output" | grep "Errors:" | tail -1 | awk '{print $2}')
total=$(echo "$output" | grep "Total:" | tail -1 | awk '{print $2}')
# Set default values if extraction failed
processed=${processed:-0}
skipped=${skipped:-0}
errors=${errors:-0}
total=${total:-0}
echo "════════════════════════════════════════"
echo "📊 Sync Statistics"
echo "════════════════════════════════════════"
echo "✨ Processed: $processed"
echo "⏭️ Skipped: $skipped"
echo "❌ Errors: $errors"
echo "📦 Total: $total"
echo "════════════════════════════════════════"
echo ""
# Set outputs for later steps
echo "processed=$processed" >> $GITHUB_OUTPUT
echo "skipped=$skipped" >> $GITHUB_OUTPUT
echo "errors=$errors" >> $GITHUB_OUTPUT
echo "total=$total" >> $GITHUB_OUTPUT
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
# Check if manual-tools.ts was modified
if git diff --quiet manual-tools.ts; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo " No changes to manual-tools.ts"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✅ manual-tools.ts was modified"
echo ""
echo "📝 Changes preview:"
git diff --stat manual-tools.ts
echo ""
git diff manual-tools.ts | head -50
fi
# Determine status for notifications
if [ "$exit_code" -ne 0 ]; then
echo "status_emoji=❌" >> $GITHUB_OUTPUT
echo "status_color=15158332" >> $GITHUB_OUTPUT # Red
echo "status_text=Failed" >> $GITHUB_OUTPUT
elif [ "$errors" -gt 0 ]; then
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
echo "status_text=Completed with errors" >> $GITHUB_OUTPUT
else
echo "status_emoji=✅" >> $GITHUB_OUTPUT
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
echo "status_text=Success" >> $GITHUB_OUTPUT
fi
# Exit with the original exit code
exit $exit_code
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Commit and push changes
if: steps.sync.outputs.has_changes == 'true'
run: |
echo "════════════════════════════════════════"
echo "📝 Committing changes to manual-tools.ts"
echo "════════════════════════════════════════"
echo ""
# Configure git
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
# Show what's being committed
echo "📋 Files to commit:"
git status --short
echo ""
# Commit changes
git add manual-tools.ts
# Create commit message
COMMIT_MSG="chore: sync ${{ steps.sync.outputs.processed }} new tools from Vercel AI registry
Added ${{ steps.sync.outputs.processed }} tools from Vercel AI SDK registry:
- Total tools in registry: ${{ steps.sync.outputs.total }}
- Already synced: ${{ steps.sync.outputs.skipped }}
- Newly added: ${{ steps.sync.outputs.processed }}
- Errors: ${{ steps.sync.outputs.errors }}
🤖 Automated by GitHub Actions
Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git commit -m "$COMMIT_MSG"
echo "✅ Changes committed"
echo ""
# Push changes
echo "📤 Pushing to remote..."
git push
echo "✅ Changes pushed successfully"
echo "════════════════════════════════════════"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Send Discord notification
if: always()
run: |
echo "════════════════════════════════════════"
echo "📢 Sending Discord notification"
echo "════════════════════════════════════════"
# Build fields array
base_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": "📝 Changes", "value": "${{ steps.sync.outputs.has_changes }}", "inline": true },
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
]'
# Add commit info if changes were made
if [ "${{ steps.sync.outputs.has_changes }}" = "true" ]; then
commit_sha=$(git rev-parse HEAD)
commit_url="https://github.com/${{ github.repository }}/commit/${commit_sha}"
additional_fields='[
{ "name": "💾 Commit", "value": "['"${commit_sha:0:7}"']('"$commit_url"')", "inline": false }
]'
# Merge fields
all_fields=$(jq -n --argjson base "$base_fields" --argjson additional "$additional_fields" '$base + $additional')
else
all_fields="$base_fields"
fi
# Create Discord embed
payload=$(jq -n \
--arg title "${{ steps.sync.outputs.status_emoji }} Vercel AI Registry Sync - ${{ steps.sync.outputs.status_text }}" \
--argjson color ${{ steps.sync.outputs.status_color }} \
--argjson fields "$all_fields" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
--arg description "Synced Vercel AI SDK tools registry with TPMJS manual tools" \
'
{
embeds: [{
title: $title,
description: $description,
color: $color,
fields: $fields,
timestamp: $timestamp,
footer: {
text: "Vercel AI Registry Sync"
}
}]
}')
echo "📤 Sending payload to Discord..."
# Send to Discord
response=$(curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d "$payload" \
-w "\nHTTP Status: %{http_code}\n" \
-s)
echo "$response"
if echo "$response" | grep -q "HTTP Status: 2"; then
echo "✅ Discord notification sent successfully"
else
echo "⚠️ Discord notification may have failed"
fi
echo "════════════════════════════════════════"
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Summary
if: always()
run: |
echo ""
echo "════════════════════════════════════════"
echo "📊 Workflow Summary"
echo "════════════════════════════════════════"
echo ""
echo "Status: ${{ steps.sync.outputs.status_text }}"
echo "Tools Processed: ${{ steps.sync.outputs.processed }}"
echo "Tools Skipped: ${{ steps.sync.outputs.skipped }}"
echo "Errors: ${{ steps.sync.outputs.errors }}"
echo "Total in Registry: ${{ steps.sync.outputs.total }}"
echo "Changes Made: ${{ steps.sync.outputs.has_changes }}"
echo ""
if [ "${{ steps.sync.outputs.has_changes }}" = "true" ]; then
echo "✅ New tools added to manual-tools.ts and committed"
else
echo " No new tools found - manual-tools.ts is up to date"
fi
echo ""
echo "════════════════════════════════════════"

7
.vercelignore Normal file
View file

@ -0,0 +1,7 @@
node_modules
.turbo
.next
dist
*.log
.env*
!.env.example

View file

@ -1,275 +0,0 @@
# 2025 Best Practices for TPMJS Monorepo
This document outlines recommendations to make TPMJS a cutting-edge 2025 monorepo optimized for both human and agentic development (Claude Code, Cursor, etc.).
## High-Impact Additions
### 1. Agent-First Documentation
```
packages/docs/
├── architecture-decisions/ # ADRs in markdown
├── patterns/ # Common patterns with examples
├── schemas/ # JSON schemas for all data structures
└── examples/ # Working code examples per feature
```
**Why:** Claude Code and other agents work better with:
- Explicit decision documentation (ADRs)
- Pattern libraries showing "the right way"
- Machine-readable schemas
- Real working examples to reference
### 2. Automated Testing Pyramid
```bash
# Add to package.json scripts
"test:unit": "vitest" # ✅ Already have this
"test:integration": "vitest -c vitest.integration.config.ts" # Add
"test:e2e": "playwright test" # Add
"test:visual": "playwright test --grep @visual" # Add
"test:contracts": "pactum" # Add for API testing
```
**Packages to add:**
- `@playwright/test` - E2E testing
- `@playwright/experimental-ct-react` - Component testing
- `pactum` or `msw` integration tests (you have mocks setup)
- `chromatic` or `percy` - Visual regression
### 3. Type Coverage & Quality Gates
```json
// Add to root package.json
{
"scripts": {
"type-check": "tsc --noEmit",
"type-coverage": "type-coverage --at-least 95",
"find-deadcode": "knip",
"check-architecture": "depcruiser --validate"
}
}
```
**Add packages:**
- `type-coverage` - Ensure no implicit `any`
- `knip` - Find unused files/exports/dependencies
- `dependency-cruiser` - Enforce architecture rules
- `@total-typescript/ts-reset` - Better built-in types
### 4. Development Containers
```json
// .devcontainer/devcontainer.json
{
"name": "TPMJS Dev",
"dockerComposeFile": "docker-compose.yml",
"service": "dev",
"features": {
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
},
"customizations": {
"vscode": {
"extensions": [
"biomejs.biome",
"bradlc.vscode-tailwindcss",
"lokalise.i18n-ally"
]
}
}
}
```
**Why:** Agents like Claude Code work better when environment is reproducible. This also helps human developers.
### 5. Code Generation & Scaffolding
```typescript
// packages/cli/ - Internal dev tool
import { scaffold } from '@tpmjs/cli';
// Commands:
pnpm gen:component ButtonGroup
pnpm gen:package @tpmjs/new-package
pnpm gen:app marketing-site
```
**Create:**
- `plop` or `hygen` templates
- Component scaffolding (with tests, stories, exports)
- Package scaffolding (with tsconfig, package.json, exports)
- Consistent file structure generation
**Why:** Agents can use these commands to create new code following your exact patterns.
### 6. Enhanced Strict Mode TypeScript
```json
// packages/tsconfig/base.json - Add these
{
"compilerOptions": {
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"noImplicitOverride": true
}
}
```
### 7. Bundle Analysis & Performance
```json
{
"scripts": {
"analyze": "turbo run build --filter=@tpmjs/web -- --analyze",
"lighthouse": "lhci autorun",
"bundle-size": "size-limit"
}
}
```
**Add:**
- `@next/bundle-analyzer`
- `@lhci/cli` - Lighthouse CI
- `size-limit` - Bundle size tracking in CI
### 8. Smart Dependency Management
```json
// .github/renovate.json
{
"extends": ["config:base"],
"packageRules": [
{
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch"
}
]
}
```
**Use:** Renovate or Dependabot with auto-merge for passing tests
### 9. API Documentation Generation
```bash
pnpm add -D -w typedoc typedoc-plugin-markdown
```
Auto-generate API docs from TSDoc comments that both humans and agents can read.
### 10. Schema-First Development
```typescript
// packages/schemas/ - Central schema definitions
export * from './tool-schema';
export * from './registry-api-schema';
export * from './event-schema';
// Use Zod for runtime + type generation
// Agents can read schemas to understand contracts
```
## Monorepo-Specific Improvements
### 11. Better Local Development
```typescript
// turbo.json
{
"pipeline": {
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
}
}
}
```
### 12. Workspace Protocols & Constraints
```yaml
# .pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
# Add constraints
pnpm-workspace-constraints:
dependencies:
'@tpmjs/ui': 'workspace:*'
'@tpmjs/utils': 'workspace:*'
```
## Recommended Final Structure
```
.
├── .devcontainer/ # Dev containers config
├── .github/
│ ├── workflows/ # CI/CD
│ └── renovate.json # Dependency automation
├── apps/
│ └── web/
├── packages/
│ ├── cli/ # ⭐ NEW: Dev tooling
│ ├── schemas/ # ⭐ NEW: Central schemas
│ └── ...existing
├── docs/
│ ├── adr/ # ⭐ NEW: Architecture decisions
│ ├── patterns/ # ⭐ NEW: Code patterns
│ └── examples/ # ⭐ NEW: Working examples
├── scripts/
│ ├── scaffold.ts # ⭐ NEW: Code generation
│ └── validate-deps.ts # ⭐ NEW: Architecture validation
├── playwright.config.ts # ⭐ NEW: E2E testing
├── .lighthouserc.json # ⭐ NEW: Performance
└── knip.json # ⭐ NEW: Dead code detection
```
## Priority Order for Implementation
### Phase 1 (Foundation)
1. **Knip + type-coverage** - Catch issues early
2. **Code generation scripts** - Ensure consistency
3. **ADR documentation structure** - Decision tracking
### Phase 2 (Quality)
4. **E2E testing with Playwright** - Full user flow coverage
5. **Bundle analysis + performance budgets** - Keep app fast
6. **Stricter TypeScript settings** - Catch more bugs at compile time
### Phase 3 (DX)
7. **Dev containers** - Reproducible environments
8. **API documentation generation** - Auto-generated from code
9. **Renovate automation** - Keep dependencies fresh
## Benefits for Agent-Driven Development
1. **Explicit Patterns** - Agents can reference documented patterns instead of guessing
2. **Code Generation** - Consistent scaffolding commands agents can use
3. **Machine-Readable Schemas** - JSON schemas help agents understand data structures
4. **Quality Gates** - Automated checks catch agent mistakes early
5. **Working Examples** - Agents can copy-paste-adapt proven patterns
6. **Architecture Enforcement** - Dependency rules prevent agents from creating invalid imports
## Next Steps
Start with the highest ROI items:
1. Install Knip to find dead code
2. Set up code generation for components/packages
3. Create docs/patterns/ with common examples
4. Add stricter TypeScript compiler options
5. Set up Playwright for E2E testing
These changes will make the codebase more maintainable and significantly improve the experience of working with AI coding agents.

View file

@ -1,932 +0,0 @@
# API Routes Timeout Issue - Complete Investigation Report
## Problem Statement
API routes deployed to Vercel are timing out with no response. The Next.js application pages work perfectly, but all API endpoints at `/api/*` return timeouts or "Redirecting..." messages.
**Affected URLs:**
- `https://tpmjs.com/api/health` - Returns "Redirecting..."
- `https://tpmjs.com/api/tools` - Returns "Redirecting..."
- `https://tpmjs-1chh44d1u-tpmjs.vercel.app/api/health` - Timeouts (exit code 28)
- `https://tpmjs-1chh44d1u-tpmjs.vercel.app/api/tools` - Timeouts (exit code 28)
**Working:**
- All page routes work correctly (e.g., `/`, `/tool/[slug]`)
- UI navigation and client-side routing function normally
- Local development API routes work perfectly
## Environment Details
### Project Structure
- **Monorepo:** Turborepo setup with pnpm workspaces
- **Framework:** Next.js 16.0.4 (App Router)
- **Node Version:** 24.x (on Vercel)
- **Deployment Platform:** Vercel
- **Custom Domains:** tpmjs.com, www.tpmjs.com
### Repository Structure
```
tpmjs/
├── apps/
│ └── web/ # Next.js 16 App Router application
│ ├── src/
│ │ └── app/
│ │ ├── api/
│ │ │ ├── health/route.ts
│ │ │ ├── stats/route.ts
│ │ │ ├── tools/
│ │ │ │ ├── route.ts
│ │ │ │ ├── [id]/route.ts
│ │ │ │ ├── [slug]/route.ts
│ │ │ │ └── validate/route.ts
│ │ │ └── sync/
│ │ │ ├── changes/route.ts
│ │ │ ├── keyword/route.ts
│ │ │ └── metrics/route.ts
│ │ ├── page.tsx
│ │ └── tool/[slug]/page.tsx
│ ├── next.config.ts
│ └── vercel.json
├── packages/
│ ├── db/ # Prisma client
│ ├── types/ # Shared TypeScript types
│ ├── utils/ # Utility functions
│ ├── env/ # Environment validation
│ └── ui/ # React component library
├── vercel.json # Root Vercel configuration
└── turbo.json
```
## API Route Examples
### `/apps/web/src/app/api/health/route.ts`
```typescript
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
/**
* GET /api/health
* Simple health check endpoint that doesn't touch the database
*/
export async function GET() {
return NextResponse.json({
status: 'ok',
timestamp: new Date().toISOString(),
env: {
hasDatabase: !!process.env.DATABASE_URL,
nodeEnv: process.env.NODE_ENV,
},
});
}
```
### `/apps/web/src/app/api/tools/route.ts`
```typescript
import { NextResponse } from 'next/server';
import { prisma } from '@tpmjs/db/client';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function GET(request: Request) {
// ... query string parsing
const [tools, totalCount] = await Promise.all([
prisma.tool.findMany({
where,
orderBy: [
{ qualityScore: 'desc' },
{ npmDownloadsLastMonth: 'desc' },
{ createdAt: 'desc' },
],
take: limit,
skip: offset,
}),
prisma.tool.count({ where }),
]);
return NextResponse.json({
data: tools,
pagination: {
page,
limit,
total: totalCount,
totalPages: Math.ceil(totalCount / limit),
},
});
}
```
## Configuration Files
### `/apps/web/next.config.ts` (Current)
```typescript
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils', '@tpmjs/db', '@tpmjs/types', '@tpmjs/env'],
reactStrictMode: true,
};
export default nextConfig;
```
### `/apps/web/vercel.json` (Current)
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "cd ../.. && pnpm --filter=@tpmjs/web build",
"installCommand": "pnpm install"
}
```
### `/vercel.json` (Root)
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": {
"deploymentEnabled": {
"main": true
}
},
"github": {
"silent": false,
"autoJobCancelation": true
},
"crons": [
{
"path": "/api/sync/changes",
"schedule": "*/2 * * * *"
},
{
"path": "/api/sync/keyword",
"schedule": "*/15 * * * *"
},
{
"path": "/api/sync/metrics",
"schedule": "0 * * * *"
}
]
}
```
## Local Build Verification
### Local Build Output Structure
```bash
$ ls -R /Users/ajaxdavis/repos/tpmjs/tpmjs/apps/web/.next/server/app/api/
health/
stats/
sync/
tools/
/apps/web/.next/server/app/api/health:
route
route.js
route.js.map
route.js.nft.json
route_client-reference-manifest.js
/apps/web/.next/server/app/api/stats:
route
route.js
route.js.map
route.js.nft.json
route_client-reference-manifest.js
/apps/web/.next/server/app/api/tools:
[id]/
[slug]/
validate/
route
route.js
route.js.map
route.js.nft.json
route_client-reference-manifest.js
```
### Routes Manifest Confirmation
```bash
$ cat /apps/web/.next/routes-manifest.json | jq '.staticRoutes[] | select(.page | contains("api"))'
{
"page": "/api/health",
"regex": "^/api/health(?:/)?$",
"routeKeys": {},
"namedRegex": "^/api/health(?:/)?$"
}
{
"page": "/api/stats",
"regex": "^/api/stats(?:/)?$",
"routeKeys": {},
"namedRegex": "^/api/stats(?:/)?$"
}
{
"page": "/api/sync/changes",
"regex": "^/api/sync/changes(?:/)?$",
"routeKeys": {},
"namedRegex": "^/api/sync/changes(?:/)?$"
}
{
"page": "/api/sync/keyword",
"regex": "^/api/sync/keyword(?:/)?$",
"routeKeys": {},
"namedRegex": "^/api/sync/keyword(?:/)?$"
}
{
"page": "/api/sync/metrics",
"regex": "^/api/sync/metrics(?:/)?$",
"routeKeys": {},
"namedRegex": "^/api/sync/metrics(?:/)?$"
}
```
### Node File Trace (NFT) Verification
```bash
$ cat /apps/web/.next/server/app/api/health/route.js.nft.json
{
"version": 1,
"files": [
"../../../../../../../node_modules/.pnpm/next@16.0.4_@babel+core@7.28.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/next/dist/client/components/app-router-headers.js",
"../../../../../../../node_modules/.pnpm/next@16.0.4_@babel+core@7.28.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/next/dist/compiled/@opentelemetry/api/index.js",
// ... many more dependencies
]
}
```
**Conclusion:** API routes build correctly locally with all dependencies properly traced.
## Vercel Deployment Analysis
### Deployment Inspection Output
```bash
$ vercel inspect https://tpmjs-1chh44d1u-tpmjs.vercel.app
General
id dpl_2FHyiTWtBZzvT8EcohwYEZmdb8rb
name tpmjs-web
target production
status ● Ready
url https://tpmjs-1chh44d1u-tpmjs.vercel.app
created Fri Nov 28 2025 20:44:03 GMT+1000
Aliases
╶ https://www.tpmjs.com
╶ https://tpmjs-web.vercel.app
╶ https://tpmjs-web-tpmjs.vercel.app
╶ https://tpmjs-web-git-main-tpmjs.vercel.app
╶ https://tpmjs.com
Builds
┌ . [0ms]
├── λ tool/[slug] (562.92KB) [iad1]
├── λ tool/[slug].rsc (562.92KB) [iad1]
├── λ _global-error (642.48KB) [iad1]
├── λ _global-error.rsc (642.48KB) [iad1]
├── λ _global-error.segments/__PAGE__.segment.rsc (642.48KB) [iad1]
└── 56 output items hidden
```
**CRITICAL FINDING:** No API routes are listed in the build output. Only pages (`tool/[slug]`, `_global-error`, etc.) appear as serverless functions (`λ`).
Expected API routes that should appear:
- `λ api/health`
- `λ api/tools`
- `λ api/tools/[id]`
- `λ api/tools/[slug]`
- `λ api/sync/changes`
- etc.
### Testing Results
```bash
# Direct Vercel URL - Timeouts
$ curl -s -m 10 https://tpmjs-1chh44d1u-tpmjs.vercel.app/api/health
# Exit code 28 (timeout)
# Custom Domain - Returns "Redirecting..."
$ curl -s -m 10 https://tpmjs.com/api/health
Redirecting...
# Custom Domain - Returns "Redirecting..."
$ curl -s -m 10 https://tpmjs.com/api/tools
Redirecting...
# Check redirect headers
$ curl -I https://tpmjs.com
HTTP/2 307
cache-control: public, max-age=0, must-revalidate
content-type: text/plain
date: Fri, 28 Nov 2025 10:38:12 GMT
location: https://www.tpmjs.com/
server: Vercel
```
## Vercel Configuration Details
### User-Confirmed Settings
- **Root Directory:** `apps/web` (set in Vercel dashboard)
- **DATABASE_URL:** Configured in Vercel environment variables (Production)
- **Framework Preset:** (Unknown - needs verification)
- **Build Output Directory:** (Unknown - using default `.next`)
### Project List
```bash
$ vercel project ls | grep -i tpmjs
tpmjs -- 16h 24.x
v0-tool-registry-page https://tpmjs.com 3d 22.x
```
**NOTE:** Two projects exist:
1. `tpmjs` - Current project (Node 24.x)
2. `v0-tool-registry-page` - Also has tpmjs.com domain (Node 22.x)
This could indicate a domain routing conflict or outdated project.
## Investigation Timeline & Attempts
### Attempt 1: Remove Root-Level Redirects
**Hypothesis:** The redirect in `/vercel.json` was intercepting API requests.
**Original `/vercel.json`:**
```json
{
"redirects": [
{
"source": "/:path*",
"has": [
{
"type": "host",
"value": "www.tpmjs.com"
}
],
"destination": "https://tpmjs.com/:path*",
"permanent": true
}
]
}
```
**Action:** Removed the `redirects` array from root `/vercel.json`.
**Result:** ❌ API routes still timeout. Redirect rule was not the root cause.
**Commit:** `9be3af7 fix(routing): move www redirect from vercel.json to Next.js config`
### Attempt 2: Move Redirects to Next.js Config
**Hypothesis:** Next.js should handle redirects after routing.
**Action:** Added `async redirects()` to `apps/web/next.config.ts`:
```typescript
async redirects() {
return [
{
source: '/:path*',
has: [
{
type: 'host',
value: 'www.tpmjs.com',
},
],
destination: 'https://tpmjs.com/:path*',
permanent: true,
},
];
}
```
**Result:** ❌ API routes returned "Redirecting..." instead of executing. Next.js `async redirects()` applies to ALL routes including API routes.
**Commit:** `9be3af7 fix(routing): move www redirect from vercel.json to Next.js config`
### Attempt 3: Exclude API Routes from Redirect
**Hypothesis:** Use regex to exclude `/api/*` from redirects.
**Action:** Modified redirect pattern:
```typescript
async redirects() {
return [
{
source: '/((?!api).*)', // Negative lookahead to exclude /api/*
has: [
{
type: 'host',
value: 'www.tpmjs.com',
},
],
destination: 'https://tpmjs.com/$1',
permanent: true,
},
];
}
```
**Result:** ❌ API routes back to timing out (not redirecting anymore, but still not working).
**Commit:** `c42bfb6 fix(redirects): exclude API routes from www redirect`
### Attempt 4: Remove All Redirects
**Hypothesis:** Eliminate redirect loop causing ERR_TOO_MANY_REDIRECTS.
**Action:** Removed `async redirects()` entirely from `next.config.ts`.
**Result:** ✅ Redirect loop fixed. ❌ API routes still timeout.
**Commit:** `b93cd42 fix: remove redirects to resolve redirect loop`
### Attempt 5: Add Vercel Functions Configuration
**Hypothesis:** Vercel needs explicit configuration to detect API routes.
**Action:** Added to `apps/web/vercel.json`:
```json
{
"functions": {
"app/api/**/*.ts": {
"maxDuration": 60
}
}
}
```
**Result:** ❌ No change. API routes still timeout.
**Commit:** `748ca4d fix(api): configure Vercel functions for API routes with maxDuration`
### Attempt 6: Add maxDuration to Route Files
**Hypothesis:** Export configuration directly in route handlers.
**Action:** Added to `apps/web/src/app/api/health/route.ts`:
```typescript
export const maxDuration = 60;
```
**Result:** ❌ No change. API routes still timeout.
**Commit:** `8281f8f fix(build): disable Turbopack for Vercel deployment`
### Attempt 7: Disable Turbopack
**Hypothesis:** Turbopack (Next.js 16 default) has compatibility issues with Vercel.
**Action:** Added `--webpack` flag to build command:
```json
{
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web -- --webpack"
}
```
**Result:** ❌ Build failed completely. Invalid flag syntax.
**Commit:** `8281f8f fix(build): disable Turbopack for Vercel deployment`
### Attempt 8: Simplify Build Command
**Hypothesis:** Use direct pnpm build instead of Turbo wrapper.
**Action:** Changed to:
```json
{
"buildCommand": "cd ../.. && pnpm --filter=@tpmjs/web build"
}
```
**Result:** ⏳ Pending deployment test.
**Commit:** `065196d fix(build): simplify Vercel build command`
## Root Cause Analysis
### What We Know FOR SURE
1. ✅ **API routes build correctly locally**
- All 9 API routes compile to `.next/server/app/api/`
- NFT (Node File Trace) files are generated with proper dependencies
- Routes manifest includes all API routes
2. ✅ **Next.js configuration is correct**
- `export const runtime = 'nodejs'` set correctly
- `export const dynamic = 'force-dynamic'` set correctly
- `transpilePackages` includes all workspace packages
3. ✅ **Pages deploy and work perfectly**
- `/tool/[slug]` renders correctly
- Homepage loads
- Client-side navigation works
4. ❌ **API routes are NOT deployed as serverless functions**
- `vercel inspect` shows NO API routes in build output
- Only pages appear as `λ` (lambda) functions
- This is the PRIMARY issue
5. ❌ **Direct Vercel URLs timeout**
- Not just a custom domain issue
- Affects `*.vercel.app` URLs
- Exit code 28 (timeout) - no response at all
6. ❌ **Custom domain shows "Redirecting..."**
- Even with all redirects removed from config
- Suggests a redirect at Vercel platform level OR DNS level
- Could be from the `v0-tool-registry-page` project conflict
### Possible Root Causes
#### Theory 1: Vercel Project Misconfiguration
**Likelihood:** HIGH
**Evidence:**
- Two projects with same domain (`tpmjs` and `v0-tool-registry-page`)
- Framework Preset might not be set to "Next.js"
- Root Directory is `apps/web` but Vercel might not be detecting Next.js properly
**What to Check:**
1. Vercel Dashboard → Project Settings → General
- Framework Preset: Should be "Next.js"
- Root Directory: Should be "apps/web"
- Build Command: Should match vercel.json
- Output Directory: Should be blank (default `.next`)
2. Vercel Dashboard → Domains
- Check if both projects have tpmjs.com
- Remove domain from `v0-tool-registry-page` if present
3. Vercel Dashboard → Deployments → Build Logs
- Search for "API" or "route"
- Look for errors about missing functions
- Check if Next.js is detected correctly
#### Theory 2: Monorepo Detection Issue
**Likelihood:** MEDIUM
**Evidence:**
- Build command uses `cd ../.. && pnpm --filter=@tpmjs/web build`
- Vercel might not be correctly detecting workspace structure
- `transpilePackages` includes workspace packages
**What to Check:**
1. Build logs for workspace resolution errors
2. Check if `node_modules` is being created in correct location
3. Verify pnpm workspace configuration
**Potential Fix:**
Try setting `installCommand` to:
```json
{
"installCommand": "pnpm install --shamefully-hoist"
}
```
#### Theory 3: Next.js 16 + Vercel Incompatibility
**Likelihood:** MEDIUM
**Evidence:**
- Next.js 16 released recently (November 2024)
- Turbopack is default (might have Vercel issues)
- App Router API routes behave differently than Pages Router
**What to Check:**
1. Vercel build logs for Next.js version detection
2. Any warnings about incompatible features
3. Check Vercel's Next.js 16 support status
**Potential Fix:**
Downgrade to Next.js 15.x temporarily to test:
```json
{
"dependencies": {
"next": "^15.0.0"
}
}
```
#### Theory 4: Environment Variable Issue
**Likelihood:** LOW
**Evidence:**
- DATABASE_URL is configured
- Pages work (they might not need env vars)
- API routes use Prisma (requires DATABASE_URL)
**What to Check:**
1. Vercel Dashboard → Settings → Environment Variables
- Verify DATABASE_URL is set for Production
- Verify it's not blocked or empty
- Check if other vars are needed
2. Build logs for Prisma generation errors
**Potential Fix:**
None - user confirmed DATABASE_URL is set.
#### Theory 5: Build Output Issue
**Likelihood:** MEDIUM-HIGH
**Evidence:**
- `vercel inspect` doesn't show API routes
- Only pages are listed as functions
- Build completes successfully (38-41 seconds)
**What to Check:**
1. Build logs: Does Next.js report building API routes?
- Look for "Route (app)" or "λ" indicators for API routes
- Compare to local build output
2. Check if Vercel is using correct build output structure
- App Router uses `.next/server/app/`
- Pages Router uses `.next/server/pages/`
**Potential Fix:**
Try forcing Vercel to recognize the build:
```json
{
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
]
}
```
(Note: `builds` is legacy, modern Next.js should auto-detect)
## Recommended Next Steps
### Immediate Actions (High Priority)
1. **Check Vercel Project Settings**
- Go to Vercel Dashboard → tpmjs-web project
- Verify Framework Preset is "Next.js"
- Verify Root Directory is `apps/web`
- Screenshot settings for reference
2. **Review Build Logs**
- Go to latest deployment
- Download complete build logs
- Search for:
- "Route (app)" - should show API routes
- "λ" - should show API functions
- "api" - any mentions
- Errors or warnings
3. **Check Domain Configuration**
- Verify only ONE project has tpmjs.com domain
- Remove domain from `v0-tool-registry-page` project if present
- Check DNS settings aren't redirecting
4. **Test Simple API Route**
- Create minimal API route:
```typescript
// apps/web/src/app/api/test/route.ts
export async function GET() {
return new Response('Hello from API', { status: 200 });
}
```
- Deploy and test
- If this doesn't work, confirms platform issue
### Investigation Actions (Medium Priority)
5. **Compare Working vs Non-Working**
- Find a deployment where pages DO work
- Compare build output between page routes and API routes
- Look for differences in how they're compiled
6. **Test Vercel CLI Deploy**
- Deploy directly via CLI: `vercel --prod`
- Check if behavior differs from Git-based deploy
- Might reveal configuration issues
7. **Check Vercel Function Logs**
- Even though functions aren't in build output, try:
- `vercel logs <deployment-url> --since 1h`
- Look for any API route invocations or errors
8. **Review Turbo Configuration**
```bash
# Check turbo.json for Next.js build config
cat /turbo.json
# Verify build runs correctly locally
pnpm --filter=@tpmjs/web build
```
### Alternative Approaches (If Above Fails)
9. **Create New Vercel Project**
- Import from Git fresh
- Use identical settings
- Test if fresh project works
10. **Contact Vercel Support**
- This may be a platform bug with Next.js 16
- Provide this document as context
- Ask specifically why API routes aren't in build output
11. **Temporary Workaround**
- Deploy API routes separately (different service)
- Use Vercel proxy to route `/api/*` to separate deployment
- Not ideal but unblocks development
## Environment Variables Needed
```bash
# Required for API routes
DATABASE_URL="postgresql://..."
# Optional (check if needed)
NODE_ENV="production"
NEXT_PUBLIC_* # Any public env vars
```
## Build Commands Reference
### Local Development
```bash
# Install dependencies
pnpm install
# Generate Prisma client
pnpm --filter=@tpmjs/db db:generate
# Run development server
pnpm --filter=@tpmjs/web dev
# Build for production
pnpm --filter=@tpmjs/web build
# Test build locally
pnpm --filter=@tpmjs/web start
```
### Vercel Configuration
**Current:**
```json
{
"buildCommand": "cd ../.. && pnpm --filter=@tpmjs/web build",
"installCommand": "pnpm install"
}
```
**Alternative to try:**
```json
{
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web",
"installCommand": "pnpm install",
"framework": "nextjs"
}
```
## Key Files to Review
1. `/apps/web/next.config.ts` - Next.js configuration
2. `/apps/web/vercel.json` - Vercel app-level config
3. `/vercel.json` - Vercel root config
4. `/turbo.json` - Turborepo configuration
5. `/apps/web/.next/routes-manifest.json` - Route definitions
6. `/apps/web/.next/build-manifest.json` - Build output
7. Vercel build logs (from dashboard)
## Questions for Vercel Support
If escalating to Vercel support, ask:
1. Why are API routes not appearing in the build output (`vercel inspect`) when pages are deploying correctly?
2. Is there a known issue with Next.js 16 App Router API routes in Turborepo monorepos?
3. What's the correct way to configure `vercel.json` for a Next.js 16 app in a monorepo with custom build commands?
4. Could having two projects (`tpmjs` and `v0-tool-registry-page`) with the same domain cause routing issues?
5. Are there any specific requirements for deploying Next.js 16 API routes that differ from Next.js 15?
## Related Documentation
- [Next.js 16 Upgrade Guide](https://nextjs.org/docs/app/guides/upgrading/version-16)
- [Vercel Next.js Deployment](https://vercel.com/docs/frameworks/nextjs)
- [Vercel Functions Configuration](https://vercel.com/docs/functions/configuring-functions)
- [Turborepo with Vercel](https://vercel.com/docs/monorepos/turborepo)
- [Next.js App Router API Routes](https://nextjs.org/docs/app/api-reference/file-conventions/route)
## Recent Commits Related to This Issue
```
065196d fix(build): simplify Vercel build command
8281f8f fix(build): disable Turbopack for Vercel deployment
748ca4d fix(api): configure Vercel functions for API routes with maxDuration
b93cd42 fix: remove redirects to resolve redirect loop
c42bfb6 fix(redirects): exclude API routes from www redirect
9be3af7 fix(routing): move www redirect from vercel.json to Next.js config
a92dfff fix(build): add workspace packages to Next.js transpilePackages
cc6c824 fix(vercel): configure Turborepo monorepo build for apps/web
```
## ✅ CONCLUSION - ROOT CAUSE IDENTIFIED
### The Real Problem
**Vercel is NOT detecting this project as a Next.js application.**
When Vercel doesn't detect Next.js, it:
- Uses `@vercel/static-builder` instead of `@vercel/next`
- Treats the deployment as a static site
- Deploys pages (static HTML) successfully
- **Completely drops all App Router API routes**
- Never generates serverless functions for `/api/*` routes
This explains EVERY symptom:
- ✅ Pages work (they're static files)
- ❌ API routes timeout (they were never deployed)
- ❌ No `λ api/*` in build output (functions don't exist)
- ❌ Direct Vercel URLs timeout (not a DNS issue)
- ❌ "Redirecting..." on custom domain (wrong project owns the domain)
### Why Vercel Doesn't Detect Next.js
**1. Wrong Root Directory**
- Vercel project likely has Root Directory set to `.` or empty
- Should be exactly: `apps/web`
- A single character difference breaks Next.js detection
**2. Wrong Framework Preset**
- When Vercel can't auto-detect Next.js (due to monorepo + wrong root)
- It defaults to Framework Preset = "Other"
- "Other" uses static builder, not Next.js builder
**3. Domain Conflict**
- Two projects exist: `tpmjs` and `v0-tool-registry-page`
- Both have `tpmjs.com` domain assigned
- Requests randomly route to wrong project
- "Redirecting..." comes from the old project, not your code
**4. Custom Build Commands**
- Custom build commands can bypass Vercel's auto-detection
- Should let Vercel auto-detect and use default commands
### Evidence
**Local build works:**
```bash
$ ls apps/web/.next/server/app/api/
health/ stats/ sync/ tools/
# All API routes compile correctly
```
**Vercel deployment missing API routes:**
```bash
$ vercel inspect https://tpmjs-1chh44d1u-tpmjs.vercel.app
Builds
├── λ tool/[slug] # Pages present
├── λ _global-error # Pages present
└── 56 output items hidden
# NO api/health, NO api/tools, NO api/* at all
```
This is **not a Next.js bug, not a Turborepo bug, not a code issue**.
This is **pure Vercel project misconfiguration**.
### The Fix (4 Steps, 5 Minutes)
See `VERCEL_FIX_CHECKLIST.md` for detailed instructions.
**Quick version:**
1. Set Root Directory to `apps/web` (exactly)
2. Set Framework Preset to "Next.js"
3. Remove `tpmjs.com` domain from `v0-tool-registry-page` project
4. Clear custom build commands (let Vercel auto-detect)
**No code changes needed. Just fix Vercel dashboard settings.**
### After the Fix
Next deployment will show:
```bash
$ vercel inspect <new-deployment>
Builds
├── λ api/health
├── λ api/tools
├── λ api/sync/changes
├── λ api/tools/[id]
├── λ api/tools/[slug]
├── λ tool/[slug]
...
```
And API routes will work instantly:
```bash
$ curl https://tpmjs.com/api/health
{"status":"ok","timestamp":"..."}
```
---
## Summary for ChatGPT
The core issue is that **Vercel is not detecting the project as Next.js** due to incorrect Root Directory, Framework Preset set to "Other", domain conflicts between two projects, and custom build commands bypassing auto-detection.
When Vercel doesn't detect Next.js, it uses a static site builder which deploys pages but **completely drops all App Router API routes** from the build. This is why pages work but all `/api/*` requests timeout - the API route serverless functions were never created.
**The fix is purely configuration** - no code changes needed. Set Root Directory to `apps/web`, Framework Preset to "Next.js", remove the domain from the old project, and clear custom build commands. See `VERCEL_FIX_CHECKLIST.md` for step-by-step instructions.

421
CLAUDE.md
View file

@ -678,4 +678,423 @@ API timeouts in serverless environments often stem from build configuration issu
4. Use `vercel inspect` to verify lambda deployment
5. Test database performance locally before deploying
The full working implementation is live at [tpmjs.com](https://tpmjs.com).
The full working implementation is live at [tpmjs.com](https://tpmjs.com).
---
## NPM Package Syncing System
TPMJS.com automatically mirrors npm packages with the `tpmjs` keyword to keep the tool registry up-to-date. This section documents how the syncing system works.
### Overview
The sync system uses three automated strategies running on Vercel Cron to discover and update TPMJS tools:
1. **Changes Feed** - Monitors npm's real-time changes feed for all package updates
2. **Keyword Search** - Actively searches npm for packages with the `tpmjs` keyword
3. **Metrics Sync** - Updates download stats and calculates quality scores
### Sync Endpoints
All sync endpoints are located in `apps/web/src/app/api/sync/`:
#### 1. Changes Feed Sync (`/api/sync/changes`)
**Purpose:** Monitors npm's changes feed to catch new packages and updates in real-time.
**Schedule:** Every 2 minutes (`*/2 * * * *`)
**How it works:**
1. Fetches the last checkpoint sequence number from the database
2. Calls npm's `/_changes` endpoint with `since=<lastSeq>` (limit 100 per run)
3. For each changed package, fetches full metadata with `fetchLatestPackageWithMetadata()`
4. Validates that the package has a valid `tpmjs` field using `validateTpmjsField()`
5. Upserts the tool to the database with `discoveryMethod: 'changes-feed'`
6. Updates the checkpoint with the new sequence number for next run
**Key Features:**
- Uses checkpoints to track progress and avoid reprocessing
- Processes up to 100 changes per run to avoid timeouts
- Logs all sync operations to `syncLog` table
- Requires `Authorization: Bearer <CRON_SECRET>` header
**Example Response:**
```json
{
"success": true,
"data": {
"processed": 5,
"skipped": 93,
"errors": 0,
"lastSeq": "12345678",
"pending": 1250,
"durationMs": 2834
}
}
```
#### 2. Keyword Search Sync (`/api/sync/keyword`)
**Purpose:** Actively searches npm for packages with the `tpmjs` keyword.
**Schedule:** Every 15 minutes (`*/15 * * * *`)
**How it works:**
1. Searches npm registry for packages with keyword `tpmjs` (up to 250 results)
2. Fetches full metadata for each package
3. Validates the `tpmjs` field
4. Upserts tools with `discoveryMethod: 'keyword'`
5. Updates checkpoint with last run timestamp
**Key Features:**
- Catches packages that might be missed by changes feed
- Useful for backfilling existing packages
- Processes up to 250 packages per run
**Example Response:**
```json
{
"success": true,
"data": {
"processed": 12,
"skipped": 3,
"errors": 0,
"packagesFound": 15,
"durationMs": 4521
}
}
```
#### 3. Metrics Sync (`/api/sync/metrics`)
**Purpose:** Updates download statistics and calculates quality scores for all tools.
**Schedule:** Every hour (`0 * * * *`)
**How it works:**
1. Fetches all tools from the database
2. For each tool, calls `fetchDownloadStats()` to get last 30 days of downloads
3. Calculates quality score based on:
- Tier (rich = 0.6, minimal = 0.4)
- Downloads (logarithmic scale, max 0.3)
- GitHub stars (logarithmic scale, max 0.1)
4. Updates `npmDownloadsLastMonth` and `qualityScore` fields
**Quality Score Formula:**
```typescript
function calculateQualityScore(params: {
tier: string;
downloads: number;
githubStars: number;
}): number {
const tierScore = tier === 'rich' ? 0.6 : 0.4;
const downloadsScore = Math.min(0.3, Math.log10(downloads + 1) / 10);
const starsScore = Math.min(0.1, Math.log10(githubStars + 1) / 10);
return Math.min(1.0, tierScore + downloadsScore + starsScore);
}
```
**Example Response:**
```json
{
"success": true,
"data": {
"processed": 25,
"skipped": 0,
"errors": 0,
"totalTools": 25,
"durationMs": 8234
}
}
```
### Automated Sync Configuration
The sync system can run via two methods:
#### Option 1: Vercel Cron (Primary)
Cron jobs are configured in `vercel.json` at the repository root:
```json
{
"crons": [
{
"path": "/api/sync/changes",
"schedule": "*/2 * * * *"
},
{
"path": "/api/sync/keyword",
"schedule": "*/15 * * * *"
},
{
"path": "/api/sync/metrics",
"schedule": "0 * * * *"
}
]
}
```
**Pros:**
- Native Vercel integration
- Automatic authentication with `CRON_SECRET`
- Same infrastructure as the app
- No setup required (works automatically on deploy)
#### Option 2: GitHub Actions (Backup)
A GitHub Actions workflow (`.github/workflows/sync.yml`) provides redundancy:
```yaml
name: NPM Package Sync
on:
schedule:
- cron: '*/2 * * * *' # Changes feed
- cron: '*/15 * * * *' # Keyword search
- cron: '0 * * * *' # Metrics
workflow_dispatch: # Manual trigger
```
**Pros:**
- Redundancy if Vercel Cron fails
- Manual trigger via GitHub UI
- Free on GitHub (included in free tier)
- Runs from GitHub's infrastructure
**Setup:**
1. Add secrets to GitHub repository settings:
- `VERCEL_PRODUCTION_URL` - Your production URL (e.g., `https://tpmjs.com`)
- `CRON_SECRET` - Same secret used in Vercel environment variables
2. Enable GitHub Actions in repository settings
3. The workflow will run automatically on schedule OR manually via:
- GitHub Actions tab → NPM Package Sync → Run workflow → Select sync type
**Schedule Breakdown:**
- Changes feed: Every 2 minutes (30 times per hour)
- Keyword search: Every 15 minutes (4 times per hour)
- Metrics: Every hour (once per hour)
**Recommendation:** Use Vercel Cron as primary and GitHub Actions as backup. Both can run simultaneously - the sync endpoints are idempotent.
### Database Schema
The sync system uses these Prisma models:
**`Tool` - The main tool registry:**
```prisma
model Tool {
id String @id @default(cuid())
npmPackageName String @unique
npmVersion String
npmDownloadsLastMonth Int @default(0)
qualityScore Float?
discoveryMethod String // 'changes-feed' | 'keyword'
tier String // 'minimal' | 'rich'
// ... other fields
@@index([qualityScore])
@@index([npmDownloadsLastMonth])
}
```
**`SyncCheckpoint` - Tracks sync progress:**
```prisma
model SyncCheckpoint {
id String @id @default(cuid())
source String @unique // 'changes-feed' | 'keyword-search' | 'metrics'
checkpoint Json // { lastSeq: string, lastRun: string, ... }
}
```
**`SyncLog` - Records all sync operations:**
```prisma
model SyncLog {
id String @id @default(cuid())
source String
status String // 'success' | 'partial' | 'error'
processed Int
skipped Int
errors Int
message String?
metadata Json?
createdAt DateTime @default(now())
}
```
### Manual Sync Triggers
To manually trigger a sync (useful for testing or debugging):
```bash
# Trigger changes feed sync
curl -X POST https://tpmjs.com/api/sync/changes \
-H "Authorization: Bearer $CRON_SECRET"
# Trigger keyword search
curl -X POST https://tpmjs.com/api/sync/keyword \
-H "Authorization: Bearer $CRON_SECRET"
# Trigger metrics update
curl -X POST https://tpmjs.com/api/sync/metrics \
-H "Authorization: Bearer $CRON_SECRET"
```
**Note:** You need the `CRON_SECRET` environment variable set in Vercel. The endpoints return 401 Unauthorized without it.
### Monitoring Sync Health
Check sync logs in the database:
```typescript
// Get recent sync operations
const recentSyncs = await prisma.syncLog.findMany({
orderBy: { createdAt: 'desc' },
take: 20,
});
// Check last successful sync for each source
const checkpoints = await prisma.syncCheckpoint.findMany();
```
**Sync Log Example:**
```json
{
"id": "clx...",
"source": "changes-feed",
"status": "success",
"processed": 5,
"skipped": 93,
"errors": 0,
"message": "Successfully processed 5 packages",
"metadata": {
"durationMs": 2834,
"lastSeq": "12345678",
"pending": 1250
},
"createdAt": "2025-11-30T12:00:00Z"
}
```
### Error Handling
All sync endpoints follow this error handling pattern:
1. **Partial Success:** If some packages fail but others succeed, status is `partial`
2. **Complete Failure:** If the entire sync fails, status is `error`
3. **Error Messages:** First 3 errors are included in the response
4. **Logging:** All operations are logged to `syncLog` regardless of success
**Example Partial Failure:**
```json
{
"success": true,
"data": {
"processed": 5,
"skipped": 2,
"errors": 3,
"durationMs": 5234
}
}
```
The sync log will contain:
```json
{
"status": "partial",
"message": "Processed with errors: Failed to process pkg1: Network timeout; Failed to process pkg2: Invalid tpmjs field; ..."
}
```
### Configuration
Required environment variables in Vercel:
```bash
# Database connection
DATABASE_URL="postgresql://..."
# Cron job authentication
CRON_SECRET="your-secret-key"
```
**Important:** Vercel Cron automatically adds the `Authorization: Bearer $CRON_SECRET` header when calling the endpoints. No manual configuration needed.
### Performance Considerations
**Timeouts:**
- All sync routes have `maxDuration: 300` (5 minutes)
- Changes feed processes max 100 packages per run to avoid timeouts
- Keyword search processes max 250 packages per run
- Metrics sync processes all tools but runs only once per hour
**Rate Limiting:**
- npm API has rate limits - be cautious when testing manually
- Vercel Cron jobs run from Vercel's infrastructure (different IP than dev)
- Consider implementing exponential backoff for npm API errors
**Cold Starts:**
- First request to each sync endpoint may be slow due to Prisma initialization
- Subsequent requests are faster with warm Prisma Client
- This is acceptable for background cron jobs
### Debugging Sync Issues
**Check if cron jobs are running:**
```bash
# View recent deployments
vercel ls
# Check logs for a specific deployment
vercel logs <deployment-url>
# Filter for sync-related logs
vercel logs <deployment-url> | grep sync
```
**Common issues:**
1. **"Unauthorized" errors:** Check that `CRON_SECRET` is set in Vercel environment variables
2. **Timeouts:** Reduce batch size in changes feed (currently 100)
3. **Missing packages:** Check `syncLog` for errors during processing
4. **Stale data:** Verify metrics sync is running every hour
**Test sync locally:**
```bash
# Start dev server
pnpm dev --filter=@tpmjs/web
# Trigger sync (requires CRON_SECRET in .env.local)
curl -X POST http://localhost:3000/api/sync/changes \
-H "Authorization: Bearer $CRON_SECRET"
```
### Package Discovery Flow
Here's how a new TPMJS tool gets discovered:
1. **Developer publishes package to npm** with `tpmjs` keyword and `tpmjs` field in package.json
2. **Within 2 minutes:** Changes feed sync picks it up from npm's `/_changes` endpoint
3. **Validation:** `validateTpmjsField()` checks that the `tpmjs` field meets requirements
4. **Database Insert:** Tool is upserted with initial data
5. **Within 1 hour:** Metrics sync updates download stats and calculates quality score
6. **Visible on tpmjs.com:** Tool appears in search results and category pages
**Backup Discovery:** If changes feed misses a package, the keyword search (every 15 minutes) will catch it.
### Future Improvements
Potential enhancements to the sync system:
- [ ] Add webhook endpoint for instant npm package notifications
- [ ] Implement exponential backoff for npm API rate limits
- [ ] Add Slack/Discord notifications for sync failures
- [ ] Create admin dashboard to monitor sync health
- [ ] Support GitHub stars syncing (requires GitHub API integration)
- [ ] Add sync metrics to Vercel Analytics
- [ ] Implement differential sync to reduce database writes

View file

@ -167,7 +167,7 @@ Not needed for Option 1 (Deployment Protection).
Add to README.md to show CI status:
```markdown
[![CI](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml)
[![CI](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml/badge.svg)](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
```
## Summary

424
HOW_TO_PUBLISH_A_TOOL.md Normal file
View file

@ -0,0 +1,424 @@
# How to Publish a TPMJS Tool
This guide shows you how to create and publish an AI tool that will be automatically discovered and listed on tpmjs.com.
## Quick Start
1. Create a new NPM package
2. Add `"tpmjs"` to the `keywords` array in package.json
3. Add a `tpmjs` field with your tool's metadata
4. Publish to NPM
5. Your tool will automatically appear on tpmjs.com within 15 minutes
## Step-by-Step Guide
### 1. Create Your NPM Package
Create a standard NPM package with your tool implementation:
```bash
mkdir my-awesome-tool
cd my-awesome-tool
npm init -y
```
### 2. Add the Required Keyword
In your `package.json`, add `"tpmjs"` to the keywords array:
```json
{
"name": "@yourname/my-awesome-tool",
"version": "1.0.0",
"keywords": ["tpmjs", "ai", "other-keywords"],
...
}
```
**Important:** The `"tpmjs"` keyword is REQUIRED for automatic discovery!
### 3. Add TPMJS Metadata
Add a `tpmjs` field to your `package.json` with your tool's metadata. There are three tiers:
#### Tier 1: Minimal (Required Fields Only)
The bare minimum to get listed:
```json
{
"tpmjs": {
"category": "text-analysis",
"description": "A concise description of what your tool does"
}
}
```
**Required fields:**
- `category` - One of: `text-analysis`, `code-generation`, `data-processing`, `image-generation`, `audio-processing`, `search`, `integration`, `other`
- `description` - Clear description of what the tool does (1-3 sentences)
#### Tier 2: Basic (Recommended)
Add parameter and return type information:
```json
{
"tpmjs": {
"category": "text-analysis",
"description": "Analyzes sentiment in text and returns a score",
"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 containing score (-1 to 1) and label (positive/negative/neutral)"
}
}
}
```
#### Tier 3: Rich (Full Documentation)
Complete metadata for maximum visibility:
```json
{
"tpmjs": {
"category": "text-analysis",
"description": "Advanced sentiment analysis with emotion detection",
"parameters": [
{
"name": "text",
"type": "string",
"description": "The text to analyze",
"required": true
},
{
"name": "language",
"type": "string",
"description": "Language code",
"required": false,
"default": "en"
},
{
"name": "includeEmotions",
"type": "boolean",
"description": "Whether to include emotion breakdown",
"required": false,
"default": false
}
],
"returns": {
"type": "SentimentResult",
"description": "Object with score, label, and optional emotions array"
},
"env": [
{
"name": "SENTIMENT_API_KEY",
"description": "API key for sentiment analysis service",
"required": true
}
],
"frameworks": ["vercel-ai", "langchain"],
"aiAgent": {
"useCase": "Use this tool when users need to analyze sentiment in text, detect emotions, or understand the tone of customer feedback, reviews, or social media posts.",
"limitations": "Only supports English and Spanish. Maximum 10,000 characters per request.",
"examples": [
"Analyze customer review sentiment",
"Detect emotions in user feedback",
"Monitor social media sentiment"
]
}
}
}
```
### 4. Implement Your Tool
Write your tool's implementation. Here's the example from `@tpmjs/createblogpost`:
```typescript
// src/index.ts
export interface BlogPostOptions {
title: string;
author: string;
content: string;
tags?: string[];
format?: 'markdown' | 'mdx';
excerpt?: string;
}
export interface BlogPost {
frontmatter: {
title: string;
author: string;
date: string;
tags: string[];
excerpt?: string;
slug: string;
wordCount: number;
readingTime: number;
};
content: string;
formattedOutput: string;
}
export async function createBlogPost(options: BlogPostOptions): Promise<BlogPost> {
// Your implementation here
const { title, author, content, tags = [], format = 'markdown', excerpt } = options;
// Validate inputs
if (!title || !author || !content) {
throw new Error('Title, author, and content are required');
}
// Process and return result
return {
frontmatter: { /* ... */ },
content,
formattedOutput: '...'
};
}
export default createBlogPost;
```
### 5. Build and Publish
Build your package and publish to NPM:
```bash
# Build your package
npm run build
# Publish to NPM
npm publish --access public
```
### 6. Verification
Your tool will be automatically discovered through:
1. **Keyword Search** - Runs every 15 minutes, searches NPM for `"tpmjs"`
2. **Changes Feed** - Monitors NPM publishes in real-time (every 2 minutes)
After publishing, your tool should appear on https://tpmjs.com within 15 minutes!
You can verify by searching: https://tpmjs.com/api/tools?q=yourpackagename
## Real Example: @tpmjs/createblogpost
Here's the complete `package.json` from the published example:
```json
{
"name": "@tpmjs/createblogpost",
"version": "0.2.0",
"description": "A tool for creating structured blog posts with AI-generated content",
"type": "module",
"keywords": ["tpmjs", "blog", "content", "ai", "writing"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/ajaxdavis/tpmjs.git",
"directory": "packages/tools/createBlogPost"
},
"homepage": "https://tpmjs.com",
"license": "MIT",
"tpmjs": {
"category": "text-analysis",
"description": "Creates structured blog posts with customizable frontmatter, content sections, and SEO metadata. Supports multiple output formats including Markdown and MDX.",
"parameters": [
{
"name": "title",
"type": "string",
"description": "The title of the blog post",
"required": true
},
{
"name": "author",
"type": "string",
"description": "The author of the blog post",
"required": true
},
{
"name": "content",
"type": "string",
"description": "The main content of the blog post",
"required": true
},
{
"name": "tags",
"type": "string[]",
"description": "Array of tags for categorization",
"required": false,
"default": []
},
{
"name": "format",
"type": "'markdown' | 'mdx'",
"description": "Output format for the blog post",
"required": false,
"default": "markdown"
},
{
"name": "excerpt",
"type": "string",
"description": "Short excerpt or summary of the post",
"required": false
}
],
"returns": {
"type": "BlogPost",
"description": "A structured blog post object with frontmatter, content, and metadata including slug, wordCount, readingTime, and formattedOutput"
},
"frameworks": ["vercel-ai", "langchain"],
"aiAgent": {
"useCase": "Use this tool when users need to generate blog posts, articles, or structured content with proper frontmatter and metadata. Ideal for content management systems, static site generators, and documentation sites.",
"limitations": "Does not include AI content generation - you must provide the content. Only formats and structures existing content.",
"examples": [
"Create a blog post about TypeScript best practices",
"Generate a tutorial post with code examples",
"Format an article with SEO metadata"
]
}
}
}
```
## Field Reference
### Required Fields (Tier 1 - Minimal)
| Field | Type | Description |
|-------|------|-------------|
| `category` | string | Tool category (see categories below) |
| `description` | string | Clear description (1-3 sentences) |
### Optional Fields (Tier 2 - Basic)
| Field | Type | Description |
|-------|------|-------------|
| `parameters` | array | Array of parameter objects |
| `returns` | object | Return type information |
### Optional Fields (Tier 3 - Rich)
| Field | Type | Description |
|-------|------|-------------|
| `env` | array | Required environment variables |
| `frameworks` | array | Compatible frameworks |
| `aiAgent` | object | AI agent integration info |
### Categories
Choose one of these for the `category` field:
- `text-analysis` - NLP, sentiment, summarization
- `code-generation` - Code generation and transformation
- `data-processing` - Data manipulation and transformation
- `image-generation` - Image creation and editing
- `audio-processing` - Audio/speech processing
- `search` - Search and retrieval
- `integration` - Third-party integrations
- `other` - Anything else
### Environment Variables
If your tool requires environment variables:
```json
"env": [
{
"name": "OPENAI_API_KEY",
"description": "API key for OpenAI services",
"required": true
},
{
"name": "API_ENDPOINT",
"description": "Custom API endpoint URL",
"required": false,
"default": "https://api.example.com"
}
]
```
## Quality Score
Your tool gets a quality score based on:
- **Tier**: Rich (1.0) > Basic (0.5) > Minimal (0.25)
- **Downloads**: Logarithmic scale based on monthly NPM downloads
- **GitHub Stars**: Logarithmic scale based on repository stars
Higher scores = better visibility on tpmjs.com!
## Tips for Success
1. **Use descriptive names** - Make your package name clear and searchable
2. **Complete metadata** - Tier 3 (Rich) tools get 4x the base score
3. **Good documentation** - Add documentation URL to package.json homepage or repository fields
4. **Active maintenance** - Regular updates boost download counts
5. **AI-friendly descriptions** - Write the `aiAgent.useCase` field as guidance for AI agents
## Testing Locally
Before publishing, you can validate your `tpmjs` field using the validation schema:
```bash
# In the tpmjs monorepo
pnpm --filter=@tpmjs/types test
```
Or manually check the structure matches the examples above.
## Troubleshooting
**Tool not appearing after 15 minutes?**
- Check that you added `"tpmjs"` to keywords
- Verify your `tpmjs` field has required fields (category, description)
- Check the NPM package is public: `npm view yourpackage`
**Tool showing as "minimal" tier?**
- Add `parameters` and `returns` fields for Basic tier
- Add all Rich tier fields for maximum visibility
**Want to force a sync?**
You can manually trigger a sync (requires auth):
```bash
curl -X POST "https://tpmjs.com/api/sync/keyword" \
-H "Authorization: Bearer YOUR_CRON_SECRET"
```
## Support
Questions or issues?
- File an issue: https://github.com/ajaxdavis/tpmjs/issues
- Check the API: https://tpmjs.com/api/tools

View file

@ -1,782 +0,0 @@
# TPMJS NPM Registry - Implementation Checklist
> **Reference:** See [NPM_MIRROR.md](./NPM_MIRROR.md) for complete architecture details
**Stack Decision:** Vercel + Neon + Vercel Cron (polling-based sync)
---
## 🎯 Implementation Strategy
### Architecture Simplification
**Original Plan (NPM_MIRROR.md):**
- Separate Node.js sync service with persistent changes feed connection
- Self-hosted PostgreSQL
- More complex deployment
**Revised Plan (This Checklist):**
- All-in-one Next.js app on Vercel
- Neon Postgres (serverless)
- Vercel Cron for sync jobs (polling-based, no persistent connections)
- Simpler, faster to ship
### Why This Approach?
✅ **Simpler Infrastructure**
- One deployment (Vercel)
- Managed database (Neon)
- Built-in cron (Vercel Cron)
✅ **Lower Cost**
- No separate sync service hosting
- Neon free tier generous
- Vercel free/hobby tier sufficient for MVP
✅ **Same Functionality**
- Poll NPM changes feed every 1-2 minutes (effectively real-time)
- All discovery features from NPM_MIRROR.md maintained
- Quality scoring, validation, etc. all work the same
---
## 📋 Phase 1: Foundation (Week 1)
**Goal:** Set up database, types, and NPM client
### 1.1 Database Setup
- [ ] **Create Neon project**
- Go to https://neon.tech/
- Create new project
- Save connection string
- [ ] **Create `packages/db` package**
```bash
mkdir -p packages/db
cd packages/db
pnpm init
pnpm add prisma @prisma/client
pnpm add -D typescript @types/node
```
- [ ] **Initialize Prisma**
```bash
npx prisma init
```
- [ ] **Create Prisma schema**
- Copy schema from NPM_MIRROR.md Database Design section
- File: `packages/db/prisma/schema.prisma`
- Include all three models: `Tool`, `SyncCheckpoint`, `SyncLog`
- [ ] **Add database URL to `.env`**
```env
DATABASE_URL="postgresql://..."
```
- [ ] **Run first migration**
```bash
npx prisma migrate dev --name init
npx prisma generate
```
- [ ] **Create Prisma client singleton**
- File: `packages/db/src/client.ts`
```typescript
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
```
- [ ] **Export from package**
- File: `packages/db/src/index.ts`
```typescript
export { prisma } from './client';
export * from '@prisma/client';
```
- [ ] **Update package.json**
```json
{
"name": "@tpmjs/db",
"main": "./src/index.ts",
"types": "./src/index.ts"
}
```
- [ ] **Seed initial sync state**
```sql
INSERT INTO sync_checkpoints (source, checkpoint)
VALUES
('changes-feed', '{"sequence": 0}'::jsonb),
('keyword-search', '{"lastRun": null}'::jsonb),
('metrics', '{"lastRun": null}'::jsonb)
ON CONFLICT (source) DO NOTHING;
```
**Verification:**
```bash
cd packages/db
npx prisma studio # Should open DB browser with empty tables
```
---
### 1.2 Types Package
**Reference:** See NPM_MIRROR.md "The 'tpmjs' Field Schema" section
- [ ] **Update `packages/types/src/tool.ts`**
- Add `TpmjsMinimalSchema` with Zod
- Add `TpmjsRichSchema` extending minimal
- Export both schemas and inferred types
- [ ] **Create validation helper**
- File: `packages/types/src/validator.ts`
```typescript
export function validateTpmjsField(tpmjs: unknown): {
valid: boolean;
tier: 'minimal' | 'rich' | null;
data?: unknown;
errors?: ZodError[];
}
```
- [ ] **Update exports**
- File: `packages/types/src/index.ts`
- Export all schemas and validators
**Verification:**
```typescript
import { validateTpmjsField } from '@tpmjs/types';
const result = validateTpmjsField({
category: 'web-scraping',
description: 'Test description that is long enough',
example: 'const x = await tool.test()'
});
console.log(result); // Should be { valid: true, tier: 'minimal', ... }
```
---
### 1.3 NPM Client Package
**Reference:** See NPM_MIRROR.md "NPM Integration Strategy" section
- [ ] **Create `packages/npm-client`**
```bash
mkdir -p packages/npm-client/src
cd packages/npm-client
pnpm init
pnpm add zod
pnpm add -D typescript @types/node
```
- [ ] **Implement changes feed client**
- File: `packages/npm-client/src/changes.ts`
```typescript
export async function fetchChanges(since: string, limit = 100): Promise<{
results: Array<{ id: string; seq: string }>;
lastSeq: string;
}>
```
- Use endpoint: `https://replicate.npmjs.com/registry/_changes`
- Poll-based (no EventSource needed)
- [ ] **Implement keyword search**
- File: `packages/npm-client/src/search.ts`
```typescript
export async function searchByKeyword(
keyword: string,
size = 250,
from = 0
): Promise<Array<{ name: string }>>
```
- Use endpoint: `/-/v1/search?text=keywords:${keyword}`
- [ ] **Implement package metadata fetcher**
- File: `packages/npm-client/src/package.ts`
```typescript
export async function fetchPackageMetadata(packageName: string): Promise<{
name: string;
'dist-tags': { latest: string };
versions: Record<string, {
version: string;
description?: string;
tpmjs?: unknown;
// ... other fields
}>;
time: Record<string, string>;
} | null>
```
- Use endpoint: `https://registry.npmjs.org/${packageName}`
- [ ] **Implement download stats**
- File: `packages/npm-client/src/stats.ts`
```typescript
export async function fetchDownloadStats(
packageName: string
): Promise<number>
```
- Use endpoint: `https://api.npmjs.org/downloads/point/last-month/${packageName}`
- [ ] **Implement GitHub stats** (optional Phase 4)
- File: `packages/npm-client/src/github.ts`
```typescript
export async function fetchGithubStars(
repoUrl: string
): Promise<number>
```
- [ ] **Add rate limiting helper**
- File: `packages/npm-client/src/rate-limiter.ts`
- Simple delay between requests
- Exponential backoff on 429
- [ ] **Export all functions**
- File: `packages/npm-client/src/index.ts`
**Verification:**
```typescript
import { fetchPackageMetadata } from '@tpmjs/npm-client';
const pkg = await fetchPackageMetadata('express');
console.log(pkg?.name); // Should print 'express'
```
---
## 📋 Phase 2: Core API Routes (Week 2)
**Goal:** Build public API for searching/listing tools
### 2.1 Tool Search/List API
**Reference:** See NPM_MIRROR.md "API Routes" section
- [ ] **Create `apps/web/src/app/api/tools/route.ts`**
- Implement `GET` handler
- Query params: `q`, `category`, `official`, `limit`, `offset`
- Use Prisma to query `tools` table
- Return paginated results with metadata
- [ ] **Add full-text search**
- Use Postgres `ts_vector` for search
- Or simple `ILIKE` for MVP
- Search across: `npmPackageName`, `description`, `tags`
- [ ] **Add filtering**
- By `category`
- By `isOfficial`
- By `tier` (optional)
- [ ] **Add sorting**
- Default: `qualityScore DESC`, `npmDownloadsLastMonth DESC`
- Optional: `createdAt DESC`, `npmPackageName ASC`
**Verification:**
```bash
curl "http://localhost:3001/api/tools?q=web&limit=5"
# Should return JSON with tools array and pagination
```
---
### 2.2 Tool Detail API
- [ ] **Create `apps/web/src/app/api/tools/[id]/route.ts`**
- Implement `GET` handler
- Accept ID or package name
- Return full tool details
**Verification:**
```bash
curl "http://localhost:3001/api/tools/1"
# Should return single tool object
```
---
### 2.3 Validation API
- [ ] **Create `apps/web/src/app/api/tools/validate/route.ts`**
- Implement `POST` handler
- Accept JSON body with `tpmjs` field
- Use `@tpmjs/types` validator
- Return validation result with errors
**Verification:**
```bash
curl -X POST http://localhost:3001/api/tools/validate \
-H "Content-Type: application/json" \
-d '{"category":"web-scraping","description":"Test tool for validation","example":"const x = await tool.test()"}'
# Should return { valid: true, tier: "minimal" }
```
---
### 2.4 Stats API
- [ ] **Create `apps/web/src/app/api/stats/route.ts`**
- Implement `GET` handler
- Aggregate counts by category
- Total tools, official tools, etc.
**Verification:**
```bash
curl "http://localhost:3001/api/stats"
# Should return { totalTools: 0, officialTools: 0, categories: {} }
```
---
## 📋 Phase 3: Sync Workers (Week 2-3)
**Goal:** Implement automatic NPM package discovery
**Reference:** See NPM_MIRROR.md "NPM Integration Strategy" section
### 3.1 Changes Feed Sync
- [ ] **Create `apps/web/src/app/api/sync/changes/route.ts`**
- [ ] **Implement POST handler**
```typescript
export async function POST(request: Request) {
// 1. Verify CRON_SECRET header
// 2. Get last sequence from sync_checkpoints
// 3. Fetch changes from NPM (limit 100-500)
// 4. For each change:
// - Fetch package metadata
// - Check for tpmjs field
// - Validate with @tpmjs/types
// - Upsert to tools table
// - Log to sync_logs
// 5. Update checkpoint with new sequence
// 6. Return summary (processed, skipped, errors)
}
```
- [ ] **Add secret protection**
```typescript
const secret = request.headers.get('x-cron-secret');
if (secret !== process.env.CRON_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
```
- [ ] **Add timeout protection**
- Limit processing to 50 packages per run
- Or 50 seconds max execution time
- Save checkpoint frequently
- [ ] **Add error handling**
- Try/catch around each package
- Log errors to `sync_logs`
- Continue processing other packages
**Verification:**
```bash
curl -X POST http://localhost:3001/api/sync/changes \
-H "x-cron-secret: your-secret"
# Should process changes and return summary
```
---
### 3.2 Keyword Search Sync
- [ ] **Create `apps/web/src/app/api/sync/keyword/route.ts`**
- [ ] **Implement POST handler**
```typescript
export async function POST(request: Request) {
// 1. Verify CRON_SECRET header
// 2. Search NPM for keyword 'tpmjs-tool'
// 3. For each result:
// - Fetch package metadata
// - Validate tpmjs field
// - Upsert with isOfficial=true
// - Log to sync_logs
// 4. Update checkpoint
// 5. Return summary
}
```
- [ ] **Handle pagination**
- NPM allows `size` up to 250
- May need multiple requests for all results
**Verification:**
```bash
curl -X POST http://localhost:3001/api/sync/keyword \
-H "x-cron-secret: your-secret"
# Should search and process keyword packages
```
---
### 3.3 Metrics Sync (Phase 4)
- [ ] **Create `apps/web/src/app/api/sync/metrics/route.ts`**
- [ ] **Implement POST handler**
```typescript
export async function POST(request: Request) {
// 1. Verify CRON_SECRET
// 2. Select tools to update (recent, popular, or sample)
// 3. For each tool:
// - Fetch NPM download stats
// - Fetch GitHub stars (if repo exists)
// - Calculate quality score
// - Update tools table
// 4. Update checkpoint
// 5. Return summary
}
```
**Verification:**
```bash
curl -X POST http://localhost:3001/api/sync/metrics \
-H "x-cron-secret: your-secret"
# Should update metrics for tools
```
---
### 3.4 Vercel Cron Configuration
- [ ] **Add to `vercel.json`**
```json
{
"crons": [
{
"path": "/api/sync/changes",
"schedule": "*/2 * * * *"
},
{
"path": "/api/sync/keyword",
"schedule": "*/15 * * * *"
},
{
"path": "/api/sync/metrics",
"schedule": "0 * * * *"
}
]
}
```
- [ ] **Set up environment variables in Vercel**
- `DATABASE_URL` - Neon connection string
- `CRON_SECRET` - Generate random secret
- `NPM_REGISTRY_URL` - https://registry.npmjs.org
- `NPM_CHANGES_URL` - https://replicate.npmjs.com/registry
---
## 📋 Phase 4: Frontend Integration (Week 3)
**Goal:** Replace mock data with real API calls
### 4.1 Update Tool Listing Page
- [ ] **Update `apps/web/src/app/tools/page.tsx`**
- Remove mock data import
- Fetch from `/api/tools`
- Add loading state
- Add error handling
- [ ] **Add search functionality**
- Search input component
- Debounced API calls
- Update URL with search params
- [ ] **Add category filter**
- Category dropdown/pills
- Filter API calls by category
- [ ] **Add pagination**
- Next/previous buttons
- Or infinite scroll
**Verification:**
- Visit http://localhost:3001/tools
- Should show real tools from database
- Search should work
- Filters should work
---
### 4.2 Update Tool Detail Page
- [ ] **Update `apps/web/src/app/tools/[id]/page.tsx`**
- Fetch from `/api/tools/[id]`
- Display all tool metadata
- Show rich tier fields if available
- [ ] **Add install instructions**
- npm install command
- Usage example from `tpmjs.example`
- [ ] **Add links**
- NPM package page
- GitHub repository
- Documentation
- Playground (if available)
**Verification:**
- Visit http://localhost:3001/tools/some-package
- Should show full tool details
---
### 4.3 Update Homepage
**Reference:** See NPM_MIRROR.md for stats display
- [ ] **Update stats in hero section**
- Fetch from `/api/stats`
- Show real tool count
- Show category breakdown
- [ ] **Update live metrics**
- Real download counts
- Real tool counts
- Update frequently (client-side polling or static)
**Verification:**
- Visit http://localhost:3001
- Stats should be real, not mock
---
## 📋 Phase 5: Testing & Polish (Week 4)
### 5.1 Create Test Packages
- [ ] **Publish 3-5 real NPM packages with `tpmjs` field**
- At least one with minimal tier
- At least one with rich tier
- Use `tpmjs-tool` keyword for official listing
- [ ] **Verify automatic discovery**
- Wait for next sync run
- Check they appear in database
- Check they appear on website
---
### 5.2 Documentation
- [ ] **Create docs section**
- `apps/web/src/app/docs/page.tsx`
- Getting started guide
- Schema reference
- Examples
- [ ] **Add validation playground**
- `apps/web/src/app/docs/validate/page.tsx`
- Form to test `tpmjs` field
- Real-time validation feedback
- Uses `/api/tools/validate`
---
### 5.3 CLI Tool (Optional)
- [ ] **Create `packages/cli`**
- Command: `tpmjs validate`
- Reads local `package.json`
- Validates `tpmjs` field
- Calls `/api/tools/validate`
---
### 5.4 Monitoring
- [ ] **Add health endpoint**
- `apps/web/src/app/api/health/route.ts`
- Check database connectivity
- Check sync status (last run times)
- [ ] **Set up uptime monitoring**
- Use UptimeRobot or Better Stack
- Monitor `/api/health`
- Alert if down or sync stale
- [ ] **Add error tracking**
- Set up Sentry for Next.js
- Track API errors
- Track sync errors
**Verification:**
```bash
curl http://localhost:3001/api/health
# Should return { status: "ok", db: "ok", sync: { ... } }
```
---
## 📋 Phase 6: Launch (Week 5)
### 6.1 Pre-Launch Checklist
- [ ] **Database**
- ✓ Prisma schema deployed
- ✓ Indexes created
- ✓ Backups enabled in Neon
- [ ] **Environment Variables**
- ✓ All secrets in Vercel
- ✓ `CRON_SECRET` set
- ✓ `DATABASE_URL` set
- [ ] **API Routes**
- ✓ All endpoints working
- ✓ Rate limiting added (optional)
- ✓ Error handling complete
- [ ] **Sync Workers**
- ✓ Changes feed running every 2 min
- ✓ Keyword search running every 15 min
- ✓ Checkpoints updating correctly
- [ ] **Frontend**
- ✓ All pages loading real data
- ✓ Search working
- ✓ Mobile responsive
- [ ] **Monitoring**
- ✓ Health check endpoint live
- ✓ Uptime monitoring active
- ✓ Error tracking active
---
### 6.2 Launch Steps
- [ ] **Deploy to production**
```bash
git push origin main
# Vercel auto-deploys
```
- [ ] **Verify deployment**
- Check all pages load
- Check API endpoints work
- Check cron jobs run
- [ ] **Publish announcement**
- Tweet/post about TPMJS
- Explain how to add `tpmjs` field
- Share validation endpoint
- [ ] **Monitor for 24 hours**
- Watch error logs
- Check sync is working
- Fix any issues
---
## 📋 Phase 7: Post-Launch (Ongoing)
### Enhancements
- [ ] **Semantic search**
- Add embeddings to tools table
- Use OpenAI/Cohere for semantic search
- [ ] **Usage analytics**
- Track tool views
- Track search queries
- Popular tools widget
- [ ] **Tool recommendations**
- "Similar tools" section
- "You might also like"
- [ ] **GitHub Actions**
- Validate `tpmjs` field in CI
- Auto-comment validation results
- [ ] **NPM webhooks**
- Listen for package updates
- Immediate sync (instead of polling)
---
## 🎯 Success Criteria
Check these metrics after launch:
### Week 1
- [ ] 10+ official tools listed
- [ ] All sync jobs running successfully
- [ ] Zero API errors
### Month 1
- [ ] 50+ official tools
- [ ] 5+ package authors using TPMJS
- [ ] <200ms API response time (p95)
### Month 3
- [ ] 200+ tools
- [ ] 20+ package authors
- [ ] Community contributions
### Month 6
- [ ] 1000+ tools
- [ ] 50+ active package authors
- [ ] Established as go-to AI tool registry
---
## 🔄 Ongoing Maintenance
Weekly:
- [ ] Check sync logs for errors
- [ ] Review new tools for quality
- [ ] Update documentation
Monthly:
- [ ] Database optimization (indexes, vacuum)
- [ ] Review and adjust quality scoring
- [ ] Update NPM_MIRROR.md with learnings
---
## 📚 Key Documents
**Read frequently during implementation:**
1. **NPM_MIRROR.md** - Complete architecture reference
- Database schema
- API specifications
- Validation rules
- Quality scoring
- All examples
2. **This checklist** - Implementation order and verification steps
3. **Plan file** - `.claude/plans/goofy-inventing-stearns.md` - Detailed planning notes
---
## 🚀 Ready to Build
This checklist is your complete implementation guide. Work through it phase by phase, checking off items as you go.
**Start with Phase 1, Step 1.1** and work sequentially. Each step has verification instructions to ensure it's working before moving on.
Good luck! 🎉

331
LAUNCH_REVIEW.md Normal file
View file

@ -0,0 +1,331 @@
# TPMJS Launch Review & Checklist
**STATUS: COMPLETED** - All critical issues have been fixed.
A comprehensive review of all public-facing content for Hacker News launch readiness.
---
## Executive Summary
**Overall Readiness: 7/10 - Needs Work Before Launch**
The website has excellent technical content and professional design, but fails the "5-second test" - a first-time visitor cannot quickly understand what TPMJS is or why they need it. The documentation is strong for existing users but assumes too much prior knowledge about AI agents and tooling.
### Critical Issues (Must Fix)
1. **Landing page doesn't explain what TPMJS is** - Hero section uses jargon without definition
2. **"Tool" vs "Package" never defined** - Core concepts assumed, not explained
3. **Knowledge gaps** - Assumes familiarity with AI agents, Zod, semantic search
4. **Category inconsistency** - HOW_TO_PUBLISH and NPM_MIRROR have different category lists
5. **NPM_MIRROR.md conflicts with other docs** - Appears outdated, creates confusion
### What's Working Well
- Publishing guide (HOW_TO_PUBLISH_A_TOOL.md) is excellent
- How It Works page has great technical depth
- Developer testimonials are concrete with real metrics
- No obvious AI-generated language on website
- Code examples are practical and well-placed
---
## The 5-Second Test: FAILED
**Question:** Can a developer understand what TPMJS is within 5 seconds of landing on the homepage?
**Answer:** No.
### What They See First
```
TOOL REGISTRY FOR AI AGENTS
Discover, share, and integrate tools that give your agents superpowers
```
### What's Missing
- What is a "tool" in this context?
- What is an "AI agent"?
- Why would I use this vs npm directly?
- Is this a package manager? A marketplace? An SDK?
### The "Aha Moment" is Unclear
A visitor still doesn't know:
- WHO should use TPMJS (tool builders? agent developers? both?)
- WHEN they would use it (at development time? runtime?)
- HOW it differs from regular npm packages
- WHY they can't just install packages normally
---
## Page-by-Page Clarity Ratings
| Page | Clarity | Human Feel | Issues |
|------|---------|------------|--------|
| **Landing Page** | 5/10 | Yes | No 5-second explanation, jargon-heavy |
| **Hero Section** | 3/10 | Yes | "Tool registry" undefined, circular language |
| **Problem Section** | 7/10 | Yes | Best section - concrete pain points |
| **Vision Section** | 5/10 | Yes | "Semantic search" unexplained |
| **Developer Stories** | 7/10 | Yes | Good metrics, but code unexplained |
| **Publish Section** | 6/10 | Yes | Assumes visitor is a tool builder |
| **How It Works** | 9/10 | Excellent | Minor density issues |
| **FAQ** | 8/10 | Yes | Missing some common questions |
| **Publish Guide** | 8.5/10 | Yes | Tier system could be clearer upfront |
| **Spec Page** | 8.5/10 | Yes | Assumes Zod/AI SDK knowledge |
| **Docs Page** | 9/10 | Excellent | Overwhelming length |
| **SDK Page** | 8.5/10 | Yes | Assumes Vercel AI SDK familiarity |
| **Privacy** | 8/10 | Yes | Hardcoded email address |
| **Terms** | 8/10 | Yes | Hardcoded date |
---
## Documentation Clarity Ratings
| Document | Clarity | Necessary | Critical Issues |
|----------|---------|-----------|-----------------|
| README.md | 8/10 | YES | Missing "what is TPMJS" explanation |
| HOW_TO_PUBLISH_A_TOOL.md | 9/10 | YES | Minor - excellent overall |
| DEPLOYMENT.md | 8/10 | YES | Confusing exit code explanation |
| QUALITY-GATES.md | 7/10 | OPTIONAL | Could merge into README |
| MANUAL_TOOLS.md | 8.5/10 | YES | Good for maintainers |
| NPM_MIRROR.md | 6.5/10 | **REMOVE** | **Conflicts with other docs, appears outdated** |
---
## Knowledge Gaps (Things Visitors Won't Understand)
### Not Explained Anywhere
1. **What is an "AI Agent"?** - The entire site assumes you know this
2. **What is a "Tool" vs a "Package"?** - Used interchangeably, never defined
3. **Why semantic search matters** - Just says "semantic" without explaining benefit
4. **What frameworks are supported** - Mentioned in FAQ but not prominently
5. **The Package → Tool relationship** - Can one package have multiple tools?
### Assumed Technical Knowledge
- Zod schemas (used throughout, never introduced)
- AI SDK tool format (referenced as "standard" but what standard?)
- esm.sh and Deno sandboxing (mentioned in How It Works)
- BM25 ranking algorithm (mentioned in docs)
### Missing Use Cases
- "Use TPMJS when..." section doesn't exist
- No comparison to alternatives (why not just npm?)
- No "before/after" showing the problem solved
---
## Human-Written Assessment
### Reads Like Human: YES ✓
- Developer stories use specific metrics ("500 lines to 3")
- Technical explanations show genuine understanding
- Problem section addresses real pain points
- No buzzword soup or meaningless marketing phrases
### Minor AI-Sounding Phrases Found
| Location | Phrase | Issue |
|----------|--------|-------|
| NPM_MIRROR.md:7 | "automated NPM-integrated registry" | Marketing speak |
| NPM_MIRROR.md:27 | "✨ Listed automatically" | Emoji in technical doc |
| NPM_MIRROR.md:500 | "Built with ❤️" | Remove emoji |
| HOW_TO_PUBLISH:389 | "AI-friendly descriptions" | Vague - what makes it "AI-friendly"? |
| Vision Section | "gives agents superpowers" | Metaphor without substance |
---
## Critical Inconsistencies Found
### Category Lists Don't Match
**HOW_TO_PUBLISH_A_TOOL.md says:**
```
text-analysis, code-generation, data-processing,
image-generation, audio-processing, search, integration, other
```
**NPM_MIRROR.md says:**
```
web-scraping, data-processing, file-operations, communication,
database, api-integration, image-processing, text-analysis,
automation, ai-ml, security, monitoring
```
**These are completely different!** Which is correct?
### Quality Score Formula Conflicts
- HOW_TO_PUBLISH: "Tier: Rich (1.0) > Basic (0.5) > Minimal (0.25)"
- MANUAL_TOOLS: "Rich tier tools get 4x quality score multiplier"
- NPM_MIRROR: Different formula entirely
### Field Names Inconsistent
- `exportName` used in MANUAL_TOOLS but not in HOW_TO_PUBLISH
- Deprecated fields (`parameters`, `returns`) mentioned but unclear when deprecated
---
## Hardcoded Values to Fix
| File | Issue | Line |
|------|-------|------|
| FAQ, Privacy, Terms | `thomasalwyndavis@gmail.com` hardcoded | Multiple |
| Privacy, Terms | Date "December 14, 2025" hardcoded | Multiple |
| Changelog page | Package list hardcoded in code | ~95-110 |
| Developer Stories | Fictional company names (Support.ai, DocFlow) | homePageData.ts |
---
## Launch Checklist
### Must Fix Before Launch (Blocking) - ALL DONE ✓
- [x] **Rewrite hero section** to explain TPMJS in one sentence
- Current: "TOOL REGISTRY FOR AI AGENTS"
- Suggested: "TPMJS lets AI agents discover and use npm packages as tools at runtime. Publish once to npm, get discovered automatically."
- [x] **Add "What is TPMJS?" section** to landing page
- Define: What is an AI agent?
- Define: What is a "tool" in this context?
- Explain: Why not just use npm directly?
- Show: 3-step "how it works" visual
- [x] **Reconcile category lists** between docs (deleted NPM_MIRROR.md)
- Pick one canonical list
- Update all docs to match
- Add categories to types package
- [x] **Delete or archive NPM_MIRROR.md** (deleted)
- Conflicts with HOW_TO_PUBLISH
- Appears to be old design doc, not current state
- Move to `/docs/internal/` if historical value
- [x] **Fix hardcoded values** (emails → hello@tpmjs.com, dates → December 2024)
- Email addresses → environment variable
- Dates → dynamic or remove
- Package lists → generated from filesystem
### Should Fix (High Priority) - MOSTLY DONE
- [x] **Add "Use TPMJS when..." section** to landing page (covered in "What is TPMJS?" section)
- List concrete scenarios: "Building a chatbot that needs web access"
- "Agent that processes different file formats"
- "Tool that should be discoverable by other agents"
- [x] **Explain Package vs Tool distinction** (covered in "What is TPMJS?" section)
- Add glossary or definitions section
- Clarify: 1 package can have N tools
- [x] **Add framework compatibility section** (mentioned in hero and publish sections)
- Which AI frameworks work with TPMJS?
- Are there adapters needed?
- Show code for each framework
- [ ] **Simplify developer stories code**
- Current code snippet unexplained:
```js
const agent = new Agent({ tools: await tpmjs.search(...) })
```
- Add: Where does `Agent` come from? What's happening here?
- [x] **Add README context** (completely rewritten with clear explanation)
- What is TPMJS for?
- Link to tpmjs.com
- Explain discovery mechanism
### Nice to Have (Post-Launch)
- [ ] Add video walkthrough (30-60 seconds)
- [ ] Interactive playground link from homepage
- [ ] "Compare to alternatives" section
- [ ] Case studies with real company names
- [ ] Quick links sidebar for docs page
- [ ] Status badges for each quality gate
---
## Recommended Hero Section Rewrite
### Current
```
TOOL REGISTRY FOR AI AGENTS
Discover, share, and integrate tools that give your agents superpowers
The registry for AI tools
```
### Suggested
```
MAKE YOUR AI AGENT SMARTER
TPMJS connects your AI agent to 2,500+ npm packages at runtime.
No config files. No manual imports. Just describe what you need.
"Find me a tool that can scrape websites" → Your agent gets web-scraper
"I need to process markdown" → Your agent gets markdown-formatter
Publish your npm package → It's discoverable by every AI agent in 15 minutes.
```
This version:
- Explains what it DOES (connects agents to npm packages)
- Shows HOW it works (natural language → tool)
- States the VALUE (no config, automatic discovery)
- Gives concrete examples
---
## Recommended "What is TPMJS?" Section
Add after hero, before featured tools:
```markdown
## What is TPMJS?
**The Problem:** AI agents need tools (web scraping, file processing, API calls)
but developers must manually configure each one. As the ecosystem grows,
this becomes unmanageable.
**The Solution:** TPMJS is a registry that automatically discovers npm packages
designed for AI agents. Agents can search for tools by description and load them
at runtime.
**For Tool Builders:** Add `tpmjs` keyword to your package.json.
Your tool appears on tpmjs.com within 15 minutes.
**For Agent Developers:** Use semantic search to find tools:
```javascript
import { searchRegistry } from '@tpmjs/sdk';
const tools = await searchRegistry('send emails and slack messages');
// Returns: email-sender, slack-notifier, ...
```
**One registry. Thousands of tools. Zero configuration.**
```
---
## Final Assessment
### Ready for Launch?
**Not yet.** The core product is solid but messaging fails first-time visitors.
### Estimated Fixes
- Hero rewrite: 30 minutes
- "What is TPMJS?" section: 1 hour
- Category reconciliation: 1 hour
- Hardcoded values: 30 minutes
- README updates: 30 minutes
- NPM_MIRROR cleanup: 15 minutes
**Total: ~4 hours of work**
### After Fixes
The site will be launch-ready. The technical content is excellent - it just needs a better front door.
---
## Appendix: Positive Highlights
Things that are already great and should NOT change:
1. **How It Works page** - Excellent technical depth, clear structure
2. **Publishing guide** - Best-in-class documentation, real examples
3. **Problem section** - Concrete pain points, relatable issues
4. **Spec page** - Clear field reference, good validation info
5. **SDK documentation** - Quick start is excellent
6. **Code examples throughout** - Practical, copy-pasteable
7. **Visual design** - Clean, professional, developer-focused
8. **Quality scoring explanation** - Transparent, well-documented

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-2025 TPMJS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

293
MANUAL_TOOLS.md Normal file
View 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

View file

@ -1,500 +0,0 @@
# TPMJS NPM-Integrated Registry Architecture
> **Automated tool discovery from NPM with zero-click submission**
## Vision
Transform TPMJS from a manual directory into an **automated NPM-integrated registry** where package authors simply publish to NPM with a `tpmjs` field in their `package.json` and their tools are discovered and listed within seconds—no manual submission, no forms, no waiting.
## Quick Start for Package Authors
```json
{
"name": "my-awesome-tool",
"version": "1.0.0",
"keywords": ["tpmjs-tool"],
"tpmjs": {
"category": "web-scraping",
"description": "Extract product data from e-commerce websites with ease",
"example": "const data = await scraper.extract('https://shop.com')"
}
}
```
```bash
npm publish
# ✨ Listed automatically within 15 minutes (keyword) or seconds (changes feed)
```
---
## Architecture Overview
```
NPM Ecosystem
Changes Feed + Keyword Search
Package Validator (Zod)
PostgreSQL Database
Next.js API Routes
TPMJS Web App
```
### Core Components
1. **NPM Sync Service** (Node.js) - Monitors NPM registry for new packages
2. **PostgreSQL Database** - Stores validated tool metadata
3. **Next.js API** - Serves tool data with search/filtering
4. **Web Frontend** - Browse, search, and discover tools
---
## Discovery Mechanism: Hybrid Approach
### Method 1: Keyword Search (Official)
- Search NPM for packages with `tpmjs-tool` keyword
- Runs every 15 minutes via cron
- Packages marked as "Official"
### Method 2: Changes Feed (Automatic)
- Monitors `replicate.npmjs.com/registry/_changes` in real-time
- Detects packages with `tpmjs` field instantly
- Packages marked as "Community" (unless they also have keyword)
### Why Hybrid?
- **Keywords** = Clear opt-in, queryable, respects NPM conventions
- **Changes Feed** = Real-time, catches packages without keywords
- **Together** = Best discoverability with fallback
---
## The "tpmjs" Field: Tiered Schema
### Minimal Tier (Required)
```json
{
"tpmjs": {
"category": "web-scraping",
"description": "Extract structured data from websites using CSS selectors",
"example": "const data = await tool.scrape({ url: 'https://example.com', selector: '.price' })"
}
}
```
**Categories:**
- web-scraping
- data-processing
- file-operations
- communication
- database
- api-integration
- image-processing
- text-analysis
- automation
- ai-ml
- security
- monitoring
### Rich Tier (Optional)
Extend with any of these optional fields:
```json
{
"tpmjs": {
// ... Required fields ...
"parameters": [
{
"name": "url",
"type": "string",
"description": "Target URL to scrape",
"required": true
}
],
"returns": {
"type": "object",
"description": "Extracted data matching the selector"
},
"authentication": {
"required": false,
"type": "api-key",
"envVar": "SCRAPER_API_KEY",
"docsUrl": "https://docs.example.com/auth"
},
"pricing": {
"model": "freemium",
"freeLimit": "100 requests/month",
"paidUrl": "https://example.com/pricing"
},
"frameworks": ["vercel-ai", "langchain", "llamaindex"],
"links": {
"documentation": "https://docs.example.com",
"playground": "https://example.com/try",
"repository": "https://github.com/user/repo"
},
"tags": ["web", "scraping", "html", "css"],
"status": "stable",
"aiAgent": {
"useCase": "Use when agent needs to extract data from websites",
"limitations": "Cannot handle JavaScript-heavy SPAs"
}
}
}
```
---
## Database Schema
### Tools Table
```sql
CREATE TABLE tools (
-- NPM Metadata
npm_package_name VARCHAR(214) UNIQUE NOT NULL,
npm_version VARCHAR(50) NOT NULL,
npm_published_at TIMESTAMP NOT NULL,
npm_description TEXT,
npm_repository JSONB,
npm_homepage TEXT,
npm_license VARCHAR(50),
-- TPMJS Metadata
category VARCHAR(50) NOT NULL,
description TEXT NOT NULL,
example TEXT NOT NULL,
parameters JSONB,
authentication JSONB,
pricing JSONB,
frameworks TEXT[],
links JSONB,
tags TEXT[],
status VARCHAR(20),
-- Discovery
discovery_method VARCHAR(20) NOT NULL, -- 'keyword' | 'changes-feed'
is_official BOOLEAN DEFAULT false,
tier VARCHAR(20) NOT NULL, -- 'minimal' | 'rich'
-- Metrics
npm_downloads_last_month INTEGER DEFAULT 0,
github_stars INTEGER DEFAULT 0,
quality_score DECIMAL(3,2), -- 0.00 to 1.00
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
---
## Sync Service Architecture
### Workers
**1. Changes Feed Worker**
- Connects to `replicate.npmjs.com/registry/_changes`
- Receives real-time change events
- Fetches package metadata for each change
- Checks for `tpmjs` field
- Validates and inserts to database
**2. Keyword Search Worker**
- Runs every 15 minutes (cron)
- Searches `/-/v1/search?text=keywords:tpmjs-tool`
- Processes all results
- Marks as "Official"
**3. Metrics Worker** (Optional Phase 4)
- Updates download counts from NPM API
- Fetches GitHub stars
- Calculates quality scores
### Package Processing Pipeline
```
1. Fetch package metadata from NPM
2. Extract `tpmjs` field from latest version
3. Validate against Zod schema
4. If valid → Insert/Update database
5. If invalid → Log error
6. If no field → Skip
```
---
## API Routes
### GET /api/tools
Search and list tools
**Query Parameters:**
- `q` - Search query
- `category` - Filter by category
- `official` - Only official tools (true/false)
- `limit` - Results per page (default 20)
- `offset` - Pagination offset
**Response:**
```json
{
"tools": [...],
"pagination": {
"total": 150,
"limit": 20,
"offset": 0,
"hasMore": true
}
}
```
### GET /api/tools/[id]
Get tool details by ID
### POST /api/tools/validate
Validate a `tpmjs` field before publishing
**Request:**
```json
{
"category": "web-scraping",
"description": "...",
"example": "..."
}
```
**Response:**
```json
{
"valid": true,
"tier": "minimal",
"errors": []
}
```
### GET /api/stats
Registry statistics
```json
{
"totalTools": 2847,
"officialTools": 150,
"categories": {
"web-scraping": 320,
"communication": 280,
...
}
}
```
---
## Quality Scoring Algorithm
Tools are scored 0.00 to 1.00 based on:
- **Base validity** (0.3) - Has valid schema
- **Tier** (0.1-0.2) - Rich tier > Minimal tier
- **NPM downloads** (0.2) - Based on monthly downloads
- **GitHub stars** (0.15) - Repository popularity
- **Documentation** (0.1) - Has docs URL
- **Example quality** (0.05) - Example length > 100 chars
Score is used for default sorting and quality indicators.
---
## Implementation Phases
### Phase 1: Foundation (Week 1-2)
- Set up PostgreSQL + Prisma
- Create Zod schemas in `@tpmjs/types`
- Build sync service structure
- Implement NPM API client
### Phase 2: Discovery (Week 2-3)
- Implement changes feed worker
- Implement keyword search worker
- Deploy sync service (Railway/Fly.io)
- Test with real packages
### Phase 3: API & Frontend (Week 3-4)
- Build Next.js API routes
- Update tool listing page
- Update tool detail pages
- Add validation endpoint
### Phase 4: Polish (Week 4-5)
- Add metrics worker
- Create documentation
- Build CLI validator
- Launch to community
### Phase 5: Enhancements (Post-Launch)
- Semantic search (embeddings)
- Usage analytics
- Tool recommendations
- GitHub Actions integration
---
## Infrastructure Requirements
### Sync Service
- **Platform:** Railway or Fly.io
- **Runtime:** Node.js 22+
- **Resources:** 512MB RAM, 1 CPU
- **Cost:** ~$5-10/month
### Database
- **Platform:** Neon Postgres (serverless)
- **Size:** Free tier (start), scale as needed
- **Backups:** Automatic with Neon
- **Cost:** Free tier available, ~$10-20/month for production
### Web App
- **Platform:** Vercel (existing)
- **No changes required**
---
## Monitoring & Health
### Metrics to Track
1. **Sync Health**
- Changes feed uptime
- Packages processed per hour
- Validation success rate
2. **Database**
- Total tools
- Official vs community ratio
- Tier distribution
3. **API**
- Request latency (p95 < 200ms)
- Search performance
- Error rates
### Alerts
- Sync service down > 5 minutes
- Database connection failures
- Validation error rate > 10%
---
## Developer Experience
### Validation Before Publishing
```bash
# Using TPMJS CLI (to be built)
npx tpmjs validate
# Or via API
curl -X POST https://tpmjs.com/api/tools/validate \
-H "Content-Type: application/json" \
-d '{"category":"web-scraping","description":"...","example":"..."}'
```
### Documentation Pages Needed
1. **Getting Started** - Adding TPMJS support
2. **Schema Reference** - Complete field docs
3. **Best Practices** - Tips for quality tools
4. **Examples** - Sample configurations
5. **FAQ** - Common questions
---
## Migration from Mock Data
### Current State
- 12 mock tools in `toolData.ts`
- Client-side search
- Hard-coded categories
### Migration Strategy
1. **Publish Real Packages**
- Create NPM packages for mock tools
- Add `tpmjs` fields
- Publish with `tpmjs-tool` keyword
2. **Update Frontend**
- Replace mock data with API calls
- Keep existing UI components
- Update types to match Prisma models
3. **Gradual Rollout**
- Dual mode (mock + real)
- Real data primary, mock fallback
- Remove mock entirely
---
## Success Metrics
### Technical
- ✓ Discovery latency < 60 seconds
- ✓ API response time < 200ms p95
- ✓ Support 10,000+ tools
- ✓ 99.9% uptime
### User Experience
- ✓ 0-click submission (automatic)
- ✓ Instant validation feedback
- ✓ <100ms search speed
- ✓ 100% mobile features
### Business
- Week 1: 10 official tools
- Month 1: 50 official tools
- Month 3: 200+ tools
- Month 6: 1000+ tools
- 50+ active package authors
---
## Comparison to Vercel's Approach
| Feature | Vercel AI SDK | TPMJS |
|---------|---------------|-------|
| **Submission** | Manual file edit + PR | Automatic via NPM |
| **Discovery** | None | Real-time changes feed |
| **Validation** | Manual review | Automated Zod schema |
| **Updates** | New PR required | Automatic on publish |
| **Search** | Static array | Full-text + categories |
| **Scale** | 6 tools | 1000+ tools ready |
---
## Next Steps
1. Review this architecture plan
2. Approve database schema and API design
3. Set up infrastructure (Railway + Postgres)
4. Start Phase 1: Foundation
5. Launch MVP in 4-5 weeks
---
## References
- [NPM Registry API Docs](https://github.com/npm/registry/blob/main/docs/REGISTRY-API.md)
- [NPM Changes Feed](https://github.com/npm/registry/blob/main/docs/REPLICATE-API.md)
- [Vercel AI Tools Registry](https://github.com/vercel/ai/blob/main/content/tools-registry/registry.ts)
- [TPMJS Architecture Plan](/.claude/plans/goofy-inventing-stearns.md) (Full details)
---
**Built with ❤️ for the AI agent ecosystem**

View file

@ -1,10 +1,59 @@
# TPMJS Monorepo
# TPMJS
[![CI](https://github.com/YOUR_ORG/tpmjs/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_ORG/tpmjs/actions/workflows/ci.yml)
[![CI](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml/badge.svg)](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
Tool Package Manager for AI Agents - A Turborepo monorepo with strict TypeScript, Next.js 16, and best practices.
**TPMJS is a registry for discovering AI tools published to npm.**
## Structure
Browse, search, and find tools at [tpmjs.com](https://tpmjs.com). Publish your tool by adding the `tpmjs` keyword to your package.json—it appears in the registry within 15 minutes.
## Why TPMJS?
- **Discover tools** - Search and browse AI tools by category, quality score, and popularity
- **Publish easily** - Add one keyword to package.json, publish to npm, done
- **Quality metrics** - Tools are scored based on documentation, downloads, and metadata completeness
- **Agent integration** - Optional SDK for agents to search and execute tools at runtime
## Quick Start
### Publishing a Tool
```bash
npx @tpmjs/create-basic-tools
```
Or add manually to your package.json:
```json
{
"keywords": ["tpmjs"],
"tpmjs": {
"category": "text-analysis"
}
}
```
Publish to npm and your tool appears on [tpmjs.com](https://tpmjs.com) within 15 minutes.
See [HOW_TO_PUBLISH_A_TOOL.md](./HOW_TO_PUBLISH_A_TOOL.md) for the full guide.
### For AI Agents (Optional)
Agents can search and execute tools from the registry:
```bash
npm install @tpmjs/registry-search @tpmjs/registry-execute
```
```typescript
import { registrySearchTool } from '@tpmjs/registry-search';
import { registryExecuteTool } from '@tpmjs/registry-execute';
// Add to your agent's tools
const tools = [registrySearchTool, registryExecuteTool];
```
---
## Monorepo Structure
```
apps/

391
TPMJS_TALK.md Normal file
View file

@ -0,0 +1,391 @@
# TPMJS: The Missing Layer Between "LLMs Can Call Tools" and "Which Tool, Exactly?"
---
## The Setup
You're building an AI agent. It needs to do things in the world—scrape a webpage, send an email, query a database, generate an image. These capabilities come from **tools**.
The problem isn't that tools don't exist. They do. Thousands of them. The problem is:
- **You can't find them.** npm has 2 million packages. Which ones are AI-callable tools? Which ones actually work?
- **You can't trust them.** No schema. No examples. README says "AI-ready" but the function signature is `(opts: any) => Promise<any>`.
- **You can't compare them.** Three packages do "web scraping." Which one handles JavaScript rendering? Which one returns structured data? Which one is maintained?
Discovery is the bottleneck. Not capability—discovery.
---
## What TPMJS Actually Is
TPMJS is infrastructure. Specifically:
1. **A registry** that indexes npm packages designed for AI tool use
2. **A metadata extraction pipeline** that pulls schemas directly from code
3. **A quality scoring system** that ranks tools by completeness and adoption
4. **A health monitoring system** that verifies tools actually work
5. **A playground** where you can test tools before integrating them
It's not magic. It's plumbing. Good plumbing.
---
## How It Works (The Technical Reality)
### Discovery: Finding Tools in the Wild
TPMJS runs three automated sync jobs:
**1. npm Changes Feed (every 2 minutes)**
```
npm registry → /_changes endpoint → filter for tpmjs keyword → process
```
This catches new packages and updates in near-real-time. We track sequence numbers so we never reprocess.
**2. Keyword Search (every 15 minutes)**
```
npm search "tpmjs" → up to 250 results → validate → ingest
```
Backup mechanism. Catches anything the changes feed missed.
**3. Metrics Sync (hourly)**
```
for each package → fetch download stats → recalculate quality scores → update health status
```
Keeps the registry fresh.
### The Publisher Contract
To get indexed, a package needs two things:
```json
{
"name": "@acme/my-tool",
"keywords": ["tpmjs"],
"tpmjs": {
"category": "web-scraping",
"description": "Scrapes URLs and returns structured markdown"
}
}
```
That's the minimum. Category + description. Everything else is either optional or auto-extracted.
**Categories are fixed** (12 total): web-scraping, data-processing, file-operations, communication, database, api-integration, image-processing, text-analysis, automation, ai-ml, security, monitoring.
Why fixed? Because agents need to filter. "Give me all database tools" has to mean something.
### Schema Extraction: The Hard Part
Here's what makes TPMJS different from a glorified npm search.
When we ingest a package, we don't just read the README. We **execute it in a sandbox** and extract the actual schema:
```
1. Spin up isolated executor (Railway)
2. npm install the package
3. Import and inspect exports
4. Extract JSON Schema from TypeScript types
5. Store schema in database
```
The result:
```json
{
"name": "scrapeUrl",
"inputSchema": {
"type": "object",
"properties": {
"url": { "type": "string", "format": "uri" },
"waitForSelector": { "type": "string" },
"timeout": { "type": "number", "default": 30000 }
},
"required": ["url"]
}
}
```
This isn't documentation. This is **extracted from the actual function signature**. It's ground truth.
If the author provides a schema in the `tpmjs` field, we use that. If not, we extract it. Either way, every tool in the registry has a schema.
### Quality Scoring: Ranking What Matters
Every tool gets a score from 0.00 to 1.00:
```typescript
// Base score from metadata completeness
const tierScore = tier === 'rich' ? 0.6 : 0.4;
// Adoption signals
const downloadsScore = Math.min(0.2, Math.log10(downloads + 1) / 15);
const starsScore = Math.min(0.1, Math.log10(githubStars + 1) / 10);
// Metadata richness bonus
let richnessScore = 0;
if (hasParameters) richnessScore += 0.04;
if (hasReturns) richnessScore += 0.03;
if (hasEnvVars) richnessScore += 0.03;
```
**Tier** is binary:
- **Minimal**: Just category + description (40% base)
- **Rich**: Has parameters, returns, env vars, or framework tags (60% base)
The formula is deliberately simple. We're not trying to be clever. We're trying to surface tools that are well-documented and actually used.
### Health Checks: Does It Actually Work?
Two checks, run during sync and periodically:
**1. Import Health**
```
Can we require() this package without it exploding?
```
You'd be surprised how many npm packages fail this.
**2. Execution Health**
```
Can we call the main function with minimal parameters without throwing?
```
Not a full test suite. Just "does it run at all?"
Results: `HEALTHY`, `BROKEN`, or `UNKNOWN`.
Broken tools still appear in the registry (with a warning). We don't hide them—we label them.
---
## The Data Model
Here's what we actually store:
### Package (npm package level)
```
npmPackageName (unique)
npmVersion, npmDescription, npmRepository, npmLicense
npmKeywords[], npmReadme, npmAuthor
category (enum)
tier ('minimal' | 'rich')
discoveryMethod ('changes-feed' | 'keyword')
npmDownloadsLastMonth, githubStars
frameworks[] (vercel-ai, langchain, etc.)
env[] (required environment variables)
```
### Tool (individual callable within a package)
```
packageId (FK)
name (export name: "scrapeUrl", "default", etc.)
description
inputSchema (JSON Schema)
schemaSource ('extracted' | 'author')
qualityScore (0.00-1.00)
importHealth, executionHealth (HEALTHY | BROKEN | UNKNOWN)
toolDiscoverySource ('auto' | 'manual')
```
One package can have multiple tools. `@acme/web-tools` might export `scrapeUrl`, `screenshotPage`, and `extractLinks`. Each is a separate tool with its own schema and health status.
### Simulation (playground execution)
```
toolId
userPrompt (what the user asked)
parameters (JSON, what was passed to the tool)
status (pending | running | success | error | timeout)
executionTimeMs, output, error
model, agentSteps
```
We track every playground execution. Not for surveillance—for debugging and improving the system.
---
## The API
### Search & Discovery
```
GET /api/tools
?q=scrape
&category=web-scraping
&importHealth=HEALTHY
&executionHealth=HEALTHY
&limit=20
&offset=0
→ Returns tools sorted by quality score
```
```
GET /api/tools/search
?q=I need to extract text from PDFs
→ BM25-ranked semantic search
```
### Execution
```
POST /api/tools/execute/{toolId}
{
"prompt": "Scrape the homepage of Hacker News",
"parameters": { "url": "https://news.ycombinator.com" }
}
→ Server-Sent Events stream with:
- Agent reasoning steps
- Tool call results
- Final output
```
Rate limited: 10 requests per IP per hour. We're not a free compute platform.
### Schema Operations
```
POST /api/tools/extract-schema
{ "packageName": "@acme/my-tool", "toolName": "scrapeUrl" }
→ Forces re-extraction of schema from source
```
---
## The Playground
A Next.js app where you can:
1. **Browse tools** by category, health status, quality score
2. **Inspect schemas** before you commit to anything
3. **Test execution** with an AI agent
4. **See real responses** with actual latency and token usage
It's not a demo. It's a debugging tool. "Does this tool do what I think it does?" Answer that question in 30 seconds instead of 30 minutes.
---
## What This Enables
### For Engineers Building Agents
Before TPMJS:
```
1. Search npm for "web scraper"
2. Get 500 results
3. Click through 20 of them
4. Read READMEs that say "easy to use!"
5. npm install three of them
6. Write test code for each
7. Find out two are broken
8. Pick the one that works
9. Hope it keeps working
```
After TPMJS:
```
1. Search tpmjs.com for "web scraper"
2. Filter by HEALTHY status
3. Sort by quality score
4. Click top result
5. See exact input schema
6. Test in playground
7. Integrate
```
### For Tool Authors
Before TPMJS:
```
Publish to npm → hope someone finds it → no visibility into usage
```
After TPMJS:
```
Publish to npm with tpmjs keyword → indexed within 2 minutes →
schema auto-extracted → quality scored → discoverable by search →
execution stats tracked
```
Your tool becomes findable. Not just by humans grepping npm, but by agents querying the registry API.
### For Agents (Yes, Really)
Agents can query TPMJS at runtime:
```typescript
const tools = await fetch('https://tpmjs.com/api/tools?' + new URLSearchParams({
q: 'send email',
executionHealth: 'HEALTHY',
limit: '5'
})).then(r => r.json());
// Agent now has 5 working email tools with full schemas
// It can pick the best one for this specific task
```
This is the endgame. Not humans browsing a registry—agents dynamically selecting tools based on capability, health, and fit.
---
## What TPMJS Is Not
**Not a package manager.** We don't host packages. npm does that. We index and enrich.
**Not an execution platform.** The playground runs tools for testing. Production execution is your responsibility.
**Not a security guarantee.** We check if tools work. We don't audit them for malice. Same rules as npm: don't run untrusted code.
**Not magic.** We're not using AI to understand what tools do. We're extracting schemas and running health checks. Boring, reliable, debuggable.
---
## The Technical Stack
- **Database**: PostgreSQL via Prisma
- **Web**: Next.js 16 (App Router)
- **Deployment**: Vercel (web) + Railway (sandbox executor)
- **Sync**: Vercel Cron + GitHub Actions backup
- **AI**: Vercel AI SDK for playground execution
- **Monorepo**: Turborepo + pnpm
Key internal packages:
- `@tpmjs/npm-client` — npm registry integration
- `@tpmjs/package-executor` — sandbox execution client
- `@tpmjs/types` — schema validation and migration
- `@tpmjs/db` — Prisma client and models
---
## Current State
- **~100 tools indexed** (and growing with every npm publish)
- **12 categories** covering most agent use cases
- **Automated sync** running 24/7
- **Health checks** on every tool
- **Schema extraction** working for TypeScript and JavaScript
- **Playground** functional for testing
---
## The Pitch (Finally)
Tools are the API surface of AI agents. The ecosystem is a mess. TPMJS is the index.
We don't compete with npm—we sit on top of it. We don't replace tool authors—we make them discoverable. We don't build agents—we give agents a way to find their tools.
Discovery is the bottleneck. We're fixing discovery.
---
## Try It
- **Browse**: https://tpmjs.com/tool-search
- **Playground**: https://tpmjs.com/playground
- **Publish**: Add `tpmjs` keyword + `tpmjs` field to your package.json
- **API**: `GET https://tpmjs.com/api/tools`
---
*Tools are inevitable. Discovery chaos isn't.*

View file

@ -1,324 +0,0 @@
# 🔥 Vercel API Routes Fix - Action Checklist
## Problem Summary
**API routes are not deploying because Vercel is not detecting your project as Next.js.**
When Vercel doesn't detect Next.js, it treats your deployment as a static site and **drops all App Router API routes** from the build output. Pages work because they're static files, but API routes require serverless function generation which only happens when Next.js is properly detected.
---
## ✅ Fix Checklist (Complete in Order)
### 1⃣ Fix Root Directory
**Where:** Vercel Dashboard → Project `tpmjs-web` → Settings → General → Root Directory
**Current (likely):** Empty, `.`, or wrong path
**Required:** `apps/web` (exactly this, no leading/trailing slashes)
**Validation:**
```
✓ Must be exactly: apps/web
✗ NOT: /apps/web/
✗ NOT: ./apps/web
✗ NOT: tpmjs/apps/web
```
---
### 2⃣ Set Framework Preset
**Where:** Vercel Dashboard → Project `tpmjs-web` → Settings → General → Framework Preset
**Current (likely):** "Other"
**Required:** "Next.js"
**Why this matters:**
- Framework = "Other" → Uses `@vercel/static-builder` → No API routes
- Framework = "Next.js" → Uses `@vercel/next` → API routes deployed
---
### 3⃣ Remove Domain from Old Project
**Where:** Vercel Dashboard → Project `v0-tool-registry-page` → Settings → Domains
**Action:** Remove these domains:
- `tpmjs.com`
- `www.tpmjs.com`
**Then:** Verify both domains are ONLY assigned to the `tpmjs` project
**Why this matters:**
The "Redirecting..." message is coming from the old project. Having two projects with the same domain causes shadow routing and API requests hitting the wrong deployment.
---
### 4⃣ Clear Custom Build Commands
**Where:** Vercel Dashboard → Project `tpmjs-web` → Settings → Build & Development Settings
**Set ALL to default/empty:**
```
Build Command: (empty - let Vercel auto-detect)
Install Command: (empty - let Vercel auto-detect)
Output Directory: .next (default)
```
**Why this matters:**
Custom build commands bypass Vercel's Next.js detection. Vercel should automatically:
- Detect monorepo structure
- Run `pnpm install`
- Run `pnpm build` in the correct workspace
- Use `@vercel/next` builder
**If you must use custom commands, use:**
```
Build Command: pnpm turbo run build --filter=@tpmjs/web
Install Command: pnpm install
```
But try empty first.
---
## 🧪 Verification Steps
### Before Deploying
Run this locally to confirm Next.js detection:
```bash
cd apps/web
vercel build
```
**Expected output should include:**
```
● route (app) /api/health
● route (app) /api/tools
● route (app) /api/sync/changes
λ /api/health
λ /api/tools
λ /api/sync/changes
```
**If you DON'T see this, Vercel won't deploy API routes.**
### After Deploying
1. **Check Build Output:**
```bash
vercel inspect <deployment-url>
```
Should show:
```
Builds
├── λ api/health (XXX KB) [region]
├── λ api/tools (XXX KB) [region]
├── λ api/sync/changes (XXX KB) [region]
├── λ tool/[slug] (XXX KB) [region]
...
```
2. **Test API Routes:**
```bash
# Should return JSON (not timeout, not "Redirecting...")
curl https://tpmjs.com/api/health
# Should return tool data
curl https://tpmjs.com/api/tools
```
---
## 📋 Expected Results
### ✅ Success Indicators
- [ ] `vercel inspect` shows API routes as `λ` functions
- [ ] `curl https://tpmjs.com/api/health` returns JSON
- [ ] `curl https://tpmjs.com/api/tools` returns tool data
- [ ] No "Redirecting..." messages
- [ ] No timeouts on direct Vercel URLs
- [ ] Build logs show "route (app) /api/*"
### ❌ Failure Indicators (Need to revisit steps)
- [ ] Only pages listed in `vercel inspect`, no API routes
- [ ] API endpoints return "Redirecting..."
- [ ] API endpoints timeout (exit code 28)
- [ ] Build logs don't mention API routes
- [ ] Framework Preset still shows "Other"
---
## 🚨 Common Mistakes
### Mistake 1: Wrong Root Directory Format
```
✗ /apps/web/ (leading/trailing slashes)
✗ ./apps/web (relative path notation)
✗ apps/web/ (trailing slash)
✓ apps/web (correct)
```
### Mistake 2: Leaving Custom Build Commands
If you have:
```json
{
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web"
}
```
This MIGHT work, but can break Next.js detection. Start with empty and only add if needed.
### Mistake 3: Not Removing Domain from Old Project
If `v0-tool-registry-page` still has `tpmjs.com`, your requests will route to the wrong project randomly based on:
- DNS propagation
- Edge cache
- Vercel's routing priority
### Mistake 4: Not Verifying Framework Preset
"Other" is Vercel's default when it can't detect a framework. This is the #1 cause of missing API routes in monorepos.
---
## 🔧 Troubleshooting
### If API routes STILL don't deploy after all 4 steps:
1. **Check Build Logs:**
- Does it say "Detected Next.js"?
- Does it list "route (app) /api/*"?
- Does it show `@vercel/next` builder?
2. **Check package.json location:**
```
✓ Should exist: apps/web/package.json
✗ Should NOT be at root ONLY
```
3. **Check next.config.ts location:**
```
✓ Should exist: apps/web/next.config.ts
```
4. **Verify pnpm workspace:**
```bash
# Should show @tpmjs/web
pnpm list --depth 0 --filter @tpmjs/web
```
5. **Test local build with Vercel CLI:**
```bash
cd apps/web
vercel build --debug
```
Look for "Framework: nextjs" in output.
---
## 📞 When to Contact Vercel Support
If after completing all 4 steps:
- Build logs show "Detected Next.js"
- Build logs show "route (app) /api/*"
- BUT `vercel inspect` still doesn't list API functions
Then you have a Vercel platform bug. Contact support with:
- This checklist
- Build logs
- `vercel inspect` output
- Link to `API_ROUTES_TIMEOUT_INVESTIGATION.md`
---
## 🎯 Quick Win Test
**Don't want to change production settings yet?**
1. Create a NEW Vercel project
2. Import the SAME repo
3. Set Root Directory to `apps/web`
4. Set Framework Preset to "Next.js"
5. Deploy
If API routes work in the new project → confirms the fix
If API routes still fail → deeper issue (contact support)
---
## ✨ Post-Fix Cleanup
Once API routes are working:
### Optional: Re-add www redirect
Now that API routes work, you can safely add the www redirect back:
**Option A - Vercel Project Settings:**
Vercel Dashboard → Domains → tpmjs.com → Redirect www to apex
**Option B - Next.js config:**
```typescript
// apps/web/next.config.ts
async redirects() {
return [
{
source: '/:path((?!api).*)*', // Exclude /api/*
has: [{ type: 'host', value: 'www.tpmjs.com' }],
destination: 'https://tpmjs.com/:path*',
permanent: true,
},
];
}
```
**Option C - vercel.json (not recommended):**
Only use if you understand the implications.
### Optional: Remove maxDuration exports
The `export const maxDuration = 60;` in your route files isn't needed unless you actually need longer timeouts. Default is 10s (Hobby) or 15s (Pro).
---
## 📊 Summary
| Issue | Root Cause | Fix |
|-------|------------|-----|
| API routes timeout | Vercel doesn't detect Next.js | Set Framework Preset to "Next.js" |
| No λ functions in build | Wrong Root Directory | Set to `apps/web` exactly |
| "Redirecting..." on API calls | Domain on two projects | Remove from old project |
| Build doesn't find API routes | Custom build commands break detection | Clear custom commands |
**Time to fix:** 5 minutes (just changing dashboard settings)
**Deployments needed:** 1 (changes take effect on next deploy)
**Code changes needed:** 0 (this is pure configuration)
---
## 🎉 When It Works
You'll know it's fixed when:
```bash
$ curl https://tpmjs.com/api/health
{"status":"ok","timestamp":"2025-11-28T...","env":{"hasDatabase":true,"nodeEnv":"production"}}
$ curl https://tpmjs.com/api/tools
{"data":[...],"pagination":{...}}
```
And `vercel inspect <url>` shows:
```
Builds
├── λ api/health
├── λ api/tools
├── λ api/stats
... (ALL your API routes)
```
**That's it. No code changes. Just fix the Vercel project configuration.**

67
alien.sh Executable file
View file

@ -0,0 +1,67 @@
#!/bin/bash
printf "\n\033[1m WALKING DOWN\033[0m\n\n"
printf " Frame 1 Frame 2 Frame 3 Frame 4\n\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;255;200;50m▄███▄\033[0m \033[38;2;255;200;50m▄███▄\033[0m \033[38;2;255;200;50m▄███▄\033[0m \033[38;2;255;200;50m▄███▄\033[0m\n"
printf " \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m\n"
printf " \033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[38;2;0;0;0m██\033[38;2;255;200;50m█\033[0m\n"
printf " \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m\n"
printf " \033[38;2;255;200;50m███\033[0m \033[38;2;255;200;50m███\033[0m \033[38;2;255;200;50m███\033[0m \033[38;2;255;200;50m███\033[0m\n"
printf " \033[38;2;139;90;43m▄█████▄\033[0m \033[38;2;139;90;43m▄█████▄\033[0m \033[38;2;139;90;43m▄█████▄\033[0m \033[38;2;139;90;43m▄█████▄\033[0m\n"
printf " \033[38;2;139;90;43m███\033[38;2;255;215;0m█\033[38;2;139;90;43m███\033[0m \033[38;2;139;90;43m███\033[38;2;255;215;0m█\033[38;2;139;90;43m███\033[0m \033[38;2;139;90;43m███\033[38;2;255;215;0m█\033[38;2;139;90;43m███\033[0m \033[38;2;139;90;43m███\033[38;2;255;215;0m█\033[38;2;139;90;43m███\033[0m\n"
printf " \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m\n"
printf " \033[38;2;139;90;43m█████\033[0m \033[38;2;139;90;43m█████\033[0m \033[38;2;139;90;43m█████\033[0m \033[38;2;139;90;43m█████\033[0m\n"
printf " \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m\n"
printf " \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m\n"
printf "\n\033[1m WALKING UP\033[0m\n\n"
printf " Frame 1 Frame 2 Frame 3 Frame 4\n\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;255;200;50m▄███▄\033[0m \033[38;2;255;200;50m▄███▄\033[0m \033[38;2;255;200;50m▄███▄\033[0m \033[38;2;255;200;50m▄███▄\033[0m\n"
printf " \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m\n"
printf " \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m \033[38;2;255;200;50m███████\033[0m\n"
printf " \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m\n"
printf " \033[38;2;255;200;50m███\033[0m \033[38;2;255;200;50m███\033[0m \033[38;2;255;200;50m███\033[0m \033[38;2;255;200;50m███\033[0m\n"
printf " \033[38;2;139;90;43m▄█████▄\033[0m \033[38;2;139;90;43m▄█████▄\033[0m \033[38;2;139;90;43m▄█████▄\033[0m \033[38;2;139;90;43m▄█████▄\033[0m\n"
printf " \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m\n"
printf " \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m \033[38;2;139;90;43m███████\033[0m\n"
printf " \033[38;2;139;90;43m█████\033[0m \033[38;2;139;90;43m█████\033[0m \033[38;2;139;90;43m█████\033[0m \033[38;2;139;90;43m█████\033[0m\n"
printf " \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m\n"
printf " \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m\n"
printf "\n\033[1m WALKING LEFT\033[0m\n\n"
printf " Frame 1 Frame 2 Frame 3 Frame 4\n\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;255;200;50m▄███\033[0m \033[38;2;255;200;50m▄███\033[0m \033[38;2;255;200;50m▄███\033[0m \033[38;2;255;200;50m▄███\033[0m\n"
printf " \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m\n"
printf " \033[38;2;0;0;0m██\033[38;2;255;200;50m███\033[0m \033[38;2;0;0;0m██\033[38;2;255;200;50m███\033[0m \033[38;2;0;0;0m██\033[38;2;255;200;50m███\033[0m \033[38;2;0;0;0m██\033[38;2;255;200;50m███\033[0m\n"
printf " \033[38;2;255;200;50m████\033[0m \033[38;2;255;200;50m████\033[0m \033[38;2;255;200;50m████\033[0m \033[38;2;255;200;50m████\033[0m\n"
printf " \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m██\033[0m\n"
printf " \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m\n"
printf " \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m\n"
printf " \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m\n"
printf " \033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[0m\n"
printf " \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m\n"
printf " \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m\n"
printf "\n\033[1m WALKING RIGHT\033[0m\n\n"
printf " Frame 1 Frame 2 Frame 3 Frame 4\n\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m \033[38;2;139;90;43m█\033[0m\n"
printf " \033[38;2;255;200;50m███▄\033[0m \033[38;2;255;200;50m███▄\033[0m \033[38;2;255;200;50m███▄\033[0m \033[38;2;255;200;50m███▄\033[0m\n"
printf " \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m \033[38;2;255;200;50m█████\033[0m\n"
printf " \033[38;2;255;200;50m███\033[38;2;0;0;0m██\033[0m \033[38;2;255;200;50m███\033[38;2;0;0;0m██\033[0m \033[38;2;255;200;50m███\033[38;2;0;0;0m██\033[0m \033[38;2;255;200;50m███\033[38;2;0;0;0m██\033[0m\n"
printf " \033[38;2;255;200;50m████\033[0m \033[38;2;255;200;50m████\033[0m \033[38;2;255;200;50m████\033[0m \033[38;2;255;200;50m████\033[0m\n"
printf " \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m██\033[0m\n"
printf " \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m\n"
printf " \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[38;2;255;215;0m█\033[38;2;139;90;43m██\033[0m\n"
printf " \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m \033[38;2;139;90;43m████\033[0m\n"
printf " \033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[0m \033[38;2;139;90;43m██\033[0m\n"
printf " \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m██\033[0m \033[38;2;255;200;50m█\033[0m \033[38;2;255;200;50m█\033[0m\n"
printf " \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m█\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m██\033[0m \033[38;2;101;67;33m█\033[0m\n"
printf "\n"

View file

@ -0,0 +1,2 @@
# Required: OpenAI API key
OPENAI_API_KEY=sk-...

View file

@ -0,0 +1,16 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default [
{
ignores: [
'.next/**',
'.turbo/**',
'node_modules/**',
'*.config.js',
'*.config.ts',
'next-env.d.ts',
'eslint.config.mjs',
],
},
...reactConfig,
];

6
apps/playground/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/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

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

View file

@ -0,0 +1,50 @@
{
"name": "@tpmjs/playground",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"type-check": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
},
"dependencies": {
"@ai-sdk/openai": "3.0.1",
"@ai-sdk/react": "3.0.3",
"@tpmjs/db": "workspace:*",
"@tpmjs/env": "workspace:*",
"@tpmjs/hello": "workspace:*",
"@tpmjs/search-registry": "workspace:*",
"@tpmjs/types": "workspace:*",
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",
"@vercel/analytics": "^1.6.1",
"ai": "6.0.3",
"firecrawl-aisdk": "^0.7.2",
"nanoid": "^5.1.6",
"next": "^16.0.8",
"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": {
"@tailwindcss/typography": "^0.5.19",
"@tpmjs/eslint-config": "workspace:*",
"@tpmjs/tailwind-config": "workspace:*",
"@tpmjs/tsconfig": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.4",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,8 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

View file

@ -0,0 +1,252 @@
import { createOpenAI } from '@ai-sdk/openai';
import { searchTpmjsToolsTool } from '@tpmjs/search-registry';
import { type UIMessage, convertToModelMessages, stepCountIs, streamText } from 'ai';
import type { NextRequest } from 'next/server';
import { env } from '~/env';
import {
addConversationTools,
loadToolsBatch,
setConversationEnv,
} from '~/lib/dynamic-tool-loader';
import { loadAllTools, sanitizeToolName } from '~/lib/tool-loader';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300; // 5 minutes for complex tool loading
// Add conversation state tracking (in-memory for MVP)
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const conversationStates = new Map<string, { loadedTools: Record<string, any> }>();
/**
* POST /api/chat
* Chat with AI agent that can execute TPMJS tools
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
console.log('📥 Request body:', JSON.stringify(body, null, 2));
const messages: UIMessage[] = body.messages || [];
const conversationId: string = body.conversationId || 'default';
const clientEnv: Record<string, string> = body.env || {};
console.log(`🔑 Conversation ID: ${conversationId}`);
console.log(
`🔐 Client env vars: ${Object.keys(clientEnv).length} keys`,
Object.keys(clientEnv)
);
// Store env vars for this conversation (so cached tools can access them)
setConversationEnv(conversationId, clientEnv);
// Initialize OpenAI with client-provided or server API key
const apiKey = clientEnv.OPENAI_API_KEY || env.OPENAI_API_KEY;
if (!apiKey) {
return new Response(
JSON.stringify({
success: false,
error: 'OPENAI_API_KEY is required. Please add it in the Settings sidebar.',
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
const openai = createOpenAI({
apiKey,
});
// Get or create conversation state
if (!conversationStates.has(conversationId)) {
console.log('✨ Creating new conversation state');
conversationStates.set(conversationId, { loadedTools: {} });
}
// biome-ignore lint/style/noNonNullAssertion: We just ensured the value exists above
const state = conversationStates.get(conversationId)!;
console.log(
`📊 Current loaded tools in conversation: ${Object.keys(state.loadedTools).length}`
);
// 1. Load static tools + search tool
const staticTools = await loadAllTools();
console.log(`🔧 Loaded ${Object.keys(staticTools).length} static tools`);
staticTools.searchTpmjsTools = searchTpmjsToolsTool;
console.log('✅ Added searchTpmjsTools to static tools');
// Debug: Check the search tool structure
console.log('🔍 Search tool structure:', {
description: searchTpmjsToolsTool.description,
inputSchema: typeof searchTpmjsToolsTool.inputSchema,
execute: typeof searchTpmjsToolsTool.execute,
});
// 2. Extract user query and last 3 user messages for tool search
const lastMessage = messages[messages.length - 1];
let userQuery = '';
if (lastMessage?.role === 'user') {
// Extract text from message parts
const parts = (lastMessage as any).parts || [];
for (const part of parts) {
if (part.type === 'text') {
userQuery = part.text;
break;
}
}
}
// Get last 3 user messages for context
const recentUserMessages = messages
.filter((msg) => msg.role === 'user')
.slice(-3)
.map((msg) => {
// Extract text from parts
const parts = (msg as any).parts || [];
for (const part of parts) {
if (part.type === 'text') {
return part.text;
}
}
return '';
})
.filter(Boolean);
console.log(`💬 User query: "${userQuery}"`);
console.log(`📝 Recent messages: ${recentUserMessages.length}`);
// 3. Automatically search for relevant tools based on the user's message
if (userQuery && userQuery.trim().length > 0) {
console.log('🔎 Searching for relevant tools...');
try {
// biome-ignore lint/style/noNonNullAssertion: Tool created with tool() always has execute
const result = await searchTpmjsToolsTool.execute!(
{
query: userQuery,
limit: 5, // Get top 5 relevant tools
recentMessages: recentUserMessages,
},
{} as any
);
// Type assertion: searchTpmjsToolsTool returns direct result, not AsyncIterable
const searchResult = result as {
query: string;
matchCount: number;
tools: any[];
};
console.log(`📦 Found ${searchResult.matchCount} matching tools`);
if (searchResult.tools && searchResult.tools.length > 0) {
console.log(
'🔧 Tools found:',
searchResult.tools.map((t: any) => `${t.packageName}/${t.name}`)
);
// Dynamically load tools from esm.sh
console.log(`📥 Loading ${searchResult.tools.length} tools dynamically...`);
const toolsToLoad = searchResult.tools.map((meta: any) => ({
packageName: meta.packageName,
name: meta.name,
version: meta.version,
importUrl: meta.importUrl,
}));
try {
const loadedTools = await loadToolsBatch(toolsToLoad, conversationId, clientEnv);
console.log(`✅ Successfully loaded ${Object.keys(loadedTools).length} tools`);
// Add sanitized tools to conversation state
for (const [key, tool] of Object.entries(loadedTools)) {
const [pkg, exp] = key.split('::');
const sanitizedKey = sanitizeToolName(`${pkg}-${exp}`);
state.loadedTools[sanitizedKey] = tool;
console.log(`✅ Added to conversation: ${sanitizedKey}`);
}
// Track for this conversation
addConversationTools(conversationId, Object.keys(state.loadedTools));
} catch (error) {
console.error('❌ Error loading tools:', error);
}
} else {
console.log(' No matching tools found for this query');
}
} catch (error) {
console.error('❌ Error searching for tools:', error);
}
}
// 4. Merge with conversation's dynamically loaded tools
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const allTools: Record<string, any> = { ...staticTools, ...state.loadedTools };
// 5. Build system prompt with available tools
const toolsList = Object.keys(allTools)
.map((name) => {
const tool = allTools[name] as { description?: string } | undefined;
return `- ${name}: ${tool?.description || 'No description'}`;
})
.join('\n');
const system = `You are an AI assistant with access to a dynamic tool registry containing thousands of tools. Your job is to EXECUTE tools to help users accomplish tasks.
## Tool Execution Rules
1. **When a user asks you to "call", "use", "run", or "execute" a tool** - you MUST invoke that tool immediately. Do not just describe it or search for it.
2. **When a user asks a question that could be answered by a tool** - invoke the appropriate tool to get real data, don't make up answers.
3. **searchTpmjsTools is for DISCOVERY only** - use it when you need to find tools you don't have loaded yet. Once a tool is loaded (listed below), call it directly.
4. **Tool names are sanitized** - if user says "extractTool from @parallel-web/ai-sdk-tools", look for a loaded tool like "parallel-web_ai-sdk-tools-extractTool".
5. **Always execute, then explain** - after calling a tool, summarize the results for the user.
## Currently Loaded Tools
${toolsList}
## Examples
User: "call extractTool on https://example.com"
Invoke the extractTool with url parameter, then explain results
User: "search for web scraping tools"
Use searchTpmjsTools to find tools, then tell user what's available
User: "what's the weather in Tokyo"
Search for a weather tool, load it, then invoke it
Remember: Your value is in EXECUTING tools to get real results, not just describing what tools could do.`;
// 6. Stream response with all available tools
const result = streamText({
model: openai('gpt-4o-mini'),
system,
messages: await convertToModelMessages(messages),
tools: allTools,
stopWhen: stepCountIs(5), // Allow model to call tools AND generate text response
});
// Return UI message stream with tool calls and text
return result.toUIMessageStreamResponse();
} catch (error) {
console.error('Chat API error:', error);
return new Response(
JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}

View file

@ -0,0 +1,85 @@
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
interface RawTool {
id: string;
name: string;
description: string;
qualityScore: number;
importHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
healthCheckError: string | null;
lastHealthCheck: string | null;
package?: {
npmPackageName: string;
npmVersion: string;
category: string;
frameworks: string[];
env: Array<{ name: string; description: string; required?: boolean; default?: string }>;
};
}
function transformTool(tool: RawTool) {
return {
toolId: tool.id,
packageName: tool.package?.npmPackageName,
name: tool.name,
description: tool.description,
category: tool.package?.category,
version: tool.package?.npmVersion,
qualityScore: tool.qualityScore,
frameworks: tool.package?.frameworks,
env: tool.package?.env,
importUrl: `https://esm.sh/${tool.package?.npmPackageName}@${tool.package?.npmVersion}`,
importHealth: tool.importHealth,
executionHealth: tool.executionHealth,
healthCheckError: tool.healthCheckError,
lastHealthCheck: tool.lastHealthCheck,
};
}
export async function GET() {
try {
const baseUrl = process.env.TPMJS_API_URL || 'https://tpmjs.com';
const allTools: RawTool[] = [];
let offset = 0;
const limit = 50; // Max allowed by the API
let hasMore = true;
// Paginate through all tools
while (hasMore) {
const response = await fetch(`${baseUrl}/api/tools?limit=${limit}&offset=${offset}`);
if (!response.ok) {
throw new Error(`Failed to fetch tools: ${response.statusText}`);
}
const data = await response.json();
const tools = data.data || [];
allTools.push(...tools);
hasMore = data.pagination?.hasMore ?? false;
offset += limit;
// Safety limit to prevent infinite loops
if (offset > 1000) break;
}
return NextResponse.json({
success: true,
tools: allTools.map(transformTool),
total: allTools.length,
});
} catch (error) {
console.error('Failed to fetch tools:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch tools',
},
{ status: 500 }
);
}
}

View file

@ -0,0 +1,30 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Light mode (default) */
:root {
/* Status Colors */
--error: 0 65% 51%; /* Red */
--error-foreground: 0 0% 100%; /* White text */
--warning: 36 100% 50%; /* Amber */
--warning-foreground: 0 0% 100%;
--success: 152 57% 45%; /* Green */
--success-foreground: 0 0% 100%;
--info: 210 100% 56%; /* Blue */
--info-foreground: 0 0% 100%;
}
/* Dark mode */
.dark {
--error: 0 65% 58%; /* Brighter red for dark mode */
--error-foreground: 0 0% 100%;
--warning: 36 100% 55%;
--warning-foreground: 0 0% 100%;
--success: 152 57% 50%;
--success-foreground: 0 0% 100%;
--info: 210 100% 60%;
--info-foreground: 0 0% 100%;
}
}

View file

@ -0,0 +1,44 @@
import { Analytics } from '@vercel/analytics/next';
import type { Metadata } from 'next';
import { ThemeProvider } from 'next-themes';
import { Space_Grotesk, Space_Mono } from 'next/font/google';
import './globals.css';
const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap',
});
const spaceMono = Space_Mono({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-mono',
display: 'swap',
});
export const metadata: Metadata = {
title: 'TPMJS Playground - Test AI Tools',
description: 'Interactive playground for testing TPMJS tools with AI agents',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): React.ReactElement {
return (
<html
lang="en"
suppressHydrationWarning
className={`${spaceGrotesk.variable} ${spaceMono.variable}`}
>
<body>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
{children}
</ThemeProvider>
<Analytics />
</body>
</html>
);
}

View file

@ -0,0 +1,24 @@
'use client';
import { ChatHeader } from '~/components/chat/ChatHeader';
import { ChatInterface } from '~/components/chat/ChatInterface';
import { SettingsSidebar } from '~/components/sidebar/SettingsSidebar';
import { ToolsSidebar } from '~/components/sidebar/ToolsSidebar';
export default function PlaygroundPage(): React.ReactElement {
const handleClearChat = () => {
// Refresh the page to clear chat
window.location.reload();
};
return (
<div className="flex h-screen flex-col bg-background">
<ChatHeader onClear={handleClearChat} />
<div className="flex flex-1 overflow-hidden">
<ToolsSidebar />
<ChatInterface />
<SettingsSidebar />
</div>
</div>
);
}

View file

@ -0,0 +1,52 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { useTheme } from 'next-themes';
import Link from 'next/link';
import { useEffect, useState } from 'react';
interface ChatHeaderProps {
onClear: () => void;
}
export function ChatHeader({ onClear }: ChatHeaderProps): React.ReactElement {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// Avoid hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
return (
<header className="border-b border-border bg-surface px-6 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<h1 className="text-lg font-bold tracking-tight">TPMJS Playground</h1>
<Link
href="https://tpmjs.com"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-foreground-tertiary transition-colors hover:text-foreground"
>
View Registry
</Link>
</div>
<div className="flex items-center gap-1">
{mounted && (
<Button variant="ghost" onClick={toggleTheme} size="sm">
{theme === 'dark' ? '☀️' : '🌙'}
</Button>
)}
<Button variant="ghost" onClick={onClear} size="sm">
Clear Chat
</Button>
</div>
</div>
</header>
);
}

View file

@ -0,0 +1,63 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import type { FormEvent } from 'react';
interface ChatInputProps {
input: string;
isLoading: boolean;
onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
setInput: (value: string) => void;
}
export function ChatInput({
input,
isLoading,
onInputChange,
onSubmit,
}: ChatInputProps): React.ReactElement {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim() && !isLoading) {
// Trigger form submission
const form = e.currentTarget.form;
if (form) {
form.requestSubmit();
}
}
}
};
return (
<form onSubmit={onSubmit} className="border-t border-border bg-surface p-4">
<div className="mx-auto max-w-3xl">
<div className="flex items-end gap-3 rounded-xl border border-border bg-background p-3 shadow-sm">
<Textarea
value={input}
onChange={onInputChange}
onKeyDown={handleKeyDown}
placeholder="Ask me to tell you a fish joke..."
className="min-h-[44px] max-h-[200px] flex-1 resize-none border-0 bg-transparent p-0 focus:ring-0"
disabled={isLoading}
rows={1}
/>
<Button
type="submit"
disabled={!input.trim() || isLoading}
loading={isLoading}
size="md"
className="shrink-0"
>
Send
</Button>
</div>
<p className="mt-2 text-center text-xs text-foreground-tertiary">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</form>
);
}

View file

@ -0,0 +1,38 @@
'use client';
import { useState } from 'react';
import { useChat } from '~/hooks/useChat';
import { ChatInput } from './ChatInput';
import { ChatMessages } from './ChatMessages';
export function ChatInterface(): React.ReactElement {
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState('');
const isLoading = status !== 'ready';
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (input.trim() && status === 'ready') {
sendMessage({ text: input });
setInput('');
}
};
return (
<div className="flex flex-1 flex-col">
<ChatMessages messages={messages} isStreaming={isLoading} />
<ChatInput
input={input}
isLoading={isLoading}
onInputChange={handleInputChange}
onSubmit={handleSubmit}
setInput={setInput}
/>
</div>
);
}

View file

@ -0,0 +1,60 @@
'use client';
import type { UIMessage } from 'ai';
import { useEffect, useRef } from 'react';
import { MessageBubble } from './MessageBubble';
interface ChatMessagesProps {
messages: UIMessage[];
isStreaming?: boolean;
}
export function ChatMessages({
messages,
isStreaming = false,
}: ChatMessagesProps): React.ReactElement {
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
});
if (messages.length === 0) {
return (
<div className="flex h-full items-center justify-center p-8">
<div className="max-w-md text-center">
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-surface">
<span className="text-3xl">🐟</span>
</div>
<h2 className="mb-2 text-lg font-semibold text-foreground">Test TPMJS Tools</h2>
<p className="mb-4 text-sm text-foreground-secondary">
Select tools from the sidebar and start chatting to test them.
</p>
<div className="rounded-lg border border-dashed border-border bg-surface/50 p-4">
<p className="text-xs text-foreground-tertiary">
Try: &ldquo;Tell me a fish joke&rdquo; or &ldquo;What tools are available?&rdquo;
</p>
</div>
</div>
</div>
);
}
return (
<div className="flex-1 space-y-4 overflow-y-auto p-4">
{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>
);
}

View file

@ -0,0 +1,225 @@
'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import type { UIMessage } from 'ai';
import { useEffect, useRef, useState } from 'react';
import { Streamdown } from 'streamdown';
interface PartTiming {
startTime: number;
endTime?: number;
duration?: number;
}
interface MessageBubbleProps {
message: UIMessage;
isStreaming?: boolean;
}
function formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
}
return `${(ms / 1000).toFixed(2)}s`;
}
export function MessageBubble({
message,
isStreaming = false,
}: MessageBubbleProps): React.ReactElement {
const isUser = message.role === 'user';
// Track timing for each part by index
const [partTimings, setPartTimings] = useState<Map<string, PartTiming>>(new Map());
const prevPartsRef = useRef<string>('');
// Track part appearances and completions
useEffect(() => {
if (!message.parts || isUser) return;
const currentPartsKey = JSON.stringify(
message.parts.map((p: any) => ({
type: p.type,
id: p.toolCallId || p.type,
state: p.state,
textLen: p.text?.length,
}))
);
// Only process if parts changed
if (currentPartsKey === prevPartsRef.current) return;
prevPartsRef.current = currentPartsKey;
const now = Date.now();
setPartTimings((prev) => {
const updated = new Map(prev);
message.parts?.forEach((part: any, idx: number) => {
const partKey = part.toolCallId || `${part.type}-${idx}`;
const existing = updated.get(partKey);
// Skip step-start markers
if (part.type === 'step-start') return;
if (!existing) {
// New part - record start time
updated.set(partKey, { startTime: now });
} else if (!existing.endTime) {
// Check if part is complete
const isToolComplete = part.type.startsWith('tool-') && part.state === 'result';
const isTextComplete = part.type === 'text' && !isStreaming;
if (isToolComplete || isTextComplete) {
updated.set(partKey, {
...existing,
endTime: now,
duration: now - existing.startTime,
});
}
}
});
return updated;
});
}, [message.parts, isStreaming, isUser]);
// Get timing for a specific part
const getPartTiming = (part: any, idx: number): PartTiming | undefined => {
const partKey = part.toolCallId || `${part.type}-${idx}`;
return partTimings.get(partKey);
};
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
<div
className={`w-full max-w-3xl rounded-xl border p-4 ${
isUser ? 'border-foreground/10 bg-foreground/5' : 'border-border bg-background shadow-sm'
}`}
>
<div className="mb-3 flex items-center gap-2">
<div
className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${
isUser ? 'bg-foreground text-background' : 'bg-surface text-foreground-secondary'
}`}
>
{isUser ? 'Y' : 'AI'}
</div>
<span className="text-xs font-medium text-foreground-secondary">
{isUser ? 'You' : 'Assistant'}
</span>
<span className="text-xs text-foreground-tertiary">
{new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
{/* Render message parts */}
{message.parts && message.parts.length > 0 ? (
<div className="space-y-4">
{message.parts.map((part, idx) => {
// Skip step-start markers
if (part.type === 'step-start') {
return null;
}
// Render text parts with markdown support
if (part.type === 'text') {
const timing = getPartTiming(part, idx);
return (
<div key={`text-${message.id}-${idx}`} className="text-sm leading-relaxed">
<Streamdown isAnimating={isStreaming && !isUser}>{part.text || ''}</Streamdown>
{timing?.duration && (
<div className="mt-2 text-xs text-foreground-tertiary">
{formatDuration(timing.duration)}
</div>
)}
</div>
);
}
// Render tool calls (type starts with 'tool-')
if (part.type.startsWith('tool-')) {
const toolName = part.type.replace('tool-', '');
// Type assertion for tool parts
const toolPart = part as any;
const timing = getPartTiming(part, idx);
const isResult = toolPart.state === 'result';
const hasError = toolPart.errorText || toolPart.error;
return (
<div
key={toolPart.toolCallId || idx}
className={`rounded-lg border p-3 ${
hasError
? 'border-red-500/30 bg-red-500/5'
: 'border-amber-500/30 bg-amber-500/5'
}`}
>
<div className="mb-2 flex items-center gap-2">
<span className="text-sm">🔧</span>
<code className="text-sm font-medium">{toolName}</code>
<Badge
variant={hasError ? 'error' : isResult ? 'success' : 'secondary'}
size="sm"
>
{hasError ? 'error' : toolPart.state || 'running'}
</Badge>
{timing?.duration && (
<span className="ml-auto text-xs text-foreground-tertiary">
{formatDuration(timing.duration)}
</span>
)}
</div>
{/* Tool Input */}
{toolPart.input && (
<details className="group">
<summary className="cursor-pointer text-xs font-medium text-foreground-secondary hover:text-foreground">
Input
</summary>
<pre className="mt-2 overflow-x-auto rounded-md bg-surface p-2 text-xs text-foreground-secondary">
{JSON.stringify(toolPart.input, null, 2)}
</pre>
</details>
)}
{/* Tool Output */}
{toolPart.output && (
<details className="group mt-2" open>
<summary className="cursor-pointer text-xs font-medium text-foreground-secondary hover:text-foreground">
Output
</summary>
<pre className="mt-2 max-h-48 overflow-auto rounded-md bg-surface p-2 text-xs text-foreground-secondary">
{JSON.stringify(toolPart.output, null, 2)}
</pre>
</details>
)}
{/* Tool Error */}
{hasError && (
<div className="mt-2">
<div className="mb-1 text-xs font-medium text-red-500">Error</div>
<pre className="overflow-x-auto rounded-md bg-red-500/10 p-2 text-xs text-red-400">
{typeof (toolPart.errorText || toolPart.error) === 'string'
? toolPart.errorText || toolPart.error
: JSON.stringify(toolPart.errorText || toolPart.error, null, 2)}
</pre>
</div>
)}
</div>
);
}
return null;
})}
</div>
) : (
// Fallback if no parts
<div className="text-sm">
<Streamdown isAnimating={isStreaming && !isUser}>...</Streamdown>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,193 @@
'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { Input } from '@tpmjs/ui/Input/Input';
import { useEffect, useState } from 'react';
interface EnvVar {
key: string;
value: string;
}
const ENV_STORAGE_KEY = 'tpmjs-playground-env-vars';
export function SettingsSidebar(): React.ReactElement {
const [envVars, setEnvVars] = useState<EnvVar[]>([]);
const [newKey, setNewKey] = useState('');
const [newValue, setNewValue] = useState('');
// Load env vars from localStorage on mount
useEffect(() => {
try {
const stored = localStorage.getItem(ENV_STORAGE_KEY);
if (stored) {
setEnvVars(JSON.parse(stored));
}
} catch (error) {
console.error('Failed to load env vars from localStorage:', error);
}
}, []);
// Save env vars to localStorage whenever they change
useEffect(() => {
try {
localStorage.setItem(ENV_STORAGE_KEY, JSON.stringify(envVars));
// Dispatch custom event so other components can react to changes
window.dispatchEvent(new CustomEvent('env-vars-updated', { detail: envVars }));
} catch (error) {
console.error('Failed to save env vars to localStorage:', error);
}
}, [envVars]);
const handleAddEnvVar = () => {
if (!newKey.trim()) return;
// Check if key already exists
const exists = envVars.some((env) => env.key === newKey);
if (exists) {
// Update existing
setEnvVars(
envVars.map((env) => (env.key === newKey ? { key: newKey, value: newValue } : env))
);
} else {
// Add new
setEnvVars([...envVars, { key: newKey, value: newValue }]);
}
setNewKey('');
setNewValue('');
};
const handleRemoveEnvVar = (key: string) => {
setEnvVars(envVars.filter((env) => env.key !== key));
};
return (
<aside className="hidden w-72 border-l border-border bg-surface md:block">
<div className="flex h-full flex-col">
{/* Header */}
<div className="border-b border-border p-4">
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground-secondary">
Settings
</h2>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{/* Environment Variables Section */}
<div className="mb-4">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground-tertiary">
Environment Variables
</h3>
<Badge variant="secondary" size="sm">
{envVars.length}
</Badge>
</div>
<p className="mb-4 text-xs text-foreground-tertiary">
Add API keys for tools that require authentication.
</p>
{/* Add new env var form */}
<div className="mb-4 space-y-2">
<Input
type="text"
placeholder="Key (e.g., FIRECRAWL_API_KEY)"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
className="font-mono text-xs"
/>
<Input
type="password"
placeholder="Value"
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
className="font-mono text-xs"
/>
<Button onClick={handleAddEnvVar} size="sm" variant="default" className="w-full">
Add Variable
</Button>
</div>
{/* List of env vars */}
<div className="space-y-2">
{envVars.length === 0 ? (
<div className="rounded-lg border border-dashed border-border p-4 text-center">
<p className="text-xs text-foreground-tertiary">No variables configured</p>
</div>
) : (
envVars.map((env) => (
<div
key={env.key}
className="flex items-center justify-between rounded-lg border border-border bg-background p-3"
>
<div className="flex-1 overflow-hidden">
<p className="truncate font-mono text-xs font-medium">{env.key}</p>
<p className="truncate font-mono text-xs text-foreground-tertiary">
{env.value ? '•'.repeat(Math.min(env.value.length, 16)) : '(empty)'}
</p>
</div>
<Button
onClick={() => handleRemoveEnvVar(env.key)}
size="sm"
variant="ghost"
className="ml-2 h-6 w-6 p-0 text-foreground-tertiary hover:text-foreground"
>
×
</Button>
</div>
))
)}
</div>
</div>
</div>
{/* Footer Info */}
<div className="border-t border-border p-4">
<p className="text-xs text-foreground-tertiary">
Variables are stored in your browser and sent with each tool execution.
</p>
</div>
</div>
</aside>
);
}
/**
* Hook to get current env vars from localStorage
* Can be used in other components to access env vars
*/
export function useEnvVars(): EnvVar[] {
const [envVars, setEnvVars] = useState<EnvVar[]>([]);
useEffect(() => {
// Load initially
const loadEnvVars = () => {
try {
const stored = localStorage.getItem(ENV_STORAGE_KEY);
if (stored) {
setEnvVars(JSON.parse(stored));
}
} catch (error) {
console.error('Failed to load env vars:', error);
}
};
loadEnvVars();
// Listen for updates
const handleUpdate = (event: Event) => {
const customEvent = event as CustomEvent<EnvVar[]>;
setEnvVars(customEvent.detail);
};
window.addEventListener('env-vars-updated', handleUpdate);
return () => window.removeEventListener('env-vars-updated', handleUpdate);
}, []);
return envVars;
}

View file

@ -0,0 +1,298 @@
'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Checkbox } from '@tpmjs/ui/Checkbox/Checkbox';
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 {
toolId?: string;
packageName: string;
name: string;
description: string;
category: string;
version: string;
qualityScore?: number;
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 {
const [tools, setTools] = useState<Tool[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
const [hideBroken, setHideBroken] = useState(true);
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
useEffect(() => {
async function fetchTools() {
try {
const response = await fetch('/api/tools');
const data = await response.json();
console.log('🔍 [ToolsSidebar] Fetched tools:', data.tools?.length, 'tools');
console.log('🔍 [ToolsSidebar] First tool sample:', data.tools?.[0]);
console.log('🔍 [ToolsSidebar] Health fields check:', {
hasImportHealth: 'importHealth' in (data.tools?.[0] || {}),
hasExecutionHealth: 'executionHealth' in (data.tools?.[0] || {}),
firstToolHealth: {
importHealth: data.tools?.[0]?.importHealth,
executionHealth: data.tools?.[0]?.executionHealth,
},
});
if (data.success) {
setTools(data.tools);
}
} catch (error) {
console.error('Failed to fetch tools:', error);
} finally {
setLoading(false);
}
}
fetchTools();
}, []);
const filteredTools = tools.filter((tool) => {
// Filter by search text
const matchesFilter =
tool.packageName?.toLowerCase().includes(filter.toLowerCase()) ||
tool.name?.toLowerCase().includes(filter.toLowerCase()) ||
tool.description?.toLowerCase().includes(filter.toLowerCase()) ||
tool.category?.toLowerCase().includes(filter.toLowerCase());
// Filter out broken tools if hideBroken is true
const isBroken = tool.importHealth === 'BROKEN' || tool.executionHealth === 'BROKEN';
const matchesHealth = !hideBroken || !isBroken;
return matchesFilter && matchesHealth;
});
return (
<>
<aside className="hidden w-72 border-r border-border bg-surface md:block">
<div className="flex h-full flex-col">
{/* Header */}
<div className="border-b border-border p-4">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground-secondary">
Tools
</h2>
<Badge variant="secondary" size="sm">
{filteredTools.length}
</Badge>
</div>
<Input
type="text"
placeholder="Search tools..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="mb-3"
/>
<div className="flex items-center gap-2 text-xs text-foreground-secondary">
<Checkbox
id="hide-broken"
checked={hideBroken}
onChange={(e) => setHideBroken(e.target.checked)}
size="sm"
/>
<label htmlFor="hide-broken" className="cursor-pointer select-none">
Hide broken tools
</label>
</div>
</div>
{/* Tools List */}
<div className="flex-1 overflow-y-auto p-3">
<div className="space-y-2">
{loading ? (
<div className="flex items-center justify-center py-8">
<p className="text-sm text-foreground-tertiary">Loading tools...</p>
</div>
) : filteredTools.length === 0 ? (
<div className="flex items-center justify-center py-8">
<p className="text-sm text-foreground-tertiary">No tools found</p>
</div>
) : (
filteredTools.map((tool) => (
// biome-ignore lint/a11y/useSemanticElements: Custom styled card with complex layout
<div
key={`${tool.packageName}-${tool.name}`}
className="cursor-pointer rounded-lg border border-border bg-background p-3 transition-all hover:border-foreground-tertiary hover:shadow-sm"
onClick={() => setSelectedTool(tool)}
onKeyDown={(e) => e.key === 'Enter' && setSelectedTool(tool)}
role="button"
tabIndex={0}
>
<div className="mb-1 flex items-start justify-between gap-2">
<h3 className="text-sm font-medium leading-tight">{tool.name}</h3>
<ToolHealthBadge
importHealth={tool.importHealth}
executionHealth={tool.executionHealth}
size="sm"
/>
</div>
<p className="mb-2 text-xs text-foreground-tertiary">{tool.packageName}</p>
<p className="text-xs text-foreground-secondary line-clamp-2">
{tool.description}
</p>
<div className="mt-2 flex items-center gap-2">
<Badge variant="secondary" size="sm">
{tool.category}
</Badge>
<span className="text-xs text-foreground-tertiary">v{tool.version}</span>
</div>
</div>
))
)}
</div>
</div>
</div>
</aside>
{/* Tool Details Modal */}
{selectedTool && (
// biome-ignore lint/a11y/useSemanticElements: Modal backdrop - standard pattern for modal overlays with role=button
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onClick={(e) => {
// Only close if clicking the backdrop itself, not the content
if (e.target === e.currentTarget) setSelectedTool(null);
}}
onKeyDown={(e) => {
if (e.key === 'Escape') setSelectedTool(null);
}}
role="button"
tabIndex={0}
aria-label="Close modal"
>
<div className="relative mx-4 max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-xl border border-border bg-background p-6 shadow-xl">
{/* Close button */}
<button
type="button"
onClick={() => setSelectedTool(null)}
className="absolute right-4 top-4 text-foreground-tertiary hover:text-foreground"
aria-label="Close modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>Close</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/* Tool header */}
<div className="mb-6 border-b border-border pb-4">
<h2 className="mb-2 text-2xl font-bold text-foreground">{selectedTool.name}</h2>
<p className="mb-2 text-sm text-foreground-secondary">{selectedTool.packageName}</p>
<div className="flex items-center gap-2">
<Badge variant="secondary">{selectedTool.category}</Badge>
<span className="text-sm text-foreground-tertiary">v{selectedTool.version}</span>
{selectedTool.qualityScore && (
<Badge variant="info">
Score: {(selectedTool.qualityScore * 100).toFixed(0)}%
</Badge>
)}
</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>
<p className="text-sm text-foreground-secondary">{selectedTool.description}</p>
</div>
{/* Frameworks */}
{selectedTool.frameworks && selectedTool.frameworks.length > 0 && (
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Frameworks</h3>
<div className="flex flex-wrap gap-2">
{selectedTool.frameworks.map((framework) => (
<Badge key={framework} variant="outline" size="sm">
{framework}
</Badge>
))}
</div>
</div>
)}
{/* Environment Variables */}
{selectedTool.env && selectedTool.env.length > 0 && (
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">
Environment Variables
</h3>
<div className="space-y-2">
{selectedTool.env.map((envVar) => (
<div key={envVar.name} className="rounded border border-border bg-surface p-3">
<div className="mb-1 flex items-center gap-2">
<code className="text-sm font-mono text-foreground">{envVar.name}</code>
{envVar.required && (
<Badge variant="error" size="sm">
Required
</Badge>
)}
</div>
<p className="text-xs text-foreground-secondary">{envVar.description}</p>
{envVar.default && (
<p className="mt-1 text-xs text-foreground-tertiary">
Default: <code className="font-mono">{envVar.default}</code>
</p>
)}
</div>
))}
</div>
</div>
)}
{/* Import URL */}
{selectedTool.importUrl && (
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Import URL</h3>
<code className="block rounded bg-surface p-3 text-xs font-mono text-foreground-secondary break-all">
{selectedTool.importUrl}
</code>
</div>
)}
{/* Tool ID */}
{selectedTool.toolId && (
<div>
<h3 className="mb-2 text-lg font-semibold text-foreground">Tool ID</h3>
<code className="block rounded bg-surface p-3 text-xs font-mono text-foreground-secondary">
{selectedTool.toolId}
</code>
</div>
)}
</div>
</div>
)}
</>
);
}

View file

@ -0,0 +1,7 @@
import { createEnv } from '@tpmjs/env';
import { z } from 'zod';
export const env = createEnv({
// Server-only (optional for playground - can be provided by client UI)
OPENAI_API_KEY: z.string().min(1).optional(),
});

View file

@ -0,0 +1,59 @@
'use client';
import { useChat as useAISDKChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { nanoid } from 'nanoid';
import { useState } from 'react';
/**
* Custom chat hook that wraps the official @ai-sdk/react useChat
* Handles SSE streaming with tool calls and UI message protocol
* Includes conversation ID tracking for dynamic tool loading
*/
const ENV_STORAGE_KEY = 'tpmjs-playground-env-vars';
export function useChat(): ReturnType<typeof useAISDKChat> & { conversationId: string } {
// Generate stable conversation ID for session
const [conversationId] = useState(() => nanoid());
const chat = useAISDKChat({
transport: new DefaultChatTransport({
api: '/api/chat',
// Use function body that reads FRESH from localStorage on each request
// This avoids React closure issues where envVars would be stale
body: () => {
// Read env vars directly from localStorage (not from React state)
let envVars: Array<{ key: string; value: string }> = [];
try {
const stored = localStorage.getItem(ENV_STORAGE_KEY);
if (stored) {
envVars = JSON.parse(stored);
}
} catch (error) {
console.error('Failed to read env vars from localStorage:', error);
}
// Convert to object format
const env = envVars.reduce(
(acc, { key, value }) => {
acc[key] = value;
return acc;
},
{} as Record<string, string>
);
console.log('🔑 [useChat] Reading env vars from localStorage:', Object.keys(env));
return {
conversationId,
env,
};
},
}),
});
return {
...chat,
conversationId,
};
}

View file

@ -0,0 +1,46 @@
'use client';
import { useCallback, useState } from 'react';
import type { ToolUsageStats } from '~/lib/types';
export function useToolUsage() {
const [toolUsage, setToolUsage] = useState<Map<string, ToolUsageStats>>(new Map());
const trackTool = useCallback((packageName: string) => {
setToolUsage((prev) => {
const newMap = new Map(prev);
const existing = newMap.get(packageName);
if (existing) {
newMap.set(packageName, {
...existing,
callCount: existing.callCount + 1,
lastCalledAt: new Date(),
});
} else {
newMap.set(packageName, {
packageName,
callCount: 1,
lastCalledAt: new Date(),
});
}
return newMap;
});
}, []);
const clearUsage = useCallback(() => {
setToolUsage(new Map());
}, []);
// Convert Map to array sorted by most recent first
const toolUsageArray = Array.from(toolUsage.values()).sort(
(a, b) => b.lastCalledAt.getTime() - a.lastCalledAt.getTime()
);
return {
toolUsage: toolUsageArray,
trackTool,
clearUsage,
};
}

View file

@ -0,0 +1,279 @@
import { jsonSchema, tool } from 'ai';
// Cache for tool wrappers (process-level)
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const moduleCache = new Map<string, any>();
// Cache for per-conversation active tools
const conversationTools = new Map<string, Set<string>>();
// Cache for per-conversation env vars (updated on each request)
const conversationEnv = new Map<string, Record<string, string>>();
// Railway service URL
const RAILWAY_SERVICE_URL =
process.env.RAILWAY_SERVICE_URL || process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3001';
/**
* Generate cache key for a tool
*/
function getCacheKey(packageName: string, name: string): string {
return `${packageName}::${name}`;
}
/**
* Set environment variables for a conversation
* This allows tools to access the latest env vars even when cached
*/
export function setConversationEnv(conversationId: string, env: Record<string, string>): void {
console.log(`🔑 Setting env for conversation ${conversationId}:`, Object.keys(env));
conversationEnv.set(conversationId, env);
}
/**
* Get environment variables for a conversation
*/
function getConversationEnv(conversationId: string): Record<string, string> {
return conversationEnv.get(conversationId) || {};
}
/**
* Dynamically load a tool via Railway service
* Railway service runs with --experimental-network-imports and can import from esm.sh
*/
export async function loadToolDynamically(
packageName: string,
name: string,
version: string,
conversationId: string,
importUrl?: string,
env?: Record<string, string>
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
): Promise<any | null> {
const cacheKey = getCacheKey(packageName, name);
// Check cache first
if (moduleCache.has(cacheKey)) {
console.log(`✅ Cache hit: ${cacheKey}`);
return moduleCache.get(cacheKey);
}
try {
console.log(`📦 Loading from Railway: ${packageName}/${name}`);
console.log(`🔗 Railway URL: ${RAILWAY_SERVICE_URL}`);
// Call Railway service to load and describe tool
// 120 second timeout per tool to handle large dependency downloads
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120000);
let response: Response | undefined;
let data:
| { success: boolean; tool?: { description: string; inputSchema?: unknown }; error?: string }
| undefined;
try {
response = await fetch(`${RAILWAY_SERVICE_URL}/load-and-describe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
name,
version,
importUrl: importUrl || `https://esm.sh/${packageName}@${version}`,
env: env || {},
}),
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ Railway service error (${response.status}): ${errorText}`);
return null;
}
data = await response.json();
if (!data || !data.success) {
const errorMsg = data?.error || 'Unknown error';
console.error(`❌ Failed to load tool: ${errorMsg}`);
return null;
}
} catch (fetchError) {
clearTimeout(timeout);
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
console.error(`❌ Railway request timeout after 120s for ${packageName}/${name}`);
return null;
}
throw fetchError;
}
// Type guard - data and data.tool are guaranteed after successful response
if (!data?.tool) {
console.error('❌ Invalid response from Railway: missing tool data');
return null;
}
console.log(`✅ Tool loaded from Railway: ${cacheKey}`);
console.log(`📋 Description: ${data.tool.description}`);
// Create a proper AI SDK tool wrapper that executes remotely
// Railway returns plain JSON Schema - wrap it with jsonSchema() for AI SDK
const toolWrapper = tool({
description: data.tool.description,
inputSchema: data.tool.inputSchema
? jsonSchema(data.tool.inputSchema)
: jsonSchema({ type: 'object', properties: {}, additionalProperties: false }),
// biome-ignore lint/suspicious/noExplicitAny: Tool params are dynamic
execute: async (params: any) => {
console.log(`🚀 Executing ${packageName}/${name} remotely with params:`, params);
// Get the latest env vars for this conversation (not from closure!)
const currentEnv = getConversationEnv(conversationId);
console.log(
`🔐 Using env vars for conversation ${conversationId}:`,
Object.keys(currentEnv)
);
const execResponse = await fetch(`${RAILWAY_SERVICE_URL}/execute-tool`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
name,
version,
importUrl: importUrl || `https://esm.sh/${packageName}@${version}`,
params,
env: currentEnv,
}),
});
const result = await execResponse.json();
if (!result.success) {
console.error(`❌ Tool execution failed: ${result.error}`);
throw new Error(result.error || 'Tool execution failed');
}
// Health status is reported by the Railway executor
console.log(`✅ Tool executed successfully in ${result.executionTimeMs}ms`);
return result.output;
},
});
// Cache the wrapper
moduleCache.set(cacheKey, toolWrapper);
console.log(`✅ Cached tool wrapper: ${cacheKey}`);
return toolWrapper;
} catch (error) {
console.error(`❌ Failed to load ${packageName}#${name}:`, error);
console.error(' Stack:', error instanceof Error ? error.stack : 'No stack trace');
return null;
}
}
/**
* Load multiple tools in parallel with collated error reporting
*/
export async function loadToolsBatch(
toolMetadata: Array<{
packageName: string;
name: string;
version: string;
importUrl?: string;
}>,
conversationId: string,
env?: Record<string, string>
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
): Promise<Record<string, any>> {
console.log(`📦 Loading ${toolMetadata.length} tools for conversation ${conversationId}`);
console.log('🔑 Env vars being passed:', Object.keys(env || {}));
const promises = toolMetadata.map((meta) =>
loadToolDynamically(
meta.packageName,
meta.name,
meta.version,
conversationId,
meta.importUrl,
env
).then((tool) => ({
packageName: meta.packageName,
name: meta.name,
key: getCacheKey(meta.packageName, meta.name),
tool,
success: tool !== null,
}))
);
const results = await Promise.all(promises);
// Separate successful and failed tools
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
// Build tools object
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex
const tools: Record<string, any> = {};
for (const result of successful) {
tools[result.key] = result.tool;
}
// Log collated error summary
console.log('\n📊 Batch Load Summary:');
console.log(` ✅ Successful: ${successful.length}/${toolMetadata.length}`);
console.log(` ❌ Failed: ${failed.length}/${toolMetadata.length}`);
if (failed.length > 0) {
console.log('\n❌ Failed Tools:');
for (const result of failed) {
console.log(` - ${result.packageName}/${result.name}`);
}
console.log('\n💡 Note: Tool failures have been reported to the health service.');
}
return tools;
}
/**
* Track tools for a conversation
*/
export function addConversationTools(conversationId: string, toolKeys: string[]): void {
if (!conversationTools.has(conversationId)) {
conversationTools.set(conversationId, new Set());
}
const tools = conversationTools.get(conversationId);
if (!tools) return; // Should never happen after the check above
for (const key of toolKeys) {
tools.add(key);
}
}
/**
* Get all tools for a conversation
*/
export function getConversationTools(conversationId: string): string[] {
return Array.from(conversationTools.get(conversationId) || []);
}
/**
* Clear conversation tools (on session end)
*/
export function clearConversationTools(conversationId: string): void {
conversationTools.delete(conversationId);
}
/**
* Get cache statistics
*/
export function getCacheStats() {
return {
moduleCacheSize: moduleCache.size,
conversationCount: conversationTools.size,
};
}

View file

@ -0,0 +1,94 @@
// Static imports for tools (required for Next.js/webpack)
import { helloNameTool, helloWorldTool } from '@tpmjs/hello';
/**
* Tool registry mapping package names + export names to actual tool functions
* This is a static mapping required for Next.js/webpack bundling
*/
const TOOL_REGISTRY: Record<string, Record<string, any>> = {
'@tpmjs/hello': {
helloWorldTool,
helloNameTool,
},
};
/**
* Load a specific TPMJS tool by package name and export name
* Uses static imports to work with Next.js/webpack bundling
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
export async function loadTpmjsTool(packageName: string, name: string): Promise<any | null> {
try {
// Look up the package in the registry
const packageTools = TOOL_REGISTRY[packageName];
if (!packageTools) {
console.warn(`Package not found in registry: ${packageName}`);
return null;
}
// Look up the specific tool export
const tool = packageTools[name];
if (!tool) {
console.warn(
`Export '${name}' not found in package ${packageName}. Available exports:`,
Object.keys(packageTools)
);
return null;
}
return tool;
} catch (error) {
console.error(`Failed to load tool ${packageName}/${name}:`, error);
return null;
}
}
/**
* Sanitize tool name to match OpenAI's requirements
* Pattern: ^[a-zA-Z0-9_-]+$ (only letters, numbers, underscores, hyphens)
*/
export function sanitizeToolName(name: string): string {
return name
.replace(/@/g, '') // Remove @ symbols
.replace(/\//g, '_') // Replace / with _
.replace(/[^a-zA-Z0-9_-]/g, '_'); // Replace any other invalid chars with _
}
/**
* Load all installed TPMJS tools
* Returns a flat object with all tools keyed by sanitized packageName-name
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
export async function loadAllTools(): Promise<Record<string, any>> {
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
const tools: Record<string, any> = {};
// Iterate through all registered packages
for (const [packageName, packageTools] of Object.entries(TOOL_REGISTRY)) {
for (const [name, tool] of Object.entries(packageTools)) {
// Create a unique, sanitized key for this tool
const toolKey = sanitizeToolName(`${packageName}-${name}`);
tools[toolKey] = tool;
}
}
return tools;
}
/**
* Get list of all available package names
*/
export function getAvailablePackages(): string[] {
return Object.keys(TOOL_REGISTRY);
}
/**
* Get list of all export names for a given package
*/
export function getPackageExports(packageName: string): string[] {
const packageTools = TOOL_REGISTRY[packageName];
if (!packageTools) {
return [];
}
return Object.keys(packageTools);
}

View file

@ -0,0 +1,37 @@
export type MessageRole = 'user' | 'assistant' | 'system';
export interface ChatMessage {
id: string;
role: MessageRole;
content: string;
timestamp: Date;
toolCalls?: ToolCallInfo[];
}
export interface ToolCallInfo {
id: string;
toolName: string;
parameters: Record<string, unknown>;
result?: unknown;
status?: 'pending' | 'success' | 'error';
error?: string;
}
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
totalTokens: number;
estimatedCost: number;
}
export interface ToolUsageStats {
packageName: string;
callCount: number;
lastCalledAt: Date;
}
export type SSEEvent =
| { type: 'chunk'; data: { text: string } }
| { type: 'tool-call'; data: ToolCallInfo }
| { type: 'complete'; data: { tokenUsage?: TokenUsage } }
| { type: 'error'; data: { message: string } };

View file

@ -0,0 +1,12 @@
import baseConfig from '@tpmjs/tailwind-config/base';
import type { Config } from 'tailwindcss';
export default {
...baseConfig,
content: [
'./src/app/**/*.{ts,tsx}',
'./src/components/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.ts',
],
plugins: [...(baseConfig.plugins || []), require('@tailwindcss/typography')],
} satisfies Config;

View file

@ -0,0 +1,12 @@
{
"extends": "@tpmjs/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"~/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

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

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

View file

@ -0,0 +1,17 @@
# Use official Deno image
FROM denoland/deno:1.39.0
# Set working directory
WORKDIR /app
# Set Deno cache directory to use mounted volume
ENV DENO_DIR=/data
# Copy server file
COPY server.ts .
# Expose port (Railway will set PORT env var)
EXPOSE 3002
# Run the Deno server
CMD ["deno", "run", "--allow-net", "--allow-env", "--allow-read", "--allow-write", "server.ts"]

View file

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

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

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

View file

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

View file

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

View file

@ -0,0 +1,957 @@
/**
* Railway Dynamic Tool Executor (Deno)
* Uses Deno's native HTTP import support
*/
// Import zod-to-json-schema for Zod v3 support
import { zodToJsonSchema } from 'https://esm.sh/zod-to-json-schema@3.25.0';
// Cache TTL: 2 minutes
const CACHE_TTL_MS = 2 * 60 * 1000;
// Cache entry with expiration
interface CacheEntry {
// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic and vary by package
module: any;
expiresAt: number;
isFactory: boolean;
}
// Cache for imported tool modules with TTL
const moduleCache = new Map<string, CacheEntry>();
/**
* Get module from cache if not expired
*/
function getCachedModule(cacheKey: string): CacheEntry | null {
const entry = moduleCache.get(cacheKey);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
moduleCache.delete(cacheKey);
console.log(`🗑️ Cache expired: ${cacheKey}`);
return null;
}
return entry;
}
/**
* Store module in cache with TTL
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic and vary by package
function setCachedModule(cacheKey: string, module: any, isFactory: boolean): void {
moduleCache.set(cacheKey, {
module,
expiresAt: Date.now() + CACHE_TTL_MS,
isFactory,
});
console.log(`📦 Cached (TTL ${CACHE_TTL_MS / 1000}s): ${cacheKey}`);
}
/**
* Cleanup expired cache entries
*/
function cleanupExpiredCache(): void {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of moduleCache.entries()) {
if (now > entry.expiresAt) {
moduleCache.delete(key);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`🗑️ Cleaned ${cleaned} expired cache entries`);
}
}
// Run cache cleanup every minute
setInterval(cleanupExpiredCache, 60 * 1000);
// Web app API URL for health status reporting
const TPMJS_API_URL = Deno.env.get('TPMJS_API_URL') || 'https://tpmjs.com';
/**
* Report tool execution result to centralized health service
* Non-blocking - fires and forgets to avoid slowing down execution
*/
async function reportToolHealth(
packageName: string,
name: string,
success: boolean,
error?: string
): Promise<void> {
try {
const response = await fetch(`${TPMJS_API_URL}/api/tools/report-health`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
name,
success,
error,
}),
});
if (response.ok) {
console.log(
`📊 Health reported for ${packageName}/${name}: ${success ? 'SUCCESS' : 'FAILURE'}`
);
} else {
console.warn(`⚠️ Failed to report health: ${response.status}`);
}
} catch (err) {
// Non-blocking - just log
console.error('❌ Failed to report tool health:', err);
}
}
/**
* Update tool schema in TPM.js database
* Non-blocking - fires and forgets to keep schemas up to date
*/
async function updateToolSchema(
packageName: string,
name: string,
description: string,
// biome-ignore lint/suspicious/noExplicitAny: JSON Schema can have any structure
inputSchema: any
): Promise<void> {
try {
const response = await fetch(`${TPMJS_API_URL}/api/tools/update-schema`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
name,
description,
inputSchema,
}),
});
if (response.ok) {
const data = await response.json();
console.log(
`📋 Schema updated for ${packageName}/${name}:`,
data.updated ? 'UPDATED' : 'NO CHANGE'
);
} else {
const errorText = await response.text();
console.warn(`⚠️ Failed to update schema: ${response.status}`, errorText);
}
} catch (err) {
// Non-blocking - just log
console.error('❌ Failed to update tool schema:', err);
}
}
/**
* Sanitize JSON Schema to fix common issues
* - Replaces invalid type "None" with "object"
* - Ensures type is always set
* - Ensures object schemas have properties
*/
// biome-ignore lint/suspicious/noExplicitAny: JSON Schema can have any structure
function sanitizeJsonSchema(schema: any): any {
if (!schema || typeof schema !== 'object') {
console.warn('⚠️ Invalid schema (not an object), returning default object schema');
return { type: 'object', properties: {}, additionalProperties: false };
}
// Clone the schema to avoid mutating the original
const sanitized = { ...schema };
// Fix invalid type "None" (common in Python-based tools)
if (sanitized.type === 'None' || sanitized.type === 'none' || sanitized.type === null) {
console.warn(`⚠️ Invalid schema type "${sanitized.type}", replacing with "object"`);
sanitized.type = 'object';
if (!sanitized.properties) {
sanitized.properties = {};
}
if (sanitized.additionalProperties === undefined) {
sanitized.additionalProperties = false;
}
}
// Ensure type is set
if (!sanitized.type) {
console.warn('⚠️ Schema missing type, defaulting to "object"');
sanitized.type = 'object';
if (!sanitized.properties) {
sanitized.properties = {};
}
if (sanitized.additionalProperties === undefined) {
sanitized.additionalProperties = false;
}
}
// Recursively sanitize nested schemas
if (sanitized.properties && typeof sanitized.properties === 'object') {
for (const [key, value] of Object.entries(sanitized.properties)) {
if (value && typeof value === 'object') {
sanitized.properties[key] = sanitizeJsonSchema(value);
}
}
}
// Sanitize array items
if (sanitized.items && typeof sanitized.items === 'object') {
sanitized.items = sanitizeJsonSchema(sanitized.items);
}
// Sanitize anyOf/oneOf/allOf
for (const key of ['anyOf', 'oneOf', 'allOf']) {
if (Array.isArray(sanitized[key])) {
sanitized[key] = sanitized[key].map((s: any) => sanitizeJsonSchema(s));
}
}
return sanitized;
}
/**
* Load and describe a tool from esm.sh
*/
async function loadAndDescribe(req: Request): Promise<Response> {
try {
const body = await req.json();
const { packageName, name, version, importUrl, env } = body;
if (!packageName || !name || !version) {
return Response.json(
{
success: false,
error: 'Missing required fields: packageName, name, version',
},
{ status: 400 }
);
}
const cacheKey = `${packageName}::${name}`;
// biome-ignore lint/suspicious/noImplicitAnyLet: Tool type is determined dynamically after import
let toolModule;
// Check cache first (with TTL)
const cachedEntry = getCachedModule(cacheKey);
if (cachedEntry) {
console.log(`✅ Cache hit: ${cacheKey}`);
toolModule = cachedEntry.module;
} else {
// Dynamic import from esm.sh (Deno supports this natively!)
const url = importUrl || `https://esm.sh/${packageName}@${version}`;
console.log(`📦 Importing: ${url}`);
const module = await import(url);
let rawExport = module[name];
if (!rawExport) {
console.error(`❌ Export "${name}" not found. Available:`, Object.keys(module));
return Response.json(
{
success: false,
error: `Export "${name}" not found in module`,
availableExports: Object.keys(module),
},
{ status: 404 }
);
}
// Check if it's a factory function (not a direct tool)
if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
console.log(`🏭 Detected factory function for ${cacheKey}, attempting to call...`);
let factoryResult = null;
// Strategy 1: Try calling with no arguments
try {
console.log(` Trying: ${name}()`);
factoryResult = rawExport();
if (factoryResult?.description && factoryResult?.execute) {
console.log(' ✅ Success with no-args factory');
rawExport = factoryResult;
}
} catch (error) {
console.log(' ❌ No-args failed:', error.message);
}
// Strategy 2: Try calling with env vars as config object
if (!factoryResult && env && typeof env === 'object') {
// Build multiple config variations to try
const configVariations = [];
// Variation 1: Raw env vars (e.g., { VALYU_API_KEY: 'xxx' })
configVariations.push({ ...env });
// Variation 2: Normalized to camelCase apiKey (e.g., { apiKey: 'xxx' })
const apiKeyValue = Object.entries(env).find(([key]) =>
key.toUpperCase().includes('API_KEY')
)?.[1];
if (apiKeyValue) {
configVariations.push({ apiKey: apiKeyValue });
}
// Variation 3: Normalized to key (e.g., { key: 'xxx' })
if (apiKeyValue) {
configVariations.push({ key: apiKeyValue });
}
// Try each config variation
for (const config of configVariations) {
try {
console.log(` Trying: ${name}(`, Object.keys(config), ')');
factoryResult = rawExport(config);
if (factoryResult?.description && factoryResult?.execute) {
console.log(' ✅ Success with config:', Object.keys(config));
rawExport = factoryResult;
break;
}
} catch (error) {
console.log(' ❌ Config', Object.keys(config), 'failed:', error.message);
}
}
}
// Strategy 3: Try calling with first env var value (single-arg pattern)
if (!factoryResult && env && typeof env === 'object') {
try {
const firstValue = Object.values(env)[0];
if (firstValue) {
console.log(` Trying: ${name}(firstEnvValue)`);
factoryResult = rawExport(firstValue);
if (factoryResult?.description && factoryResult?.execute) {
console.log(' ✅ Success with single-arg factory');
rawExport = factoryResult;
}
}
} catch (error) {
console.log(' ❌ Single-arg failed:', error.message);
}
}
// If all factory strategies failed, return error
if (!factoryResult) {
console.error('❌ Factory function detected but all call strategies failed');
return Response.json(
{
success: false,
error: `Tool "${name}" is a factory function but couldn't be initialized. Tried: no-args, config object, and single-arg patterns.`,
hint: 'This tool may require specific configuration. Check package documentation.',
},
{ status: 400 }
);
}
}
toolModule = rawExport;
// Validate it's an AI SDK tool
if (!toolModule.description || !toolModule.execute) {
console.error('❌ Invalid AI SDK tool structure:', {
hasDescription: !!toolModule.description,
hasExecute: !!toolModule.execute,
hasInputSchema: !!toolModule.inputSchema,
keys: Object.keys(toolModule),
});
return Response.json(
{
success: false,
error: 'Invalid AI SDK tool structure (missing description or execute)',
toolKeys: Object.keys(toolModule),
},
{ status: 400 }
);
}
// Cache it with TTL (mark as non-factory for loadAndDescribe)
setCachedModule(cacheKey, toolModule, false);
}
// Extract tool definition - try multiple schema formats
let rawJsonSchema = null;
if (toolModule.inputSchema) {
// Strategy 1: Try Zod v4 native JSON Schema export
if (typeof toolModule.inputSchema.toJSONSchema === 'function') {
console.log(`📋 Using Zod v4 toJSONSchema() for ${cacheKey}`);
try {
rawJsonSchema = toolModule.inputSchema.toJSONSchema();
} catch (error) {
console.warn(`⚠️ Zod toJSONSchema() failed for ${cacheKey}:`, error);
}
} else if (typeof toolModule.inputSchema.jsonSchema === 'function') {
console.log(`📋 Using Zod v4 jsonSchema() for ${cacheKey}`);
try {
rawJsonSchema = toolModule.inputSchema.jsonSchema();
} catch (error) {
console.warn(`⚠️ Zod jsonSchema() failed for ${cacheKey}:`, error);
}
}
// Strategy 2: Try AI SDK v6 jsonSchema() wrapper (has .schema property)
if (!rawJsonSchema && toolModule.inputSchema.schema) {
console.log(`📋 Using AI SDK jsonSchema.schema for ${cacheKey}`);
rawJsonSchema = toolModule.inputSchema.schema;
}
// Strategy 2.5: Try AI SDK jsonSchema() wrapper (has .jsonSchema property)
// Note: Some versions use .jsonSchema instead of .schema
if (
!rawJsonSchema &&
toolModule.inputSchema.jsonSchema &&
typeof toolModule.inputSchema.jsonSchema === 'object'
) {
console.log(`📋 Using AI SDK jsonSchema.jsonSchema for ${cacheKey}`);
rawJsonSchema = toolModule.inputSchema.jsonSchema;
}
// Strategy 3: Try Zod v4 schema (detect via _zod property - new in Zod v4)
if (!rawJsonSchema && toolModule.inputSchema._zod) {
console.log(`📋 Detected Zod v4 schema for ${cacheKey}`);
try {
// Dynamically import Zod v4 and use its toJSONSchema method
const zod = await import('https://esm.sh/zod@4');
if (zod.toJSONSchema) {
rawJsonSchema = zod.toJSONSchema(toolModule.inputSchema);
console.log(`✅ Successfully converted Zod v4 schema using z.toJSONSchema for ${cacheKey}`);
} else if (zod.default?.toJSONSchema) {
rawJsonSchema = zod.default.toJSONSchema(toolModule.inputSchema);
console.log(`✅ Successfully converted Zod v4 schema using z.default.toJSONSchema for ${cacheKey}`);
} else {
console.warn(`⚠️ Zod v4 toJSONSchema not found. Available exports:`, Object.keys(zod));
}
} catch (error) {
console.warn(`⚠️ Zod v4 toJSONSchema conversion failed for ${cacheKey}:`, error);
}
}
// Strategy 4: Try Zod v3 schema (detect via _def property and convert)
if (!rawJsonSchema && toolModule.inputSchema._def) {
console.log(
`📋 Detected Zod schema (v3), converting with zod-to-json-schema for ${cacheKey}`
);
try {
rawJsonSchema = zodToJsonSchema(toolModule.inputSchema);
console.log(`✅ Successfully converted Zod schema for ${cacheKey}`);
} catch (error) {
console.warn(`⚠️ zod-to-json-schema conversion failed for ${cacheKey}:`, error);
}
}
}
// If no schema found, fail with helpful error
if (!rawJsonSchema) {
console.error(`❌ No valid schema found for ${cacheKey}`, {
hasInputSchema: !!toolModule.inputSchema,
inputSchemaType: typeof toolModule.inputSchema,
hasToJSONSchema: typeof toolModule.inputSchema?.toJSONSchema === 'function',
hasJsonSchemaFunction: typeof toolModule.inputSchema?.jsonSchema === 'function',
hasJsonSchemaProperty:
!!toolModule.inputSchema?.jsonSchema &&
typeof toolModule.inputSchema?.jsonSchema === 'object',
hasSchema: !!toolModule.inputSchema?.schema,
keys: toolModule.inputSchema ? Object.keys(toolModule.inputSchema) : [],
});
return Response.json(
{
success: false,
error: `Tool "${name}" has no valid inputSchema. Tools must use AI SDK jsonSchema(), Zod v4 (._zod), or Zod v3 (._def) schemas.`,
debug: {
hasInputSchema: !!toolModule.inputSchema,
availableMethods: toolModule.inputSchema ? Object.keys(toolModule.inputSchema) : [],
hasZodV4: !!toolModule.inputSchema?._zod,
hasZodV3Def: !!toolModule.inputSchema?._def,
},
},
{ status: 400 }
);
}
console.log(`✅ Extracted schema for ${cacheKey}`);
// Sanitize schema - fix common issues with invalid schemas
const sanitizedSchema = sanitizeJsonSchema(rawJsonSchema);
// Update TPM.js database with the schema (async, non-blocking)
updateToolSchema(packageName, name, toolModule.description, sanitizedSchema).catch((err) => {
console.warn('⚠️ Failed to update schema in database:', err);
});
return Response.json({
success: true,
tool: {
name,
description: toolModule.description,
inputSchema: sanitizedSchema, // Plain JSON Schema - fully serializable
},
});
} catch (error) {
console.error('❌ Failed to load tool:', error);
return Response.json(
{
success: false,
error: error.message,
},
{ status: 500 }
);
}
}
/**
* Execute a tool with parameters
*/
async function executeTool(req: Request): Promise<Response> {
const startTime = Date.now();
// Declare these before try block so they're available in catch for error reporting
let packageName = 'unknown';
let toolName = 'unknown';
try {
const body = await req.json();
const { packageName: pkg, name, version, importUrl, params, env } = body;
packageName = pkg || 'unknown';
toolName = name || 'unknown';
console.log('📥 Execute request:', {
packageName,
name: toolName,
version,
envKeys: env ? Object.keys(env) : [],
envValues: env || {},
});
if (!packageName || !toolName || !version) {
return Response.json(
{
success: false,
error: 'Missing required fields: packageName, name, version',
},
{ status: 400 }
);
}
const cacheKey = `${packageName}::${toolName}`;
// Inject environment variables FIRST - before cache check and factory calls
// This ensures process.env is set when factory functions read from it
if (env && typeof env === 'object') {
const envKeys = Object.keys(env);
if (envKeys.length > 0) {
console.log(`🔐 Injecting ${envKeys.length} environment variables:`, envKeys);
for (const [key, value] of Object.entries(env)) {
const stringValue = String(value);
// Set in Deno environment (for esm.sh imports)
Deno.env.set(key, stringValue);
// ALSO set in Node.js process.env (for npm: imports)
// @ts-ignore - process is available in Node.js compatibility mode
if (typeof globalThis.process !== 'undefined' && globalThis.process.env) {
// @ts-ignore - process.env exists in Node compat mode
globalThis.process.env[key] = stringValue;
}
console.log(` ✅ Set ${key} = ${stringValue.substring(0, 10)}...`);
}
} else {
console.log('⚠️ No env vars provided in request');
}
} else {
console.log('⚠️ No env object in request body');
}
// biome-ignore lint/suspicious/noImplicitAnyLet: Tool type is determined dynamically after import
let toolModule;
let needsImport = true;
// Check cache first (with TTL) - but skip cache for factory functions
// since they may read env vars at creation time
const cachedEntry = getCachedModule(cacheKey);
if (cachedEntry && !cachedEntry.isFactory) {
console.log(`✅ Cache hit (non-factory): ${cacheKey}`);
toolModule = cachedEntry.module;
needsImport = false;
} else if (cachedEntry?.isFactory) {
console.log(`🏭 Cache hit but factory - will re-import: ${cacheKey}`);
}
if (needsImport) {
const url = importUrl || `https://esm.sh/${packageName}@${version}`;
console.log(`📦 Importing for execution: ${url}`);
const module = await import(url);
let rawExport = module[toolName];
if (!rawExport) {
return Response.json(
{
success: false,
error: 'Tool not found',
executionTimeMs: Date.now() - startTime,
},
{ status: 404 }
);
}
// Track if this is a factory function - we won't cache factory-created tools
// because they may read env vars at creation time
let isFactoryFunction = false;
// Check if it's a factory function (not a direct tool)
if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
console.log(`🏭 Detected factory function for ${cacheKey}, attempting to call...`);
isFactoryFunction = true;
let factoryResult = null;
// Strategy 1: Try calling with no arguments
try {
console.log(` Trying: ${toolName}()`);
factoryResult = rawExport();
if (factoryResult?.execute) {
console.log(' ✅ Success with no-args factory');
rawExport = factoryResult;
}
} catch (error) {
console.log(' ❌ No-args failed:', error.message);
}
// Strategy 2: Try calling with env vars as config object
if (!factoryResult && env && typeof env === 'object') {
// Build multiple config variations to try
const configVariations = [];
// Variation 1: Raw env vars (e.g., { VALYU_API_KEY: 'xxx' })
configVariations.push({ ...env });
// Variation 2: Normalized to camelCase apiKey (e.g., { apiKey: 'xxx' })
const apiKeyValue = Object.entries(env).find(([key]) =>
key.toUpperCase().includes('API_KEY')
)?.[1];
if (apiKeyValue) {
configVariations.push({ apiKey: apiKeyValue });
}
// Variation 3: Normalized to key (e.g., { key: 'xxx' })
if (apiKeyValue) {
configVariations.push({ key: apiKeyValue });
}
// Try each config variation
for (const config of configVariations) {
try {
console.log(` Trying: ${toolName}(`, Object.keys(config), ')');
factoryResult = rawExport(config);
if (factoryResult?.execute) {
console.log(' ✅ Success with config:', Object.keys(config));
rawExport = factoryResult;
break;
}
} catch (error) {
console.log(' ❌ Config', Object.keys(config), 'failed:', error.message);
}
}
}
// Strategy 3: Try calling with first env var value (single-arg pattern)
if (!factoryResult && env && typeof env === 'object') {
try {
const firstValue = Object.values(env)[0];
if (firstValue) {
console.log(` Trying: ${toolName}(firstEnvValue)`);
factoryResult = rawExport(firstValue);
if (factoryResult?.execute) {
console.log(' ✅ Success with single-arg factory');
rawExport = factoryResult;
}
}
} catch (error) {
console.log(' ❌ Single-arg failed:', error.message);
}
}
if (!factoryResult) {
return Response.json(
{
success: false,
error: `Tool "${toolName}" is a factory function but couldn't be initialized`,
executionTimeMs: Date.now() - startTime,
},
{ status: 400 }
);
}
}
toolModule = rawExport;
if (!toolModule.execute) {
return Response.json(
{
success: false,
error: 'Tool missing execute function',
executionTimeMs: Date.now() - startTime,
},
{ status: 400 }
);
}
// Cache with TTL - mark factory functions so we know to re-import them
setCachedModule(cacheKey, toolModule, isFactoryFunction);
}
// Note: Environment variables are already injected at the start of this function
// before cache check and factory calls, so they're available when tools read process.env
// Execute the tool with AI SDK execution context
// Some tools expect a second argument with { abortSignal, ... }
const abortController = new AbortController();
const executionContext = {
abortSignal: abortController.signal,
// Add other context properties that AI SDK tools might expect
messages: [],
toolCallId: `exec_${Date.now()}`,
};
console.log(`🚀 Executing ${cacheKey} with params:`, params);
const result = await toolModule.execute(params || {}, executionContext);
const executionTimeMs = Date.now() - startTime;
console.log(`✅ Execution complete in ${executionTimeMs}ms`);
// Report successful execution to health service (non-blocking)
reportToolHealth(packageName, toolName, true).catch(() => {});
return Response.json({
success: true,
output: result,
executionTimeMs,
});
} catch (error) {
const executionTimeMs = Date.now() - startTime;
console.error('❌ Tool execution failed:', error);
// Report failed execution to health service (non-blocking)
reportToolHealth(packageName, toolName, false, error.message).catch(() => {});
return Response.json(
{
success: false,
error: error.message,
executionTimeMs,
},
{ status: 500 }
);
}
}
/**
* List all exports from a package and identify which are valid AI SDK tools
*/
async function listExports(req: Request): Promise<Response> {
try {
const body = await req.json();
const { packageName, version, importUrl, env } = body;
if (!packageName || !version) {
return Response.json(
{
success: false,
error: 'Missing required fields: packageName, version',
},
{ status: 400 }
);
}
// Dynamic import from esm.sh
const url = importUrl || `https://esm.sh/${packageName}@${version}`;
console.log(`📦 Listing exports from: ${url}`);
const module = await import(url);
const allExports = Object.keys(module);
// Filter out 'default' and identify which exports are valid tools
const tools: Array<{
name: string;
isValidTool: boolean;
description?: string;
error?: string;
}> = [];
for (const exportName of allExports) {
if (exportName === 'default') continue;
let rawExport = module[exportName];
// Check if it's a factory function
if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
// Try to call factory with no args
try {
const factoryResult = rawExport();
if (factoryResult?.description && factoryResult?.execute) {
rawExport = factoryResult;
} else if (env && typeof env === 'object') {
// Try with env config
const configResult = rawExport({ ...env });
if (configResult?.description && configResult?.execute) {
rawExport = configResult;
}
}
} catch {
// Factory call failed, continue checking
}
}
// Check if it's a valid AI SDK tool
if (rawExport?.description && rawExport?.execute) {
tools.push({
name: exportName,
isValidTool: true,
description: rawExport.description,
});
} else if (typeof rawExport === 'object' && rawExport !== null) {
// It's an object but not a valid tool - might be a factory that needs specific config
tools.push({
name: exportName,
isValidTool: false,
error: 'Not a valid AI SDK tool (missing description or execute)',
});
}
// Skip non-object exports (they're definitely not tools)
}
console.log(`✅ Found ${tools.length} potential tool exports in ${packageName}`);
return Response.json({
success: true,
packageName,
version,
exports: allExports,
tools,
});
} catch (error) {
console.error('❌ Failed to list exports:', error);
return Response.json(
{
success: false,
error: error.message,
},
{ status: 500 }
);
}
}
/**
* Health check
*/
function health(): Response {
return Response.json({
status: 'ok',
timestamp: new Date().toISOString(),
cacheSize: moduleCache.size,
denoVersion: Deno.version.deno,
v8Version: Deno.version.v8,
httpImports: true,
});
}
/**
* Cache stats
*/
function cacheStats(): Response {
const now = Date.now();
const entries = Array.from(moduleCache.entries()).map(([key, entry]) => ({
key,
isFactory: entry.isFactory,
expiresIn: Math.max(0, Math.round((entry.expiresAt - now) / 1000)),
}));
return Response.json({
success: true,
cacheSize: moduleCache.size,
ttlSeconds: CACHE_TTL_MS / 1000,
cachedTools: entries,
});
}
/**
* Clear cache
*/
function clearCache(): Response {
const size = moduleCache.size;
moduleCache.clear();
console.log(`🗑️ Cleared cache (${size} entries)`);
return Response.json({
success: true,
message: `Cleared ${size} cached modules`,
});
}
/**
* Main request handler
*/
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
// Add CORS headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (req.method === 'OPTIONS') {
return new Response(null, { headers });
}
try {
let response: Response;
if (url.pathname === '/health' && req.method === 'GET') {
response = health();
} else if (url.pathname === '/load-and-describe' && req.method === 'POST') {
response = await loadAndDescribe(req);
} else if (url.pathname === '/list-exports' && req.method === 'POST') {
response = await listExports(req);
} else if (url.pathname === '/execute-tool' && req.method === 'POST') {
response = await executeTool(req);
} else if (url.pathname === '/cache/stats' && req.method === 'GET') {
response = cacheStats();
} else if (url.pathname === '/cache/clear' && req.method === 'POST') {
response = clearCache();
} else {
response = Response.json({ error: 'Not found' }, { status: 404 });
}
// Add CORS headers to response
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
} catch (error) {
console.error('Request handler error:', error);
return Response.json(
{
success: false,
error: error.message,
},
{ status: 500, headers }
);
}
}
// Start server
const port = Number.parseInt(Deno.env.get('PORT') || '3002');
console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`);
console.log('📦 HTTP imports: ENABLED');
console.log(`🔗 Health check: http://localhost:${port}/health`);
console.log('🛠️ Endpoints:');
console.log(' POST /load-and-describe - Load tool and get schema');
console.log(' POST /list-exports - List all exports and identify valid tools');
console.log(' POST /execute-tool - Execute a tool with params');
console.log(' POST /cache/clear - Clear module cache');
console.log(' GET /cache/stats - Get cache statistics');
Deno.serve({ port }, handler);

1
apps/tutorial/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.vercel

View file

@ -0,0 +1,16 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default [
{
ignores: [
'.next/**',
'.turbo/**',
'node_modules/**',
'*.config.js',
'*.config.ts',
'next-env.d.ts',
'eslint.config.mjs',
],
},
...reactConfig,
];

6
apps/tutorial/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/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

@ -0,0 +1,8 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils'],
reactStrictMode: true,
};
export default nextConfig;

View file

@ -0,0 +1,35 @@
{
"name": "@tpmjs/tutorial",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"type-check": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
},
"dependencies": {
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",
"framer-motion": "^11.15.0",
"next": "^16.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tpmjs/eslint-config": "workspace:*",
"@tpmjs/tailwind-config": "workspace:*",
"@tpmjs/tsconfig": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.4",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -0,0 +1,16 @@
import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Real-World Agent Example | TPMJS Tutorials',
description:
'Build a complete AI agent that discovers and uses tools dynamically from the TPMJS registry. Working code examples included.',
};
export default function AgentExamplePage(): React.ReactElement {
return (
<main className="h-screen w-screen overflow-hidden">
<AgentExampleSlideshow />
</main>
);
}

View file

@ -0,0 +1,11 @@
'use client';
import { AgentSlideshow } from '@/components/AgentSlideshow';
export default function AgentsTutorial(): React.ReactElement {
return (
<main className="h-screen w-screen overflow-hidden">
<AgentSlideshow />
</main>
);
}

View file

@ -0,0 +1,11 @@
'use client';
import { AuthorSlideshow } from '@/components/AuthorSlideshow';
export default function AuthorsTutorial(): React.ReactElement {
return (
<main className="h-screen w-screen overflow-hidden">
<AuthorSlideshow />
</main>
);
}

View file

@ -0,0 +1,16 @@
import { FirstToolSlideshow } from '@/components/FirstToolSlideshow';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Build Your First Tool | TPMJS Tutorials',
description:
'Step-by-step guide to creating your first AI SDK tool for the TPMJS registry. From npm init to published tool in 10 minutes.',
};
export default function FirstToolPage(): React.ReactElement {
return (
<main className="h-screen w-screen overflow-hidden">
<FirstToolSlideshow />
</main>
);
}

View file

@ -0,0 +1,143 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Dark mode by default for dramatic slideshow */
--background: 210 10% 5%;
--surface: 210 10% 8%;
--surface-elevated: 210 10% 12%;
--foreground: 210 10% 90%;
--foreground-secondary: 210 8% 60%;
--foreground-tertiary: 210 6% 40%;
--foreground-muted: 210 6% 30%;
--border: 210 10% 20%;
--border-strong: 210 12% 30%;
--border-subtle: 210 8% 15%;
/* Accent colors - cyan and purple */
--accent-cyan: 186 100% 50%;
--accent-purple: 260 80% 60%;
--accent-gradient: linear-gradient(135deg, hsl(186 100% 50%) 0%, hsl(260 80% 60%) 100%);
--primary: 210 10% 98%;
--primary-foreground: 210 10% 5%;
/* Radii and shadows */
--radius: 8px;
--radius-lg: 12px;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
height: 100%;
}
}
@layer utilities {
.text-gradient {
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.bg-gradient-accent {
background: var(--accent-gradient);
}
.glow-cyan {
box-shadow: 0 0 20px hsl(var(--accent-cyan) / 0.3), 0 0 40px hsl(var(--accent-cyan) / 0.2), 0 0
60px hsl(var(--accent-cyan) / 0.1);
}
.glow-purple {
box-shadow: 0 0 20px hsl(var(--accent-purple) / 0.3), 0 0 40px hsl(var(--accent-purple) / 0.2),
0 0 60px hsl(var(--accent-purple) / 0.1);
}
/* Animated background gradient */
@keyframes gradient-shift {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
.animate-gradient {
background-size: 200% 200%;
animation: gradient-shift 15s ease infinite;
}
/* Floating animation */
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
/* Pulse glow */
@keyframes pulse-glow {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
/* Stagger delays */
.delay-100 {
animation-delay: 100ms;
}
.delay-200 {
animation-delay: 200ms;
}
.delay-300 {
animation-delay: 300ms;
}
.delay-400 {
animation-delay: 400ms;
}
.delay-500 {
animation-delay: 500ms;
}
/* Motion Preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
}

View file

@ -0,0 +1,38 @@
import type { Metadata } from 'next';
import { Space_Grotesk, Space_Mono } from 'next/font/google';
import './globals.css';
const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap',
});
const spaceMono = Space_Mono({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-mono',
display: 'swap',
});
export const metadata: Metadata = {
title: 'TPMJS Tutorial - Learn How It Works',
description: 'Interactive tutorial explaining TPMJS - the Tool Package Manager for AI Agents',
openGraph: {
title: 'TPMJS Tutorial',
description: 'Interactive tutorial explaining TPMJS - the Tool Package Manager for AI Agents',
type: 'website',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): React.ReactElement {
return (
<html lang="en" className={`${spaceGrotesk.variable} ${spaceMono.variable}`}>
<body className={spaceGrotesk.className}>{children}</body>
</html>
);
}

View file

@ -0,0 +1,16 @@
import { OverviewSlideshow } from '@/components/OverviewSlideshow';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Overview | TPMJS Tutorials',
description:
'General overview of TPMJS - the tool registry for AI agents. Learn what TPMJS is, how it works, and who uses it.',
};
export default function OverviewPage(): React.ReactElement {
return (
<main className="h-screen w-screen overflow-hidden">
<OverviewSlideshow />
</main>
);
}

View file

@ -0,0 +1,282 @@
'use client';
import { motion } from 'framer-motion';
import Link from 'next/link';
const tutorials = [
{
id: 'overview',
title: 'General Overview',
subtitle: 'Start Here',
description: 'What is TPMJS? Learn about the registry, ecosystem, and architecture.',
href: '/overview',
gradient: 'from-emerald-500 to-teal-600',
icon: (
<svg
className="w-8 h-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
/>
</svg>
),
features: ['What is TPMJS', 'Ecosystem', 'Architecture'],
},
{
id: 'agents',
title: 'For Agent Developers',
subtitle: 'Search & Execute',
description: 'Give your agent access to 200+ tools at runtime. No imports. No bundling.',
href: '/agents',
gradient: 'from-cyan-500 to-blue-600',
icon: (
<svg
className="w-8 h-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
/>
</svg>
),
features: ['searchTpmjsToolsTool', 'registryExecuteTool', 'Sandbox execution'],
},
{
id: 'authors',
title: 'For Package Authors',
subtitle: 'Publish Your Tools',
description:
'Get your AI SDK tools discovered by agents worldwide. npm publish is all it takes.',
href: '/authors',
gradient: 'from-purple-500 to-pink-600',
icon: (
<svg
className="w-8 h-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
),
features: ['tpmjs keyword', 'Schema extraction', 'Quality scoring'],
},
{
id: 'first-tool',
title: 'Build Your First Tool',
subtitle: 'Step-by-Step',
description: 'Create an AI SDK tool from scratch and publish it to the registry in 10 minutes.',
href: '/first-tool',
gradient: 'from-orange-500 to-amber-600',
icon: (
<svg
className="w-8 h-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z"
/>
</svg>
),
features: ['npm init', 'Zod schema', 'npm publish'],
},
{
id: 'agent-example',
title: 'Real-World Agent',
subtitle: 'Working Example',
description: 'Build a complete agent that discovers and uses tools dynamically.',
href: '/agent-example',
gradient: 'from-rose-500 to-pink-600',
icon: (
<svg
className="w-8 h-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25zm.75-12h9v9h-9v-9z"
/>
</svg>
),
features: ['generateText', 'maxSteps', 'Meta-tools'],
},
{
id: 'playground',
title: 'Interactive Playground',
subtitle: 'Try Before Install',
description: 'Test any tool directly in your browser without installation.',
href: '/playground',
gradient: 'from-sky-500 to-blue-600',
icon: (
<svg
className="w-8 h-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"
/>
</svg>
),
features: ['Live execution', 'Schema forms', 'JSON output'],
},
];
export default function TutorialHome(): React.ReactElement {
return (
<main className="min-h-screen bg-[#0a0a0f] flex flex-col items-center justify-center px-6 py-12">
{/* Background */}
<div className="fixed inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
<div
className="fixed inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
{/* Content */}
<div className="relative z-10 max-w-6xl w-full">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-6">
<span className="text-cyan-400 font-mono text-sm">tpmjs.com</span>
<span className="text-white/30">/</span>
<span className="text-white/60 font-mono text-sm">tutorials</span>
</div>
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4">Learn TPMJS</h1>
<p className="text-xl text-white/50 max-w-2xl mx-auto">
The tool registry for the Vercel AI SDK. Choose your path.
</p>
</motion.div>
{/* Tutorial Cards */}
<div className="grid md:grid-cols-3 gap-6">
{tutorials.map((tutorial, index) => (
<motion.div
key={tutorial.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 + index * 0.1 }}
>
<Link href={tutorial.href} className="block group">
<div className="relative p-8 rounded-2xl bg-white/5 border border-white/10 hover:border-white/20 transition-all duration-300 hover:bg-white/[0.07] overflow-hidden">
{/* Gradient glow on hover */}
<div
className={`absolute inset-0 bg-gradient-to-br ${tutorial.gradient} opacity-0 group-hover:opacity-5 transition-opacity duration-300`}
/>
{/* Icon */}
<div
className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${tutorial.gradient} text-white mb-6`}
>
{tutorial.icon}
</div>
{/* Content */}
<div className="relative">
<div className="text-xs font-mono text-white/40 mb-2">{tutorial.subtitle}</div>
<h2 className="text-2xl font-bold text-white mb-3">{tutorial.title}</h2>
<p className="text-white/50 mb-6">{tutorial.description}</p>
{/* Features */}
<div className="flex flex-wrap gap-2 mb-6">
{tutorial.features.map((feature) => (
<span
key={feature}
className="px-2 py-1 rounded text-xs font-mono bg-white/5 text-white/60"
>
{feature}
</span>
))}
</div>
{/* CTA */}
<div
className={`inline-flex items-center gap-2 text-sm font-medium bg-gradient-to-r ${tutorial.gradient} bg-clip-text text-transparent group-hover:gap-3 transition-all`}
>
Start Tutorial
<svg
className="w-4 h-4 text-white/60 group-hover:translate-x-1 transition-transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
</div>
</Link>
</motion.div>
))}
</div>
{/* Footer */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-center mt-12"
>
<a
href="https://tpmjs.com"
target="_blank"
rel="noopener noreferrer"
className="text-white/30 hover:text-white/50 text-sm transition-colors"
>
Back to tpmjs.com
</a>
</motion.div>
</div>
</main>
);
}

View file

@ -0,0 +1,16 @@
import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Interactive Playground | TPMJS Tutorials',
description:
'Learn how to use the TPMJS playground to test tools directly in your browser. No installation required.',
};
export default function PlaygroundPage(): React.ReactElement {
return (
<main className="h-screen w-screen overflow-hidden">
<PlaygroundSlideshow />
</main>
);
}

View file

@ -0,0 +1,77 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { AgentCodeSlide } from './agent-example-slides/AgentCodeSlide';
import { AgentExampleNextStepsSlide } from './agent-example-slides/AgentExampleNextStepsSlide';
import { AgentExampleWelcomeSlide } from './agent-example-slides/AgentExampleWelcomeSlide';
import { ExecutionFlowSlide } from './agent-example-slides/ExecutionFlowSlide';
import { SetupCodeSlide } from './agent-example-slides/SetupCodeSlide';
import { TheGoalSlide } from './agent-example-slides/TheGoalSlide';
const SLIDES = [
{ id: 'welcome', Component: AgentExampleWelcomeSlide },
{ id: 'the-goal', Component: TheGoalSlide },
{ id: 'setup', Component: SetupCodeSlide },
{ id: 'agent-code', Component: AgentCodeSlide },
{ id: 'execution-flow', Component: ExecutionFlowSlide },
{ id: 'next-steps', Component: AgentExampleNextStepsSlide },
];
export function AgentExampleSlideshow(): React.ReactElement {
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
totalSlides: SLIDES.length,
});
return (
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
<Link
href="/"
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Tutorials
</Link>
<AnimatePresence mode="wait" custom={direction}>
{SLIDES.map((slide, index) => (
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
<slide.Component />
</Slide>
))}
</AnimatePresence>
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
Use arrow keys or click to navigate
</div>
</div>
);
}

View file

@ -0,0 +1,91 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { DiscoverySlide } from './slides/DiscoverySlide';
import { GetStartedSlide } from './slides/GetStartedSlide';
import { HowItWorksSlide } from './slides/HowItWorksSlide';
import { IntegrationSlide } from './slides/IntegrationSlide';
import { ProblemSlide } from './slides/ProblemSlide';
import { QualitySlide } from './slides/QualitySlide';
import { SolutionSlide } from './slides/SolutionSlide';
import { ToolDetailSlide } from './slides/ToolDetailSlide';
import { WelcomeSlide } from './slides/WelcomeSlide';
const SLIDES = [
{ id: 'welcome', Component: WelcomeSlide },
{ id: 'problem', Component: ProblemSlide },
{ id: 'solution', Component: SolutionSlide },
{ id: 'how-it-works', Component: HowItWorksSlide },
{ id: 'discovery', Component: DiscoverySlide },
{ id: 'integration', Component: IntegrationSlide },
{ id: 'quality', Component: QualitySlide },
{ id: 'tool-detail', Component: ToolDetailSlide },
{ id: 'get-started', Component: GetStartedSlide },
];
export function AgentSlideshow(): React.ReactElement {
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
totalSlides: SLIDES.length,
});
return (
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
{/* Subtle grid pattern */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
{/* Back button */}
<Link
href="/"
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Tutorials
</Link>
{/* Slides */}
<AnimatePresence mode="wait" custom={direction}>
{SLIDES.map((slide, index) => (
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
<slide.Component />
</Slide>
))}
</AnimatePresence>
{/* Navigation */}
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
{/* Progress */}
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
{/* Keyboard hint */}
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
Use arrow keys or click to navigate
</div>
</div>
);
}

View file

@ -0,0 +1,91 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { AuthorNextStepsSlide } from './author-slides/AuthorNextStepsSlide';
import { AuthorWelcomeSlide } from './author-slides/AuthorWelcomeSlide';
import { GetDiscoveredSlide } from './author-slides/GetDiscoveredSlide';
import { HealthChecksSlide } from './author-slides/HealthChecksSlide';
import { HowSyncWorksSlide } from './author-slides/HowSyncWorksSlide';
import { PackageSetupSlide } from './author-slides/PackageSetupSlide';
import { QualityScoringSlide } from './author-slides/QualityScoringSlide';
import { ToolDefinitionSlide } from './author-slides/ToolDefinitionSlide';
import { WhatIsRegistrySlide } from './author-slides/WhatIsRegistrySlide';
const SLIDES = [
{ id: 'welcome', Component: AuthorWelcomeSlide },
{ id: 'what-is', Component: WhatIsRegistrySlide },
{ id: 'sync', Component: HowSyncWorksSlide },
{ id: 'setup', Component: PackageSetupSlide },
{ id: 'definition', Component: ToolDefinitionSlide },
{ id: 'quality', Component: QualityScoringSlide },
{ id: 'health', Component: HealthChecksSlide },
{ id: 'discovered', Component: GetDiscoveredSlide },
{ id: 'next-steps', Component: AuthorNextStepsSlide },
];
export function AuthorSlideshow(): React.ReactElement {
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
totalSlides: SLIDES.length,
});
return (
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
{/* Subtle grid pattern */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
{/* Back button */}
<Link
href="/"
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Tutorials
</Link>
{/* Slides */}
<AnimatePresence mode="wait" custom={direction}>
{SLIDES.map((slide, index) => (
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
<slide.Component />
</Slide>
))}
</AnimatePresence>
{/* Navigation */}
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
{/* Progress */}
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
{/* Keyboard hint */}
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
Use arrow keys or click to navigate
</div>
</div>
);
}

View file

@ -0,0 +1,77 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { FirstToolNextStepsSlide } from './first-tool-slides/FirstToolNextStepsSlide';
import { FirstToolWelcomeSlide } from './first-tool-slides/FirstToolWelcomeSlide';
import { PackageJsonSlide } from './first-tool-slides/PackageJsonSlide';
import { ProjectSetupSlide } from './first-tool-slides/ProjectSetupSlide';
import { PublishSlide } from './first-tool-slides/PublishSlide';
import { WriteToolSlide } from './first-tool-slides/WriteToolSlide';
const SLIDES = [
{ id: 'welcome', Component: FirstToolWelcomeSlide },
{ id: 'project-setup', Component: ProjectSetupSlide },
{ id: 'write-tool', Component: WriteToolSlide },
{ id: 'package-json', Component: PackageJsonSlide },
{ id: 'publish', Component: PublishSlide },
{ id: 'next-steps', Component: FirstToolNextStepsSlide },
];
export function FirstToolSlideshow(): React.ReactElement {
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
totalSlides: SLIDES.length,
});
return (
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
<Link
href="/"
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Tutorials
</Link>
<AnimatePresence mode="wait" custom={direction}>
{SLIDES.map((slide, index) => (
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
<slide.Component />
</Slide>
))}
</AnimatePresence>
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
Use arrow keys or click to navigate
</div>
</div>
);
}

View file

@ -0,0 +1,85 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { ArchitectureSlide } from './overview-slides/ArchitectureSlide';
import { EcosystemSlide } from './overview-slides/EcosystemSlide';
import { ExploreSlide } from './overview-slides/ExploreSlide';
import { OverviewWelcomeSlide } from './overview-slides/OverviewWelcomeSlide';
import { UseCasesSlide } from './overview-slides/UseCasesSlide';
import { WhatIsTpmjsSlide } from './overview-slides/WhatIsTpmjsSlide';
const SLIDES = [
{ id: 'welcome', Component: OverviewWelcomeSlide },
{ id: 'what-is-tpmjs', Component: WhatIsTpmjsSlide },
{ id: 'ecosystem', Component: EcosystemSlide },
{ id: 'architecture', Component: ArchitectureSlide },
{ id: 'use-cases', Component: UseCasesSlide },
{ id: 'explore', Component: ExploreSlide },
];
export function OverviewSlideshow(): React.ReactElement {
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
totalSlides: SLIDES.length,
});
return (
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
{/* Subtle grid pattern */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
{/* Back button */}
<Link
href="/"
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Tutorials
</Link>
{/* Slides */}
<AnimatePresence mode="wait" custom={direction}>
{SLIDES.map((slide, index) => (
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
<slide.Component />
</Slide>
))}
</AnimatePresence>
{/* Navigation */}
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
{/* Progress */}
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
{/* Keyboard hint */}
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
Use arrow keys or click to navigate
</div>
</div>
);
}

View file

@ -0,0 +1,77 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { FillParametersSlide } from './playground-slides/FillParametersSlide';
import { FindToolSlide } from './playground-slides/FindToolSlide';
import { OpenPlaygroundSlide } from './playground-slides/OpenPlaygroundSlide';
import { PlaygroundNextStepsSlide } from './playground-slides/PlaygroundNextStepsSlide';
import { PlaygroundWelcomeSlide } from './playground-slides/PlaygroundWelcomeSlide';
import { SeeResultsSlide } from './playground-slides/SeeResultsSlide';
const SLIDES = [
{ id: 'welcome', Component: PlaygroundWelcomeSlide },
{ id: 'find-tool', Component: FindToolSlide },
{ id: 'open-playground', Component: OpenPlaygroundSlide },
{ id: 'fill-parameters', Component: FillParametersSlide },
{ id: 'see-results', Component: SeeResultsSlide },
{ id: 'next-steps', Component: PlaygroundNextStepsSlide },
];
export function PlaygroundSlideshow(): React.ReactElement {
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
totalSlides: SLIDES.length,
});
return (
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
<Link
href="/"
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Tutorials
</Link>
<AnimatePresence mode="wait" custom={direction}>
{SLIDES.map((slide, index) => (
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
<slide.Component />
</Slide>
))}
</AnimatePresence>
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
Use arrow keys or click to navigate
</div>
</div>
);
}

View file

@ -0,0 +1,46 @@
'use client';
import { type Variants, motion } from 'framer-motion';
import type { ReactNode } from 'react';
interface SlideProps {
children: ReactNode;
isActive: boolean;
direction: 'left' | 'right' | null;
}
const variants: Variants = {
enter: (direction: 'left' | 'right') => ({
x: direction === 'right' ? '100%' : '-100%',
opacity: 0,
}),
center: {
x: 0,
opacity: 1,
},
exit: (direction: 'left' | 'right') => ({
x: direction === 'right' ? '-100%' : '100%',
opacity: 0,
}),
};
export function Slide({ children, isActive, direction }: SlideProps): React.ReactElement | null {
if (!isActive) return null;
return (
<motion.div
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: 'spring', stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
}}
className="absolute inset-0 flex items-center justify-center"
>
{children}
</motion.div>
);
}

View file

@ -0,0 +1,69 @@
'use client';
import { motion } from 'framer-motion';
interface SlideNavigationProps {
onNext: () => void;
onPrev: () => void;
isFirst: boolean;
isLast: boolean;
}
export function SlideNavigation({
onNext,
onPrev,
isFirst,
isLast,
}: SlideNavigationProps): React.ReactElement {
return (
<>
{/* Previous button */}
<motion.button
onClick={onPrev}
disabled={isFirst}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: isFirst ? 0.3 : 1, x: 0 }}
whileHover={!isFirst ? { scale: 1.1 } : undefined}
whileTap={!isFirst ? { scale: 0.95 } : undefined}
className={`
fixed left-8 top-1/2 -translate-y-1/2 z-50
w-12 h-12 rounded-full
bg-white/10 backdrop-blur-sm
border border-white/20
flex items-center justify-center
transition-colors
${isFirst ? 'cursor-not-allowed' : 'hover:bg-white/20 cursor-pointer'}
`}
aria-label="Previous slide"
>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</motion.button>
{/* Next button */}
<motion.button
onClick={onNext}
disabled={isLast}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: isLast ? 0.3 : 1, x: 0 }}
whileHover={!isLast ? { scale: 1.1 } : undefined}
whileTap={!isLast ? { scale: 0.95 } : undefined}
className={`
fixed right-8 top-1/2 -translate-y-1/2 z-50
w-12 h-12 rounded-full
bg-white/10 backdrop-blur-sm
border border-white/20
flex items-center justify-center
transition-colors
${isLast ? 'cursor-not-allowed' : 'hover:bg-white/20 cursor-pointer'}
`}
aria-label="Next slide"
>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</motion.button>
</>
);
}

View file

@ -0,0 +1,46 @@
'use client';
import { motion } from 'framer-motion';
interface SlideProgressProps {
currentSlide: number;
totalSlides: number;
goTo: (index: number) => void;
}
export function SlideProgress({
currentSlide,
totalSlides,
goTo,
}: SlideProgressProps): React.ReactElement {
return (
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3">
{Array.from({ length: totalSlides }).map((_, index) => (
<button
key={index}
onClick={() => goTo(index)}
className="group relative p-1"
aria-label={`Go to slide ${index + 1}`}
>
<div
className={`
w-2 h-2 rounded-full transition-all duration-300
${
index === currentSlide
? 'bg-white scale-125'
: 'bg-white/30 group-hover:bg-white/50'
}
`}
/>
{index === currentSlide && (
<motion.div
layoutId="active-dot"
className="absolute inset-0 m-auto w-4 h-4 rounded-full border border-white/50"
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
</button>
))}
</div>
);
}

View file

@ -0,0 +1,104 @@
'use client';
import { motion } from 'framer-motion';
export function AgentCodeSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-rose-900/10 via-transparent to-pink-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-5xl w-full"
>
<div className="text-rose-400/60 font-mono text-sm mb-4">Step 2</div>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">The Agent Loop</h2>
<p className="text-xl text-white/50 mb-6">Pass meta-tools to generateText with maxSteps</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-[#0d1117] rounded-xl border border-white/10 p-5 text-left max-w-3xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-3">agent.ts</div>
<pre className="text-xs font-mono overflow-x-auto leading-relaxed">
<code>
<span className="text-purple-400">const</span>
<span className="text-white"> result = </span>
<span className="text-purple-400">await</span>
<span className="text-yellow-400"> generateText</span>
<span className="text-white">{'({'}</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">model</span>
<span className="text-white">: </span>
<span className="text-yellow-400">openai</span>
<span className="text-white">(</span>
<span className="text-amber-300">{"'gpt-4o'"}</span>
<span className="text-white">),</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">tools</span>
<span className="text-white">{': {'}</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">search</span>
<span className="text-white">: </span>
<span className="text-rose-400">searchTpmjsToolsTool</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">execute</span>
<span className="text-white">: </span>
<span className="text-rose-400">registryExecuteTool</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' },'}</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">maxSteps</span>
<span className="text-white">: </span>
<span className="text-amber-300">10</span>
<span className="text-white">,</span>
<span className="text-white/50">{' // Allow multiple tool calls'}</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">system</span>
<span className="text-white">: </span>
<span className="text-amber-300">`You are an assistant that can find and use</span>
{'\n'}
<span className="text-amber-300">
{' '}
tools from the TPMJS registry. First search for
</span>
{'\n'}
<span className="text-amber-300"> relevant tools, then execute them.`</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">prompt</span>
<span className="text-white">: userRequest,</span>
{'\n'}
<span className="text-white">{'});'}</span>
</code>
</pre>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-6 p-4 rounded-lg bg-rose-500/10 border border-rose-500/20 max-w-lg mx-auto"
>
<p className="text-rose-400 text-sm">
<span className="font-semibold">Key:</span> maxSteps allows the agent to search, then
execute, then reason about results
</p>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,80 @@
'use client';
import { motion } from 'framer-motion';
export function AgentExampleNextStepsSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-rose-900/20 via-transparent to-pink-900/20 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Try It Yourself</h2>
<p className="text-xl text-white/50 mb-10">Complete working example</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-[#0d1117] rounded-xl border border-white/10 p-6 text-left max-w-2xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-3">Clone & Run</div>
<pre className="text-sm font-mono overflow-x-auto">
<code>
<span className="text-rose-400">git clone</span>
<span className="text-white"> github.com/tpmjs/agent-example</span>
{'\n'}
<span className="text-rose-400">cd</span>
<span className="text-white"> agent-example</span>
{'\n'}
<span className="text-rose-400">npm</span>
<span className="text-white"> install</span>
{'\n'}
<span className="text-rose-400">npm</span>
<span className="text-white"> run dev</span>
</code>
</pre>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mt-8 grid grid-cols-2 gap-4 max-w-xl mx-auto"
>
<a
href="https://tpmjs.com"
target="_blank"
rel="noopener noreferrer"
className="p-5 rounded-xl bg-white/5 border border-rose-500/20 text-left hover:bg-white/10 hover:border-rose-500/40 transition-all group"
>
<div className="text-2xl mb-2">🔍</div>
<div className="text-sm font-semibold text-rose-400 mb-1">Browse Registry</div>
<div className="text-xs text-white/50">See all available tools</div>
</a>
<a
href="/agents"
className="p-5 rounded-xl bg-white/5 border border-pink-500/20 text-left hover:bg-white/10 hover:border-pink-500/40 transition-all group"
>
<div className="text-2xl mb-2">📚</div>
<div className="text-sm font-semibold text-pink-400 mb-1">Agent Docs</div>
<div className="text-xs text-white/50">Deep dive into meta-tools</div>
</a>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="mt-10 text-white/30 text-xs font-mono"
>
Your agent can now use 200+ tools without installing any of them
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,87 @@
'use client';
import { motion } from 'framer-motion';
export function AgentExampleWelcomeSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-rose-900/20 via-transparent to-pink-900/20 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="relative z-10 max-w-4xl"
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="mb-6"
>
<span className="text-sm font-mono text-rose-400/60 tracking-widest uppercase">
Practical Example
</span>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mb-8"
>
<span className="text-5xl md:text-6xl font-bold bg-gradient-to-r from-rose-400 to-pink-400 bg-clip-text text-transparent">
Real-World Agent
</span>
</motion.div>
<motion.h1
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-2xl md:text-3xl font-medium text-white/90 mb-6"
>
Build an agent that discovers and uses tools dynamically
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.7 }}
className="text-lg text-white/50 font-mono max-w-xl mx-auto"
>
A complete working example with search, execute, and reasoning
</motion.p>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 1.2 }}
className="absolute bottom-12 flex flex-col items-center"
>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY, ease: 'easeInOut' }}
className="text-white/40"
>
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M19 9l-7 7-7-7"
/>
</svg>
</motion.div>
<span className="mt-2 text-sm text-white/30 font-mono">Press any key</span>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,95 @@
'use client';
import { motion } from 'framer-motion';
const steps = [
{
step: '1',
action: 'Agent receives request',
detail: '"Format this markdown..."',
color: 'rose',
},
{
step: '2',
action: 'Calls searchTpmjsToolsTool',
detail: 'query: "markdown formatter"',
color: 'pink',
},
{
step: '3',
action: 'Receives tool list',
detail: '[markdownFormat, mdToHtml, ...]',
color: 'purple',
},
{
step: '4',
action: 'Calls registryExecuteTool',
detail: 'toolId: "markdownFormat", args: {...}',
color: 'violet',
},
{
step: '5',
action: 'Returns result to user',
detail: 'Formatted blog post',
color: 'indigo',
},
];
export function ExecutionFlowSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-rose-900/10 via-transparent to-pink-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Execution Flow</h2>
<p className="text-xl text-white/50 mb-10">What happens when the agent runs</p>
<div className="space-y-3 max-w-2xl mx-auto">
{steps.map((step, index) => (
<motion.div
key={step.step}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 + index * 0.1 }}
className={`flex items-center gap-4 p-4 rounded-lg bg-white/5 border border-${step.color}-500/20`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
step.color === 'rose'
? 'bg-rose-500/20 text-rose-400'
: step.color === 'pink'
? 'bg-pink-500/20 text-pink-400'
: step.color === 'purple'
? 'bg-purple-500/20 text-purple-400'
: step.color === 'violet'
? 'bg-violet-500/20 text-violet-400'
: 'bg-indigo-500/20 text-indigo-400'
}`}
>
{step.step}
</div>
<div className="text-left flex-1">
<div className="text-sm font-semibold text-white/80">{step.action}</div>
<div className="text-xs text-white/40 font-mono">{step.detail}</div>
</div>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.9 }}
className="mt-8 text-white/40 text-sm font-mono"
>
All tool execution happens in a secure sandbox
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,81 @@
'use client';
import { motion } from 'framer-motion';
export function SetupCodeSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-rose-900/10 via-transparent to-pink-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<div className="text-rose-400/60 font-mono text-sm mb-4">Step 1</div>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Install & Setup</h2>
<p className="text-xl text-white/50 mb-8">Add the TPMJS meta-tools to your project</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-[#0d1117] rounded-xl border border-white/10 p-6 text-left max-w-2xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-3">Terminal</div>
<pre className="text-sm font-mono overflow-x-auto">
<code>
<span className="text-rose-400">npm</span>
<span className="text-white">
{' '}
install @tpmjs/search-registry @tpmjs/registry-execute
</span>
</code>
</pre>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mt-6 bg-[#0d1117] rounded-xl border border-white/10 p-6 text-left max-w-2xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-3">agent.ts</div>
<pre className="text-sm font-mono overflow-x-auto">
<code>
<span className="text-purple-400">import</span>
<span className="text-white">{' { searchTpmjsToolsTool } '}</span>
<span className="text-purple-400">from</span>
{'\n'}
<span className="text-amber-300">{" '@tpmjs/search-registry'"}</span>
<span className="text-white">;</span>
{'\n'}
<span className="text-purple-400">import</span>
<span className="text-white">{' { registryExecuteTool } '}</span>
<span className="text-purple-400">from</span>
{'\n'}
<span className="text-amber-300">{" '@tpmjs/registry-execute'"}</span>
<span className="text-white">;</span>
{'\n'}
<span className="text-purple-400">import</span>
<span className="text-white">{' { generateText } '}</span>
<span className="text-purple-400">from</span>
<span className="text-amber-300">{" 'ai'"}</span>
<span className="text-white">;</span>
</code>
</pre>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
className="mt-6 text-white/40 text-sm font-mono"
>
Two tools that give your agent access to the entire registry
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,77 @@
'use client';
import { motion } from 'framer-motion';
export function TheGoalSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-rose-900/10 via-transparent to-pink-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">The Goal</h2>
<p className="text-xl text-white/50 mb-10">
An agent that can handle any request by finding the right tools
</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-[#0d1117] rounded-xl border border-white/10 p-6 text-left max-w-2xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-4">User Request</div>
<p className="text-lg text-white/80 italic">
{'"Convert this markdown to a formatted blog post and generate a summary"'}
</p>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="mt-8 flex items-center justify-center"
>
<svg
className="w-6 h-6 text-rose-400/50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="mt-8 grid grid-cols-3 gap-4 max-w-2xl mx-auto"
>
<div className="p-4 rounded-lg bg-rose-500/10 border border-rose-500/20 text-center">
<div className="text-rose-400 font-semibold text-sm mb-1">1. Search</div>
<div className="text-xs text-white/50">Find markdown tools</div>
</div>
<div className="p-4 rounded-lg bg-pink-500/10 border border-pink-500/20 text-center">
<div className="text-pink-400 font-semibold text-sm mb-1">2. Execute</div>
<div className="text-xs text-white/50">Format the content</div>
</div>
<div className="p-4 rounded-lg bg-purple-500/10 border border-purple-500/20 text-center">
<div className="text-purple-400 font-semibold text-sm mb-1">3. Return</div>
<div className="text-xs text-white/50">Give result to user</div>
</div>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,82 @@
'use client';
import { motion } from 'framer-motion';
const steps = [
{ num: '1', text: 'Add "tpmjs" to your package.json keywords' },
{ num: '2', text: 'Define your tools in the "tpmjs" field' },
{ num: '3', text: 'npm publish' },
];
export function AuthorNextStepsSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div
className="absolute w-[600px] h-[600px] rounded-full bg-gradient-to-r from-purple-500/10 to-pink-500/10 blur-[100px]"
style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
/>
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-3xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Ready to Publish?</h2>
<p className="text-xl text-white/50 mb-10">Three steps. That&apos;s it.</p>
<div className="space-y-4 max-w-md mx-auto mb-10">
{steps.map((step, i) => (
<motion.div
key={step.num}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 + i * 0.1 }}
className="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10 text-left"
>
<span className="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/20 text-purple-400 flex items-center justify-center font-mono text-lg">
{step.num}
</span>
<span className="text-white/70">{step.text}</span>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="flex flex-wrap justify-center gap-4"
>
<a
href="https://tpmjs.com/tool-search"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg bg-purple-500/20 text-purple-400 text-sm font-medium hover:bg-purple-500/30 transition-colors"
>
Browse Existing Tools
</a>
<a
href="https://tpmjs.com/playground"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg bg-cyan-500/20 text-cyan-400 text-sm font-medium hover:bg-cyan-500/30 transition-colors"
>
Try the Playground
</a>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="mt-10 text-white/30 text-sm"
>
Indexed within 2 minutes. No registration required.
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,58 @@
'use client';
import { motion } from 'framer-motion';
export function AuthorWelcomeSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-transparent to-pink-900/20 pointer-events-none" />
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="relative z-10"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-6"
>
<span className="px-4 py-2 rounded-full bg-purple-500/20 text-purple-400 text-sm font-mono">
For Package Authors
</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="text-5xl md:text-7xl font-bold text-white mb-6"
>
Publish Your Tools
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="text-xl md:text-2xl text-white/50 max-w-2xl mx-auto mb-8"
>
Get your AI SDK tools discovered by agents worldwide.
<br />
<span className="text-purple-400">npm publish</span> is all it takes.
</motion.p>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="text-white/30 text-sm font-mono"
>
Press arrow keys or click to continue
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,72 @@
'use client';
import { motion } from 'framer-motion';
const searchExample = [
{ id: 'agent', line: 'Agent: "I need to format some markdown"', color: 'white' },
{ id: 'search', line: 'searchTpmjsToolsTool({ query: "markdown format" })', color: 'cyan' },
{ id: 'result', line: '→ Found: @your-org/tools::formatMarkdownTable', color: 'emerald' },
{ id: 'execute', line: 'registryExecuteTool({ toolId: "...", params })', color: 'purple' },
{ id: 'done', line: '→ Your tool runs. Agent gets result.', color: 'yellow' },
];
export function GetDiscoveredSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/10 via-transparent to-purple-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-3xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">How Agents Find You</h2>
<p className="text-xl text-white/50 mb-10">Your tool in action</p>
<div className="space-y-3 text-left max-w-xl mx-auto">
{searchExample.map((item, i) => (
<motion.div
key={item.id}
initial={{ opacity: 0, x: item.id === 'agent' ? -20 : 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 + i * 0.15 }}
className={`p-3 rounded-lg font-mono text-sm ${
item.id === 'agent' ? 'bg-white/5' : 'bg-white/5 ml-4'
}`}
>
<span
className={
item.color === 'cyan'
? 'text-cyan-400'
: item.color === 'purple'
? 'text-purple-400'
: item.color === 'emerald'
? 'text-emerald-400'
: item.color === 'yellow'
? 'text-yellow-400'
: 'text-white/70'
}
>
{item.line}
</span>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.2 }}
className="mt-10 p-4 rounded-xl bg-white/5 border border-white/10"
>
<div className="text-sm text-white/60">
The agent never installed your package.
<br />
<span className="text-cyan-400">They found and ran it at runtime.</span>
</div>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,91 @@
'use client';
import { motion } from 'framer-motion';
const checks = [
{
status: 'HEALTHY',
icon: '✓',
title: 'Import Health',
desc: 'We require() your package to verify it loads',
color: 'emerald',
},
{
status: 'HEALTHY',
icon: '✓',
title: 'Execution Health',
desc: 'We call your tool with test params',
color: 'emerald',
},
{
status: 'BROKEN',
icon: '!',
title: 'Labeled, Not Hidden',
desc: 'Failed tools show warning, still searchable',
color: 'yellow',
},
];
export function HealthChecksSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-900/10 via-transparent to-yellow-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-3xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Health Checks</h2>
<p className="text-xl text-white/50 mb-12">We verify your tools actually work</p>
<div className="space-y-4 max-w-xl mx-auto">
{checks.map((check, i) => (
<motion.div
key={check.title}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 + i * 0.15 }}
className="flex items-start gap-4 p-4 rounded-xl bg-white/5 border border-white/10 text-left"
>
<div
className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold ${
check.color === 'emerald'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-yellow-500/20 text-yellow-400'
}`}
>
{check.icon}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-white">{check.title}</span>
<span
className={`text-xs font-mono px-2 py-0.5 rounded ${
check.status === 'HEALTHY'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-yellow-500/20 text-yellow-400'
}`}
>
{check.status}
</span>
</div>
<div className="text-sm text-white/50">{check.desc}</div>
</div>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="mt-8 text-white/40 text-sm"
>
Health checks run hourly. Fix issues anytime by publishing a new version.
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,65 @@
'use client';
import { motion } from 'framer-motion';
const syncSteps = [
{ step: '1', text: 'You publish to npm with tpmjs keyword', color: 'purple' },
{ step: '2', text: 'Registry detects your package within 2 minutes', color: 'cyan' },
{ step: '3', text: 'We extract schemas from your package.json', color: 'emerald' },
{ step: '4', text: 'Your tool appears in search results', color: 'yellow' },
];
export function HowSyncWorksSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/10 via-transparent to-cyan-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-3xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Automatic Discovery</h2>
<p className="text-xl text-white/50 mb-12">Publish once. We handle the rest.</p>
<div className="space-y-4 text-left max-w-xl mx-auto">
{syncSteps.map((item, i) => (
<motion.div
key={item.step}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 + i * 0.15 }}
className="flex items-start gap-4"
>
<span
className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-lg font-mono ${
item.color === 'purple'
? 'bg-purple-500/20 text-purple-400'
: item.color === 'cyan'
? 'bg-cyan-500/20 text-cyan-400'
: item.color === 'emerald'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-yellow-500/20 text-yellow-400'
}`}
>
{item.step}
</span>
<span className="text-white/70 pt-2">{item.text}</span>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.2 }}
className="mt-10 p-4 rounded-xl bg-purple-500/10 border border-purple-500/20"
>
<code className="text-purple-400 font-mono text-sm">npm publish</code>
<div className="text-white/40 text-sm mt-1">No registration. No API keys. Just npm.</div>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,87 @@
'use client';
import { motion } from 'framer-motion';
const packageJson = [
{ id: 'open', line: '{' },
{ id: 'name', line: ' "name": "@your-org/your-tools",' },
{ id: 'version', line: ' "version": "1.0.0",' },
{ id: 'keywords-open', line: ' "keywords": [' },
{ id: 'keyword', line: ' "tpmjs"' },
{ id: 'keywords-close', line: ' ],' },
{ id: 'tpmjs-open', line: ' "tpmjs": {' },
{ id: 'tools-open', line: ' "tools": [{ ... }]' },
{ id: 'tpmjs-close', line: ' }' },
{ id: 'close', line: '}' },
];
export function PackageSetupSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-900/10 via-transparent to-cyan-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Package Setup</h2>
<p className="text-xl text-white/50 mb-10">Two things: a keyword and a tpmjs field</p>
<div className="grid md:grid-cols-2 gap-8 max-w-3xl mx-auto">
{/* package.json */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
className="text-left"
>
<div className="text-xs text-emerald-400/60 font-mono mb-2">package.json</div>
<div className="bg-[#1e1e2e] p-4 rounded-xl font-mono text-sm">
{packageJson.map((item) => (
<div
key={item.id}
className={
item.line.includes('tpmjs')
? 'text-cyan-400'
: item.line.includes('"tpmjs"')
? 'text-purple-400'
: item.line.includes('tools')
? 'text-emerald-400'
: 'text-white/60'
}
>
{item.line}
</div>
))}
</div>
</motion.div>
{/* Explanation */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="text-left space-y-4"
>
<div className="p-4 rounded-lg bg-white/5 border border-cyan-500/20">
<div className="text-sm font-medium text-cyan-400 mb-1">tpmjs keyword</div>
<div className="text-xs text-white/50">Required. This is how we find you on npm.</div>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-purple-500/20">
<div className="text-sm font-medium text-purple-400 mb-1">tpmjs field</div>
<div className="text-xs text-white/50">
Defines your tools: name, description, schemas.
</div>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-emerald-500/20">
<div className="text-sm font-medium text-emerald-400 mb-1">tools array</div>
<div className="text-xs text-white/50">One entry per exported tool function.</div>
</div>
</motion.div>
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,90 @@
'use client';
import { motion } from 'framer-motion';
const factors = [
{
name: 'Schema Completeness',
weight: '40%',
desc: 'Rich parameter definitions with types and descriptions',
color: 'cyan',
},
{
name: 'npm Downloads',
weight: '30%',
desc: 'Community adoption signals quality',
color: 'purple',
},
{
name: 'Health Status',
weight: '20%',
desc: 'Can it import and execute successfully?',
color: 'emerald',
},
{
name: 'Documentation',
weight: '10%',
desc: 'Good descriptions help agents choose',
color: 'yellow',
},
];
export function QualityScoringSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-yellow-900/10 via-transparent to-cyan-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Quality Score</h2>
<p className="text-xl text-white/50 mb-10">Higher scores = better search ranking</p>
<div className="grid md:grid-cols-2 gap-4 max-w-3xl mx-auto">
{factors.map((factor, i) => (
<motion.div
key={factor.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + i * 0.1 }}
className="p-4 rounded-xl bg-white/5 border border-white/10 text-left"
>
<div className="flex items-center justify-between mb-2">
<span
className={`text-sm font-medium ${
factor.color === 'cyan'
? 'text-cyan-400'
: factor.color === 'purple'
? 'text-purple-400'
: factor.color === 'emerald'
? 'text-emerald-400'
: 'text-yellow-400'
}`}
>
{factor.name}
</span>
<span className="text-xs font-mono text-white/40">{factor.weight}</span>
</div>
<div className="text-xs text-white/50">{factor.desc}</div>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="mt-8 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 max-w-md mx-auto"
>
<div className="text-emerald-400 font-mono text-sm">qualityScore: 0.92</div>
<div className="text-white/40 text-xs mt-1">
Rich schemas + healthy + documented = high score
</div>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,111 @@
'use client';
import { motion } from 'framer-motion';
const toolDef = [
{ id: 'open', line: '{' },
{ id: 'name', line: ' "name": "formatMarkdownTable",' },
{ id: 'desc', line: ' "description": "Format CSV data as markdown table",' },
{ id: 'params-open', line: ' "parameters": [{' },
{ id: 'param-name', line: ' "name": "csvData",' },
{ id: 'param-type', line: ' "type": "string",' },
{ id: 'param-req', line: ' "required": true,' },
{ id: 'param-desc', line: ' "description": "CSV data to format"' },
{ id: 'params-close', line: ' }],' },
{ id: 'returns', line: ' "returns": { "type": "string" }' },
{ id: 'close', line: '}' },
];
export function ToolDefinitionSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/10 via-transparent to-emerald-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Tool Definition</h2>
<p className="text-xl text-white/50 mb-10">
Each tool needs a name, description, and parameter schema
</p>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{/* Tool JSON */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
className="text-left"
>
<div className="text-xs text-cyan-400/60 font-mono mb-2">tpmjs.tools[0]</div>
<div className="bg-[#1e1e2e] p-4 rounded-xl font-mono text-xs">
{toolDef.map((item) => (
<div
key={item.id}
className={
item.line.includes('"name"')
? 'text-emerald-400'
: item.line.includes('"description"')
? 'text-cyan-400'
: item.line.includes('"parameters"') || item.line.includes('"returns"')
? 'text-purple-400'
: item.line.includes('"type"') || item.line.includes('"required"')
? 'text-yellow-400'
: 'text-white/60'
}
>
{item.line}
</div>
))}
</div>
</motion.div>
{/* Field explanations */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="text-left space-y-3"
>
<div className="p-3 rounded-lg bg-white/5">
<div className="text-xs font-mono text-emerald-400 mb-1">name</div>
<div className="text-xs text-white/50">
Function export name. Must match your code.
</div>
</div>
<div className="p-3 rounded-lg bg-white/5">
<div className="text-xs font-mono text-cyan-400 mb-1">description</div>
<div className="text-xs text-white/50">
How agents understand what your tool does.
</div>
</div>
<div className="p-3 rounded-lg bg-white/5">
<div className="text-xs font-mono text-purple-400 mb-1">parameters</div>
<div className="text-xs text-white/50">
Input schema. Each param needs name, type, required.
</div>
</div>
<div className="p-3 rounded-lg bg-white/5">
<div className="text-xs font-mono text-purple-400 mb-1">returns</div>
<div className="text-xs text-white/50">
Output type. Helps agents understand responses.
</div>
</div>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="mt-8 text-white/40 text-sm"
>
Schemas are converted to Zod for AI SDK compatibility.
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,52 @@
'use client';
import { motion } from 'framer-motion';
const features = [
{ icon: '🔍', title: 'Discoverable', desc: 'Agents search by natural language query' },
{ icon: '📦', title: 'npm-native', desc: 'Synced from npm every 2 minutes' },
{ icon: '⚡', title: 'Instant Execution', desc: 'Run in sandbox, no install needed' },
];
export function WhatIsRegistrySlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/10 via-transparent to-purple-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">What is TPMJS?</h2>
<p className="text-xl text-white/50 mb-12">A tool registry for the Vercel AI SDK</p>
<div className="grid md:grid-cols-3 gap-6 max-w-3xl mx-auto">
{features.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.15 }}
className="p-6 rounded-xl bg-white/5 border border-white/10"
>
<div className="text-4xl mb-4">{feature.icon}</div>
<div className="text-lg font-semibold text-white mb-2">{feature.title}</div>
<div className="text-sm text-white/50">{feature.desc}</div>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="mt-10 text-white/40 text-sm"
>
AI agents use TPMJS to find and run tools they don&apos;t have installed.
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,71 @@
'use client';
import { motion } from 'framer-motion';
const nextSteps = [
{
title: 'Add Rich Metadata',
description: 'Include tpmjs field in package.json for better quality scores',
href: '/authors',
icon: '📋',
},
{
title: 'Test in Playground',
description: 'Try your tool at tpmjs.com/tool/your-package/yourTool',
href: '/playground',
icon: '🎮',
},
{
title: 'View on Registry',
description: 'See how agents discover your tool',
href: 'https://tpmjs.com',
icon: '🔍',
},
];
export function FirstToolNextStepsSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-orange-900/20 via-transparent to-amber-900/20 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">{"You're Published!"}</h2>
<p className="text-xl text-white/50 mb-12">Your tool is now discoverable by AI agents</p>
<div className="grid md:grid-cols-3 gap-4 max-w-3xl mx-auto">
{nextSteps.map((step, index) => (
<motion.a
key={step.title}
href={step.href}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.1 }}
className="p-5 rounded-xl bg-white/5 border border-orange-500/20 text-left hover:bg-white/10 hover:border-orange-500/40 transition-all group cursor-pointer"
>
<div className="text-3xl mb-3 group-hover:scale-110 transition-transform">
{step.icon}
</div>
<div className="text-sm font-semibold text-orange-400 mb-1">{step.title}</div>
<div className="text-xs text-white/50">{step.description}</div>
</motion.a>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="mt-12 p-6 rounded-xl bg-gradient-to-r from-orange-500/10 to-amber-500/10 border border-orange-500/20 max-w-md mx-auto"
>
<p className="text-white/70 text-sm mb-2">Complete code example:</p>
<code className="text-orange-400 font-mono text-sm">github.com/tpmjs/example-tool</code>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,87 @@
'use client';
import { motion } from 'framer-motion';
export function FirstToolWelcomeSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-orange-900/20 via-transparent to-amber-900/20 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="relative z-10 max-w-4xl"
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="mb-6"
>
<span className="text-sm font-mono text-orange-400/60 tracking-widest uppercase">
Step-by-Step Guide
</span>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mb-8"
>
<span className="text-5xl md:text-6xl font-bold bg-gradient-to-r from-orange-400 to-amber-400 bg-clip-text text-transparent">
Build Your First Tool
</span>
</motion.div>
<motion.h1
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-2xl md:text-3xl font-medium text-white/90 mb-6"
>
From npm init to published tool in 10 minutes
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.7 }}
className="text-lg text-white/50 font-mono max-w-xl mx-auto"
>
Create an AI SDK tool that agents can discover and execute
</motion.p>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 1.2 }}
className="absolute bottom-12 flex flex-col items-center"
>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY, ease: 'easeInOut' }}
className="text-white/40"
>
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M19 9l-7 7-7-7"
/>
</svg>
</motion.div>
<span className="mt-2 text-sm text-white/30 font-mono">Press any key</span>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,96 @@
'use client';
import { motion } from 'framer-motion';
export function PackageJsonSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-orange-900/10 via-transparent to-amber-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<div className="text-orange-400/60 font-mono text-sm mb-4">Step 3</div>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Configure package.json</h2>
<p className="text-xl text-white/50 mb-8">Add the tpmjs keyword for discovery</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-[#0d1117] rounded-xl border border-white/10 p-6 text-left max-w-2xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-3">package.json</div>
<pre className="text-sm font-mono overflow-x-auto">
<code>
<span className="text-white">{'{'}</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"name"'}</span>
<span className="text-white">: </span>
<span className="text-amber-300">{'"my-ai-tool"'}</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"version"'}</span>
<span className="text-white">: </span>
<span className="text-amber-300">{'"1.0.0"'}</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"keywords"'}</span>
<span className="text-white">: [</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-amber-300 bg-orange-500/20 px-1 rounded">{'"tpmjs"'}</span>
<span className="text-white/50">{' ← Required for discovery'}</span>
{'\n'}
<span className="text-white">{' ],'}]</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"main"'}</span>
<span className="text-white">: </span>
<span className="text-amber-300">{'"dist/index.js"'}</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"types"'}</span>
<span className="text-white">: </span>
<span className="text-amber-300">{'"dist/index.d.ts"'}</span>
<span className="text-white">,</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"exports"'}</span>
<span className="text-white">{': {'}</span>
{'\n'}
<span className="text-white">{' '}</span>
<span className="text-cyan-400">{'"."'}</span>
<span className="text-white">: </span>
<span className="text-amber-300">{'"./dist/index.js"'}</span>
{'\n'}
<span className="text-white">{' }'}</span>
{'\n'}
<span className="text-white">{'}'}</span>
</code>
</pre>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-8 p-4 rounded-lg bg-orange-500/10 border border-orange-500/20 max-w-md mx-auto"
>
<p className="text-orange-400 text-sm">
<span className="font-semibold">Key:</span> The{' '}
<code className="bg-orange-500/20 px-1 rounded">tpmjs</code> keyword triggers automatic
indexing
</p>
</motion.div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,68 @@
'use client';
import { motion } from 'framer-motion';
export function ProjectSetupSlide(): React.ReactElement {
return (
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
<div className="absolute inset-0 bg-gradient-to-br from-orange-900/10 via-transparent to-amber-900/10 pointer-events-none" />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="relative z-10 max-w-4xl w-full"
>
<div className="text-orange-400/60 font-mono text-sm mb-4">Step 1</div>
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Project Setup</h2>
<p className="text-xl text-white/50 mb-10">Create a new npm package</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-[#0d1117] rounded-xl border border-white/10 p-6 text-left max-w-2xl mx-auto"
>
<div className="text-xs text-white/40 font-mono mb-3">Terminal</div>
<pre className="text-sm font-mono overflow-x-auto">
<code>
<span className="text-white/50"># Create directory</span>
{'\n'}
<span className="text-orange-400">mkdir</span>
<span className="text-white"> my-ai-tool</span>
{'\n'}
<span className="text-orange-400">cd</span>
<span className="text-white"> my-ai-tool</span>
{'\n\n'}
<span className="text-white/50"># Initialize package</span>
{'\n'}
<span className="text-orange-400">npm</span>
<span className="text-white"> init -y</span>
{'\n\n'}
<span className="text-white/50"># Install dependencies</span>
{'\n'}
<span className="text-orange-400">npm</span>
<span className="text-white"> install ai zod</span>
</code>
</pre>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-8 flex items-center justify-center gap-4 text-white/40 text-sm"
>
<span className="px-3 py-1 rounded-full bg-orange-500/10 text-orange-400 border border-orange-500/20">
ai
</span>
<span className="text-white/20">+</span>
<span className="px-3 py-1 rounded-full bg-amber-500/10 text-amber-400 border border-amber-500/20">
zod
</span>
<span className="text-white/30 ml-2">= AI SDK tools</span>
</motion.div>
</motion.div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show more