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
50 changes: 42 additions & 8 deletions src/skilz/commands/read_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,19 @@
import argparse
import sys

from skilz.agents import ExtendedAgentType
from skilz.scanner import find_installed_skill

# Fallback search order when --agent is not specified
# Searches directories in order until skill is found
FALLBACK_SEARCH_ORDER: list[ExtendedAgentType] = [
"claude", # .claude/skills/
"universal", # .skilz/skills/
"gemini", # .skilz/skills/ (gemini uses .skilz for project)
"opencode", # .opencode/skill/ or .skilz/skills/
"codex", # .codex/skills/
]


def cmd_read(args: argparse.Namespace) -> int:
"""
Expand All @@ -29,21 +40,44 @@ def cmd_read(args: argparse.Namespace) -> int:
agent = getattr(args, "agent", None)
project_level: bool = getattr(args, "project", False)

# Find the skill
skill = find_installed_skill(
skill_id_or_name=skill_name,
agent=agent,
project_level=project_level,
)
skill = None

if skill is None:
if agent:
# Agent specified - search only that agent's directories
skill = find_installed_skill(
skill_id_or_name=skill_name,
agent=agent,
project_level=project_level,
)
# Try project-level if user-level not found
if not project_level:
if skill is None and not project_level:
skill = find_installed_skill(
skill_id_or_name=skill_name,
agent=agent,
project_level=True,
)
else:
# No agent specified - use fallback search across multiple directories
# First try user-level for agents that support it
for fallback_agent in FALLBACK_SEARCH_ORDER:
skill = find_installed_skill(
skill_id_or_name=skill_name,
agent=fallback_agent,
project_level=False, # Try user-level first
)
if skill is not None:
break

# If not found at user-level, try project-level
if skill is None:
for fallback_agent in FALLBACK_SEARCH_ORDER:
skill = find_installed_skill(
skill_id_or_name=skill_name,
agent=fallback_agent,
project_level=True,
)
if skill is not None:
break

if skill is None:
print(f"Error: Skill '{skill_name}' not found.", file=sys.stderr)
Expand Down
82 changes: 74 additions & 8 deletions src/skilz/config_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,50 @@
SECTION_START = "<!-- SKILLS_TABLE_START -->"
SECTION_END = "<!-- SKILLS_TABLE_END -->"

# Usage instructions template
USAGE_TEMPLATE = """<usage>

def _generate_usage_template(
agent_name: str,
native_support: str,
force_extended: bool = False,
) -> str:
"""Generate agent-specific usage template.

Args:
agent_name: The agent identifier (e.g., "claude", "gemini")
native_support: The native_skill_support value ("all", "home", "none")
force_extended: If True, always include extended step-by-step instructions
(used when --force-config is specified)

Returns:
Formatted XML usage block with correct invocation command
"""
# Claude with native "all" support doesn't need --agent flag
if agent_name == "claude" and native_support == "all":
invocation = 'Bash("skilz read <skill-name>")'
else:
invocation = f'Bash("skilz read <skill-name> --agent {agent_name}")'

# Extended instructions for agents without native support OR when forced
if native_support == "none" or force_extended:
extra_steps = """
Step-by-step process:
1. Identify a skill from <available_skills> that matches the user's request
2. Run the command above to load the skill's SKILL.md content
3. Follow the instructions in the loaded skill content
4. Skills may include bundled scripts, templates, and references
"""
else:
extra_steps = ""

return f"""<usage>
When users ask you to perform tasks, check if any of the available skills
below can help complete the task more effectively.

How to use skills:
- Invoke: Bash("skilz read <skill-name>")
- Invoke: {invocation}
- The skill content will load with detailed instructions
- Base directory provided in output for resolving bundled resources

{extra_steps}
Usage notes:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already loaded in your context
Expand Down Expand Up @@ -189,12 +223,18 @@ def format_skill_element(
def _build_skills_section(
skills: list[SkillReference],
project_dir: Path | None = None,
agent_name: str = "claude",
native_support: str = "all",
force_extended: bool = False,
) -> str:
"""Build the complete skills section content following agentskills.io standard.

Args:
skills: List of skills to include.
project_dir: Project directory for calculating relative paths.
agent_name: The agent identifier for generating correct invocation.
native_support: The agent's native_skill_support level.
force_extended: If True, include extended instructions (--force-config).

Returns:
Complete section content including markers.
Expand All @@ -204,13 +244,14 @@ def _build_skills_section(
skill_elements.append(format_skill_element(skill, project_dir))

skills_xml = "\n\n".join(skill_elements)
usage_template = _generate_usage_template(agent_name, native_support, force_extended)

return f"""<skills_system priority="1">

## Available Skills

{SECTION_START}
{USAGE_TEMPLATE}
{usage_template}

<available_skills>

Expand Down Expand Up @@ -258,13 +299,19 @@ def _merge_skill_into_section(
existing_section: str,
new_skill: SkillReference,
project_dir: Path | None = None,
agent_name: str = "claude",
native_support: str = "all",
force_extended: bool = False,
) -> str:
"""Merge a new skill into an existing skills section.

Args:
existing_section: The existing section content.
new_skill: The new skill to add.
project_dir: Project directory for calculating relative paths.
agent_name: The agent identifier for generating correct invocation.
native_support: The agent's native_skill_support level.
force_extended: If True, include extended instructions (--force-config).

Returns:
Updated section content.
Expand Down Expand Up @@ -299,13 +346,14 @@ def _get_skill_name(elem: str) -> str:
skill_elements.sort(key=_get_skill_name)

skills_xml = "\n\n".join(skill_elements)
usage_template = _generate_usage_template(agent_name, native_support, force_extended)

return f"""<skills_system priority="1">

## Available Skills

{SECTION_START}
{USAGE_TEMPLATE}
{usage_template}

<available_skills>

Expand All @@ -323,6 +371,7 @@ def update_config_file(
agent_config: AgentConfig,
project_dir: Path | None = None,
create_if_missing: bool = True,
force_extended: bool = False,
) -> ConfigSyncResult:
"""Update a config file with a skill reference.

Expand All @@ -334,6 +383,7 @@ def update_config_file(
agent_config: The agent configuration.
project_dir: Project directory for calculating relative paths.
create_if_missing: If True, create the config file if it doesn't exist.
force_extended: If True, include extended instructions (--force-config).

Returns:
ConfigSyncResult indicating what happened.
Expand Down Expand Up @@ -375,15 +425,28 @@ def update_config_file(
if system_start and system_end:
# Update existing section
existing_section = content[system_start.start() : system_end.end()]
new_section = _merge_skill_into_section(existing_section, skill, project_dir)
new_section = _merge_skill_into_section(
existing_section,
skill,
project_dir,
agent_name=agent_config.name,
native_support=agent_config.native_skill_support,
force_extended=force_extended,
)

new_content = (
content[: system_start.start()] + new_section + content[system_end.end() :]
)
updated = True
else:
# Append new section at end
new_section = _build_skills_section([skill], project_dir)
new_section = _build_skills_section(
[skill],
project_dir,
agent_name=agent_config.name,
native_support=agent_config.native_skill_support,
force_extended=force_extended,
)
if not content.endswith("\n"):
content += "\n"
new_content = content + "\n" + new_section + "\n"
Expand Down Expand Up @@ -418,6 +481,7 @@ def sync_skill_to_configs(
agent: str | None = None,
verbose: bool = False,
target_files: tuple[str, ...] | None = None, # SKILZ-50: Custom file override
force_extended: bool = False,
) -> list[ConfigSyncResult]:
"""Sync a skill reference to all relevant config files.

Expand All @@ -430,6 +494,7 @@ def sync_skill_to_configs(
target_files: Optional tuple of config file names to update (e.g., ("GEMINI.md",)).
If provided, overrides auto-detection and only updates these files.
The agent parameter is ignored when target_files is specified.
force_extended: If True, include extended instructions (--force-config).

Returns:
List of ConfigSyncResult for each config file processed.
Expand Down Expand Up @@ -484,6 +549,7 @@ def sync_skill_to_configs(
agent_config=agent_config,
project_dir=project_dir,
create_if_missing=(agent is not None), # Only create if agent specified
force_extended=force_extended,
)
results.append(result)

Expand Down
84 changes: 84 additions & 0 deletions src/skilz/data/LOAD_SKILLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# How to Use Skills

This guide explains how AI coding agents can discover and use skills installed via the skilz CLI.

## What Are Skills?

Skills are reusable instruction sets that extend an AI agent's capabilities. Each skill contains:
- **SKILL.md** - The main instruction file with guidance, examples, and commands
- **Bundled resources** - Scripts, templates, reference docs, and other files

## Discovering Available Skills

Skills are listed in your agent's config file under `<available_skills>`:

```xml
<available_skills>
<skill>
<name>plantuml</name>
<description>Generate PlantUML diagrams from text descriptions</description>
<location>.claude/skills/plantuml/SKILL.md</location>
</skill>
</available_skills>
```

## Loading a Skill

When a user's request matches a skill's purpose, load it using the CLI:

```bash
skilz read <skill-name> --agent <your-agent>
```

**Examples:**
- Claude: `skilz read plantuml` (no --agent needed)
- Gemini: `skilz read plantuml --agent gemini`
- Codex: `skilz read plantuml --agent codex`

The command outputs:
1. **Base Directory** - Path for resolving bundled resources
2. **SKILL.md Content** - Full instructions to follow

## Using Bundled Resources

Skills may include additional files. The base directory path tells you where to find them:

```
# Skill: plantuml
# Base Directory: /Users/name/.claude/skills/plantuml
# SKILL.md Path: /Users/name/.claude/skills/plantuml/SKILL.md

[SKILL.md content follows...]
```

To use bundled files, resolve paths relative to the base directory:
- `references/syntax-guide.md` -> `/Users/name/.claude/skills/plantuml/references/syntax-guide.md`
- `scripts/convert.py` -> `/Users/name/.claude/skills/plantuml/scripts/convert.py`

## Step-by-Step Process

1. **Identify** - Check `<available_skills>` for a skill matching the user's request
2. **Load** - Run `skilz read <skill-name> --agent <your-agent>`
3. **Follow** - Execute the instructions in the loaded SKILL.md
4. **Use Resources** - Access bundled scripts/templates via the base directory path

## Lazy Loading

Don't load skills preemptively. Load them only when:
- A user explicitly requests the skill's functionality
- The task clearly matches the skill's description
- You need the skill's specific guidance to complete a request

## Troubleshooting

**Skill not found:**
```bash
skilz list --agent <your-agent> # See installed skills
skilz install <skill-name> --agent <your-agent> # Install if missing
```

**Wrong agent directory:**
Each agent has its own skills directory. Use `--agent` to specify which one.

**Broken symlink:**
Run `skilz list` to identify broken skills, then reinstall them.
2 changes: 2 additions & 0 deletions src/skilz/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def install_local_skill(
agent=resolved_agent if agent else None,
verbose=verbose,
target_files=target_files,
force_extended=force_config,
)

for result in sync_results:
Expand Down Expand Up @@ -567,6 +568,7 @@ def install_skill(
agent=resolved_agent if agent else None,
verbose=verbose,
target_files=target_files,
force_extended=force_config,
)

# Report what was updated
Expand Down
Loading