Compare commits
252 commits
main
...
@tpmjs/too
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81405982ce | ||
|
|
aaf3cdd424 | ||
|
|
7edd4f43b2 | ||
|
|
4043df2f91 | ||
|
|
e9a7a689a1 | ||
|
|
24bc48bcef | ||
|
|
10116cd460 | ||
|
|
5d9e256660 | ||
|
|
6ef33735a7 | ||
|
|
d132da9c20 | ||
|
|
bfdb4064b1 | ||
|
|
1fd5407944 | ||
|
|
957cfd5828 | ||
|
|
a03e7a560b | ||
|
|
5200f271f9 | ||
|
|
7e6c0194c6 | ||
|
|
364f507518 | ||
|
|
f24eebc196 | ||
|
|
6c28f01633 | ||
|
|
1e779007be | ||
|
|
bda49615fb | ||
|
|
20f5fcc4b0 | ||
|
|
4f191d64c0 | ||
|
|
06029fbc60 | ||
|
|
fa9b2c626d | ||
|
|
87b200a735 | ||
|
|
6cb172f87b | ||
|
|
0f7f17ba94 | ||
|
|
d5efd970c2 | ||
|
|
16f8f79fbf | ||
|
|
6c5e1a84c5 | ||
|
|
75a386359c | ||
|
|
7a23181339 | ||
|
|
cd895c10cb | ||
|
|
c7c375138a | ||
|
|
b4f6eb46b8 | ||
|
|
52ea479ad9 | ||
|
|
c2cc29114c | ||
|
|
82f7d2da72 | ||
|
|
e5fea299dd | ||
|
|
a2ea72739a | ||
|
|
a3cf662b1c | ||
|
|
6cc8c5bdcf | ||
|
|
91e00afde4 | ||
|
|
d806a51599 | ||
|
|
f157393ec7 | ||
|
|
c0c805b7b1 | ||
|
|
751fc24bfd | ||
|
|
6f2218f88d | ||
|
|
ada3ffb341 | ||
|
|
1a1cc36ff0 | ||
|
|
10792991ea | ||
|
|
cad30ab931 | ||
|
|
84d1e382a1 | ||
|
|
704632bc75 | ||
|
|
3b77f74d4b | ||
|
|
aadd304ca5 | ||
|
|
984936a3d9 | ||
|
|
0062cfee8d | ||
|
|
65efebe692 | ||
|
|
5cc223af6e | ||
|
|
f7fd6ad023 | ||
|
|
fb1ed57471 | ||
|
|
0a3b004243 | ||
|
|
d2b22201aa | ||
|
|
6ccb5fbd79 | ||
|
|
ad694e27e1 | ||
|
|
098d25b6e0 | ||
|
|
ff17182e04 | ||
|
|
5ebad3cd35 | ||
|
|
3cb636a703 | ||
|
|
72bdc5e854 | ||
|
|
7aabba7e8f | ||
|
|
5dd3a6b631 | ||
|
|
d78025723f | ||
|
|
eaefae34c3 | ||
|
|
d261c0cce0 | ||
|
|
9f1f93b532 | ||
|
|
604deb224a | ||
|
|
67df5e456d | ||
|
|
48c777425d | ||
|
|
34ae497c75 | ||
|
|
7fd6ab9dc6 | ||
|
|
b2984d1f61 | ||
|
|
5df484718a | ||
|
|
3ce5ea9bb7 | ||
|
|
a793322710 | ||
|
|
a99ea61cfe | ||
|
|
55bb521d77 | ||
|
|
25e83a1809 | ||
|
|
0a856dda39 | ||
|
|
db482855d3 | ||
|
|
196cbacf62 | ||
|
|
46088f09c3 | ||
|
|
36b84294b4 | ||
|
|
aa1001a0ee | ||
|
|
dcab29baa2 | ||
|
|
53d30f7eca | ||
|
|
92135cf9f0 | ||
|
|
02b9b7e484 | ||
|
|
be5cae3f1c | ||
|
|
463889b7c4 | ||
|
|
051550ee76 | ||
|
|
2a46021341 | ||
|
|
6c3267eb98 | ||
|
|
c2447e8467 | ||
|
|
6b11a2f9a7 | ||
|
|
25dec1795b | ||
|
|
572b7a081d | ||
|
|
970e161390 | ||
|
|
0804f1bd1e | ||
|
|
1a2128d3ae | ||
|
|
1050914f86 | ||
|
|
38a9710474 | ||
|
|
b27f377319 | ||
|
|
2a16218871 | ||
|
|
256699099b | ||
|
|
ee31201204 | ||
|
|
07b23e6ef6 | ||
|
|
bdacc7142e | ||
|
|
5561d5c177 | ||
|
|
e9e995fd14 | ||
|
|
66b24dd13e | ||
|
|
c962daabce | ||
|
|
937ffc3194 | ||
|
|
8162b1366f | ||
|
|
d8b5f67a1c | ||
|
|
bb967ec527 | ||
|
|
8d0cdc1154 | ||
|
|
579bfcfbeb | ||
|
|
f50f838328 | ||
|
|
b934cd4c64 | ||
|
|
96c53ed49a | ||
|
|
79547ff52e | ||
|
|
6765ad9f17 | ||
|
|
6ec6f790d3 | ||
|
|
750ff2afbe | ||
|
|
227a7e2dd1 | ||
|
|
74811cc269 | ||
|
|
eb8a5ff91e | ||
|
|
578ce944d2 | ||
|
|
9fae5f22b2 | ||
|
|
3cfc141944 | ||
|
|
8b7a52ef89 | ||
|
|
20353987e9 | ||
|
|
75f652e2f2 | ||
|
|
051e58c23d | ||
|
|
c4161f1d35 | ||
|
|
31b29c306f | ||
|
|
0f758aecca | ||
|
|
e384951aa8 | ||
|
|
e97ecfaa31 | ||
|
|
5b5cffa248 | ||
|
|
722cb6af0b | ||
|
|
0e907338ed | ||
|
|
f6812b2267 | ||
|
|
9abff1f203 | ||
|
|
5b5be16770 | ||
|
|
177f8136a0 | ||
|
|
07809784db | ||
|
|
f50b747e45 | ||
|
|
addb6ba06e | ||
|
|
56a6128ac6 | ||
|
|
2c2eac7992 | ||
|
|
801b0d81b7 | ||
|
|
564543274d | ||
|
|
daedf035af | ||
|
|
fd04674501 | ||
|
|
1642ec07d9 | ||
|
|
dc4846c7b1 | ||
|
|
a39f28546a | ||
|
|
b4e22723bb | ||
|
|
709c2c20db | ||
|
|
c200a10015 | ||
|
|
84c6a579b2 | ||
|
|
bf8d27a59c | ||
|
|
f0ecdab824 | ||
|
|
787410a7e0 | ||
|
|
e5ba9fa441 | ||
|
|
b00eae9fa2 | ||
|
|
148207bcaa | ||
|
|
8562eb5b38 | ||
|
|
934c8e9c0b | ||
|
|
19837ae800 | ||
|
|
d3f8adc0ea | ||
|
|
d0110f990d | ||
|
|
c6b4456baf | ||
|
|
4094c661be | ||
|
|
d339637ebf | ||
|
|
9f85898937 | ||
|
|
45b477397f | ||
|
|
cc14476a36 | ||
|
|
8b5757c9aa | ||
|
|
54b504f661 | ||
|
|
d4ec877c6c | ||
|
|
e8ef703c6e | ||
|
|
f127b47f08 | ||
|
|
c18d1b7ea6 | ||
|
|
b6cc98d1e0 | ||
|
|
b95edd8541 | ||
|
|
1e61ba1551 | ||
|
|
b7d1accc6f | ||
|
|
09df09c4df | ||
|
|
d3b928c97e | ||
|
|
3450d7a6d5 | ||
|
|
2158ee6dfd | ||
|
|
0612eac5e2 | ||
|
|
fdd1b2c304 | ||
|
|
635fc96cac | ||
|
|
fcd6667357 | ||
|
|
14cd668a2b | ||
|
|
493cd96d7b | ||
|
|
2bf8565c1d | ||
|
|
7fab0440b3 | ||
|
|
bc34f50705 | ||
|
|
4febe71d76 | ||
|
|
8e8e511e5a | ||
|
|
054bac9a53 | ||
|
|
4146f279e6 | ||
|
|
4dc53338f2 | ||
|
|
251442f1d4 | ||
|
|
c8ccab5cb8 | ||
|
|
0b4a314bd1 | ||
|
|
61ac2aa5dd | ||
|
|
47ddc4dea7 | ||
|
|
45da32e8ec | ||
|
|
266a6ef74c | ||
|
|
2a05e68aa3 | ||
|
|
78c72b85f3 | ||
|
|
59da95ce85 | ||
|
|
1cdbf8c891 | ||
|
|
c0383f5440 | ||
|
|
ad4fde7e79 | ||
|
|
4705af6b1a | ||
|
|
8f3c6a2910 | ||
|
|
fdfc721d40 | ||
|
|
cc13e7cb09 | ||
|
|
492a7221c9 | ||
|
|
664bbdad57 | ||
|
|
140a2fa218 | ||
|
|
2ce37ae2aa | ||
|
|
b176ae54df | ||
|
|
d73db1c333 | ||
|
|
ca57de533a | ||
|
|
6eb51d1371 | ||
|
|
91f950d453 | ||
|
|
48c8775acf | ||
|
|
5feac0d2db | ||
|
|
c52a044b47 | ||
|
|
f9ba1c094e | ||
|
|
e38e627a07 | ||
|
|
7f5d307c0f |
505 changed files with 57457 additions and 4861 deletions
|
|
@ -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
41
.env.vercel.production
Normal 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
18
.github/workflows/health-check.yml
vendored
Normal 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"
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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
18
.github/workflows/sync-changes.yml
vendored
Normal 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
131
.github/workflows/sync-keyword.yml
vendored
Normal 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
97
.github/workflows/sync-manual.yml
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
name: Sync Manual Tools
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at midnight UTC
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
# Run on pushes to main that modify manual-tools.ts
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'manual-tools.ts'
|
||||
- 'sync-manual-tools.ts'
|
||||
|
||||
jobs:
|
||||
sync-manual:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Prisma Client
|
||||
run: pnpm --filter=@tpmjs/db db:generate
|
||||
|
||||
- name: Run manual tools sync
|
||||
id: sync
|
||||
run: |
|
||||
# Run the sync script and capture output
|
||||
output=$(pnpm tsx sync-manual-tools.ts 2>&1)
|
||||
echo "$output"
|
||||
|
||||
# Extract statistics from output
|
||||
processed=$(echo "$output" | grep "Processed:" | awk '{print $2}')
|
||||
skipped=$(echo "$output" | grep "Skipped:" | awk '{print $2}')
|
||||
errors=$(echo "$output" | grep "Errors:" | awk '{print $2}')
|
||||
total=$(echo "$output" | grep "Total manual tools:" | awk '{print $4}')
|
||||
|
||||
# Set outputs for Discord notification
|
||||
echo "processed=${processed:-0}" >> $GITHUB_OUTPUT
|
||||
echo "skipped=${skipped:-0}" >> $GITHUB_OUTPUT
|
||||
echo "errors=${errors:-0}" >> $GITHUB_OUTPUT
|
||||
echo "total=${total:-0}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "${errors:-0}" -gt 0 ]; then
|
||||
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
|
||||
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
|
||||
else
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
|
||||
fi
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
# Build Discord payload
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} Manual Tools Sync" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
color: $color,
|
||||
fields: [
|
||||
{ name: "📦 Total Tools", value: "${{ steps.sync.outputs.total }}", inline: true },
|
||||
{ name: "✨ Processed", value: "${{ steps.sync.outputs.processed }}", inline: true },
|
||||
{ name: "⏭️ Skipped", value: "${{ steps.sync.outputs.skipped }}", inline: true },
|
||||
{ name: "❌ Errors", value: "${{ steps.sync.outputs.errors }}", inline: true },
|
||||
{ name: "🔗 Run", value: "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", inline: true }
|
||||
],
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Discord
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload"
|
||||
18
.github/workflows/sync-metrics.yml
vendored
Normal file
18
.github/workflows/sync-metrics.yml
vendored
Normal 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"
|
||||
267
.github/workflows/sync-vercel-registry.yml
vendored
Normal file
267
.github/workflows/sync-vercel-registry.yml
vendored
Normal 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
7
.vercelignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.turbo
|
||||
.next
|
||||
dist
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
421
CLAUDE.md
|
|
@ -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
|
||||
|
|
@ -167,7 +167,7 @@ Not needed for Option 1 (Deployment Protection).
|
|||
Add to README.md to show CI status:
|
||||
|
||||
```markdown
|
||||
[](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml)
|
||||
[](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
|
|
|||
424
HOW_TO_PUBLISH_A_TOOL.md
Normal file
424
HOW_TO_PUBLISH_A_TOOL.md
Normal 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
|
||||
|
|
@ -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
331
LAUNCH_REVIEW.md
Normal 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
21
LICENSE
Normal 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
293
MANUAL_TOOLS.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Manual Tools Registry
|
||||
|
||||
## Overview
|
||||
|
||||
This system allows TPMJS to include high-quality tools that don't follow the standard `tpmjs` field specification in their package.json. These tools are manually curated and synced to the database.
|
||||
|
||||
## Why Manual Tools?
|
||||
|
||||
Some excellent tools (like Vercel's code execution, Exa search, Firecrawl, etc.) don't include the `tpmjs` field in their package.json. Rather than wait for these package maintainers to adopt the spec, we manually curate metadata for these tools.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Files
|
||||
|
||||
1. **`manual-tools.ts`** - The registry of manually curated tools
|
||||
2. **`sync-manual-tools.ts`** - Script to sync manual tools to database
|
||||
3. **`MANUAL_TOOLS.md`** - This documentation
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Manual Tool Registry** (`manual-tools.ts`)
|
||||
- Exports a `manualTools` array with metadata for each tool
|
||||
- Each entry includes npm package name, export name, category, description, parameters, etc.
|
||||
- Follows the same schema as the standard `tpmjs` field
|
||||
|
||||
2. **Sync Script** (`sync-manual-tools.ts`)
|
||||
- Fetches latest package metadata from npm
|
||||
- Combines npm metadata with manual metadata
|
||||
- Upserts Package + Tool records to database
|
||||
- Marks tools with `discoveryMethod: 'manual'`
|
||||
|
||||
3. **Database Storage**
|
||||
- Manual tools stored in same `packages` and `tools` tables as auto-discovered tools
|
||||
- No special handling needed in API or frontend
|
||||
- `discoveryMethod: 'manual'` field distinguishes them
|
||||
|
||||
## Adding a New Manual Tool
|
||||
|
||||
### Step 1: Add to Registry
|
||||
|
||||
Edit `manual-tools.ts` and add a new entry:
|
||||
|
||||
```typescript
|
||||
{
|
||||
npmPackageName: 'example-package',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
exportName: 'exampleTool',
|
||||
description: 'A clear, concise description of what this tool does',
|
||||
|
||||
// Optional but recommended for 'rich' tier
|
||||
parameters: [
|
||||
{
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
description: 'The search query',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
returns: {
|
||||
type: 'array',
|
||||
description: 'Array of search results',
|
||||
},
|
||||
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to search for X',
|
||||
limitations: 'Rate limits apply',
|
||||
examples: [
|
||||
'Search for current news',
|
||||
'Find specific information',
|
||||
],
|
||||
},
|
||||
|
||||
// Environment variables
|
||||
env: [
|
||||
{
|
||||
name: 'EXAMPLE_API_KEY',
|
||||
description: 'API key for the service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Additional metadata
|
||||
tags: ['search', 'web'],
|
||||
docsUrl: 'https://example.com/docs',
|
||||
apiKeyUrl: 'https://example.com/api-keys',
|
||||
websiteUrl: 'https://example.com',
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Run Sync Script
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
pnpm tsx sync-manual-tools.ts
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Fetch the package from npm
|
||||
2. Create/update Package record
|
||||
3. Create/update Tool record(s)
|
||||
4. Set `discoveryMethod: 'manual'`
|
||||
|
||||
### Step 3: Verify
|
||||
|
||||
Check that the tool appears on tpmjs.com:
|
||||
|
||||
```bash
|
||||
# Start dev server
|
||||
pnpm dev --filter=@tpmjs/web
|
||||
|
||||
# Visit http://localhost:3000/tool/tool-search
|
||||
# Search for your package name
|
||||
```
|
||||
|
||||
## Multi-Tool Packages
|
||||
|
||||
If a package exports multiple tools, add multiple entries with the same `npmPackageName` but different `exportName`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
exportName: 'scrapeTool',
|
||||
description: 'Scrape websites...',
|
||||
// ...
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
exportName: 'searchTool',
|
||||
description: 'Search the web...',
|
||||
// ...
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
exportName: 'crawlTool',
|
||||
description: 'Crawl entire websites...',
|
||||
// ...
|
||||
},
|
||||
```
|
||||
|
||||
## Tier Calculation
|
||||
|
||||
Tools are automatically assigned a tier:
|
||||
|
||||
- **Rich tier**: Has `parameters` OR `returns` OR `aiAgent` fields
|
||||
- **Minimal tier**: Only has basic metadata
|
||||
|
||||
Rich tier tools get 4x quality score multiplier, so add detailed metadata when possible.
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updating Manual Tools
|
||||
|
||||
1. Edit the entry in `manual-tools.ts`
|
||||
2. Run `pnpm tsx sync-manual-tools.ts`
|
||||
3. The upsert will update existing records
|
||||
|
||||
### Removing Manual Tools
|
||||
|
||||
1. Remove the entry from `manual-tools.ts`
|
||||
2. Manually delete from database OR wait for metrics sync to mark as stale
|
||||
|
||||
### Version Updates
|
||||
|
||||
The sync script automatically fetches the latest version from npm unless you specify `npmVersion` in the manual tool entry.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Option 1: Manual Sync on Deploy
|
||||
|
||||
Add to your deployment workflow:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
- name: Sync manual tools
|
||||
run: pnpm tsx sync-manual-tools.ts
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
### Option 2: Scheduled Sync
|
||||
|
||||
Create a cron job or GitHub Action to sync periodically:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/sync-manual.yml
|
||||
name: Sync Manual Tools
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly on Sunday
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm tsx sync-manual-tools.ts
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
### Option 3: API Endpoint
|
||||
|
||||
Create a sync endpoint (similar to keyword/changes sync):
|
||||
|
||||
```typescript
|
||||
// apps/web/src/app/api/sync/manual/route.ts
|
||||
import { manualTools } from '@/manual-tools';
|
||||
// ... sync logic
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Verify CRON_SECRET
|
||||
// Run manual sync
|
||||
// Return results
|
||||
}
|
||||
```
|
||||
|
||||
## Currently Included Manual Tools
|
||||
|
||||
As of this documentation:
|
||||
|
||||
- **ai-sdk-tool-code-execution** - Vercel Sandbox code execution
|
||||
- **@exalabs/ai-sdk** - Exa web search
|
||||
- **@parallel-web/ai-sdk-tools** - Parallel search and extraction (2 tools)
|
||||
- **ctx-zip** - MCP + Vercel Sandbox integration
|
||||
- **@perplexity-ai/ai-sdk** - Perplexity search
|
||||
- **@tavily/ai-sdk** - Tavily web research
|
||||
- **firecrawl-aisdk** - Firecrawl scraping, search, crawling (3 tools)
|
||||
- **bedrock-agentcore** - AWS Bedrock code interpreter and browser (2 tools)
|
||||
- **@superagent-ai/ai-sdk** - Superagent security tools (3 tools)
|
||||
- **@valyu/ai-sdk** - Valyu domain-specific search tools (8 tools)
|
||||
|
||||
**Total: 24 manually curated tools across 10 packages**
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why not just ask package maintainers to add the tpmjs field?
|
||||
|
||||
We should! But:
|
||||
1. Some packages are from large companies (Vercel, AWS, etc.) with slow adoption cycles
|
||||
2. We want these tools available on TPMJS now
|
||||
3. Manual curation lets us provide better metadata than package authors might
|
||||
|
||||
### Will manual tools be replaced by auto-discovered ones?
|
||||
|
||||
Yes! If a package adds a proper `tpmjs` field, the auto-discovery sync will update it with `discoveryMethod: 'keyword'` or `'changes-feed'`. Manual entries can then be removed from `manual-tools.ts`.
|
||||
|
||||
### Can I mix manual and auto-discovered tools from the same package?
|
||||
|
||||
Yes. If a package has some tools in the `tpmjs` field but is missing others, you can manually add the missing ones. The sync scripts will coexist peacefully.
|
||||
|
||||
### How do I know if a tool is manually curated?
|
||||
|
||||
Check the `discoveryMethod` field in the database:
|
||||
- `'manual'` = Manually curated
|
||||
- `'keyword'` = Auto-discovered via keyword search
|
||||
- `'changes-feed'` = Auto-discovered via npm changes feed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Complete Metadata** - Provide as much metadata as possible for rich tier
|
||||
2. **Accurate Descriptions** - Tool descriptions should be clear and specific
|
||||
3. **AI-Friendly** - Write `aiAgent.useCase` as guidance for LLMs
|
||||
4. **Keep Updated** - Periodically check if packages have added native `tpmjs` support
|
||||
5. **Link to Docs** - Always include `docsUrl` when available
|
||||
6. **API Key URLs** - Include `apiKeyUrl` for tools requiring authentication
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute new manual tools:
|
||||
|
||||
1. Fork the repository
|
||||
2. Add your tool to `manual-tools.ts`
|
||||
3. Test with `pnpm tsx sync-manual-tools.ts`
|
||||
4. Open a pull request with:
|
||||
- Why this tool should be included
|
||||
- Link to the npm package
|
||||
- Screenshot of it working in TPMJS
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [HOW_TO_PUBLISH_A_TOOL.md](./HOW_TO_PUBLISH_A_TOOL.md) - Standard tpmjs field spec
|
||||
- [CLAUDE.md](./CLAUDE.md) - General project documentation
|
||||
- [packages/types/src/tpmjs.ts](./packages/types/src/tpmjs.ts) - TypeScript schema definitions
|
||||
500
NPM_MIRROR.md
500
NPM_MIRROR.md
|
|
@ -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**
|
||||
57
README.md
57
README.md
|
|
@ -1,10 +1,59 @@
|
|||
# TPMJS Monorepo
|
||||
# TPMJS
|
||||
|
||||
[](https://github.com/YOUR_ORG/tpmjs/actions/workflows/ci.yml)
|
||||
[](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
391
TPMJS_TALK.md
Normal 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.*
|
||||
|
|
@ -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
67
alien.sh
Executable 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"
|
||||
2
apps/playground/.env.local.example
Normal file
2
apps/playground/.env.local.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Required: OpenAI API key
|
||||
OPENAI_API_KEY=sk-...
|
||||
16
apps/playground/eslint.config.mjs
Normal file
16
apps/playground/eslint.config.mjs
Normal 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
6
apps/playground/next-env.d.ts
vendored
Normal 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.
|
||||
11
apps/playground/next.config.ts
Normal file
11
apps/playground/next.config.ts
Normal 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;
|
||||
50
apps/playground/package.json
Normal file
50
apps/playground/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
apps/playground/postcss.config.mjs
Normal file
8
apps/playground/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
252
apps/playground/src/app/api/chat/route.ts
Normal file
252
apps/playground/src/app/api/chat/route.ts
Normal 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' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
85
apps/playground/src/app/api/tools/route.ts
Normal file
85
apps/playground/src/app/api/tools/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
30
apps/playground/src/app/globals.css
Normal file
30
apps/playground/src/app/globals.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
44
apps/playground/src/app/layout.tsx
Normal file
44
apps/playground/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
apps/playground/src/app/page.tsx
Normal file
24
apps/playground/src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
apps/playground/src/components/chat/ChatHeader.tsx
Normal file
52
apps/playground/src/components/chat/ChatHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
apps/playground/src/components/chat/ChatInput.tsx
Normal file
63
apps/playground/src/components/chat/ChatInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
apps/playground/src/components/chat/ChatInterface.tsx
Normal file
38
apps/playground/src/components/chat/ChatInterface.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
apps/playground/src/components/chat/ChatMessages.tsx
Normal file
60
apps/playground/src/components/chat/ChatMessages.tsx
Normal 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: “Tell me a fish joke” or “What tools are available?”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
225
apps/playground/src/components/chat/MessageBubble.tsx
Normal file
225
apps/playground/src/components/chat/MessageBubble.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
193
apps/playground/src/components/sidebar/SettingsSidebar.tsx
Normal file
193
apps/playground/src/components/sidebar/SettingsSidebar.tsx
Normal 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;
|
||||
}
|
||||
298
apps/playground/src/components/sidebar/ToolsSidebar.tsx
Normal file
298
apps/playground/src/components/sidebar/ToolsSidebar.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
apps/playground/src/env.ts
Normal file
7
apps/playground/src/env.ts
Normal 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(),
|
||||
});
|
||||
59
apps/playground/src/hooks/useChat.ts
Normal file
59
apps/playground/src/hooks/useChat.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
46
apps/playground/src/hooks/useToolUsage.ts
Normal file
46
apps/playground/src/hooks/useToolUsage.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
279
apps/playground/src/lib/dynamic-tool-loader.ts
Normal file
279
apps/playground/src/lib/dynamic-tool-loader.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
94
apps/playground/src/lib/tool-loader.ts
Normal file
94
apps/playground/src/lib/tool-loader.ts
Normal 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);
|
||||
}
|
||||
37
apps/playground/src/lib/types.ts
Normal file
37
apps/playground/src/lib/types.ts
Normal 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 } };
|
||||
12
apps/playground/tailwind.config.ts
Normal file
12
apps/playground/tailwind.config.ts
Normal 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;
|
||||
12
apps/playground/tsconfig.json
Normal file
12
apps/playground/tsconfig.json
Normal 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
3
apps/railway-executor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
17
apps/railway-executor/Dockerfile
Normal file
17
apps/railway-executor/Dockerfile
Normal 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"]
|
||||
160
apps/railway-executor/README.md
Normal file
160
apps/railway-executor/README.md
Normal 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
939
apps/railway-executor/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
apps/railway-executor/package.json
Normal file
15
apps/railway-executor/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
229
apps/railway-executor/server.js
Normal file
229
apps/railway-executor/server.js
Normal 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`);
|
||||
});
|
||||
957
apps/railway-executor/server.ts
Normal file
957
apps/railway-executor/server.ts
Normal 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
1
apps/tutorial/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.vercel
|
||||
16
apps/tutorial/eslint.config.mjs
Normal file
16
apps/tutorial/eslint.config.mjs
Normal 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
6
apps/tutorial/next-env.d.ts
vendored
Normal 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.
|
||||
8
apps/tutorial/next.config.ts
Normal file
8
apps/tutorial/next.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils'],
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
35
apps/tutorial/package.json
Normal file
35
apps/tutorial/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
apps/tutorial/postcss.config.js
Normal file
6
apps/tutorial/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
16
apps/tutorial/src/app/agent-example/page.tsx
Normal file
16
apps/tutorial/src/app/agent-example/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
apps/tutorial/src/app/agents/page.tsx
Normal file
11
apps/tutorial/src/app/agents/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
apps/tutorial/src/app/authors/page.tsx
Normal file
11
apps/tutorial/src/app/authors/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
apps/tutorial/src/app/first-tool/page.tsx
Normal file
16
apps/tutorial/src/app/first-tool/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
143
apps/tutorial/src/app/globals.css
Normal file
143
apps/tutorial/src/app/globals.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
apps/tutorial/src/app/layout.tsx
Normal file
38
apps/tutorial/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
apps/tutorial/src/app/overview/page.tsx
Normal file
16
apps/tutorial/src/app/overview/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
282
apps/tutorial/src/app/page.tsx
Normal file
282
apps/tutorial/src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
apps/tutorial/src/app/playground/page.tsx
Normal file
16
apps/tutorial/src/app/playground/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
apps/tutorial/src/components/AgentExampleSlideshow.tsx
Normal file
77
apps/tutorial/src/components/AgentExampleSlideshow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
apps/tutorial/src/components/AgentSlideshow.tsx
Normal file
91
apps/tutorial/src/components/AgentSlideshow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
apps/tutorial/src/components/AuthorSlideshow.tsx
Normal file
91
apps/tutorial/src/components/AuthorSlideshow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
apps/tutorial/src/components/FirstToolSlideshow.tsx
Normal file
77
apps/tutorial/src/components/FirstToolSlideshow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
85
apps/tutorial/src/components/OverviewSlideshow.tsx
Normal file
85
apps/tutorial/src/components/OverviewSlideshow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
apps/tutorial/src/components/PlaygroundSlideshow.tsx
Normal file
77
apps/tutorial/src/components/PlaygroundSlideshow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
apps/tutorial/src/components/Slide.tsx
Normal file
46
apps/tutorial/src/components/Slide.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
69
apps/tutorial/src/components/SlideNavigation.tsx
Normal file
69
apps/tutorial/src/components/SlideNavigation.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
46
apps/tutorial/src/components/SlideProgress.tsx
Normal file
46
apps/tutorial/src/components/SlideProgress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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't have installed.
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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-tool 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-tool"'}</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-tool</code> keyword triggers
|
||||
automatic indexing
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue