fix: skills hub inspect/resolve — 4 bugs

Cherry-picked from PR #2122 by @AtlasMeridia.

1. do_inspect bytes crash: bundle.files returns bytes for official
   skills, .split() expected str. Added decode guard.
2. GitHub redirects: three httpx.get calls missing follow_redirects=True,
   causing silent 301 failures on renamed orgs.
3. Skill discovery fallback: scan repo root directories when standard
   paths (skills/, .agents/skills/, .claude/skills/) miss.
4. tap list KeyError: t['repo'] crashes for local taps. Use safe .get().
This commit is contained in:
Teknium
2026-03-22 04:03:28 -07:00
parent 306e67f32d
commit 7d0e4510b8
2 changed files with 46 additions and 7 deletions

View File

@@ -455,6 +455,8 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
if bundle and "SKILL.md" in bundle.files:
content = bundle.files["SKILL.md"]
if isinstance(content, bytes):
content = content.decode("utf-8", errors="replace")
# Show first 50 lines as preview
lines = content.split("\n")
preview = "\n".join(lines[:50])
@@ -640,7 +642,8 @@ def do_tap(action: str, repo: str = "", console: Optional[Console] = None) -> No
table.add_column("Repo", style="bold cyan")
table.add_column("Path", style="dim")
for t in taps:
table.add_row(t["repo"], t.get("path", "skills/"))
label = t.get("repo") or t.get("name") or t.get("path", "unknown")
table.add_row(label, t.get("path", "skills/"))
c.print(table)
c.print()

View File

@@ -375,7 +375,7 @@ class GitHubSource(SkillSource):
url = f"https://api.github.com/repos/{repo}/contents/{path.rstrip('/')}"
try:
resp = httpx.get(url, headers=self.auth.get_headers(), timeout=15)
resp = httpx.get(url, headers=self.auth.get_headers(), timeout=15, follow_redirects=True)
if resp.status_code != 200:
return []
except httpx.HTTPError:
@@ -407,7 +407,7 @@ class GitHubSource(SkillSource):
"""Recursively download all text files from a GitHub directory."""
url = f"https://api.github.com/repos/{repo}/contents/{path.rstrip('/')}"
try:
resp = httpx.get(url, headers=self.auth.get_headers(), timeout=15)
resp = httpx.get(url, headers=self.auth.get_headers(), timeout=15, follow_redirects=True)
if resp.status_code != 200:
return {}
except httpx.HTTPError:
@@ -441,7 +441,7 @@ class GitHubSource(SkillSource):
resp = httpx.get(
url,
headers={**self.auth.get_headers(), "Accept": "application/vnd.github.v3.raw"},
timeout=15,
timeout=15, follow_redirects=True,
)
if resp.status_code == 200:
return resp.text
@@ -961,8 +961,8 @@ class SkillsShSource(SkillSource):
default_repo = f"{parts[0]}/{parts[1]}"
repo = detail.get("repo", default_repo) if isinstance(detail, dict) else default_repo
skill_token = parts[2]
tokens = [skill_token]
skill_token=parts[2].split("/")[-1]
tokens=[skill_token]
if isinstance(detail, dict):
tokens.extend([
detail.get("install_skill", ""),
@@ -970,7 +970,10 @@ class SkillsShSource(SkillSource):
detail.get("body_title", ""),
])
for base_path in ("skills/", ".agents/skills/", ".claude/skills/"):
# Standard skill paths
base_paths = ["skills/", ".agents/skills/", ".claude/skills/"]
for base_path in base_paths:
try:
skills = self.github._list_skills_in_repo(repo, base_path)
except Exception:
@@ -978,6 +981,39 @@ class SkillsShSource(SkillSource):
for meta in skills:
if self._matches_skill_tokens(meta, tokens):
return meta.identifier
# Fallback: scan repo root for directories that might contain skills
try:
root_url = f"https://api.github.com/repos/{repo}/contents/"
resp = httpx.get(root_url, headers=self.github.auth.get_headers(),
timeout=15, follow_redirects=True)
if resp.status_code == 200:
entries = resp.json()
if isinstance(entries, list):
for entry in entries:
if entry.get("type") != "dir":
continue
dir_name = entry["name"]
if dir_name.startswith(".") or dir_name.startswith("_"):
continue
if dir_name in ("skills", ".agents", ".claude"):
continue # already tried
# Try direct: repo/dir/skill_token
direct_id = f"{repo}/{dir_name}/{skill_token}"
meta = self.github.inspect(direct_id)
if meta:
return meta.identifier
# Try listing skills in this directory
try:
skills = self.github._list_skills_in_repo(repo, dir_name + "/")
except Exception:
continue
for meta in skills:
if self._matches_skill_tokens(meta, tokens):
return meta.identifier
except Exception:
pass
return None
def _finalize_inspect_meta(self, meta: SkillMeta, canonical: str, detail: Optional[dict]) -> SkillMeta: