Compare commits
No commits in common. "main" and "@tpmjs/ui@0.1.1" have entirely different histories.
main
...
@tpmjs/ui@
2329 changed files with 1899 additions and 1227648 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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*
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
```
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../.agents/skills/agentmail
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 />`.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
};
|
||||
```
|
||||
|
|
@ -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})`} />;
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
```
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
|
@ -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"));
|
||||
```
|
||||
|
|
@ -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"));
|
||||
```
|
||||
|
|
@ -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"));
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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}} />;
|
||||
```
|
||||
|
||||
|
|
@ -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>;
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -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>
|
||||
```
|
||||
|
|
@ -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>
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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',
|
||||
});
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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 />`.
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
80
.env.example
80
.env.example
|
|
@ -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"
|
||||
12
.gitallowed
12
.gitallowed
|
|
@ -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]+\.\.\.
|
||||
66
.github/workflows/auto-close-published.yml
vendored
66
.github/workflows/auto-close-published.yml
vendored
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
.github/workflows/build-omega-mac.yml
vendored
72
.github/workflows/build-omega-mac.yml
vendored
|
|
@ -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
|
||||
54
.github/workflows/ci.yml
vendored
54
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
44
.github/workflows/claude-code-review.yml
vendored
44
.github/workflows/claude-code-review.yml
vendored
|
|
@ -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
|
||||
|
||||
96
.github/workflows/claude.yml
vendored
96
.github/workflows/claude.yml
vendored
|
|
@ -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:*)"'
|
||||
48
.github/workflows/discord-summary.yml
vendored
48
.github/workflows/discord-summary.yml
vendored
|
|
@ -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"
|
||||
279
.github/workflows/endpoint-health-check.yml
vendored
279
.github/workflows/endpoint-health-check.yml
vendored
|
|
@ -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
|
||||
18
.github/workflows/health-check.yml
vendored
18
.github/workflows/health-check.yml
vendored
|
|
@ -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"
|
||||
135
.github/workflows/integration-tests.yml
vendored
135
.github/workflows/integration-tests.yml
vendored
|
|
@ -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
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 21
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
|
|
|
|||
18
.github/workflows/sync-changes.yml
vendored
18
.github/workflows/sync-changes.yml
vendored
|
|
@ -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"
|
||||
114
.github/workflows/sync-enrich.yml
vendored
114
.github/workflows/sync-enrich.yml
vendored
|
|
@ -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"
|
||||
131
.github/workflows/sync-keyword.yml
vendored
131
.github/workflows/sync-keyword.yml
vendored
|
|
@ -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"
|
||||
97
.github/workflows/sync-manual.yml
vendored
97
.github/workflows/sync-manual.yml
vendored
|
|
@ -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"
|
||||
18
.github/workflows/sync-metrics.yml
vendored
18
.github/workflows/sync-metrics.yml
vendored
|
|
@ -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"
|
||||
95
.github/workflows/sync-package.yml
vendored
95
.github/workflows/sync-package.yml
vendored
|
|
@ -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"
|
||||
267
.github/workflows/sync-vercel-registry.yml
vendored
267
.github/workflows/sync-vercel-registry.yml
vendored
|
|
@ -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 "════════════════════════════════════════"
|
||||
89
.github/workflows/tool-request.yml
vendored
89
.github/workflows/tool-request.yml
vendored
|
|
@ -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
|
||||
69
.github/workflows/update-docs.yml
vendored
69
.github/workflows/update-docs.yml
vendored
|
|
@ -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
53
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
49
.gitsecrets
49
.gitsecrets
|
|
@ -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
31
.ignore
|
|
@ -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
1
.nvmrc
|
|
@ -1 +0,0 @@
|
|||
22
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
node_modules
|
||||
.turbo
|
||||
.next
|
||||
dist
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -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
210
AGENTS.md
|
|
@ -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
|
||||
844
ARCHITECTURE.md
844
ARCHITECTURE.md
|
|
@ -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
340
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
180
DEPLOYMENT.md
180
DEPLOYMENT.md
|
|
@ -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
|
||||
[](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.
|
||||
559
DESIGN_SYSTEM.md
559
DESIGN_SYSTEM.md
|
|
@ -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*
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
331
LAUNCH_REVIEW.md
331
LAUNCH_REVIEW.md
|
|
@ -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
21
LICENSE
|
|
@ -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.
|
||||
293
MANUAL_TOOLS.md
293
MANUAL_TOOLS.md
|
|
@ -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
|
||||
118
QUALITY-GATES.md
118
QUALITY-GATES.md
|
|
@ -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
|
||||
```
|
||||
92
README.md
92
README.md
|
|
@ -1,59 +1,8 @@
|
|||
# TPMJS
|
||||
# TPMJS Monorepo
|
||||
|
||||
[](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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
391
TPMJS_TALK.md
391
TPMJS_TALK.md
|
|
@ -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.*
|
||||
1
apps/omega-mac/.gitignore
vendored
1
apps/omega-mac/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
.build
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue