feat: implement multi-tool package architecture with manual tool registry
BREAKING CHANGE: Complete refactoring from single-tool to multi-tool package support Database Schema: - Split Tool model into Package (1) and Tool (many) with one-to-many relationship - Package stores npm metadata and package-level tpmjs fields (category, env, frameworks, tier) - Tool stores individual tool exports with tool-level metadata (exportName, description, parameters, returns, aiAgent) - Unique constraint on (packageId, exportName) to prevent duplicate tools - Cascade deletes when packages are removed Type System: - Updated tpmjs field schema to support tools array - Each tool has exportName, description, parameters, returns, aiAgent - Package-level fields: category, env, frameworks shared across all tools - Backward compatible with legacy single-tool format (auto-migrates to exportName: "default") API Updates: - Updated all /api/tools routes to query Tool model with Package relations - Updated /api/tools/[slug] to accept package/export path segments - Updated tool-executor-agent to use actual exportName instead of hardcoded "default" - Updated metrics sync to calculate quality scores per Tool Frontend Updates: - Updated tool search page to display exportName as primary heading - Updated tool detail pages to show package name as secondary info - Removed tag-based filtering (tags moved to package level) Manual Tool Registry: - Added manual-tools.ts with 23 curated tools from major providers - Created sync-manual-tools.ts script to sync manual tools to database - Added MANUAL_TOOLS.md documentation for manual tool system - Added GitHub workflow for automated daily sync - Includes tools from: Vercel, Exa, Firecrawl, AWS Bedrock, Perplexity, Tavily, Superagent, Valyu Playground Updates: - Updated tool loader to load multiple tools per package - Added sanitizeToolName for OpenAI API compatibility Sync System Updates: - Updated changes feed sync to handle multi-tool packages - Updated keyword sync to upsert multiple tools per package - Added orphaned tool deletion when tools removed from package.json Migration Strategy: - Database uses same Neon instance for dev and prod - Schema updated via prisma db push (no migration files yet) - All data repopulates from npm via sync system 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f59c2c5123
commit
141a64d888
31 changed files with 3783 additions and 786 deletions
|
|
@ -67,23 +67,53 @@ export const TpmjsAiAgentSchema = z.object({
|
|||
export type TpmjsAiAgent = z.infer<typeof TpmjsAiAgentSchema>;
|
||||
|
||||
/**
|
||||
* Minimal tier schema - required fields only
|
||||
* This is the minimum required to publish a tool to TPMJS
|
||||
* Individual tool definition within a multi-tool package
|
||||
*/
|
||||
export const TpmjsMinimalSchema = z.object({
|
||||
export const TpmjsToolDefinitionSchema = z.object({
|
||||
exportName: z.string().min(1, 'Export name is required'),
|
||||
description: z.string().min(20, 'Description must be at least 20 characters').max(500),
|
||||
parameters: z.array(TpmjsParameterSchema).optional(),
|
||||
returns: TpmjsReturnsSchema.optional(),
|
||||
aiAgent: TpmjsAiAgentSchema.optional(),
|
||||
});
|
||||
|
||||
export type TpmjsToolDefinition = z.infer<typeof TpmjsToolDefinitionSchema>;
|
||||
|
||||
/**
|
||||
* Multi-tool format - NEW SCHEMA
|
||||
* Package-level metadata with array of tools
|
||||
*/
|
||||
export const TpmjsMultiToolSchema = z.object({
|
||||
category: z.enum(TPMJS_CATEGORIES, {
|
||||
message: `Category must be one of: ${TPMJS_CATEGORIES.join(', ')}`,
|
||||
}),
|
||||
tools: z.array(TpmjsToolDefinitionSchema).min(1, 'At least one tool is required'),
|
||||
env: z.array(TpmjsEnvSchema).optional(),
|
||||
frameworks: z
|
||||
.array(z.enum(['vercel-ai', 'langchain', 'llamaindex', 'haystack', 'semantic-kernel']))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TpmjsMultiTool = z.infer<typeof TpmjsMultiToolSchema>;
|
||||
|
||||
/**
|
||||
* Legacy minimal tier schema - DEPRECATED
|
||||
* Kept for backward compatibility with auto-migration
|
||||
*/
|
||||
export const TpmjsLegacyMinimalSchema = z.object({
|
||||
category: z.enum(TPMJS_CATEGORIES, {
|
||||
message: `Category must be one of: ${TPMJS_CATEGORIES.join(', ')}`,
|
||||
}),
|
||||
description: z.string().min(20, 'Description must be at least 20 characters').max(500),
|
||||
});
|
||||
|
||||
export type TpmjsMinimal = z.infer<typeof TpmjsMinimalSchema>;
|
||||
export type TpmjsLegacyMinimal = z.infer<typeof TpmjsLegacyMinimalSchema>;
|
||||
|
||||
/**
|
||||
* Rich tier schema - includes optional enhanced metadata
|
||||
* Tools with these fields get better visibility and quality scores
|
||||
* Legacy rich tier schema - DEPRECATED
|
||||
* Kept for backward compatibility with auto-migration
|
||||
*/
|
||||
export const TpmjsRichSchema = TpmjsMinimalSchema.extend({
|
||||
export const TpmjsLegacyRichSchema = TpmjsLegacyMinimalSchema.extend({
|
||||
parameters: z.array(TpmjsParameterSchema).optional(),
|
||||
returns: TpmjsReturnsSchema.optional(),
|
||||
env: z.array(TpmjsEnvSchema).optional(),
|
||||
|
|
@ -93,57 +123,129 @@ export const TpmjsRichSchema = TpmjsMinimalSchema.extend({
|
|||
aiAgent: TpmjsAiAgentSchema.optional(),
|
||||
});
|
||||
|
||||
export type TpmjsRich = z.infer<typeof TpmjsRichSchema>;
|
||||
export type TpmjsLegacyRich = z.infer<typeof TpmjsLegacyRichSchema>;
|
||||
|
||||
/**
|
||||
* Union type for either tier
|
||||
* Union type for legacy formats
|
||||
*/
|
||||
export type TpmjsField = TpmjsMinimal | TpmjsRich;
|
||||
export type TpmjsLegacy = TpmjsLegacyMinimal | TpmjsLegacyRich;
|
||||
|
||||
/**
|
||||
* Validation result type
|
||||
* Union type for all formats (new multi-tool + legacy)
|
||||
*/
|
||||
export type TpmjsField = TpmjsMultiTool | TpmjsLegacy;
|
||||
|
||||
// Backward compatibility aliases
|
||||
export type TpmjsMinimal = TpmjsLegacyMinimal;
|
||||
export type TpmjsRich = TpmjsLegacyRich;
|
||||
export const TpmjsMinimalSchema = TpmjsLegacyMinimalSchema;
|
||||
export const TpmjsRichSchema = TpmjsLegacyRichSchema;
|
||||
|
||||
/**
|
||||
* Extended validation result type for multi-tool support
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
tier: 'minimal' | 'rich' | null;
|
||||
data?: TpmjsField;
|
||||
errors?: z.ZodError;
|
||||
// New fields for multi-tool support
|
||||
packageData?: {
|
||||
category: TpmjsCategory;
|
||||
env?: TpmjsEnv[];
|
||||
frameworks?: string[];
|
||||
};
|
||||
tools?: TpmjsToolDefinition[];
|
||||
wasLegacyFormat?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a tpmjs field and determines its tier
|
||||
* Supports both new multi-tool format and legacy single-tool format with auto-migration
|
||||
*/
|
||||
export function validateTpmjsField(tpmjs: unknown): ValidationResult {
|
||||
// Try rich tier first
|
||||
const richResult = TpmjsRichSchema.safeParse(tpmjs);
|
||||
if (richResult.success) {
|
||||
// Check if it has any rich-tier fields
|
||||
const data = richResult.data;
|
||||
const hasRichFields =
|
||||
data.parameters || data.returns || data.env || data.frameworks || data.aiAgent;
|
||||
// Try new multi-tool format first
|
||||
const multiResult = TpmjsMultiToolSchema.safeParse(tpmjs);
|
||||
if (multiResult.success) {
|
||||
const data = multiResult.data;
|
||||
|
||||
// Determine tier based on tool richness
|
||||
const hasRichFields = data.tools.some(
|
||||
(tool) => tool.parameters || tool.returns || tool.aiAgent
|
||||
) || data.env || data.frameworks;
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tier: hasRichFields ? 'rich' : 'minimal',
|
||||
data: richResult.data,
|
||||
data: data,
|
||||
packageData: {
|
||||
category: data.category,
|
||||
env: data.env,
|
||||
frameworks: data.frameworks,
|
||||
},
|
||||
tools: data.tools,
|
||||
wasLegacyFormat: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Try minimal tier
|
||||
const minimalResult = TpmjsMinimalSchema.safeParse(tpmjs);
|
||||
// Try legacy rich tier format with auto-migration
|
||||
const richResult = TpmjsLegacyRichSchema.safeParse(tpmjs);
|
||||
if (richResult.success) {
|
||||
const legacyData = richResult.data;
|
||||
|
||||
// Auto-migrate to multi-tool format
|
||||
const tool: TpmjsToolDefinition = {
|
||||
exportName: 'default',
|
||||
description: legacyData.description,
|
||||
parameters: legacyData.parameters,
|
||||
returns: legacyData.returns,
|
||||
aiAgent: legacyData.aiAgent,
|
||||
};
|
||||
|
||||
const hasRichFields =
|
||||
legacyData.parameters || legacyData.returns || legacyData.env ||
|
||||
legacyData.frameworks || legacyData.aiAgent;
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tier: hasRichFields ? 'rich' : 'minimal',
|
||||
data: legacyData,
|
||||
packageData: {
|
||||
category: legacyData.category,
|
||||
env: legacyData.env,
|
||||
frameworks: legacyData.frameworks,
|
||||
},
|
||||
tools: [tool],
|
||||
wasLegacyFormat: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Try legacy minimal tier format with auto-migration
|
||||
const minimalResult = TpmjsLegacyMinimalSchema.safeParse(tpmjs);
|
||||
if (minimalResult.success) {
|
||||
// Auto-migrate to multi-tool format
|
||||
const tool: TpmjsToolDefinition = {
|
||||
exportName: 'default',
|
||||
description: minimalResult.data.description,
|
||||
};
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tier: 'minimal',
|
||||
data: minimalResult.data,
|
||||
packageData: {
|
||||
category: minimalResult.data.category,
|
||||
},
|
||||
tools: [tool],
|
||||
wasLegacyFormat: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Invalid
|
||||
// Invalid - return error from multi-tool schema (most informative)
|
||||
return {
|
||||
valid: false,
|
||||
tier: null,
|
||||
errors: minimalResult.error,
|
||||
errors: multiResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue