diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts index c4b328cdc337..d22c14f429b7 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts @@ -233,6 +233,57 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { }), ); + it.effect("uses the display name a skill declares for Codex", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); + const skillsDir = path.join(tempDir, "claude-home", "skills"); + const codexMetadata = { + "hr-handoff": 'interface:\n display_name: "HR: Handoff"\n', + "same-name": "interface:\n display_name: same-name\n", + "blank-name": 'interface:\n display_name: " "\n', + "broken-yaml": "interface: [unclosed\n", + "no-interface": "policy:\n allow_implicit_invocation: true\n", + "scalar-document": "just text\n", + "not-a-string": "interface:\n display_name: 3\n", + }; + for (const [directoryName, metadata] of Object.entries(codexMetadata)) { + yield* writeSkill(skillsDir, directoryName, "---\ndescription: A skill.\n---\n"); + yield* fs.makeDirectory(path.join(skillsDir, directoryName, "agents")); + yield* fs.writeFileString( + path.join(skillsDir, directoryName, "agents", "openai.yaml"), + metadata, + ); + } + yield* writeSkill(skillsDir, "no-metadata", "---\ndescription: A skill.\n---\n"); + yield* writeSkill(skillsDir, "directory-metadata", "---\ndescription: A skill.\n---\n"); + yield* fs.makeDirectory(path.join(skillsDir, "directory-metadata", "agents", "openai.yaml"), { + recursive: true, + }); + + const skills = yield* discoverClaudeSkills( + { homePath: path.join(tempDir, "claude-home") }, + undefined, + ); + + assert.deepEqual( + skills.map((skill) => [skill.name, skill.displayName, skill.description]), + [ + ["blank-name", undefined, "A skill."], + ["broken-yaml", undefined, "A skill."], + ["directory-metadata", undefined, "A skill."], + ["hr-handoff", "HR: Handoff", "A skill."], + ["no-interface", undefined, "A skill."], + ["no-metadata", undefined, "A skill."], + ["not-a-string", undefined, "A skill."], + ["same-name", undefined, "A skill."], + ["scalar-document", undefined, "A skill."], + ], + ); + }), + ); + it.effect("honors CLAUDE_CONFIG_DIR from the environment when homePath is unset", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index 3ef7d925e031..5c7573e8afed 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -116,6 +116,26 @@ function parseSkillFrontmatter(contents: string): SkillFrontmatter { }; } +/** + * The display name a skill declares for Codex in `agents/openai.yaml` beside + * its `SKILL.md`. Claude Code has no display-name field and requires a + * lowercase `name`, so a skill shared with Codex would otherwise show its + * title-cased directory name here and its chosen label under Codex. + */ +function parseCodexDisplayName(contents: string): string | undefined { + let parsed: unknown; + try { + parsed = parseYamlDocument(contents); + } catch { + return undefined; + } + if (typeof parsed !== "object" || parsed === null) return undefined; + const skillInterface = (parsed as Record).interface; + if (typeof skillInterface !== "object" || skillInterface === null) return undefined; + const displayName = (skillInterface as Record).display_name; + return typeof displayName === "string" && displayName.trim() ? displayName.trim() : undefined; +} + /** * Where an administrator installs the policy file whose settings outrank every * user and project one. Absent on almost every machine, which is why a missing @@ -377,6 +397,12 @@ export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* continue; } + const codexMetadata = yield* fileSystem + .readFileString(path.join(root.directory, entry, "agents", "openai.yaml")) + .pipe(Effect.orElseSucceed(() => undefined)); + const displayName = + codexMetadata === undefined ? undefined : parseCodexDisplayName(codexMetadata); + const override = skillOverrides.get(name); const userInvocationOnly = (frontmatter.kind === "parsed" && frontmatter.userInvocationOnly === true) || @@ -386,6 +412,7 @@ export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* path: skillPath, enabled: override?.enabled ?? true, scope: root.scope, + ...(displayName && displayName !== name ? { displayName } : {}), ...(frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {}),