diff --git a/server/src/__tests__/company-skills.test.ts b/server/src/__tests__/company-skills.test.ts index 17da78042..bcc173d8b 100644 --- a/server/src/__tests__/company-skills.test.ts +++ b/server/src/__tests__/company-skills.test.ts @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { discoverProjectWorkspaceSkillDirectories, findMissingLocalSkillIds, + normalizeGitHubSkillDirectory, parseSkillImportSourceInput, readLocalSkillImportFromDirectory, } from "../services/company-skills.js"; @@ -86,6 +87,13 @@ describe("company skill import source parsing", () => { }); describe("project workspace skill discovery", () => { + it("normalizes GitHub skill directories for blob imports and legacy metadata", () => { + expect(normalizeGitHubSkillDirectory("retro/.", "retro")).toBe("retro"); + expect(normalizeGitHubSkillDirectory("retro/SKILL.md", "retro")).toBe("retro"); + expect(normalizeGitHubSkillDirectory("SKILL.md", "root-skill")).toBe(""); + expect(normalizeGitHubSkillDirectory("", "fallback-skill")).toBe("fallback-skill"); + }); + it("finds bounded skill roots under supported workspace paths", async () => { const workspace = await makeTempDir("paperclip-skill-workspace-"); await writeSkillDir(workspace, "Workspace Root"); diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index 43de8ee6b..2b97da208 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -190,6 +190,18 @@ function normalizeSkillKey(value: string | null | undefined) { return segments.length > 0 ? segments.join("/") : null; } +export function normalizeGitHubSkillDirectory( + value: string | null | undefined, + fallback: string, +) { + const normalized = normalizePortablePath(value ?? ""); + if (!normalized) return normalizePortablePath(fallback); + if (path.posix.basename(normalized).toLowerCase() === "skill.md") { + return normalizePortablePath(path.posix.dirname(normalized)); + } + return normalized; +} + function hashSkillValue(value: string) { return createHash("sha256").update(value).digest("hex").slice(0, 10); } @@ -1019,7 +1031,10 @@ async function readUrlSkillImports( repo: parsed.repo, ref: ref, trackingRef, - repoSkillDir: basePrefix ? `${basePrefix}${skillDir}` : skillDir, + repoSkillDir: normalizeGitHubSkillDirectory( + basePrefix ? `${basePrefix}${skillDir}` : skillDir, + slug, + ), }; const inventory = filteredPaths .filter((entry) => entry === relativeSkillPath || entry.startsWith(`${skillDir}/`)) @@ -1665,7 +1680,7 @@ export function companySkillService(db: Db) { const owner = asString(metadata.owner); const repo = asString(metadata.repo); const ref = skill.sourceRef ?? asString(metadata.ref) ?? "main"; - const repoSkillDir = normalizePortablePath(asString(metadata.repoSkillDir) ?? skill.slug); + const repoSkillDir = normalizeGitHubSkillDirectory(asString(metadata.repoSkillDir), skill.slug); if (!owner || !repo) { throw unprocessable("Skill source metadata is incomplete."); }