Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/cli/commands/plugin-skills.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,8 @@ function extractInlineRef(spec: string): string | undefined {
* If the skill argument is a GitHub URL, extract the skill name and return
* it along with the URL as the plugin source. Returns null if not a URL.
*
* With subpath: skill name = last path segment
* With subpath: skill name = last path segment. A URL ending in SKILL.md is
* normalized to its containing skill directory.
* Without subpath: skill name = repo name (caller should use resolveSkillNameFromRepo to check frontmatter)
*/
export function resolveSkillFromUrl(
Expand All@@ -183,7 +184,18 @@ export function resolveSkillFromUrl(

if (parsed.subpath) {
const segments = parsed.subpath.split('/').filter(Boolean);
const name = segments[segments.length - 1];
if (segments.at(-1) === 'SKILL.md') {
const from = skill.slice(0, -'/SKILL.md'.length);
const containingParsed = parseGitHubUrl(from);
if (!containingParsed) return null;
return {
skill: segments.at(-2) ?? parsed.repo,
from,
parsed: containingParsed,
};
}

const name = segments.at(-1);
if (!name) return null;
return { skill: name, from: skill, parsed };
}
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/cli/skills-add-url-detection.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,37 @@ describe('resolveSkillFromUrl', () => {
});
});

it('resolves a SKILL.md blob URL to its containing skill directory', () => {
const result = resolveSkillFromUrl(
'https://github.com/anthropics/skills/blob/main/skills/pdf/SKILL.md',
);
expect(result).toEqual({
skill: 'pdf',
from: 'https://github.com/anthropics/skills/blob/main/skills/pdf',
parsed: expect.objectContaining({
owner: 'anthropics',
repo: 'skills',
branch: 'main',
subpath: 'skills/pdf',
}),
});
});

it('resolves a root SKILL.md blob URL to the repository root', () => {
const result = resolveSkillFromUrl(
'https://github.com/owner/root-skill/blob/main/SKILL.md',
);
expect(result).toEqual({
skill: 'root-skill',
from: 'https://github.com/owner/root-skill/blob/main',
parsed: {
owner: 'owner',
repo: 'root-skill',
branch: 'main',
},
});
});

it('extracts skill name from URL with deep subpath', () => {
const result = resolveSkillFromUrl(
'https://github.com/org/repo/tree/main/plugins/my-plugin/skills/cool-skill',
Expand Down