diff --git a/src/cli/skills-cli.format.ts b/src/cli/skills-cli.format.ts index 045281bc7d1..977f0f651bf 100644 --- a/src/cli/skills-cli.format.ts +++ b/src/cli/skills-cli.format.ts @@ -94,6 +94,52 @@ function formatSkillMissingSummary(skill: SkillStatusEntry): string { return missing.join("; "); } +function normalizeSkillLookupToken(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[\s_/]+/g, "-") + .replace(/[^a-z0-9-]+/g, "") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function resolveSkillByName( + report: SkillStatusReport, + requestedName: string, +): SkillStatusEntry | null { + const raw = requestedName.trim(); + if (!raw) { + return null; + } + + const direct = report.skills.find((s) => s.name === raw || s.skillKey === raw); + if (direct) { + return direct; + } + + const lower = raw.toLowerCase(); + const caseInsensitive = report.skills.find( + (s) => s.name.toLowerCase() === lower || s.skillKey.toLowerCase() === lower, + ); + if (caseInsensitive) { + return caseInsensitive; + } + + const normalized = normalizeSkillLookupToken(raw); + if (!normalized) { + return null; + } + + return ( + report.skills.find( + (s) => + normalizeSkillLookupToken(s.name) === normalized || + normalizeSkillLookupToken(s.skillKey) === normalized, + ) ?? null + ); +} + export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOptions): string { const skills = opts.eligible ? report.skills.filter((s) => s.eligible) : report.skills; @@ -168,14 +214,15 @@ export function formatSkillInfo( skillName: string, opts: SkillInfoOptions, ): string { - const skill = report.skills.find((s) => s.name === skillName || s.skillKey === skillName); + const requestedName = skillName.trim(); + const skill = resolveSkillByName(report, requestedName); if (!skill) { if (opts.json) { - return JSON.stringify({ error: "not found", skill: skillName }, null, 2); + return JSON.stringify({ error: "not found", skill: requestedName }, null, 2); } return appendClawHubHint( - `Skill "${skillName}" not found. Run \`${formatCliCommand("openclaw skills list")}\` to see available skills.`, + `Skill "${requestedName}" not found. Run \`${formatCliCommand("openclaw skills list")}\` to see available skills.`, opts.json, ); } diff --git a/src/cli/skills-cli.test.ts b/src/cli/skills-cli.test.ts index 27031fc0fdf..ad7ee0ffa6e 100644 --- a/src/cli/skills-cli.test.ts +++ b/src/cli/skills-cli.test.ts @@ -149,16 +149,30 @@ describe("skills-cli", () => { expect(output).toContain("API_KEY"); }); - it("normalizes text-presentation emoji selectors in info output", () => { + it("resolves skill info case-insensitively", () => { const report = createMockReport([ createMockSkill({ - name: "info-emoji", - emoji: "🎛\uFE0E", + name: "Excel XLSX", + skillKey: "Excel-XLSX", + description: "Spreadsheet helpers", }), ]); - const output = formatSkillInfo(report, "info-emoji", {}); - expect(output).toContain("🎛️"); + const output = formatSkillInfo(report, "excel-xlsx", {}); + expect(output).toContain("Spreadsheet helpers"); + }); + + it("resolves skill info across separator variants", () => { + const report = createMockReport([ + createMockSkill({ + name: "Excel XLSX", + skillKey: "excel_xlsx", + description: "Spreadsheet helpers", + }), + ]); + + const output = formatSkillInfo(report, "excel-xlsx", {}); + expect(output).toContain("Spreadsheet helpers"); }); });