Compare commits

..

No commits in common. "main" and "@tpmjs/ui@0.1.1" have entirely different histories.

2329 changed files with 1899 additions and 1227648 deletions

View file

@ -7,7 +7,11 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@tpmjs/config", "@tpmjs/storybook", "@tpmjs/web"],
"ignore": [
"@tpmjs/config",
"@tpmjs/storybook",
"@tpmjs/web"
],
"privatePackages": {
"version": false,
"tag": false

View file

@ -1,102 +0,0 @@
---
description: Develop and validate TPMJS tools using the blocks CLI
---
Help the user develop new tools for the TPMJS registry using the blocks CLI. This workflow covers defining tools in blocks.yml, implementing them with AI SDK v6, validating with the blocks CLI, and publishing to npm.
## Development Workflow
### 1. Define Tool in blocks.yml
Add tool definition to `packages/tools/official/blocks.yml`:
```yaml
blocks:
category.toolName:
type: utility
description: "Clear description for LLMs"
path: "tool-directory-name"
domain_rules:
- id: rule_name
description: "Implementation requirement"
inputs:
- name: paramName
type: string
description: "Parameter description"
outputs:
- name: result
type: ResultType
description: "Output description"
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
```
### 2. Create Package Structure
```
packages/tools/official/tool-name/
├── package.json # npm package with tpmjs field
├── tsconfig.json # Extends @tpmjs/tsconfig
├── tsup.config.ts # Build config
├── block.ts # REQUIRED by validator
├── index.ts # Re-export from src
└── src/index.ts # Main implementation
```
### 3. Implement with AI SDK v6
```typescript
import { jsonSchema, tool } from 'ai';
export const myTool = tool({
description: 'Description for LLMs',
parameters: jsonSchema<InputType>({
type: 'object',
properties: { /* ... */ },
required: ['field1'],
}),
async execute(input): Promise<OutputType> {
// REAL implementation - no stubs
return result;
},
});
export default myTool;
```
### 4. Run Validation
```bash
cd packages/tools/official
pnpm blocks run tool-name # Validate single tool
pnpm blocks run tool-name --force # Force full validation
pnpm blocks run --all # Validate all tools
```
### 5. Build and Publish
```bash
pnpm build
npm publish --access public
# Trigger sync to tpmjs.com
source apps/web/.env.local
curl -X POST https://tpmjs.com/api/sync/keyword -H "Authorization: Bearer $CRON_SECRET"
```
## Valid Categories
For `tpmjs.category` in package.json: `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`
## Required Files
- **block.ts** at root: `export const block = { name: 'tool-name', tools: { myTool } };`
- **index.ts** at root: `export * from './src/index.js';`
- Both are required for the validator to find the tool
## Common Issues
- "invalid tpmjs field" during sync = Invalid category or missing tools array
- "Tool not found in exports" = Export name must match blocks.yml
- "Required file not found" = Need index.ts and block.ts at package root
When helping the user, read the full skill documentation at `.claude/skills/blocks-develop.md` for comprehensive details on entities, measures, and multi-tool packages.

View file

@ -1,13 +0,0 @@
---
description: Check if database connection is working and show table counts
---
Check if the database connection is working by:
1. Running `pnpm --filter=@tpmjs/db db:studio` in the background to verify Prisma can connect
2. If successful, kill the studio process immediately
3. Report connection status to the user
Then show me the current row counts for all tables (Tool, SyncCheckpoint, SyncLog) by writing a quick script that imports the Prisma client and queries each table.
This helps verify the database is properly configured and shows if any data exists.

View file

@ -1,12 +0,0 @@
---
description: Create and apply a Prisma migration
---
Create and apply a new Prisma migration:
1. Run `pnpm --filter=@tpmjs/db db:migrate` to create and apply a migration
2. The migration will be named automatically based on changes
3. After migration completes, regenerate the Prisma client with `pnpm --filter=@tpmjs/db db:generate`
4. Report the results to the user
This is used for production-ready database schema changes that create migration files.

View file

@ -1,11 +0,0 @@
---
description: Push Prisma schema changes to database (dev only)
---
Push the current Prisma schema to the database without creating migrations:
1. Run `pnpm --filter=@tpmjs/db db:push` to sync schema changes to the database
2. After push completes, regenerate the Prisma client with `pnpm --filter=@tpmjs/db db:generate`
3. Report the results to the user
This is useful for development when you want to quickly iterate on schema changes without creating migration files. Do NOT use this in production - use db:migrate instead.

View file

@ -1,11 +0,0 @@
---
description: Seed the database with initial data
---
Run the database seed script to initialize the database with default data:
1. Run `pnpm --filter=@tpmjs/db db:seed` to execute the seed script
2. The seed script will create initial SyncCheckpoint records for the changes feed and keyword sync
3. Report the results to the user
This should be run once after creating the database to set up the initial sync checkpoints. It's safe to run multiple times - it will only create records if they don't already exist.

View file

@ -1,13 +0,0 @@
---
description: Open Prisma Studio to view and edit database data
---
Open Prisma Studio, a visual database browser:
1. Run `pnpm --filter=@tpmjs/db db:studio` in the background
2. Wait for the "Prisma Studio is up on http://localhost:5555" message
3. Tell the user that Prisma Studio is now running at http://localhost:5555
4. Remind the user they can view and edit all database tables (Tool, SyncCheckpoint, SyncLog) in the browser
5. Tell them to use Ctrl+C in the terminal or kill the background process when done
This provides a visual interface to browse, search, and edit database records.

View file

@ -1,248 +0,0 @@
# Tool Request Pipeline Specification
Automated pipeline for creating TPMJS tools from GitHub issues using Claude.
## Overview
When a maintainer applies the `tool-request` label to an issue, Claude automatically:
1. Analyzes the tool idea and designs the implementation
2. Determines the best package (existing or new)
3. Implements the tool with AI SDK v6
4. Validates using blocks CLI
5. Creates an auto-merge PR
6. Publishes to npm
7. Syncs to tpmjs.com registry
## Trigger
| Setting | Value |
|---------|-------|
| Label | `tool-request` |
| Who can apply | Maintainers only |
| Trigger mechanism | Label application triggers workflow, which comments `@claude` |
| Concurrency | Parallel execution allowed |
| Rate limit | None (trust maintainers) |
## Input Requirements
| Setting | Value |
|---------|-------|
| Input format | Accept vague ideas - Claude designs autonomously |
| Structured template | Not required |
| Clarification | Claude fills gaps autonomously, doesn't ask first |
| Mid-flight edits | Incorporate edits - check for changes at each step |
## Package Organization
| Setting | Value |
|---------|-------|
| Strategy | Hybrid - default to categories, allow functional cohesion exceptions |
| Package selection | Analyze all existing tools in candidate packages to find best fit |
| New vs existing | Claude decides based on functional cohesion analysis |
| blocks.yml access | Full access - Claude adds entries as part of workflow |
### Decision Logic for Package Selection
1. Search existing packages for functionally related tools
2. If strong match found (>70% conceptual overlap), add to existing package
3. If no match or tool is foundational for a new domain, create new package
4. Exception: tightly coupled tools (e.g., e2b-*) stay together regardless of category
## Validation & Iteration
| Setting | Value |
|---------|-------|
| Max attempts | 3 before escalating to human review |
| On failure | Iterate in-issue - Claude fixes and retries |
| Runtime test | Execute with sample inputs, capture output as screenshot |
| Tool restrictions | None - any valid tool that passes validation is allowed |
### Validation Steps
1. `pnpm blocks run <tool-name>` - domain rules and output measures
2. TypeScript compilation check
3. Execute tool with generated sample inputs
4. Verify output structure matches schema
5. Capture execution output as proof in issue comment
## Publishing
| Setting | Value |
|---------|-------|
| Branch strategy | Auto-merge PR - create for visibility, auto-merge if CI passes |
| Version bump | Minor (0.X.0) - new functionality = minor version |
| NPM auth | Use existing `NPM_TOKEN` secret |
| On publish failure | Comment explaining failure, wait for human to fix and re-trigger |
### PR Template
```markdown
## Tool: `<tool-name>`
**Package:** `@tpmjs/tools-<package>`
**Version:** `0.X.0` -> `0.Y.0`
### Description
<tool description>
### Implementation
- [ ] blocks.yml entry added
- [ ] Package files created
- [ ] Validation passed
- [ ] Runtime test passed
### Test Output
<screenshot of tool execution>
---
Auto-generated by Claude from #<issue-number>
```
## Post-Publish
| Setting | Value |
|---------|-------|
| Registry sync | Auto-sync - call `/api/sync/keyword` after publish |
| Verify listing | Confirm tool appears on tpmjs.com before reporting success |
| Collections | Standalone only - no auto-add |
| Duplicates | Propose enhancement to existing tool if duplicate detected |
## Status Tracking
### Labels (managed by Claude)
| Label | Meaning |
|-------|---------|
| `tool-request` | Initial trigger (applied by maintainer) |
| `claude-working` | Claude is actively processing |
| `validation-failed` | Validation failed, iterating |
| `published` | Successfully published to npm |
| `escalated` | Requires human intervention |
### Issue Lifecycle
1. Maintainer applies `tool-request` label
2. Workflow triggers, adds `claude-working` label
3. On validation failure: add `validation-failed`, retry (max 3x)
4. On success: remove other labels, add `published`
5. Keep issue open 24h for feedback
6. Auto-close after 24h
## Success Report
Full changelog posted to issue:
```markdown
## Tool Published Successfully
**Package:** `@tpmjs/tools-<package>@<version>`
**NPM:** https://www.npmjs.com/package/@tpmjs/tools-<package>
**Registry:** https://tpmjs.com/tool/@tpmjs/tools-<package>/<tool-name>
### Changes
- Added `<tool-name>` tool
- Updated blocks.yml
- Bumped version from X.Y.Z to X.Y+1.0
### Validation Results
<validation output>
### Test Execution
<screenshot of tool running with sample inputs>
### Files Changed
<file diff summary>
---
This issue will auto-close in 24 hours. Reply if you have feedback.
```
## Error Handling
| Scenario | Action |
|----------|--------|
| Validation fails 3x | Add `escalated` label, assign to maintainer with diagnostic info |
| NPM publish fails | Comment explaining failure, wait for human fix |
| Duplicate detected | Comment explaining existing tool, propose enhancement instead |
| blocks.yml conflict | Rebase and retry automatically |
| Issue edited mid-work | Detect changes, incorporate into implementation |
## Context & Memory
| Setting | Value |
|---------|-------|
| State tracking | Full conversation - Claude remembers entire issue thread |
| Previous attempts | Tracked within issue context |
| Cross-issue | No memory between different issues |
## Workflow File Structure
```yaml
name: Tool Request Pipeline
on:
issues:
types: [labeled]
jobs:
trigger-claude:
if: github.event.label.name == 'tool-request'
runs-on: ubuntu-latest
steps:
- name: Add working label
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['claude-working']
});
- name: Comment to trigger Claude
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '@claude Please implement this tool request following the tool-request pipeline specification at `.claude/pipelines/tool-request.md`.'
});
```
## Claude Instructions
When triggered, Claude should:
1. **Read this spec** at `.claude/pipelines/tool-request.md`
2. **Analyze the issue** - extract tool name, description, intended functionality
3. **Check for duplicates** - search existing tools for similar functionality
4. **Select package** - analyze existing packages, decide new vs existing
5. **Design the tool** - define inputs, outputs, implementation approach
6. **Implement** - create/update blocks.yml, create package files
7. **Validate** - run `pnpm blocks run <tool>` in packages/tools/official
8. **Test** - execute with sample inputs, capture output
9. **Create PR** - feature branch, include all changes
10. **Publish** - after CI passes, `npm publish`
11. **Sync** - trigger registry sync
12. **Report** - full changelog to issue
13. **Cleanup** - update labels, schedule auto-close
## Security Considerations
- Only maintainers can apply trigger label
- NPM_TOKEN is existing secret, not exposed in logs
- Tool code is reviewed via PR (even if auto-merged)
- No restrictions on tool types - trust validation + maintainer judgment
- Full audit trail in issue comments
## Dry Run
No dry run mode. Validation is sufficient safeguard. If testing needed, create a test issue and manually delete artifacts after.
---
*Specification created: 2026-01-19*
*Interview conducted with: @ajax*

View file

@ -1,16 +0,0 @@
---
description: Cancel the active Ralph loop
command: rm -f .claude/ralph-loop.local.md && echo "Ralph loop cancelled"
---
# Cancel Ralph Loop
Immediately cancel any active Ralph loop and allow normal session exit.
## Usage
```
/cancel-ralph
```
This removes the state file that drives the loop, allowing the session to exit normally.

View file

@ -1,43 +0,0 @@
---
description: Start Ralph Wiggum loop in current session
command: "${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" $ARGUMENTS
---
# Ralph Loop
Start an iterative development loop that continues until the task is complete.
## Usage
```
/ralph-loop "Your task description" [--max-iterations N] [--validation-script PATH] [--completion-promise TEXT]
```
## How It Works
1. You provide a task and optional validation criteria
2. Claude works on the task
3. When Claude tries to exit, the stop hook intercepts
4. If validation fails OR completion promise not met, the loop continues
5. Claude sees previous work and continues iterating
6. Loop ends when validation passes or max iterations reached
## Important Rules
- If a completion promise is set, you may ONLY output it when the statement is completely and unequivocally TRUE
- Do NOT use false completion promises as an exit strategy
- The loop persists until genuine completion is achieved
- Use validation scripts for programmatic verification
## Examples
```bash
# With validation script only
/ralph-loop "Build the SDK package" --validation-script ./scripts/validate-sdk.sh
# With completion promise
/ralph-loop "Fix all type errors" --completion-promise "ALL_TYPES_PASS"
# With both
/ralph-loop "Complete feature X" --max-iterations 15 --validation-script ./validate.sh --completion-promise "FEATURE_COMPLETE"
```

View file

@ -1,15 +0,0 @@
{
"description": "Ralph Wiggum plugin stop hook for self-referential loops",
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"
}
]
}
]
}
}

View file

@ -1,111 +0,0 @@
#!/bin/bash
# Ralph Wiggum Stop Hook - Self-referential loop for iterative development
# This hook intercepts the Stop event and decides whether to continue the loop
set -euo pipefail
STATE_FILE=".claude/ralph-loop.local.md"
TRANSCRIPT_FILE="${CLAUDE_TRANSCRIPT:-}"
# Check if ralph loop is active
if [[ ! -f "$STATE_FILE" ]]; then
# No active loop, allow normal exit
exit 0
fi
# Parse the state file frontmatter
parse_frontmatter() {
local key="$1"
sed -n '/^---$/,/^---$/p' "$STATE_FILE" | grep "^${key}:" | sed "s/^${key}: *//" | tr -d '"'
}
iteration=$(parse_frontmatter "iteration")
max_iterations=$(parse_frontmatter "max_iterations")
completion_promise=$(parse_frontmatter "completion_promise")
prompt=$(parse_frontmatter "prompt")
validation_script=$(parse_frontmatter "validation_script")
# Validate numeric fields
if ! [[ "$iteration" =~ ^[0-9]+$ ]]; then
echo "Error: Invalid iteration count in state file" >&2
rm -f "$STATE_FILE"
exit 0
fi
if ! [[ "$max_iterations" =~ ^[0-9]+$ ]]; then
echo "Error: Invalid max_iterations in state file" >&2
rm -f "$STATE_FILE"
exit 0
fi
# Check if max iterations reached
if [[ "$max_iterations" -gt 0 ]] && [[ "$iteration" -ge "$max_iterations" ]]; then
echo "Ralph loop reached max iterations ($max_iterations). Exiting." >&2
rm -f "$STATE_FILE"
exit 0
fi
# Run validation script if provided
validation_passed=false
if [[ -n "$validation_script" ]] && [[ -f "$validation_script" ]]; then
echo "Running validation script: $validation_script" >&2
if bash "$validation_script" 2>&1; then
validation_passed=true
echo "Validation PASSED!" >&2
else
echo "Validation FAILED. Continuing loop..." >&2
fi
fi
# Check for completion promise in transcript
if [[ -n "$completion_promise" ]] && [[ -n "$TRANSCRIPT_FILE" ]] && [[ -f "$TRANSCRIPT_FILE" ]]; then
# Get the last assistant message
last_message=$(tail -100 "$TRANSCRIPT_FILE" | grep -o '<promise>[^<]*</promise>' | tail -1 | sed 's/<promise>\(.*\)<\/promise>/\1/' || true)
if [[ "$last_message" == "$completion_promise" ]]; then
# Also check if validation passed (if validation script exists)
if [[ -z "$validation_script" ]] || [[ "$validation_passed" == "true" ]]; then
echo "Completion promise matched and validation passed. Ralph loop complete!" >&2
rm -f "$STATE_FILE"
exit 0
else
echo "Completion promise matched but validation failed. Continuing..." >&2
fi
fi
fi
# If validation passed without explicit promise, we can exit
if [[ "$validation_passed" == "true" ]] && [[ -z "$completion_promise" ]]; then
echo "Validation passed. Ralph loop complete!" >&2
rm -f "$STATE_FILE"
exit 0
fi
# Increment iteration
new_iteration=$((iteration + 1))
# Update state file
sed -i.bak "s/^iteration: .*/iteration: $new_iteration/" "$STATE_FILE"
rm -f "${STATE_FILE}.bak"
# Build the continuation message
cat << EOF
{
"decision": "block",
"reason": "Ralph loop iteration $new_iteration of $max_iterations",
"message": "
---
RALPH LOOP - Iteration $new_iteration / $max_iterations
---
Continue working on the task. Your previous iteration's work is preserved in the codebase.
TASK: $prompt
$(if [[ -n "$validation_script" ]]; then echo "VALIDATION: Run the validation to check progress. Script: $validation_script"; fi)
$(if [[ -n "$completion_promise" ]]; then echo "COMPLETION: Output <promise>$completion_promise</promise> ONLY when the task is completely done AND validation passes."; fi)
Review what you've done so far and continue from where you left off.
"
}
EOF

View file

@ -1,120 +0,0 @@
#!/bin/bash
# Setup Ralph Loop - Initialize the iterative development loop
# Usage: setup-ralph-loop.sh "PROMPT" [--max-iterations N] [--completion-promise TEXT] [--validation-script PATH]
set -euo pipefail
STATE_FILE=".claude/ralph-loop.local.md"
# Default values
MAX_ITERATIONS=20
COMPLETION_PROMISE=""
VALIDATION_SCRIPT=""
PROMPT=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--max-iterations)
MAX_ITERATIONS="$2"
shift 2
;;
--completion-promise)
COMPLETION_PROMISE="$2"
shift 2
;;
--validation-script)
VALIDATION_SCRIPT="$2"
shift 2
;;
--help|-h)
cat << EOF
Ralph Loop Setup
Usage: setup-ralph-loop.sh "PROMPT" [OPTIONS]
Options:
--max-iterations N Maximum iterations before stopping (default: 20, 0 = unlimited)
--completion-promise TXT Phrase to output when complete (use <promise>TXT</promise>)
--validation-script PATH Script to run for validation (exit 0 = pass)
--help, -h Show this help
Example:
setup-ralph-loop.sh "Build the SDK package" --max-iterations 10 --validation-script ./validate.sh
EOF
exit 0
;;
*)
if [[ -z "$PROMPT" ]]; then
PROMPT="$1"
else
PROMPT="$PROMPT $1"
fi
shift
;;
esac
done
# Validate prompt
if [[ -z "$PROMPT" ]]; then
echo "Error: PROMPT is required" >&2
exit 1
fi
# Validate max iterations
if ! [[ "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then
echo "Error: --max-iterations must be a number" >&2
exit 1
fi
# Validate validation script exists if provided
if [[ -n "$VALIDATION_SCRIPT" ]] && [[ ! -f "$VALIDATION_SCRIPT" ]]; then
echo "Error: Validation script not found: $VALIDATION_SCRIPT" >&2
exit 1
fi
# Create state directory
mkdir -p "$(dirname "$STATE_FILE")"
# Create state file
cat << EOF > "$STATE_FILE"
---
iteration: 1
max_iterations: $MAX_ITERATIONS
completion_promise: "$COMPLETION_PROMISE"
validation_script: "$VALIDATION_SCRIPT"
prompt: "$PROMPT"
started_at: "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
---
# Ralph Loop State
This file tracks the state of an active Ralph loop. DO NOT DELETE while loop is running.
## Configuration
- **Task**: $PROMPT
- **Max Iterations**: $MAX_ITERATIONS
- **Validation Script**: ${VALIDATION_SCRIPT:-"None"}
- **Completion Promise**: ${COMPLETION_PROMISE:-"None (validation only)"}
## Progress Log
Iteration logs will be appended below as the loop progresses.
---
EOF
echo "Ralph loop initialized!"
echo " Task: $PROMPT"
echo " Max iterations: $MAX_ITERATIONS"
echo " Validation: ${VALIDATION_SCRIPT:-"None"}"
echo " Completion promise: ${COMPLETION_PROMISE:-"None"}"
echo ""
echo "The loop will continue until:"
if [[ -n "$VALIDATION_SCRIPT" ]]; then
echo " - Validation script passes ($VALIDATION_SCRIPT returns exit code 0)"
fi
if [[ -n "$COMPLETION_PROMISE" ]]; then
echo " - You output: <promise>$COMPLETION_PROMISE</promise>"
fi
echo " - OR max iterations ($MAX_ITERATIONS) is reached"

View file

@ -1 +0,0 @@
../../.agents/skills/agentmail

View file

@ -1,357 +0,0 @@
# TPMJS Tool Development with Blocks CLI
Use this skill when developing new tools for the TPMJS registry. This covers the full workflow from defining a tool in blocks.yml through implementation, validation, and publishing.
## Quick Start
```bash
# Navigate to official tools directory
cd packages/tools/official
# Run validation on a specific tool
pnpm blocks run <block-name>
# Run validation on all tools
pnpm blocks run --all
# Force full validation (ignore cache)
pnpm blocks run <block-name> --force
```
## Development Workflow
### 1. Define the Tool Block in blocks.yml
Add your tool definition to `packages/tools/official/blocks.yml` in the `blocks:` section:
```yaml
blocks:
# Category.toolName format
sandbox.myTool:
type: utility
description: "Clear, LLM-friendly description of what the tool does"
path: "my-tool" # Directory name under packages/tools/official/
domain_rules:
- id: rule_name
description: "What this implementation must do"
inputs:
- name: inputName
type: string
description: "Description for LLMs"
- name: optionalInput
type: number
optional: true
description: "Optional parameter"
outputs:
- name: result
type: MyResultType
description: "What the tool returns"
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
```
**Key Fields:**
- `type`: Usually `utility` for single-shot tools
- `path`: Directory name (kebab-case)
- `domain_rules`: Implementation requirements the validator checks
- `inputs/outputs`: Schema for validation
- `measures`: Quality constraints from the domain section
### 2. Create the Tool Package
Create the directory structure:
```
packages/tools/official/my-tool/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── block.ts # Required by validator
├── index.ts # Re-export from src
└── src/
└── index.ts # Main implementation
```
**package.json:**
```json
{
"name": "@tpmjs/tools-my-tool",
"version": "0.1.0",
"description": "Short description for npm",
"type": "module",
"keywords": ["tpmjs", "category-name", "ai"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
},
"dependencies": {
"ai": "6.0.23"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/tpmjs/tpmjs.git",
"directory": "packages/tools/official/my-tool"
},
"homepage": "https://tpmjs.com",
"license": "MIT",
"tpmjs": {
"category": "sandbox",
"frameworks": ["vercel-ai"],
"tools": [
{
"name": "myTool",
"description": "Clear description (20+ chars) of what this tool does."
}
]
}
}
```
**Valid categories for tpmjs.category:**
- `research`, `web`, `data`, `documentation`, `engineering`
- `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`
- `html`, `compliance`
**tsconfig.json:**
```json
{
"extends": "@tpmjs/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
**tsup.config.ts:**
```typescript
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
target: 'es2022',
});
```
**block.ts (Required by validator):**
```typescript
import { myTool } from './src/index.js';
export const block = {
name: 'my-tool',
description: 'Short description',
tools: { myTool },
};
export default block;
```
**index.ts (Root re-export):**
```typescript
export * from './src/index.js';
export { default } from './src/index.js';
```
### 3. Implement the Tool
**src/index.ts:**
```typescript
import { jsonSchema, tool } from 'ai';
// Define input/output types
interface MyToolInput {
param1: string;
param2?: number;
}
interface MyToolResult {
data: string;
metadata: {
processedAt: string;
};
}
// Export the tool using AI SDK v6 pattern
export const myTool = tool({
description: 'Clear description for LLMs explaining what this tool does and when to use it.',
parameters: jsonSchema<MyToolInput>({
type: 'object',
properties: {
param1: {
type: 'string',
description: 'Description of param1',
},
param2: {
type: 'number',
description: 'Optional description of param2',
},
},
required: ['param1'],
}),
async execute(input): Promise<MyToolResult> {
// REAL implementation - no stubs, no TODOs
const result = await doSomething(input.param1);
return {
data: result,
metadata: {
processedAt: new Date().toISOString(),
},
};
},
});
// Default export for compatibility
export default myTool;
```
### 4. Run Validation
```bash
cd packages/tools/official
# Validate your tool
pnpm blocks run my-tool
# The validator runs 3 stages:
# 1. schema - Validates inputs/outputs match blocks.yml
# 2. shape - Verifies exports and structure
# 3. domain - Checks domain rules are satisfied
```
**Common validation errors:**
- `Required file "index.ts" not found` - Need index.ts at package root
- `Required file "block.ts" not found` - Need block.ts at package root
- `Tool "myTool" not found in exports` - Export name must match blocks.yml
- `invalid tpmjs field` - Category must be valid, tools array required
### 5. Build and Publish
```bash
# Build the package
pnpm build
# Publish to npm
npm publish --access public
# Trigger sync to tpmjs.com
source apps/web/.env.local
curl -X POST https://tpmjs.com/api/sync/keyword \
-H "Authorization: Bearer $CRON_SECRET"
```
## Multi-Tool Packages
For packages with multiple tools (like unsandbox):
**blocks.yml:**
```yaml
blocks:
sandbox.executeCodeAsync:
type: utility
path: "unsandbox" # Same path for all tools in package
# ...
sandbox.getJob:
type: utility
path: "unsandbox" # Same path
# ...
```
**block.ts:**
```typescript
import { executeCodeAsync, getJob, listJobs } from './src/index.js';
export const block = {
name: 'unsandbox',
tools: { executeCodeAsync, getJob, listJobs },
};
export default block;
```
**package.json tpmjs field:**
```json
{
"tpmjs": {
"category": "sandbox",
"frameworks": ["vercel-ai"],
"tools": [
{ "name": "executeCodeAsync", "description": "..." },
{ "name": "getJob", "description": "..." },
{ "name": "listJobs", "description": "..." }
]
}
}
```
## Philosophy (from blocks.yml)
- Every tool MUST be a working, production-ready implementation - no stubs, no TODOs
- Tools use AI SDK v6 `tool()` + `jsonSchema()` pattern exclusively
- Each tool does ONE thing exceptionally well (single-shot, one call in, one result out)
- Tools return structured, typed outputs that agents can reliably parse
- Error handling is explicit - throw meaningful errors, never silently fail
- Dependencies are minimal and production-stable
## Domain Entities
When defining outputs, reference existing entities from blocks.yml:
```yaml
# Example entities available:
url: [href, domain, protocol, path, query, fragment]
webpage: [url, title, html, text, metadata]
text_content: [raw, sentences, paragraphs, wordCount]
claim: [statement, confidence, needsCitation, category]
timeline: [events, dateRange, gaps, eventCount]
```
Or define new entities in the `domain.entities` section if needed.
## Quality Measures
Reference these in your tool's `measures` array:
- `working_implementation` - No stubs, TODOs, or placeholders
- `valid_output_structure` - Returns correct typed object
- `proper_error_handling` - Throws descriptive errors
- `ai_sdk_compliance` - Uses tool() and jsonSchema()
- `npm_publishable` - Valid package.json with tpmjs field
- `readme_documentation` - Has README with examples
## Debugging Tips
```bash
# Force rebuild without cache
pnpm blocks run my-tool --force --no-cache
# See JSON output for debugging
pnpm blocks run my-tool --json
# Check if validator finds your package
ls packages/tools/official/my-tool/
# Must have: index.ts, block.ts at root level
```

View file

@ -1,43 +0,0 @@
---
name: remotion-best-practices
description: Best practices for Remotion - Video creation in React
metadata:
tags: remotion, video, react, animation, composition
---
## When to use
Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.
## How to use
Read individual rule files for detailed explanations and code examples:
- [rules/3d.md](rules/3d.md) - 3D content in Remotion using Three.js and React Three Fiber
- [rules/animations.md](rules/animations.md) - Fundamental animation skills for Remotion
- [rules/assets.md](rules/assets.md) - Importing images, videos, audio, and fonts into Remotion
- [rules/audio.md](rules/audio.md) - Using audio and sound in Remotion - importing, trimming, volume, speed, pitch
- [rules/calculate-metadata.md](rules/calculate-metadata.md) - Dynamically set composition duration, dimensions, and props
- [rules/can-decode.md](rules/can-decode.md) - Check if a video can be decoded by the browser using Mediabunny
- [rules/charts.md](rules/charts.md) - Chart and data visualization patterns for Remotion
- [rules/compositions.md](rules/compositions.md) - Defining compositions, stills, folders, default props and dynamic metadata
- [rules/display-captions.md](rules/display-captions.md) - Displaying captions in Remotion with TikTok-style pages and word highlighting
- [rules/extract-frames.md](rules/extract-frames.md) - Extract frames from videos at specific timestamps using Mediabunny
- [rules/fonts.md](rules/fonts.md) - Loading Google Fonts and local fonts in Remotion
- [rules/get-audio-duration.md](rules/get-audio-duration.md) - Getting the duration of an audio file in seconds with Mediabunny
- [rules/get-video-dimensions.md](rules/get-video-dimensions.md) - Getting the width and height of a video file with Mediabunny
- [rules/get-video-duration.md](rules/get-video-duration.md) - Getting the duration of a video file in seconds with Mediabunny
- [rules/gifs.md](rules/gifs.md) - Displaying GIFs synchronized with Remotion's timeline
- [rules/images.md](rules/images.md) - Embedding images in Remotion using the Img component
- [rules/import-srt-captions.md](rules/import-srt-captions.md) - Importing .srt subtitle files into Remotion using @remotion/captions
- [rules/lottie.md](rules/lottie.md) - Embedding Lottie animations in Remotion
- [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) - Measuring DOM element dimensions in Remotion
- [rules/measuring-text.md](rules/measuring-text.md) - Measuring text dimensions, fitting text to containers, and checking overflow
- [rules/sequencing.md](rules/sequencing.md) - Sequencing patterns for Remotion - delay, trim, limit duration of items
- [rules/tailwind.md](rules/tailwind.md) - Using TailwindCSS in Remotion
- [rules/text-animations.md](rules/text-animations.md) - Typography and text animation patterns for Remotion
- [rules/timing.md](rules/timing.md) - Interpolation curves in Remotion - linear, easing, spring animations
- [rules/transcribe-captions.md](rules/transcribe-captions.md) - Transcribing audio to generate captions in Remotion
- [rules/transitions.md](rules/transitions.md) - Scene transition patterns for Remotion
- [rules/trimming.md](rules/trimming.md) - Trimming patterns for Remotion - cut the beginning or end of animations
- [rules/videos.md](rules/videos.md) - Embedding videos in Remotion - trimming, volume, speed, looping, pitch

View file

@ -1,86 +0,0 @@
---
name: 3d
description: 3D content in Remotion using Three.js and React Three Fiber.
metadata:
tags: 3d, three, threejs
---
# Using Three.js and React Three Fiber in Remotion
Follow React Three Fiber and Three.js best practices.
Only the following Remotion-specific rules need to be followed:
## Prerequisites
First, the `@remotion/three` package needs to be installed.
If it is not, use the following command:
```bash
npx remotion add @remotion/three # If project uses npm
bunx remotion add @remotion/three # If project uses bun
yarn remotion add @remotion/three # If project uses yarn
pnpm exec remotion add @remotion/three # If project uses pnpm
```
## Using ThreeCanvas
You MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.
`<ThreeCanvas>` MUST have a `width` and `height` prop.
```tsx
import { ThreeCanvas } from "@remotion/three";
import { useVideoConfig } from "remotion";
const { width, height } = useVideoConfig();
<ThreeCanvas width={width} height={height}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={0.8} />
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="red" />
</mesh>
</ThreeCanvas>
```
## No animations not driven by `useCurrentFrame()`
Shaders, models etc MUST NOT animate by themselves.
No animations are allowed unless they are driven by `useCurrentFrame()`.
Otherwise, it will cause flickering during rendering.
Using `useFrame()` from `@react-three/fiber` is forbidden.
## Animate using `useCurrentFrame()`
Use `useCurrentFrame()` to perform animations.
```tsx
const frame = useCurrentFrame();
const rotationY = frame * 0.02;
<mesh rotation={[0, rotationY, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
```
## Using `<Sequence>` inside `<ThreeCanvas>`
The `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.
```tsx
import { Sequence } from "remotion";
import { ThreeCanvas } from "@remotion/three";
const { width, height } = useVideoConfig();
<ThreeCanvas width={width} height={height}>
<Sequence layout="none">
<mesh>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
</Sequence>
</ThreeCanvas>
```

View file

@ -1,29 +0,0 @@
---
name: animations
description: Fundamental animation skills for Remotion
metadata:
tags: animations, transitions, frames, useCurrentFrame
---
All animations MUST be driven by the `useCurrentFrame()` hook.
Write animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.
```tsx
import { useCurrentFrame } from "remotion";
export const FadeIn = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<div style={{ opacity }}>Hello World!</div>
);
};
```
CSS transitions or animations are FORBIDDEN - they will not render correctly.
Tailwind animation class names are FORBIDDEN - they will not render correctly.

View file

@ -1,78 +0,0 @@
---
name: assets
description: Importing images, videos, audio, and fonts into Remotion
metadata:
tags: assets, staticFile, images, fonts, public
---
# Importing assets in Remotion
## The public folder
Place assets in the `public/` folder at your project root.
## Using staticFile()
You MUST use `staticFile()` to reference files from the `public/` folder:
```tsx
import {Img, staticFile} from 'remotion';
export const MyComposition = () => {
return <Img src={staticFile('logo.png')} />;
};
```
The function returns an encoded URL that works correctly when deploying to subdirectories.
## Using with components
**Images:**
```tsx
import {Img, staticFile} from 'remotion';
<Img src={staticFile('photo.png')} />;
```
**Videos:**
```tsx
import {Video} from '@remotion/media';
import {staticFile} from 'remotion';
<Video src={staticFile('clip.mp4')} />;
```
**Audio:**
```tsx
import {Audio} from '@remotion/media';
import {staticFile} from 'remotion';
<Audio src={staticFile('music.mp3')} />;
```
**Fonts:**
```tsx
import {staticFile} from 'remotion';
const fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);
await fontFamily.load();
document.fonts.add(fontFamily);
```
## Remote URLs
Remote URLs can be used directly without `staticFile()`:
```tsx
<Img src="https://example.com/image.png" />
<Video src="https://remotion.media/video.mp4" />
```
## Important notes
- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering
- Special characters in filenames (`#`, `?`, `&`) are automatically encoded

View file

@ -1,165 +0,0 @@
import { loadFont } from '@remotion/google-fonts/Inter';
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from 'remotion';
const { fontFamily } = loadFont();
const COLOR_BAR = '#D4AF37';
const COLOR_TEXT = '#ffffff';
const COLOR_MUTED = '#888888';
const COLOR_BG = '#0a0a0a';
const COLOR_AXIS = '#333333';
// Ideal composition size: 1280x720
const Title: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div style={{ textAlign: 'center', marginBottom: 40 }}>
<div style={{ color: COLOR_TEXT, fontSize: 48, fontWeight: 600 }}>{children}</div>
</div>
);
const YAxis: React.FC<{ steps: number[]; height: number }> = ({ steps, height }) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
height,
paddingRight: 16,
}}
>
{steps
.slice()
.reverse()
.map((step) => (
<div
key={step}
style={{
color: COLOR_MUTED,
fontSize: 20,
textAlign: 'right',
}}
>
{step.toLocaleString()}
</div>
))}
</div>
);
const Bar: React.FC<{
height: number;
progress: number;
}> = ({ height, progress }) => (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
}}
>
<div
style={{
width: '100%',
height,
backgroundColor: COLOR_BAR,
borderRadius: '8px 8px 0 0',
opacity: progress,
}}
/>
</div>
);
const XAxis: React.FC<{
children: React.ReactNode;
labels: string[];
height: number;
}> = ({ children, labels, height }) => (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div
style={{
display: 'flex',
alignItems: 'flex-end',
gap: 16,
height,
borderLeft: `2px solid ${COLOR_AXIS}`,
borderBottom: `2px solid ${COLOR_AXIS}`,
paddingLeft: 16,
}}
>
{children}
</div>
<div
style={{
display: 'flex',
gap: 16,
paddingLeft: 16,
marginTop: 12,
}}
>
{labels.map((label) => (
<div
key={label}
style={{
flex: 1,
textAlign: 'center',
color: COLOR_MUTED,
fontSize: 20,
}}
>
{label}
</div>
))}
</div>
</div>
);
export const MyAnimation = () => {
const frame = useCurrentFrame();
const { fps, height } = useVideoConfig();
const data = [
{ month: 'Jan', price: 2039 },
{ month: 'Mar', price: 2160 },
{ month: 'May', price: 2327 },
{ month: 'Jul', price: 2426 },
{ month: 'Sep', price: 2634 },
{ month: 'Nov', price: 2672 },
];
const minPrice = 2000;
const maxPrice = 2800;
const priceRange = maxPrice - minPrice;
const chartHeight = height - 280;
const yAxisSteps = [2000, 2400, 2800];
return (
<AbsoluteFill
style={{
backgroundColor: COLOR_BG,
padding: 60,
display: 'flex',
flexDirection: 'column',
fontFamily,
}}
>
<Title>Gold Price 2024</Title>
<div style={{ display: 'flex', flex: 1 }}>
<YAxis steps={yAxisSteps} height={chartHeight} />
<XAxis height={chartHeight} labels={data.map((d) => d.month)}>
{data.map((item, i) => {
const progress = spring({
frame: frame - i * 5 - 10,
fps,
config: { damping: 18, stiffness: 80 },
});
const barHeight = ((item.price - minPrice) / priceRange) * chartHeight * progress;
return <Bar key={item.month} height={barHeight} progress={progress} />;
})}
</XAxis>
</div>
</AbsoluteFill>
);
};

View file

@ -1,89 +0,0 @@
import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
const COLOR_BG = '#ffffff';
const COLOR_TEXT = '#000000';
const FULL_TEXT = 'From prompt to motion graphics. This is Remotion.';
const PAUSE_AFTER = 'From prompt to motion graphics.';
const FONT_SIZE = 72;
const FONT_WEIGHT = 700;
const CHAR_FRAMES = 2;
const CURSOR_BLINK_FRAMES = 16;
const PAUSE_SECONDS = 1;
// Ideal composition size: 1280x720
const getTypedText = ({
frame,
fullText,
pauseAfter,
charFrames,
pauseFrames,
}: {
frame: number;
fullText: string;
pauseAfter: string;
charFrames: number;
pauseFrames: number;
}): string => {
const pauseIndex = fullText.indexOf(pauseAfter);
const preLen = pauseIndex >= 0 ? pauseIndex + pauseAfter.length : fullText.length;
let typedChars = 0;
if (frame < preLen * charFrames) {
typedChars = Math.floor(frame / charFrames);
} else if (frame < preLen * charFrames + pauseFrames) {
typedChars = preLen;
} else {
const postPhase = frame - preLen * charFrames - pauseFrames;
typedChars = Math.min(fullText.length, preLen + Math.floor(postPhase / charFrames));
}
return fullText.slice(0, typedChars);
};
const Cursor: React.FC<{
frame: number;
blinkFrames: number;
symbol?: string;
}> = ({ frame, blinkFrames, symbol = '\u258C' }) => {
const opacity = interpolate(frame % blinkFrames, [0, blinkFrames / 2, blinkFrames], [1, 0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return <span style={{ opacity }}>{symbol}</span>;
};
export const MyAnimation = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const pauseFrames = Math.round(fps * PAUSE_SECONDS);
const typedText = getTypedText({
frame,
fullText: FULL_TEXT,
pauseAfter: PAUSE_AFTER,
charFrames: CHAR_FRAMES,
pauseFrames,
});
return (
<AbsoluteFill
style={{
backgroundColor: COLOR_BG,
}}
>
<div
style={{
color: COLOR_TEXT,
fontSize: FONT_SIZE,
fontWeight: FONT_WEIGHT,
fontFamily: 'sans-serif',
}}
>
<span>{typedText}</span>
<Cursor frame={frame} blinkFrames={CURSOR_BLINK_FRAMES} />
</div>
</AbsoluteFill>
);
};

View file

@ -1,101 +0,0 @@
import { loadFont } from '@remotion/google-fonts/Inter';
import type React from 'react';
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from 'remotion';
/*
* Highlight a word in a sentence with a spring-animated wipe effect.
*/
// Ideal composition size: 1280x720
const COLOR_BG = '#ffffff';
const COLOR_TEXT = '#000000';
const COLOR_HIGHLIGHT = '#A7C7E7';
const FULL_TEXT = 'This is Remotion.';
const HIGHLIGHT_WORD = 'Remotion';
const FONT_SIZE = 72;
const FONT_WEIGHT = 700;
const HIGHLIGHT_START_FRAME = 30;
const HIGHLIGHT_WIPE_DURATION = 18;
const { fontFamily } = loadFont();
const Highlight: React.FC<{
word: string;
color: string;
delay: number;
durationInFrames: number;
}> = ({ word, color, delay, durationInFrames }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const highlightProgress = spring({
fps,
frame,
config: { damping: 200 },
delay,
durationInFrames,
});
const scaleX = Math.max(0, Math.min(1, highlightProgress));
return (
<span style={{ position: 'relative', display: 'inline-block' }}>
<span
style={{
position: 'absolute',
left: 0,
right: 0,
top: '50%',
height: '1.05em',
transform: `translateY(-50%) scaleX(${scaleX})`,
transformOrigin: 'left center',
backgroundColor: color,
borderRadius: '0.18em',
zIndex: 0,
}}
/>
<span style={{ position: 'relative', zIndex: 1 }}>{word}</span>
</span>
);
};
export const MyAnimation = () => {
const highlightIndex = FULL_TEXT.indexOf(HIGHLIGHT_WORD);
const hasHighlight = highlightIndex >= 0;
const preText = hasHighlight ? FULL_TEXT.slice(0, highlightIndex) : FULL_TEXT;
const postText = hasHighlight ? FULL_TEXT.slice(highlightIndex + HIGHLIGHT_WORD.length) : '';
return (
<AbsoluteFill
style={{
backgroundColor: COLOR_BG,
alignItems: 'center',
justifyContent: 'center',
fontFamily,
}}
>
<div
style={{
color: COLOR_TEXT,
fontSize: FONT_SIZE,
fontWeight: FONT_WEIGHT,
}}
>
{hasHighlight ? (
<>
<span>{preText}</span>
<Highlight
word={HIGHLIGHT_WORD}
color={COLOR_HIGHLIGHT}
delay={HIGHLIGHT_START_FRAME}
durationInFrames={HIGHLIGHT_WIPE_DURATION}
/>
<span>{postText}</span>
</>
) : (
<span>{FULL_TEXT}</span>
)}
</div>
</AbsoluteFill>
);
};

View file

@ -1,172 +0,0 @@
---
name: audio
description: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch
metadata:
tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx
---
# Using audio in Remotion
## Prerequisites
First, the @remotion/media package needs to be installed.
If it is not installed, use the following command:
```bash
npx remotion add @remotion/media # If project uses npm
bunx remotion add @remotion/media # If project uses bun
yarn remotion add @remotion/media # If project uses yarn
pnpm exec remotion add @remotion/media # If project uses pnpm
```
## Importing Audio
Use `<Audio>` from `@remotion/media` to add audio to your composition.
```tsx
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Audio src={staticFile("audio.mp3")} />;
};
```
Remote URLs are also supported:
```tsx
<Audio src="https://remotion.media/audio.mp3" />
```
By default, audio plays from the start, at full volume and full length.
Multiple audio tracks can be layered by adding multiple `<Audio>` components.
## Trimming
Use `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.
```tsx
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
trimBefore={2 * fps} // Skip the first 2 seconds
trimAfter={10 * fps} // End at the 10 second mark
/>
);
```
The audio still starts playing at the beginning of the composition - only the specified portion is played.
## Delaying
Wrap the audio in a `<Sequence>` to delay when it starts:
```tsx
import { Sequence, staticFile } from "remotion";
import { Audio } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Audio src={staticFile("audio.mp3")} />
</Sequence>
);
```
The audio will start playing after 1 second.
## Volume
Set a static volume (0 to 1):
```tsx
<Audio src={staticFile("audio.mp3")} volume={0.5} />
```
Or use a callback for dynamic volume based on the current frame:
```tsx
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);
```
The value of `f` starts at 0 when the audio begins to play, not the composition frame.
## Muting
Use `muted` to silence the audio. It can be set dynamically:
```tsx
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s
/>
);
```
## Speed
Use `playbackRate` to change the playback speed:
```tsx
<Audio src={staticFile("audio.mp3")} playbackRate={2} /> {/* 2x speed */}
<Audio src={staticFile("audio.mp3")} playbackRate={0.5} /> {/* Half speed */}
```
Reverse playback is not supported.
## Looping
Use `loop` to loop the audio indefinitely:
```tsx
<Audio src={staticFile("audio.mp3")} loop />
```
Use `loopVolumeCurveBehavior` to control how the frame count behaves when looping:
- `"repeat"`: Frame count resets to 0 each loop (default)
- `"extend"`: Frame count continues incrementing
```tsx
<Audio
src={staticFile("audio.mp3")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
/>
```
## Pitch
Use `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:
```tsx
<Audio
src={staticFile("audio.mp3")}
toneFrequency={1.5} // Higher pitch
/>
<Audio
src={staticFile("audio.mp3")}
toneFrequency={0.8} // Lower pitch
/>
```
Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.

View file

@ -1,104 +0,0 @@
---
name: calculate-metadata
description: Dynamically set composition duration, dimensions, and props
metadata:
tags: calculateMetadata, duration, dimensions, props, dynamic
---
# Using calculateMetadata
Use `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.
```tsx
<Composition id="MyComp" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />
```
## Setting duration based on a video
Use the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:
```tsx
import {CalculateMetadataFunction} from 'remotion';
import {getMediaMetadata} from '../get-media-metadata';
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
const {durationInSeconds} = await getMediaMetadata(props.videoSrc);
return {
durationInFrames: Math.ceil(durationInSeconds * 30),
};
};
```
## Matching dimensions of a video
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);
return {
durationInFrames: Math.ceil(durationInSeconds * 30),
width: dimensions?.width ?? 1920,
height: dimensions?.height ?? 1080,
};
};
```
## Setting duration based on multiple videos
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));
const allMetadata = await Promise.all(metadataPromises);
const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);
return {
durationInFrames: Math.ceil(totalDuration * 30),
};
};
```
## Setting a default outName
Set the default output filename based on props:
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
return {
defaultOutName: `video-${props.id}.mp4`,
};
};
```
## Transforming props
Fetch data or transform props before rendering:
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {
const response = await fetch(props.dataUrl, {signal: abortSignal});
const data = await response.json();
return {
props: {
...props,
fetchedData: data,
},
};
};
```
The `abortSignal` cancels stale requests when props change in the Studio.
## Return value
All fields are optional. Returned values override the `<Composition>` props:
- `durationInFrames`: Number of frames
- `width`: Composition width in pixels
- `height`: Composition height in pixels
- `fps`: Frames per second
- `props`: Transformed props passed to the component
- `defaultOutName`: Default output filename
- `defaultCodec`: Default codec for rendering

View file

@ -1,75 +0,0 @@
---
name: can-decode
description: Check if a video can be decoded by the browser using Mediabunny
metadata:
tags: decode, validation, video, audio, compatibility, browser
---
# Checking if a video can be decoded
Use Mediabunny to check if a video can be decoded by the browser before attempting to play it.
## The `canDecode()` function
This function can be copy-pasted into any project.
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const canDecode = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
try {
await input.getFormat();
} catch {
return false;
}
const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack && !(await videoTrack.canDecode())) {
return false;
}
const audioTrack = await input.getPrimaryAudioTrack();
if (audioTrack && !(await audioTrack.canDecode())) {
return false;
}
return true;
};
```
## Usage
```tsx
const src = "https://remotion.media/video.mp4";
const isDecodable = await canDecode(src);
if (isDecodable) {
console.log("Video can be decoded");
} else {
console.log("Video cannot be decoded by this browser");
}
```
## Using with Blob
For file uploads or drag-and-drop, use `BlobSource`:
```tsx
import { Input, ALL_FORMATS, BlobSource } from "mediabunny";
export const canDecodeBlob = async (blob: Blob) => {
const input = new Input({
formats: ALL_FORMATS,
source: new BlobSource(blob),
});
// Same validation logic as above
};
```

View file

@ -1,58 +0,0 @@
---
name: charts
description: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.
metadata:
tags: charts, data, visualization, bar-chart, pie-chart, graphs
---
# Charts in Remotion
You can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.
## No animations not powered by `useCurrentFrame()`
Disable all animations by third party libraries.
They will cause flickering during rendering.
Instead, drive all animations from `useCurrentFrame()`.
## Bar Chart Animations
See [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.
### Staggered Bars
You can animate the height of the bars and stagger them like this:
```tsx
const STAGGER_DELAY = 5;
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const bars = data.map((item, i) => {
const delay = i * STAGGER_DELAY;
const height = spring({
frame,
fps,
delay,
config: {damping: 200},
});
return <div style={{height: height * item.value}} />;
});
```
## Pie Chart Animation
Animate segments using stroke-dashoffset, starting from 12 o'clock.
```tsx
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const progress = interpolate(frame, [0, 100], [0, 1]);
const circumference = 2 * Math.PI * radius;
const segmentLength = (value / total) * circumference;
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
<circle r={radius} cx={center} cy={center} fill="none" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;
```

View file

@ -1,146 +0,0 @@
---
name: compositions
description: Defining compositions, stills, folders, default props and dynamic metadata
metadata:
tags: composition, still, folder, props, metadata
---
A `<Composition>` defines the component, width, height, fps and duration of a renderable video.
It normally is placed in the `src/Root.tsx` file.
```tsx
import { Composition } from "remotion";
import { MyComposition } from "./MyComposition";
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
/>
);
};
```
## Default Props
Pass `defaultProps` to provide initial values for your component.
Values must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).
```tsx
import { Composition } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
defaultProps={{
title: "Hello World",
color: "#ff0000",
} satisfies MyCompositionProps}
/>
);
};
```
Use `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.
## Folders
Use `<Folder>` to organize compositions in the sidebar.
Folder names can only contain letters, numbers, and hyphens.
```tsx
import { Composition, Folder } from "remotion";
export const RemotionRoot = () => {
return (
<>
<Folder name="Marketing">
<Composition id="Promo" /* ... */ />
<Composition id="Ad" /* ... */ />
</Folder>
<Folder name="Social">
<Folder name="Instagram">
<Composition id="Story" /* ... */ />
<Composition id="Reel" /* ... */ />
</Folder>
</Folder>
</>
);
};
```
## Stills
Use `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.
```tsx
import { Still } from "remotion";
import { Thumbnail } from "./Thumbnail";
export const RemotionRoot = () => {
return (
<Still
id="Thumbnail"
component={Thumbnail}
width={1280}
height={720}
/>
);
};
```
## Calculate Metadata
Use `calculateMetadata` to make dimensions, duration, or props dynamic based on data.
```tsx
import { Composition, CalculateMetadataFunction } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
const calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({
props,
abortSignal,
}) => {
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
signal: abortSignal,
}).then((res) => res.json());
return {
durationInFrames: Math.ceil(data.duration * 30),
props: {
...props,
videoUrl: data.url,
},
};
};
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100} // Placeholder, will be overridden
fps={30}
width={1080}
height={1080}
defaultProps={{ videoId: "abc123" }}
calculateMetadata={calculateMetadata}
/>
);
};
```
The function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.

View file

@ -1,126 +0,0 @@
---
name: display-captions
description: Displaying captions in Remotion with TikTok-style pages and word highlighting
metadata:
tags: captions, subtitles, display, tiktok, highlight
---
# Displaying captions in Remotion
This guide explains how to display captions in Remotion, assuming you already have captions in the `Caption` format.
## Prerequisites
First, the @remotion/captions package needs to be installed.
If it is not installed, use the following command:
```bash
npx remotion add @remotion/captions # If project uses npm
bunx remotion add @remotion/captions # If project uses bun
yarn remotion add @remotion/captions # If project uses yarn
pnpm exec remotion add @remotion/captions # If project uses pnpm
```
## Creating pages
Use `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:
```tsx
import {useMemo} from 'react';
import {createTikTokStyleCaptions} from '@remotion/captions';
import type {Caption} from '@remotion/captions';
// How often captions should switch (in milliseconds)
// Higher values = more words per page
// Lower values = fewer words (more word-by-word)
const SWITCH_CAPTIONS_EVERY_MS = 1200;
const {pages} = useMemo(() => {
return createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
});
}, [captions]);
```
## Rendering with Sequences
Map over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:
```tsx
import {Sequence, useVideoConfig, AbsoluteFill} from 'remotion';
import type {TikTokPage} from '@remotion/captions';
const CaptionedContent: React.FC = () => {
const {fps} = useVideoConfig();
return (
<AbsoluteFill>
{pages.map((page, index) => {
const nextPage = pages[index + 1] ?? null;
const startFrame = (page.startMs / 1000) * fps;
const endFrame = Math.min(
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
);
const durationInFrames = endFrame - startFrame;
if (durationInFrames <= 0) {
return null;
}
return (
<Sequence
key={index}
from={startFrame}
durationInFrames={durationInFrames}
>
<CaptionPage page={page} />
</Sequence>
);
})}
</AbsoluteFill>
);
};
```
## Word highlighting
A caption page contains `tokens` which you can use to highlight the currently spoken word:
```tsx
import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion';
import type {TikTokPage} from '@remotion/captions';
const HIGHLIGHT_COLOR = '#39E508';
const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
// Current time relative to the start of the sequence
const currentTimeMs = (frame / fps) * 1000;
// Convert to absolute time by adding the page start
const absoluteTimeMs = page.startMs + currentTimeMs;
return (
<AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
<div style={{fontSize: 80, fontWeight: 'bold', whiteSpace: 'pre'}}>
{page.tokens.map((token) => {
const isActive =
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
return (
<span
key={token.fromMs}
style={{color: isActive ? HIGHLIGHT_COLOR : 'white'}}
>
{token.text}
</span>
);
})}
</div>
</AbsoluteFill>
);
};
```

View file

@ -1,229 +0,0 @@
---
name: extract-frames
description: Extract frames from videos at specific timestamps using Mediabunny
metadata:
tags: frames, extract, video, thumbnail, filmstrip, canvas
---
# Extracting frames from videos
Use Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.
## The `extractFrames()` function
This function can be copy-pasted into any project.
```tsx
import {
ALL_FORMATS,
Input,
UrlSource,
VideoSample,
VideoSampleSink,
} from "mediabunny";
type Options = {
track: { width: number; height: number };
container: string;
durationInSeconds: number | null;
};
export type ExtractFramesTimestampsInSecondsFn = (
options: Options
) => Promise<number[]> | number[];
export type ExtractFramesProps = {
src: string;
timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
onVideoSample: (sample: VideoSample) => void;
signal?: AbortSignal;
};
export async function extractFrames({
src,
timestampsInSeconds,
onVideoSample,
signal,
}: ExtractFramesProps): Promise<void> {
using input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src),
});
const [durationInSeconds, format, videoTrack] = await Promise.all([
input.computeDuration(),
input.getFormat(),
input.getPrimaryVideoTrack(),
]);
if (!videoTrack) {
throw new Error("No video track found in the input");
}
if (signal?.aborted) {
throw new Error("Aborted");
}
const timestamps =
typeof timestampsInSeconds === "function"
? await timestampsInSeconds({
track: {
width: videoTrack.displayWidth,
height: videoTrack.displayHeight,
},
container: format.name,
durationInSeconds,
})
: timestampsInSeconds;
if (timestamps.length === 0) {
return;
}
if (signal?.aborted) {
throw new Error("Aborted");
}
const sink = new VideoSampleSink(videoTrack);
for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {
if (signal?.aborted) {
break;
}
if (!videoSample) {
continue;
}
onVideoSample(videoSample);
}
}
```
## Basic usage
Extract frames at specific timestamps:
```tsx
await extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: [0, 1, 2, 3, 4],
onVideoSample: (sample) => {
const canvas = document.createElement("canvas");
canvas.width = sample.displayWidth;
canvas.height = sample.displayHeight;
const ctx = canvas.getContext("2d");
sample.draw(ctx!, 0, 0);
},
});
```
## Creating a filmstrip
Use a callback function to dynamically calculate timestamps based on video metadata:
```tsx
const canvasWidth = 500;
const canvasHeight = 80;
const fromSeconds = 0;
const toSeconds = 10;
await extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: async ({ track, durationInSeconds }) => {
const aspectRatio = track.width / track.height;
const amountOfFramesFit = Math.ceil(
canvasWidth / (canvasHeight * aspectRatio)
);
const segmentDuration = toSeconds - fromSeconds;
const timestamps: number[] = [];
for (let i = 0; i < amountOfFramesFit; i++) {
timestamps.push(
fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)
);
}
return timestamps;
},
onVideoSample: (sample) => {
console.log(`Frame at ${sample.timestamp}s`);
const canvas = document.createElement("canvas");
canvas.width = sample.displayWidth;
canvas.height = sample.displayHeight;
const ctx = canvas.getContext("2d");
sample.draw(ctx!, 0, 0);
},
});
```
## Cancellation with AbortSignal
Cancel frame extraction after a timeout:
```tsx
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
await extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: [0, 1, 2, 3, 4],
onVideoSample: (sample) => {
using frame = sample;
const canvas = document.createElement("canvas");
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
const ctx = canvas.getContext("2d");
frame.draw(ctx!, 0, 0);
},
signal: controller.signal,
});
console.log("Frame extraction complete!");
} catch (error) {
console.error("Frame extraction was aborted or failed:", error);
}
```
## Timeout with Promise.race
```tsx
const controller = new AbortController();
const timeoutPromise = new Promise<never>((_, reject) => {
const timeoutId = setTimeout(() => {
controller.abort();
reject(new Error("Frame extraction timed out after 10 seconds"));
}, 10000);
controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), {
once: true,
});
});
try {
await Promise.race([
extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: [0, 1, 2, 3, 4],
onVideoSample: (sample) => {
using frame = sample;
const canvas = document.createElement("canvas");
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
const ctx = canvas.getContext("2d");
frame.draw(ctx!, 0, 0);
},
signal: controller.signal,
}),
timeoutPromise,
]);
console.log("Frame extraction complete!");
} catch (error) {
console.error("Frame extraction was aborted or failed:", error);
}
```

View file

@ -1,152 +0,0 @@
---
name: fonts
description: Loading Google Fonts and local fonts in Remotion
metadata:
tags: fonts, google-fonts, typography, text
---
# Using fonts in Remotion
## Google Fonts with @remotion/google-fonts
The recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.
### Prerequisites
First, the @remotion/google-fonts package needs to be installed.
If it is not installed, use the following command:
```bash
npx remotion add @remotion/google-fonts # If project uses npm
bunx remotion add @remotion/google-fonts # If project uses bun
yarn remotion add @remotion/google-fonts # If project uses yarn
pnpm exec remotion add @remotion/google-fonts # If project uses pnpm
```
```tsx
import { loadFont } from "@remotion/google-fonts/Lobster";
const { fontFamily } = loadFont();
export const MyComposition = () => {
return <div style={{ fontFamily }}>Hello World</div>;
};
```
Preferrably, specify only needed weights and subsets to reduce file size:
```tsx
import { loadFont } from "@remotion/google-fonts/Roboto";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});
```
### Waiting for font to load
Use `waitUntilDone()` if you need to know when the font is ready:
```tsx
import { loadFont } from "@remotion/google-fonts/Lobster";
const { fontFamily, waitUntilDone } = loadFont();
await waitUntilDone();
```
## Local fonts with @remotion/fonts
For local font files, use the `@remotion/fonts` package.
### Prerequisites
First, install @remotion/fonts:
```bash
npx remotion add @remotion/fonts # If project uses npm
bunx remotion add @remotion/fonts # If project uses bun
yarn remotion add @remotion/fonts # If project uses yarn
pnpm exec remotion add @remotion/fonts # If project uses pnpm
```
### Loading a local font
Place your font file in the `public/` folder and use `loadFont()`:
```tsx
import { loadFont } from "@remotion/fonts";
import { staticFile } from "remotion";
await loadFont({
family: "MyFont",
url: staticFile("MyFont-Regular.woff2"),
});
export const MyComposition = () => {
return <div style={{ fontFamily: "MyFont" }}>Hello World</div>;
};
```
### Loading multiple weights
Load each weight separately with the same family name:
```tsx
import { loadFont } from "@remotion/fonts";
import { staticFile } from "remotion";
await Promise.all([
loadFont({
family: "Inter",
url: staticFile("Inter-Regular.woff2"),
weight: "400",
}),
loadFont({
family: "Inter",
url: staticFile("Inter-Bold.woff2"),
weight: "700",
}),
]);
```
### Available options
```tsx
loadFont({
family: "MyFont", // Required: name to use in CSS
url: staticFile("font.woff2"), // Required: font file URL
format: "woff2", // Optional: auto-detected from extension
weight: "400", // Optional: font weight
style: "normal", // Optional: normal or italic
display: "block", // Optional: font-display behavior
});
```
## Using in components
Call `loadFont()` at the top level of your component or in a separate file that's imported early:
```tsx
import { loadFont } from "@remotion/google-fonts/Montserrat";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});
export const Title: React.FC<{ text: string }> = ({ text }) => {
return (
<h1
style={{
fontFamily,
fontSize: 80,
fontWeight: "bold",
}}
>
{text}
</h1>
);
};
```

View file

@ -1,58 +0,0 @@
---
name: get-audio-duration
description: Getting the duration of an audio file in seconds with Mediabunny
metadata:
tags: duration, audio, length, time, seconds, mp3, wav
---
# Getting audio duration with Mediabunny
Mediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.
## Getting audio duration
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getAudioDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};
```
## Usage
```tsx
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
console.log(duration); // e.g. 180.5 (seconds)
```
## Using with local files
For local files, use `FileSource` instead of `UrlSource`:
```tsx
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // File object from input or drag-drop
});
const durationInSeconds = await input.computeDuration();
```
## Using with staticFile in Remotion
```tsx
import { staticFile } from "remotion";
const duration = await getAudioDuration(staticFile("audio.mp3"));
```

View file

@ -1,68 +0,0 @@
---
name: get-video-dimensions
description: Getting the width and height of a video file with Mediabunny
metadata:
tags: dimensions, width, height, resolution, size, video
---
# Getting video dimensions with Mediabunny
Mediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.
## Getting video dimensions
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getVideoDimensions = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const videoTrack = await input.getPrimaryVideoTrack();
if (!videoTrack) {
throw new Error("No video track found");
}
return {
width: videoTrack.displayWidth,
height: videoTrack.displayHeight,
};
};
```
## Usage
```tsx
const dimensions = await getVideoDimensions("https://remotion.media/video.mp4");
console.log(dimensions.width); // e.g. 1920
console.log(dimensions.height); // e.g. 1080
```
## Using with local files
For local files, use `FileSource` instead of `UrlSource`:
```tsx
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // File object from input or drag-drop
});
const videoTrack = await input.getPrimaryVideoTrack();
const width = videoTrack.displayWidth;
const height = videoTrack.displayHeight;
```
## Using with staticFile in Remotion
```tsx
import { staticFile } from "remotion";
const dimensions = await getVideoDimensions(staticFile("video.mp4"));
```

View file

@ -1,58 +0,0 @@
---
name: get-video-duration
description: Getting the duration of a video file in seconds with Mediabunny
metadata:
tags: duration, video, length, time, seconds
---
# Getting video duration with Mediabunny
Mediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.
## Getting video duration
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getVideoDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};
```
## Usage
```tsx
const duration = await getVideoDuration("https://remotion.media/video.mp4");
console.log(duration); // e.g. 10.5 (seconds)
```
## Using with local files
For local files, use `FileSource` instead of `UrlSource`:
```tsx
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // File object from input or drag-drop
});
const durationInSeconds = await input.computeDuration();
```
## Using with staticFile in Remotion
```tsx
import { staticFile } from "remotion";
const duration = await getVideoDuration(staticFile("video.mp4"));
```

View file

@ -1,138 +0,0 @@
---
name: gif
description: Displaying GIFs, APNG, AVIF and WebP in Remotion
metadata:
tags: gif, animation, images, animated, apng, avif, webp
---
# Using Animated images in Remotion
## Basic usage
Use `<AnimatedImage>` to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:
```tsx
import {AnimatedImage, staticFile} from 'remotion';
export const MyComposition = () => {
return <AnimatedImage src={staticFile('animation.gif')} width={500} height={500} />;
};
```
Remote URLs are also supported (must have CORS enabled):
```tsx
<AnimatedImage src="https://example.com/animation.gif" width={500} height={500} />
```
## Sizing and fit
Control how the image fills its container with the `fit` prop:
```tsx
// Stretch to fill (default)
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="fill" />
// Maintain aspect ratio, fit inside container
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="contain" />
// Fill container, crop if needed
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="cover" />
```
## Playback speed
Use `playbackRate` to control the animation speed:
```tsx
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}
```
## Looping behavior
Control what happens when the animation finishes:
```tsx
// Loop indefinitely (default)
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="loop" />
// Play once, show final frame
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="pause-after-finish" />
// Play once, then clear canvas
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="clear-after-finish" />
```
## Styling
Use the `style` prop for additional CSS (use `width` and `height` props for sizing):
```tsx
<AnimatedImage
src={staticFile('animation.gif')}
width={500}
height={500}
style={{
borderRadius: 20,
position: 'absolute',
top: 100,
left: 50,
}}
/>
```
## Getting GIF duration
Use `getGifDurationInSeconds()` from `@remotion/gif` to get the duration of a GIF.
```bash
npx remotion add @remotion/gif # If project uses npm
bunx remotion add @remotion/gif # If project uses bun
yarn remotion add @remotion/gif # If project uses yarn
pnpm exec remotion add @remotion/gif # If project uses pnpm
```
```tsx
import {getGifDurationInSeconds} from '@remotion/gif';
import {staticFile} from 'remotion';
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
console.log(duration); // e.g. 2.5
```
This is useful for setting the composition duration to match the GIF:
```tsx
import {getGifDurationInSeconds} from '@remotion/gif';
import {staticFile, CalculateMetadataFunction} from 'remotion';
const calculateMetadata: CalculateMetadataFunction = async () => {
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
return {
durationInFrames: Math.ceil(duration * 30),
};
};
```
## Alternative
If `<AnimatedImage>` does not work (only supported in Chrome and Firefox), you can use `<Gif>` from `@remotion/gif` instead.
```bash
npx remotion add @remotion/gif # If project uses npm
bunx remotion add @remotion/gif # If project uses bun
yarn remotion add @remotion/gif # If project uses yarn
pnpm exec remotion add @remotion/gif # If project uses pnpm
```
```tsx
import {Gif} from '@remotion/gif';
import {staticFile} from 'remotion';
export const MyComposition = () => {
return <Gif src={staticFile('animation.gif')} width={500} height={500} />;
};
```
The `<Gif>` component has the same props as `<AnimatedImage>` but only supports GIF files.

View file

@ -1,130 +0,0 @@
---
name: images
description: Embedding images in Remotion using the <Img> component
metadata:
tags: images, img, staticFile, png, jpg, svg, webp
---
# Using images in Remotion
## The `<Img>` component
Always use the `<Img>` component from `remotion` to display images:
```tsx
import { Img, staticFile } from "remotion";
export const MyComposition = () => {
return <Img src={staticFile("photo.png")} />;
};
```
## Important restrictions
**You MUST use the `<Img>` component from `remotion`.** Do not use:
- Native HTML `<img>` elements
- Next.js `<Image>` component
- CSS `background-image`
The `<Img>` component ensures images are fully loaded before rendering, preventing flickering and blank frames during video export.
## Local images with staticFile()
Place images in the `public/` folder and use `staticFile()` to reference them:
```
my-video/
├─ public/
│ ├─ logo.png
│ ├─ avatar.jpg
│ └─ icon.svg
├─ src/
├─ package.json
```
```tsx
import { Img, staticFile } from "remotion";
<Img src={staticFile("logo.png")} />
```
## Remote images
Remote URLs can be used directly without `staticFile()`:
```tsx
<Img src="https://example.com/image.png" />
```
Ensure remote images have CORS enabled.
For animated GIFs, use the `<Gif>` component from `@remotion/gif` instead.
## Sizing and positioning
Use the `style` prop to control size and position:
```tsx
<Img
src={staticFile("photo.png")}
style={{
width: 500,
height: 300,
position: "absolute",
top: 100,
left: 50,
objectFit: "cover",
}}
/>
```
## Dynamic image paths
Use template literals for dynamic file references:
```tsx
import { Img, staticFile, useCurrentFrame } from "remotion";
const frame = useCurrentFrame();
// Image sequence
<Img src={staticFile(`frames/frame${frame}.png`)} />
// Selecting based on props
<Img src={staticFile(`avatars/${props.userId}.png`)} />
// Conditional images
<Img src={staticFile(`icons/${isActive ? "active" : "inactive"}.svg`)} />
```
This pattern is useful for:
- Image sequences (frame-by-frame animations)
- User-specific avatars or profile images
- Theme-based icons
- State-dependent graphics
## Getting image dimensions
Use `getImageDimensions()` to get the dimensions of an image:
```tsx
import { getImageDimensions, staticFile } from "remotion";
const { width, height } = await getImageDimensions(staticFile("photo.png"));
```
This is useful for calculating aspect ratios or sizing compositions:
```tsx
import { getImageDimensions, staticFile, CalculateMetadataFunction } from "remotion";
const calculateMetadata: CalculateMetadataFunction = async () => {
const { width, height } = await getImageDimensions(staticFile("photo.png"));
return {
width,
height,
};
};
```

View file

@ -1,67 +0,0 @@
---
name: import-srt-captions
description: Importing .srt subtitle files into Remotion using @remotion/captions
metadata:
tags: captions, subtitles, srt, import, parse
---
# Importing .srt subtitles into Remotion
If you have an existing `.srt` subtitle file, you can import it into Remotion using `parseSrt()` from `@remotion/captions`.
## Prerequisites
First, the @remotion/captions package needs to be installed.
If it is not installed, use the following command:
```bash
npx remotion add @remotion/captions # If project uses npm
bunx remotion add @remotion/captions # If project uses bun
yarn remotion add @remotion/captions # If project uses yarn
pnpm exec remotion add @remotion/captions # If project uses pnpm
```
## Reading an .srt file
Use `staticFile()` to reference an `.srt` file in your `public` folder, then fetch and parse it:
```tsx
import {useState, useEffect, useCallback} from 'react';
import {AbsoluteFill, staticFile, useDelayRender} from 'remotion';
import {parseSrt} from '@remotion/captions';
import type {Caption} from '@remotion/captions';
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const {delayRender, continueRender, cancelRender} = useDelayRender();
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
const response = await fetch(staticFile('subtitles.srt'));
const text = await response.text();
const {captions: parsed} = parseSrt({input: text});
setCaptions(parsed);
continueRender(handle);
} catch (e) {
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;
};
```
Remote URLs are also supported - you can `fetch()` a remote file via URL instead of using `staticFile()`.
## Using imported captions
Once parsed, the captions are in the `Caption` format and can be used with all `@remotion/captions` utilities.

View file

@ -1,68 +0,0 @@
---
name: lottie
description: Embedding Lottie animations in Remotion.
metadata:
category: Animation
---
# Using Lottie Animations in Remotion
## Prerequisites
First, the @remotion/lottie package needs to be installed.
If it is not, use the following command:
```bash
npx remotion add @remotion/lottie # If project uses npm
bunx remotion add @remotion/lottie # If project uses bun
yarn remotion add @remotion/lottie # If project uses yarn
pnpm exec remotion add @remotion/lottie # If project uses pnpm
```
## Displaying a Lottie file
To import a Lottie animation:
- Fetch the Lottie asset
- Wrap the loading process in `delayRender()` and `continueRender()`
- Save the animation data in a state
- Render the Lottie animation using the `Lottie` component from the `@remotion/lottie` package
```tsx
import {Lottie, LottieAnimationData} from '@remotion/lottie';
import {useEffect, useState} from 'react';
import {cancelRender, continueRender, delayRender} from 'remotion';
export const MyAnimation = () => {
const [handle] = useState(() => delayRender('Loading Lottie animation'));
const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);
useEffect(() => {
fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')
.then((data) => data.json())
.then((json) => {
setAnimationData(json);
continueRender(handle);
})
.catch((err) => {
cancelRender(err);
});
}, [handle]);
if (!animationData) {
return null;
}
return <Lottie animationData={animationData} />;
};
```
## Styling and animating
Lottie supports the `style` prop to allow styles and animations:
```tsx
return <Lottie animationData={animationData} style={{width: 400, height: 400}} />;
```

View file

@ -1,35 +0,0 @@
---
name: measuring-dom-nodes
description: Measuring DOM element dimensions in Remotion
metadata:
tags: measure, layout, dimensions, getBoundingClientRect, scale
---
# Measuring DOM nodes in Remotion
Remotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.
## Measuring element dimensions
```tsx
import { useCurrentScale } from "remotion";
import { useRef, useEffect, useState } from "react";
export const MyComponent = () => {
const ref = useRef<HTMLDivElement>(null);
const scale = useCurrentScale();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
setDimensions({
width: rect.width / scale,
height: rect.height / scale,
});
}, [scale]);
return <div ref={ref}>Content to measure</div>;
};
```

View file

@ -1,143 +0,0 @@
---
name: measuring-text
description: Measuring text dimensions, fitting text to containers, and checking overflow
metadata:
tags: measure, text, layout, dimensions, fitText, fillTextBox
---
# Measuring text in Remotion
## Prerequisites
Install @remotion/layout-utils if it is not already installed:
```bash
npx remotion add @remotion/layout-utils # If project uses npm
bunx remotion add @remotion/layout-utils # If project uses bun
yarn remotion add @remotion/layout-utils # If project uses yarn
pnpm exec remotion add @remotion/layout-utils # If project uses pnpm
```
## Measuring text dimensions
Use `measureText()` to calculate the width and height of text:
```tsx
import { measureText } from "@remotion/layout-utils";
const { width, height } = measureText({
text: "Hello World",
fontFamily: "Arial",
fontSize: 32,
fontWeight: "bold",
});
```
Results are cached - duplicate calls return the cached result.
## Fitting text to a width
Use `fitText()` to find the optimal font size for a container:
```tsx
import { fitText } from "@remotion/layout-utils";
const { fontSize } = fitText({
text: "Hello World",
withinWidth: 600,
fontFamily: "Inter",
fontWeight: "bold",
});
return (
<div
style={{
fontSize: Math.min(fontSize, 80), // Cap at 80px
fontFamily: "Inter",
fontWeight: "bold",
}}
>
Hello World
</div>
);
```
## Checking text overflow
Use `fillTextBox()` to check if text exceeds a box:
```tsx
import { fillTextBox } from "@remotion/layout-utils";
const box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });
const words = ["Hello", "World", "This", "is", "a", "test"];
for (const word of words) {
const { exceedsBox } = box.add({
text: word + " ",
fontFamily: "Arial",
fontSize: 24,
});
if (exceedsBox) {
// Text would overflow, handle accordingly
break;
}
}
```
## Best practices
**Load fonts first:** Only call measurement functions after fonts are loaded.
```tsx
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily, waitUntilDone } = loadFont("normal", {
weights: ["400"],
subsets: ["latin"],
});
waitUntilDone().then(() => {
// Now safe to measure
const { width } = measureText({
text: "Hello",
fontFamily,
fontSize: 32,
});
})
```
**Use validateFontIsLoaded:** Catch font loading issues early:
```tsx
measureText({
text: "Hello",
fontFamily: "MyCustomFont",
fontSize: 32,
validateFontIsLoaded: true, // Throws if font not loaded
});
```
**Match font properties:** Use the same properties for measurement and rendering:
```tsx
const fontStyle = {
fontFamily: "Inter",
fontSize: 32,
fontWeight: "bold" as const,
letterSpacing: "0.5px",
};
const { width } = measureText({
text: "Hello",
...fontStyle,
});
return <div style={fontStyle}>Hello</div>;
```
**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:
```tsx
<div style={{ outline: "2px solid red" }}>Text</div>
```

View file

@ -1,106 +0,0 @@
---
name: sequencing
description: Sequencing patterns for Remotion - delay, trim, limit duration of items
metadata:
tags: sequence, series, timing, delay, trim
---
Use `<Sequence>` to delay when an element appears in the timeline.
```tsx
import { Sequence } from "remotion";
const {fps} = useVideoConfig();
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Title />
</Sequence>
<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Subtitle />
</Sequence>
```
This will by default wrap the component in an absolute fill element.
If the items should not be wrapped, use the `layout` prop:
```tsx
<Sequence layout="none">
<Title />
</Sequence>
```
## Premounting
This loads the component in the timeline before it is actually played.
Always premount any `<Sequence>`!
```tsx
<Sequence premountFor={1 * fps}>
<Title />
</Sequence>
```
## Series
Use `<Series>` when elements should play one after another without overlap.
```tsx
import {Series} from 'remotion';
<Series>
<Series.Sequence durationInFrames={45}>
<Intro />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<MainContent />
</Series.Sequence>
<Series.Sequence durationInFrames={30}>
<Outro />
</Series.Sequence>
</Series>;
```
Same as with `<Sequence>`, the items will be wrapped in an absolute fill element by default when using `<Series.Sequence>`, unless the `layout` prop is set to `none`.
### Series with overlaps
Use negative offset for overlapping sequences:
```tsx
<Series>
<Series.Sequence durationInFrames={60}>
<SceneA />
</Series.Sequence>
<Series.Sequence offset={-15} durationInFrames={60}>
{/* Starts 15 frames before SceneA ends */}
<SceneB />
</Series.Sequence>
</Series>
```
## Frame References Inside Sequences
Inside a Sequence, `useCurrentFrame()` returns the local frame (starting from 0):
```tsx
<Sequence from={60} durationInFrames={30}>
<MyComponent />
{/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}
</Sequence>
```
## Nested Sequences
Sequences can be nested for complex timing:
```tsx
<Sequence from={0} durationInFrames={120}>
<Background />
<Sequence from={15} durationInFrames={90} layout="none">
<Title />
</Sequence>
<Sequence from={45} durationInFrames={60} layout="none">
<Subtitle />
</Sequence>
</Sequence>
```

View file

@ -1,11 +0,0 @@
---
name: tailwind
description: Using TailwindCSS in Remotion.
metadata:
---
You can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.
Don't use `transition-*` or `animate-*` classes - always animate using the `useCurrentFrame()` hook.
Tailwind must be installed and enabled first in a Remotion project - fetch https://www.remotion.dev/docs/tailwind using WebFetch for instructions.

View file

@ -1,20 +0,0 @@
---
name: text-animations
description: Typography and text animation patterns for Remotion.
metadata:
tags: typography, text, typewriter, highlighter ken
---
## Text animations
Based on `useCurrentFrame()`, reduce the string character by character to create a typewriter effect.
## Typewriter Effect
See [Typewriter](assets/text-animations-typewriter.tsx) for an advanced example with a blinking cursor and a pause after the first sentence.
Always use string slicing for typewriter effects. Never use per-character opacity.
## Word Highlighting
See [Word Highlight](assets/text-animations-word-highlight.tsx) for an example for how a word highlight is animated, like with a highlighter pen.

View file

@ -1,179 +0,0 @@
---
name: timing
description: Interpolation curves in Remotion - linear, easing, spring animations
metadata:
tags: spring, bounce, easing, interpolation
---
A simple linear interpolation is done using the `interpolate` function.
```ts title="Going from 0 to 1 over 100 frames"
import {interpolate} from 'remotion';
const opacity = interpolate(frame, [0, 100], [0, 1]);
```
By default, the values are not clamped, so the value can go outside the range [0, 1].
Here is how they can be clamped:
```ts title="Going from 0 to 1 over 100 frames with extrapolation"
const opacity = interpolate(frame, [0, 100], [0, 1], {
extrapolateRight: 'clamp',
extrapolateLeft: 'clamp',
});
```
## Spring animations
Spring animations have a more natural motion.
They go from 0 to 1 over time.
```ts title="Spring animation from 0 to 1 over 100 frames"
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const scale = spring({
frame,
fps,
});
```
### Physical properties
The default configuration is: `mass: 1, damping: 10, stiffness: 100`.
This leads to the animation having a bit of bounce before it settles.
The config can be overwritten like this:
```ts
const scale = spring({
frame,
fps,
config: {damping: 200},
});
```
The recommended configuration for a natural motion without a bounce is: `{ damping: 200 }`.
Here are some common configurations:
```tsx
const smooth = {damping: 200}; // Smooth, no bounce (subtle reveals)
const snappy = {damping: 20, stiffness: 200}; // Snappy, minimal bounce (UI elements)
const bouncy = {damping: 8}; // Bouncy entrance (playful animations)
const heavy = {damping: 15, stiffness: 80, mass: 2}; // Heavy, slow, small bounce
```
### Delay
The animation starts immediately by default.
Use the `delay` parameter to delay the animation by a number of frames.
```tsx
const entrance = spring({
frame: frame - ENTRANCE_DELAY,
fps,
delay: 20,
});
```
### Duration
A `spring()` has a natural duration based on the physical properties.
To stretch the animation to a specific duration, use the `durationInFrames` parameter.
```tsx
const spring = spring({
frame,
fps,
durationInFrames: 40,
});
```
### Combining spring() with interpolate()
Map spring output (0-1) to custom ranges:
```tsx
const springProgress = spring({
frame,
fps,
});
// Map to rotation
const rotation = interpolate(springProgress, [0, 1], [0, 360]);
<div style={{rotate: rotation + 'deg'}} />;
```
### Adding springs
Springs return just numbers, so math can be performed:
```tsx
const frame = useCurrentFrame();
const {fps, durationInFrames} = useVideoConfig();
const inAnimation = spring({
frame,
fps,
});
const outAnimation = spring({
frame,
fps,
durationInFrames: 1 * fps,
delay: durationInFrames - 1 * fps,
});
const scale = inAnimation - outAnimation;
```
## Easing
Easing can be added to the `interpolate` function:
```ts
import {interpolate, Easing} from 'remotion';
const value1 = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.inOut(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```
The default easing is `Easing.linear`.
There are various other convexities:
- `Easing.in` for starting slow and accelerating
- `Easing.out` for starting fast and slowing down
- `Easing.inOut`
and curves (sorted from most linear to most curved):
- `Easing.quad`
- `Easing.sin`
- `Easing.exp`
- `Easing.circle`
Convexities and curves need be combined for an easing function:
```ts
const value1 = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.inOut(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```
Cubic bezier curves are also supported:
```ts
const value1 = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```

View file

@ -1,19 +0,0 @@
---
name: transcribe-captions
description: Transcribing audio to generate captions in Remotion
metadata:
tags: captions, transcribe, whisper, audio, speech-to-text
---
# Transcribing audio
Remotion provides several built-in options for transcribing audio to generate captions:
- `@remotion/install-whisper-cpp` - Transcribe locally on a server using Whisper.cpp. Fast and free, but requires server infrastructure.
https://remotion.dev/docs/install-whisper-cpp
- `@remotion/whisper-web` - Transcribe in the browser using WebAssembly. No server needed and free, but slower due to WASM overhead.
https://remotion.dev/docs/whisper-web
- `@remotion/openai-whisper` - Use OpenAI Whisper API for cloud-based transcription. Fast and no server needed, but requires payment.
https://remotion.dev/docs/openai-whisper/openai-whisper-api-to-captions

View file

@ -1,122 +0,0 @@
---
name: transitions
description: Fullscreen scene transitions for Remotion.
metadata:
tags: transitions, fade, slide, wipe, scenes
---
## Fullscreen transitions
Using `<TransitionSeries>` to animate between multiple scenes or clips.
This will absolutely position the children.
## Prerequisites
First, the @remotion/transitions package needs to be installed.
If it is not, use the following command:
```bash
npx remotion add @remotion/transitions # If project uses npm
bunx remotion add @remotion/transitions # If project uses bun
yarn remotion add @remotion/transitions # If project uses yarn
pnpm exec remotion add @remotion/transitions # If project uses pnpm
```
## Example usage
```tsx
import {TransitionSeries, linearTiming} from '@remotion/transitions';
import {fade} from '@remotion/transitions/fade';
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={fade()} timing={linearTiming({durationInFrames: 15})} />
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>;
```
## Available Transition Types
Import transitions from their respective modules:
```tsx
import {fade} from '@remotion/transitions/fade';
import {slide} from '@remotion/transitions/slide';
import {wipe} from '@remotion/transitions/wipe';
import {flip} from '@remotion/transitions/flip';
import {clockWipe} from '@remotion/transitions/clock-wipe';
```
## Slide Transition with Direction
Specify slide direction for enter/exit animations.
```tsx
import {slide} from '@remotion/transitions/slide';
<TransitionSeries.Transition presentation={slide({direction: 'from-left'})} timing={linearTiming({durationInFrames: 20})} />;
```
Directions: `"from-left"`, `"from-right"`, `"from-top"`, `"from-bottom"`
## Timing Options
```tsx
import {linearTiming, springTiming} from '@remotion/transitions';
// Linear timing - constant speed
linearTiming({durationInFrames: 20});
// Spring timing - organic motion
springTiming({config: {damping: 200}, durationInFrames: 25});
```
## Duration calculation
Transitions overlap adjacent scenes, so the total composition length is **shorter** than the sum of all sequence durations.
For example, with two 60-frame sequences and a 15-frame transition:
- Without transitions: `60 + 60 = 120` frames
- With transition: `60 + 60 - 15 = 105` frames
The transition duration is subtracted because both scenes play simultaneously during the transition.
### Getting the duration of a transition
Use the `getDurationInFrames()` method on the timing object:
```tsx
import {linearTiming, springTiming} from '@remotion/transitions';
const linearDuration = linearTiming({durationInFrames: 20}).getDurationInFrames({fps: 30});
// Returns 20
const springDuration = springTiming({config: {damping: 200}}).getDurationInFrames({fps: 30});
// Returns calculated duration based on spring physics
```
For `springTiming` without an explicit `durationInFrames`, the duration depends on `fps` because it calculates when the spring animation settles.
### Calculating total composition duration
```tsx
import {linearTiming} from '@remotion/transitions';
const scene1Duration = 60;
const scene2Duration = 60;
const scene3Duration = 60;
const timing1 = linearTiming({durationInFrames: 15});
const timing2 = linearTiming({durationInFrames: 20});
const transition1Duration = timing1.getDurationInFrames({fps: 30});
const transition2Duration = timing2.getDurationInFrames({fps: 30});
const totalDuration = scene1Duration + scene2Duration + scene3Duration - transition1Duration - transition2Duration;
// 60 + 60 + 60 - 15 - 20 = 145 frames
```

View file

@ -1,53 +0,0 @@
---
name: trimming
description: Trimming patterns for Remotion - cut the beginning or end of animations
metadata:
tags: sequence, trim, clip, cut, offset
---
Use `<Sequence>` with a negative `from` value to trim the start of an animation.
## Trim the Beginning
A negative `from` value shifts time backwards, making the animation start partway through:
```tsx
import { Sequence, useVideoConfig } from "remotion";
const fps = useVideoConfig();
<Sequence from={-0.5 * fps}>
<MyAnimation />
</Sequence>
```
The animation appears 15 frames into its progress - the first 15 frames are trimmed off.
Inside `<MyAnimation>`, `useCurrentFrame()` starts at 15 instead of 0.
## Trim the End
Use `durationInFrames` to unmount content after a specified duration:
```tsx
<Sequence durationInFrames={1.5 * fps}>
<MyAnimation />
</Sequence>
```
The animation plays for 45 frames, then the component unmounts.
## Trim and Delay
Nest sequences to both trim the beginning and delay when it appears:
```tsx
<Sequence from={30}>
<Sequence from={-15}>
<MyAnimation />
</Sequence>
</Sequence>
```
The inner sequence trims 15 frames from the start, and the outer sequence delays the result by 30 frames.

View file

@ -1,171 +0,0 @@
---
name: videos
description: Embedding videos in Remotion - trimming, volume, speed, looping, pitch
metadata:
tags: video, media, trim, volume, speed, loop, pitch
---
# Using videos in Remotion
## Prerequisites
First, the @remotion/media package needs to be installed.
If it is not, use the following command:
```bash
npx remotion add @remotion/media # If project uses npm
bunx remotion add @remotion/media # If project uses bun
yarn remotion add @remotion/media # If project uses yarn
pnpm exec remotion add @remotion/media # If project uses pnpm
```
Use `<Video>` from `@remotion/media` to embed videos into your composition.
```tsx
import { Video } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Video src={staticFile("video.mp4")} />;
};
```
Remote URLs are also supported:
```tsx
<Video src="https://remotion.media/video.mp4" />
```
## Trimming
Use `trimBefore` and `trimAfter` to remove portions of the video. Values are in seconds.
```tsx
const { fps } = useVideoConfig();
return (
<Video
src={staticFile("video.mp4")}
trimBefore={2 * fps} // Skip the first 2 seconds
trimAfter={10 * fps} // End at the 10 second mark
/>
);
```
## Delaying
Wrap the video in a `<Sequence>` to delay when it appears:
```tsx
import { Sequence, staticFile } from "remotion";
import { Video } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Video src={staticFile("video.mp4")} />
</Sequence>
);
```
The video will appear after 1 second.
## Sizing and Position
Use the `style` prop to control size and position:
```tsx
<Video
src={staticFile("video.mp4")}
style={{
width: 500,
height: 300,
position: "absolute",
top: 100,
left: 50,
objectFit: "cover",
}}
/>
```
## Volume
Set a static volume (0 to 1):
```tsx
<Video src={staticFile("video.mp4")} volume={0.5} />
```
Or use a callback for dynamic volume based on the current frame:
```tsx
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Video
src={staticFile("video.mp4")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);
```
Use `muted` to silence the video entirely:
```tsx
<Video src={staticFile("video.mp4")} muted />
```
## Speed
Use `playbackRate` to change the playback speed:
```tsx
<Video src={staticFile("video.mp4")} playbackRate={2} /> {/* 2x speed */}
<Video src={staticFile("video.mp4")} playbackRate={0.5} /> {/* Half speed */}
```
Reverse playback is not supported.
## Looping
Use `loop` to loop the video indefinitely:
```tsx
<Video src={staticFile("video.mp4")} loop />
```
Use `loopVolumeCurveBehavior` to control how the frame count behaves when looping:
- `"repeat"`: Frame count resets to 0 each loop (for `volume` callback)
- `"extend"`: Frame count continues incrementing
```tsx
<Video
src={staticFile("video.mp4")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
/>
```
## Pitch
Use `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:
```tsx
<Video
src={staticFile("video.mp4")}
toneFrequency={1.5} // Higher pitch
/>
<Video
src={staticFile("video.mp4")}
toneFrequency={0.8} // Lower pitch
/>
```
Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.

View file

@ -1,296 +0,0 @@
---
name: tpmjs-tool-creator
description: Guide for creating official TPMJS tools using the blocks CLI. Use when a user wants to create a new tool for the TPMJS registry, add a tool to packages/tools/official/, implement an AI SDK v6 tool, define a block in blocks.yml, validate a tool with `pnpm blocks run`, or publish a tool to npm with the tpmjs keyword.
---
# TPMJS Tool Creator
Create production-ready tools for the TPMJS registry using the blocks CLI. Tools are npm packages following the AI SDK v6 pattern, validated by blocks, and automatically synced to tpmjs.com.
## Workflow
1. Define the tool block in `packages/tools/official/blocks.yml`
2. Create the tool package directory
3. Implement the tool using AI SDK v6 `tool()` + `jsonSchema()`
4. Validate with `pnpm blocks run <tool-name>`
5. Build and publish to npm
## Step 1: Define in blocks.yml
Add to the `blocks:` section of `packages/tools/official/blocks.yml`:
```yaml
blocks:
category.toolName:
type: utility
description: "LLM-friendly description of what the tool does"
path: "tool-directory-name"
domain_rules:
- id: rule_name
description: "What this implementation must do"
inputs:
- name: inputName
type: string
description: "Description for LLMs"
- name: optionalInput
type: number
optional: true
description: "Optional parameter"
outputs:
- name: result
type: ResultType
description: "What the tool returns"
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
```
**Category prefix** (before the dot): `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`, `finance`, `legal`, `hr`, `marketing`, `cx`, `edu`, `sales`.
For domain entities and quality measures, see [references/domain.md](references/domain.md).
## Step 2: Create Package Directory
Create `packages/tools/official/<tool-name>/`:
```
<tool-name>/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── README.md
└── src/
└── index.ts
```
**package.json:**
```json
{
"name": "@tpmjs/official-<tool-name>",
"version": "0.1.0",
"description": "Short description",
"type": "module",
"keywords": ["tpmjs", "<category>", "ai"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
},
"dependencies": {
"ai": "6.0.49"
},
"publishConfig": { "access": "public" },
"repository": {
"type": "git",
"url": "https://github.com/tpmjs/tpmjs.git",
"directory": "packages/tools/official/<tool-name>"
},
"homepage": "https://tpmjs.com",
"license": "MIT",
"tpmjs": {
"category": "<category>",
"frameworks": ["vercel-ai"],
"tools": [
{
"name": "toolName",
"description": "Clear description (20+ chars)."
}
]
}
}
```
**tsconfig.json:**
```json
{
"extends": "@tpmjs/tsconfig/react-library.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
**tsup.config.ts:**
```typescript
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
target: 'es2022',
});
```
## Step 3: Implement the Tool
Every tool follows this AI SDK v6 pattern in `src/index.ts`:
```typescript
import { jsonSchema, tool } from 'ai';
interface MyToolInput {
param1: string;
param2?: number;
}
export interface MyToolResult {
data: string;
metadata: { processedAt: string };
}
export const myTool = tool({
description: 'Clear LLM-friendly description of what this tool does.',
parameters: jsonSchema<MyToolInput>({
type: 'object',
properties: {
param1: {
type: 'string',
description: 'What param1 is for',
},
param2: {
type: 'number',
description: 'Optional: what param2 is for',
},
},
required: ['param1'],
additionalProperties: false,
}),
execute: async (input): Promise<MyToolResult> => {
if (!input.param1) {
throw new Error('param1 is required and must be non-empty');
}
try {
const result = await processData(input.param1);
return {
data: result,
metadata: { processedAt: new Date().toISOString() },
};
} catch (error) {
throw new Error(
`Failed to process: ${error instanceof Error ? error.message : String(error)}`
);
}
},
});
export default myTool;
```
**Hard rules:**
- No stubs, TODOs, or placeholders — every tool must be fully working
- Single-shot: one call in, one structured result out
- Validate inputs before processing
- Try-catch with descriptive errors including context
- `additionalProperties: false` on jsonSchema
- Description on every schema property
- Export as both named and default export
- Output interface must be exported
### Multi-Tool Packages
For packages with multiple tools, add root-level files:
**block.ts:**
```typescript
import { toolA, toolB } from './src/index.js';
export const block = { name: 'package-name', tools: { toolA, toolB } };
export default block;
```
**index.ts (root):**
```typescript
export * from './src/index.js';
export { default } from './src/index.js';
```
Each tool gets its own entry in blocks.yml (same `path`) and in `tpmjs.tools` array.
## Step 4: Validate
The blocks CLI domain validator requires an OpenAI API key. Source it from `.env.local` before running:
```bash
cd packages/tools/official
# Load the OpenAI API key for domain validation
source ../../../.env.local
export OPENAI_API_KEY
pnpm blocks run <tool-name> # Validate (schema → shape → domain)
pnpm blocks run <tool-name> --force # Force full validation (skip cache)
pnpm blocks run <tool-name> --json # JSON output for debugging
pnpm blocks run --all # Validate all tools
```
**Common errors:**
- `Tool "X" not found in exports` → Export name must match blocks.yml
- `Required file not found` → Check package root has all required files
- `invalid tpmjs field` → Category must be valid, tools array required
## Step 5: Build and Publish
```bash
pnpm --filter=@tpmjs/official-<tool-name> build
cd packages/tools/official/<tool-name> && npm publish --access public
```
The tool syncs to tpmjs.com automatically via the changes feed (every 2 min) and keyword search (every 15 min). To trigger immediately:
```bash
source apps/web/.env.local
curl -X POST https://tpmjs.com/api/sync/keyword \
-H "Authorization: Bearer $CRON_SECRET"
```
## README Template
Every tool needs a README:
```markdown
# @tpmjs/official-<tool-name>
Short description.
## Installation
npm install @tpmjs/official-<tool-name>
## Usage
\`\`\`typescript
import { myTool } from '@tpmjs/official-<tool-name>';
const result = await myTool.execute({ param1: 'example' });
\`\`\`
## Parameters
| Name | Type | Required | Description |
|--------|--------|----------|--------------------|
| param1 | string | Yes | What param1 is for |
## Output
| Field | Type | Description |
|-------|--------|----------------------|
| data | string | The processed result |
## License
MIT
```

View file

@ -1,58 +0,0 @@
# Domain Reference
## Entities
Reusable output types defined in blocks.yml. Reference these in your tool's output `type` field.
| Entity | Fields |
|--------|--------|
| url | href, domain, protocol, path, query, fragment |
| webpage | url, title, html, text, metadata |
| text_content | raw, sentences, paragraphs, wordCount |
| claim | statement, confidence, needsCitation, category |
| timeline | events, dateRange, gaps, eventCount |
| evidence | source, type, strength, relevance |
| summary | text, keyPoints, length, compressionRatio |
| sentiment | score, label, confidence, aspects |
| entity | name, type, mentions, context |
| relationship | source, target, type, strength |
| pattern | name, frequency, examples, significance |
| anomaly | description, severity, context, recommendation |
| metric | name, value, unit, trend |
| comparison | items, criteria, rankings, analysis |
| recommendation | action, priority, rationale, impact |
| risk | description, likelihood, impact, mitigation |
| code_snippet | language, code, explanation, complexity |
| api_endpoint | method, path, parameters, response |
| data_schema | fields, types, constraints, relationships |
| workflow_step | action, input, output, conditions |
## Quality Measures
Reference these in your output's `measures` array.
| Measure | Severity | What it checks |
|---------|----------|---------------|
| working_implementation | error | No TODOs, stubs, or placeholders. Returns actual computed values. |
| valid_output_structure | error | Returns object matching declared interface. All required fields present. Arrays never undefined. |
| proper_error_handling | error | Throws descriptive Error with context. Validates inputs. Catches external API errors. |
| ai_sdk_compliance | error | Uses `tool()` + `jsonSchema()` from 'ai'. Clear description. Every property has description. |
| npm_publishable | error | Valid package.json with tpmjs field. Named + default exports. Proper types. Semver version. |
| readme_documentation | error | README exists. Describes tool. Usage example. Documents inputs/outputs. |
| deterministic_output | warning | Same input produces same output (where applicable). |
| minimal_dependencies | warning | Uses stable, well-maintained packages. Avoids unnecessary deps. |
## Domain Rules
Common domain rule categories for the `domain_rules` field in blocks.yml:
- **Core implementation**: working code, proper types, error handling
- **Web & fetch**: URL validation, content extraction, timeout handling
- **Document generation**: format compliance, template rendering
- **Data transformation**: schema validation, type coercion, encoding
- **Engineering/code analysis**: AST parsing, complexity metrics
- **Security & compliance**: input sanitization, safe execution
- **Statistical rigor**: numerical accuracy, proper rounding
- **Workflow/recipe**: step sequencing, state management
Define custom rules specific to your tool's requirements. Each rule needs an `id` and `description`.

View file

@ -1,170 +0,0 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** @type {import('dependency-cruiser').IConfiguration} */
export default {
forbidden: [
{
name: 'no-circular',
severity: 'error',
comment:
'This dependency is part of a circular relationship. You might want to revise ' +
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
from: {},
to: {
circular: true,
},
},
{
name: 'no-orphans',
comment:
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
'add an exception for it in your dependency-cruiser configuration. By default ' +
'this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration ' +
'files (.d.ts), tsconfig.json and some of the babel and webpack configs.',
severity: 'warn',
from: {
orphan: true,
pathNot: [
'(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files
'\\.d\\.ts$', // TypeScript declaration files
'(^|/)tsconfig\\.json$', // tsconfig
'(^|/)postcss\\.config\\.(js|cjs|mjs)$', // postcss config
'(^|/)(babel|webpack|tailwind)\\.config\\.(js|cjs|mjs|ts|json)$', // other configs
'/tokens\\.ts$', // token files
],
},
to: {},
},
{
name: 'no-deprecated-core',
comment:
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
"bound to exist - node doesn't deprecate lightly.",
severity: 'warn',
from: {},
to: {
dependencyTypes: ['core'],
path: [
'^(v8/tools/codemap)$',
'^(v8/tools/consarray)$',
'^(v8/tools/csvparser)$',
'^(v8/tools/logreader)$',
'^(v8/tools/profile_view)$',
'^(v8/tools/profile)$',
'^(v8/tools/SourceMap)$',
'^(v8/tools/splaytree)$',
'^(v8/tools/tickprocessor-driver)$',
'^(v8/tools/tickprocessor)$',
'^(node-inspect/lib/_inspect)$',
'^(node-inspect/lib/internal/inspect_client)$',
'^(node-inspect/lib/internal/inspect_repl)$',
'^(async_hooks)$',
'^(punycode)$',
'^(domain)$',
'^(constants)$',
'^(sys)$',
'^(_linklist)$',
'^(_stream_wrap)$',
],
},
},
{
name: 'not-to-deprecated',
comment:
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
'version of that module, or find an alternative. Deprecated modules are a security risk.',
severity: 'warn',
from: {},
to: {
dependencyTypes: ['deprecated'],
},
},
{
name: 'no-non-package-json',
severity: 'error',
comment:
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
'available on live with an non-guaranteed version. Fix it by adding the package to the dependencies ' +
'in your package.json.',
from: {},
to: {
dependencyTypes: ['npm-no-pkg', 'npm-unknown'],
},
},
{
name: 'not-to-unresolvable',
comment:
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
'module: add it to your package.json. In all other cases you likely already know what to do.',
severity: 'error',
from: {},
to: {
couldNotResolve: true,
// Allow TypeScript path aliases and workspace packages that are resolved by the TS compiler
pathNot: ['^~/', '^@/', '^@tpmjs/'],
},
},
{
name: 'no-duplicate-dep-types',
comment:
"Likeley this module depends on an external ('npm') package that occurs more than once " +
'in your package.json i.e. both as a devDependencies and in dependencies. This will cause ' +
'maintenance problems later on.',
severity: 'warn',
from: {},
to: {
moreThanOneDependencyType: true,
// as it's pretty common to have a type import be a type only import
// _and_ (e.g.) a devDependency - don't consider type-only dependency
// types for this rule
dependencyTypesNot: ['type-only'],
},
},
/* Custom monorepo rules - keep it simple */
{
name: 'no-package-to-app-imports',
comment: 'Packages cannot import from apps - keeps packages reusable',
severity: 'error',
from: {
path: '^packages/',
},
to: {
path: '^apps/',
},
},
],
options: {
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',
},
enhancedResolveOptions: {
exportsFields: ['exports'],
conditionNames: ['import', 'require', 'node', 'default'],
},
reporterOptions: {
dot: {
collapsePattern: 'node_modules/[^/]+',
},
archi: {
collapsePattern: '^(packages|apps)/[^/]+|node_modules/[^/]+',
},
text: {
highlightFocused: true,
},
},
},
};

View file

@ -1,80 +0,0 @@
# =============================================================================
# TPMJS Environment Variables
# =============================================================================
# Copy this file to .env.local and fill in the values.
# NEVER commit .env files with real secrets!
#
# Required variables are marked with [REQUIRED]
# Optional variables are marked with [OPTIONAL]
# =============================================================================
# -----------------------------------------------------------------------------
# Database [REQUIRED]
# -----------------------------------------------------------------------------
# Neon PostgreSQL connection string (get from https://console.neon.tech)
DATABASE_URL="postgresql://user:password@host/database?sslmode=require"
DATABASE_URL_UNPOOLED="postgresql://user:password@host/database?sslmode=require"
# -----------------------------------------------------------------------------
# Authentication [REQUIRED for auth features]
# -----------------------------------------------------------------------------
# Better Auth secret - generate with: openssl rand -base64 32
BETTER_AUTH_SECRET="your-32-char-minimum-secret-here"
# Base URL for auth callbacks (optional, auto-detected in most cases)
BETTER_AUTH_URL="http://localhost:3000"
# -----------------------------------------------------------------------------
# Cron Jobs [REQUIRED for sync endpoints]
# -----------------------------------------------------------------------------
# Secret for authenticating Vercel Cron requests - generate with: openssl rand -hex 32
CRON_SECRET="your-64-char-hex-secret-here"
# -----------------------------------------------------------------------------
# API Key Encryption [REQUIRED for API key features]
# -----------------------------------------------------------------------------
# Secret for encrypting user API keys - generate with: openssl rand -base64 32
API_KEY_ENCRYPTION_SECRET="your-encryption-secret-here"
# -----------------------------------------------------------------------------
# External Services [OPTIONAL]
# -----------------------------------------------------------------------------
# Resend - for sending emails (https://resend.com)
RESEND_API_KEY="re_your_resend_api_key"
# OpenAI - for AI features (https://platform.openai.com)
OPENAI_API_KEY="sk-your-openai-api-key"
# Vercel KV - for rate limiting (auto-configured on Vercel)
KV_REST_API_URL="https://your-kv-instance.kv.vercel-storage.com"
KV_REST_API_TOKEN="your-kv-token"
# -----------------------------------------------------------------------------
# Executor Services [OPTIONAL]
# -----------------------------------------------------------------------------
# Railway executor for tool execution
RAILWAY_EXECUTOR_URL="https://your-railway-service.up.railway.app"
# -----------------------------------------------------------------------------
# Discord Integration [OPTIONAL]
# -----------------------------------------------------------------------------
DISCORD_SUMMARY_AGENT_ID="your-agent-id"
DISCORD_GUILD_ID="your-guild-id"
DISCORD_SUMMARY_CHANNEL_ID="your-channel-id"
# -----------------------------------------------------------------------------
# Public Variables (safe to expose to browser)
# -----------------------------------------------------------------------------
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
# -----------------------------------------------------------------------------
# Development/Testing [OPTIONAL]
# -----------------------------------------------------------------------------
NODE_ENV="development"
# Integration test credentials (only for test environment)
# INTEGRATION_TEST_SESSION_TOKEN="test-session-token"
# INTEGRATION_TEST_API_KEY="test-api-key"
# INTEGRATION_TEST_USER_ID="test-user-id"
# INTEGRATION_TEST_USERNAME="test-username"
# TEST_BASE_URL="http://localhost:3000"

View file

@ -1,12 +0,0 @@
# Allowed patterns for git-secrets (false positive exclusions)
# These are documentation examples, not real secrets
# Example API keys in documentation
tpmjs_sk_your_api_key_here
tpmjs_sk_your_api_key
tpmjs_sk_xxx
tpmjs_sk_abc123
tpmjs_sk_xxxxxxxxxxxxxxxxxxxx
# Ellipsized examples in docs (e.g., "tpmjs_sk_abc1...")
tpmjs_sk_[a-z0-9]+\.\.\.

View file

@ -1,66 +0,0 @@
name: Auto-Close Published Issues
on:
schedule:
# Run every hour to check for issues to close
- cron: '0 * * * *'
workflow_dispatch: # Allow manual trigger
jobs:
auto-close:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Close published issues older than 24h
uses: actions/github-script@v7
with:
script: |
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'published',
state: 'open',
per_page: 100
});
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
for (const issue of issues) {
// Find when 'published' label was added
const { data: events } = await github.rest.issues.listEvents({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
const publishedEvent = events
.filter(e => e.event === 'labeled' && e.label?.name === 'published')
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
if (publishedEvent) {
const labeledAt = new Date(publishedEvent.created_at);
if (labeledAt < twentyFourHoursAgo) {
console.log(`Closing issue #${issue.number} - published ${labeledAt.toISOString()}`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Auto-closing after 24 hours. The tool has been published successfully. Reopen if you encounter any issues.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'completed'
});
}
}
}

View file

@ -1,72 +0,0 @@
name: Build Omega Mac
on:
workflow_dispatch:
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
- name: Resolve dependencies
working-directory: apps/omega-mac
run: swift package resolve
- name: Build release
working-directory: apps/omega-mac
run: swift build -c release
- name: Package .app bundle
working-directory: apps/omega-mac
run: |
mkdir -p OmegaMac.app/Contents/MacOS
mkdir -p OmegaMac.app/Contents/Resources
cp .build/release/OmegaMac OmegaMac.app/Contents/MacOS/OmegaMac
cat > OmegaMac.app/Contents/Info.plist << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>OmegaMac</string>
<key>CFBundleIdentifier</key>
<string>com.tpmjs.omega-mac</string>
<key>CFBundleName</key>
<string>Omega</string>
<key>CFBundleDisplayName</key>
<string>Omega</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.network.client</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
PLIST
codesign --force --sign - OmegaMac.app
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: OmegaMac
path: apps/omega-mac/OmegaMac.app

View file

@ -22,15 +22,12 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm build
- name: Lint
run: pnpm lint
@ -48,7 +45,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
cache: 'pnpm'
- name: Install dependencies
@ -68,7 +65,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
cache: 'pnpm'
- name: Install dependencies
@ -88,7 +85,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
cache: 'pnpm'
- name: Install dependencies
@ -96,46 +93,3 @@ jobs:
- name: Build
run: pnpm build
architecture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm build
- name: Check architecture
run: pnpm check-architecture
deadcode:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Find dead code
run: pnpm find-deadcode || true

View file

@ -1,44 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

View file

@ -1,96 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
pull_request_review:
types: [submitted]
jobs:
# Standard Claude trigger - responds to @claude mentions
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'assigned') && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: write # Push branches, create commits
pull-requests: write # Create and manage PRs
issues: write # Manage labels, close issues
id-token: write
actions: read # Read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
actions: read
# Label-triggered Claude - for tool-request pipeline
claude-label-trigger:
if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'claude-working'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get prompt from issue comments
id: get-prompt
uses: actions/github-script@v7
with:
script: |
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100
});
// Find the most recent comment with @claude
const claudeComments = comments.data.filter(c => c.body.includes('@claude'));
if (claudeComments.length > 0) {
const latestComment = claudeComments[claudeComments.length - 1];
core.setOutput('prompt', latestComment.body);
core.setOutput('found', 'true');
} else {
core.setOutput('found', 'false');
core.setFailed('No @claude comment found in issue');
}
- name: Run Claude Code
if: steps.get-prompt.outputs.found == 'true'
uses: anthropics/claude-code-action@v1
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
actions: read
prompt: ${{ steps.get-prompt.outputs.prompt }}
# Allow gh CLI for issue management, npm for publishing
claude_args: '--allowedTools "Bash(gh:*)" "Bash(npm:*)" "Bash(pnpm:*)" "Bash(git:*)"'

View file

@ -1,48 +0,0 @@
name: Discord Daily Summary
on:
schedule:
# Run daily at 9 AM UTC
- cron: '0 9 * * *'
workflow_dispatch:
# Allow manual trigger
jobs:
post-summary:
runs-on: ubuntu-latest
steps:
- name: Generate conversation ID with date
id: conv-id
run: |
# Create a date-based conversation ID like "discord-summary-2026-01-11"
CONV_ID="discord-summary-$(date -u +%Y-%m-%d)"
echo "conv_id=$CONV_ID" >> $GITHUB_OUTPUT
echo "Generated conversation ID: $CONV_ID"
- name: Trigger Discord Summary Agent
env:
TPMJS_API_KEY: ${{ secrets.TPMJS_API_KEY }}
run: |
echo "Triggering agent with conversation: ${{ steps.conv-id.outputs.conv_id }}"
# POST to the agent conversation endpoint
# Uses username/agent-slug URL format: /api/{username}/agents/{agent-slug}/conversation/{conv-id}
# Agent: ajax/tpmjs-discord
# Requires API key with agent:chat scope
RESPONSE=$(curl -s -X POST \
"https://tpmjs.com/api/ajax/agents/tpmjs-discord/conversation/${{ steps.conv-id.outputs.conv_id }}" \
-H "Authorization: Bearer $TPMJS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Read the Discord server (guild ID 1349727923434815519) for the past 24 hours, excluding bots. Then post a detailed summary with an embed to channel 1442666515425132644. Include key discussions, announcements, and any action items."
}' \
--max-time 300)
echo "Response received"
# The response is SSE, so we just check if we got something back
if [ -z "$RESPONSE" ]; then
echo "Error: No response from agent"
exit 1
fi
echo "Summary triggered successfully"

View file

@ -1,279 +0,0 @@
name: Endpoint Health Check
on:
schedule:
# Run every 5 minutes
- cron: '*/5 * * * *'
workflow_dispatch:
inputs:
verbose:
description: 'Enable verbose output'
required: false
default: 'false'
type: boolean
env:
BASE_URL: ${{ secrets.VERCEL_PRODUCTION_URL || 'https://tpmjs.com' }}
# Test data
TEST_USERNAME: ajax
TEST_COLLECTION_SLUG: ajax-collection-tbc
jobs:
health-check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Setup
run: |
echo "Starting health checks at $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "Base URL: $BASE_URL"
- name: Check Basic Health Endpoint
id: basic-health
run: |
echo "Testing: GET /api/health"
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/health" --connect-timeout 10 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY"
fi
if [ "$HTTP_CODE" -eq 200 ]; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ Basic health check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ Basic health check failed with status $HTTP_CODE"
fi
- name: Check Database Health
id: db-health
run: |
echo "Testing: GET /api/tools (database connectivity)"
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/tools?limit=1" --connect-timeout 10 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY"
fi
if [ "$HTTP_CODE" -eq 200 ]; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ Database health check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ Database health check failed with status $HTTP_CODE"
fi
- name: Check Platform Stats API
id: stats-api
run: |
echo "Testing: GET /api/stats"
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/stats" --connect-timeout 10 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY" | head -c 500
fi
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"success":true'; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ Platform stats API check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ Platform stats API check failed with status $HTTP_CODE"
fi
- name: Check MCP HTTP Transport - Initialize
id: mcp-http-init
run: |
echo "Testing: POST /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http (initialize)"
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.INTEGRATION_TEST_API_KEY }}" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
--connect-timeout 15 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY"
fi
# Check for successful JSON-RPC response
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"result"'; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ MCP HTTP initialize check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ MCP HTTP initialize check failed"
fi
- name: Check MCP HTTP Transport - Tools List
id: mcp-http-tools
run: |
echo "Testing: POST /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http (tools/list)"
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.INTEGRATION_TEST_API_KEY }}" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
--connect-timeout 15 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY" | head -c 500
fi
# Check for successful JSON-RPC response with tools
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"tools"'; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ MCP HTTP tools/list check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ MCP HTTP tools/list check failed"
fi
- name: Check MCP SSE Transport
id: mcp-sse
run: |
echo "Testing: POST /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/sse (initialize)"
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/sse" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.INTEGRATION_TEST_API_KEY }}" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
--connect-timeout 15 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY"
fi
# Check for SSE response with data prefix
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q 'data:'; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ MCP SSE check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ MCP SSE check failed"
fi
- name: Check MCP Server Info (GET)
id: mcp-info
run: |
echo "Testing: GET /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http"
RESPONSE=$(curl -s -w "\n%{http_code}" \
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http" \
--connect-timeout 10 --max-time 20)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY"
fi
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"protocol":"mcp"'; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ MCP server info check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ MCP server info check failed"
fi
- name: Check Tool Health Stats
id: tool-health-stats
run: |
echo "Testing: GET /api/stats/health"
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/stats/health" --connect-timeout 10 --max-time 30)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
if [ "${{ inputs.verbose }}" = "true" ]; then
echo "Response: $BODY" | head -c 500
fi
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"success":true'; then
echo "status=pass" >> $GITHUB_OUTPUT
echo "✅ Tool health stats check passed"
else
echo "status=fail" >> $GITHUB_OUTPUT
echo "❌ Tool health stats check failed"
fi
- name: Report Health Status to API
if: always()
run: |
# Collect all results
RESULTS=$(cat << EOF
{
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"source": "github-actions",
"runId": "${{ github.run_id }}",
"checks": {
"basic_health": "${{ steps.basic-health.outputs.status }}",
"database": "${{ steps.db-health.outputs.status }}",
"stats_api": "${{ steps.stats-api.outputs.status }}",
"mcp_http_init": "${{ steps.mcp-http-init.outputs.status }}",
"mcp_http_tools": "${{ steps.mcp-http-tools.outputs.status }}",
"mcp_sse": "${{ steps.mcp-sse.outputs.status }}",
"mcp_info": "${{ steps.mcp-info.outputs.status }}",
"tool_health_stats": "${{ steps.tool-health-stats.outputs.status }}"
}
}
EOF
)
echo "Health Check Results:"
echo "$RESULTS" | jq .
# Report to the health status API if secret is available
if [ -n "${{ secrets.CRON_SECRET }}" ]; then
curl -s -X POST "$BASE_URL/api/health/report" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-d "$RESULTS" || true
fi
- name: Summary
if: always()
run: |
echo "## Health Check Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Endpoint | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Basic Health | ${{ steps.basic-health.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Database | ${{ steps.db-health.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Platform Stats | ${{ steps.stats-api.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| MCP HTTP Init | ${{ steps.mcp-http-init.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| MCP HTTP Tools | ${{ steps.mcp-http-tools.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| MCP SSE | ${{ steps.mcp-sse.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| MCP Server Info | ${{ steps.mcp-info.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Tool Health Stats | ${{ steps.tool-health-stats.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
- name: Fail if any check failed
if: |
steps.basic-health.outputs.status == 'fail' ||
steps.db-health.outputs.status == 'fail' ||
steps.mcp-http-init.outputs.status == 'fail' ||
steps.mcp-http-tools.outputs.status == 'fail' ||
steps.mcp-sse.outputs.status == 'fail'
run: |
echo "One or more critical health checks failed!"
exit 1

View file

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

View file

@ -1,135 +0,0 @@
name: Integration Tests
on:
push:
branches: [main]
workflow_dispatch:
inputs:
verbose:
description: 'Run tests in verbose mode'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'
concurrency:
group: integration-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
integration-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- 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: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm build
- name: Cleanup orphaned test data (pre-test)
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
run: pnpm --filter=@tpmjs/web test:cleanup-orphans
- name: Setup OpenAI key for test user
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY_ENCRYPTION_SECRET: ${{ secrets.API_KEY_ENCRYPTION_SECRET }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
run: pnpm --filter=@tpmjs/web test:setup-openai-key
- name: Wait for API
run: |
echo "Checking if API is available at $TEST_BASE_URL..."
for i in {1..30}; do
if curl -sf "$TEST_BASE_URL/api/health" > /dev/null 2>&1; then
echo "✅ API is available"
exit 0
fi
echo "Attempt $i/30: API not ready yet, waiting..."
sleep 2
done
echo "❌ API is not available after 60 seconds"
exit 1
env:
TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }}
- name: Run integration tests
env:
INTEGRATION_TESTS: 'true'
TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
INTEGRATION_TEST_USERNAME: ${{ secrets.INTEGRATION_TEST_USERNAME }}
INTEGRATION_TEST_SESSION_TOKEN: ${{ secrets.INTEGRATION_TEST_SESSION_TOKEN }}
INTEGRATION_TEST_API_KEY: ${{ secrets.INTEGRATION_TEST_API_KEY }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
run: |
if [ "${{ github.event.inputs.verbose }}" = "true" ]; then
pnpm --filter=@tpmjs/web test:integration -- --reporter=verbose
else
pnpm --filter=@tpmjs/web test:integration
fi
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-test-results
path: |
apps/web/test-results/
apps/web/coverage/
retention-days: 7
cleanup:
runs-on: ubuntu-latest
needs: integration-tests
if: always()
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
- 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: pnpm install --frozen-lockfile
- name: Build @tpmjs/db
run: pnpm --filter=@tpmjs/db build
- name: Cleanup orphaned test data
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
run: pnpm --filter=@tpmjs/web test:cleanup-orphans

View file

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

View file

@ -1,18 +0,0 @@
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"

View file

@ -1,114 +0,0 @@
name: Sync Tool Enrichment
on:
schedule:
# Run every 2 minutes
- cron: '*/2 * * * *'
workflow_dispatch:
jobs:
sync-enrich:
runs-on: ubuntu-latest
steps:
- name: Trigger enrichment sync
id: sync
run: |
# Call the sync API and capture response
response=$(curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/enrich" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-f -s -S)
echo "Response: $response"
# Extract data using jq
enriched=$(echo "$response" | jq -r '.data.enriched')
discovered=$(echo "$response" | jq -r '.data.discovered')
skipped=$(echo "$response" | jq -r '.data.skipped')
errors=$(echo "$response" | jq -r '.data.errors')
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 "⚠️ ENRICHMENT 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 "enriched=$enriched" >> $GITHUB_OUTPUT
echo "discovered=$discovered" >> $GITHUB_OUTPUT
echo "skipped=$skipped" >> $GITHUB_OUTPUT
echo "errors=$errors" >> $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
echo "• $errorSummary" > /tmp/error_summary.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
error_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
# Build fields array dynamically
base_fields='[
{ "name": "🔧 Enriched", "value": "${{ steps.sync.outputs.enriched }}", "inline": true },
{ "name": "🔍 Discovered", "value": "${{ steps.sync.outputs.discovered }}", "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 }} Tool Enrichment Sync" \
--argjson color ${{ steps.sync.outputs.status_color }} \
--argjson baseFields "$base_fields" \
--arg error_text "$error_text" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
'
{
embeds: [{
title: $title,
color: $color,
fields: (
$baseFields +
(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"

View file

@ -1,131 +0,0 @@
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"

View file

@ -1,97 +0,0 @@
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: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
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"

View file

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

View file

@ -1,95 +0,0 @@
name: Sync Single Package
on:
workflow_dispatch:
inputs:
packageName:
description: 'NPM package name to sync (e.g., fbx2vrma-converter)'
required: true
type: string
jobs:
sync-package:
runs-on: ubuntu-latest
steps:
- name: Sync package
id: sync
run: |
echo "Syncing package: ${{ inputs.packageName }}"
# Call the sync API and capture response
response=$(curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/package" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-d '{"packageName": "${{ inputs.packageName }}"}' \
-s -S)
echo "Response: $response"
# Check if sync was successful
success=$(echo "$response" | jq -r '.success')
if [ "$success" = "true" ]; then
echo "status_emoji=✅" >> $GITHUB_OUTPUT
echo "status_color=5763719" >> $GITHUB_OUTPUT
echo "status_text=Success" >> $GITHUB_OUTPUT
# Extract data
packageId=$(echo "$response" | jq -r '.data.packageId')
version=$(echo "$response" | jq -r '.data.version')
toolCount=$(echo "$response" | jq -r '.data.toolCount')
tools=$(echo "$response" | jq -r '.data.tools | join(", ")')
author=$(echo "$response" | jq -r '.data.author')
echo "packageId=$packageId" >> $GITHUB_OUTPUT
echo "version=$version" >> $GITHUB_OUTPUT
echo "toolCount=$toolCount" >> $GITHUB_OUTPUT
echo "tools=$tools" >> $GITHUB_OUTPUT
echo "author=$author" >> $GITHUB_OUTPUT
else
echo "status_emoji=❌" >> $GITHUB_OUTPUT
echo "status_color=15158332" >> $GITHUB_OUTPUT
echo "status_text=Failed" >> $GITHUB_OUTPUT
error=$(echo "$response" | jq -r '.error // "Unknown error"')
echo "error=$error" >> $GITHUB_OUTPUT
fi
- name: Send Discord notification
if: always()
run: |
if [ "${{ steps.sync.outputs.status_text }}" = "Success" ]; then
fields='[
{ "name": "📦 Package", "value": "${{ inputs.packageName }}", "inline": true },
{ "name": "🏷️ Version", "value": "${{ steps.sync.outputs.version }}", "inline": true },
{ "name": "👤 Author", "value": "${{ steps.sync.outputs.author }}", "inline": true },
{ "name": "🔧 Tools", "value": "${{ steps.sync.outputs.toolCount }}", "inline": true },
{ "name": "📋 Tool Names", "value": "${{ steps.sync.outputs.tools }}", "inline": false },
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
]'
else
fields='[
{ "name": "📦 Package", "value": "${{ inputs.packageName }}", "inline": true },
{ "name": "❌ Error", "value": "${{ steps.sync.outputs.error }}", "inline": false },
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
]'
fi
payload=$(jq -n \
--arg title "${{ steps.sync.outputs.status_emoji }} Package Sync: ${{ inputs.packageName }}" \
--argjson color ${{ steps.sync.outputs.status_color }} \
--argjson fields "$fields" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
'
{
embeds: [{
title: $title,
color: $color,
fields: $fields,
timestamp: $timestamp
}]
}')
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d "$payload"

View file

@ -1,267 +0,0 @@
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 "════════════════════════════════════════"

View file

@ -1,89 +0,0 @@
name: Tool Request Pipeline
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to process'
required: true
type: number
jobs:
trigger-claude:
# Only run when 'tool-request' label is added or manual dispatch
if: github.event.label.name == 'tool-request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add working label
uses: actions/github-script@v7
with:
script: |
const issueNumber = context.issue?.number || ${{ inputs.issue_number || 0 }};
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['claude-working']
});
- name: Comment to trigger Claude
uses: actions/github-script@v7
with:
script: |
const issueNumber = context.issue?.number || ${{ inputs.issue_number || 0 }};
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber
});
const lines = [
'@claude Please implement this tool request.',
'',
'## Instructions',
'1. Read the pipeline specification at `.claude/pipelines/tool-request.md`',
'2. Follow all steps: analyze, design, implement, validate, test, publish',
'3. Update labels as you progress (remove `claude-working`, add `published` or `validation-failed`)',
'4. Post full changelog when complete',
'5. This issue will auto-close 24h after successful publish',
'',
'## Issue Context',
`- Issue #${issueNumber}`,
`- Author: @${issue.user.login}`,
`- Created: ${issue.created_at}`,
'',
'Begin implementation.'
];
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: lines.join('\n')
});
# Auto-close published issues after 24 hours
auto-close:
runs-on: ubuntu-latest
if: github.event.label.name == 'published'
permissions:
issues: write
steps:
- name: Schedule auto-close
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'This issue will auto-close in 24 hours. Reply if you have feedback or issues with the published tool.'
});
# Separate workflow handles the actual auto-close via scheduled job
# See: .github/workflows/auto-close-published.yml

View file

@ -1,69 +0,0 @@
name: Update Documentation
on:
push:
branches:
- main
paths:
- 'packages/**'
- 'apps/**'
- 'templates/**'
- '!**/*.md'
jobs:
update-docs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Get changed files
id: changed
run: |
echo "files=$(git diff --name-only HEAD~1 HEAD | tr '\n' ' ')" >> $GITHUB_OUTPUT
- name: Run Claude Code
uses: anthropics/claude-code-action@beta
continue-on-error: true
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Analyze the recent code changes and update any relevant documentation.
Changed files: ${{ steps.changed.outputs.files }}
Tasks:
1. Read the changed files to understand what was modified
2. Check if any README files, doc pages, or code comments need updating
3. Update documentation to reflect the code changes
4. Keep docs concise and accurate
Focus on:
- API changes that affect usage examples
- New features that need documentation
- Changed behavior that affects existing docs
- Executor template documentation (templates/vercel-executor/README.md)
- Package READMEs in packages/
Only make changes if documentation is actually out of sync with code.
If no documentation updates are needed, do nothing.
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: auto-update documentation based on code changes'
title: 'docs: Auto-update documentation'
body: |
This PR was automatically generated by Claude Code to update documentation based on recent code changes.
Please review the changes before merging.
branch: auto-docs-update
delete-branch: true

53
.gitignore vendored
View file

@ -15,12 +15,7 @@ dist
# misc
.DS_Store
# video files
*.mp4
*.webm
*.mov
*.avi
*.pem
# debug
npm-debug.log*
@ -28,57 +23,18 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment files - NEVER commit secrets
# local env files
.env*.local
.env
.env.*
!.env.example
.env.local
.env.development
.env.development.local
.env.test
.env.test.local
.env.production
.env.production.local
.env.staging
.env.vercel*
# secret files
*.pem
*.key
*.p12
*.pfx
credentials.json
secrets.json
*_secret*
*_credentials*
# turbo
.turbo
# ai sdk devtools
.devtools
# typescript
*.tsbuildinfo
# ide
.idea
.agent/
.agents/
.continue/
.cursor/
.windsurf/
# temporary analysis docs
COMPREHENSIVE_ANALYSIS.md
PLAN.md
REGISTRY_TOOLS_ANALYSIS.md
USER_ACCOUNT_ANALYSIS_*.md
*.skill
# symlinked skill dirs (source is .agents/ which is gitignored)
/skills/
.claude/skills/skill-creator
# storybook
storybook-static
@ -86,6 +42,3 @@ storybook-static
# changesets
.changeset/*.md
!.changeset/README.md
.vercel
packages/tool-ideas/data/tools-export.json
.env*.local

View file

@ -1,49 +0,0 @@
# Secret patterns for git-secrets
# Run `git secrets --add-provider -- cat .gitsecrets` to load these patterns
# Or manually add with `git secrets --add '<pattern>'`
# =============================================================================
# TPMJS-specific patterns
# =============================================================================
# TPMJS API keys (format: tpmjs_sk_<base64>)
tpmjs_sk_[A-Za-z0-9_-]+
# =============================================================================
# Database credentials
# =============================================================================
# Neon database passwords (format: npg_<alphanumeric>)
npg_[A-Za-z0-9]+
# PostgreSQL connection strings with embedded passwords
postgresql://[^:]+:[^@]+@.*neon
# Generic database URLs with passwords
DATABASE_URL=.*://[^:]+:[^@]+@
# =============================================================================
# Generic secret patterns
# =============================================================================
# Long hex strings (API keys, tokens) - 64 chars like CRON_SECRET
[a-f0-9]{64}
# JWT tokens (common format)
eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*
# Generic API key patterns
[Aa][Pp][Ii][-_]?[Kk][Ee][Yy].*['"][A-Za-z0-9_-]{20,}['"]
# =============================================================================
# Cloud provider patterns (via --register-aws)
# =============================================================================
# AWS patterns are automatically registered with `git secrets --register-aws`
# - AWS Access Key IDs: AKIA[0-9A-Z]{16}
# - AWS Secret Access Keys
# =============================================================================
# Allowed patterns (false positive exclusions)
# =============================================================================
# Add allowed patterns with: git secrets --add --allowed '<pattern>'
# Example: git secrets --add --allowed 'example\.com'

31
.ignore
View file

@ -1,31 +0,0 @@
# Ignore patterns for OpenCode
# These directories are excluded from search to reduce noise and improve relevance
# Build outputs and caches
**/dist/**
**/.next/**
**/.turbo/**
**/coverage/**
**/.cache/**
**/node_modules/**
# Generated files
**/.DS_Store/**
**/*.log
**/tmp/**
# Lock files (unless explicitly requested)
**/pnpm-lock.yaml
**/package-lock.json
**/yarn.lock
# Environment files
**/.env*
**/.envrc
# IDE files
**/.vscode/**
**/.idea/**
# OS files
**/Thumbs.db

1
.nvmrc
View file

@ -1 +0,0 @@
22

View file

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

View file

@ -2,7 +2,8 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit"
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true

210
AGENTS.md
View file

@ -1,210 +0,0 @@
# TPMJS OpenCode Configuration
This file contains project-specific rules and guidance for OpenCode agents working in the TPMJS monorepo.
## Repository Overview
TPMJS is a Turborepo monorepo for AI tool discovery and registry. Key characteristics:
- **Package Manager**: pnpm with workspace configuration
- **Build System**: Turborepo for task orchestration
- **Main App**: Next.js 16 App Router (`apps/web`)
- **Component Library**: `.ts`-only React components (`packages/ui`)
- **Database**: Prisma with PostgreSQL (`packages/db`)
- **Tool Registry**: npm package discovery and metadata sync
## Core Commands (Always Use These)
```bash
# Development
pnpm dev # Start all dev servers
pnpm --filter=@tpmjs/web dev # Start web app only
# Building (Respects Dependencies)
pnpm build # Build all packages
pnpm --filter=@tpmjs/ui build # Build specific package
pnpm --filter=@tpmjs/web... build # Build web + all dependencies
# Testing & Quality
pnpm test # Run all tests
pnpm lint # Lint all packages
pnpm format # Format with Biome
pnpm type-check # TypeScript checking
```
## Architecture Rules (Critical)
### Module Boundaries
- **Apps** (`apps/*`) can only import from published packages (`@tpmjs/*`)
- **Packages** (`packages/*`) cannot import from apps
- **UI Package** (`packages/ui`) cannot import from utils (stays dependency-free)
- **No barrel exports** - always import directly: `@tpmjs/ui/Button/Button`
### Component Usage
**ALWAYS use `@tpmjs/ui` components instead of raw HTML:**
```typescript
// Good
import { Button } from '@tpmjs/ui/Button/Button';
import { Input } from '@tpmjs/ui/Input/Input';
// Bad
<button onClick={handleClick}>Submit</button>
<input value={value} onChange={onChange} />
```
### TypeScript Configuration
- All packages extend from `@tpmjs/tsconfig`
- Strict mode enabled
- Composite projects for proper dependency resolution
## Package Structure
### Published Packages (@tpmjs scope)
- `@tpmjs/ui` - React component library (.ts-only, createElement)
- `@tpmjs/utils` - Utility functions (cn, format, etc.)
- `@tpmjs/types` - Shared TypeScript types and Zod schemas
- `@tpmjs/env` - Environment variable validation with Zod
### Internal Tooling (Private)
- `@tpmjs/config` - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
- `@tpmjs/test` - Vitest shared configuration
- `@tpmjs/mocks` - MSW mock server for testing
- `@tpmjs/storybook` - Component documentation
### Applications
- `@tpmjs/web` - Next.js 16 App Router (main website)
- `@tpmjs/playground` - Tool testing playground
## Development Workflow
### Before Making Changes
1. Run `pnpm type-check` to ensure clean state
2. Check existing patterns in similar files
3. Use `@tpmjs/ui` components for any UI changes
### After Making Changes
1. `pnpm lint` - Check linting
2. `pnpm type-check` - Verify TypeScript
3. `pnpm test` - Run tests if applicable
4. `pnpm format` - Auto-format with Biome
### Database Changes
If modifying Prisma schema:
```bash
pnpm --filter=@tpmjs/db db:generate # Regenerate client
pnpm --filter=@tpmjs/db db:push # Apply changes (dev)
```
## Tool Development
### Tool Package Structure
Tools live in `packages/tools/*` with this pattern:
```
packages/tools/tool-name/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Main export
│ ├── tool.ts # Tool definition
│ └── implementation.ts # Actual logic
├── README.md
└── examples/
└── basic.ts
```
### Tool Metadata
Tools must have proper `tpmjs` field in package.json:
```json
{
"tpmjs": {
"category": "text-analysis",
"tier": "rich",
"description": "Tool description"
}
}
```
## Quality Standards
### Code Quality
- No `any` types or `@ts-ignore`
- Strict TypeScript compliance
- Proper error handling with try/catch
- Meaningful variable names
### Testing
- Unit tests for utilities
- Integration tests for API routes
- Component tests for UI changes
- Use Vitest + Testing Library
### Documentation
- README for all packages
- JSDoc for public APIs
- Examples for tool usage
- Type definitions for all public interfaces
## Common Patterns
### API Routes
```typescript
import { NextResponse } from 'next/server';
import { prisma } from '@tpmjs/db';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
export async function GET() {
try {
// Implementation
return NextResponse.json({ success: true, data });
} catch (error) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 500 }
);
}
}
```
### Component Pattern
```typescript
import { createElement } from 'react';
import { cn } from '@tpmjs/utils';
interface ButtonProps {
onClick?: () => void;
children: React.ReactNode;
className?: string;
}
export function Button({ onClick, children, className }: ButtonProps) {
return createElement('button', {
onClick,
className: cn('default-styles', className),
}, children);
}
```
## What NOT to Do
- **Never edit lockfiles** unless explicitly requested
- **Never use barrel exports** (`index.ts` files)
- **Never suppress TypeScript errors** with `as any` or `@ts-ignore`
- **Never use raw HTML elements** when `@tpmjs/ui` components exist
- **Never import from apps** in packages
- **Never commit without running** `pnpm lint` and `pnpm type-check`
## Deployment & CI
- Vercel deployment requires all CI checks to pass
- Pre-commit hooks run `format`, `lint`, and `type-check`
- Use `vercel inspect` to debug deployments
- Check `/api/health` to verify production deployments
## Getting Help
- Check existing implementations in similar packages
- Use `pnpm --filter=<package> dev` for package-specific development
- Refer to `CLAUDE.md` for detailed architectural decisions
- Look at `packages/tools/*` for tool development examples

View file

@ -1,844 +0,0 @@
# TPMJS Architecture Documentation
A comprehensive guide to the TPMJS platform architecture - from tool discovery to sandboxed execution, collections, agents, and custom executors.
---
## Table of Contents
1. [Platform Overview](#1-platform-overview)
2. [Monorepo Structure](#2-monorepo-structure)
3. [Database Layer](#3-database-layer)
4. [Tool Execution System](#4-tool-execution-system)
5. [MCP Protocol Implementation](#5-mcp-protocol-implementation)
6. [Agent System](#6-agent-system)
7. [Collection System](#7-collection-system)
8. [NPM Sync System](#8-npm-sync-system)
9. [API Layer](#9-api-layer)
10. [SDK Packages](#10-sdk-packages)
11. [UI & Frontend](#11-ui--frontend)
12. [Security & Authentication](#12-security--authentication)
---
## 1. Platform Overview
TPMJS is a **tool registry platform** that automatically discovers, validates, and executes npm packages as AI agent tools. The platform supports multiple AI providers (OpenAI, Anthropic, Google, Groq, Mistral) and exposes tools via MCP (Model Context Protocol) for use with Claude Desktop, Cursor, and other MCP clients.
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER PRODUCTS │
├─────────────────────┬─────────────────────┬─────────────────────────────────┤
│ tpmjs.com │ SDK Packages │ MCP Protocol │
│ ───────────────── │ ───────────────── │ ───────────────────────────── │
│ • Dashboard │ • @tpmjs/types │ • Claude Desktop │
│ • Tool Browser │ • registry-search │ • Cursor │
│ • Collection Editor│ • registry-execute │ • Claude Code │
│ • Agent Builder │ │ • Any MCP Client │
│ • Playground │ │ │
└─────────────────────┴─────────────────────┴─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ API LAYER (Next.js 16) │
├─────────────────────────────────────────────────────────────────────────────┤
│ /api/tools /api/agents /api/collections /api/mcp/* /api/sync/* │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE │
├───────────────────────┬───────────────────────┬─────────────────────────────┤
│ Database │ Execution │ External │
│ ─────────────────── │ ─────────────────── │ ───────────────────────── │
│ • PostgreSQL (Neon) │ • Vercel Sandbox │ • npm Registry │
│ • Prisma ORM │ • Custom Executors │ • esm.sh CDN │
│ │ │ • GitHub API │
└───────────────────────┴───────────────────────┴─────────────────────────────┘
```
### Key Concepts
| Concept | Description |
|---------|-------------|
| **Tool** | A single executable function from an npm package |
| **Package** | An npm package containing one or more tools |
| **Collection** | A user-curated bundle of tools exposed via MCP |
| **Agent** | An AI assistant with access to tools and collections |
| **Executor** | A sandboxed environment for running tool code |
---
## 2. Monorepo Structure
TPMJS uses **Turborepo** with **pnpm** workspaces. The codebase is organized into packages and applications.
### Directory Structure
```
tpmjs/
├── apps/
│ ├── web/ # Main Next.js 16 application
│ ├── playground/ # Interactive tool testing
│ ├── tutorial/ # Tutorial application
│ └── railway-executor/ # Deno executor service
├── packages/
│ ├── ui/ # React component library (@tpmjs/ui)
│ ├── types/ # TypeScript types & Zod schemas (@tpmjs/types)
│ ├── utils/ # Utility functions (@tpmjs/utils)
│ ├── env/ # Environment validation (@tpmjs/env)
│ ├── db/ # Prisma database client (@tpmjs/db)
│ ├── npm-client/ # NPM Registry API client
│ ├── package-executor/ # Tool execution client
│ ├── config/ # Shared configs (Biome, ESLint, Tailwind, TS)
│ └── tools/ # 150+ official TPMJS tools
│ └── official/ # @tpmjs/tools-* packages
├── turbo.json # Turborepo task configuration
├── pnpm-workspace.yaml # Workspace definitions
└── vercel.json # Deployment & cron configuration
```
### Published Packages (npm @tpmjs scope)
| Package | Version | Purpose |
|---------|---------|---------|
| `@tpmjs/types` | 0.2.0 | TypeScript types and Zod validation schemas |
| `@tpmjs/utils` | 0.1.1 | Utility functions (cn, format helpers) |
| `@tpmjs/ui` | 0.1.3 | React component library (30+ components) |
| `@tpmjs/env` | 0.1.1 | Environment variable validation |
### Internal Packages
| Package | Purpose |
|---------|---------|
| `@tpmjs/db` | Prisma client and database schema |
| `@tpmjs/npm-client` | NPM Registry API client for syncing |
| `@tpmjs/package-executor` | Remote executor HTTP client |
| `@tpmjs/config` | Shared Biome, ESLint, Tailwind, TypeScript configs |
### Key Architecture Principles
1. **No Barrel Exports**: Components imported directly (`@tpmjs/ui/Button/Button`)
2. **Strict Module Boundaries**: Apps import from packages, not vice versa
3. **TypeScript Everywhere**: Strict mode with composite projects
4. **Shared Configurations**: Centralized in `packages/config/`
---
## 3. Database Layer
The database layer uses **Prisma ORM** with **PostgreSQL** (Neon) as the data store.
### Core Models
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ TOOL REGISTRY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Package (1) ──────────────────────► (N) Tool │
│ ├── npmPackageName (unique) ├── id (PK) │
│ ├── npmVersion ├── name │
│ ├── category ├── description │
│ ├── tier (minimal|rich) ├── inputSchema (JSON) │
│ ├── npmDownloadsLastMonth ├── qualityScore │
│ └── githubStars ├── importHealth │
│ └── executionHealth │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER & SOCIAL │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ User (1) ──────► (N) Agent ──────► (N) Conversation ──────► (N) Message │
│ │ │ │
│ │ └──────► (N) AgentTool │
│ │ └──────► (N) AgentCollection │
│ │ │
│ └──────► (N) Collection ──────► (N) CollectionTool │
│ │ │
│ └──────► (N) ToolLike, CollectionLike, AgentLike │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ SYNC & MONITORING │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ SyncCheckpoint SyncLog HealthCheck │
│ ├── source (unique) ├── source ├── toolId │
│ └── checkpoint (JSON) ├── status ├── importStatus │
│ ├── processed ├── executionStatus │
│ └── errors └── checkType │
│ │
│ Simulation TokenUsage StatsSnapshot │
│ ├── toolId ├── simulationId ├── date (unique) │
│ ├── status ├── inputTokens ├── totalTools │
│ └── output └── totalTokens └── healthStats │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Key Query Patterns
**1. Pagination without COUNT (limit+1 technique):**
```typescript
const tools = await prisma.tool.findMany({
take: limit + 1, // Fetch one extra to check hasMore
skip: offset,
});
const hasMore = tools.length > limit;
const actualTools = hasMore ? tools.slice(0, limit) : tools;
```
**2. Atomic Like/Unlike with Transactions:**
```typescript
const [like, updatedTool] = await prisma.$transaction([
prisma.toolLike.create({ data: { userId, toolId } }),
prisma.tool.update({
where: { id: toolId },
data: { likeCount: { increment: 1 } }
})
]);
```
**3. Upsert for Idempotent Sync Operations:**
```typescript
await prisma.package.upsert({
where: { npmPackageName: pkg.name },
create: { /* ... */ },
update: { /* ... */ }
});
```
---
## 4. Tool Execution System
The execution system provides sandboxed environments for safely running npm package tools.
### Execution Flow
```
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 1. REQUEST │────►│ 2. RESOLVE │────►│ 3. EXECUTE │────►│ 4. RESPONSE │
├───────────────┤ ├───────────────┤ ├───────────────┤ ├───────────────┤
│ SDK: │ │ Lookup tool │ │ npm install │ │ output: any │
│ registryExec │ │ by ID │ │ pkg │ │ │
│ │ │ │ │ │ │ executionTime │
│ MCP: │ │ Resolve │ │ tool.execute │ │ Ms │
│ tools/call │ │ executor │ │ (params) │ │ │
│ │ │ config │ │ │ │ success: │
│ Agent: │ │ │ │ Return │ │ boolean │
│ tool_call │ │ Build import │ │ result │ │ │
│ │ │ URL │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘
```
### Executor Types
**1. Default Executor (Vercel Sandbox)**
- Pre-configured sandbox environment
- Node.js 22, 2 vCPUs, 2 minute timeout
- Network isolated, per-request env injection
- Automatic npm install
**2. Custom URL Executor**
- User-deployed executor service
- Deploy to Vercel, Railway, AWS Lambda, or self-host
- Custom dependencies pre-installed
- Your own API keys built-in
### Executor Config Cascade
```
┌─────────────────────┐
│ System Default │ ◄─── Vercel Sandbox
│ (lowest priority) │
└─────────┬───────────┘
│ overridden by
┌─────────────────────┐
│ Collection Config │ ◄─── executorConfig on Collection
│ │
└─────────┬───────────┘
│ overridden by
┌─────────────────────┐
│ Agent Config │ ◄─── executorConfig on Agent
│ (highest priority) │
└─────────────────────┘
```
### Executor API Contract
All executors must implement:
**POST /execute-tool**
```typescript
interface ExecuteToolRequest {
packageName: string; // "@tpmjs/hello"
name: string; // "helloWorldTool"
version?: string; // "1.0.0" or "latest"
params: Record<string, unknown>;
env?: Record<string, string>;
}
interface ExecuteToolResponse {
success: boolean;
output?: unknown;
error?: string;
executionTimeMs: number;
}
```
**GET /health**
```typescript
interface HealthResponse {
status: 'ok' | 'degraded' | 'error';
version?: string;
}
```
---
## 5. MCP Protocol Implementation
TPMJS implements the **Model Context Protocol (MCP)** to expose collections as tool servers for AI clients.
### MCP Endpoints
| Transport | Endpoint | Purpose |
|-----------|----------|---------|
| HTTP | `/api/mcp/{username}/{slug}/http` | Request-response |
| SSE | `/api/mcp/{username}/{slug}/sse` | Streaming |
### JSON-RPC Methods
**initialize** - Returns server capabilities
```json
{
"protocolVersion": "2024-11-05",
"serverInfo": { "name": "TPMJS: My Collection", "version": "1.0.0" },
"capabilities": { "tools": {} }
}
```
**tools/list** - Returns available tools in collection
```json
{
"tools": [{
"name": "tpmjs-hello--helloWorldTool",
"description": "A simple hello world tool",
"inputSchema": { "type": "object", "properties": { ... } }
}]
}
```
**tools/call** - Executes a tool
```json
{
"content": [{ "type": "text", "text": "Hello World!" }]
}
```
### Tool Name Format
MCP tool names are sanitized from npm package names:
```
@tpmjs/hello + helloWorldTool → tpmjs-hello--helloWorldTool
```
---
## 6. Agent System
Agents are AI-powered assistants with multi-turn conversations and tool access.
### Agent Configuration
```typescript
interface Agent {
// Identity
id: string;
uid: string; // URL-friendly ID
name: string;
description?: string;
// Model Configuration
provider: 'OPENAI' | 'ANTHROPIC' | 'GOOGLE' | 'GROQ' | 'MISTRAL';
modelId: string; // e.g., "gpt-4o", "claude-3-5-sonnet"
systemPrompt?: string;
temperature: number; // 0-2, default 0.7
// Behavior
maxToolCallsPerTurn: number; // 1-100, default 20
maxMessagesInContext: number; // 1-100, default 10
// Visibility
isPublic: boolean;
// Executor Override
executorType?: 'default' | 'custom_url';
executorConfig?: { url: string; apiKey?: string };
// Relations
collections: AgentCollection[];
tools: AgentTool[];
}
```
### Conversation Flow
```
User Message
┌────────────────────────────────────┐
│ Save MESSAGE (role=USER) │
└────────────────┬───────────────────┘
┌────────────────────────────────────┐
│ Fetch message history │
│ (maxMessagesInContext) │
└────────────────┬───────────────────┘
┌────────────────────────────────────┐
│ Build AI SDK messages + tools │
│ • System prompt │
│ • Conversation history │
│ • Tool definitions │
└────────────────┬───────────────────┘
┌────────────────────────────────────┐
│ streamText() with tool use │
│ • SSE chunks to client │
│ • Tool calls executed │
│ • Results fed back to model │
└────────────────┬───────────────────┘
┌────────────────────────────────────┐
│ Save MESSAGE (role=ASSISTANT) │
│ Save MESSAGE (role=TOOL) for each │
│ tool call result │
└────────────────────────────────────┘
```
### SSE Event Types
| Event | Description |
|-------|-------------|
| `chunk` | Text token from AI |
| `tool_call` | AI decided to call a tool |
| `tool_result` | Tool execution completed |
| `tokens` | Token usage statistics |
| `complete` | Conversation finished |
| `error` | Error occurred |
---
## 7. Collection System
Collections are user-curated bundles of tools that can be shared and exposed via MCP.
### Collection Structure
```typescript
interface Collection {
id: string;
name: string;
slug: string; // URL-friendly, unique per user
description?: string;
isPublic: boolean;
// Executor Override (applies to all tools)
executorType?: 'default' | 'custom_url';
executorConfig?: { url: string; apiKey?: string };
// Relations
tools: CollectionTool[]; // Junction table with position, notes
}
interface CollectionTool {
toolId: string;
position: number; // User-defined ordering
note?: string; // User notes about the tool
}
```
### Collection Limits
| Limit | Value |
|-------|-------|
| Max collections per user | 50 |
| Max tools per collection | 100 |
| Max name length | 100 chars |
| Max description length | 500 chars |
### MCP Access URLs
Public collections can be accessed via MCP:
```
HTTP: https://tpmjs.com/api/mcp/{username}/{slug}/http
SSE: https://tpmjs.com/api/mcp/{username}/{slug}/sse
```
---
## 8. NPM Sync System
TPMJS automatically discovers tools from npm using multiple sync strategies.
### Sync Jobs
| Job | Schedule | Purpose |
|-----|----------|---------|
| Changes Feed | Every 2 min | Monitor npm real-time updates |
| Keyword Search | Every 15 min | Search for `tpmjs` keyword |
| Metrics | Every hour | Update downloads & quality scores |
| Health Check | Daily | Verify tool import/execution |
| Stats Snapshot | Daily | Capture historical statistics |
### Discovery Flow
```
npm Registry
├──► Changes Feed (/api/sync/changes)
│ • Polls /_changes endpoint
│ • 30 packages per run
│ • Checkpoint-based (lastSeq)
└──► Keyword Search (/api/sync/keyword)
• Searches for keyword:tpmjs
• 250 packages per run
• Backup discovery
┌────────────────────────────────────┐
│ Validate tpmjs field │
│ • Multi-tool format (new) │
│ • Legacy rich format │
│ • Legacy minimal format │
└────────────────┬───────────────────┘
┌────────────────────────────────────┐
│ Auto-discover tools │
│ • If tools[] missing/empty │
│ • Call executor listToolExports │
│ • Extract JSON schemas │
└────────────────┬───────────────────┘
┌────────────────────────────────────┐
│ Update database │
│ • Upsert Package record │
│ • Upsert Tool records │
│ • Trigger health checks │
└────────────────────────────────────┘
```
### Quality Score Calculation
```typescript
qualityScore = tierScore + downloadsScore + starsScore + richnessScore
// tierScore: 0.6 (rich) or 0.4 (minimal)
// downloadsScore: log10(downloads) / 15, max 0.2
// starsScore: log10(stars) / 10, max 0.1
// richnessScore: +0.04 (params) +0.03 (returns) +0.03 (aiAgent)
// Range: 0.00 - 1.00
```
### tpmjs Field Specification
**Multi-Tool Format (Recommended):**
```json
{
"tpmjs": {
"category": "utilities",
"tools": [
{
"name": "helloWorld",
"description": "Greets a user by name"
},
{
"name": "goodbye",
"description": "Says goodbye to a user"
}
],
"frameworks": ["vercel-ai"]
}
}
```
**Valid Categories:**
- Core: `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `utilities`, `html`, `compliance`
- Legacy: `web-scraping`, `data-processing`, `file-operations`, `communication`, `database`, `api-integration`, `image-processing`, `text-analysis`, `automation`, `ai-ml`, `monitoring`
---
## 9. API Layer
The API is built on Next.js 16 App Router with standardized response formats.
### Response Format
**Success:**
```typescript
{
success: true,
data: T,
meta: {
version: "1.0.0",
timestamp: "2025-01-11T...",
requestId: "uuid"
},
pagination?: {
limit: number,
offset: number,
count: number,
hasMore: boolean
}
}
```
**Error:**
```typescript
{
success: false,
error: {
code: "VALIDATION_ERROR" | "NOT_FOUND" | "UNAUTHORIZED" | ...,
message: "Human-readable message",
details?: { ... }
},
meta: { ... }
}
```
### Key Endpoints
| Category | Endpoint | Purpose |
|----------|----------|---------|
| **Tools** | `GET /api/tools` | List/search tools |
| | `POST /api/tools/execute/[...slug]` | Execute tool (SSE) |
| **Agents** | `GET /api/agents` | List user agents |
| | `POST /api/{username}/agents/{uid}/conversation/{convId}` | Chat with agent (SSE) |
| **Collections** | `GET /api/collections` | List user collections |
| | `POST /api/collections/[id]/tools` | Add tool to collection |
| **MCP** | `POST /api/mcp/{username}/{slug}/{transport}` | MCP protocol |
| **Sync** | `POST /api/sync/changes` | Cron: npm changes |
| **Stats** | `GET /api/stats` | Registry statistics |
### Rate Limiting
| Endpoint Type | Limit | Window |
|---------------|-------|--------|
| Default | 100 requests | 1 minute |
| Strict | 20 requests | 1 minute |
| Tool Execute | 10 requests | 1 hour |
| Conversation | 30 requests | 1 minute |
### Authentication
- **Library:** `better-auth` with Prisma adapter
- **Session:** 7-day expiry, cookie-based
- **Email:** Verification required for login
- **Protected Routes:** Check `auth.api.getSession()`
---
## 10. SDK Packages
### @tpmjs/types
Core TypeScript types and Zod validation schemas.
**Exports:**
- `./tool` - Tool and ToolParameter schemas
- `./registry` - Search result schemas
- `./tpmjs` - tpmjs field validation (validateTpmjsField)
- `./agent` - Agent configuration schemas
- `./collection` - Collection schemas
- `./user` - User profile schemas
- `./executor` - Executor request/response types
### @tpmjs/npm-client (Internal)
NPM Registry API client for sync operations.
**Functions:**
- `fetchChanges()` - Poll changes feed
- `searchByKeyword()` - Search packages
- `fetchLatestPackageWithMetadata()` - Get package info
- `fetchDownloadStats()` - Get npm downloads
- `fetchGitHubStars()` - Get GitHub stars
### @tpmjs/package-executor (Internal)
Remote executor client for tool execution.
**Functions:**
- `executePackage(packageName, functionName, params)` - Execute tool
- `clearCache()` - Clear executor cache
- `checkHealth()` - Check executor health
---
## 11. UI & Frontend
### Component Library (@tpmjs/ui)
30+ React components with no-barrel-exports architecture.
**Categories:**
- **Form:** Button, Input, Select, Checkbox, Radio, Switch, Textarea, Slider
- **Layout:** Card, Container, Section, GridContainer, Header
- **Display:** Badge, ProgressBar, Spinner, Icon, CodeBlock, Table
- **Advanced:** Tabs, AnimatedCounter, StatCard, ActivityStream, FlowDiagram
### Design System
**Color System (CSS Variables):**
```css
/* Backgrounds */
--background, --surface, --surface-secondary, --surface-elevated
/* Text */
--foreground, --foreground-secondary, --foreground-tertiary, --foreground-muted
/* Interactive */
--primary, --secondary, --accent
/* Status */
--success, --error, --warning, --info
/* Borders */
--border, --border-strong
```
**Theme Support:**
- Light mode (default)
- Dark mode (Vercel/Cursor aesthetic)
- `next-themes` provider
### Dashboard Structure
```
/dashboard
├── Overview # Quick actions, profile, activity
├── Agents # Create/manage AI agents
│ └── [id]/chat # Chat interface
├── Collections # Organize tools
├── Settings
│ └── api-keys # Manage API keys
└── Likes
├── tools
├── collections
└── agents
```
---
## 12. Security & Authentication
### Authentication Flow
```
Sign Up → Email Verification → Sign In → Session Cookie → Protected Routes
```
### API Key Storage
User API keys (OpenAI, Anthropic, etc.) are stored encrypted:
- AES-256-CBC encryption
- Unique IV per key
- Only hint (last 4 chars) visible in UI
### Rate Limiting
- **Distributed:** Vercel KV with in-memory fallback
- **Per-IP:** Based on `x-forwarded-for`, `x-real-ip`, or `cf-connecting-ip`
- **Headers:** `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After`
### Cron Security
All sync endpoints require:
```
Authorization: Bearer {CRON_SECRET}
```
Vercel Cron automatically adds this header.
### Executor Verification
Custom executor URLs are verified:
1. HTTPS required in production
2. Private IP ranges blocked
3. Health endpoint checked
4. Test tool execution validated
---
## Quick Reference
### Environment Variables
| Variable | Required | Purpose |
|----------|----------|---------|
| `DATABASE_URL` | Yes | PostgreSQL connection |
| `BETTER_AUTH_SECRET` | Yes | Session encryption (32+ chars) |
| `CRON_SECRET` | Yes | Cron job auth (32+ chars) |
| `SANDBOX_EXECUTOR_URL` | No | Default executor URL |
| `GITHUB_TOKEN` | No | GitHub API for stars |
### Commands
```bash
# Development
pnpm dev # Run all dev servers
pnpm --filter=@tpmjs/web dev # Run web app only
# Database
pnpm --filter=@tpmjs/db db:generate # Generate Prisma client
pnpm --filter=@tpmjs/db db:push # Push schema changes
pnpm --filter=@tpmjs/db db:studio # Open Prisma Studio
# Testing
pnpm test # Run all tests
pnpm type-check # Type-check all packages
pnpm lint # Lint all packages
# Building
pnpm build # Build all packages
```
### Tech Stack
| Category | Technology |
|----------|------------|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 5.9 (strict) |
| Database | PostgreSQL + Prisma 6.19 |
| Auth | better-auth 1.4 |
| AI SDK | Vercel AI SDK 6.0 |
| Styling | Tailwind CSS 4.1 |
| Build | Turborepo + pnpm |
| Testing | Vitest + Testing Library |
| Deployment | Vercel |
---
*This documentation was auto-generated from codebase exploration. Last updated: January 2025*

340
CLAUDE.md
View file

@ -1,144 +1,242 @@
## Project Overview
# TPMJS - Tool Package Manager for AI Agents
Turborepo monorepo. pnpm workspaces. Next.js 16 App Router (`apps/web`). PostgreSQL via Prisma (`packages/db`). Deployed on Vercel. Database on Neon (via Railway for some services).
## Philosophy
## Architecture Rules
TPMJS is a registry and package manager for AI agent tools. Just as npm transformed how developers share and consume JavaScript packages, TPMJS aims to do the same for the emerging ecosystem of AI agent tooling.
1. **Use `@tpmjs/ui` components** — never raw HTML `<button>`, `<input>`, `<table>`, etc.
2. **No barrel exports** — import directly: `@tpmjs/ui/Button/Button`, not `@tpmjs/ui`
3. **Module boundaries** — apps import packages, never the reverse. UI has no deps on utils.
4. **Avoid `count()` in API routes** — use `take: limit + 1` technique for pagination (Prisma cold start is slow in serverless)
5. **All API routes need** `export const runtime = 'nodejs'` and `export const maxDuration = 60`
### The Problem We're Solving
## Essential Commands
AI agents are becoming increasingly capable, but they face a fundamental challenge: **tool discovery and selection at scale**.
```bash
pnpm install # Install deps
pnpm dev --filter=@tpmjs/web # Dev server
pnpm build # Build all
pnpm type-check # Type-check all
pnpm lint # Lint all
pnpm format # Biome format
pnpm --filter=@tpmjs/db db:generate # Regenerate Prisma client (after schema changes)
pnpm --filter=@tpmjs/db db:push # Push schema to DB (dev)
pnpm --filter=@tpmjs/db db:migrate # Create migration (prod)
pnpm --filter=@tpmjs/db db:studio # Prisma Studio GUI
1. **Context Window Limitations** - When an agent has access to 10+ tools, LLMs struggle to remember and correctly select from all available options. Tool schemas consume precious context tokens.
2. **Tool Hallucination** - Models sometimes attempt to call tools that don't exist, or use incorrect parameter schemas, leading to failed executions and poor user experiences.
3. **Static Tool Sets** - Most agent implementations hardcode their available tools at build time. There's no standard way to discover, add, or share tools dynamically.
4. **Fragmented Ecosystem** - Developers building AI agents are recreating the same tools (web search, file operations, API integrations) over and over. There's no central place to share and discover production-ready implementations.
### Our Vision
We believe AI agent development should be:
- **Elegant** - Simple APIs, clear conventions, minimal boilerplate
- **Productive** - Leverage community-built tools instead of reinventing wheels
- **Safe** - Vetted tools with clear security boundaries and permissions
## Core Concepts
### Tools
A tool is a capability that an AI agent can invoke. Tools have:
- A unique name/identifier
- A description (used for semantic search and LLM understanding)
- A parameter schema (typically defined with Zod or JSON Schema)
- An implementation function
### Registry
The registry is the central index of available tools. It enables:
- Browsing by category
- Semantic search (find tools by what they do, not just their name)
- Version management
- Usage analytics
### Meta-Tools
Meta-tools are tools that help agents work with other tools. The most important is `tool-search`, which allows an agent to query the registry and load only the tools relevant to its current task. This "search-then-execute" pattern dramatically improves accuracy and token efficiency.
## How It Works
### The Search-Then-Execute Pattern
Instead of loading all tool schemas into context upfront (expensive and error-prone), agents using TPMJS:
1. **Search** - Use the `tool-search` meta-tool to find relevant tools based on the current task
2. **Load** - Dynamically load only the matched tools into context
3. **Execute** - Make a follow-up call with the focused tool set
This pattern:
- Reduces token usage (only load what you need)
- Improves selection accuracy (smaller choice set)
- Eliminates hallucination (tools are confirmed to exist before use)
- Enables runtime flexibility (tools can be added/removed without restarts)
## Technical Details
### Compatibility
- TypeScript-first with full type safety via Zod schemas
- Compatible with Anthropic AI SDK, OpenAI, and other major providers
- Minimal footprint (~340 tokens for the meta-tool)
### Scale
- Supports registries with 1,000+ tools
- Sub-2ms search latency
- Semantic search, fuzzy matching, and category filtering
## Categories
Tools in the registry span:
- Web & APIs
- Databases
- Documents
- Images
- Email
- Calendar
- Search
- Code Execution
- Communication
- Analytics
- Security
- Workflows
## Development Notes
This project is in early development. Key areas to work on:
- [ ] Core registry API design
- [ ] Tool schema specification
- [ ] CLI for publishing and discovering tools
- [ ] SDK integrations (Anthropic, OpenAI, etc.)
- [ ] Search algorithm (semantic + fuzzy matching)
- [ ] Security model and sandboxing
- [ ] Documentation and examples
## Open Questions
1. **Trust & Security** - How do we vet tools? What sandboxing is needed?
2. **Versioning** - How do tools handle breaking changes?
3. **Monetization** - Free tier + Pro? Marketplace cuts?
4. **Governance** - Who decides what gets published? Moderation?
5. **Offline/Local** - Can tools be cached locally? Private registries?
---
*"The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers."*
---
## Monorepo Setup
This project uses a Turborepo monorepo architecture with the following structure:
### Packages
**Published to npm (@tpmjs scope):**
- `@tpmjs/ui` - React component library with .ts-only components
- `@tpmjs/utils` - Utility functions (cn, format, etc.)
- `@tpmjs/types` - Shared TypeScript types and Zod schemas
- `@tpmjs/env` - Environment variable validation with Zod
**Internal tooling (private):**
- `@tpmjs/config` - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
- `@tpmjs/eslint-config` - ESLint configuration with module boundary rules
- `@tpmjs/tailwind-config` - Tailwind configuration with design tokens
- `@tpmjs/tsconfig` - TypeScript configurations (base, nextjs, react-library)
- `@tpmjs/test` - Vitest shared configuration
- `@tpmjs/mocks` - MSW mock server for testing
- `@tpmjs/storybook` - Component documentation and showcase
### Applications
- `@tpmjs/web` - Next.js 16 App Router application (main website)
### Architecture Principles
#### 1. .ts-only React Components
All UI components use `.ts` extension instead of `.tsx` and utilize `createElement`:
```typescript
import { createElement, forwardRef } from 'react';
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => createElement('button', { ref, ...props })
);
```
## Git Hooks (Lefthook)
**Why?**
- Explicit runtime behavior
- Prevents JSX spreading anti-patterns
- Better for code generation
- Forces consideration of every prop
Pre-commit runs: format, lint, type-check. Pre-push runs: test. If hooks pass locally, CI will pass too.
#### 2. No Barrel Exports
## Vercel Build
Components are imported directly without `index.ts` files:
Build command: `cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build` (the `...` suffix builds all workspace dependencies first).
```typescript
// Good
import { Button } from '@tpmjs/ui/Button/Button';
## Debugging Production Issues
You have access to `gh`, `vercel`, and `railway` CLIs. **Always use these first** when debugging production problems rather than guessing at fixes.
### Verify Deployment Status
```bash
# Check what commit is live in production
curl -s https://tpmjs.com/api/health | jq .
# Compare with local commit
git log --oneline -1
// Bad (not allowed)
import { Button } from '@tpmjs/ui';
```
The health endpoint returns `commitSha`, `commitMessage`, and `deploymentUrl`.
**Benefits:**
- Clearer dependency graphs
- Better tree-shaking
- Prevents circular dependencies
- Explicit imports
### GitHub Actions (CI)
#### 3. Module Boundaries
ESLint enforces strict module boundaries:
- Apps can only import from published packages
- Packages cannot import from apps
- UI package cannot import from utils (stays dependency-free)
#### 4. Shared Configurations
All configuration is centralized in `packages/config/`:
- **Biome** - Formatting + basic linting
- **ESLint** - Semantic rules and module boundaries
- **Tailwind** - Design tokens and shared theme
- **TypeScript** - Multiple configs for different contexts
### Development Workflow
```bash
gh run list --limit 10 # Recent runs
gh run view <run-id> --log-failed # See failure logs
gh run view <run-id> --job <job-id> --log # Specific job logs
gh run rerun <run-id> --failed # Rerun failed jobs
gh run watch # Watch current run
gh pr checks <pr-number> # Check status on a PR
# Install dependencies
pnpm install
# Run development servers
pnpm dev
# Build all packages
pnpm build
# Run tests
pnpm test
# Lint and format
pnpm lint
pnpm format
```
### Vercel (Deployments)
### Component Development
```bash
vercel ls # List deployments
vercel inspect <deployment-url> # Build info + lambda list
vercel logs <deployment-url> # Runtime logs
vercel logs <deployment-url> --since 1h # Last hour of logs
vercel env ls # List env vars
```
1. Create component in `packages/ui/src/ComponentName/ComponentName.ts`
2. Use `.ts` extension with `createElement`
3. Add tests in `ComponentName.test.ts`
4. Export in `package.json` exports map
5. Add Storybook story in `packages/storybook/stories/`
Key things to check:
- `vercel inspect` shows lambda functions (λ) — if you only see static pages (○), API routes didn't deploy
- `vercel logs` shows runtime errors, timeouts, and cold start issues
### Publishing Flow
### Railway (Database / Services)
1. Make changes to packages
2. Create changeset: `pnpm changeset`
3. Version packages: `pnpm changeset:version`
4. Publish to npm: `pnpm changeset:publish`
5. Push with tags: `git push --follow-tags`
```bash
railway status # Current project/environment
railway logs # Service logs
railway logs --deployment <id> # Specific deployment logs
railway variables # List env vars
railway connect postgres # Connect to DB directly
railway up # Deploy current directory
```
### Tech Stack
### Debugging Workflow
1. **Identify the problem**: Is it a build failure, runtime error, or timeout?
2. **Check CI first**: `gh run list` then `gh run view <id> --log-failed`
3. **Check Vercel**: `vercel inspect <url>` to verify lambdas deployed, `vercel logs <url>` for runtime errors
4. **Check database**: `railway logs` or connect directly with `railway connect postgres`
5. **Verify the fix**: Push, watch CI with `gh run watch`, then `curl https://tpmjs.com/api/health`
### Direct Database Access
The production database is Neon PostgreSQL. Connection strings are in `.env.local` (`DATABASE_URL` for pooled, `DATABASE_URL_UNPOOLED` for direct).
**Prisma Studio** (GUI for browsing/editing data):
```bash
# Reads connection from packages/db/.env or DATABASE_URL env var
pnpm --filter=@tpmjs/db db:studio
```
**psql** (raw SQL queries):
```bash
# Connect using the unpooled URL for direct access
psql "$DATABASE_URL_UNPOOLED"
# Common queries
SELECT count(*) FROM tools;
SELECT id, name, slug, quality_score, view_count FROM tools ORDER BY view_count DESC LIMIT 20;
SELECT * FROM stats_snapshots ORDER BY date DESC LIMIT 5;
SELECT * FROM sync_logs ORDER BY created_at DESC LIMIT 10;
SELECT * FROM page_views ORDER BY date DESC LIMIT 20;
```
**One-off Prisma scripts** (when you need Prisma's type safety):
```bash
# Run a .ts script against prod DB using tsx
cd packages/db && npx tsx scripts/my-script.ts
```
**Note:** Prisma reads `.env` from `packages/db/`, not the root. If `db:studio` can't connect, ensure `DATABASE_URL` is set there or exported in your shell.
### Manual Cron Triggers
```bash
curl -X POST https://tpmjs.com/api/sync/changes -H "Authorization: Bearer $CRON_SECRET"
curl -X POST https://tpmjs.com/api/sync/keyword -H "Authorization: Bearer $CRON_SECRET"
curl -X POST https://tpmjs.com/api/sync/metrics -H "Authorization: Bearer $CRON_SECRET"
curl -X POST https://tpmjs.com/api/sync/view-rollup -H "Authorization: Bearer $CRON_SECRET"
curl -X POST https://tpmjs.com/api/sync/stats-snapshot -H "Authorization: Bearer $CRON_SECRET"
```
## Publishing Packages
```bash
pnpm changeset # Create changeset
pnpm changeset:version # Version packages
pnpm changeset:publish # Publish to npm
git push --follow-tags # Push with tags
```
- **Build System:** Turborepo
- **Package Manager:** pnpm
- **TypeScript:** Strict mode, composite projects
- **React:** v19 with .ts-only components
- **Next.js:** v16 App Router
- **Styling:** Tailwind CSS
- **Testing:** Vitest + Testing Library
- **Linting:** Biome + ESLint
- **Documentation:** Storybook
- **CI/CD:** GitHub Actions + Changesets
- **Git Hooks:** Lefthook

View file

@ -1,180 +0,0 @@
# Deployment Configuration
This document explains how to configure Vercel to only deploy when GitHub Actions CI passes.
## Overview
The project is configured to run comprehensive CI checks on every push and pull request:
- **Linting** - Code style and quality
- **Type checking** - TypeScript validation
- **Tests** - Unit and integration tests
- **Build** - Production build verification
- **Architecture** - Dependency rules validation
- **Dead code** - Unused code detection
Vercel should only deploy after all these checks pass on the main branch.
## Configuration Options
There are two ways to prevent Vercel from deploying when CI fails:
### Option 1: Vercel Deployment Protection (Recommended)
This is the simplest and most reliable approach.
1. **Enable Deployment Protection in Vercel:**
- Go to your Vercel project settings
- Navigate to **Git** → **Deployment Protection**
- Enable **"Wait for Checks to Complete"**
- This makes Vercel wait for all GitHub status checks before deploying
2. **Configure Branch Protection (GitHub):**
- Go to GitHub repository settings
- Navigate to **Branches** → **Branch protection rules**
- Add rule for `main` branch
- Enable **"Require status checks to pass before merging"**
- Select all CI jobs: `lint`, `type-check`, `test`, `build`, `architecture`, `deadcode`
- Enable **"Require branches to be up to date before merging"**
This ensures:
- ✅ PRs cannot be merged unless CI passes
- ✅ Vercel waits for CI to complete before deploying
- ✅ Production always has passing CI
### Option 2: Ignored Build Step (Advanced)
Use a custom script to check CI status before building.
1. **Add GitHub Token to Vercel:**
- Go to Vercel project settings
- Navigate to **Environment Variables**
- Add `GITHUB_TOKEN` with a Personal Access Token
- Scope: `repo:status` (read commit status)
- Apply to: Production, Preview, Development
2. **Configure Ignored Build Step:**
- Go to Vercel project settings
- Navigate to **Git** → **Ignored Build Step**
- Set custom command:
```bash
bash scripts/vercel-should-deploy.sh
```
3. **How it works:**
- Script checks if CI has passed via GitHub API
- Exit code 0 = skip build (CI failed/pending)
- Exit code 1 = proceed with build (CI passed)
- Preview deployments always proceed
- Production deployments wait for CI
## Deployment Workflow
### For Pull Requests (Preview)
1. Push commits to PR branch
2. GitHub Actions runs CI checks
3. Vercel creates preview deployment (regardless of CI status)
4. CI status is shown on PR
5. Can only merge if CI passes (branch protection)
### For Production (Main Branch)
1. PR is merged to `main`
2. GitHub Actions runs CI checks
3. **Vercel waits for CI to complete** (if Deployment Protection enabled)
4. Once CI passes, Vercel deploys to production
5. If CI fails, deployment is blocked
## CI Jobs
The following jobs must pass for deployment:
| Job | Description | Blocks Deploy |
|-----|-------------|---------------|
| `lint` | ESLint + Biome formatting | ✅ Yes |
| `type-check` | TypeScript compilation | ✅ Yes |
| `test` | Vitest unit tests | ✅ Yes |
| `build` | Production build | ✅ Yes |
| `architecture` | Dependency rules | ✅ Yes |
| `deadcode` | Unused code detection | ⚠️ Warning only |
## Manual Deployment Override
If you need to deploy even when CI fails (emergency hotfix):
1. **Temporarily disable branch protection:**
- GitHub → Settings → Branches → Edit rule
- Uncheck "Require status checks to pass"
- Merge PR
- Re-enable protection immediately after
2. **Or push directly to main** (not recommended):
```bash
git push origin main --no-verify
```
## Troubleshooting
### Vercel deploys even though CI failed
**Solution:** Enable "Deployment Protection" in Vercel settings.
### CI is stuck in pending state
**Solution:** Check GitHub Actions workflow logs. Ensure all jobs complete.
### Preview deployments are blocked
**Solution:** Preview deployments should never be blocked. Check Ignored Build Step script logic.
### Need to deploy urgently
**Solution:** Use manual override (see above), but fix CI issues immediately after.
## Best Practices
1. ✅ Always ensure CI passes before merging
2. ✅ Use preview deployments to test changes
3. ✅ Fix CI failures immediately - don't merge broken code
4. ✅ Review CI logs when checks fail
5. ❌ Don't bypass CI unless absolutely necessary
6. ❌ Don't merge with failing tests "to fix later"
## Verification
To verify the setup is working:
1. Create a PR with intentionally broken code (e.g., TypeScript error)
2. Verify CI fails
3. Verify PR cannot be merged
4. Verify Vercel deployment is blocked/skipped
5. Fix the code
6. Verify CI passes
7. Verify PR can be merged
8. Verify Vercel deploys successfully
## Environment Variables
Required environment variables in Vercel:
| Variable | Required For | Description |
|----------|--------------|-------------|
| `GITHUB_TOKEN` | Option 2 only | GitHub Personal Access Token with `repo:status` scope |
Not needed for Option 1 (Deployment Protection).
## Status Badge
Add to README.md to show CI status:
```markdown
[![CI](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml/badge.svg)](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
```
## Summary
**Recommended Setup:**
1. Enable Vercel "Deployment Protection" (wait for checks)
2. Enable GitHub branch protection for `main`
3. Require all CI jobs to pass before merging
This ensures production always has high-quality, tested code.

View file

@ -1,559 +0,0 @@
# TPMJS Design System Specification
> A technical, precise design system inspired by [turbopuffer.com](https://turbopuffer.com) - warm, monospace-driven, with generous whitespace and fieldset-style containers.
---
## Brand Direction
### Mood & Personality
- **Technical & Precise** - Engineering-focused, trustworthy, developer-first
- **Warm & Distinctive** - Not cold/corporate, the copper accent adds warmth
- **Confident & Minimal** - Let the content speak, reduce visual noise
### Reference Sites
- [turbopuffer.com](https://turbopuffer.com) - Primary inspiration
- Linear, Vercel - Secondary references for technical clarity
---
## Color Palette
### Primary Accent
```css
--color-accent: #A6592D; /* Copper/terracotta - primary brand color */
--color-accent-hover: #8B4A26; /* Darker copper for hover states */
--color-accent-light: #D4A574; /* Light copper for backgrounds/highlights */
```
### Gradient Header
```css
/* Warm gradient for top bar/hero sections */
--gradient-header: linear-gradient(135deg, #D4732A 0%, #8B3D1A 50%, #2D1810 100%);
```
### Neutral Palette
```css
/* Backgrounds */
--color-bg-primary: #FFFFFF; /* Main background */
--color-bg-secondary: #FAFAFA; /* Subtle sections */
--color-bg-elevated: #FFFFFF; /* Cards, elevated surfaces */
/* Text */
--color-text-primary: #1A1A1A; /* Primary text - near black */
--color-text-secondary: #666666; /* Secondary/muted text */
--color-text-tertiary: #999999; /* Placeholder, hints */
/* Borders */
--color-border: #E5E5E5; /* Default borders */
--color-border-strong: #CCCCCC; /* Emphasized borders */
--color-border-focus: #A6592D; /* Focus state - uses accent */
```
### Semantic Colors
```css
--color-success: #22C55E;
--color-error: #EF4444;
--color-warning: #F59E0B;
--color-info: #3B82F6;
```
### Dark Mode (Future)
```css
/* Dark mode should invert while keeping the warm accent */
--color-bg-primary-dark: #0D0D0D;
--color-bg-secondary-dark: #1A1A1A;
--color-text-primary-dark: #F5F5F5;
--color-border-dark: #333333;
```
---
## Typography
### Font Stack
**Headings & Code: Monospace**
```css
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', monospace;
```
**Body Text: Sans-serif (for longer reading)**
```css
--font-sans: 'Inter', 'IBM Plex Sans', system-ui, sans-serif;
```
### Type Scale
| Element | Font | Size | Weight | Line Height | Letter Spacing |
|---------|------|------|--------|-------------|----------------|
| H1 | Mono | 48px (3rem) | 600 | 1.1 | -0.02em |
| H2 | Mono | 36px (2.25rem) | 600 | 1.2 | -0.01em |
| H3 | Mono | 24px (1.5rem) | 600 | 1.3 | 0 |
| H4 | Mono | 20px (1.25rem) | 600 | 1.4 | 0 |
| Body Large | Sans | 18px (1.125rem) | 400 | 1.7 | 0 |
| Body | Sans | 16px (1rem) | 400 | 1.7 | 0 |
| Body Small | Sans | 14px (0.875rem) | 400 | 1.6 | 0 |
| Caption | Sans | 12px (0.75rem) | 400 | 1.5 | 0.01em |
| Code | Mono | 14px (0.875rem) | 400 | 1.6 | 0 |
### Typography Rules
1. **Headings are lowercase** - "pricing", "faq", "tools" (not "Pricing", "FAQ", "Tools")
2. **Generous line-height** - Minimum 1.6 for body text, 1.7 preferred
3. **Bold sparingly** - Use weight 600 for emphasis, not 700+
4. **Monospace for data** - Numbers, metrics, technical values always in mono
### CSS Variables
```css
/* Font families */
--font-heading: var(--font-mono);
--font-body: var(--font-sans);
--font-code: var(--font-mono);
/* Font sizes */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 2.25rem; /* 36px */
--text-4xl: 3rem; /* 48px */
/* Line heights */
--leading-tight: 1.2;
--leading-normal: 1.5;
--leading-relaxed: 1.7;
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
```
---
## Spacing
### Spacing Scale
```css
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
```
### Spacing Philosophy
- **Generous whitespace** - When in doubt, add more space
- **Vertical rhythm** - Use consistent spacing between sections (typically `--space-16` to `--space-24`)
- **Component padding** - Cards and containers use `--space-6` to `--space-8`
- **Text spacing** - Paragraphs separated by `--space-4` to `--space-6`
---
## Borders & Containers
### Border Radius
```css
--radius-none: 0; /* DEFAULT - sharp corners */
--radius-sm: 2px; /* Use sparingly for special cases */
--radius-md: 4px; /* Use sparingly for special cases */
```
**Rule: Default to 0 border-radius. Sharp corners are the brand.**
### Border Styles
**Dashed (Primary)**
```css
border: 1px dashed var(--color-border);
```
**Solid (Emphasis)**
```css
border: 2px solid var(--color-text-primary); /* Featured items */
```
### Fieldset-Style Containers
The signature container style with a label that "cuts into" the border:
```html
<fieldset class="fieldset-container">
<legend>section title</legend>
<!-- content -->
</fieldset>
```
```css
.fieldset-container {
border: 1px dashed var(--color-border);
padding: var(--space-6);
margin: 0;
}
.fieldset-container legend {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text-secondary);
padding: 0 var(--space-2);
text-transform: lowercase;
}
```
### Container Variants
| Variant | Border | Background | Use Case |
|---------|--------|------------|----------|
| Default | 1px dashed | transparent | Most containers |
| Elevated | 1px dashed | white | Cards on gray bg |
| Featured | 2px solid | white | Highlighted item |
| Ghost | none | transparent | Minimal grouping |
---
## Components
### Buttons
**Primary Button (Accent)**
```css
.btn-primary {
background: var(--color-accent);
color: white;
border: none;
padding: var(--space-3) var(--space-6);
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: background 150ms ease;
}
.btn-primary:hover {
background: var(--color-accent-hover);
}
```
**Secondary Button (Outline)**
```css
.btn-secondary {
background: transparent;
color: var(--color-text-primary);
border: 1px solid var(--color-border);
padding: var(--space-3) var(--space-6);
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: border-color 150ms ease;
}
.btn-secondary:hover {
border-color: var(--color-text-primary);
}
```
**Button Sizes**
| Size | Padding | Font Size |
|------|---------|-----------|
| sm | `--space-2` `--space-4` | `--text-xs` |
| md | `--space-3` `--space-6` | `--text-sm` |
| lg | `--space-4` `--space-8` | `--text-base` |
### Links
```css
a {
color: var(--color-text-primary);
text-decoration: underline;
text-underline-offset: 3px;
transition: opacity 150ms ease;
}
a:hover {
opacity: 0.7;
}
```
**Rule: Links are underlined, not colored.** Use underline as the primary affordance.
### Inputs
```css
.input {
width: 100%;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-none);
font-family: var(--font-mono);
font-size: var(--text-base);
background: white;
transition: border-color 150ms ease;
}
.input:focus {
outline: none;
border-color: var(--color-accent);
}
.input::placeholder {
color: var(--color-text-tertiary);
}
```
### Cards
```css
.card {
border: 1px dashed var(--color-border);
padding: var(--space-6);
background: white;
}
.card--featured {
border: 2px solid var(--color-text-primary);
}
.card__title {
font-family: var(--font-mono);
font-size: var(--text-lg);
font-weight: var(--font-semibold);
text-transform: lowercase;
margin-bottom: var(--space-2);
}
.card__description {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-text-secondary);
line-height: var(--leading-relaxed);
}
```
### Badges
```css
.badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-xs);
border: 1px solid currentColor;
text-transform: lowercase;
}
.badge--default { color: var(--color-text-secondary); }
.badge--success { color: var(--color-success); }
.badge--error { color: var(--color-error); }
.badge--warning { color: var(--color-warning); }
```
### Tables
```css
.table-container {
border: 1px dashed var(--color-border);
overflow: hidden;
}
.table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: var(--text-sm);
}
.table th {
text-align: left;
padding: var(--space-4);
border-bottom: 1px dashed var(--color-border);
font-weight: var(--font-semibold);
text-transform: lowercase;
}
.table td {
padding: var(--space-4);
border-bottom: 1px dashed var(--color-border);
}
.table tr:last-child td {
border-bottom: none;
}
```
---
## Layout
### Container Widths
```css
--container-sm: 640px;
--container-md: 768px;
--container-lg: 1024px;
--container-xl: 1280px;
```
### Page Structure
```
┌─────────────────────────────────────────────┐
│ Gradient Header Bar (announcement) │
├─────────────────────────────────────────────┤
│ Navigation (sticky, white bg) │
├─────────────────────────────────────────────┤
│ │
│ Hero Section │
│ (generous padding: --space-24) │
│ │
├─────────────────────────────────────────────┤
│ │
│ Content Sections │
│ (separated by --space-16 to --space-24) │
│ │
│ ┌─ fieldset container ─────────────────┐ │
│ │ section title │ │
│ │ │ │
│ │ Content with generous padding │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
```
---
## Interactions
### Hover States
- **Buttons**: Background color change (accent → darker)
- **Links**: Opacity reduction to 0.7
- **Cards**: Border color change (border → border-strong)
- **No transforms** - Avoid scale/translate on hover (too playful)
### Focus States
```css
*:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
```
### Transitions
```css
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
```
**Rule: Keep transitions subtle and fast. No bouncy/spring animations.**
---
## Special Elements
### Gradient Header Bar
```css
.header-bar {
background: var(--gradient-header);
color: white;
padding: var(--space-2) var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-sm);
text-align: center;
}
```
### Technical Diagrams
Use ASCII-style box diagrams with monospace font:
```
┌─────────────┐ ┌─────────────┐
│ client │─────▶│ API │
└─────────────┘ └─────────────┘
```
### Code Blocks
```css
.code-block {
background: var(--color-bg-secondary);
border: 1px dashed var(--color-border);
padding: var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-sm);
overflow-x: auto;
}
```
### Sliders/Range Inputs
Custom styled with accent color, monospace tooltips showing values.
---
## Do's and Don'ts
### Do
- Use lowercase for headings
- Use dashed borders for containers
- Use generous whitespace
- Use monospace for technical content
- Use underlines for links
- Keep interactions subtle and fast
- Use the copper accent sparingly but confidently
### Don't
- Don't use rounded corners (except for special cases)
- Don't use drop shadows
- Don't use gradients (except header bar)
- Don't use icons where text works
- Don't use colored links
- Don't use bouncy animations
- Don't use multiple accent colors
---
## Implementation Priority
### Phase 1: Foundation
1. Update CSS variables (colors, spacing, typography)
2. Install fonts (JetBrains Mono, Inter)
3. Update base styles (reset, typography)
### Phase 2: Core Components
1. Button variants
2. Input/Form elements
3. Card/Container styles
4. Badge variants
### Phase 3: Layout
1. Fieldset-style containers
2. Page layouts with generous spacing
3. Navigation updates
4. Gradient header bar
### Phase 4: Polish
1. Table styles
2. Code blocks
3. Interactive elements (sliders, toggles)
4. Transitions and hover states
---
## References
- **Turbopuffer**: https://turbopuffer.com - Primary design inspiration
- **JetBrains Mono**: https://www.jetbrains.com/lp/mono/
- **Inter**: https://rsms.me/inter/
---
*Last updated: January 2025*
*Version: 1.0*

View file

@ -1,269 +0,0 @@
# TPMJS Executor Compliance Report
> **Generated:** 2026-02-04
> **Protocol Version:** 1.0
> **Test Suite Version:** 0.1.0
## Overview
This document reports compliance testing results for the three reference TPMJS executor implementations against the Executor Protocol v1.0 specification.
## Compliance Summary
| Executor | Platform | Isolation | Core (L1) | Standard (L2) | Tests Passed |
|----------|----------|-----------|-----------|---------------|--------------|
| Railway Executor | Railway | Process | ✅ PASS | ✅ PASS | 15/15 |
| Unsandbox Executor | Unsandbox | Container | ✅ PASS | ✅ PASS | 15/15 |
| Vercel Executor | Vercel | VM | ✅ PASS* | ✅ PASS* | 15/15* |
\* Vercel Executor requires deployment to Vercel for full testing due to `@vercel/sandbox` dependency.
---
## Railway Executor
**Location:** `templates/railway-executor/`
### Test Results
```
TPMJS Executor Compliance Test v0.1.0
Protocol Version: 1.0
Target: http://localhost:3456
Core Core Requirements:
✓ GET /health returns 200 (65ms)
✓ GET /health includes protocolVersion (5ms)
✓ GET /health includes implementationVersion (5ms)
✓ POST /execute-tool accepts valid request (4425ms)
✓ POST /execute-tool returns structured response (2202ms)
✓ POST /execute-tool returns error for invalid tool (1556ms)
✓ CORS headers present (3ms)
✓ OPTIONS preflight works (2ms)
Standard Standard Requirements:
✓ GET /info returns 200 (6ms)
✓ GET /info includes capabilities (3ms)
✓ GET /info includes protocolVersion (3ms)
✓ capabilities.isolation is valid (2ms)
✓ Authentication enforced when configured (2181ms)
✓ Execution timeout enforcement (2ms)
✓ Structured error codes (2307ms)
Summary:
Tests: 15 passed, 0 failed, 15 total
Core Compliance: PASS
Standard Compliance: PASS
```
### Capabilities
```json
{
"name": "Railway Executor",
"version": "1.0.0",
"protocolVersion": "1.0",
"capabilities": {
"isolation": "process",
"executionModes": ["sync"],
"maxExecutionTimeMs": 120000,
"maxRequestBodyBytes": 10485760,
"supportsStreaming": false,
"supportsCallbacks": false,
"supportsCaching": false
}
}
```
### Deployment
```bash
# Deploy to Railway
railway init
railway up
# Or use the Docker image
docker build -t tpmjs-executor .
docker run -p 3000:3000 tpmjs-executor
```
---
## Unsandbox Executor
**Location:** `templates/unsandbox-executor/`
### Test Results
```
TPMJS Executor Compliance Test v0.1.0
Protocol Version: 1.0
Target: http://localhost:3457
Core Core Requirements:
✓ GET /health returns 200 (44ms)
✓ GET /health includes protocolVersion (5ms)
✓ GET /health includes implementationVersion (2ms)
✓ POST /execute-tool accepts valid request (1747ms)
✓ POST /execute-tool returns structured response (1446ms)
✓ POST /execute-tool returns error for invalid tool (701ms)
✓ CORS headers present (2ms)
✓ OPTIONS preflight works (1ms)
Standard Standard Requirements:
✓ GET /info returns 200 (3ms)
✓ GET /info includes capabilities (1ms)
✓ GET /info includes protocolVersion (1ms)
✓ capabilities.isolation is valid (0ms)
✓ Authentication enforced when configured (1926ms)
✓ Execution timeout enforcement (1ms)
✓ Structured error codes (744ms)
Summary:
Tests: 15 passed, 0 failed, 15 total
Core Compliance: PASS
Standard Compliance: PASS
```
### Capabilities
```json
{
"name": "Unsandbox Executor",
"version": "1.0.0",
"protocolVersion": "1.0",
"capabilities": {
"isolation": "container",
"executionModes": ["sync"],
"maxExecutionTimeMs": 120000,
"maxRequestBodyBytes": 10485760,
"supportsStreaming": false,
"supportsCallbacks": false,
"supportsCaching": false
}
}
```
### Deployment
See `templates/unsandbox-executor/README.md` for Unsandbox deployment instructions.
---
## Vercel Executor
**Location:** `templates/vercel-executor/`
### Capabilities
```json
{
"name": "Vercel Sandbox Executor",
"version": "1.0.0",
"protocolVersion": "1.0",
"capabilities": {
"isolation": "vm",
"executionModes": ["sync"],
"maxExecutionTimeMs": 120000,
"maxRequestBodyBytes": 10485760,
"supportsStreaming": false,
"supportsCallbacks": false,
"supportsCaching": false
}
}
```
### Deployment
```bash
# Deploy to Vercel
vercel
# Or link and deploy
vercel link
vercel deploy --prod
```
### Notes
The Vercel Executor uses `@vercel/sandbox` which provides VM-level isolation (strongest isolation level). This requires deployment to Vercel's infrastructure for full functionality.
---
## Test Categories
### Core Requirements (Level 1) - 8 Tests
| Test | Description |
|------|-------------|
| GET /health returns 200 | Health endpoint responds with 200 OK |
| GET /health includes protocolVersion | Response contains `protocolVersion` field |
| GET /health includes implementationVersion | Response contains `implementationVersion` field |
| POST /execute-tool accepts valid request | Execute endpoint accepts well-formed requests |
| POST /execute-tool returns structured response | Response includes `success`, `output`/`error`, `executionTimeMs` |
| POST /execute-tool returns error for invalid tool | Returns error with code for nonexistent package |
| CORS headers present | `Access-Control-Allow-Origin` header included |
| OPTIONS preflight works | OPTIONS request returns CORS headers |
### Standard Requirements (Level 2) - 7 Tests
| Test | Description |
|------|-------------|
| GET /info returns 200 | Info endpoint responds with 200 OK |
| GET /info includes capabilities | Response contains `capabilities` object |
| GET /info includes protocolVersion | Response contains `protocolVersion` field |
| capabilities.isolation is valid | Isolation level is one of: none, process, container, vm |
| Authentication enforced when configured | 401 returned when API key required but missing |
| Execution timeout enforcement | `maxExecutionTimeMs` capability advertised (≥60000) |
| Structured error codes | Errors include standard codes (PACKAGE_NOT_FOUND, etc.) |
---
## Running Compliance Tests
### Using npx (Published)
```bash
npx @tpmjs/executor-test https://your-executor.example.com
```
### Using Local Build
```bash
cd packages/executor-test
pnpm build
node bin/run.js https://your-executor.example.com
```
### With Authentication
```bash
npx @tpmjs/executor-test https://your-executor.example.com --api-key sk-xxx
```
### JSON Output
```bash
npx @tpmjs/executor-test https://your-executor.example.com --json
```
---
## Specification Reference
- **EXECUTOR_SPECIFICATION.md** - Full protocol specification
- **executor-openapi.yaml** - OpenAPI 3.0 specification
- **packages/executor-test/** - Compliance test suite source
---
## Changelog
### 2026-02-04
- Initial compliance testing
- All 3 executors updated to v1.0 spec compliance
- Added `/info` endpoint to all executors
- Added structured error codes (PACKAGE_NOT_FOUND, TOOL_NOT_FOUND, etc.)
- Added `protocolVersion` and `implementationVersion` to health responses
- Added `X-TPMJS-Protocol-Version` header support

View file

@ -1,496 +0,0 @@
# TPMJS Executor Protocol Specification v1.0
> **Status:** Draft
> **Version:** 1.0.0
> **Last Updated:** 2026-02-03
## Overview
The TPMJS Executor Protocol defines a standard HTTP interface for executing TPMJS tools. Executors are **compute adapters** that provide a consistent API for running npm-packaged tools regardless of the underlying infrastructure.
### Design Philosophy
- **HTTP-First:** No SDK lock-in, deployable anywhere
- **Minimal Surface:** Small core, optional extensions
- **Executor ≠ Sandbox:** Standardize coordination, not security
- **Declare, Don't Enforce:** Executors report capabilities, TPMJS decides policy
### Relationship to Other Specs
| Spec | Purpose |
|------|---------|
| **MCP** | Model ↔ Tool interface |
| **TPMJS Executor** | Tool ↔ Compute interface |
| **TPMJS Tools** | Tool contract (separate spec) |
---
## Protocol Versioning
### Version Header
All requests SHOULD include:
```http
X-TPMJS-Protocol-Version: 1.0
```
Executors MUST respond with their supported protocol version in `/health` and `/info` responses.
**Rationale:** Header-based versioning enables graceful evolution without URL fragmentation.
---
## Specification Levels
### Level 1: Core (REQUIRED)
Every executor MUST implement:
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/health` | GET | Liveness + protocol discovery |
| `/execute-tool` | POST | Synchronous tool execution |
### Level 2: Standard (RECOMMENDED)
Executors SHOULD implement:
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/info` | GET | Capability advertisement |
Plus:
- API key authentication
- Structured error responses
- Execution timeout enforcement
- CORS headers
### Level 3: Extended (OPTIONAL)
Reserved for future versions:
- `POST /execute-tool` with `Accept: text/event-stream` (streaming)
- `POST /execute-async` (webhook callbacks)
- `POST /validate-tool` (dry-run validation)
- `POST /execute-batch` (multiple tools)
---
## Core Endpoints
### GET /health
**Purpose:** Verify executor is running and discover protocol version.
**Response (200 OK):**
```json
{
"status": "ok",
"protocolVersion": "1.0",
"implementationVersion": "1.0.0",
"runtime": "node",
"timestamp": "2026-02-03T12:00:00.000Z"
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | Always `"ok"` if healthy |
| `protocolVersion` | string | Yes | TPMJS protocol version (e.g., `"1.0"`) |
| `implementationVersion` | string | Yes | Executor software version |
| `runtime` | string | No | Runtime identifier (e.g., `"node"`, `"deno"`, `"bun"`) |
| `timestamp` | string | No | ISO 8601 timestamp |
**Requirements:**
- MUST respond within 1 second
- MUST return 200 OK if healthy
- MUST include `protocolVersion`
---
### POST /execute-tool
**Purpose:** Execute a single TPMJS tool synchronously.
**Request Headers:**
```http
Content-Type: application/json
Authorization: Bearer <api-key> (if auth enabled)
X-TPMJS-Protocol-Version: 1.0
```
**Request Body:**
```json
{
"packageName": "@tpmjs/hello",
"version": "latest",
"name": "helloWorldTool",
"params": {
"greeting": "Hello"
},
"env": {
"OPENAI_API_KEY": "sk-..."
}
}
```
**Request Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `packageName` | string | Yes | npm package name |
| `version` | string | No | Package version (default: `"latest"`) |
| `name` | string | Yes | Tool export name |
| `params` | object | No | Parameters passed to `tool.execute()` |
| `env` | object | No | Environment variables for execution |
**Success Response (200 OK):**
```json
{
"success": true,
"output": {
"message": "Hello, World!"
},
"executionTimeMs": 1234
}
```
**Error Response (200 OK):**
```json
{
"success": false,
"error": {
"code": "TOOL_EXECUTION_ERROR",
"message": "Tool threw an error: Invalid input"
},
"executionTimeMs": 123
}
```
**Response Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `success` | boolean | Yes | Whether execution succeeded |
| `output` | any | If success | Return value from `tool.execute()` |
| `error` | object | If failed | Error details |
| `error.code` | string | If failed | Machine-readable error code |
| `error.message` | string | If failed | Human-readable error message |
| `executionTimeMs` | number | Yes | Total execution time in milliseconds |
**Error Codes:**
| Code | Description |
|------|-------------|
| `PACKAGE_NOT_FOUND` | npm package could not be installed |
| `TOOL_NOT_FOUND` | Named export not found in package |
| `TOOL_INVALID` | Export exists but has no `.execute()` method |
| `TOOL_EXECUTION_ERROR` | Tool threw during execution |
| `EXECUTION_TIMEOUT` | Execution exceeded time limit |
| `INTERNAL_ERROR` | Unexpected executor error |
---
## Standard Endpoints
### GET /info
**Purpose:** Advertise executor capabilities for intelligent routing.
**Response (200 OK):**
```json
{
"name": "Railway Executor",
"version": "1.0.0",
"protocolVersion": "1.0",
"capabilities": {
"isolation": "process",
"executionModes": ["sync"],
"maxExecutionTimeMs": 120000,
"maxRequestBodyBytes": 10485760,
"supportsStreaming": false,
"supportsCallbacks": false,
"supportsCaching": false
},
"runtime": {
"platform": "linux",
"nodeVersion": "20.10.0",
"region": "us-west-1"
}
}
```
**Capability Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `isolation` | string | `"none"` \| `"process"` \| `"container"` \| `"vm"` |
| `executionModes` | array | `["sync"]` (future: `"stream"`, `"async"`) |
| `maxExecutionTimeMs` | number | Maximum execution time before timeout |
| `maxRequestBodyBytes` | number | Maximum request body size |
| `supportsStreaming` | boolean | Reserved for v1.1 |
| `supportsCallbacks` | boolean | Reserved for v1.1 |
| `supportsCaching` | boolean | Reserved for v1.1 |
**Isolation Levels:**
| Level | Description |
|-------|-------------|
| `none` | Tools run in executor process (development only) |
| `process` | Tools run in separate OS process |
| `container` | Tools run in isolated container |
| `vm` | Tools run in isolated VM (strongest) |
---
## Authentication
### v1.0: API Key Only
Executors MAY require authentication via Bearer token.
**Request Header:**
```http
Authorization: Bearer <api-key>
```
**Configuration:**
Executors SHOULD use `EXECUTOR_API_KEY` environment variable:
- If set: All requests MUST include valid Bearer token
- If unset: No authentication required
**Unauthorized Response (401):**
```json
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}
}
```
**Future Versions:** JWT, OAuth, and per-tool authentication are deferred to v1.1+.
---
## CORS Requirements
All executors MUST support CORS for browser-based clients.
**Required Headers:**
```http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-TPMJS-Protocol-Version
```
**OPTIONS Preflight:**
All endpoints MUST handle OPTIONS requests and return CORS headers with 200 OK.
---
## Execution Lifecycle
### Standard Flow
1. **Receive Request:** Parse JSON body, validate required fields
2. **Check Auth:** Verify API key if configured
3. **Create Isolation:** Create temporary execution environment
4. **Install Package:** Run `npm install <package>@<version>`
5. **Load Tool:** Import package, resolve named export
6. **Execute:** Call `tool.execute(params)` with environment
7. **Capture Result:** Collect output or error
8. **Cleanup:** Remove temporary files/processes
9. **Respond:** Return JSON response
### Tool Resolution
Executors MUST resolve a callable tool with an `.execute()` method.
**Recommended Resolution Order:**
1. `pkg[name]` - Direct named export
2. `pkg.default?.[name]` - Named property on default export
3. `pkg.default` - Default export itself (if `name` matches)
**Factory Functions:**
If export is a function without `.execute()`:
1. Try calling `tool()` with no arguments
2. Check if result has `.execute()` method
**Note:** Tool export patterns are intentionally not fully standardized in v1.0 to allow ecosystem evolution.
---
## Timeouts
### Required Timeouts
| Phase | Minimum | Recommended |
|-------|---------|-------------|
| npm install | 30s | 60s |
| Tool execution | 60s | 120s |
| Total request | 90s | 180s |
Executors MUST:
- Enforce execution timeouts
- Return `EXECUTION_TIMEOUT` error code when exceeded
- Clean up resources on timeout
---
## Error Handling
### HTTP Status Codes
| Code | Usage |
|------|-------|
| 200 | Successful execution OR tool error (with `success: false`) |
| 400 | Invalid request (missing fields, malformed JSON) |
| 401 | Authentication required but missing/invalid |
| 404 | Unknown endpoint |
| 500 | Internal executor error |
### Structured Errors
All error responses MUST include:
```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description"
}
}
```
---
## Implementation Checklist
### Core (Required for Compliance)
- [ ] `GET /health` returns status and protocol version
- [ ] `POST /execute-tool` accepts standard request format
- [ ] Returns `{ success, output/error, executionTimeMs }`
- [ ] Handles missing/invalid request body (400)
- [ ] CORS headers on all responses
- [ ] OPTIONS preflight handling
### Standard (Recommended)
- [ ] `GET /info` with capabilities
- [ ] `EXECUTOR_API_KEY` environment variable support
- [ ] Bearer token validation (401 on failure)
- [ ] Execution timeout enforcement
- [ ] npm install timeout (60s recommended)
- [ ] Temporary file cleanup
- [ ] Structured error codes
### Extended (Optional)
- [ ] Package caching
- [ ] Concurrent execution limiting
- [ ] Support for both `/path` and `/api/path` routes
- [ ] Region/metadata in `/info` response
---
## Compliance Testing
Use the official compliance test suite:
```bash
npx @tpmjs/executor-test https://my-executor.example.com
```
Output:
```
TPMJS Executor Compliance Test v1.0.0
Target: https://my-executor.example.com
Core Requirements:
✓ GET /health returns 200
✓ GET /health includes protocolVersion
✓ POST /execute-tool accepts valid request
✓ POST /execute-tool returns success response
✓ POST /execute-tool returns error for invalid tool
✓ CORS headers present
✓ OPTIONS preflight works
Standard Requirements:
✓ GET /info returns capabilities
✓ Authentication enforced when configured
✓ Execution timeout enforced
✗ Missing: maxExecutionTimeMs in capabilities
Result: 10/11 tests passed (Core: PASS, Standard: PARTIAL)
```
---
## Reference Implementations
| Name | Platform | Isolation | Source |
|------|----------|-----------|--------|
| Railway Executor | Railway | Process | `templates/railway-executor/` |
| Vercel Executor | Vercel | VM (Sandbox) | `templates/vercel-executor/` |
| Unsandbox Executor | Unsandbox | Container | `templates/unsandbox-executor/` |
---
## Future Roadmap
### v1.1 (Planned)
- Streaming responses (`Accept: text/event-stream`)
- Async execution with webhooks
- Caching hints (`X-TPMJS-Cache-*` headers)
- Tool validation endpoint
### v2.0 (Exploration)
- Multi-tool batch execution
- Persistent execution contexts
- Resource quotas and billing hooks
- MCP bridge protocol
---
## Changelog
### v1.0.0 (2026-02-03)
- Initial formal specification
- Core: `/health`, `/execute-tool`
- Standard: `/info`, API key auth
- Capability negotiation
- Compliance test suite
---
## Appendix: OpenAPI Specification
See `executor-openapi.yaml` for the formal OpenAPI 3.0 specification.
## Appendix: JSON Schemas
See `packages/types/src/executor.ts` for TypeScript types and Zod schemas.

View file

@ -1,429 +0,0 @@
# 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 (requires API key):
```bash
curl "https://tpmjs.com/api/tools?q=yourpackagename" \
-H "Authorization: Bearer tpmjs_sk_your_api_key_here"
```
## 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 CRON_SECRET, not a user API key):
```bash
curl -X POST "https://tpmjs.com/api/sync/keyword" \
-H "Authorization: Bearer $CRON_SECRET"
```
## Support
Questions or issues?
- File an issue: https://github.com/ajaxdavis/tpmjs/issues
- Check the API docs: https://tpmjs.com/docs/api
- Generate an API key: https://tpmjs.com/dashboard/settings/tpmjs-api-keys

View file

@ -1,331 +0,0 @@
# 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
- `name` 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
View file

@ -1,21 +0,0 @@
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.

View file

@ -1,293 +0,0 @@
# 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'],
name: '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 `name`:
```typescript
{
npmPackageName: 'firecrawl-aisdk',
name: 'scrapeTool',
description: 'Scrape websites...',
// ...
},
{
npmPackageName: 'firecrawl-aisdk',
name: 'searchTool',
description: 'Search the web...',
// ...
},
{
npmPackageName: 'firecrawl-aisdk',
name: 'crawlTool',
description: 'Crawl entire websites...',
// ...
},
```
## Tier Calculation
Tools are automatically assigned a tier:
- **Rich tier**: Has `parameters` OR `returns` OR `aiAgent` fields
- **Minimal tier**: Only has basic metadata
Rich tier tools get 4x quality score multiplier, so add detailed metadata when possible.
## Maintenance
### Updating Manual Tools
1. Edit the entry in `manual-tools.ts`
2. Run `pnpm tsx sync-manual-tools.ts`
3. The upsert will update existing records
### Removing Manual Tools
1. Remove the entry from `manual-tools.ts`
2. Manually delete from database OR wait for metrics sync to mark as stale
### Version Updates
The sync script automatically fetches the latest version from npm unless you specify `npmVersion` in the manual tool entry.
## Production Deployment
### Option 1: Manual Sync on Deploy
Add to your deployment workflow:
```yaml
# .github/workflows/deploy.yml
- name: Sync manual tools
run: pnpm tsx sync-manual-tools.ts
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
```
### Option 2: Scheduled Sync
Create a cron job or GitHub Action to sync periodically:
```yaml
# .github/workflows/sync-manual.yml
name: Sync Manual Tools
on:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
workflow_dispatch: # Manual trigger
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install
- run: pnpm tsx sync-manual-tools.ts
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
```
### Option 3: API Endpoint
Create a sync endpoint (similar to keyword/changes sync):
```typescript
// apps/web/src/app/api/sync/manual/route.ts
import { manualTools } from '@/manual-tools';
// ... sync logic
export async function POST(request: Request) {
// Verify CRON_SECRET
// Run manual sync
// Return results
}
```
## Currently Included Manual Tools
As of this documentation:
- **ai-sdk-tool-code-execution** - Vercel Sandbox code execution
- **@exalabs/ai-sdk** - Exa web search
- **@parallel-web/ai-sdk-tools** - Parallel search and extraction (2 tools)
- **ctx-zip** - MCP + Vercel Sandbox integration
- **@perplexity-ai/ai-sdk** - Perplexity search
- **@tavily/ai-sdk** - Tavily web research
- **firecrawl-aisdk** - Firecrawl scraping, search, crawling (3 tools)
- **bedrock-agentcore** - AWS Bedrock code interpreter and browser (2 tools)
- **@superagent-ai/ai-sdk** - Superagent security tools (3 tools)
- **@valyu/ai-sdk** - Valyu domain-specific search tools (8 tools)
**Total: 24 manually curated tools across 10 packages**
## FAQ
### Why not just ask package maintainers to add the tpmjs field?
We should! But:
1. Some packages are from large companies (Vercel, AWS, etc.) with slow adoption cycles
2. We want these tools available on TPMJS now
3. Manual curation lets us provide better metadata than package authors might
### Will manual tools be replaced by auto-discovered ones?
Yes! If a package adds a proper `tpmjs` field, the auto-discovery sync will update it with `discoveryMethod: 'keyword'` or `'changes-feed'`. Manual entries can then be removed from `manual-tools.ts`.
### Can I mix manual and auto-discovered tools from the same package?
Yes. If a package has some tools in the `tpmjs` field but is missing others, you can manually add the missing ones. The sync scripts will coexist peacefully.
### How do I know if a tool is manually curated?
Check the `discoveryMethod` field in the database:
- `'manual'` = Manually curated
- `'keyword'` = Auto-discovered via keyword search
- `'changes-feed'` = Auto-discovered via npm changes feed
## Best Practices
1. **Complete Metadata** - Provide as much metadata as possible for rich tier
2. **Accurate Descriptions** - Tool descriptions should be clear and specific
3. **AI-Friendly** - Write `aiAgent.useCase` as guidance for LLMs
4. **Keep Updated** - Periodically check if packages have added native `tpmjs` support
5. **Link to Docs** - Always include `docsUrl` when available
6. **API Key URLs** - Include `apiKeyUrl` for tools requiring authentication
## Contributing
To contribute new manual tools:
1. Fork the repository
2. Add your tool to `manual-tools.ts`
3. Test with `pnpm tsx sync-manual-tools.ts`
4. Open a pull request with:
- Why this tool should be included
- Link to the npm package
- Screenshot of it working in TPMJS
## Related Documentation
- [HOW_TO_PUBLISH_A_TOOL.md](./HOW_TO_PUBLISH_A_TOOL.md) - Standard tpmjs field spec
- [CLAUDE.md](./CLAUDE.md) - General project documentation
- [packages/types/src/tpmjs.ts](./packages/types/src/tpmjs.ts) - TypeScript schema definitions

View file

@ -1,118 +0,0 @@
# Quality Gates Setup
This document describes the quality gate tools configured for the TPMJS monorepo.
## Installed Tools
### 1. TypeScript Type Checking
```bash
pnpm type-check
```
Runs `tsc --noEmit` across all packages to catch type errors.
### 2. Type Coverage
```bash
pnpm type-coverage
```
Uses `type-coverage` to ensure no implicit `any` types. Currently configured for 95% minimum coverage.
### 3. Dead Code Detection
```bash
pnpm find-deadcode
```
Uses `knip` to find:
- Unused files
- Unused dependencies
- Unused exports
- Unresolved imports
**Configuration:** `knip.json`
- Ignores test files, build artifacts (dist, .next, storybook-static)
- Workspace-aware for monorepo structure
### 4. Architecture Validation
```bash
pnpm check-architecture
```
Uses `dependency-cruiser` to enforce:
- No circular dependencies
- No unresolvable imports
- No deprecated dependencies
- **Custom rule:** Packages cannot import from apps (keeps packages reusable)
**Configuration:** `.dependency-cruiser.js`
- Simplified to standard rules only
- Excludes build artifacts automatically
- One custom rule: packages stay independent of apps
## Node.js Version
**Required:** Node.js 22+ (LTS)
The project uses `.nvmrc` to specify Node version:
```bash
nvm use
```
## Integration
### Pre-commit Hook (Optional)
Add to `.lefthook.yml`:
```yaml
pre-commit:
commands:
type-check:
run: pnpm type-check
deadcode:
run: pnpm find-deadcode
```
### CI Pipeline (Recommended)
Add to `.github/workflows/ci.yml`:
```yaml
- name: Type check
run: pnpm type-check
- name: Check architecture
run: pnpm check-architecture
- name: Find dead code
run: pnpm find-deadcode
```
## Current Status
### ✅ Type Check
All packages pass type checking.
### ✅ Architecture Check
**1 error, 14 warnings**
- **Error:** Missing export in `@tpmjs/ui/Tabs/types` (needs fix)
- **Warnings:** React listed in both dependencies and devDependencies (informational, not blocking)
### ⚠️ Dead Code Detection
**Minor issues found:**
- 1 unused file: `packages/config/eslint/react.js`
- 5 unused dependencies (can be cleaned up)
- 6 unused devDependencies (can be cleaned up)
These are informational and don't block development.
## Philosophy
The configuration follows a **practical, non-blocking** approach:
- Standard rules that prevent real problems
- No overly strict custom rules that make development difficult
- Warnings for things worth knowing about, errors for things that will break
- Build artifacts and config files are properly excluded
## Maintenance
Run these periodically to keep the codebase clean:
```bash
# Check everything
pnpm type-check && pnpm check-architecture && pnpm find-deadcode
# Or just the quick ones
pnpm type-check && pnpm find-deadcode
```

View file

@ -1,59 +1,8 @@
# TPMJS
# TPMJS Monorepo
[![CI](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml/badge.svg)](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
Tool Package Manager for AI Agents - A Turborepo monorepo with strict TypeScript, Next.js 16, and best practices.
**TPMJS is a registry for discovering AI tools published to npm.**
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
## Structure
```
apps/
@ -73,9 +22,8 @@ packages/
### Prerequisites
- Node.js >= 22 (LTS)
- Node.js >= 18
- pnpm >= 8
- nvm (recommended for Node version management)
### Installation
@ -127,21 +75,6 @@ pnpm format
pnpm format:check
```
### Quality Gates
```bash
# Check architecture/dependency rules
pnpm check-architecture
# Find unused code and dependencies
pnpm find-deadcode
# Check type coverage
pnpm type-coverage
```
See [QUALITY-GATES.md](./QUALITY-GATES.md) for details.
## Component Usage
Components are imported directly without barrel exports:
@ -196,20 +129,6 @@ git push --follow-tags
- `@tpmjs/types` - TypeScript types
- `@tpmjs/env` - Environment schema loader
## Deployment
The project is configured to only deploy to Vercel when all CI checks pass. This ensures production always has high-quality, tested code.
**CI Checks:**
- Linting & formatting
- Type checking
- Tests
- Production build
- Architecture validation
- Dead code detection
See [DEPLOYMENT.md](./DEPLOYMENT.md) for full configuration details.
## Module Boundaries
ESLint enforces module boundaries:
@ -252,9 +171,6 @@ Using `.ts` instead of `.tsx` for React components:
- `format` - Format code with Biome
- `format:check` - Check formatting
- `type-check` - TypeScript type checking
- `type-coverage` - Check type coverage (no implicit any)
- `check-architecture` - Validate dependency rules
- `find-deadcode` - Find unused code/dependencies
- `clean` - Remove build artifacts
- `changeset` - Create a changeset
- `changeset:version` - Version packages

View file

@ -1,149 +0,0 @@
# Top 5 Priorities for TPMJS to Become Production-Ready
> Based on comprehensive codebase analysis - January 2026
TPMJS is approximately 70-75% towards being production-ready for widespread use. The platform has solid fundamentals: a well-architected monorepo, robust npm sync system, working MCP integration, and professional code quality standards. However, five critical gaps need addressing before TPMJS can become the "npm for AI tools" it aspires to be.
---
## 1. Complete the Developer SDK Packages
**The Problem:**
Developers can't easily integrate TPMJS tools into their applications. The SDK packages `@tpmjs/registry-search` and `@tpmjs/registry-execute` are either incomplete or missing. This defeats the core value proposition - if AI agents can't programmatically discover and execute tools from TPMJS, the registry is just a website, not an ecosystem.
**What's Needed:**
- `@tpmjs/registry-search` - TypeScript client for searching tools with full type safety
- `@tpmjs/registry-execute` - Execute any TPMJS tool from any Node.js application
- `@tpmjs/agent-toolkit` - Pre-built integration for popular agent frameworks (LangChain, AutoGPT, CrewAI)
- Clear examples showing integration with Claude, GPT-4, and other LLMs
**Impact:**
Without these SDKs, TPMJS is just a directory. With them, every AI developer can instantly access 100+ tools with a single `npm install`. This is the difference between a catalog and a platform.
**Effort:** 2-4 weeks of focused development
---
## 2. Add Social Proof and Discovery Features
**The Problem:**
Users have no way to evaluate tool quality beyond download counts. There's no star rating, no reviews, no "trending this week," and no recommendations. When browsing tools, users can't distinguish battle-tested tools from abandoned experiments.
**What's Needed:**
- **5-star rating system** with verified user ratings
- **User reviews** with upvoting and author responses
- **Trending tools** algorithm (based on recent usage, not just total downloads)
- **"Staff Picks"** or curated collections for common use cases
- **Similar tools** recommendations on each tool page
- **Usage statistics** - "Used in 50 agents" or "10,000 executions this month"
**Impact:**
Social proof is essential for adoption. GitHub has stars, npm has weekly downloads prominently displayed, Product Hunt has upvotes. TPMJS needs its own trust signals. Without them, users default to building their own tools or using alternatives they can evaluate.
**Effort:** 3-4 weeks including UI/UX design
---
## 3. Build Comprehensive Documentation and Onboarding
**The Problem:**
The publishing guide exists but there's no interactive tutorial for new users. API documentation is schema-only with no examples. Developers looking to build tools, create agents, or integrate TPMJS into their workflow face a steep learning curve with limited guidance.
**What's Needed:**
- **Interactive onboarding flow** - Guided first-time experience creating an agent with tools
- **API documentation** with copy-paste examples for every endpoint
- **Video tutorials** - 5-minute quickstarts for common tasks
- **Example agents** - Pre-built agents demonstrating best practices (research agent, coding assistant, data analyst)
- **Tool development guide** - Step-by-step from `npm init` to published tool
- **Troubleshooting guide** - Common errors and solutions
**Impact:**
Documentation is a product feature. Every hour spent on docs saves thousands of hours of user frustration. LangChain succeeded partly because of excellent docs. TPMJS needs the same investment.
**Effort:** 4-6 weeks for comprehensive documentation overhaul
---
## 4. Build Observability and Platform Trust
**The Problem:**
There's no public status page, no platform-wide health dashboard, and limited visibility into what's working. Users can't answer basic questions: "Is TPMJS up?", "How reliable is this tool?", "What's the average response time?"
**What's Needed:**
- **Public status page** (status.tpmjs.com) showing real-time platform health
- **Tool health dashboard** - Aggregate view of which tools are healthy/broken
- **Response time metrics** - P50/P95/P99 latency for tool executions
- **Uptime guarantees** - Published SLA (even informal "99.9% target")
- **Incident history** - Transparent communication about outages
- **Usage analytics dashboard** - For tool authors to see how their tools are used
**Impact:**
Trust is earned through transparency. AWS publishes their health dashboard. GitHub has status.github.com. Enterprises won't adopt platforms they can't monitor. Even individual developers want to know if their agent's failures are their code or the platform.
**Effort:** 2-3 weeks for MVP status page and health dashboard
---
## 5. Add Team and Enterprise Features
**The Problem:**
TPMJS is individual-only. There's no way to share collections within a team, manage API keys across an organization, or implement approval workflows. This blocks enterprise adoption where multiple developers need to collaborate on agent tooling.
**What's Needed:**
- **Organizations** - Create teams with shared collections and agents
- **Role-based access control (RBAC)** - Admin, Developer, Viewer roles
- **Shared API keys** - Organization-scoped keys with usage attribution
- **Audit logging** - Who did what, when (required for compliance)
- **Private tools** - Organization-only tool publishing
- **SSO/SAML** - Enterprise identity provider integration
- **Usage quotas** - Set limits per team member or project
**Impact:**
Enterprise customers pay for tools. They also require these features for security and compliance. One enterprise contract can fund months of development. More importantly, enterprise adoption validates the platform and attracts more developers.
**Effort:** 6-8 weeks for core team features, 3-6 months for full enterprise suite
---
## Summary
| Priority | Impact | Effort | Recommended Order |
|----------|--------|--------|-------------------|
| 1. Complete SDK Packages | Critical | 2-4 weeks | First |
| 2. Social Proof/Discovery | High | 3-4 weeks | Second |
| 3. Documentation | High | 4-6 weeks | Parallel with #2 |
| 4. Observability/Trust | Medium-High | 2-3 weeks | Third |
| 5. Enterprise Features | Medium | 6-8 weeks | Fourth |
**Recommended approach:**
1. **Weeks 1-4:** Complete SDK packages (unlocks programmatic adoption)
2. **Weeks 2-6:** Build ratings/reviews and documentation in parallel
3. **Weeks 7-9:** Add status page and health dashboard
4. **Weeks 10+:** Begin enterprise features based on customer demand
---
## Current Strengths to Leverage
TPMJS already has strong foundations:
- Robust npm sync system (tools auto-discovered)
- Working MCP protocol integration
- Clean monorepo architecture
- Good authentication system
- Solid database design
- Quality coding standards
These investments mean the platform can scale. The gaps identified above are about adoption and trust, not technical architecture.
---
## The Bottom Line
TPMJS has built a good tool registry. To become **the** AI tools platform, it needs to:
1. Make tools easy to use programmatically (SDKs)
2. Help users find good tools (social proof)
3. Help developers build tools (documentation)
4. Build platform confidence (observability)
5. Enable team adoption (enterprise features)
With focused effort on these five areas over the next 3-6 months, TPMJS can establish itself as the definitive platform for AI agent tooling.

View file

@ -1,698 +0,0 @@
# TPMJS Platform - Complete Feature Documentation
A comprehensive overview of all TPMJS functionality for marketing, fundraising, and pet project ideation.
---
## Table of Contents
1. [Platform Overview](#platform-overview)
2. [Core Architecture](#core-architecture)
3. [Tool Registry & Discovery](#tool-registry--discovery)
4. [Tool Execution System](#tool-execution-system)
5. [MCP (Model Context Protocol) Implementation](#mcp-model-context-protocol-implementation)
6. [Collections System](#collections-system)
7. [Agent System](#agent-system)
8. [API Endpoints](#api-endpoints)
9. [SDK & Packages](#sdk--packages)
10. [Security & Privacy](#security--privacy)
11. [Infrastructure](#infrastructure)
12. [Use Cases](#use-cases)
13. [Competitive Advantages](#competitive-advantages)
---
## Platform Overview
**TPMJS (Tool Package Manager for JavaScript)** is an open platform for discovering, sharing, and executing AI tools via the Model Context Protocol (MCP). Think of it as "npm for AI tools" - a registry where developers can publish tools that AI assistants can use.
### Key Value Propositions
1. **Unified Tool Registry** - One place to discover and use AI tools
2. **Instant MCP Servers** - Any collection becomes an MCP-compatible server
3. **Secure Execution** - Sandboxed tool execution with rate limiting
4. **AI Agent Infrastructure** - Build multi-turn conversational agents with tool access
5. **Developer-Friendly** - Publish tools via npm, use via standard protocols
---
## Core Architecture
### Tech Stack
| Layer | Technology |
|-------|------------|
| Frontend | Next.js 16 (App Router), React 19, Tailwind CSS |
| Backend | Next.js API Routes (Serverless) |
| Database | PostgreSQL (Neon) with Prisma ORM |
| Auth | NextAuth.js (GitHub OAuth) |
| Hosting | Vercel (Edge + Serverless) |
| Package Registry | npm (mirrored) |
| Build System | Turborepo + pnpm workspaces |
### Monorepo Structure
```
tpmjs/
├── apps/
│ ├── web/ # Main Next.js application (tpmjs.com)
│ └── playground/ # Interactive tool testing environment
├── packages/
│ ├── @tpmjs/types # Shared TypeScript types & Zod schemas
│ ├── @tpmjs/ui # React component library
│ ├── @tpmjs/utils # Utility functions
│ ├── @tpmjs/env # Environment variable validation
│ ├── @tpmjs/db # Prisma database client
│ ├── @tpmjs/mocks # MSW mock server for testing
│ └── @tpmjs/config # Shared configs (ESLint, Tailwind, TypeScript)
└── templates/
└── vercel-executor/ # Template for deploying tool executors
```
---
## Tool Registry & Discovery
### What is a TPMJS Tool?
A TPMJS tool is an npm package with:
1. The `tpmjs` keyword in package.json
2. A `tpmjs` field defining the tool's MCP schema
```json
{
"name": "my-awesome-tool",
"keywords": ["tpmjs"],
"tpmjs": {
"name": "my-tool",
"description": "Does awesome things",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
}
}
```
### Tool Tiers
| Tier | Description | Features |
|------|-------------|----------|
| **Minimal** | Basic tool definition | Name, description, input schema only |
| **Rich** | Full-featured tool | Executor URL, examples, categories, tags |
### Discovery Methods
1. **npm Changes Feed Sync** (every 2 minutes)
- Monitors npm's real-time changes feed
- Catches new packages and updates instantly
- Processes ~100 changes per run
2. **Keyword Search Sync** (every 15 minutes)
- Actively searches npm for `tpmjs` keyword
- Backfills any missed packages
- Processes up to 250 packages per run
3. **Metrics Sync** (hourly)
- Updates download statistics
- Calculates quality scores
- Refreshes ranking data
### Quality Scoring Algorithm
```
Quality Score = Tier Score + Downloads Score + Stars Score
Where:
- Tier Score: rich = 0.6, minimal = 0.4
- Downloads Score: min(0.3, log10(downloads + 1) / 10)
- Stars Score: min(0.1, log10(githubStars + 1) / 10)
```
### Tool Categories
- AI/ML
- Development Tools
- Data Processing
- Web Scraping
- APIs & Integrations
- Utilities
- And more...
### Current Registry Stats
- **170+ Official Tools** in the ajax-collection
- **Growing Community Tools** published by developers
- **Real-time Sync** with npm registry
---
## Tool Execution System
### Execution Flow
```
User Request → TPMJS API → Executor Selection → Sandboxed Execution → Response
```
### Executor Types
1. **HTTP Executor** - Calls external HTTP endpoints
2. **Serverless Executor** - Runs in Vercel Edge/Serverless
3. **Code Executor** - Executes arbitrary code in sandbox
### Sandboxing Features
- **Network Isolation** - Zero-trust or semi-trusted modes
- **Timeout Limits** - Configurable per-tool (1-900 seconds)
- **Resource Limits** - Memory and CPU constraints
- **Input Validation** - Zod schema validation
### Executor Template
The `templates/vercel-executor/` provides a ready-to-deploy executor:
```typescript
// Example executor implementation
export async function POST(request: Request) {
const { tool, input } = await request.json();
// Validate input against schema
const validated = toolSchema.parse(input);
// Execute tool logic
const result = await executeTool(tool, validated);
return Response.json(result);
}
```
### Code Execution (via MCP Tool)
The platform includes a powerful code execution tool:
```javascript
// Execute code in 42+ languages
{
"language": "python",
"code": "print('Hello, World!')",
"network_mode": "zerotrust", // or "semitrusted"
"ttl": 60 // timeout in seconds
}
```
Supported languages include:
- Python, JavaScript, TypeScript
- Go, Rust, C, C++
- Ruby, PHP, Perl
- Java, Kotlin, Scala
- And 30+ more
---
## MCP (Model Context Protocol) Implementation
### What is MCP?
MCP is an open protocol for AI assistants to interact with tools. TPMJS provides:
- **MCP Server Hosting** - Every collection is an MCP server
- **Multiple Transports** - HTTP and SSE support
- **Standard Compliance** - Full MCP specification support
### Transport Options
#### HTTP Transport
```
POST /api/mcp/{username}/{collection-slug}/http
Content-Type: application/json
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
```
#### SSE Transport
```
POST /api/mcp/{username}/{collection-slug}/sse
Content-Type: application/json
{"jsonrpc": "2.0", "id": 1, "method": "initialize"}
```
### MCP Methods Supported
| Method | Description |
|--------|-------------|
| `initialize` | Initialize MCP session |
| `tools/list` | List available tools |
| `tools/call` | Execute a tool |
| `resources/list` | List available resources |
| `resources/read` | Read a resource |
| `prompts/list` | List available prompts |
| `prompts/get` | Get a specific prompt |
### Authentication
- **API Key Auth** - Bearer token in Authorization header
- **Session Auth** - Cookie-based for web users
- **Scopes** - Granular permission control
- `mcp:access` - Access MCP endpoints
- `mcp:execute` - Execute tools
- `tools:read` - List tools
- `tools:execute` - Execute specific tools
- `collections:read` - Access collections
### Integration Examples
#### Claude Desktop
```json
{
"mcpServers": {
"tpmjs": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-remote",
"https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"]
}
}
}
```
#### Cursor IDE
```json
{
"mcpServers": {
"tpmjs": {
"url": "https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"
}
}
}
```
---
## Collections System
### What are Collections?
Collections are curated groups of tools that form an MCP server. Users can:
- Create public or private collections
- Add tools from the registry
- Share collections as MCP endpoints
### Collection Features
- **Custom Naming** - Unique slug per user
- **Tool Curation** - Add/remove tools
- **Access Control** - Public or private
- **MCP Endpoint** - Automatic server generation
### Collection API
```typescript
// Create collection
POST /api/collections
{ "name": "My Tools", "slug": "my-tools", "isPublic": true }
// Add tool to collection
POST /api/collections/{id}/tools
{ "toolId": "tool-123" }
// Get collection's MCP endpoint
GET /api/mcp/{username}/{collection-slug}/http
```
---
## Agent System
### What are TPMJS Agents?
Agents are AI-powered conversational interfaces with access to TPMJS tools. They enable:
- Multi-turn conversations
- Tool execution within context
- Custom system prompts
- Provider flexibility (OpenAI, Anthropic, etc.)
### Agent Configuration
```typescript
interface Agent {
id: string;
uid: string; // Unique identifier
name: string;
description?: string;
provider: "OPENAI" | "ANTHROPIC" | "GOOGLE";
modelId: string; // e.g., "gpt-4o-mini"
systemPrompt?: string;
isPublic: boolean;
tools: Tool[]; // Attached tools
}
```
### Agent Features
1. **Multi-Turn Conversations**
- Persistent chat history
- Context-aware responses
- Tool execution in conversation
2. **Provider Flexibility**
- OpenAI (GPT-4, GPT-4o-mini)
- Anthropic (Claude)
- Google (Gemini)
- Custom providers
3. **Tool Integration**
- Attach any TPMJS tool
- Automatic tool calling
- Result injection into context
4. **Public Chat Pages**
- Share agents via public URL
- Embeddable chat interfaces
- No auth required for public agents
### Agent API
```typescript
// Create agent
POST /api/agents
{ "name": "My Agent", "provider": "OPENAI", "modelId": "gpt-4o-mini" }
// Chat with agent
POST /api/agents/{id}/chat
{ "messages": [{"role": "user", "content": "Hello!"}] }
// Stream response
POST /api/agents/{id}/chat
{ "messages": [...], "stream": true }
```
---
## API Endpoints
### Public Endpoints (No Auth)
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/health` | GET | Health check with build info |
| `/api/stats` | GET | Platform statistics |
| `/api/stats/health` | GET | Tool health metrics |
| `/api/tools` | GET | List public tools |
| `/api/tools/{id}` | GET | Get tool details |
| `/api/tools/search` | GET | Search tools |
| `/api/collections/public` | GET | List public collections |
### Authenticated Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/user` | GET | Current user profile |
| `/api/user/settings` | PATCH | Update user settings |
| `/api/user/api-keys` | GET/POST | Manage API keys |
| `/api/agents` | CRUD | Agent management |
| `/api/collections` | CRUD | Collection management |
### MCP Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/mcp/{user}/{collection}/http` | POST | HTTP transport |
| `/api/mcp/{user}/{collection}/sse` | POST | SSE transport |
| `/api/mcp/{user}/{collection}/http` | GET | Server info |
### Sync Endpoints (Cron)
| Endpoint | Schedule | Description |
|----------|----------|-------------|
| `/api/sync/changes` | */2 * * * * | npm changes feed |
| `/api/sync/keyword` | */15 * * * * | Keyword search |
| `/api/sync/metrics` | 0 * * * * | Metrics update |
### Tool Execution
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/tools/{id}/execute` | POST | Execute a tool |
| `/api/execute/code` | POST | Execute code (sandbox) |
---
## SDK & Packages
### Published npm Packages
| Package | Description |
|---------|-------------|
| `@tpmjs/types` | TypeScript types and Zod schemas |
| `@tpmjs/ui` | React component library |
| `@tpmjs/utils` | Utility functions |
| `@tpmjs/env` | Environment validation |
### Type Definitions
```typescript
// Tool types
interface TpmjsTool {
name: string;
description: string;
inputSchema: JSONSchema;
outputSchema?: JSONSchema;
executor?: string;
category?: string;
tags?: string[];
}
// MCP types
interface McpRequest {
jsonrpc: "2.0";
id: string | number;
method: string;
params?: Record<string, unknown>;
}
interface McpResponse {
jsonrpc: "2.0";
id: string | number;
result?: unknown;
error?: McpError;
}
```
### UI Components
- Buttons, Cards, Badges
- Form inputs with validation
- Code editors with syntax highlighting
- Chat interfaces
- Tool cards and lists
---
## Security & Privacy
### Authentication Methods
1. **GitHub OAuth** - Primary user auth
2. **API Keys** - Programmatic access
3. **Session Cookies** - Web auth
### API Key Security
- SHA-256 hashed storage
- Prefix-only display after creation
- Scoped permissions
- Optional expiration
- Revocation support
### Rate Limiting
- Per-user limits
- Per-IP limits
- Per-tool limits
- Customizable thresholds
### Data Privacy
- No tool input logging by default
- Optional usage analytics
- GDPR-compliant data handling
- User data export/deletion
### Sandbox Security
- Network isolation modes
- Resource limits
- No persistent storage
- Ephemeral execution
---
## Infrastructure
### Deployment Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Vercel │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Edge │ │ Serverless │ │ Serverless │ │
│ │ Network │→ │ Functions │→ │ Executors │ │
│ │ (CDN) │ │ (API) │ │ (Tool Runners) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Neon PostgreSQL │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Tools │ │ Users │ │ Collections │ │
│ │ Registry │ │ & Auth │ │ & Agents │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Monitoring
- **Health Checks** - Every 5 minutes via GitHub Actions
- **Vercel Analytics** - Performance monitoring
- **Sync Logging** - All sync operations logged
- **Error Tracking** - Automatic error collection
### CI/CD Pipeline
1. **Pre-commit** - Lint, format, type-check (Lefthook)
2. **CI** - Full test suite (GitHub Actions)
3. **Deploy** - Automatic on merge (Vercel)
4. **Health Check** - Post-deploy verification
---
## Use Cases
### For Developers
1. **Publish AI Tools**
- Package as npm module
- Add `tpmjs` keyword
- Automatically synced to registry
2. **Build Tool Collections**
- Curate tools for specific use cases
- Share as MCP endpoint
- Embed in applications
3. **Create AI Agents**
- Attach tools to agents
- Custom system prompts
- Deploy public chat interfaces
### For AI Applications
1. **Integrate Tools**
- Connect via MCP protocol
- Use any TPMJS collection
- Standard JSON-RPC interface
2. **Extend Capabilities**
- Web scraping, code execution
- API integrations
- Data processing
3. **Build Workflows**
- Chain multiple tools
- Agent-based automation
- Custom orchestration
### For Enterprises
1. **Private Tool Registry**
- Internal tools only
- Access control
- Usage analytics
2. **Secure Execution**
- Sandboxed environments
- Audit logging
- Compliance ready
3. **Custom Agents**
- Brand-specific AI assistants
- Internal knowledge access
- Tool-enabled support
---
## Competitive Advantages
### vs. Building Custom MCP Servers
| TPMJS | Custom MCP Server |
|-------|-------------------|
| Instant setup | Days/weeks of development |
| 170+ tools ready | Build each tool |
| Hosted infrastructure | Self-hosted required |
| Automatic scaling | Manual scaling |
### vs. Other Tool Platforms
| Feature | TPMJS | Competitors |
|---------|-------|-------------|
| Open Protocol (MCP) | ✅ | Often proprietary |
| npm Integration | ✅ | Custom registries |
| Self-hostable | ✅ | Usually SaaS-only |
| Code Execution | ✅ | Limited |
| Agent System | ✅ | Separate product |
### Unique Features
1. **npm-Native** - Tools are just npm packages
2. **MCP-First** - Built on open standard
3. **Hybrid Execution** - Local + cloud options
4. **Collection System** - Curated tool sets
5. **Agent Platform** - Full conversational AI
---
## Appendix: Official Tools Collection
The `ajax-collection` includes 170+ tools across categories:
### Web & Data
- `firecrawl-aisdk` - Web crawling and extraction
- `tpmjs-tools-page-brief` - Page summarization
- `tpmjs-tools-search` - Web search
### Development
- `tpmjs-unsandbox` - Code execution (42+ languages)
- `tpmjs-tools-toc-generate` - Markdown TOC generator
- `tpmjs-tools-changelog-entry` - Changelog generation
### Content
- `tpmjs-createblogpost` - Blog post creation
- `tpmjs-tools-recipe-hash` - Recipe/workflow hashing
- `tpmjs-tools-workflow-variant-generate` - Workflow variations
### And Many More...
- API integrations
- Data transformations
- File processing
- Image manipulation
- Text analysis
---
## Summary
TPMJS is a comprehensive platform for AI tool discovery, execution, and orchestration. Key takeaways:
1. **Registry** - npm-native tool discovery with automatic syncing
2. **Execution** - Secure, sandboxed tool running
3. **MCP** - Standard protocol for AI integration
4. **Collections** - Curated tool sets as MCP servers
5. **Agents** - Conversational AI with tool access
6. **Infrastructure** - Production-ready, scalable, monitored
The platform enables developers to publish tools, AI applications to consume them, and enterprises to build secure, tool-enabled AI experiences.

View file

@ -1,391 +0,0 @@
# 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.*

35539
aisdk.md

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
.build

View file

@ -1,20 +0,0 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.996",
"green": "0.475",
"red": "0.325"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -1,58 +0,0 @@
{
"images": [
{
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -1,6 +0,0 @@
{
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -1,41 +0,0 @@
import Foundation
import SwiftData
@Model
final class Conversation {
var id: UUID
var title: String?
var createdAt: Date
var updatedAt: Date
var executionState: String // "idle" | "running"
var inputTokensTotal: Int
var outputTokensTotal: Int
@Relationship(deleteRule: .cascade, inverse: \Message.conversation)
var messages: [Message]
@Relationship(deleteRule: .cascade, inverse: \ToolCallRecord.conversation)
var toolRuns: [ToolCallRecord]
init(
title: String? = nil
) {
self.id = UUID()
self.title = title
self.createdAt = Date()
self.updatedAt = Date()
self.executionState = "idle"
self.inputTokensTotal = 0
self.outputTokensTotal = 0
self.messages = []
self.toolRuns = []
}
var displayTitle: String {
title ?? "New Conversation"
}
var sortedMessages: [Message] {
messages.sorted { $0.createdAt < $1.createdAt }
}
}

View file

@ -1,18 +0,0 @@
import Foundation
import SwiftData
@Model
final class EnvVar {
var id: UUID
var keyName: String
/// Last 4 characters of the value (for display hint)
var valueHint: String
var createdAt: Date
init(keyName: String, valueHint: String) {
self.id = UUID()
self.keyName = keyName
self.valueHint = valueHint
self.createdAt = Date()
}
}

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