diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 4350596700..2589d3fc20 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -34,6 +34,7 @@ import { } from "../providerSnapshot.ts"; import { compareCliVersions } from "../cliVersion.ts"; import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; +import { claudeSlashCommandsAsProviderSkills } from "../claudeSlashCommandsAsProviderSkills.ts"; const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], @@ -622,6 +623,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( : undefined; const slashCommands = capabilities?.slashCommands ?? []; const dedupedSlashCommands = dedupeSlashCommands(slashCommands); + const skillsFromSlashCommands = claudeSlashCommandsAsProviderSkills(dedupedSlashCommands); if (!capabilities) { return buildServerProvider({ @@ -630,6 +632,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( checkedAt, models, slashCommands: dedupedSlashCommands, + skills: skillsFromSlashCommands, probe: { installed: true, version: parsedVersion, @@ -650,6 +653,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( checkedAt, models, slashCommands: dedupedSlashCommands, + skills: skillsFromSlashCommands, probe: { installed: true, version: parsedVersion, diff --git a/apps/server/src/provider/claudeSlashCommandsAsProviderSkills.test.ts b/apps/server/src/provider/claudeSlashCommandsAsProviderSkills.test.ts new file mode 100644 index 0000000000..18fe1ca4c7 --- /dev/null +++ b/apps/server/src/provider/claudeSlashCommandsAsProviderSkills.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { claudeSlashCommandsAsProviderSkills } from "./claudeSlashCommandsAsProviderSkills.ts"; + +describe("claudeSlashCommandsAsProviderSkills", () => { + it("maps slash commands to skills with stable synthetic paths", () => { + const skills = claudeSlashCommandsAsProviderSkills([ + { name: "frontend-design", description: "UI help" }, + { name: "gh-fix-ci", input: { hint: "[pr-url]" } }, + ]); + expect(skills).toHaveLength(2); + expect(skills[0]).toMatchObject({ + name: "frontend-design", + enabled: true, + scope: "claude-agent", + description: "UI help", + shortDescription: "UI help", + path: "claude-agent:///slash-command/frontend-design", + }); + expect(skills[1]).toMatchObject({ + name: "gh-fix-ci", + enabled: true, + scope: "claude-agent", + shortDescription: "[pr-url]", + path: "claude-agent:///slash-command/gh-fix-ci", + }); + }); + + it("encodes command names in the path", () => { + const [skill] = claudeSlashCommandsAsProviderSkills([{ name: "a:b" }]); + expect(skill?.path).toBe("claude-agent:///slash-command/a%3Ab"); + expect(skill?.name).toBe("a:b"); + }); +}); diff --git a/apps/server/src/provider/claudeSlashCommandsAsProviderSkills.ts b/apps/server/src/provider/claudeSlashCommandsAsProviderSkills.ts new file mode 100644 index 0000000000..981c48f9ed --- /dev/null +++ b/apps/server/src/provider/claudeSlashCommandsAsProviderSkills.ts @@ -0,0 +1,24 @@ +import type { ServerProviderSlashCommand, ServerProviderSkill } from "@t3tools/contracts"; + +/** + * Claude reports capabilities as slash commands from SDK init. The composer + * `$` picker reads {@link ServerProviderSkill}; mirror the same inventory so + * `$` works for Claude the way Codex does with `skills/list`. + */ +export function claudeSlashCommandsAsProviderSkills( + commands: ReadonlyArray, +): ReadonlyArray { + return commands.map((command) => { + const hint = command.input?.hint?.trim(); + const desc = command.description?.trim(); + const shortDescription = hint ?? desc; + return { + name: command.name, + path: `claude-agent:///slash-command/${encodeURIComponent(command.name)}`, + enabled: true, + scope: "claude-agent", + ...(desc ? { description: desc } : {}), + ...(shortDescription ? { shortDescription } : {}), + } satisfies ServerProviderSkill; + }); +}