skills/ carries the SKILL.md files; scripts/import_skills.py runs inside the VM via turnstone's own storage layer (SKILL.md parser + risk scanner) so rows match what the console admin API would write. Server must be restarted after import (stale WAL snapshot).
121 lines
4.0 KiB
Python
121 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Import SKILL.md skill directories into the turnstone DB.
|
|
|
|
Runs INSIDE the VM (needs the turnstone venv), using turnstone's own
|
|
storage layer and SKILL.md parser so rows match what the console admin
|
|
API / in-session `skills` tool would have written (risk scan included):
|
|
|
|
/opt/turnstone/bin/python import_skills.py --dir /tmp/skills
|
|
|
|
The DB (data/turnstone.db on the host) is shared into the guest at
|
|
/var/lib/turnstone/turnstone.db. Idempotent: existing skill names are
|
|
skipped unless --update (which re-imports and appends a version row).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import pathlib
|
|
import uuid
|
|
|
|
from turnstone.core.skill_parser import parse_skill_md
|
|
from turnstone.core.storage._sqlite import SQLiteBackend
|
|
|
|
DB_DEFAULT = "/var/lib/turnstone/turnstone.db"
|
|
|
|
|
|
def next_version(backend: SQLiteBackend, skill_id: str) -> int:
|
|
try:
|
|
rows = backend.list_skill_versions(skill_id)
|
|
return max((int(r["version"]) for r in rows), default=0) + 1
|
|
except Exception: # noqa: BLE001
|
|
return 1
|
|
|
|
|
|
def import_one(
|
|
backend: SQLiteBackend, skill_dir: pathlib.Path, update: bool
|
|
) -> str:
|
|
raw = (skill_dir / "SKILL.md").read_text()
|
|
p = parse_skill_md(raw)
|
|
if p is None or not p.name or not p.description:
|
|
return f"SKIP {skill_dir.name}: unparseable frontmatter"
|
|
if not p.content.strip():
|
|
return f"SKIP {skill_dir.name}: empty body"
|
|
|
|
allowed = p.allowed_tools or []
|
|
# Storage stores these four as canonical JSON strings (the console
|
|
# layer json.dumps them before calling in).
|
|
tags_str = json.dumps(p.tags or [])
|
|
allowed_str = json.dumps(allowed)
|
|
paths_str = json.dumps(p.paths or [])
|
|
arguments_str = json.dumps(p.arguments or [])
|
|
|
|
existing = backend.get_skill_by_name(p.name)
|
|
if existing and not update:
|
|
return f"SKIP {p.name}: already exists"
|
|
|
|
if existing:
|
|
skill_id = existing["template_id"]
|
|
backend.update_prompt_template(
|
|
skill_id,
|
|
description=p.description,
|
|
content=p.content,
|
|
allowed_tools=allowed_str,
|
|
skill_license=p.license or "",
|
|
compatibility=(p.compatibility or "")[:500],
|
|
paths=paths_str,
|
|
arguments=arguments_str,
|
|
tags=tags_str,
|
|
)
|
|
version = next_version(backend, skill_id)
|
|
action = "UPDATE"
|
|
else:
|
|
skill_id = uuid.uuid4().hex
|
|
backend.create_prompt_template(
|
|
template_id=skill_id,
|
|
name=p.name,
|
|
category="general",
|
|
content=p.content,
|
|
created_by="marvin",
|
|
description=p.description,
|
|
tags=tags_str,
|
|
version=str(p.version or "1.0.0"),
|
|
author=p.author or "",
|
|
skill_license=p.license or "",
|
|
compatibility=(p.compatibility or "")[:500],
|
|
token_estimate=len(p.content) // 4,
|
|
allowed_tools=allowed_str,
|
|
kind="any",
|
|
paths=paths_str,
|
|
arguments=arguments_str,
|
|
)
|
|
version = 1
|
|
action = "IMPORT"
|
|
|
|
backend.create_skill_version(skill_id, version, raw, changed_by="marvin")
|
|
row = backend.get_prompt_template(skill_id)
|
|
return f"{action} {p.name} v{version} risk={row.get('risk_level') or 'none'}"
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--dir", required=True, help="directory of skill folders")
|
|
ap.add_argument("--db", default=DB_DEFAULT)
|
|
ap.add_argument("--update", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
backend = SQLiteBackend(args.db, create_tables=False)
|
|
root = pathlib.Path(args.dir)
|
|
dirs = sorted(d for d in root.iterdir() if (d / "SKILL.md").is_file())
|
|
if not dirs:
|
|
raise SystemExit(f"no SKILL.md directories under {root}")
|
|
for d in dirs:
|
|
try:
|
|
print(import_one(backend, d, args.update))
|
|
except Exception as e: # noqa: BLE001
|
|
print(f"FAIL {d.name}: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|