Odysseus v1.0
This commit is contained in:
148
scripts/odysseus-skills
Executable file
148
scripts/odysseus-skills
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/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()))
|
||||
Reference in New Issue
Block a user