fix(cli): resolve SKILL.md not found errors from duplicate path prefixes

When skillPath from the registry had a prefix (e.g., "skills/foo"), fallback
paths were incorrectly generated with duplicate prefixes like
".claude/skills/skills/foo/SKILL.md". Now extracts the leaf skill name to
build correct fallback paths. Also adds raw.githubusercontent.com as a
universal fallback for 404s, improves error messages with actual paths tried,
and skips API results that lack the main instruction file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-13 04:46:22 +03:30
parent 83a3766838
commit 394830280f
2 changed files with 63 additions and 15 deletions

View File

@@ -103,10 +103,16 @@ export async function install(skillId: string, options: InstallOptions): Promise
sourceFormat = cachedFiles.sourceFormat as SourceFormat;
}
// Convert API response to SkillContent format
content = convertCachedFilesToSkillContent(cachedFiles, sourceFormat);
const converted = convertCachedFilesToSkillContent(cachedFiles, sourceFormat);
// Only use API result if the main instruction file was found
if (converted.skillMd) {
content = converted;
spinner.text = cachedFiles.fromCache
? `Using cached files (${cachedFiles.files.length} files)`
: `Downloaded ${cachedFiles.files.length} files via API`;
} else {
spinner.text = 'API returned files but main instruction file missing, falling back...';
}
}
} catch {
// API was reachable but file fetch failed (timeout, server error, etc.)

View File

@@ -100,12 +100,26 @@ export async function fetchSkillContent(
}
} else {
// SKILL.md - try multiple common paths
pathsToTry = [
basePath,
...(skillPath && !skillPath.startsWith('skills/') ? [`skills/${skillPath}/SKILL.md`] : []),
...(skillPath && !skillPath.startsWith('.claude/') ? [`.claude/skills/${skillPath}/SKILL.md`] : []),
...(skillPath && !skillPath.startsWith('.github/') ? [`.github/skills/${skillPath}/SKILL.md`] : []),
// Extract the leaf skill name to avoid duplicate prefixes
// e.g., "skills/claude-md-architect" → "claude-md-architect"
const skillName = skillPath
? skillPath.replace(/^(skills|\.claude\/skills|\.github\/skills)\//, '')
: '';
pathsToTry = [basePath];
if (skillName) {
// Add all common locations, deduplicating against basePath
const candidates = [
`skills/${skillName}/${filename}`,
`.claude/skills/${skillName}/${filename}`,
`.github/skills/${skillName}/${filename}`,
];
for (const candidate of candidates) {
if (candidate !== basePath) {
pathsToTry.push(candidate);
}
}
}
}
for (const pathToTry of pathsToTry) {
@@ -123,7 +137,6 @@ export async function fetchSkillContent(
if (error.message?.includes('timeout') || error.message?.includes('network')) {
try {
const rawContent = await fetchRawFile(owner, repo, pathToTry, branch);
// Create a mock response compatible with Octokit
skillMdResponse = {
data: {
content: Buffer.from(rawContent).toString('base64'),
@@ -131,22 +144,51 @@ export async function fetchSkillContent(
}
} as any;
break;
} catch (rawError) {
// If raw fetch also fails, throw original error
throw new Error(`GitHub API timeout. Try using --no-api flag or check your network connection.`);
} catch {
// Raw fetch also failed, continue to next path
continue;
}
}
// If 404, try next path
if (error.status === 404) {
continue;
}
// Rate limiting - provide a clear error
if (error.status === 403) {
throw new Error(
`GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable for higher rate limits.`
);
}
// Other errors, throw immediately
throw new Error(`Failed to fetch from GitHub: ${error.message}`);
}
}
// If Octokit failed on all paths, try raw.githubusercontent.com as last resort
// (handles cases where GitHub API returns 404 but raw CDN has the file,
// e.g., during API caching delays or edge cases)
if (!skillMdResponse) {
throw new Error(`${filename} not found at ${owner}/${repo} (tried ${pathsToTry.length} paths)`);
for (const pathToTry of pathsToTry) {
try {
const rawContent = await fetchRawFile(owner, repo, pathToTry, branch);
skillMdResponse = {
data: {
content: Buffer.from(rawContent).toString('base64'),
encoding: 'base64' as const,
}
} as any;
break;
} catch {
continue;
}
}
}
if (!skillMdResponse) {
const triedPaths = pathsToTry.map(p => ` - ${owner}/${repo}/${p}`).join('\n');
throw new Error(
`${filename} not found at ${owner}/${repo} (tried ${pathsToTry.length} paths):\n${triedPaths}\n\nThe skill may have been moved or deleted from the repository.`
);
}
if (!('content' in skillMdResponse.data)) {