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>
This commit is contained in:
Ajax Davis 2025-12-29 21:30:42 +10:00
parent 4a13709a3d
commit fada29ecbe
42 changed files with 426 additions and 97 deletions

View file

@ -684,14 +684,14 @@ The full working implementation is live at [tpmjs.com](https://tpmjs.com).
## NPM Package Syncing System ## NPM Package Syncing System
TPMJS.com automatically mirrors npm packages with the `tpmjs-tool` keyword to keep the tool registry up-to-date. This section documents how the syncing system works. 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 ### Overview
The sync system uses three automated strategies running on Vercel Cron to discover and update TPMJS tools: 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 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-tool` keyword 2. **Keyword Search** - Actively searches npm for packages with the `tpmjs` keyword
3. **Metrics Sync** - Updates download stats and calculates quality scores 3. **Metrics Sync** - Updates download stats and calculates quality scores
### Sync Endpoints ### Sync Endpoints
@ -735,12 +735,12 @@ All sync endpoints are located in `apps/web/src/app/api/sync/`:
#### 2. Keyword Search Sync (`/api/sync/keyword`) #### 2. Keyword Search Sync (`/api/sync/keyword`)
**Purpose:** Actively searches npm for packages with the `tpmjs-tool` keyword. **Purpose:** Actively searches npm for packages with the `tpmjs` keyword.
**Schedule:** Every 15 minutes (`*/15 * * * *`) **Schedule:** Every 15 minutes (`*/15 * * * *`)
**How it works:** **How it works:**
1. Searches npm registry for packages with keyword `tpmjs-tool` (up to 250 results) 1. Searches npm registry for packages with keyword `tpmjs` (up to 250 results)
2. Fetches full metadata for each package 2. Fetches full metadata for each package
3. Validates the `tpmjs` field 3. Validates the `tpmjs` field
4. Upserts tools with `discoveryMethod: 'keyword'` 4. Upserts tools with `discoveryMethod: 'keyword'`
@ -1078,7 +1078,7 @@ curl -X POST http://localhost:3000/api/sync/changes \
Here's how a new TPMJS tool gets discovered: Here's how a new TPMJS tool gets discovered:
1. **Developer publishes package to npm** with `tpmjs-tool` keyword and `tpmjs` field in package.json 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 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 3. **Validation:** `validateTpmjsField()` checks that the `tpmjs` field meets requirements
4. **Database Insert:** Tool is upserted with initial data 4. **Database Insert:** Tool is upserted with initial data

View file

@ -5,7 +5,7 @@ This guide shows you how to create and publish an AI tool that will be automatic
## Quick Start ## Quick Start
1. Create a new NPM package 1. Create a new NPM package
2. Add `"tpmjs-tool"` to the `keywords` array in package.json 2. Add `"tpmjs"` to the `keywords` array in package.json
3. Add a `tpmjs` field with your tool's metadata 3. Add a `tpmjs` field with your tool's metadata
4. Publish to NPM 4. Publish to NPM
5. Your tool will automatically appear on tpmjs.com within 15 minutes 5. Your tool will automatically appear on tpmjs.com within 15 minutes
@ -24,18 +24,18 @@ npm init -y
### 2. Add the Required Keyword ### 2. Add the Required Keyword
In your `package.json`, add `"tpmjs-tool"` to the keywords array: In your `package.json`, add `"tpmjs"` to the keywords array:
```json ```json
{ {
"name": "@yourname/my-awesome-tool", "name": "@yourname/my-awesome-tool",
"version": "1.0.0", "version": "1.0.0",
"keywords": ["tpmjs-tool", "ai", "other-keywords"], "keywords": ["tpmjs", "ai", "other-keywords"],
... ...
} }
``` ```
**Important:** The `"tpmjs-tool"` keyword is REQUIRED for automatic discovery! **Important:** The `"tpmjs"` keyword is REQUIRED for automatic discovery!
### 3. Add TPMJS Metadata ### 3. Add TPMJS Metadata
@ -212,7 +212,7 @@ npm publish --access public
Your tool will be automatically discovered through: Your tool will be automatically discovered through:
1. **Keyword Search** - Runs every 15 minutes, searches NPM for `"tpmjs-tool"` 1. **Keyword Search** - Runs every 15 minutes, searches NPM for `"tpmjs"`
2. **Changes Feed** - Monitors NPM publishes in real-time (every 2 minutes) 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! After publishing, your tool should appear on https://tpmjs.com within 15 minutes!
@ -229,7 +229,7 @@ Here's the complete `package.json` from the published example:
"version": "0.2.0", "version": "0.2.0",
"description": "A tool for creating structured blog posts with AI-generated content", "description": "A tool for creating structured blog posts with AI-generated content",
"type": "module", "type": "module",
"keywords": ["tpmjs-tool", "blog", "content", "ai", "writing"], "keywords": ["tpmjs", "blog", "content", "ai", "writing"],
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -402,7 +402,7 @@ Or manually check the structure matches the examples above.
## Troubleshooting ## Troubleshooting
**Tool not appearing after 15 minutes?** **Tool not appearing after 15 minutes?**
- Check that you added `"tpmjs-tool"` to keywords - Check that you added `"tpmjs"` to keywords
- Verify your `tpmjs` field has required fields (category, description) - Verify your `tpmjs` field has required fields (category, description)
- Check the NPM package is public: `npm view yourpackage` - Check the NPM package is public: `npm view yourpackage`

View file

@ -282,7 +282,7 @@ this becomes unmanageable.
designed for AI agents. Agents can search for tools by description and load them designed for AI agents. Agents can search for tools by description and load them
at runtime. at runtime.
**For Tool Builders:** Add `tpmjs-tool` keyword to your package.json. **For Tool Builders:** Add `tpmjs` keyword to your package.json.
Your tool appears on tpmjs.com within 15 minutes. Your tool appears on tpmjs.com within 15 minutes.
**For Agent Developers:** Use semantic search to find tools: **For Agent Developers:** Use semantic search to find tools:

View file

@ -4,7 +4,7 @@
**TPMJS is a registry for discovering AI tools published to npm.** **TPMJS is a registry for discovering AI tools published to npm.**
Browse, search, and find tools at [tpmjs.com](https://tpmjs.com). Publish your tool by adding the `tpmjs-tool` keyword to your package.json—it appears in the registry within 15 minutes. 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? ## Why TPMJS?
@ -24,7 +24,7 @@ npx @tpmjs/create-basic-tools
Or add manually to your package.json: Or add manually to your package.json:
```json ```json
{ {
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis" "category": "text-analysis"
} }

View file

@ -38,13 +38,13 @@ TPMJS runs three automated sync jobs:
**1. npm Changes Feed (every 2 minutes)** **1. npm Changes Feed (every 2 minutes)**
``` ```
npm registry → /_changes endpoint → filter for tpmjs-tool keyword → process 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. This catches new packages and updates in near-real-time. We track sequence numbers so we never reprocess.
**2. Keyword Search (every 15 minutes)** **2. Keyword Search (every 15 minutes)**
``` ```
npm search "tpmjs-tool" → up to 250 results → validate → ingest npm search "tpmjs" → up to 250 results → validate → ingest
``` ```
Backup mechanism. Catches anything the changes feed missed. Backup mechanism. Catches anything the changes feed missed.
@ -61,7 +61,7 @@ To get indexed, a package needs two things:
```json ```json
{ {
"name": "@acme/my-tool", "name": "@acme/my-tool",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "web-scraping", "category": "web-scraping",
"description": "Scrapes URLs and returns structured markdown" "description": "Scrapes URLs and returns structured markdown"
@ -303,7 +303,7 @@ Publish to npm → hope someone finds it → no visibility into usage
After TPMJS: After TPMJS:
``` ```
Publish to npm with tpmjs-tool keyword → indexed within 2 minutes → Publish to npm with tpmjs keyword → indexed within 2 minutes →
schema auto-extracted → quality scored → discoverable by search → schema auto-extracted → quality scored → discoverable by search →
execution stats tracked execution stats tracked
``` ```
@ -383,7 +383,7 @@ Discovery is the bottleneck. We're fixing discovery.
- **Browse**: https://tpmjs.com/tool-search - **Browse**: https://tpmjs.com/tool-search
- **Playground**: https://tpmjs.com/playground - **Playground**: https://tpmjs.com/playground
- **Publish**: Add `tpmjs-tool` keyword + `tpmjs` field to your package.json - **Publish**: Add `tpmjs` keyword + `tpmjs` field to your package.json
- **API**: `GET https://tpmjs.com/api/tools` - **API**: `GET https://tpmjs.com/api/tools`
--- ---

View file

@ -53,7 +53,7 @@ const tutorials = [
/> />
</svg> </svg>
), ),
features: ['tpmjs-tool keyword', 'Schema extraction', 'Quality scoring'], features: ['tpmjs keyword', 'Schema extraction', 'Quality scoring'],
}, },
]; ];

View file

@ -3,7 +3,7 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
const steps = [ const steps = [
{ num: '1', text: 'Add "tpmjs-tool" to your package.json keywords' }, { num: '1', text: 'Add "tpmjs" to your package.json keywords' },
{ num: '2', text: 'Define your tools in the "tpmjs" field' }, { num: '2', text: 'Define your tools in the "tpmjs" field' },
{ num: '3', text: 'npm publish' }, { num: '3', text: 'npm publish' },
]; ];

View file

@ -3,7 +3,7 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
const syncSteps = [ const syncSteps = [
{ step: '1', text: 'You publish to npm with tpmjs-tool keyword', color: 'purple' }, { 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: '2', text: 'Registry detects your package within 2 minutes', color: 'cyan' },
{ step: '3', text: 'We extract schemas from your package.json', color: 'emerald' }, { step: '3', text: 'We extract schemas from your package.json', color: 'emerald' },
{ step: '4', text: 'Your tool appears in search results', color: 'yellow' }, { step: '4', text: 'Your tool appears in search results', color: 'yellow' },

View file

@ -7,7 +7,7 @@ const packageJson = [
{ id: 'name', line: ' "name": "@your-org/your-tools",' }, { id: 'name', line: ' "name": "@your-org/your-tools",' },
{ id: 'version', line: ' "version": "1.0.0",' }, { id: 'version', line: ' "version": "1.0.0",' },
{ id: 'keywords-open', line: ' "keywords": [' }, { id: 'keywords-open', line: ' "keywords": [' },
{ id: 'keyword', line: ' "tpmjs-tool"' }, { id: 'keyword', line: ' "tpmjs"' },
{ id: 'keywords-close', line: ' ],' }, { id: 'keywords-close', line: ' ],' },
{ id: 'tpmjs-open', line: ' "tpmjs": {' }, { id: 'tpmjs-open', line: ' "tpmjs": {' },
{ id: 'tools-open', line: ' "tools": [{ ... }]' }, { id: 'tools-open', line: ' "tools": [{ ... }]' },
@ -43,7 +43,7 @@ export function PackageSetupSlide(): React.ReactElement {
<div <div
key={item.id} key={item.id}
className={ className={
item.line.includes('tpmjs-tool') item.line.includes('tpmjs')
? 'text-cyan-400' ? 'text-cyan-400'
: item.line.includes('"tpmjs"') : item.line.includes('"tpmjs"')
? 'text-purple-400' ? 'text-purple-400'
@ -66,7 +66,7 @@ export function PackageSetupSlide(): React.ReactElement {
className="text-left space-y-4" className="text-left space-y-4"
> >
<div className="p-4 rounded-lg bg-white/5 border border-cyan-500/20"> <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-tool keyword</div> <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 className="text-xs text-white/50">Required. This is how we find you on npm.</div>
</div> </div>
<div className="p-4 rounded-lg bg-white/5 border border-purple-500/20"> <div className="p-4 rounded-lg bg-white/5 border border-purple-500/20">

View file

@ -5,7 +5,7 @@ import { motion } from 'framer-motion';
const packageJsonLines = [ const packageJsonLines = [
{ id: 'open', line: '{' }, { id: 'open', line: '{' },
{ id: 'name', line: ' "name": "@your-org/your-tools",' }, { id: 'name', line: ' "name": "@your-org/your-tools",' },
{ id: 'keywords', line: ' "keywords": ["tpmjs-tool"],' }, { id: 'keywords', line: ' "keywords": ["tpmjs"],' },
{ id: 'tpmjs', line: ' "tpmjs": {' }, { id: 'tpmjs', line: ' "tpmjs": {' },
{ id: 'tools', line: ' "tools": [{' }, { id: 'tools', line: ' "tools": [{' },
{ id: 'toolName', line: ' "name": "yourTool",' }, { id: 'toolName', line: ' "name": "yourTool",' },
@ -16,7 +16,7 @@ const packageJsonLines = [
]; ];
const steps = [ const steps = [
{ step: '1', text: 'Add tpmjs-tool keyword', color: 'cyan' }, { step: '1', text: 'Add tpmjs keyword', color: 'cyan' },
{ step: '2', text: 'Define tpmjs field with tool metadata', color: 'purple' }, { step: '2', text: 'Define tpmjs field with tool metadata', color: 'purple' },
{ step: '3', text: 'npm publish', color: 'emerald' }, { step: '3', text: 'npm publish', color: 'emerald' },
]; ];
@ -56,7 +56,7 @@ export function GetStartedSlide(): React.ReactElement {
<div <div
key={item.id} key={item.id}
className={ className={
item.line.includes('tpmjs-tool') item.line.includes('tpmjs')
? 'text-cyan-400' ? 'text-cyan-400'
: item.line.includes('tpmjs') : item.line.includes('tpmjs')
? 'text-purple-400' ? 'text-purple-400'

View file

@ -0,0 +1,60 @@
import { NextResponse } from 'next/server';
export const runtime = 'edge';
interface BundlephobiaResponse {
name: string;
version: string;
size: number;
gzip: number;
dependencyCount: number;
}
/**
* GET /api/bundlephobia?package=@scope/name&version=1.0.0
* Proxies bundlephobia API to avoid CORS issues
*/
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const packageName = searchParams.get('package');
const version = searchParams.get('version');
if (!packageName) {
return NextResponse.json({ error: 'Package name required' }, { status: 400 });
}
const packageWithVersion = version ? `${packageName}@${version}` : packageName;
try {
const response = await fetch(
`https://bundlephobia.com/api/size?package=${encodeURIComponent(packageWithVersion)}`,
{
headers: {
'User-Agent': 'TPMJS/1.0',
},
}
);
if (!response.ok) {
if (response.status === 404) {
return NextResponse.json({ error: 'Package not found on bundlephobia' }, { status: 404 });
}
return NextResponse.json(
{ error: 'Failed to fetch bundle size' },
{ status: response.status }
);
}
const data: BundlephobiaResponse = await response.json();
return NextResponse.json({
name: data.name,
version: data.version,
size: data.size,
gzip: data.gzip,
dependencyCount: data.dependencyCount,
});
} catch {
return NextResponse.json({ error: 'Failed to fetch bundle size' }, { status: 500 });
}
}

View file

@ -108,7 +108,7 @@ export async function POST(request: NextRequest) {
frameworks: validation.packageData.frameworks || [], frameworks: validation.packageData.frameworks || [],
tier: validation.tier || 'minimal', tier: validation.tier || 'minimal',
discoveryMethod: 'changes-feed', discoveryMethod: 'changes-feed',
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false, isOfficial: pkg.keywords?.includes('tpmjs') || false,
npmDownloadsLastMonth: 0, // Will be updated by metrics sync npmDownloadsLastMonth: 0, // Will be updated by metrics sync
githubStars: githubStars, githubStars: githubStars,
}, },
@ -127,7 +127,7 @@ export async function POST(request: NextRequest) {
env: validation.packageData.env ?? undefined, env: validation.packageData.env ?? undefined,
frameworks: validation.packageData.frameworks || [], frameworks: validation.packageData.frameworks || [],
tier: validation.tier || 'minimal', tier: validation.tier || 'minimal',
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false, isOfficial: pkg.keywords?.includes('tpmjs') || false,
}, },
}); });

View file

@ -17,7 +17,7 @@ export const maxDuration = 300; // 5 minutes max for cron jobs
/** /**
* POST /api/sync/keyword * POST /api/sync/keyword
* Sync tools by searching NPM for 'tpmjs-tool' keyword * Sync tools by searching NPM for 'tpmjs' keyword
* *
* This endpoint is called by Vercel Cron (every 15 minutes) * This endpoint is called by Vercel Cron (every 15 minutes)
* Requires Authorization: Bearer <CRON_SECRET> * Requires Authorization: Bearer <CRON_SECRET>
@ -40,9 +40,9 @@ export async function POST(request: NextRequest) {
const skippedPackages: Array<{ name: string; author: string; reason: string }> = []; const skippedPackages: Array<{ name: string; author: string; reason: string }> = [];
try { try {
// Search for packages with 'tpmjs-tool' keyword // Search for packages with 'tpmjs' keyword
const searchResults = await searchByKeyword({ const searchResults = await searchByKeyword({
keyword: 'tpmjs-tool', keyword: 'tpmjs',
size: 250, // Get up to 250 packages per sync size: 250, // Get up to 250 packages per sync
}); });
@ -122,7 +122,7 @@ export async function POST(request: NextRequest) {
frameworks: validation.packageData.frameworks || [], frameworks: validation.packageData.frameworks || [],
tier: validation.tier || 'minimal', tier: validation.tier || 'minimal',
discoveryMethod: 'keyword', discoveryMethod: 'keyword',
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false, isOfficial: pkg.keywords?.includes('tpmjs') || false,
npmDownloadsLastMonth: 0, // Will be updated by metrics sync npmDownloadsLastMonth: 0, // Will be updated by metrics sync
githubStars: githubStars, githubStars: githubStars,
}, },
@ -141,7 +141,7 @@ export async function POST(request: NextRequest) {
env: validation.packageData.env ?? undefined, env: validation.packageData.env ?? undefined,
frameworks: validation.packageData.frameworks || [], frameworks: validation.packageData.frameworks || [],
tier: validation.tier || 'minimal', tier: validation.tier || 'minimal',
isOfficial: pkg.keywords?.includes('tpmjs-tool') || false, isOfficial: pkg.keywords?.includes('tpmjs') || false,
}, },
}); });

View file

@ -370,9 +370,7 @@ Use registrySearch to find tools, then registryExecute to run them.\`,
<DocSubSection title="Tool Discovery"> <DocSubSection title="Tool Discovery">
<p className="text-foreground-secondary mb-4"> <p className="text-foreground-secondary mb-4">
TPMJS automatically discovers tools from npm packages that have the{' '} TPMJS automatically discovers tools from npm packages that have the{' '}
<code className="text-primary bg-surface px-1.5 py-0.5 rounded"> <code className="text-primary bg-surface px-1.5 py-0.5 rounded">tpmjs</code>{' '}
tpmjs-tool
</code>{' '}
keyword. Tools are indexed every 2-15 minutes. keyword. Tools are indexed every 2-15 minutes.
</p> </p>
</DocSubSection> </DocSubSection>
@ -832,7 +830,7 @@ while (true) {
</p> </p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{[ {[
{ step: '1', label: 'Add tpmjs-tool keyword' }, { step: '1', label: 'Add tpmjs keyword' },
{ step: '2', label: 'Add tpmjs field' }, { step: '2', label: 'Add tpmjs field' },
{ step: '3', label: 'Publish to npm' }, { step: '3', label: 'Publish to npm' },
{ step: '4', label: 'Live in 15 minutes!' }, { step: '4', label: 'Live in 15 minutes!' },
@ -878,7 +876,7 @@ while (true) {
code={`{ code={`{
"name": "@yourname/my-tool", "name": "@yourname/my-tool",
"version": "1.0.0", "version": "1.0.0",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai"], "frameworks": ["vercel-ai"],
@ -1118,7 +1116,7 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
{[ {[
{ {
q: 'How long does it take for my tool to appear?', q: 'How long does it take for my tool to appear?',
a: 'Tools are discovered within 2-15 minutes of publishing to npm. Make sure you have the "tpmjs-tool" keyword in your package.json.', a: 'Tools are discovered within 2-15 minutes of publishing to npm. Make sure you have the "tpmjs" keyword in your package.json.',
}, },
{ {
q: 'What is auto-discovery?', q: 'What is auto-discovery?',
@ -1155,8 +1153,8 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
<ul className="list-disc list-inside space-y-2 text-foreground-secondary"> <ul className="list-disc list-inside space-y-2 text-foreground-secondary">
<li> <li>
Ensure you have{' '} Ensure you have{' '}
<code className="text-primary bg-surface px-1 rounded">tpmjs-tool</code> in <code className="text-primary bg-surface px-1 rounded">tpmjs</code> in your
your keywords keywords
</li> </li>
<li> <li>
Verify your <code className="text-primary">tpmjs</code> field is valid JSON Verify your <code className="text-primary">tpmjs</code> field is valid JSON

View file

@ -83,7 +83,7 @@ export default function FAQPage(): React.ReactElement {
<p> <p>
Think of it as npm for AI tools. Developers publish tools to npm with the{' '} Think of it as npm for AI tools. Developers publish tools to npm with the{' '}
<code className="text-foreground bg-background px-2 py-1 rounded border border-border"> <code className="text-foreground bg-background px-2 py-1 rounded border border-border">
tpmjs-tool tpmjs
</code>{' '} </code>{' '}
keyword, and TPMJS automatically syncs them to our registry where they can be keyword, and TPMJS automatically syncs them to our registry where they can be
discovered by AI agents and developers. discovered by AI agents and developers.
@ -97,7 +97,7 @@ export default function FAQPage(): React.ReactElement {
<li> <li>
Add the{' '} Add the{' '}
<code className="text-foreground bg-background px-2 py-1 rounded border border-border"> <code className="text-foreground bg-background px-2 py-1 rounded border border-border">
tpmjs-tool tpmjs
</code>{' '} </code>{' '}
keyword to your package.json keyword to your package.json
</li> </li>
@ -254,7 +254,7 @@ export default function FAQPage(): React.ReactElement {
<strong className="text-foreground">Keyword Search (every 15 minutes):</strong>{' '} <strong className="text-foreground">Keyword Search (every 15 minutes):</strong>{' '}
Actively searches for packages with the{' '} Actively searches for packages with the{' '}
<code className="text-foreground bg-background px-2 py-1 rounded border border-border"> <code className="text-foreground bg-background px-2 py-1 rounded border border-border">
tpmjs-tool tpmjs
</code>{' '} </code>{' '}
keyword keyword
</li> </li>

View file

@ -108,7 +108,7 @@ export default function HowItWorksPage(): React.ReactElement {
code={`{ code={`{
"name": "@yourname/awesome-tool", "name": "@yourname/awesome-tool",
"version": "1.0.0", "version": "1.0.0",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai"], "frameworks": ["vercel-ai"],
@ -232,7 +232,7 @@ const result = await streamText({
<div className="p-4 border border-border rounded-lg bg-surface"> <div className="p-4 border border-border rounded-lg bg-surface">
<h4 className="font-semibold mb-2 text-foreground">Keyword Search</h4> <h4 className="font-semibold mb-2 text-foreground">Keyword Search</h4>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Searches npm for &quot;tpmjs-tool&quot; keyword Searches npm for &quot;tpmjs&quot; keyword
</p> </p>
<div className="mt-2 text-xs text-foreground-tertiary">Every 15 minutes</div> <div className="mt-2 text-xs text-foreground-tertiary">Every 15 minutes</div>
</div> </div>
@ -398,8 +398,7 @@ const result = await streamText({
<h4 className="font-semibold text-foreground mb-1">Developer publishes to npm</h4> <h4 className="font-semibold text-foreground mb-1">Developer publishes to npm</h4>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Package with{' '} Package with{' '}
<code className="text-xs bg-surface px-1 py-0.5 rounded">tpmjs-tool</code>{' '} <code className="text-xs bg-surface px-1 py-0.5 rounded">tpmjs</code> keyword
keyword
</p> </p>
</div> </div>
<span className="text-xs text-foreground-tertiary ml-auto">~1 second</span> <span className="text-xs text-foreground-tertiary ml-auto">~1 second</span>

View file

@ -58,7 +58,7 @@ export default function PrivacyPage(): React.ReactElement {
<p className="text-foreground-secondary mb-4"> <p className="text-foreground-secondary mb-4">
We automatically collect and index public metadata from npm packages that use the{' '} We automatically collect and index public metadata from npm packages that use the{' '}
<code className="text-foreground bg-background px-2 py-1 rounded text-sm"> <code className="text-foreground bg-background px-2 py-1 rounded text-sm">
tpmjs-tool tpmjs
</code>{' '} </code>{' '}
keyword. This includes: keyword. This includes:
</p> </p>
@ -430,8 +430,7 @@ export default function PrivacyPage(): React.ReactElement {
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Request deletion of your data. To remove a tool from TPMJS, unpublish it from npm Request deletion of your data. To remove a tool from TPMJS, unpublish it from npm
or remove the{' '} or remove the{' '}
<code className="text-xs bg-background px-1 py-0.5 rounded">tpmjs-tool</code>{' '} <code className="text-xs bg-background px-1 py-0.5 rounded">tpmjs</code> keyword.
keyword.
</p> </p>
</div> </div>

View file

@ -86,9 +86,8 @@ export default function PublishPage(): React.ReactElement {
<ol className="space-y-4 text-foreground-secondary"> <ol className="space-y-4 text-foreground-secondary">
<li className="text-lg">Create a new NPM package</li> <li className="text-lg">Create a new NPM package</li>
<li className="text-lg"> <li className="text-lg">
Add{' '} Add <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code> to
<code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs-tool</code>{' '} keywords
to keywords
</li> </li>
<li className="text-lg"> <li className="text-lg">
Add a <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code>{' '} Add a <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code>{' '}
@ -112,8 +111,7 @@ export default function PublishPage(): React.ReactElement {
Step 1: Add Required Keyword Step 1: Add Required Keyword
</h2> </h2>
<p className="text-lg text-foreground-secondary mb-6"> <p className="text-lg text-foreground-secondary mb-6">
Add the{' '} Add the <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code>{' '}
<code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs-tool</code>{' '}
keyword to your package.json. This is required for automatic discovery. keyword to your package.json. This is required for automatic discovery.
</p> </p>
<CodeBlock <CodeBlock
@ -121,7 +119,7 @@ export default function PublishPage(): React.ReactElement {
code={`{ code={`{
"name": "@yourname/my-awesome-tool", "name": "@yourname/my-awesome-tool",
"version": "1.0.0", "version": "1.0.0",
"keywords": ["tpmjs-tool", "ai", "text"], "keywords": ["tpmjs", "ai", "text"],
... ...
}`} }`}
/> />
@ -186,7 +184,7 @@ export default function PublishPage(): React.ReactElement {
language="json" language="json"
code={`{ code={`{
"name": "@yourname/my-awesome-tool", "name": "@yourname/my-awesome-tool",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis" "category": "text-analysis"
} }
@ -211,7 +209,7 @@ export default function PublishPage(): React.ReactElement {
language="json" language="json"
code={`{ code={`{
"name": "@yourname/sentiment-tool", "name": "@yourname/sentiment-tool",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai", "langchain"], "frameworks": ["vercel-ai", "langchain"],
@ -336,7 +334,7 @@ npm publish --access public
code={`{ code={`{
"name": "@tpmjs/createblogpost", "name": "@tpmjs/createblogpost",
"version": "0.2.0", "version": "0.2.0",
"keywords": ["tpmjs-tool", "blog", "content"], "keywords": ["tpmjs", "blog", "content"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai", "langchain"], "frameworks": ["vercel-ai", "langchain"],

View file

@ -85,8 +85,7 @@ export default function SpecPage(): React.ReactElement {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Add the{' '} Add the <code className="text-foreground bg-surface px-1 rounded">tpmjs</code>{' '}
<code className="text-foreground bg-surface px-1 rounded">tpmjs-tool</code>{' '}
keyword and a{' '} keyword and a{' '}
<code className="text-foreground bg-surface px-1 rounded">tpmjs</code> metadata <code className="text-foreground bg-surface px-1 rounded">tpmjs</code> metadata
field to your package.json field to your package.json
@ -207,7 +206,7 @@ export default function SpecPage(): React.ReactElement {
language="json" language="json"
code={`{ code={`{
"name": "@yourname/my-tool", "name": "@yourname/my-tool",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis" "category": "text-analysis"
} }
@ -341,7 +340,7 @@ export default function SpecPage(): React.ReactElement {
language="json" language="json"
code={`{ code={`{
"name": "@yourname/sentiment-tool", "name": "@yourname/sentiment-tool",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai", "langchain"], "frameworks": ["vercel-ai", "langchain"],
@ -681,8 +680,8 @@ export default function SpecPage(): React.ReactElement {
<CardContent> <CardContent>
<p className="text-sm text-foreground-secondary mb-2"> <p className="text-sm text-foreground-secondary mb-2">
Searches for{' '} Searches for{' '}
<code className="text-foreground bg-surface px-1 rounded">tpmjs-tool</code>{' '} <code className="text-foreground bg-surface px-1 rounded">tpmjs</code> keyword
keyword every 15 minutes every 15 minutes
</p> </p>
<Badge variant="outline" size="sm"> <Badge variant="outline" size="sm">
Every 15 min Every 15 min
@ -760,7 +759,7 @@ export default function SpecPage(): React.ReactElement {
<CardContent className="pt-6 text-center"> <CardContent className="pt-6 text-center">
<div className="text-3xl mb-2">1️⃣</div> <div className="text-3xl mb-2">1️⃣</div>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Add <code className="text-foreground bg-surface px-1 rounded">tpmjs-tool</code>{' '} Add <code className="text-foreground bg-surface px-1 rounded">tpmjs</code>{' '}
keyword keyword
</p> </p>
</CardContent> </CardContent>

View file

@ -63,7 +63,7 @@ export default function TermsPage(): React.ReactElement {
<ul className="list-disc pl-4 sm:pl-6 space-y-2"> <ul className="list-disc pl-4 sm:pl-6 space-y-2">
<li> <li>
Automatic discovery and indexing of npm packages with the{' '} Automatic discovery and indexing of npm packages with the{' '}
<code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs-tool</code>{' '} <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code>{' '}
keyword keyword
</li> </li>
<li>A searchable registry of AI tools with quality scoring and health checks</li> <li>A searchable registry of AI tools with quality scoring and health checks</li>
@ -247,7 +247,7 @@ export default function TermsPage(): React.ReactElement {
</p> </p>
<p> <p>
By publishing a tool to npm with the{' '} By publishing a tool to npm with the{' '}
<code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs-tool</code>{' '} <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code>{' '}
keyword, you grant TPMJS a non-exclusive, worldwide, royalty-free license to: keyword, you grant TPMJS a non-exclusive, worldwide, royalty-free license to:
</p> </p>
<ul className="list-disc pl-4 sm:pl-6 space-y-2"> <ul className="list-disc pl-4 sm:pl-6 space-y-2">
@ -321,7 +321,7 @@ export default function TermsPage(): React.ReactElement {
<p> <p>
You may stop using the Service at any time. If you have published tools, they will You may stop using the Service at any time. If you have published tools, they will
remain in the registry unless you remove the{' '} remain in the registry unless you remove the{' '}
<code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs-tool</code>{' '} <code className="text-foreground bg-surface px-2 py-1 rounded">tpmjs</code>{' '}
keyword from your package or unpublish your package from npm. keyword from your package or unpublish your package from npm.
</p> </p>
</div> </div>

View file

@ -11,6 +11,8 @@ import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { AppHeader } from '~/components/AppHeader'; import { AppHeader } from '~/components/AppHeader';
import { BundleSize } from '~/components/BundleSize';
import { DownloadSparkline } from '~/components/DownloadSparkline';
import { Markdown } from '~/components/Markdown'; import { Markdown } from '~/components/Markdown';
import { ToolPlayground } from '~/components/ToolPlayground'; import { ToolPlayground } from '~/components/ToolPlayground';
@ -340,6 +342,21 @@ export function ToolDetailClient({ tool, slug }: ToolDetailClientProps): React.R
language="bash" language="bash"
showCopy={true} showCopy={true}
/> />
<CodeBlock
code={`yarn add ${pkg.npmPackageName}`}
language="bash"
showCopy={true}
/>
<CodeBlock
code={`bun add ${pkg.npmPackageName}`}
language="bash"
showCopy={true}
/>
<CodeBlock
code={`deno add npm:${pkg.npmPackageName}`}
language="bash"
showCopy={true}
/>
</div> </div>
</div> </div>
@ -547,6 +564,9 @@ console.log(result.text);`}
<p className="text-2xl font-bold text-foreground"> <p className="text-2xl font-bold text-foreground">
{pkg.npmDownloadsLastMonth?.toLocaleString() || '0'} {pkg.npmDownloadsLastMonth?.toLocaleString() || '0'}
</p> </p>
<div className="mt-2">
<DownloadSparkline packageName={pkg.npmPackageName} />
</div>
</div> </div>
{pkg.githubStars != null && ( {pkg.githubStars != null && (
<div> <div>
@ -574,6 +594,9 @@ console.log(result.text);`}
</CardContent> </CardContent>
</Card> </Card>
{/* Bundle Size */}
<BundleSize packageName={pkg.npmPackageName} version={pkg.npmVersion} />
{/* NPM Keywords */} {/* NPM Keywords */}
{pkg.npmKeywords && pkg.npmKeywords.length > 0 && ( {pkg.npmKeywords && pkg.npmKeywords.length > 0 && (
<Card> <Card>

View file

@ -463,7 +463,7 @@ export default function ToolSearchPage(): React.ReactElement {
variant="outline" variant="outline"
onClick={() => onClick={() =>
window.open( window.open(
'https://www.npmjs.com/search?q=keywords:tpmjs-tool', 'https://www.npmjs.com/search?q=keywords:tpmjs',
'_blank', '_blank',
'noopener' 'noopener'
) )
@ -483,9 +483,7 @@ export default function ToolSearchPage(): React.ReactElement {
</div> </div>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Add{' '} Add{' '}
<code className="px-1.5 py-0.5 bg-muted rounded text-xs"> <code className="px-1.5 py-0.5 bg-muted rounded text-xs">tpmjs</code>{' '}
tpmjs-tool
</code>{' '}
keyword to your package.json keyword to your package.json
</p> </p>
</div> </div>

View file

@ -24,7 +24,7 @@ const nodeDescriptions: Record<string, { title: string; description: string }> =
npm: { npm: {
title: 'NPM Registry', title: 'NPM Registry',
description: description:
'The public npm registry containing all JavaScript packages. TPMJS monitors packages with the tpmjs-tool keyword.', 'The public npm registry containing all JavaScript packages. TPMJS monitors packages with the tpmjs keyword.',
}, },
'changes-feed': { 'changes-feed': {
title: 'Changes Feed', title: 'Changes Feed',
@ -34,7 +34,7 @@ const nodeDescriptions: Record<string, { title: string; description: string }> =
'keyword-search': { 'keyword-search': {
title: 'Keyword Search', title: 'Keyword Search',
description: description:
'Periodic search for packages with the tpmjs-tool keyword. Acts as a backup to ensure no packages are missed.', 'Periodic search for packages with the tpmjs keyword. Acts as a backup to ensure no packages are missed.',
}, },
validation: { validation: {
title: 'Schema Validation', title: 'Schema Validation',

View file

@ -0,0 +1,124 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
import { useEffect, useState } from 'react';
interface BundleSizeData {
name: string;
version: string;
size: number;
gzip: number;
dependencyCount: number;
}
interface BundleSizeProps {
packageName: string;
version: string;
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'kB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
}
export function BundleSize({ packageName, version }: BundleSizeProps): React.ReactElement | null {
const [data, setData] = useState<BundleSizeData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchBundleSize = async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({ package: packageName, version });
const response = await fetch(`/api/bundlephobia?${params}`);
if (!response.ok) {
if (response.status === 404) {
setError('not-found');
} else {
setError('failed');
}
return;
}
const result = await response.json();
setData(result);
} catch {
setError('failed');
} finally {
setLoading(false);
}
};
fetchBundleSize();
}, [packageName, version]);
// Don't render anything if package not found (common for scoped packages)
if (error === 'not-found') {
return null;
}
if (loading) {
return (
<Card>
<CardHeader>
<CardTitle>Bundle Size</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 animate-pulse">
<div className="h-4 bg-muted rounded w-24" />
<div className="h-4 bg-muted rounded w-20" />
</div>
</CardContent>
</Card>
);
}
if (error || !data) {
return null;
}
return (
<Card>
<CardHeader>
<CardTitle>Bundle Size</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-foreground-secondary">Minified</span>
<span className="text-sm font-mono font-medium text-foreground">
{formatBytes(data.size)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-foreground-secondary">Gzipped</span>
<span className="text-sm font-mono font-medium text-foreground">
{formatBytes(data.gzip)}
</span>
</div>
{data.dependencyCount > 0 && (
<div className="flex justify-between items-center">
<span className="text-sm text-foreground-secondary">Dependencies</span>
<span className="text-sm font-mono font-medium text-foreground">
{data.dependencyCount}
</span>
</div>
)}
<a
href={`https://bundlephobia.com/package/${packageName}@${version}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-foreground-tertiary hover:text-foreground-secondary pt-2"
>
<span>View on Bundlephobia →</span>
</a>
</CardContent>
</Card>
);
}

View file

@ -0,0 +1,131 @@
'use client';
import { useEffect, useState } from 'react';
interface DownloadDay {
downloads: number;
day: string;
}
interface DownloadSparklineProps {
packageName: string;
}
export function DownloadSparkline({ packageName }: DownloadSparklineProps): React.ReactElement {
const [downloads, setDownloads] = useState<DownloadDay[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
const fetchDownloads = async () => {
setLoading(true);
setError(false);
try {
const response = await fetch(
`https://api.npmjs.org/downloads/range/last-month/${encodeURIComponent(packageName)}`
);
if (!response.ok) {
setError(true);
return;
}
const data = await response.json();
setDownloads(data.downloads || []);
} catch {
setError(true);
} finally {
setLoading(false);
}
};
fetchDownloads();
}, [packageName]);
if (loading) {
return <div className="h-8 w-full animate-pulse bg-muted rounded" />;
}
if (error || downloads.length === 0) {
return <div className="h-8" />;
}
// Calculate sparkline path
const values = downloads.map((d) => d.downloads);
const max = Math.max(...values, 1);
const min = Math.min(...values);
const range = max - min || 1;
const width = 200;
const height = 32;
const padding = 2;
const points = values.map((value, index) => {
const x = padding + (index / (values.length - 1)) * (width - padding * 2);
const y = height - padding - ((value - min) / range) * (height - padding * 2);
return `${x},${y}`;
});
const pathD = `M ${points.join(' L ')}`;
// Calculate trend (compare first week vs last week)
const firstWeek = values.slice(0, 7).reduce((a, b) => a + b, 0);
const lastWeek = values.slice(-7).reduce((a, b) => a + b, 0);
const trendUp = lastWeek > firstWeek;
const trendPercent = firstWeek > 0 ? Math.round(((lastWeek - firstWeek) / firstWeek) * 100) : 0;
return (
<div className="space-y-1">
<svg
viewBox={`0 0 ${width} ${height}`}
className="w-full h-8"
preserveAspectRatio="none"
role="img"
aria-label="Download trend over the last 30 days"
>
{/* Gradient fill under the line */}
<defs>
<linearGradient
id={`sparkline-gradient-${packageName.replace(/[^a-z0-9]/gi, '-')}`}
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="currentColor" stopOpacity="0.3" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
{/* Area fill */}
<path
d={`${pathD} L ${width - padding},${height - padding} L ${padding},${height - padding} Z`}
fill={`url(#sparkline-gradient-${packageName.replace(/[^a-z0-9]/gi, '-')})`}
className="text-primary"
/>
{/* Line */}
<path
d={pathD}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="text-primary"
/>
</svg>
{/* Trend indicator */}
{Math.abs(trendPercent) >= 5 && (
<div
className={`text-xs flex items-center gap-1 ${trendUp ? 'text-emerald-500' : 'text-red-500'}`}
>
<span>{trendUp ? '↑' : '↓'}</span>
<span>{Math.abs(trendPercent)}% vs last week</span>
</div>
)}
</div>
);
}

View file

@ -907,7 +907,7 @@ const providers: Record<ExecutionProvider, (toolId: string, params: any, env?: a
'aws-lambda': async (toolId, params, env) => { 'aws-lambda': async (toolId, params, env) => {
const lambda = new AWS.Lambda(); const lambda = new AWS.Lambda();
const result = await lambda.invoke({ const result = await lambda.invoke({
FunctionName: `tpmjs-tool-${toolId.replace('::', '-')}`, FunctionName: `tpmjs-${toolId.replace('::', '-')}`,
Payload: JSON.stringify({ params, env }), Payload: JSON.stringify({ params, env }),
}).promise(); }).promise();
return JSON.parse(result.Payload as string); return JSON.parse(result.Payload as string);

View file

@ -377,7 +377,7 @@ packages/registryExecute/
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
} }
}, },
"keywords": ["tpmjs", "tpmjs-tool", "ai-sdk", "vercel-ai", "tools", "registry", "search"], "keywords": ["tpmjs", "tpmjs", "ai-sdk", "vercel-ai", "tools", "registry", "search"],
"peerDependencies": { "peerDependencies": {
"ai": "^4.0.0", "ai": "^4.0.0",
"zod": "^3.0.0" "zod": "^3.0.0"
@ -402,7 +402,7 @@ packages/registryExecute/
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
} }
}, },
"keywords": ["tpmjs", "tpmjs-tool", "ai-sdk", "vercel-ai", "tools", "registry", "execute"], "keywords": ["tpmjs", "tpmjs", "ai-sdk", "vercel-ai", "tools", "registry", "execute"],
"peerDependencies": { "peerDependencies": {
"ai": "^4.0.0", "ai": "^4.0.0",
"zod": "^3.0.0" "zod": "^3.0.0"

View file

@ -98,7 +98,7 @@ Tools declare their capabilities via a `tpmjs` field in package.json:
{ {
"name": "@myorg/web-scraper", "name": "@myorg/web-scraper",
"version": "1.0.0", "version": "1.0.0",
"keywords": ["tpmjs-tool"], "keywords": ["tpmjs"],
"tpmjs": { "tpmjs": {
"category": "web-scraping", "category": "web-scraping",
"frameworks": ["vercel-ai"], "frameworks": ["vercel-ai"],

View file

@ -34,7 +34,7 @@ The CLI asks for just your package name and uses sensible defaults for everythin
```bash ```bash
$ pnpmx @tpmjs/create-basic-tools $ pnpmx @tpmjs/create-basic-tools
┌ create-tpmjs-tool ┌ create-tpmjs
│ │
◇ Package name ◇ Package name
│ @myorg/content-tools │ @myorg/content-tools

View file

@ -18,7 +18,7 @@ import * as logger from './utils/logger.js';
* Runs the interactive CLI workflow * Runs the interactive CLI workflow
*/ */
export async function runInteractiveCLI(): Promise<GenerationResult> { export async function runInteractiveCLI(): Promise<GenerationResult> {
clack.intro(logger.bold('create-tpmjs-tool')); clack.intro(logger.bold('create-tpmjs'));
// Only ask for package name // Only ask for package name
const name = await clack.text({ const name = await clack.text({

View file

@ -18,7 +18,7 @@ export function generatePackageJson(config: GeneratorConfig): string {
dev: 'tsup --watch', dev: 'tsup --watch',
'type-check': 'tsc --noEmit', 'type-check': 'tsc --noEmit',
}, },
keywords: ['tpmjs-tool', 'ai-sdk', packageInfo.category], keywords: ['tpmjs', 'ai-sdk', packageInfo.category],
author: packageInfo.author || '', author: packageInfo.author || '',
license: packageInfo.license, license: packageInfo.license,
tpmjs: { tpmjs: {

View file

@ -10,4 +10,4 @@
- Supports both Markdown and MDX formats - Supports both Markdown and MDX formats
- Automatic slug generation, word count, and reading time calculation - Automatic slug generation, word count, and reading time calculation
- Rich TPMJS metadata including parameters, authentication, pricing, and AI agent guidance - Rich TPMJS metadata including parameters, authentication, pricing, and AI agent guidance
- Tagged with 'tpmjs-tool' keyword for NPM registry discovery - Tagged with 'tpmjs' keyword for NPM registry discovery

View file

@ -3,7 +3,7 @@
"version": "0.3.0", "version": "0.3.0",
"description": "A tool for creating structured blog posts with AI-generated content", "description": "A tool for creating structured blog posts with AI-generated content",
"type": "module", "type": "module",
"keywords": ["tpmjs-tool", "blog", "content", "ai", "writing"], "keywords": ["tpmjs", "blog", "content", "ai", "writing"],
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View file

@ -9,7 +9,7 @@
"build": "tsc", "build": "tsc",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "emoji", "ai", "text-processing"], "keywords": ["tpmjs", "emoji", "ai", "text-processing"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai"], "frameworks": ["vercel-ai"],

View file

@ -64,7 +64,7 @@ This package serves as a template for creating your own TPMJS tools. Key require
"description": "Your tool description" "description": "Your tool description"
} }
``` ```
5. **Include the `tpmjs-tool` keyword** in package.json 5. **Include the `tpmjs` keyword** in package.json
## Development ## Development

View file

@ -10,7 +10,7 @@
"clean": "rm -rf dist", "clean": "rm -rf dist",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "ai-sdk", "hello", "example"], "keywords": ["tpmjs", "ai-sdk", "hello", "example"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai"], "frameworks": ["vercel-ai"],

View file

@ -9,7 +9,7 @@
"build": "tsc", "build": "tsc",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "markdown", "formatter", "text-processing"], "keywords": ["tpmjs", "markdown", "formatter", "text-processing"],
"tpmjs": { "tpmjs": {
"category": "text-analysis", "category": "text-analysis",
"frameworks": ["vercel-ai"], "frameworks": ["vercel-ai"],

View file

@ -11,7 +11,7 @@
"clean": "rm -rf dist", "clean": "rm -rf dist",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "ai-sdk", "example"], "keywords": ["tpmjs", "ai-sdk", "example"],
"dependencies": { "dependencies": {
"ai": "6.0.0-beta.124", "ai": "6.0.0-beta.124",
"zod": "^4.0.0" "zod": "^4.0.0"

View file

@ -9,7 +9,7 @@
"dev": "tsc --watch", "dev": "tsc --watch",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "ai-sdk", "vercel-ai", "registry", "execute"], "keywords": ["tpmjs", "ai-sdk", "vercel-ai", "registry", "execute"],
"dependencies": { "dependencies": {
"ai": "6.0.0-beta.124" "ai": "6.0.0-beta.124"
}, },

View file

@ -9,7 +9,7 @@
"dev": "tsc --watch", "dev": "tsc --watch",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "ai-sdk", "vercel-ai", "registry", "search"], "keywords": ["tpmjs", "ai-sdk", "vercel-ai", "registry", "search"],
"dependencies": { "dependencies": {
"ai": "6.0.0-beta.124" "ai": "6.0.0-beta.124"
}, },

View file

@ -9,7 +9,7 @@
"dev": "tsc --watch", "dev": "tsc --watch",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "ai", "search", "tool-registry"], "keywords": ["tpmjs", "ai", "search", "tool-registry"],
"dependencies": { "dependencies": {
"ai": "6.0.0-beta.124", "ai": "6.0.0-beta.124",
"zod": "^3.23.0" "zod": "^3.23.0"

View file

@ -9,7 +9,7 @@
"dev": "tsc --watch", "dev": "tsc --watch",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"keywords": ["tpmjs-tool", "ai-sdk", "code-execution", "sandbox", "unsandbox"], "keywords": ["tpmjs", "ai-sdk", "code-execution", "sandbox", "unsandbox"],
"dependencies": { "dependencies": {
"@thomasdavis/unsandbox": "^0.0.3" "@thomasdavis/unsandbox": "^0.0.3"
}, },