fix: require name field in TPMJS spec, add workflow permissions

Breaking change for TPMJS spec:
- Remove `exportName` field support from TpmjsToolDefinitionSchema
- Make `name` field required (replaces deprecated `exportName`)
- Update create-basic-tools to generate `name` field
- Update sync routes to use `name` from validated schema
- Add `permissions: contents: write` to Vercel registry sync workflow

Packages using the old `exportName` field must update to use `name`.
This commit is contained in:
Ajax Davis 2025-12-17 16:20:20 +10:00
parent b7ea9bf4d6
commit 5e795bc086
5 changed files with 22 additions and 38 deletions

View file

@ -12,6 +12,9 @@ on:
paths: paths:
- 'sync-vercel-registry.ts' - 'sync-vercel-registry.ts'
permissions:
contents: write
jobs: jobs:
sync-vercel: sync-vercel:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -167,8 +167,8 @@ export async function POST(request: NextRequest) {
// Upsert each tool // Upsert each tool
for (const toolDef of toolsToProcess) { for (const toolDef of toolsToProcess) {
// Use 'name' field (new) or fall back to 'exportName' (legacy support) // Get tool name from validated schema
const toolName = toolDef.name || (toolDef as { exportName?: string }).exportName; const toolName = toolDef.name;
if (!toolName) { if (!toolName) {
console.warn(`Skipping tool without name in ${pkg.name}`); console.warn(`Skipping tool without name in ${pkg.name}`);
continue; continue;
@ -255,10 +255,7 @@ export async function POST(request: NextRequest) {
// Delete orphaned tools (tools removed from package.json) // Delete orphaned tools (tools removed from package.json)
const orphanedTools = existingTools.filter( const orphanedTools = existingTools.filter(
(existingTool) => (existingTool) =>
!toolsToProcess.some((toolDef) => { !toolsToProcess.some((toolDef) => toolDef.name === existingTool.exportName)
const toolName = toolDef.name || (toolDef as { exportName?: string }).exportName;
return toolName === existingTool.exportName;
})
); );
if (orphanedTools.length > 0) { if (orphanedTools.length > 0) {

View file

@ -186,8 +186,8 @@ export async function POST(request: NextRequest) {
// Upsert each tool // Upsert each tool
for (const toolDef of toolsToProcess) { for (const toolDef of toolsToProcess) {
// Use 'name' field (new) or fall back to 'exportName' (legacy support) // Get tool name from validated schema
const toolName = toolDef.name || (toolDef as { exportName?: string }).exportName; const toolName = toolDef.name;
if (!toolName) { if (!toolName) {
console.warn(`Skipping tool without name in ${pkg.name}`); console.warn(`Skipping tool without name in ${pkg.name}`);
continue; continue;
@ -274,10 +274,7 @@ export async function POST(request: NextRequest) {
// Delete orphaned tools (tools removed from package.json) // Delete orphaned tools (tools removed from package.json)
const orphanedTools = existingTools.filter( const orphanedTools = existingTools.filter(
(existingTool) => (existingTool) =>
!toolsToProcess.some((toolDef) => { !toolsToProcess.some((toolDef) => toolDef.name === existingTool.exportName)
const toolName = toolDef.name || (toolDef as { exportName?: string }).exportName;
return toolName === existingTool.exportName;
})
); );
if (orphanedTools.length > 0) { if (orphanedTools.length > 0) {

View file

@ -24,11 +24,8 @@ export function generatePackageJson(config: GeneratorConfig): string {
tpmjs: { tpmjs: {
category: packageInfo.category, category: packageInfo.category,
tools: tools.map((tool) => ({ tools: tools.map((tool) => ({
exportName: tool.exportName, name: tool.exportName,
description: tool.description, description: tool.description,
...(tool.parameters && { parameters: tool.parameters }),
...(tool.returns && { returns: tool.returns }),
...(tool.aiAgent && { aiAgent: tool.aiAgent }),
})), })),
...(tools.some((t) => t.env) && { ...(tools.some((t) => t.env) && {
env: tools.flatMap((t) => t.env || []), env: tools.flatMap((t) => t.env || []),

View file

@ -90,28 +90,18 @@ export type TpmjsAiAgent = z.infer<typeof TpmjsAiAgentSchema>;
* - returns: Tool return type - auto-extracted from tool * - returns: Tool return type - auto-extracted from tool
* - aiAgent: AI agent guidance - auto-extracted from tool * - aiAgent: AI agent guidance - auto-extracted from tool
*/ */
export const TpmjsToolDefinitionSchema = z export const TpmjsToolDefinitionSchema = z.object({
.object({ // Required: The export name of the tool from the package
// New field name (preferred) name: z.string().min(1),
name: z.string().min(1).optional(), // Optional - auto-extracted from tool if not provided
// @deprecated - use 'name' instead. Kept for backward compatibility. description: z.string().min(20, 'Description must be at least 20 characters').max(500).optional(),
exportName: z.string().min(1).optional(), // @deprecated - now auto-extracted from tool's inputSchema
// Optional - auto-extracted from tool if not provided parameters: z.array(TpmjsParameterSchema).optional(),
description: z // @deprecated - now auto-extracted from tool
.string() returns: TpmjsReturnsSchema.optional(),
.min(20, 'Description must be at least 20 characters') // @deprecated - now auto-extracted from tool
.max(500) aiAgent: TpmjsAiAgentSchema.optional(),
.optional(), });
// @deprecated - now auto-extracted from tool's inputSchema
parameters: z.array(TpmjsParameterSchema).optional(),
// @deprecated - now auto-extracted from tool
returns: TpmjsReturnsSchema.optional(),
// @deprecated - now auto-extracted from tool
aiAgent: TpmjsAiAgentSchema.optional(),
})
.refine((data) => data.name || data.exportName, {
message: 'Either name or exportName is required',
});
export type TpmjsToolDefinition = z.infer<typeof TpmjsToolDefinitionSchema>; export type TpmjsToolDefinition = z.infer<typeof TpmjsToolDefinitionSchema>;