import type { ServerProvider, ServerProviderSkill, ServerProviderSlashCommand, } from "@t3tools/contracts"; export type ProviderSkillSourceKind = "app" | "repo" | "project" | "personal" | "system" | "other"; function titleCaseWords(value: string): string { const words: string[] = []; for (const segment of value.split(/[\s:_-]+/)) { if (segment.length === 0) continue; words.push(segment.charAt(0).toUpperCase() + segment.slice(1)); } return words.join(" "); } function normalizePathSeparators(pathValue: string): string { return pathValue.replaceAll("\\", "/"); } export function formatProviderSkillDisplayName( skill: Pick, ): string { const displayName = skill.displayName?.trim(); if (displayName) { return displayName; } return titleCaseWords(skill.name); } export function dedupeProviderSkillsByName( skills: ReadonlyArray, ): ServerProviderSkill[] { const seenNames = new Set(); return skills.filter((skill) => { const normalizedName = skill.name.trim().toLowerCase(); if (seenNames.has(normalizedName)) { return false; } seenNames.add(normalizedName); return true; }); } /** * Whether a composer pick can start this skill. A skill switched off in the * provider's settings will not run, and one the provider reserves for the * agent (Claude Code's `user-invocable: false`) rejects a user invocation. * Everything else, including skills the agent may not start on its own, is * fair game: the server dispatches the pick in the provider's native form. */ export function isProviderSkillUserInvocable( skill: Pick, ): boolean { return skill.enabled && skill.userInvocable !== false; } export function getProviderSkillsForSlashMenu( skills: ReadonlyArray, showSkillsInSlashMenu: boolean, ): ServerProviderSkill[] { return showSkillsInSlashMenu ? dedupeProviderSkillsByName(skills.filter(isProviderSkillUserInvocable)) : []; } export function getProviderSlashCommandsForSlashMenu( slashCommands: ReadonlyArray, visibleSkills: ReadonlyArray, ): ServerProviderSlashCommand[] { const skillNames = new Set(visibleSkills.map((skill) => skill.name.trim().toLowerCase())); return slashCommands.filter((command) => !skillNames.has(command.name.trim().toLowerCase())); } export function resolveProviderSkillSourceKind( skill: Pick, ): ProviderSkillSourceKind { const normalizedPath = normalizePathSeparators(skill.path); if (normalizedPath.includes("/.codex/plugins/") || normalizedPath.includes("/.agents/plugins/")) { return "app"; } const normalizedScope = skill.scope?.trim().toLowerCase(); switch (normalizedScope) { case "repo": case "repository": return "repo"; case "project": case "workspace": case "local": return "project"; case "user": case "personal": return "personal"; case "system": return "system"; case undefined: case "": return "other"; default: return "other"; } } function resolveProviderWorkspaceSnapshot( provider: ServerProvider, cwd: string | null | undefined, ) { if (!cwd) return undefined; return provider.workspaceSnapshots?.find((snapshot) => snapshot.cwd === cwd); } export function resolveProviderSkillsForCwd( provider: ServerProvider, cwd: string | null | undefined, ): ServerProvider["skills"] { return resolveProviderWorkspaceSnapshot(provider, cwd)?.skills ?? provider.skills; } export function resolveProviderSlashCommandsForCwd( provider: ServerProvider, cwd: string | null | undefined, ): ServerProvider["slashCommands"] { return resolveProviderWorkspaceSnapshot(provider, cwd)?.slashCommands ?? provider.slashCommands; }