Import 13 skills from the old opencode setup into turnstone
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).
This commit is contained in:
1 parent
93fa05bdcc
commit
18fdff7d8c
15 files changed
+1973
No files matched your search
@@ -0,0 +1,120 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user