feat: Add git-based shared skill registry for team skill management
Adds skill_registry.py CLI tool with 7 subcommands (init, publish, list, search, install, info, remove) for managing a git-friendly shared skill catalog. No new dependencies — stdlib only. Integrates with existing validate.py and security_scan.py for publish-time checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bac2b27bb8
commit
736bbda78a
3 changed files with 1090 additions and 277 deletions
653
README.md
653
README.md
|
|
@ -1,154 +1,203 @@
|
||||||
# Agent Skill Creator v4.0
|
# Agent Skill Creator
|
||||||
|
|
||||||
**Create Cross-Platform Agent Skills from Workflow Descriptions**
|
**Create cross-platform agent skills from natural language workflow descriptions.**
|
||||||
|
|
||||||
[](https://github.com/anthropics/agent-skills-spec)
|
[](https://github.com/anthropics/agent-skills-spec)
|
||||||
[](LICENSE)
|
|
||||||
[]()
|
[]()
|
||||||
|
[]()
|
||||||
> Works on **8+ platforms**: Claude Code, GitHub Copilot, Cursor, Windsurf, Cline, Codex CLI, Gemini CLI, and any platform supporting the Agent Skills Open Standard.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What It Does
|
## What Is This?
|
||||||
|
|
||||||
Agent Skill Creator is a **meta-skill** -- a skill that creates other skills. Describe a repetitive workflow in natural language, and it generates a complete, validated, cross-platform agent skill through an autonomous 5-phase pipeline.
|
Agent Skill Creator is a **meta-skill** -- a skill that creates other skills. Describe a repetitive workflow in plain English and it generates a complete, validated, cross-platform agent skill through an autonomous 5-phase pipeline.
|
||||||
|
|
||||||
**Input**: A workflow description like *"Every day I download stock data, analyze trends, and create reports"*
|
|
||||||
|
|
||||||
|
**Input**: *"Every day I download stock data, analyze trends, and create reports"*
|
||||||
**Output**: A ready-to-install skill directory with functional scripts, documentation, cross-platform installer, and spec-compliant SKILL.md.
|
**Output**: A ready-to-install skill directory with functional scripts, documentation, cross-platform installer, and spec-compliant SKILL.md.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Install
|
### Claude Code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone and install as a Claude Code skill
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git ~/.claude/skills/agent-skill-creator
|
||||||
git clone https://github.com/user/agent-skill-creator.git
|
|
||||||
cp -r agent-skill-creator/ ~/.claude/skills/agent-skill-creator/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use
|
### GitHub Copilot
|
||||||
|
|
||||||
Just describe what you need in your agent:
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .github/skills/agent-skill-creator
|
||||||
```
|
|
||||||
"Create a skill for analyzing stock market data"
|
|
||||||
|
|
||||||
"Every day I process CSV files manually, automate this"
|
|
||||||
|
|
||||||
"Create a cross-platform skill for weather alerts"
|
|
||||||
|
|
||||||
"Validate this skill for spec compliance"
|
|
||||||
|
|
||||||
"Export this skill for Cursor and Copilot"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The skill creator activates automatically when it detects these patterns and walks through the full pipeline.
|
### Cursor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .cursor/rules/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
After installing, open your agent and type:
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a skill for analyzing CSV files
|
||||||
|
```
|
||||||
|
|
||||||
|
The skill creator activates and walks you through the full pipeline.
|
||||||
|
|
||||||
|
For Windsurf, Cline, Codex CLI, Gemini CLI, and other platforms see [Setup by Platform](#setup-by-platform-complete-guide) below.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Supported Platforms
|
## Usage
|
||||||
|
|
||||||
Generated skills work on any platform that supports the Agent Skills Open Standard:
|
### Trigger Phrases
|
||||||
|
|
||||||
| Platform | Install Location | Notes |
|
Say any of these to your agent:
|
||||||
|----------|-----------------|-------|
|
|
||||||
| **Claude Code** | `~/.claude/skills/` or `.claude/skills/` | Global or per-project |
|
|
||||||
| **GitHub Copilot** | `.github/skills/` | Repository-level |
|
|
||||||
| **Cursor** | `.cursor/rules/` | Workspace rules |
|
|
||||||
| **Windsurf** | `.windsurf/skills/` | Workspace skills |
|
|
||||||
| **Cline** | `.clinerules/` | Rule-based skills |
|
|
||||||
| **Codex CLI** | `.codex/skills/` | OpenAI Codex CLI |
|
|
||||||
| **Gemini CLI** | `.gemini/skills/` | Google Gemini CLI |
|
|
||||||
|
|
||||||
Each generated skill includes an `install.sh` script that auto-detects your platform and installs to the correct location.
|
```
|
||||||
|
"Create a skill for analyzing stock market data"
|
||||||
|
"Every day I process CSV files manually, automate this"
|
||||||
|
"Create a cross-platform skill for weather alerts"
|
||||||
|
"Automate this workflow"
|
||||||
|
"I need to automate [repetitive task]"
|
||||||
|
"Validate this skill"
|
||||||
|
"Export this skill for Cursor and Copilot"
|
||||||
|
"Migrate this skill to v4"
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Happens
|
||||||
|
|
||||||
|
The creator runs a **5-phase autonomous pipeline**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: DISCOVERY Research APIs, data sources, and domain knowledge
|
||||||
|
|
|
||||||
|
Phase 2: DESIGN Define use cases, methodologies, and outputs
|
||||||
|
|
|
||||||
|
Phase 3: ARCHITECTURE Structure skill directory (simple vs. complex suite)
|
||||||
|
|
|
||||||
|
Phase 4: DETECTION Generate description + keywords for reliable activation
|
||||||
|
|
|
||||||
|
Phase 5: IMPLEMENTATION Create all files, run validation, run security scan
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: a complete skill directory you can install on any supported platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup by Platform (Complete Guide)
|
||||||
|
|
||||||
|
Each platform installs with a single `git clone` directly into the right location. Replace `agent-skill-creator` with the skill name when installing generated skills.
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Personal skill (available in all projects)
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git ~/.claude/skills/agent-skill-creator
|
||||||
|
|
||||||
|
# Per-project (scoped to one repo)
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .claude/skills/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub Copilot (CLI + VS Code)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .github/skills/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .cursor/rules/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
Cursor reads SKILL.md natively alongside its `.mdc` rules.
|
||||||
|
|
||||||
|
### Windsurf
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .windsurf/skills/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cline
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .clinerules/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAI Codex CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .codex/skills/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gemini CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .gemini/skills/agent-skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Desktop / claude.ai (Export)
|
||||||
|
|
||||||
|
These platforms use `.zip` upload instead of directory copying:
|
||||||
|
|
||||||
|
1. Export: `python3 scripts/export_utils.py ./agent-skill-creator/ --variant desktop`
|
||||||
|
2. Open Claude Desktop or claude.ai
|
||||||
|
3. Go to Settings > Skills > Upload skill
|
||||||
|
4. Select the generated `.zip` file
|
||||||
|
|
||||||
|
### Claude API (Programmatic)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/export_utils.py ./agent-skill-creator/ --variant api
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
import anthropic
|
||||||
|
|
||||||
|
client = anthropic.Anthropic()
|
||||||
|
|
||||||
|
with open("agent-skill-creator-api-v4.0.0.zip", "rb") as f:
|
||||||
|
skill = client.skills.create(file=f, name="agent-skill-creator")
|
||||||
|
|
||||||
|
response = client.messages.create(
|
||||||
|
model="claude-sonnet-4",
|
||||||
|
messages=[{"role": "user", "content": "Your query here"}],
|
||||||
|
container={"type": "custom_skill", "skill_id": skill.id},
|
||||||
|
betas=["code-execution-2025-08-25", "skills-2025-10-02"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: API sandbox has no network access, no pip install at runtime, and an 8 MB size limit.
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
|
||||||
|
To update an installed skill, just `git pull` from inside the skill directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.claude/skills/agent-skill-creator && git pull
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
The creator runs a **5-phase autonomous pipeline**:
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 1: DISCOVERY Research APIs, data sources, tools, and domain knowledge
|
|
||||||
|
|
|
||||||
Phase 2: DESIGN Define use cases, analyses, methodologies, and outputs
|
|
||||||
|
|
|
||||||
Phase 3: ARCHITECTURE Structure skill directory (simple skill vs. complex suite)
|
|
||||||
|
|
|
||||||
Phase 4: DETECTION Generate description + keywords for reliable activation
|
|
||||||
|
|
|
||||||
Phase 5: IMPLEMENTATION Create all files, run validation, run security scan
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase Details
|
|
||||||
|
|
||||||
| Phase | What Happens | Key Output |
|
| Phase | What Happens | Key Output |
|
||||||
|-------|-------------|------------|
|
|-------|-------------|------------|
|
||||||
| **Discovery** | Researches the domain, identifies APIs and data sources, maps user needs | Domain model, API list, data sources |
|
| **Discovery** | Researches the domain, identifies APIs and data sources | Domain model, API list |
|
||||||
| **Design** | Defines use cases, analysis methods, output formats | Use case specs, methodology docs |
|
| **Design** | Defines use cases, analysis methods, output formats | Use case specs, methodology docs |
|
||||||
| **Architecture** | Decides simple skill vs. complex suite, plans directory structure | Architecture decision, file plan |
|
| **Architecture** | Decides simple skill vs. complex suite, plans directory structure | Architecture decision, file plan |
|
||||||
| **Detection** | Crafts SKILL.md description and activation keywords for reliable triggering | SKILL.md frontmatter, trigger phrases |
|
| **Detection** | Crafts SKILL.md description and activation keywords | SKILL.md frontmatter, trigger phrases |
|
||||||
| **Implementation** | Generates all code, docs, installer; validates and scans for security issues | Complete skill directory |
|
| **Implementation** | Generates all code, docs, installer; validates and scans | Complete skill directory |
|
||||||
|
|
||||||
For full pipeline documentation, see [references/pipeline-phases.md](references/pipeline-phases.md).
|
For full pipeline documentation, see [references/pipeline-phases.md](references/pipeline-phases.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture: Simple Skill vs. Complex Suite
|
|
||||||
|
|
||||||
The creator automatically decides the right architecture based on scope:
|
|
||||||
|
|
||||||
### Simple Skill
|
|
||||||
|
|
||||||
For focused, single-domain tasks (e.g., "analyze CSV files", "extract text from PDFs").
|
|
||||||
|
|
||||||
```
|
|
||||||
stock-analyzer/
|
|
||||||
SKILL.md # Under 500 lines, spec-compliant
|
|
||||||
scripts/
|
|
||||||
analyze.py
|
|
||||||
fetch_data.py
|
|
||||||
references/
|
|
||||||
api-guide.md
|
|
||||||
assets/
|
|
||||||
report-template.html
|
|
||||||
install.sh
|
|
||||||
README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complex Suite
|
|
||||||
|
|
||||||
For multi-domain workflows requiring coordinated agents (e.g., "full financial analysis pipeline with data collection, analysis, and reporting").
|
|
||||||
|
|
||||||
```
|
|
||||||
financial-analysis-suite/
|
|
||||||
SKILL.md # Suite orchestrator, under 500 lines
|
|
||||||
scripts/
|
|
||||||
orchestrator.py
|
|
||||||
data_collector.py
|
|
||||||
analyzer.py
|
|
||||||
report_generator.py
|
|
||||||
references/
|
|
||||||
architecture-guide.md
|
|
||||||
api-reference.md
|
|
||||||
assets/
|
|
||||||
templates/
|
|
||||||
schemas/
|
|
||||||
install.sh
|
|
||||||
README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
For detailed architecture guidance, see [references/architecture-guide.md](references/architecture-guide.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Generated Skill Format
|
## Generated Skill Format
|
||||||
|
|
||||||
Every generated skill follows the Agent Skills Open Standard structure:
|
Every generated skill follows the Agent Skills Open Standard:
|
||||||
|
|
||||||
```
|
```
|
||||||
skill-name/
|
skill-name/
|
||||||
|
|
@ -160,9 +209,7 @@ skill-name/
|
||||||
README.md # Multi-platform install instructions
|
README.md # Multi-platform install instructions
|
||||||
```
|
```
|
||||||
|
|
||||||
### SKILL.md Structure
|
### SKILL.md Frontmatter
|
||||||
|
|
||||||
The generated SKILL.md includes standard frontmatter:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
|
|
@ -182,45 +229,26 @@ compatibility: >-
|
||||||
|
|
||||||
Followed by sections: When to Use, Overview, Workflow, Implementation Guidelines, and References.
|
Followed by sections: When to Use, Overview, Workflow, Implementation Guidelines, and References.
|
||||||
|
|
||||||
---
|
**Naming rules**: `kebab-case`, 1-64 characters, pattern `^[a-z][a-z0-9-]*[a-z0-9]$`, must match directory name.
|
||||||
|
|
||||||
## Naming Convention
|
|
||||||
|
|
||||||
Skills follow the **Agent Skills Open Standard** naming rules:
|
|
||||||
|
|
||||||
- **Format**: `kebab-case` (lowercase letters and hyphens only)
|
|
||||||
- **Length**: 1-64 characters
|
|
||||||
- **Pattern**: `^[a-z][a-z0-9-]*[a-z0-9]$`
|
|
||||||
- **No special suffixes** required
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
| Good | Bad |
|
|
||||||
|------|-----|
|
|
||||||
| `stock-analyzer` | `Stock_Analyzer` |
|
|
||||||
| `csv-data-cleaner` | `csv_data_cleaner` |
|
|
||||||
| `financial-analysis-suite` | `FinancialAnalysis` |
|
|
||||||
| `weather-alerts` | `weather-alerts-cskill` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Validation and Security
|
## Tools
|
||||||
|
|
||||||
### Validate a Skill
|
### Validate a Skill
|
||||||
|
|
||||||
Check that a generated skill is compliant with the Agent Skills Open Standard:
|
Check spec compliance against the Agent Skills Open Standard:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/validate.py ./my-skill/
|
python3 scripts/validate.py ./my-skill/
|
||||||
|
|
||||||
|
# JSON output (for CI/CD)
|
||||||
|
python3 scripts/validate.py ./my-skill/ --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Validates:
|
**Checks**: SKILL.md existence, valid frontmatter, kebab-case name (1-64 chars), description under 1024 chars, body under 500 lines, required directory structure, install.sh exists and is executable.
|
||||||
- SKILL.md exists and has valid frontmatter
|
|
||||||
- Name follows kebab-case convention (1-64 chars)
|
**Exit codes**: `0` = valid (may have warnings), `1` = invalid (errors found).
|
||||||
- Description is under 1024 characters
|
|
||||||
- SKILL.md is under 500 lines
|
|
||||||
- Required directory structure is present
|
|
||||||
- install.sh exists and is executable
|
|
||||||
|
|
||||||
### Security Scan
|
### Security Scan
|
||||||
|
|
||||||
|
|
@ -228,165 +256,209 @@ Scan for common security issues before sharing or deploying:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/security_scan.py ./my-skill/
|
python3 scripts/security_scan.py ./my-skill/
|
||||||
|
|
||||||
|
# JSON output
|
||||||
|
python3 scripts/security_scan.py ./my-skill/ --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Detects:
|
**Detects**: hardcoded API keys (OpenAI, AWS, GitHub, GitLab), tokens and secrets, command injection patterns, unsafe file operations, credential exposure in config files.
|
||||||
- Hardcoded API keys, tokens, and secrets
|
|
||||||
- Potential command injection patterns
|
|
||||||
- Unsafe file operations
|
|
||||||
- Credential exposure in configuration files
|
|
||||||
|
|
||||||
---
|
**Exit codes**: `0` = clean, `1` = issues found.
|
||||||
|
|
||||||
## Cross-Platform Export
|
### Export for Other Platforms
|
||||||
|
|
||||||
Export skills for different deployment targets:
|
Package skills for distribution:
|
||||||
|
|
||||||
### Desktop/Web Export
|
|
||||||
|
|
||||||
Generates a `.zip` archive suitable for sharing or manual installation:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Desktop/Web (.zip for Claude Desktop, claude.ai)
|
||||||
python3 scripts/export_utils.py ./my-skill/ --variant desktop
|
python3 scripts/export_utils.py ./my-skill/ --variant desktop
|
||||||
|
|
||||||
|
# API (.zip for Claude API, <=8MB)
|
||||||
|
python3 scripts/export_utils.py ./my-skill/ --variant api
|
||||||
|
|
||||||
|
# All variants
|
||||||
|
python3 scripts/export_utils.py ./my-skill/
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Export
|
Output goes to `exports/`. See [references/export-guide.md](references/export-guide.md) for full documentation.
|
||||||
|
|
||||||
Generates a package suitable for Claude API integration:
|
### Skill Registry
|
||||||
|
|
||||||
|
Manage a shared skill catalog for teams using a git-based registry:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/export_utils.py ./my-skill/ --variant api
|
# Initialize a registry
|
||||||
|
python3 scripts/skill_registry.py init --registry ./my-registry --name "Team Skills"
|
||||||
|
|
||||||
|
# Publish a skill (validates and security-scans first)
|
||||||
|
python3 scripts/skill_registry.py publish ./my-skill/ --registry ./my-registry --tags data,csv
|
||||||
|
|
||||||
|
# List all published skills
|
||||||
|
python3 scripts/skill_registry.py list --registry ./my-registry
|
||||||
|
|
||||||
|
# Search for skills
|
||||||
|
python3 scripts/skill_registry.py search "finance" --registry ./my-registry
|
||||||
|
|
||||||
|
# Show full details about a skill
|
||||||
|
python3 scripts/skill_registry.py info stock-analyzer --registry ./my-registry
|
||||||
|
|
||||||
|
# Install a skill for a specific platform
|
||||||
|
python3 scripts/skill_registry.py install stock-analyzer --registry ./my-registry --platform claude-code
|
||||||
|
|
||||||
|
# Install at project level instead of user level
|
||||||
|
python3 scripts/skill_registry.py install stock-analyzer --registry ./my-registry --project
|
||||||
|
|
||||||
|
# Remove a skill from the registry
|
||||||
|
python3 scripts/skill_registry.py remove stock-analyzer --registry ./my-registry --force
|
||||||
```
|
```
|
||||||
|
|
||||||
For full export documentation, see [references/export-guide.md](references/export-guide.md).
|
All commands support `--json` for machine-readable output. The registry is a plain directory with `registry.json` and `skills/` — commit it to git for version history, access control via repo permissions, and review workflow via PRs.
|
||||||
|
|
||||||
|
**Exit codes**: `0` = success, `1` = error.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Example Skill
|
## Architecture Decisions
|
||||||
|
|
||||||
The repository includes a complete example skill:
|
The creator automatically decides simple vs. complex based on scope:
|
||||||
|
|
||||||
### article-to-prototype
|
| Factor | Simple Skill | Complex Suite |
|
||||||
|
|--------|-------------|---------------|
|
||||||
|
| Workflows | 1-2 | 3+ distinct |
|
||||||
|
| Code size | <1000 lines | >2000 lines |
|
||||||
|
| Structure | Single SKILL.md | Multiple component SKILL.md files |
|
||||||
|
|
||||||
Converts academic articles and research papers into functional prototypes. Demonstrates the full skill structure including scripts, references, and cross-platform installer.
|
For detailed decision logic, see [references/architecture-guide.md](references/architecture-guide.md).
|
||||||
|
|
||||||
```
|
|
||||||
article-to-prototype/
|
|
||||||
SKILL.md
|
|
||||||
scripts/
|
|
||||||
article_processor.py
|
|
||||||
prototype_generator.py
|
|
||||||
validation_engine.py
|
|
||||||
references/
|
|
||||||
methodology.md
|
|
||||||
supported-formats.md
|
|
||||||
assets/
|
|
||||||
prototype-templates/
|
|
||||||
install.sh
|
|
||||||
README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
See [article-to-prototype/](article-to-prototype/) for the full example.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Structure
|
## For AI Agents (Machine-Readable Reference)
|
||||||
|
|
||||||
The agent-skill-creator repository itself:
|
This section provides structured metadata for AI agents ingesting this README as context.
|
||||||
|
|
||||||
|
### Activation Triggers
|
||||||
|
|
||||||
```
|
```
|
||||||
agent-skill-creator/
|
create an agent for [objective]
|
||||||
SKILL.md # Meta-skill definition (this skill's spec)
|
create a skill for [domain]
|
||||||
README.md # This file
|
develop a custom skill
|
||||||
MIGRATION.md # v3.x to v4.0 migration guide
|
automate this workflow
|
||||||
.gitignore
|
every day I [task]
|
||||||
scripts/
|
I repeatedly need to [process]
|
||||||
validate.py # Spec compliance validator
|
I need to automate [task]
|
||||||
security_scan.py # Security scanner
|
create a cross-platform skill for [objective]
|
||||||
export_utils.py # Cross-platform export tool
|
validate this skill
|
||||||
install-template.sh # Template for generated install.sh
|
check if this skill is spec-compliant
|
||||||
references/
|
export this skill for [platform]
|
||||||
pipeline-phases.md # Full 5-phase pipeline docs
|
package this skill for [platform]
|
||||||
architecture-guide.md # Simple skill vs. complex suite
|
migrate this skill to v4
|
||||||
cross-platform-guide.md # Platform-specific details
|
update this skill to the new standard
|
||||||
export-guide.md # Export system documentation
|
create a multi-agent suite for [objective]
|
||||||
phase1-discovery.md # Phase 1 deep dive
|
create a skill from the [name] template
|
||||||
phase2-design.md # Phase 2 deep dive
|
|
||||||
phase3-architecture.md # Phase 3 deep dive
|
|
||||||
phase4-detection.md # Phase 4 deep dive
|
|
||||||
phase5-implementation.md # Phase 5 deep dive
|
|
||||||
phase6-testing.md # Testing guide
|
|
||||||
quality-standards.md # Quality standards reference
|
|
||||||
templates-guide.md # Template system guide
|
|
||||||
templates/ # Skill templates
|
|
||||||
tools/ # Validation and scanning tools
|
|
||||||
examples/ # Example configurations
|
|
||||||
integrations/
|
|
||||||
agentdb_bridge.py # AgentDB integration bridge
|
|
||||||
agentdb_real_integration.py
|
|
||||||
fallback_system.py # Graceful degradation system
|
|
||||||
learning_feedback.py # Learning loop integration
|
|
||||||
validation_system.py # Integration validation
|
|
||||||
article-to-prototype/ # Example generated skill
|
|
||||||
exports/ # Export output directory
|
|
||||||
docs/ # Additional documentation
|
|
||||||
CHANGELOG.md # Version history
|
|
||||||
NAMING_CONVENTIONS.md # Naming rules reference
|
|
||||||
PIPELINE_ARCHITECTURE.md # Pipeline internals
|
|
||||||
DECISION_LOGIC.md # Architecture decision logic
|
|
||||||
CLAUDE_SKILLS_ARCHITECTURE.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Claude Code (personal)
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git ~/.claude/skills/agent-skill-creator
|
||||||
|
# GitHub Copilot
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .github/skills/agent-skill-creator
|
||||||
|
# Cursor
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .cursor/rules/agent-skill-creator
|
||||||
|
# Windsurf
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .windsurf/skills/agent-skill-creator
|
||||||
|
# Cline
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .clinerules/agent-skill-creator
|
||||||
|
# Codex CLI
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .codex/skills/agent-skill-creator
|
||||||
|
# Gemini CLI
|
||||||
|
git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .gemini/skills/agent-skill-creator
|
||||||
|
# Update
|
||||||
|
cd <install-path>/agent-skill-creator && git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate
|
||||||
|
python3 scripts/validate.py PATH # Human output
|
||||||
|
python3 scripts/validate.py PATH --json # Machine output
|
||||||
|
|
||||||
|
# Security scan
|
||||||
|
python3 scripts/security_scan.py PATH
|
||||||
|
python3 scripts/security_scan.py PATH --json
|
||||||
|
|
||||||
|
# Export
|
||||||
|
python3 scripts/export_utils.py PATH --variant desktop
|
||||||
|
python3 scripts/export_utils.py PATH --variant api
|
||||||
|
|
||||||
|
# Registry
|
||||||
|
python3 scripts/skill_registry.py init --registry PATH --name NAME
|
||||||
|
python3 scripts/skill_registry.py publish SKILL_PATH --registry PATH --tags T1,T2
|
||||||
|
python3 scripts/skill_registry.py list --registry PATH [--json]
|
||||||
|
python3 scripts/skill_registry.py search QUERY --registry PATH [--json]
|
||||||
|
python3 scripts/skill_registry.py install SKILL_NAME --registry PATH --platform PLATFORM
|
||||||
|
python3 scripts/skill_registry.py info SKILL_NAME --registry PATH [--json]
|
||||||
|
python3 scripts/skill_registry.py remove SKILL_NAME --registry PATH --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform Paths
|
||||||
|
|
||||||
|
| Platform | Path | Scope |
|
||||||
|
|----------|------|-------|
|
||||||
|
| Claude Code | `~/.claude/skills/` | User-level |
|
||||||
|
| Claude Code | `.claude/skills/` | Project-level |
|
||||||
|
| GitHub Copilot | `.github/skills/` | Project-level |
|
||||||
|
| Cursor | `.cursor/rules/` | Workspace |
|
||||||
|
| Windsurf | `.windsurf/skills/` | Workspace |
|
||||||
|
| Cline | `.clinerules/` | Workspace |
|
||||||
|
| Codex CLI | `.codex/skills/` | Workspace |
|
||||||
|
| Gemini CLI | `.gemini/skills/` | Workspace |
|
||||||
|
| Claude Desktop | `.zip` upload | App-level |
|
||||||
|
| claude.ai | `.zip` upload | Web |
|
||||||
|
| Claude API | `.zip` via API | Programmatic |
|
||||||
|
|
||||||
|
### SKILL.md Spec (Required Fields)
|
||||||
|
|
||||||
|
```yaml
|
||||||
---
|
---
|
||||||
|
name: kebab-case-name # 1-64 chars, ^[a-z][a-z0-9-]*[a-z0-9]$
|
||||||
## Activation Triggers
|
description: >- # 1-1024 chars, include activation keywords
|
||||||
|
What this skill does...
|
||||||
The skill creator activates when it detects phrases like:
|
license: MIT
|
||||||
|
metadata:
|
||||||
- "Create an agent for ..."
|
author: Author Name
|
||||||
- "Create a skill for ..."
|
version: X.Y.Z
|
||||||
- "Automate this workflow"
|
|
||||||
- "Every day I have to ..."
|
|
||||||
- "I need to automate ..."
|
|
||||||
- "Create a cross-platform skill for ..."
|
|
||||||
- "Validate this skill"
|
|
||||||
- "Export this skill for [platform]"
|
|
||||||
- "Migrate this skill to v4"
|
|
||||||
|
|
||||||
See [references/phase4-detection.md](references/phase4-detection.md) for the full activation pattern reference.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
# Body: <500 lines. Move detailed content to references/.
|
||||||
|
```
|
||||||
|
|
||||||
## AgentDB Integration (Optional)
|
### Pipeline Phases
|
||||||
|
|
||||||
Skills can optionally integrate with AgentDB for persistent learning across sessions:
|
```
|
||||||
|
DISCOVERY -> DESIGN -> ARCHITECTURE -> DETECTION -> IMPLEMENTATION
|
||||||
|
```
|
||||||
|
|
||||||
- **Learning feedback**: Skills improve based on usage patterns
|
Each phase is documented in `references/phase{1..5}-*.md`.
|
||||||
- **Cross-session memory**: Retain context between conversations
|
|
||||||
- **Performance metrics**: Track skill effectiveness over time
|
|
||||||
|
|
||||||
AgentDB is not required. Skills work fully without it. See [references/agentdb-integration.md](references/agentdb-integration.md) for setup details.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration from v3.x
|
## Migration from v3.x
|
||||||
|
|
||||||
If you have skills created with v3.x of agent-skill-creator:
|
Key changes in v4.0:
|
||||||
|
|
||||||
**Key changes in v4.0:**
|
|
||||||
- `-cskill` suffix removed from skill names (use standard kebab-case)
|
- `-cskill` suffix removed from skill names (use standard kebab-case)
|
||||||
- `marketplace.json` simplified (optional for simple skills)
|
|
||||||
- SKILL.md body limited to 500 lines (move detail to `references/`)
|
- SKILL.md body limited to 500 lines (move detail to `references/`)
|
||||||
- `install.sh` cross-platform installer added
|
- `install.sh` cross-platform installer added
|
||||||
- Spec validation and security scanning tools added
|
- Spec validation and security scanning tools added
|
||||||
|
- `marketplace.json` simplified (optional for simple skills)
|
||||||
|
|
||||||
|
Quick migration:
|
||||||
|
|
||||||
**Quick migration:**
|
|
||||||
```bash
|
```bash
|
||||||
# Rename directory (remove -cskill suffix)
|
|
||||||
mv my-skill-cskill/ my-skill/
|
mv my-skill-cskill/ my-skill/
|
||||||
|
# Update SKILL.md name field to remove -cskill suffix
|
||||||
# Update SKILL.md name field
|
|
||||||
# Validate the migrated skill
|
|
||||||
python3 scripts/validate.py ./my-skill/
|
python3 scripts/validate.py ./my-skill/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -394,38 +466,65 @@ For the complete migration guide, see [MIGRATION.md](MIGRATION.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Advanced Features
|
## Troubleshooting
|
||||||
|
|
||||||
### Interactive Mode
|
**Skill not activating**: Ensure SKILL.md `description` field contains the trigger phrases you expect. The description is the primary activation mechanism.
|
||||||
|
|
||||||
For complex skills, the creator can run in interactive mode, asking clarifying questions before generating:
|
**Validation fails on name**: Names must be kebab-case, 1-64 characters, no consecutive hyphens, no leading/trailing hyphens. Pattern: `^[a-z][a-z0-9-]*[a-z0-9]$`.
|
||||||
|
|
||||||
|
**SKILL.md too long**: Body must be under 500 lines. Move detailed documentation to `references/` and link from the main SKILL.md.
|
||||||
|
|
||||||
|
**Export fails with size error**: API exports have an 8 MB limit. Reduce asset sizes or exclude large files.
|
||||||
|
|
||||||
|
**install.sh not executable**: Run `chmod +x install.sh` before executing.
|
||||||
|
|
||||||
|
**Platform not auto-detected**: Use `./install.sh --platform <name>` to specify explicitly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
"Create a skill for financial analysis" (interactive)
|
agent-skill-creator/
|
||||||
|
SKILL.md # Meta-skill definition
|
||||||
|
README.md # This file
|
||||||
|
MIGRATION.md # v3.x to v4.0 migration guide
|
||||||
|
scripts/
|
||||||
|
validate.py # Spec compliance validator
|
||||||
|
security_scan.py # Security scanner
|
||||||
|
export_utils.py # Cross-platform export tool
|
||||||
|
skill_registry.py # Git-based shared skill registry
|
||||||
|
install-template.sh # Template for generated install.sh
|
||||||
|
references/
|
||||||
|
pipeline-phases.md # Full 5-phase pipeline docs
|
||||||
|
architecture-guide.md # Simple skill vs. complex suite
|
||||||
|
cross-platform-guide.md # Platform-specific details
|
||||||
|
export-guide.md # Export system documentation
|
||||||
|
phase1-discovery.md # Phase 1 deep dive
|
||||||
|
phase2-design.md # Phase 2 deep dive
|
||||||
|
phase3-architecture.md # Phase 3 deep dive
|
||||||
|
phase4-detection.md # Phase 4 deep dive
|
||||||
|
phase5-implementation.md # Phase 5 deep dive
|
||||||
|
phase6-testing.md # Testing guide
|
||||||
|
quality-standards.md # Quality standards reference
|
||||||
|
templates-guide.md # Template system guide
|
||||||
|
templates/ # Skill templates
|
||||||
|
tools/ # Validation and scanning tools
|
||||||
|
examples/ # Example configurations
|
||||||
|
integrations/
|
||||||
|
agentdb_bridge.py # AgentDB integration bridge
|
||||||
|
fallback_system.py # Graceful degradation system
|
||||||
|
learning_feedback.py # Learning loop integration
|
||||||
|
validation_system.py # Integration validation
|
||||||
|
article-to-prototype/ # Example generated skill
|
||||||
|
exports/ # Export output directory
|
||||||
|
docs/
|
||||||
|
CHANGELOG.md # Version history
|
||||||
|
NAMING_CONVENTIONS.md # Naming rules reference
|
||||||
|
PIPELINE_ARCHITECTURE.md # Pipeline internals
|
||||||
|
DECISION_LOGIC.md # Architecture decision logic
|
||||||
```
|
```
|
||||||
|
|
||||||
See [references/interactive-mode.md](references/interactive-mode.md).
|
|
||||||
|
|
||||||
### Multi-Agent Suites
|
|
||||||
|
|
||||||
Create coordinated multi-agent systems where specialized agents collaborate:
|
|
||||||
|
|
||||||
```
|
|
||||||
"Create a multi-agent suite for end-to-end data pipeline"
|
|
||||||
```
|
|
||||||
|
|
||||||
See [references/multi-agent-guide.md](references/multi-agent-guide.md).
|
|
||||||
|
|
||||||
### Template-Based Creation
|
|
||||||
|
|
||||||
Use pre-built templates to accelerate skill creation:
|
|
||||||
|
|
||||||
```
|
|
||||||
"Create a skill from the data-analysis template"
|
|
||||||
```
|
|
||||||
|
|
||||||
See [references/templates-guide.md](references/templates-guide.md).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
@ -441,7 +540,7 @@ See [references/templates-guide.md](references/templates-guide.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License. See [LICENSE](LICENSE) for details.
|
MIT License.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
686
scripts/skill_registry.py
Normal file
686
scripts/skill_registry.py
Normal file
|
|
@ -0,0 +1,686 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Git-Based Shared Skill Registry.
|
||||||
|
|
||||||
|
Manages a git-friendly skill registry for publishing, discovering, and installing
|
||||||
|
cross-platform agent skills. The registry is a directory with a registry.json
|
||||||
|
manifest and a skills/ folder — no servers, no databases, no new dependencies.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/skill_registry.py init [--name NAME] [--registry PATH]
|
||||||
|
python3 scripts/skill_registry.py publish <skill-path> [--registry PATH] [--tags T1,T2] [--force] [--json]
|
||||||
|
python3 scripts/skill_registry.py list [--registry PATH] [--json]
|
||||||
|
python3 scripts/skill_registry.py search <query> [--registry PATH] [--json]
|
||||||
|
python3 scripts/skill_registry.py install <skill-name> [--registry PATH] [--platform PLATFORM] [--project] [--force] [--json]
|
||||||
|
python3 scripts/skill_registry.py info <skill-name> [--registry PATH] [--json]
|
||||||
|
python3 scripts/skill_registry.py remove <skill-name> [--registry PATH] [--force]
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - Success
|
||||||
|
1 - Error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# --- Import sibling scripts ---
|
||||||
|
|
||||||
|
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||||
|
if str(_SCRIPTS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
||||||
|
|
||||||
|
from validate import validate_skill, _parse_frontmatter, _parse_yaml_field, _parse_subfield_value
|
||||||
|
from security_scan import security_scan
|
||||||
|
|
||||||
|
|
||||||
|
# --- Constants ---
|
||||||
|
|
||||||
|
ALL_PLATFORMS = ["claude-code", "copilot", "cursor", "windsurf", "cline", "codex", "gemini"]
|
||||||
|
|
||||||
|
PLATFORM_PATHS_USER = {
|
||||||
|
"claude-code": "~/.claude/skills",
|
||||||
|
"copilot": "~/.copilot/skills",
|
||||||
|
"cursor": "~/.cursor/rules",
|
||||||
|
"windsurf": "~/.windsurf/skills",
|
||||||
|
"cline": "~/.cline/rules",
|
||||||
|
"codex": "~/.codex/skills",
|
||||||
|
"gemini": "~/.gemini/skills",
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_PATHS_PROJECT = {
|
||||||
|
"claude-code": ".claude/skills",
|
||||||
|
"copilot": ".github/skills",
|
||||||
|
"cursor": ".cursor/rules",
|
||||||
|
"windsurf": ".windsurf/skills",
|
||||||
|
"cline": ".clinerules",
|
||||||
|
"codex": ".codex/skills",
|
||||||
|
"gemini": ".gemini/skills",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Directories/files to exclude when copying skills
|
||||||
|
COPY_IGNORE_PATTERNS = shutil.ignore_patterns(
|
||||||
|
".git", "__pycache__", "node_modules", ".venv", "venv", "env",
|
||||||
|
".pytest_cache", ".mypy_cache", "dist", "build", "*.pyc", "*.pyo",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stop words for auto-tagging
|
||||||
|
STOP_WORDS = {
|
||||||
|
"a", "an", "the", "and", "or", "but", "is", "are", "was", "were", "be",
|
||||||
|
"been", "being", "in", "on", "at", "to", "for", "of", "with", "by",
|
||||||
|
"from", "as", "into", "through", "during", "before", "after", "above",
|
||||||
|
"below", "between", "out", "off", "over", "under", "again", "further",
|
||||||
|
"then", "once", "here", "there", "when", "where", "why", "how", "all",
|
||||||
|
"each", "every", "both", "few", "more", "most", "other", "some", "such",
|
||||||
|
"no", "nor", "not", "only", "own", "same", "so", "than", "too", "very",
|
||||||
|
"can", "will", "just", "should", "now", "it", "its", "this", "that",
|
||||||
|
"these", "those", "he", "she", "we", "they", "what", "which", "who",
|
||||||
|
"whom", "do", "does", "did", "has", "have", "had", "having", "using",
|
||||||
|
}
|
||||||
|
|
||||||
|
MIN_TAG_LENGTH = 3
|
||||||
|
|
||||||
|
|
||||||
|
# --- Registry I/O ---
|
||||||
|
|
||||||
|
def load_registry(registry_path: Path) -> dict:
|
||||||
|
"""Read and parse registry.json from the registry directory."""
|
||||||
|
manifest = registry_path / "registry.json"
|
||||||
|
if not manifest.exists():
|
||||||
|
print(f"Error: registry.json not found in {registry_path}", file=sys.stderr)
|
||||||
|
print("Run 'skill_registry.py init' first.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
return json.loads(manifest.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError) as exc:
|
||||||
|
print(f"Error reading registry.json: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def save_registry(registry_path: Path, data: dict) -> None:
|
||||||
|
"""Atomic write: write to .tmp then rename."""
|
||||||
|
manifest = registry_path / "registry.json"
|
||||||
|
tmp = registry_path / "registry.json.tmp"
|
||||||
|
try:
|
||||||
|
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
tmp.replace(manifest)
|
||||||
|
except OSError as exc:
|
||||||
|
# Clean up tmp on failure
|
||||||
|
if tmp.exists():
|
||||||
|
tmp.unlink()
|
||||||
|
print(f"Error writing registry.json: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Metadata Extraction ---
|
||||||
|
|
||||||
|
def extract_skill_metadata(skill_path: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Parse SKILL.md frontmatter into a metadata dict.
|
||||||
|
|
||||||
|
Returns dict with keys: name, description, version, author, license.
|
||||||
|
Missing fields default to empty string.
|
||||||
|
"""
|
||||||
|
skill_md = skill_path / "SKILL.md"
|
||||||
|
if not skill_md.exists():
|
||||||
|
return {"name": "", "description": "", "version": "", "author": "", "license": ""}
|
||||||
|
|
||||||
|
content = skill_md.read_text(encoding="utf-8")
|
||||||
|
frontmatter, _ = _parse_frontmatter(content)
|
||||||
|
if frontmatter is None:
|
||||||
|
return {"name": "", "description": "", "version": "", "author": "", "license": ""}
|
||||||
|
|
||||||
|
name = _parse_yaml_field(frontmatter, "name") or ""
|
||||||
|
description = _parse_yaml_field(frontmatter, "description") or ""
|
||||||
|
license_val = _parse_yaml_field(frontmatter, "license") or ""
|
||||||
|
|
||||||
|
# Version: try metadata.version first, then top-level version
|
||||||
|
version = _parse_subfield_value(frontmatter, "metadata", "version")
|
||||||
|
if not version:
|
||||||
|
version = _parse_yaml_field(frontmatter, "version") or ""
|
||||||
|
|
||||||
|
# Author: try metadata.author first
|
||||||
|
author = _parse_subfield_value(frontmatter, "metadata", "author") or ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name.strip(),
|
||||||
|
"description": description.strip(),
|
||||||
|
"version": version.strip(),
|
||||||
|
"author": author.strip(),
|
||||||
|
"license": license_val.strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def auto_extract_tags(description: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Extract keyword tags from a description string.
|
||||||
|
|
||||||
|
Splits on non-alphanumeric characters, filters stop words and short words,
|
||||||
|
returns up to 10 unique lowercase tags.
|
||||||
|
"""
|
||||||
|
if not description:
|
||||||
|
return []
|
||||||
|
words = re.split(r"[^a-zA-Z0-9-]+", description.lower())
|
||||||
|
seen: set[str] = set()
|
||||||
|
tags: list[str] = []
|
||||||
|
for word in words:
|
||||||
|
word = word.strip("-")
|
||||||
|
if len(word) < MIN_TAG_LENGTH:
|
||||||
|
continue
|
||||||
|
if word in STOP_WORDS:
|
||||||
|
continue
|
||||||
|
if word not in seen:
|
||||||
|
seen.add(word)
|
||||||
|
tags.append(word)
|
||||||
|
if len(tags) >= 10:
|
||||||
|
break
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
# --- Platform Detection ---
|
||||||
|
|
||||||
|
def detect_platform() -> str:
|
||||||
|
"""
|
||||||
|
Auto-detect the installed agent platform by checking known directories.
|
||||||
|
|
||||||
|
Returns the platform name or "claude-code" as default.
|
||||||
|
"""
|
||||||
|
checks = [
|
||||||
|
("claude-code", "~/.claude"),
|
||||||
|
("copilot", "~/.copilot"),
|
||||||
|
("cursor", "~/.cursor"),
|
||||||
|
("windsurf", "~/.windsurf"),
|
||||||
|
("cline", "~/.cline"),
|
||||||
|
("codex", "~/.codex"),
|
||||||
|
("gemini", "~/.gemini"),
|
||||||
|
]
|
||||||
|
for platform, path in checks:
|
||||||
|
if Path(path).expanduser().exists():
|
||||||
|
return platform
|
||||||
|
return "claude-code"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_install_path(name: str, platform: str, project: bool) -> Path:
|
||||||
|
"""
|
||||||
|
Map platform + scope to the filesystem install path for a skill.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Skill name (used as subdirectory).
|
||||||
|
platform: Platform identifier.
|
||||||
|
project: If True, use project-level path; otherwise user-level.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path where the skill should be installed.
|
||||||
|
"""
|
||||||
|
if project:
|
||||||
|
base = PLATFORM_PATHS_PROJECT.get(platform)
|
||||||
|
else:
|
||||||
|
base = PLATFORM_PATHS_USER.get(platform)
|
||||||
|
|
||||||
|
if base is None:
|
||||||
|
print(f"Error: unknown platform '{platform}'", file=sys.stderr)
|
||||||
|
print(f"Supported: {', '.join(ALL_PLATFORMS)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return Path(base).expanduser().resolve() / name
|
||||||
|
|
||||||
|
|
||||||
|
# --- Table Formatting ---
|
||||||
|
|
||||||
|
def _format_table(entries: list[dict]) -> str:
|
||||||
|
"""Format skill entries as an aligned text table."""
|
||||||
|
if not entries:
|
||||||
|
return "No skills found."
|
||||||
|
|
||||||
|
headers = ["NAME", "VERSION", "AUTHOR", "TAGS"]
|
||||||
|
rows = []
|
||||||
|
for entry in entries:
|
||||||
|
tags = ", ".join(entry.get("tags", []))
|
||||||
|
rows.append([
|
||||||
|
entry.get("name", ""),
|
||||||
|
entry.get("version", ""),
|
||||||
|
entry.get("author", ""),
|
||||||
|
tags,
|
||||||
|
])
|
||||||
|
|
||||||
|
# Calculate column widths
|
||||||
|
widths = [len(h) for h in headers]
|
||||||
|
for row in rows:
|
||||||
|
for i, cell in enumerate(row):
|
||||||
|
widths[i] = max(widths[i], len(cell))
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
lines = []
|
||||||
|
header_line = " ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
|
||||||
|
lines.append(header_line)
|
||||||
|
for row in rows:
|
||||||
|
lines.append(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Subcommands ---
|
||||||
|
|
||||||
|
def cmd_init(args: argparse.Namespace) -> None:
|
||||||
|
"""Initialize a new skill registry."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
manifest = registry_path / "registry.json"
|
||||||
|
|
||||||
|
if manifest.exists():
|
||||||
|
print(f"Error: registry already exists at {registry_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
registry_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
(registry_path / "skills").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
name = args.name or "Shared Skills"
|
||||||
|
data = {
|
||||||
|
"registry": {
|
||||||
|
"name": name,
|
||||||
|
"created": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||||
|
"schema_version": "1",
|
||||||
|
},
|
||||||
|
"skills": [],
|
||||||
|
}
|
||||||
|
save_registry(registry_path, data)
|
||||||
|
print(f"Registry initialized: {registry_path}")
|
||||||
|
print(f" Name: {name}")
|
||||||
|
print(f" Manifest: {manifest}")
|
||||||
|
print(f" Skills dir: {registry_path / 'skills'}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_publish(args: argparse.Namespace) -> None:
|
||||||
|
"""Publish a skill to the registry."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
skill_path = Path(args.skill_path).resolve()
|
||||||
|
|
||||||
|
if not skill_path.is_dir():
|
||||||
|
print(f"Error: skill path is not a directory: {skill_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 1: Validate
|
||||||
|
validation = validate_skill(str(skill_path))
|
||||||
|
if not validation["valid"]:
|
||||||
|
print("Validation failed:", file=sys.stderr)
|
||||||
|
for err in validation["errors"]:
|
||||||
|
print(f" [ERROR] {err}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 2: Security scan
|
||||||
|
scan = security_scan(str(skill_path))
|
||||||
|
high_issues = [i for i in scan["issues"] if i["severity"] == "high"]
|
||||||
|
other_issues = [i for i in scan["issues"] if i["severity"] != "high"]
|
||||||
|
|
||||||
|
if other_issues:
|
||||||
|
for issue in other_issues:
|
||||||
|
location = issue["file"]
|
||||||
|
if issue["line"] > 0:
|
||||||
|
location += f":{issue['line']}"
|
||||||
|
print(f" [WARN] {location}: {issue['description']}")
|
||||||
|
|
||||||
|
if high_issues and not args.force:
|
||||||
|
print("Security scan found high-severity issues:", file=sys.stderr)
|
||||||
|
for issue in high_issues:
|
||||||
|
location = issue["file"]
|
||||||
|
if issue["line"] > 0:
|
||||||
|
location += f":{issue['line']}"
|
||||||
|
print(f" [HIGH] {location}: {issue['description']}", file=sys.stderr)
|
||||||
|
print("Use --force to publish anyway.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 3: Extract metadata
|
||||||
|
metadata = extract_skill_metadata(skill_path)
|
||||||
|
name = metadata["name"]
|
||||||
|
version = metadata["version"] or "0.0.0"
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
print("Error: could not extract skill name from SKILL.md frontmatter", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 4: Tags
|
||||||
|
tags = []
|
||||||
|
if args.tags:
|
||||||
|
tags = [t.strip() for t in args.tags.split(",") if t.strip()]
|
||||||
|
if not tags:
|
||||||
|
tags = auto_extract_tags(metadata["description"])
|
||||||
|
|
||||||
|
# Step 5: Check duplicates
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
for existing in data["skills"]:
|
||||||
|
if existing["name"] == name and existing["version"] == version:
|
||||||
|
if not args.force:
|
||||||
|
print(
|
||||||
|
f"Error: skill '{name}' version '{version}' already exists in registry.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print("Use --force to overwrite.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
# Remove old entry if forcing
|
||||||
|
data["skills"] = [s for s in data["skills"] if not (s["name"] == name and s["version"] == version)]
|
||||||
|
|
||||||
|
# Step 6: Copy skill to registry
|
||||||
|
dest = registry_path / "skills" / name
|
||||||
|
if dest.exists():
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
shutil.copytree(skill_path, dest, ignore=COPY_IGNORE_PATTERNS)
|
||||||
|
|
||||||
|
# Step 7: Add entry
|
||||||
|
entry = {
|
||||||
|
"name": name,
|
||||||
|
"description": metadata["description"],
|
||||||
|
"version": version,
|
||||||
|
"author": metadata["author"],
|
||||||
|
"license": metadata["license"],
|
||||||
|
"tags": tags,
|
||||||
|
"platforms": list(ALL_PLATFORMS),
|
||||||
|
"published": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||||
|
"path": f"skills/{name}",
|
||||||
|
"validation": {
|
||||||
|
"valid": validation["valid"],
|
||||||
|
"errors": len(validation["errors"]),
|
||||||
|
"warnings": len(validation["warnings"]),
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"clean": scan["clean"],
|
||||||
|
"issues": len(scan["issues"]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data["skills"].append(entry)
|
||||||
|
save_registry(registry_path, data)
|
||||||
|
|
||||||
|
if getattr(args, "json", False):
|
||||||
|
print(json.dumps(entry, indent=2))
|
||||||
|
else:
|
||||||
|
print(f"Published '{name}' v{version} to registry.")
|
||||||
|
print(f" Path: {dest}")
|
||||||
|
print(f" Tags: {', '.join(tags)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list(args: argparse.Namespace) -> None:
|
||||||
|
"""List all skills in the registry."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
|
||||||
|
if getattr(args, "json", False):
|
||||||
|
print(json.dumps(data["skills"], indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
print(_format_table(data["skills"]))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_search(args: argparse.Namespace) -> None:
|
||||||
|
"""Search for skills matching a query."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
query = args.query.lower()
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
for skill in data["skills"]:
|
||||||
|
searchable = " ".join([
|
||||||
|
skill.get("name", ""),
|
||||||
|
skill.get("description", ""),
|
||||||
|
skill.get("author", ""),
|
||||||
|
" ".join(skill.get("tags", [])),
|
||||||
|
]).lower()
|
||||||
|
if query in searchable:
|
||||||
|
matches.append(skill)
|
||||||
|
|
||||||
|
if getattr(args, "json", False):
|
||||||
|
print(json.dumps(matches, indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
print(f"No skills matching '{args.query}'.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Skills matching '{args.query}':\n")
|
||||||
|
print(_format_table(matches))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_install(args: argparse.Namespace) -> None:
|
||||||
|
"""Install a skill from the registry."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
|
||||||
|
# Find skill
|
||||||
|
skill_entry = None
|
||||||
|
for skill in data["skills"]:
|
||||||
|
if skill["name"] == args.skill_name:
|
||||||
|
skill_entry = skill
|
||||||
|
break
|
||||||
|
|
||||||
|
if skill_entry is None:
|
||||||
|
print(f"Error: skill '{args.skill_name}' not found in registry.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Resolve platform
|
||||||
|
platform = args.platform or detect_platform()
|
||||||
|
if platform not in ALL_PLATFORMS:
|
||||||
|
print(f"Error: unknown platform '{platform}'", file=sys.stderr)
|
||||||
|
print(f"Supported: {', '.join(ALL_PLATFORMS)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Resolve target path
|
||||||
|
project = getattr(args, "project", False)
|
||||||
|
target = resolve_install_path(args.skill_name, platform, project)
|
||||||
|
|
||||||
|
# Check if already installed
|
||||||
|
if target.exists() and not args.force:
|
||||||
|
print(f"Error: skill already installed at {target}", file=sys.stderr)
|
||||||
|
print("Use --force to overwrite.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Copy
|
||||||
|
source = registry_path / skill_entry["path"]
|
||||||
|
if not source.exists():
|
||||||
|
print(f"Error: skill files not found at {source}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if target.exists():
|
||||||
|
shutil.rmtree(target)
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copytree(source, target, ignore=COPY_IGNORE_PATTERNS)
|
||||||
|
|
||||||
|
if getattr(args, "json", False):
|
||||||
|
print(json.dumps({
|
||||||
|
"installed": True,
|
||||||
|
"skill": args.skill_name,
|
||||||
|
"platform": platform,
|
||||||
|
"path": str(target),
|
||||||
|
}, indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
scope = "project" if project else "user"
|
||||||
|
print(f"Installed '{args.skill_name}' for {platform} ({scope}-level).")
|
||||||
|
print(f" Path: {target}")
|
||||||
|
|
||||||
|
# Platform-specific activation tips
|
||||||
|
tips = {
|
||||||
|
"claude-code": "Skill is auto-loaded. Start a new conversation to activate.",
|
||||||
|
"copilot": "Skill is auto-loaded by Copilot Chat.",
|
||||||
|
"cursor": "Skill is loaded alongside .mdc rules.",
|
||||||
|
"windsurf": "Skill is auto-loaded by Windsurf.",
|
||||||
|
"cline": "Skill is loaded from .clinerules.",
|
||||||
|
"codex": "Skill is auto-loaded by Codex CLI.",
|
||||||
|
"gemini": "Skill is auto-loaded by Gemini CLI.",
|
||||||
|
}
|
||||||
|
tip = tips.get(platform)
|
||||||
|
if tip:
|
||||||
|
print(f" Tip: {tip}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_info(args: argparse.Namespace) -> None:
|
||||||
|
"""Show detailed info about a skill."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
|
||||||
|
skill_entry = None
|
||||||
|
for skill in data["skills"]:
|
||||||
|
if skill["name"] == args.skill_name:
|
||||||
|
skill_entry = skill
|
||||||
|
break
|
||||||
|
|
||||||
|
if skill_entry is None:
|
||||||
|
print(f"Error: skill '{args.skill_name}' not found in registry.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if getattr(args, "json", False):
|
||||||
|
print(json.dumps(skill_entry, indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Skill: {skill_entry['name']}")
|
||||||
|
print(f"{'=' * 50}")
|
||||||
|
print(f" Version: {skill_entry.get('version', 'N/A')}")
|
||||||
|
print(f" Author: {skill_entry.get('author', 'N/A')}")
|
||||||
|
print(f" License: {skill_entry.get('license', 'N/A')}")
|
||||||
|
print(f" Description: {skill_entry.get('description', 'N/A')}")
|
||||||
|
print(f" Tags: {', '.join(skill_entry.get('tags', []))}")
|
||||||
|
print(f" Platforms: {', '.join(skill_entry.get('platforms', []))}")
|
||||||
|
print(f" Published: {skill_entry.get('published', 'N/A')}")
|
||||||
|
print(f" Path: {skill_entry.get('path', 'N/A')}")
|
||||||
|
|
||||||
|
validation = skill_entry.get("validation", {})
|
||||||
|
if validation:
|
||||||
|
status = "valid" if validation.get("valid") else "invalid"
|
||||||
|
print(f" Validation: {status} ({validation.get('errors', 0)} errors, {validation.get('warnings', 0)} warnings)")
|
||||||
|
|
||||||
|
security = skill_entry.get("security", {})
|
||||||
|
if security:
|
||||||
|
status = "clean" if security.get("clean") else f"{security.get('issues', 0)} issues"
|
||||||
|
print(f" Security: {status}")
|
||||||
|
|
||||||
|
print(f"{'=' * 50}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_remove(args: argparse.Namespace) -> None:
|
||||||
|
"""Remove a skill from the registry."""
|
||||||
|
registry_path = Path(args.registry).resolve()
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
|
||||||
|
# Find skill
|
||||||
|
skill_entry = None
|
||||||
|
for skill in data["skills"]:
|
||||||
|
if skill["name"] == args.skill_name:
|
||||||
|
skill_entry = skill
|
||||||
|
break
|
||||||
|
|
||||||
|
if skill_entry is None:
|
||||||
|
print(f"Error: skill '{args.skill_name}' not found in registry.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not args.force:
|
||||||
|
print(f"Remove '{args.skill_name}' from registry? Use --force to confirm.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Remove files
|
||||||
|
skill_dir = registry_path / skill_entry["path"]
|
||||||
|
if skill_dir.exists():
|
||||||
|
shutil.rmtree(skill_dir)
|
||||||
|
|
||||||
|
# Remove entry
|
||||||
|
data["skills"] = [s for s in data["skills"] if s["name"] != args.skill_name]
|
||||||
|
save_registry(registry_path, data)
|
||||||
|
|
||||||
|
print(f"Removed '{args.skill_name}' from registry.")
|
||||||
|
|
||||||
|
|
||||||
|
# --- CLI ---
|
||||||
|
|
||||||
|
def _add_registry_arg(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""Add the --registry argument to a subparser."""
|
||||||
|
parser.add_argument(
|
||||||
|
"--registry", default="./registry",
|
||||||
|
help="Path to the registry directory (default: ./registry)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
"""Build the argument parser with all subcommands."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="skill_registry",
|
||||||
|
description="Git-based shared skill registry for cross-platform agent skills.",
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||||
|
|
||||||
|
# init
|
||||||
|
p_init = subparsers.add_parser("init", help="Initialize a new skill registry")
|
||||||
|
_add_registry_arg(p_init)
|
||||||
|
p_init.add_argument("--name", help="Registry name (default: 'Shared Skills')")
|
||||||
|
|
||||||
|
# publish
|
||||||
|
p_publish = subparsers.add_parser("publish", help="Publish a skill to the registry")
|
||||||
|
p_publish.add_argument("skill_path", help="Path to the skill directory")
|
||||||
|
_add_registry_arg(p_publish)
|
||||||
|
p_publish.add_argument("--tags", help="Comma-separated tags (auto-extracted if omitted)")
|
||||||
|
p_publish.add_argument("--force", action="store_true", help="Overwrite existing or ignore high-severity issues")
|
||||||
|
p_publish.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
|
||||||
|
# list
|
||||||
|
p_list = subparsers.add_parser("list", help="List all skills in the registry")
|
||||||
|
_add_registry_arg(p_list)
|
||||||
|
p_list.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
|
||||||
|
# search
|
||||||
|
p_search = subparsers.add_parser("search", help="Search for skills")
|
||||||
|
p_search.add_argument("query", help="Search query (matches name, description, author, tags)")
|
||||||
|
_add_registry_arg(p_search)
|
||||||
|
p_search.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
|
||||||
|
# install
|
||||||
|
p_install = subparsers.add_parser("install", help="Install a skill from the registry")
|
||||||
|
p_install.add_argument("skill_name", help="Name of the skill to install")
|
||||||
|
_add_registry_arg(p_install)
|
||||||
|
p_install.add_argument("--platform", choices=ALL_PLATFORMS, help="Target platform (auto-detected if omitted)")
|
||||||
|
p_install.add_argument("--project", action="store_true", help="Install at project level instead of user level")
|
||||||
|
p_install.add_argument("--force", action="store_true", help="Overwrite existing installation")
|
||||||
|
p_install.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
|
||||||
|
# info
|
||||||
|
p_info = subparsers.add_parser("info", help="Show detailed info about a skill")
|
||||||
|
p_info.add_argument("skill_name", help="Name of the skill")
|
||||||
|
_add_registry_arg(p_info)
|
||||||
|
p_info.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
|
||||||
|
# remove
|
||||||
|
p_remove = subparsers.add_parser("remove", help="Remove a skill from the registry")
|
||||||
|
p_remove.add_argument("skill_name", help="Name of the skill to remove")
|
||||||
|
_add_registry_arg(p_remove)
|
||||||
|
p_remove.add_argument("--force", action="store_true", help="Confirm removal")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""CLI entry point."""
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command is None:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"init": cmd_init,
|
||||||
|
"publish": cmd_publish,
|
||||||
|
"list": cmd_list,
|
||||||
|
"search": cmd_search,
|
||||||
|
"install": cmd_install,
|
||||||
|
"info": cmd_info,
|
||||||
|
"remove": cmd_remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_func = commands.get(args.command)
|
||||||
|
if cmd_func is None:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd_func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -147,6 +147,34 @@ def _subfield_exists(frontmatter: str, parent: str, child: str) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_subfield_value(frontmatter: str, parent: str, child: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Extract a sub-field value from under a parent field in YAML frontmatter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frontmatter: The frontmatter text.
|
||||||
|
parent: The parent field name (e.g., ``metadata``).
|
||||||
|
child: The child field name (e.g., ``author``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The sub-field value as a string, or None if not found.
|
||||||
|
"""
|
||||||
|
lines = frontmatter.split("\n")
|
||||||
|
in_parent = False
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith(f"{parent}:"):
|
||||||
|
in_parent = True
|
||||||
|
continue
|
||||||
|
if in_parent:
|
||||||
|
if line and (line[0] == " " or line[0] == "\t"):
|
||||||
|
if stripped.startswith(f"{child}:"):
|
||||||
|
return stripped[len(child) + 1:].strip()
|
||||||
|
else:
|
||||||
|
in_parent = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _extract_local_links(body: str) -> list[str]:
|
def _extract_local_links(body: str) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Extract local file paths referenced in markdown links within the body.
|
Extract local file paths referenced in markdown links within the body.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue