This commit is contained in:
Danilo Reyes
2026-03-16 16:16:32 -06:00
parent 4f4c23d1df
commit 195c55891e
35 changed files with 430 additions and 469 deletions

View File

@@ -1 +1 @@
6277a8b83820afd8
821f2ce8d073b28f

View File

@@ -11,7 +11,7 @@ This skill provides guidance for creating effective skills.
## About Skills
Skills are modular, self-contained folders that extend Codex's capabilities by providing
Skills are modular, self-contained packages that extend Codex's capabilities by providing
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
domains or tasks—they transform Codex from a general-purpose agent into a specialized agent
equipped with procedural knowledge that no model can fully possess.
@@ -56,8 +56,6 @@ skill-name/
│ │ ├── name: (required)
│ │ └── description: (required)
│ └── Markdown instructions (required)
├── agents/ (recommended)
│ └── openai.yaml - UI metadata for skill lists and chips
└── Bundled Resources (optional)
├── scripts/ - Executable code (Python/Bash/etc.)
├── references/ - Documentation intended to be loaded into context as needed
@@ -71,16 +69,6 @@ Every SKILL.md consists of:
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
#### Agents metadata (recommended)
- UI-facing metadata for skill lists and chips
- Read references/openai_yaml.md before generating values and follow its descriptions and constraints
- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill
- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`
- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale
- Only include other optional interface fields (icons, brand color) if explicitly provided
- See references/openai_yaml.md for field definitions and examples
#### Bundled Resources (optional)
##### Scripts (`scripts/`)
@@ -220,7 +208,7 @@ Skill creation involves these steps:
2. Plan reusable skill contents (scripts, references, assets)
3. Initialize the skill (run init_skill.py)
4. Edit the skill (implement resources and write SKILL.md)
5. Validate the skill (run quick_validate.py)
5. Package the skill (run package_skill.py)
6. Iterate based on real usage
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
@@ -278,7 +266,7 @@ To establish the skill's contents, analyze each concrete example to create a lis
At this point, it is time to actually create the skill.
Skip this step only if the skill being developed already exists. In this case, continue to the next step.
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
@@ -300,24 +288,24 @@ The script:
- Creates the skill directory at the specified path
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`
- Optionally creates resource directories based on `--resources`
- Optionally adds example files when `--examples` is set
After initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.
Generate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:
```bash
scripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value
```
Only include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.
### Step 4: Edit the Skill
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.
#### Learn Proven Design Patterns
Consult these helpful guides based on your skill's needs:
- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
These files contain established best practices for effective skill design.
#### Start with Reusable Skill Contents
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
@@ -340,21 +328,40 @@ Write the YAML frontmatter with `name` and `description`:
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Codex.
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
Ensure the frontmatter is valid YAML. Keep `name` and `description` as single-line scalars. If either could be interpreted as YAML syntax, wrap it in quotes.
Do not include any other fields in YAML frontmatter.
##### Body
Write instructions for using the skill and its bundled resources.
### Step 5: Validate the Skill
### Step 5: Packaging a Skill
Once development of the skill is complete, validate the skill folder to catch basic issues early:
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
```bash
scripts/quick_validate.py <path/to/skill-folder>
scripts/package_skill.py <path/to/skill-folder>
```
The validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.
Optional output directory specification:
```bash
scripts/package_skill.py <path/to/skill-folder> ./dist
```
The packaging script will:
1. **Validate** the skill automatically, checking:
- YAML frontmatter format and required fields
- Skill naming conventions and directory structure
- Description completeness and quality
- File organization and resource references
2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
### Step 6: Iterate

View File

@@ -1,5 +0,0 @@
interface:
display_name: "Skill Creator"
short_description: "Create or update a skill"
icon_small: "./assets/skill-creator-small.svg"
icon_large: "./assets/skill-creator.png"

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
<path fill="#0D0D0D" d="M12.03 4.113a3.612 3.612 0 0 1 5.108 5.108l-6.292 6.29c-.324.324-.56.561-.791.752l-.235.176c-.205.14-.422.261-.65.36l-.229.093a4.136 4.136 0 0 1-.586.16l-.764.134-2.394.4c-.142.024-.294.05-.423.06-.098.007-.232.01-.378-.026l-.149-.05a1.081 1.081 0 0 1-.521-.474l-.046-.093a1.104 1.104 0 0 1-.075-.527c.01-.129.035-.28.06-.422l.398-2.394c.1-.602.162-.987.295-1.35l.093-.23c.1-.228.22-.445.36-.65l.176-.235c.19-.232.428-.467.751-.79l6.292-6.292Zm-5.35 7.232c-.35.35-.534.535-.66.688l-.11.147a2.67 2.67 0 0 0-.24.433l-.062.154c-.08.22-.124.462-.232 1.112l-.398 2.394-.001.001h.003l2.393-.399.717-.126a2.63 2.63 0 0 0 .394-.105l.154-.063a2.65 2.65 0 0 0 .433-.24l.147-.11c.153-.126.339-.31.688-.66l4.988-4.988-3.227-3.226-4.987 4.988Zm9.517-6.291a2.281 2.281 0 0 0-3.225 0l-.364.362 3.226 3.227.363-.364c.89-.89.89-2.334 0-3.225ZM4.583 1.783a.3.3 0 0 1 .294.241c.117.585.347 1.092.707 1.48.357.385.859.668 1.549.783a.3.3 0 0 1 0 .592c-.69.115-1.192.398-1.549.783-.315.34-.53.77-.657 1.265l-.05.215a.3.3 0 0 1-.588 0c-.117-.585-.347-1.092-.707-1.48-.357-.384-.859-.668-1.549-.783a.3.3 0 0 1 0-.592c.69-.115 1.192-.398 1.549-.783.36-.388.59-.895.707-1.48l.015-.05a.3.3 0 0 1 .279-.19Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,49 +0,0 @@
# openai.yaml fields (full example + descriptions)
`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.
## Full example
```yaml
interface:
display_name: "Optional user-facing name"
short_description: "Optional user-facing description"
icon_small: "./assets/small-400px.png"
icon_large: "./assets/large-logo.svg"
brand_color: "#3B82F6"
default_prompt: "Optional surrounding prompt to use the skill with"
dependencies:
tools:
- type: "mcp"
value: "github"
description: "GitHub MCP server"
transport: "streamable_http"
url: "https://api.githubcopilot.com/mcp/"
policy:
allow_implicit_invocation: true
```
## Field descriptions and constraints
Top-level constraints:
- Quote all string values.
- Keep keys unquoted.
- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., "Use $skill-name-here to draft a concise weekly status update.").
- `interface.display_name`: Human-facing title shown in UI skill lists and chips.
- `interface.short_description`: Human-facing short UI blurb (2564 chars) for quick scanning.
- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
- `interface.brand_color`: Hex color used for UI accents (e.g., badges).
- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.
- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.
- `dependencies.tools[].value`: Identifier of the tool or dependency.
- `dependencies.tools[].description`: Human-readable explanation of the dependency.
- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.
- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.
- `policy.allow_implicit_invocation`: When false, the skill is not injected into
the model context by default, but can still be invoked explicitly via `$skill`.
Defaults to true.

View File

@@ -1,226 +0,0 @@
#!/usr/bin/env python3
"""
OpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.
Usage:
generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]
"""
import argparse
import re
import sys
from pathlib import Path
ACRONYMS = {
"GH",
"MCP",
"API",
"CI",
"CLI",
"LLM",
"PDF",
"PR",
"UI",
"URL",
"SQL",
}
BRANDS = {
"openai": "OpenAI",
"openapi": "OpenAPI",
"github": "GitHub",
"pagerduty": "PagerDuty",
"datadog": "DataDog",
"sqlite": "SQLite",
"fastapi": "FastAPI",
}
SMALL_WORDS = {"and", "or", "to", "up", "with"}
ALLOWED_INTERFACE_KEYS = {
"display_name",
"short_description",
"icon_small",
"icon_large",
"brand_color",
"default_prompt",
}
def yaml_quote(value):
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
return f'"{escaped}"'
def format_display_name(skill_name):
words = [word for word in skill_name.split("-") if word]
formatted = []
for index, word in enumerate(words):
lower = word.lower()
upper = word.upper()
if upper in ACRONYMS:
formatted.append(upper)
continue
if lower in BRANDS:
formatted.append(BRANDS[lower])
continue
if index > 0 and lower in SMALL_WORDS:
formatted.append(lower)
continue
formatted.append(word.capitalize())
return " ".join(formatted)
def generate_short_description(display_name):
description = f"Help with {display_name} tasks"
if len(description) < 25:
description = f"Help with {display_name} tasks and workflows"
if len(description) < 25:
description = f"Help with {display_name} tasks with guidance"
if len(description) > 64:
description = f"Help with {display_name}"
if len(description) > 64:
description = f"{display_name} helper"
if len(description) > 64:
description = f"{display_name} tools"
if len(description) > 64:
suffix = " helper"
max_name_length = 64 - len(suffix)
trimmed = display_name[:max_name_length].rstrip()
description = f"{trimmed}{suffix}"
if len(description) > 64:
description = description[:64].rstrip()
if len(description) < 25:
description = f"{description} workflows"
if len(description) > 64:
description = description[:64].rstrip()
return description
def read_frontmatter_name(skill_dir):
skill_md = Path(skill_dir) / "SKILL.md"
if not skill_md.exists():
print(f"[ERROR] SKILL.md not found in {skill_dir}")
return None
content = skill_md.read_text()
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not match:
print("[ERROR] Invalid SKILL.md frontmatter format.")
return None
frontmatter_text = match.group(1)
import yaml
try:
frontmatter = yaml.safe_load(frontmatter_text)
except yaml.YAMLError as exc:
print(f"[ERROR] Invalid YAML frontmatter: {exc}")
return None
if not isinstance(frontmatter, dict):
print("[ERROR] Frontmatter must be a YAML dictionary.")
return None
name = frontmatter.get("name", "")
if not isinstance(name, str) or not name.strip():
print("[ERROR] Frontmatter 'name' is missing or invalid.")
return None
return name.strip()
def parse_interface_overrides(raw_overrides):
overrides = {}
optional_order = []
for item in raw_overrides:
if "=" not in item:
print(f"[ERROR] Invalid interface override '{item}'. Use key=value.")
return None, None
key, value = item.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
print(f"[ERROR] Invalid interface override '{item}'. Key is empty.")
return None, None
if key not in ALLOWED_INTERFACE_KEYS:
allowed = ", ".join(sorted(ALLOWED_INTERFACE_KEYS))
print(f"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}")
return None, None
overrides[key] = value
if key not in ("display_name", "short_description") and key not in optional_order:
optional_order.append(key)
return overrides, optional_order
def write_openai_yaml(skill_dir, skill_name, raw_overrides):
overrides, optional_order = parse_interface_overrides(raw_overrides)
if overrides is None:
return None
display_name = overrides.get("display_name") or format_display_name(skill_name)
short_description = overrides.get("short_description") or generate_short_description(display_name)
if not (25 <= len(short_description) <= 64):
print(
"[ERROR] short_description must be 25-64 characters "
f"(got {len(short_description)})."
)
return None
interface_lines = [
"interface:",
f" display_name: {yaml_quote(display_name)}",
f" short_description: {yaml_quote(short_description)}",
]
for key in optional_order:
value = overrides.get(key)
if value is not None:
interface_lines.append(f" {key}: {yaml_quote(value)}")
agents_dir = Path(skill_dir) / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
output_path = agents_dir / "openai.yaml"
output_path.write_text("\n".join(interface_lines) + "\n")
print(f"[OK] Created agents/openai.yaml")
return output_path
def main():
parser = argparse.ArgumentParser(
description="Create agents/openai.yaml for a skill directory.",
)
parser.add_argument("skill_dir", help="Path to the skill directory")
parser.add_argument(
"--name",
help="Skill name override (defaults to SKILL.md frontmatter)",
)
parser.add_argument(
"--interface",
action="append",
default=[],
help="Interface override in key=value format (repeatable)",
)
args = parser.parse_args()
skill_dir = Path(args.skill_dir).resolve()
if not skill_dir.exists():
print(f"[ERROR] Skill directory not found: {skill_dir}")
sys.exit(1)
if not skill_dir.is_dir():
print(f"[ERROR] Path is not a directory: {skill_dir}")
sys.exit(1)
skill_name = args.name or read_frontmatter_name(skill_dir)
if not skill_name:
sys.exit(1)
result = write_openai_yaml(skill_dir, skill_name, args.interface)
if result:
sys.exit(0)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -3,14 +3,13 @@
Skill Initializer - Creates a new skill from template
Usage:
init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples] [--interface key=value]
init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples]
Examples:
init_skill.py my-new-skill --path skills/public
init_skill.py my-new-skill --path skills/public --resources scripts,references
init_skill.py my-api-helper --path skills/private --resources scripts --examples
init_skill.py custom-skill --path /custom/location
init_skill.py my-skill --path skills/public --interface short_description="Short UI label"
"""
import argparse
@@ -18,8 +17,6 @@ import re
import sys
from pathlib import Path
from generate_openai_yaml import write_openai_yaml
MAX_SKILL_NAME_LENGTH = 64
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
@@ -255,7 +252,7 @@ def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_
print("[OK] Created assets/")
def init_skill(skill_name, path, resources, include_examples, interface_overrides):
def init_skill(skill_name, path, resources, include_examples):
"""
Initialize a new skill directory with template SKILL.md.
@@ -296,15 +293,6 @@ def init_skill(skill_name, path, resources, include_examples, interface_override
print(f"[ERROR] Error creating SKILL.md: {e}")
return None
# Create agents/openai.yaml
try:
result = write_openai_yaml(skill_dir, skill_name, interface_overrides)
if not result:
return None
except Exception as e:
print(f"[ERROR] Error creating agents/openai.yaml: {e}")
return None
# Create resource directories if requested
if resources:
try:
@@ -324,8 +312,7 @@ def init_skill(skill_name, path, resources, include_examples, interface_override
print("2. Add resources to scripts/, references/, and assets/ as needed")
else:
print("2. Create resource directories only if needed (scripts/, references/, assets/)")
print("3. Update agents/openai.yaml if the UI metadata should differ")
print("4. Run the validator when ready to check the skill structure")
print("3. Run the validator when ready to check the skill structure")
return skill_dir
@@ -346,12 +333,6 @@ def main():
action="store_true",
help="Create example files inside the selected resource directories",
)
parser.add_argument(
"--interface",
action="append",
default=[],
help="Interface override in key=value format (repeatable)",
)
args = parser.parse_args()
raw_skill_name = args.skill_name
@@ -385,7 +366,7 @@ def main():
print(" Resources: none (create as needed)")
print()
result = init_skill(skill_name, path, resources, args.examples, args.interface)
result = init_skill(skill_name, path, resources, args.examples)
if result:
sys.exit(0)

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Skill Packager - Creates a distributable .skill file of a skill folder
Usage:
python utils/package_skill.py <path/to/skill-folder> [output-directory]
Example:
python utils/package_skill.py skills/public/my-skill
python utils/package_skill.py skills/public/my-skill ./dist
"""
import sys
import zipfile
from pathlib import Path
from quick_validate import validate_skill
def package_skill(skill_path, output_dir=None):
"""
Package a skill folder into a .skill file.
Args:
skill_path: Path to the skill folder
output_dir: Optional output directory for the .skill file (defaults to current directory)
Returns:
Path to the created .skill file, or None if error
"""
skill_path = Path(skill_path).resolve()
# Validate skill folder exists
if not skill_path.exists():
print(f"[ERROR] Skill folder not found: {skill_path}")
return None
if not skill_path.is_dir():
print(f"[ERROR] Path is not a directory: {skill_path}")
return None
# Validate SKILL.md exists
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
print(f"[ERROR] SKILL.md not found in {skill_path}")
return None
# Run validation before packaging
print("Validating skill...")
valid, message = validate_skill(skill_path)
if not valid:
print(f"[ERROR] Validation failed: {message}")
print(" Please fix the validation errors before packaging.")
return None
print(f"[OK] {message}\n")
# Determine output location
skill_name = skill_path.name
if output_dir:
output_path = Path(output_dir).resolve()
output_path.mkdir(parents=True, exist_ok=True)
else:
output_path = Path.cwd()
skill_filename = output_path / f"{skill_name}.skill"
# Create the .skill file (zip format)
try:
with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
# Walk through the skill directory
for file_path in skill_path.rglob("*"):
if file_path.is_file():
# Calculate the relative path within the zip
arcname = file_path.relative_to(skill_path.parent)
zipf.write(file_path, arcname)
print(f" Added: {arcname}")
print(f"\n[OK] Successfully packaged skill to: {skill_filename}")
return skill_filename
except Exception as e:
print(f"[ERROR] Error creating .skill file: {e}")
return None
def main():
if len(sys.argv) < 2:
print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]")
print("\nExample:")
print(" python utils/package_skill.py skills/public/my-skill")
print(" python utils/package_skill.py skills/public/my-skill ./dist")
sys.exit(1)
skill_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
print(f"Packaging skill: {skill_path}")
if output_dir:
print(f" Output directory: {output_dir}")
print()
result = package_skill(skill_path, output_dir)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -7,10 +7,10 @@ metadata:
# Skill Installer
Helps install skills. By default these are from https://github.com/openai/skills/tree/main/skills/.curated, but users can also provide other locations. Experimental skills live in https://github.com/openai/skills/tree/main/skills/.experimental and can be installed the same way.
Helps install skills. By default these are from https://github.com/openai/skills/tree/main/skills/.curated, but users can also provide other locations.
Use the helper scripts based on the task:
- List skills when the user asks what is available, or if the user uses this skill without specifying what to do. Default listing is `.curated`, but you can pass `--path skills/.experimental` when they ask about experimental skills.
- List curated skills when the user asks what is available, or if the user uses this skill without specifying what to do.
- Install from the curated list when the user provides a skill name.
- Install from another repo when the user provides a GitHub repo/path (including private repos).
@@ -18,7 +18,7 @@ Install skills with the helper scripts.
## Communication
When listing skills, output approximately as follows, depending on the context of the user's request. If they ask about experimental skills, list from `.experimental` instead of `.curated` and label the source accordingly:
When listing curated skills, output approximately as follows, depending on the context of the user's request:
"""
Skills from {repo}:
1. skill-1
@@ -33,12 +33,10 @@ After installing a skill, tell the user: "Restart Codex to pick up new skills."
All of these scripts use network, so when running in the sandbox, request escalation when running them.
- `scripts/list-skills.py` (prints skills list with installed annotations)
- `scripts/list-skills.py --format json`
- Example (experimental list): `scripts/list-skills.py --path skills/.experimental`
- `scripts/list-curated-skills.py` (prints curated list with installed annotations)
- `scripts/list-curated-skills.py --format json`
- `scripts/install-skill-from-github.py --repo <owner>/<repo> --path <path/to/skill> [<path/to/skill> ...]`
- `scripts/install-skill-from-github.py --url https://github.com/<owner>/<repo>/tree/<ref>/<path>`
- Example (experimental skill): `scripts/install-skill-from-github.py --repo openai/skills --path skills/.experimental/<skill-name>`
## Behavior and Options

View File

@@ -1,5 +0,0 @@
interface:
display_name: "Skill Installer"
short_description: "Install curated skills from openai/skills or other repos"
icon_small: "./assets/skill-installer-small.svg"
icon_large: "./assets/skill-installer.png"

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill="#0D0D0D" d="M2.145 3.959a2.033 2.033 0 0 1 2.022-1.824h5.966c.551 0 .997 0 1.357.029.367.03.692.093.993.246l.174.098c.397.243.72.593.932 1.01l.053.114c.116.269.168.557.194.878.03.36.03.805.03 1.357v4.3a2.365 2.365 0 0 1-2.366 2.365h-1.312a2.198 2.198 0 0 1-4.377 0H4.167A2.032 2.032 0 0 1 2.135 10.5V9.333l.004-.088A.865.865 0 0 1 3 8.468l.116-.006A1.135 1.135 0 0 0 3 6.199a.865.865 0 0 1-.865-.864V4.167l.01-.208Zm1.054 1.186a2.198 2.198 0 0 1 0 4.376v.98c0 .534.433.967.968.967H6l.089.004a.866.866 0 0 1 .776.861 1.135 1.135 0 0 0 2.27 0c0-.478.387-.865.865-.865h1.5c.719 0 1.301-.583 1.301-1.301v-4.3c0-.57 0-.964-.025-1.27a1.933 1.933 0 0 0-.09-.493L12.642 4a1.47 1.47 0 0 0-.541-.585l-.102-.056c-.126-.065-.295-.11-.596-.135a17.31 17.31 0 0 0-1.27-.025H4.167a.968.968 0 0 0-.968.968v.978Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""List skills from a GitHub repo path."""
"""List curated skills from a GitHub repo path."""
from __future__ import annotations
@@ -47,32 +47,28 @@ def _installed_skills() -> set[str]:
return entries
def _list_skills(repo: str, path: str, ref: str) -> list[str]:
def _list_curated(repo: str, path: str, ref: str) -> list[str]:
api_url = github_api_contents_url(repo, path, ref)
try:
payload = _request(api_url)
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise ListError(
"Skills path not found: "
"Curated skills path not found: "
f"https://github.com/{repo}/tree/{ref}/{path}"
) from exc
raise ListError(f"Failed to fetch skills: HTTP {exc.code}") from exc
raise ListError(f"Failed to fetch curated skills: HTTP {exc.code}") from exc
data = json.loads(payload.decode("utf-8"))
if not isinstance(data, list):
raise ListError("Unexpected skills listing response.")
raise ListError("Unexpected curated listing response.")
skills = [item["name"] for item in data if item.get("type") == "dir"]
return sorted(skills)
def _parse_args(argv: list[str]) -> Args:
parser = argparse.ArgumentParser(description="List skills.")
parser = argparse.ArgumentParser(description="List curated skills.")
parser.add_argument("--repo", default=DEFAULT_REPO)
parser.add_argument(
"--path",
default=DEFAULT_PATH,
help="Repo path to list (default: skills/.curated)",
)
parser.add_argument("--path", default=DEFAULT_PATH)
parser.add_argument("--ref", default=DEFAULT_REF)
parser.add_argument(
"--format",
@@ -86,7 +82,7 @@ def _parse_args(argv: list[str]) -> Args:
def main(argv: list[str]) -> int:
args = _parse_args(argv)
try:
skills = _list_skills(args.repo, args.path, args.ref)
skills = _list_curated(args.repo, args.path, args.ref)
installed = _installed_skills()
if args.format == "json":
payload = [

View File

@@ -3,17 +3,25 @@
config,
pkgs,
lib,
osConfig,
osConfig ? null,
preferredShell ? "zsh",
userEmail ? "danilo.reyes.251@proton.me",
...
}:
let
inherit (pkgs) fd fzf;
inherit (inputs.jawz-scripts.packages.x86_64-linux) pokemon-colorscripts;
shellType = osConfig.my.shell.type;
commonInit = ''
${pokemon-colorscripts}/bin/pokemon-colorscripts -r --no-title
export command_timeout=60
'';
pokemon-colorscripts = lib.attrByPath [
pkgs.system
"pokemon-colorscripts"
] null inputs.jawz-scripts.packages;
shellType = if osConfig == null then preferredShell else osConfig.my.shell.type;
commonInit =
lib.optionalString (pokemon-colorscripts != null) ''
${pokemon-colorscripts}/bin/pokemon-colorscripts -r --no-title
''
+ ''
export command_timeout=60
'';
commonAliases = inputs.self.lib.commonAliases // {
open-gallery = ''
cd /srv/pool/scrapping/JawZ/gallery-dl &&
@@ -21,7 +29,7 @@ let
};
in
{
imports = [ ./jawz-home.nix ];
imports = lib.optionals (osConfig != null) [ ./jawz-home.nix ];
home.stateVersion = "23.05";
programs = {
direnv = {
@@ -36,7 +44,7 @@ in
pull.rebase = true;
init.defaultBranch = "main";
user = {
inherit (osConfig.my) email;
email = if osConfig == null then userEmail else osConfig.my.email;
name = "Danilo Reyes";
};
};
@@ -60,7 +68,7 @@ in
};
zsh = lib.mkIf (shellType == "zsh") {
enable = true;
dotDir = ".config/zsh";
dotDir = "${config.xdg.configHome}/zsh";
shellAliases = commonAliases;
initContent = commonInit;
history = {
@@ -73,7 +81,7 @@ in
};
xdg = {
enable = true;
userDirs = {
userDirs = lib.mkIf pkgs.stdenv.isLinux {
enable = true;
createDirectories = false;
desktop = "${config.home.homeDirectory}";

View File

@@ -7,9 +7,11 @@
## Repository Overview
- Architecture: Flake-based repo using `flake-parts` with inputs for pkgs (stable/unstable), stylix, home-manager, sops-nix, and service overlays. Common modules are composed through `parts/core.nix` and `parts/hosts.nix`.
- Host outputs: `parts/hosts.nix` publishes NixOS hosts through `flake.nixosConfigurations` and standalone Home Manager hosts through `flake.homeConfigurations`.
- Module auto-import: `modules/modules.nix` auto-imports legacy flat modules under `modules/apps`, `modules/dev`, `modules/scripts`, `modules/servers`, `modules/services`, `modules/shell`, `modules/websites`, and `modules/network`, excluding `librewolf.nix`, and also discovers nested `nixos.nix` files under those trees. `config/base.nix` registers `modules/home-manager.nix` as a Home Manager shared module, which discovers nested `home.nix` files under `modules/` for every Home Manager user. Factories live in `modules/factories/` (`mkserver`, `mkscript`), and shared options are in `modules/nix` and `modules/users`.
- Home Manager helper layer: Common Home Manager wrapper logic belongs in `parts/core.nix` helpers under `inputs.self.lib` when it is repeated across multiple `home.nix` modules. Current helpers include split-loader support plus `hmModule`, `hmShellType`, and `hmOnlyUser` for shared enablement and shell-selection patterns.
- Hosts and toggles: Host definitions live in `hosts/<name>/configuration.nix` with host-specific toggles in `hosts/<name>/toggles.nix`. The `my` namespace carries toggles for apps/dev/scripts/services/shell, feature flags like `enableProxy` and `enableContainers`, and per-host `interfaces` and `ips` maps.
- Standalone Home Manager hosts: Home-only hosts may live under `hosts/<name>/home.nix` with `hosts/<name>/toggles.nix`, and should only enable modules that have a `home.nix` surface or are otherwise known to be Home Manager-compatible on that platform.
- Port assignment: Service ports should live with the service module when the port is intrinsic to a server definition under `modules/servers/`. Miscellaneous or host-specific ports that do not belong to a server module should be centralized in `my.ports` in `modules/modules.nix` and referenced via `config.my.ports.*` (use `toString config.my.ports.*` where a string is required).
- Main server and proxies: `my.mainServer` selects the host that should serve traffic by default (default `vps`). Reverse proxies use helpers in `parts/core.nix` (`proxy`, `proxyReverse`, `proxyReverseFix`, `proxyReversePrivate`) and pick IPs from `my.ips` plus the hostName/ip set by `mkserver` options. Nginx defaults to `proxyReverse` for any server with `enableProxy = true` unless `useDefaultProxy = false` or the server is listed in the Fix/Private proxy lists.
- Secure hosts and secrets: `my.secureHost` gates SOPS secrets. Secure hosts load secrets from `secrets/*.yaml` and wireguard definitions; non-secure hosts (e.g., `hosts/emacs`) skip secret-dependent services. Default SOPS file is `secrets/secrets.yaml` via `config/base.nix`. Proxy-only services that need private certificates must still define their cert secrets when `enableProxy = true`.
@@ -53,6 +55,7 @@ config.services = {
- Home module helper rule: Extract repeated Home Manager wrapper code into `parts/core.nix` once at least a few modules share the same structure. Keep only feature-specific package lists, files, and service settings inside each `home.nix`.
- User config split: Personal configs may also split across `config/<name>.nix` for NixOS-side account/secrets state and `config/<name>-home.nix` for Home Manager-only state imported from the user home module.
- Active hosts: `workstation`, `server`, `miniserver`, `galaxy`, `emacs`, `vps`. Host roles and secure status are defined in `hosts/<name>/configuration.nix` and toggles in `hosts/<name>/toggles.nix`.
- Standalone Home Manager hosts: `mac` is a planned `aarch64-darwin` Home Manager target under `flake.homeConfigurations`, intended to reuse workstation-style dev and shell modules without Linux GUI app modules.
## Precedence and Conflict Resolution
- Precedence: This constitution is authoritative for AI. Human docs must be updated to match. If conflicts are found, align human docs to the constitution and log the resolution in `specs/001-ai-docs/research.md`.

View File

@@ -38,8 +38,10 @@
- Usage rule: Use these helpers for repeated wrapper logic; keep feature-specific package sets and config payloads in local `common.nix` or `home.nix` files.
## Hosts and Roles
- Configs: `hosts/<name>/configuration.nix` with toggles in `hosts/<name>/toggles.nix`.
- Active hosts: `workstation`, `server`, `miniserver`, `galaxy`, `emacs`, `vps`.
- NixOS configs: `hosts/<name>/configuration.nix` with toggles in `hosts/<name>/toggles.nix`.
- Standalone Home Manager configs: `hosts/<name>/home.nix` with optional toggles in `hosts/<name>/toggles.nix`.
- Active NixOS hosts: `workstation`, `server`, `miniserver`, `galaxy`, `emacs`, `vps`.
- Active Home Manager hosts: `mac`.
- Roles:
- workstation: developer desktop; provides build power for distributed builds.
- server: primary services host; runs most services and WireGuard targets.
@@ -47,8 +49,15 @@
- galaxy: small server variant using nixpkgs-small.
- emacs: VM profile, `my.secureHost = false` for secret-free usage.
- vps: Linode VPS image target, secure host with enrollment-based secrets.
- mac: planned standalone `aarch64-darwin` Home Manager target for dev and shell tooling.
- Network maps: `my.ips` and `my.interfaces` declared in `modules/modules.nix`; host toggles may override.
## Home Configuration Outputs
- NixOS outputs: `flake.nixosConfigurations.<name>` for Linux system hosts.
- Home Manager outputs: `flake.homeConfigurations.<name>` for standalone HM hosts.
- Current standalone HM example: `homeConfigurations.mac`.
- Validation note: Darwin home configurations can be evaluated from Linux, but their activation packages cannot be realized locally on a Linux builder.
## Proxy, Firewall, and Networking
- Proxy enablement: `my.enableProxy` toggles Nginx reverse proxy; assertions require at least one `my.servers.*.enableProxy` when enabled.
- Proxy helpers: use `parts/core.nix` helpers (`proxy`, `proxyReverse`, `proxyReverseFix` for header preservation, `proxyReversePrivate` for mutual TLS). `mkserver` supplies `host`, `ip`, `url`, `enableProxy`, and `useDefaultProxy`.

11
hosts/mac/home.nix Normal file
View File

@@ -0,0 +1,11 @@
{
inputs,
...
}:
{
home = {
username = "cdreyes";
homeDirectory = "/Users/cdreyes";
};
my = import ./toggles.nix { inherit inputs; };
}

17
hosts/mac/toggles.nix Normal file
View File

@@ -0,0 +1,17 @@
{ inputs }:
let
inherit (inputs.self.lib) mkEnabled enableList;
in
{
emacs.enable = true;
dev = enableList mkEnabled [
"nix"
"python"
"sh"
];
shell = enableList mkEnabled [
"exercism"
"multimedia"
"tools"
];
}

View File

@@ -7,7 +7,7 @@
...
}:
let
shellType = inputs.self.lib.hmShellType config osConfig;
shellType = inputs.self.lib.hmShellType osConfig "zsh";
hm = inputs.self.lib.hmModule {
inherit
config
@@ -39,7 +39,7 @@ in
"doom/templates/programming.org".source = ../../../dotfiles/doom/templates/programming.org;
};
services = {
lorri.enable = true;
lorri.enable = pkgs.stdenv.isLinux;
emacs = {
enable = true;
defaultEditor = true;

View File

@@ -7,7 +7,7 @@
...
}:
let
shellType = inputs.self.lib.hmShellType config osConfig;
shellType = inputs.self.lib.hmShellType osConfig "zsh";
hm = inputs.self.lib.hmModule {
inherit
config

View File

@@ -1,43 +0,0 @@
{
config,
inputs,
lib,
pkgs,
...
}:
let
packages = builtins.attrValues {
inherit (pkgs)
shellcheck # Shell script linter
shfmt # Shell parser and formatter
;
# LSP for Bash and sh
inherit (pkgs.nodePackages) bash-language-server;
};
in
{
options = {
my.dev.sh = {
enable = lib.mkEnableOption "Install shell scripting tools globally";
users = lib.mkOption {
type = inputs.self.lib.usersOptionType lib;
default = config.my.toggleUsers.dev;
description = "Users to install shell scripting packages for";
};
};
devShells.sh = lib.mkOption {
type = lib.types.package;
default = pkgs.mkShell {
inherit packages;
name = "sh-dev-shell";
shellHook = ''
echo "💻 Shell scripting dev environment"
'';
};
description = "Shell scripting dev shell";
};
};
config = lib.mkIf config.my.dev.sh.enable {
users.users = inputs.self.lib.mkUserAttrs lib config.my.dev.sh.users { inherit packages; };
};
}

20
modules/dev/sh/common.nix Normal file
View File

@@ -0,0 +1,20 @@
{ pkgs }:
let
packages = builtins.attrValues {
inherit (pkgs)
shellcheck
shfmt
;
inherit (pkgs.nodePackages) bash-language-server;
};
in
{
inherit packages;
devShell = pkgs.mkShell {
inherit packages;
name = "sh-dev-shell";
shellHook = ''
echo "💻 Shell scripting dev environment"
'';
};
}

34
modules/dev/sh/home.nix Normal file
View File

@@ -0,0 +1,34 @@
{
config,
inputs,
lib,
osConfig ? null,
pkgs,
...
}:
let
hm = inputs.self.lib.hmModule {
inherit
config
inputs
osConfig
;
optionPath = [
"dev"
"sh"
];
};
cfg = config.my.dev.sh;
sh = import ./common.nix { inherit pkgs; };
in
{
options.my.dev.sh.enable = lib.mkEnableOption "Install shell scripting tools globally";
config = lib.mkMerge [
{
my.dev.sh.enable = lib.mkDefault hm.enabledByDefault;
}
(lib.mkIf cfg.enable {
home.packages = sh.packages;
})
];
}

27
modules/dev/sh/nixos.nix Normal file
View File

@@ -0,0 +1,27 @@
{
config,
inputs,
lib,
pkgs,
...
}:
let
sh = import ./common.nix { inherit pkgs; };
in
{
options = {
my.dev.sh = {
enable = lib.mkEnableOption "Install shell scripting tools globally";
users = lib.mkOption {
type = inputs.self.lib.usersOptionType lib;
default = config.my.toggleUsers.dev;
description = "Users to install shell scripting packages for";
};
};
devShells.sh = lib.mkOption {
type = lib.types.package;
default = sh.devShell;
description = "Shell scripting dev shell";
};
};
}

View File

@@ -7,7 +7,7 @@
...
}:
let
shellType = inputs.self.lib.hmShellType config osConfig;
shellType = inputs.self.lib.hmShellType osConfig "zsh";
enabled =
inputs.self.lib.hmOnlyUser config osConfig "jawz"
&& (osConfig.my.units.download.enable || osConfig.my.units.downloadManga.enable);

View File

@@ -1,29 +0,0 @@
{
config,
inputs,
lib,
pkgs,
...
}:
{
options.my.shell.exercism = {
enable = lib.mkEnableOption "Exercism coding practice platform";
users = lib.mkOption {
type = inputs.self.lib.usersOptionType lib;
default = config.my.toggleUsers.shell;
description = "Users to install Exercism for";
};
};
config = lib.mkIf config.my.shell.exercism.enable {
users.users =
let
packages = builtins.attrValues {
inherit (pkgs)
exercism # learn to code
bats # testing system, required by Exercism
;
};
in
inputs.self.lib.mkUserPackages lib config.my.shell.exercism.users packages;
};
}

View File

@@ -0,0 +1,9 @@
{ pkgs }:
{
packages = builtins.attrValues {
inherit (pkgs)
exercism
bats
;
};
}

View File

@@ -0,0 +1,34 @@
{
config,
inputs,
lib,
osConfig ? null,
pkgs,
...
}:
let
hm = inputs.self.lib.hmModule {
inherit
config
inputs
osConfig
;
optionPath = [
"shell"
"exercism"
];
};
cfg = config.my.shell.exercism;
exercism = import ./common.nix { inherit pkgs; };
in
{
options.my.shell.exercism.enable = lib.mkEnableOption "Exercism coding practice platform";
config = lib.mkMerge [
{
my.shell.exercism.enable = lib.mkDefault hm.enabledByDefault;
}
(lib.mkIf cfg.enable {
home.packages = exercism.packages;
})
];
}

View File

@@ -0,0 +1,16 @@
{
config,
inputs,
lib,
...
}:
{
options.my.shell.exercism = {
enable = lib.mkEnableOption "Exercism coding practice platform";
users = lib.mkOption {
type = inputs.self.lib.usersOptionType lib;
default = config.my.toggleUsers.shell;
description = "Users to install Exercism for";
};
};
}

View File

@@ -7,7 +7,7 @@
...
}:
let
shellType = inputs.self.lib.hmShellType config osConfig;
shellType = inputs.self.lib.hmShellType osConfig "zsh";
hm = inputs.self.lib.hmModule {
inherit
config

View File

@@ -7,7 +7,7 @@
...
}:
let
shellType = inputs.self.lib.hmShellType config osConfig;
shellType = inputs.self.lib.hmShellType osConfig "zsh";
hm = inputs.self.lib.hmModule {
inherit
config

View File

@@ -12,6 +12,10 @@ in
systems = [ system ];
flake = {
lib = {
commonOverlays = mkpkgs: inputs: [
(import ../config/overlay.nix { inherit mkpkgs inputs; })
inputs.doom-emacs.overlays.default
];
commonModules = name: [
../hosts/${name}/configuration.nix
inputs.nur.modules.nixos.default
@@ -19,10 +23,7 @@ in
inputs.stylix.nixosModules.stylix
inputs.nixtendo-switch.nixosModules.nixtendo-switch
{
nixpkgs.overlays = [
(import ../config/overlay.nix { inherit mkpkgs inputs; })
inputs.doom-emacs.overlays.default
];
nixpkgs.overlays = inputs.self.lib.commonOverlays mkpkgs inputs;
}
{
nix.registry = {
@@ -44,6 +45,41 @@ in
outputs = inputs.self;
};
};
createHomeConfig =
{
name,
system,
extraModules ? [ ],
extraSpecialArgs ? { },
}:
let
pkgs = import inputs.nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = inputs.self.lib.commonOverlays mkpkgs inputs;
};
in
inputs.home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [
../modules/home-manager.nix
../config/home-manager.nix
../hosts/${name}/home.nix
{
nixpkgs.config.allowUnfree = true;
programs.home-manager.enable = true;
}
]
++ extraModules;
extraSpecialArgs = {
inherit inputs;
outputs = inputs.self;
osConfig = null;
preferredShell = "zsh";
userEmail = "danilo.reyes.251@proton.me";
}
// extraSpecialArgs;
};
langList =
builtins.readDir ../modules/dev
|> builtins.attrNames
@@ -301,11 +337,7 @@ in
inherit osCfg enabledByDefault;
};
hmShellType =
config: osConfig:
if osConfig == null then
if config.programs.bash.enable then "bash" else "zsh"
else
osConfig.my.shell.type;
osConfig: preferredShell: if osConfig == null then preferredShell else osConfig.my.shell.type;
hmOnlyUser =
config: osConfig: user:
osConfig != null && config.home.username == user;

View File

@@ -1,11 +1,17 @@
{ inputs, ... }:
{
flake.nixosConfigurations = {
workstation = inputs.self.lib.createConfig "workstation" inputs.nixpkgs;
miniserver = inputs.self.lib.createConfig "miniserver" inputs.nixpkgs-small;
server = inputs.self.lib.createConfig "server" inputs.nixpkgs-small;
galaxy = inputs.self.lib.createConfig "galaxy" inputs.nixpkgs-small;
emacs = inputs.self.lib.createConfig "emacs" inputs.nixpkgs;
vps = inputs.self.lib.createConfig "vps" inputs.nixpkgs-small;
flake = {
nixosConfigurations = {
workstation = inputs.self.lib.createConfig "workstation" inputs.nixpkgs;
miniserver = inputs.self.lib.createConfig "miniserver" inputs.nixpkgs-small;
server = inputs.self.lib.createConfig "server" inputs.nixpkgs-small;
galaxy = inputs.self.lib.createConfig "galaxy" inputs.nixpkgs-small;
emacs = inputs.self.lib.createConfig "emacs" inputs.nixpkgs;
vps = inputs.self.lib.createConfig "vps" inputs.nixpkgs-small;
};
homeConfigurations.mac = inputs.self.lib.createHomeConfig {
name = "mac";
system = "aarch64-darwin";
};
};
}

View File

@@ -44,3 +44,8 @@
- **Decision**: Keep split feature modules, but move repeated Home Manager wrapper logic into shared helpers in `parts/core.nix` exposed via `inputs.self.lib`.
- **Rationale**: Reduces boilerplate in `home.nix` modules without losing the explicit split between NixOS-owned and Home Manager-owned behavior.
- **Alternatives considered**: (a) Leave repeated wrapper code in every `home.nix` (rejected: noisy and brittle); (b) hide full feature behavior behind one generic abstraction (rejected: makes modules harder to read and reason about).
## Decision 10 (2026-03-16): Standalone Home Manager host for macOS
- **Decision**: Add a standalone `homeConfigurations.mac` target backed by `hosts/mac/home.nix` and `hosts/mac/toggles.nix`, reusing workstation-style dev and shell modules while excluding Linux GUI app modules.
- **Rationale**: Lets the repo model an upcoming macOS machine without forcing NixOS host semantics onto a Home Manager-only system.
- **Alternatives considered**: (a) Wait for the physical Mac and keep no target in the flake (rejected: delays validation and reuse work); (b) model the Mac as a fake NixOS host (rejected: does not match the intended deployment model).