Skip to content

Commit 18b9fc2

Browse files
lhassa8claude
andcommitted
feat: Add SkillForge Hub - community skill registry
- Add skillforge/hub module with HubClient for fetching skills - Add CLI commands: hub search, hub list, hub info, hub install - Install skills from github.com/lhassa8/skillforge-hub - Support user (~/.claude/skills) and project (.claude/skills) install Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6655d7a commit 18b9fc2

4 files changed

Lines changed: 429 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ api_key.txt
5858

5959
# Claude Code settings (may contain sensitive data)
6060
.claude/
61+
62+
# Nested repos
63+
skillforge-hub/

skillforge/cli.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4715,6 +4715,213 @@ def config_init(
47154715
console.print(f"[green]✓ Created {scope} config: {config_path}[/green]")
47164716

47174717

4718+
# =============================================================================
4719+
# Hub Commands (Community Skill Registry)
4720+
# =============================================================================
4721+
4722+
hub_app = typer.Typer(
4723+
help="SkillForge Hub - community skill registry",
4724+
no_args_is_help=True,
4725+
)
4726+
app.add_typer(hub_app, name="hub")
4727+
4728+
4729+
@hub_app.command("search")
4730+
def hub_search(
4731+
query: str = typer.Argument(..., help="Search query"),
4732+
) -> None:
4733+
"""Search for skills in the SkillForge Hub.
4734+
4735+
Searches skill names, descriptions, and tags in the community registry.
4736+
4737+
Example:
4738+
4739+
\b
4740+
skillforge hub search debug
4741+
skillforge hub search "code review"
4742+
"""
4743+
from skillforge.hub import search_skills
4744+
4745+
console.print(f"[dim]Searching for '{query}'...[/dim]")
4746+
4747+
try:
4748+
results = search_skills(query)
4749+
4750+
if not results:
4751+
console.print()
4752+
console.print(f"[yellow]No skills found matching '{query}'[/yellow]")
4753+
console.print("[dim]Try a different search term or run 'skillforge hub list' to see all skills[/dim]")
4754+
return
4755+
4756+
console.print()
4757+
table = Table(title=f"Search Results ({len(results)})", show_header=True, header_style="bold")
4758+
table.add_column("Skill", style="cyan")
4759+
table.add_column("Description")
4760+
table.add_column("Author", style="dim")
4761+
table.add_column("Version", style="dim")
4762+
4763+
for skill in results:
4764+
desc = skill.description[:50] + "..." if len(skill.description) > 50 else skill.description
4765+
table.add_row(skill.name, desc, skill.author, skill.version)
4766+
4767+
console.print(table)
4768+
console.print()
4769+
console.print("[dim]Install with: skillforge hub install <skill-name>[/dim]")
4770+
4771+
except ConnectionError as e:
4772+
console.print(f"[red]Error:[/red] {e}")
4773+
raise typer.Exit(code=1)
4774+
4775+
4776+
@hub_app.command("list")
4777+
def hub_list() -> None:
4778+
"""List all skills in the SkillForge Hub.
4779+
4780+
Example:
4781+
4782+
\b
4783+
skillforge hub list
4784+
"""
4785+
from skillforge.hub import list_skills
4786+
4787+
console.print("[dim]Fetching skills from hub...[/dim]")
4788+
4789+
try:
4790+
skills = list_skills()
4791+
4792+
if not skills:
4793+
console.print()
4794+
console.print("[yellow]No skills found in the hub[/yellow]")
4795+
return
4796+
4797+
console.print()
4798+
table = Table(title=f"SkillForge Hub ({len(skills)} skills)", show_header=True, header_style="bold")
4799+
table.add_column("Skill", style="cyan")
4800+
table.add_column("Description")
4801+
table.add_column("Author", style="dim")
4802+
table.add_column("Tags", style="dim")
4803+
4804+
for skill in skills:
4805+
desc = skill.description[:45] + "..." if len(skill.description) > 45 else skill.description
4806+
tags = ", ".join(skill.tags[:3])
4807+
if len(skill.tags) > 3:
4808+
tags += "..."
4809+
table.add_row(skill.name, desc, skill.author, tags)
4810+
4811+
console.print(table)
4812+
console.print()
4813+
console.print("[dim]Install with: skillforge hub install <skill-name>[/dim]")
4814+
4815+
except ConnectionError as e:
4816+
console.print(f"[red]Error:[/red] {e}")
4817+
raise typer.Exit(code=1)
4818+
4819+
4820+
@hub_app.command("info")
4821+
def hub_info(
4822+
name: str = typer.Argument(..., help="Skill name"),
4823+
) -> None:
4824+
"""Show details about a skill in the hub.
4825+
4826+
Example:
4827+
4828+
\b
4829+
skillforge hub info code-reviewer
4830+
"""
4831+
from skillforge.hub import get_skill
4832+
4833+
try:
4834+
skill = get_skill(name)
4835+
4836+
if not skill:
4837+
console.print(f"[red]Skill not found:[/red] {name}")
4838+
console.print("[dim]Run 'skillforge hub search' to find skills[/dim]")
4839+
raise typer.Exit(code=1)
4840+
4841+
console.print()
4842+
console.print(f"[bold cyan]{skill.name}[/bold cyan] v{skill.version}")
4843+
console.print()
4844+
console.print(f"[bold]Description:[/bold] {skill.description}")
4845+
console.print(f"[bold]Author:[/bold] {skill.author}")
4846+
console.print(f"[bold]Tags:[/bold] {', '.join(skill.tags)}")
4847+
console.print()
4848+
console.print("[dim]Install with:[/dim]")
4849+
console.print(f" skillforge hub install {skill.name}")
4850+
console.print(f" skillforge hub install {skill.name} --project [dim]# install in current project[/dim]")
4851+
4852+
except ConnectionError as e:
4853+
console.print(f"[red]Error:[/red] {e}")
4854+
raise typer.Exit(code=1)
4855+
4856+
4857+
@hub_app.command("install")
4858+
def hub_install(
4859+
name: str = typer.Argument(..., help="Skill name to install"),
4860+
project: bool = typer.Option(
4861+
False,
4862+
"--project",
4863+
"-p",
4864+
help="Install to project's .claude/skills/ instead of user's ~/.claude/skills/",
4865+
),
4866+
output: Optional[Path] = typer.Option(
4867+
None,
4868+
"--output",
4869+
"-o",
4870+
help="Custom output directory",
4871+
),
4872+
) -> None:
4873+
"""Install a skill from the SkillForge Hub.
4874+
4875+
By default, installs to ~/.claude/skills/ (user scope).
4876+
Use --project to install to ./.claude/skills/ (project scope).
4877+
4878+
Example:
4879+
4880+
\b
4881+
skillforge hub install code-reviewer
4882+
skillforge hub install debugger --project
4883+
skillforge hub install sql-helper -o ./my-skills
4884+
"""
4885+
from skillforge.hub import install_skill, get_skill
4886+
4887+
# First check if skill exists
4888+
skill = get_skill(name)
4889+
if not skill:
4890+
console.print(f"[red]Skill not found:[/red] {name}")
4891+
console.print("[dim]Run 'skillforge hub search' to find skills[/dim]")
4892+
raise typer.Exit(code=1)
4893+
4894+
console.print(f"[dim]Installing {skill.name} v{skill.version}...[/dim]")
4895+
4896+
try:
4897+
skill_dir = install_skill(name, output_dir=output, project=project)
4898+
4899+
console.print()
4900+
console.print(f"[green]✓ Installed:[/green] {skill.name}")
4901+
console.print(f" Location: {skill_dir}")
4902+
console.print()
4903+
4904+
# Show files installed
4905+
console.print("[bold]Files:[/bold]")
4906+
for item in sorted(skill_dir.rglob("*")):
4907+
if item.is_file():
4908+
rel_path = item.relative_to(skill_dir)
4909+
console.print(f" {rel_path}")
4910+
4911+
console.print()
4912+
if not project and not output:
4913+
console.print("[dim]Skill is now available globally in Claude Code[/dim]")
4914+
elif project:
4915+
console.print("[dim]Skill is available in this project's Claude Code sessions[/dim]")
4916+
4917+
except ConnectionError as e:
4918+
console.print(f"[red]Error:[/red] Failed to download skill: {e}")
4919+
raise typer.Exit(code=1)
4920+
except ValueError as e:
4921+
console.print(f"[red]Error:[/red] {e}")
4922+
raise typer.Exit(code=1)
4923+
4924+
47184925
# =============================================================================
47194926
# Info Command
47204927
# =============================================================================

skillforge/hub/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""SkillForge Hub - Community skill registry."""
2+
3+
from skillforge.hub.client import (
4+
HubClient,
5+
HubSkill,
6+
search_skills,
7+
get_skill,
8+
install_skill,
9+
list_skills,
10+
HUB_REPO,
11+
HUB_INDEX_URL,
12+
HUB_RAW_URL,
13+
)
14+
15+
__all__ = [
16+
"HubClient",
17+
"HubSkill",
18+
"search_skills",
19+
"get_skill",
20+
"install_skill",
21+
"list_skills",
22+
"HUB_REPO",
23+
"HUB_INDEX_URL",
24+
"HUB_RAW_URL",
25+
]

0 commit comments

Comments
 (0)