From 394830280faa29b936f70b697c257f7eb0ce31e7 Mon Sep 17 00:00:00 2001 From: airano Date: Fri, 13 Feb 2026 04:46:22 +0330 Subject: [PATCH] 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 --- apps/cli/src/commands/install.ts | 14 +++++-- apps/cli/src/utils/github.ts | 64 ++++++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/apps/cli/src/commands/install.ts b/apps/cli/src/commands/install.ts index 59252b6..3e52458 100644 --- a/apps/cli/src/commands/install.ts +++ b/apps/cli/src/commands/install.ts @@ -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); - spinner.text = cachedFiles.fromCache - ? `Using cached files (${cachedFiles.files.length} files)` - : `Downloaded ${cachedFiles.files.length} files via API`; + 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.) diff --git a/apps/cli/src/utils/github.ts b/apps/cli/src/utils/github.ts index a34974e..cd507cd 100644 --- a/apps/cli/src/utils/github.ts +++ b/apps/cli/src/utils/github.ts @@ -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)) {