mirror of
https://github.com/gnekt/My-Brain-Is-Full-Crew.git
synced 2026-08-26 02:04:43 +00:00
- generate-skills.py now respects SKILLS_DIR env var - launchme.sh and updateme.sh generate into mktemp -d, copy to vault, then clean up — no more untracked skills/ left in the repo - Warn the user when python3 is missing so Cowork/Desktop users know why .claude/skills/ is absent
125 lines
3.6 KiB
Python
125 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate skills/ from agents/ — single source of truth stays in agents/*.md.
|
|
|
|
Each agent becomes a skill at skills/{name}/SKILL.md with:
|
|
- tools: → allowed-tools:
|
|
- disallowedTools: → (dropped if allowed-tools is set)
|
|
- model: → model:
|
|
- description: → description:
|
|
+ context: fork → run in isolated subagent (like a real subagent)
|
|
|
|
Usage: python3 scripts/generate-skills.py
|
|
"""
|
|
|
|
import os, re, sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
AGENTS_DIR = os.path.join(ROOT, "agents")
|
|
SKILLS_DIR = os.environ.get("SKILLS_DIR", os.path.join(ROOT, "skills"))
|
|
|
|
|
|
def parse_agent(path: str):
|
|
"""Return (frontmatter_dict, body_str) from an agent .md file."""
|
|
with open(path, encoding="utf-8") as f:
|
|
text = f.read()
|
|
|
|
# Split on the two --- delimiters
|
|
m = re.match(r"^---\n(.*?\n)---\n(.*)", text, re.DOTALL)
|
|
if not m:
|
|
return None, text
|
|
|
|
raw_fm = m.group(1)
|
|
body = m.group(2)
|
|
|
|
# Simple YAML-ish parser for the flat fields we care about
|
|
fm = {}
|
|
current_key = None
|
|
current_val = ""
|
|
|
|
for line in raw_fm.split("\n"):
|
|
# New key
|
|
key_match = re.match(r"^(\w[\w-]*):\s*(.*)", line)
|
|
if key_match:
|
|
if current_key:
|
|
fm[current_key] = current_val.strip()
|
|
current_key = key_match.group(1)
|
|
val = key_match.group(2)
|
|
if val == ">":
|
|
current_val = ""
|
|
else:
|
|
current_val = val
|
|
elif current_key and line.startswith(" "):
|
|
current_val += " " + line.strip()
|
|
|
|
if current_key:
|
|
fm[current_key] = current_val.strip()
|
|
|
|
return fm, body
|
|
|
|
|
|
def build_skill_frontmatter(fm: dict) -> str:
|
|
"""Convert agent frontmatter to skill frontmatter."""
|
|
lines = ["---"]
|
|
|
|
name = fm.get("name", "unknown")
|
|
lines.append(f"name: {name}")
|
|
|
|
if "description" in fm:
|
|
desc = fm["description"]
|
|
lines.append("description: >")
|
|
# Wrap description at ~90 chars, indent with exactly 2 spaces
|
|
words = desc.split()
|
|
line = ""
|
|
for w in words:
|
|
if line and len(line) + len(w) + 1 > 88:
|
|
lines.append(" " + line)
|
|
line = w
|
|
else:
|
|
line = line + " " + w if line else w
|
|
if line:
|
|
lines.append(" " + line)
|
|
|
|
# Skills only support: name, description, argument-hint, compatibility,
|
|
# disable-model-invocation, license, metadata, user-invocable.
|
|
# No tool restrictions, model override, or fork context.
|
|
|
|
lines.append("---")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
if not os.path.isdir(AGENTS_DIR):
|
|
print(f"Error: {AGENTS_DIR} not found", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
count = 0
|
|
for fname in sorted(os.listdir(AGENTS_DIR)):
|
|
if not fname.endswith(".md"):
|
|
continue
|
|
|
|
agent_path = os.path.join(AGENTS_DIR, fname)
|
|
fm, body = parse_agent(agent_path)
|
|
if fm is None:
|
|
print(f" ! Skipped {fname} (no frontmatter)")
|
|
continue
|
|
|
|
name = fm.get("name", fname.replace(".md", ""))
|
|
skill_dir = os.path.join(SKILLS_DIR, name)
|
|
os.makedirs(skill_dir, exist_ok=True)
|
|
|
|
skill_content = build_skill_frontmatter(fm) + "\n" + body
|
|
skill_path = os.path.join(skill_dir, "SKILL.md")
|
|
|
|
with open(skill_path, "w", encoding="utf-8") as f:
|
|
f.write(skill_content)
|
|
|
|
print(f" ✓ {name}/SKILL.md")
|
|
count += 1
|
|
|
|
print(f"\n Generated {count} skills in skills/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|