149 lines
4.5 KiB
Python
Executable File
149 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""odysseus-skills — shell wrapper for AI skill scripts.
|
|
|
|
Skills are `SKILL.md` files under `data/skills/<category>/<name>/`. Each
|
|
captures a reusable how-to the assistant has learned. This CLI lets you
|
|
list, inspect, and prune them from the shell.
|
|
|
|
odysseus-skills list [--category general]
|
|
odysseus-skills show SKILL_NAME
|
|
odysseus-skills categories
|
|
odysseus-skills delete SKILL_NAME
|
|
odysseus-skills export SKILL_NAME --raw # full SKILL.md to stdout
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import sys
|
|
import os, sys
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
|
|
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
|
|
quiet_logs()
|
|
|
|
import argparse, json, logging, os, shutil, sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from services.memory.skills import SkillsManager
|
|
quiet_logs()
|
|
except ModuleNotFoundError as e:
|
|
sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n")
|
|
sys.exit(2)
|
|
|
|
|
|
_DATA_DIR = str(_REPO_ROOT / "data")
|
|
_mgr: SkillsManager | None = None
|
|
|
|
|
|
def _manager() -> SkillsManager:
|
|
global _mgr
|
|
if _mgr is None:
|
|
_mgr = SkillsManager(_DATA_DIR)
|
|
return _mgr
|
|
|
|
|
|
def _summary(skill: dict) -> dict:
|
|
return {
|
|
"name": skill.get("name", ""),
|
|
"category": skill.get("category", "general"),
|
|
"description": (skill.get("description") or "")[:200],
|
|
"status": skill.get("status", ""),
|
|
"uses": skill.get("uses", 0),
|
|
"last_used": skill.get("last_used") or "",
|
|
"tags": skill.get("tags") or [],
|
|
}
|
|
|
|
|
|
def cmd_list(args):
|
|
out = _manager().load_all()
|
|
if args.category:
|
|
out = [s for s in out if (s.get("category") or "general") == args.category]
|
|
out.sort(key=lambda s: (-int(s.get("uses") or 0), s.get("name", "")))
|
|
emit([_summary(s) for s in out[: args.limit]], args)
|
|
|
|
|
|
def cmd_show(args):
|
|
for s in _manager().load_all():
|
|
if s.get("name") == args.name:
|
|
emit(s, args)
|
|
return
|
|
fail(f"no skill named {args.name!r}")
|
|
|
|
|
|
def cmd_categories(args):
|
|
counts: dict[str, int] = {}
|
|
for s in _manager().load_all():
|
|
c = s.get("category") or "general"
|
|
counts[c] = counts.get(c, 0) + 1
|
|
emit([{"category": c, "count": n} for c, n in sorted(counts.items())], args)
|
|
|
|
|
|
def cmd_delete(args):
|
|
# Locate the skill's directory and rm -rf it.
|
|
skills_root = Path(_DATA_DIR) / "skills"
|
|
for s in _manager().load_all():
|
|
if s.get("name") != args.name:
|
|
continue
|
|
cat = s.get("category") or "general"
|
|
path = skills_root / cat / s["name"]
|
|
if path.is_dir():
|
|
shutil.rmtree(path)
|
|
emit({"ok": True, "deleted": s.get("name"), "path": str(path)}, args)
|
|
return
|
|
fail(f"skill record found but directory missing: {path}")
|
|
fail(f"no skill named {args.name!r}")
|
|
|
|
|
|
def cmd_export(args):
|
|
for s in _manager().load_all():
|
|
if s.get("name") != args.name:
|
|
continue
|
|
cat = s.get("category") or "general"
|
|
md_path = Path(_DATA_DIR) / "skills" / cat / args.name / "SKILL.md"
|
|
if not md_path.exists():
|
|
fail(f"SKILL.md missing for {args.name!r}")
|
|
if args.raw:
|
|
sys.stdout.write(md_path.read_text())
|
|
return
|
|
emit({
|
|
"name": args.name,
|
|
"category": cat,
|
|
"path": str(md_path),
|
|
"content": md_path.read_text(),
|
|
}, args)
|
|
return
|
|
fail(f"no skill named {args.name!r}")
|
|
|
|
|
|
def _build_parser():
|
|
common = argparse.ArgumentParser(add_help=False)
|
|
common.add_argument("--pretty", action="store_true")
|
|
p = argparse.ArgumentParser(prog="odysseus-skills", parents=[common])
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
|
|
pl = sub.add_parser("list", parents=[common])
|
|
pl.add_argument("--category")
|
|
pl.add_argument("--limit", type=int, default=100)
|
|
pl.set_defaults(func=cmd_list)
|
|
|
|
psh = sub.add_parser("show", parents=[common])
|
|
psh.add_argument("name")
|
|
psh.set_defaults(func=cmd_show)
|
|
|
|
pc = sub.add_parser("categories", parents=[common])
|
|
pc.set_defaults(func=cmd_categories)
|
|
|
|
pd = sub.add_parser("delete", parents=[common])
|
|
pd.add_argument("name")
|
|
pd.set_defaults(func=cmd_delete)
|
|
|
|
pe = sub.add_parser("export", parents=[common])
|
|
pe.add_argument("name")
|
|
pe.add_argument("--raw", action="store_true", help="write raw SKILL.md to stdout")
|
|
pe.set_defaults(func=cmd_export)
|
|
|
|
return p
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(run(_build_parser()))
|