diff --git a/.gitignore b/.gitignore index 82e41184..fdc480a7 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ coverage/ *.seed *.pid.lock +.worktrees diff --git a/src/core/resources/agent/config.ts b/src/core/resources/agent/config.ts index c29a5a3b..e4a77636 100644 --- a/src/core/resources/agent/config.ts +++ b/src/core/resources/agent/config.ts @@ -14,9 +14,22 @@ import { import type { AgentConfig, AgentConfigApiResponse } from "./schema.js"; import { AgentConfigSchema } from "./schema.js"; +/** + * Convert an agent name to a filesystem-safe filename slug. + * Lowercases, replaces non-alphanumeric characters with underscores, + * and collapses consecutive underscores. + */ +function toFileSlug(name: string): string { + return name + .toLowerCase() + .replace(/[^a-z0-9_]/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, ""); +} + export function generateAgentConfigContent(name: string): string { return `// Base44 Agent Configuration -// Agent name must be lowercase alphanumeric with underscores only +// Agent configuration file { "name": "${name}", // Brief description of what this agent does @@ -82,7 +95,8 @@ export async function writeAgents( const toDelete = existingAgents.filter((a) => !newNames.has(a.name)); for (const agent of toDelete) { - const files = await globby(`${agent.name}.${CONFIG_FILE_EXTENSION_GLOB}`, { + const slug = toFileSlug(agent.name); + const files = await globby(`${slug}.${CONFIG_FILE_EXTENSION_GLOB}`, { cwd: agentsDir, absolute: true, }); @@ -92,7 +106,8 @@ export async function writeAgents( } for (const agent of remoteAgents) { - const filePath = join(agentsDir, `${agent.name}.${CONFIG_FILE_EXTENSION}`); + const slug = toFileSlug(agent.name); + const filePath = join(agentsDir, `${slug}.${CONFIG_FILE_EXTENSION}`); await writeJsonFile(filePath, agent); } diff --git a/src/core/resources/agent/schema.ts b/src/core/resources/agent/schema.ts index 9ea14472..be07055c 100644 --- a/src/core/resources/agent/schema.ts +++ b/src/core/resources/agent/schema.ts @@ -18,14 +18,7 @@ const ToolConfigSchema = z.union([ ]); export const AgentConfigSchema = z.looseObject({ - name: z - .string() - .regex( - /^[a-z0-9_]+$/, - "Agent name must be lowercase alphanumeric with underscores" - ) - .min(1) - .max(100), + name: z.string().trim().min(1).max(100), description: z.string().trim().min(1, "Description is required"), instructions: z.string().trim().min(1, "Instructions are required"), tool_configs: z.array(ToolConfigSchema).optional().default([]), diff --git a/tests/cli/agents_push.spec.ts b/tests/cli/agents_push.spec.ts index 5e8f639b..8b8906b6 100644 --- a/tests/cli/agents_push.spec.ts +++ b/tests/cli/agents_push.spec.ts @@ -54,13 +54,12 @@ describe("agents push command", () => { t.expectResult(result).toContain("Deleted: old_agent"); }); - it("fails with helpful error when agent has invalid name format", async () => { + it("fails with helpful error when agent has empty name", async () => { await t.givenLoggedInWithProject(fixture("invalid-agent")); const result = await t.run("agents", "push"); t.expectResult(result).toFail(); - t.expectResult(result).toContain("name"); }); it("fails when API returns error", async () => { diff --git a/tests/fixtures/invalid-agent/base44/agents/broken.json b/tests/fixtures/invalid-agent/base44/agents/broken.json index bd75d157..4b220d8f 100644 --- a/tests/fixtures/invalid-agent/base44/agents/broken.json +++ b/tests/fixtures/invalid-agent/base44/agents/broken.json @@ -1,4 +1,5 @@ { - "name": "INVALID-NAME!", - "description": "" + "name": "", + "description": "A valid description", + "instructions": "Valid instructions" }