"""SkillExportable: skills are leaves (no deps, no requirements). A skill is either a single markdown file or a folder (SKILL.md + supporting files like scripts/templates), so this powers both the .swarm round-trip AND the generic "import a .md or a zip-of-SKILL.md" path. Folder skills ride the entity files() channel so their supporting files survive export/import. Nothing here is secret, but the body still rides the central scrub in case someone pasted a token in.""" from __future__ import annotations import os import shutil from backend.apps.skills import skills as store from backend.apps.swarm.exportable import DepRef, ExportContext, RemapTable from backend.apps.swarm.models import EntityType, Requirement class SkillExportable: type = EntityType.skill def __init__(self, local_id: str, name: str, payload: dict, files: dict[str, bytes] | None = None): self.local_id = local_id self.name = name self.payload = payload self.p_files = files or {} @classmethod def load(cls, local_id: str) -> "SkillExportable | None": md_path, kind = store.skill_md_path(local_id) if not md_path: return None with open(md_path, encoding="utf-8") as f: content = f.read() meta = store.load_index().get(local_id, {}) name = meta.get("name") or local_id.replace("-", " ").replace("_", " ").title() payload = { "slug": local_id, "name": name, "description": meta.get("description", ""), "command": meta.get("command", local_id), "content": content, "builtin": bool(meta.get("built_in", False)), } files: dict[str, bytes] = {} if kind == "folder": files = p_read_supporting_files(os.path.join(store.SKILLS_DIR, local_id)) return cls(local_id, name, payload, files) def serialize(self, ctx: ExportContext) -> dict: return dict(self.payload) def files(self) -> dict[str, bytes]: return dict(self.p_files) def dependencies(self) -> list[DepRef]: return [] def requirements(self) -> list[Requirement]: return [] @classmethod def conflict(cls, payload: dict) -> str | None: slug = payload.get("slug") or "" if slug and p_slug_taken(slug): return "already exists; will be added as a copy" return None @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: base = (payload.get("slug") or payload.get("name") or "skill").lower().replace(" ", "-") slug = p_free_slug(base) meta = { "name": payload.get("name", slug), "description": payload.get("description", ""), "command": payload.get("command", slug), } # Every imported skill lands as a folder (SKILL.md + any supporting files), one path for one-file and multi-file skills alike. write_folder_skill is path-traversal-safe, so an untrusted bundle can't escape the skill dir. bundle = {"SKILL.md": payload.get("content", "")} for rel, data in files.items(): bundle[rel] = data.decode("utf-8", errors="replace") skill = store.write_folder_skill(slug, bundle, meta) return skill.id @classmethod def rollback(cls, local_id: str) -> None: skill_dir = os.path.join(store.SKILLS_DIR, local_id) flat = os.path.join(store.SKILLS_DIR, f"{local_id}.md") if os.path.isdir(skill_dir): shutil.rmtree(skill_dir, ignore_errors=True) if os.path.isfile(flat): os.remove(flat) index = store.load_index() if local_id in index: index.pop(local_id, None) store.save_index(index) def p_read_supporting_files(skill_dir: str) -> dict[str, bytes]: """Every file in a skill folder except SKILL.md, as {relpath: bytes}.""" out: dict[str, bytes] = {} for root, p_dirs, names in os.walk(skill_dir): for n in names: full = os.path.join(root, n) rel = os.path.relpath(full, skill_dir) if rel == "SKILL.md" or n.startswith("."): continue try: with open(full, "rb") as f: out[rel] = f.read() except OSError: continue return out def p_slug_taken(slug: str) -> bool: return ( slug in store.load_index() or os.path.isfile(os.path.join(store.SKILLS_DIR, f"{slug}.md")) or os.path.isdir(os.path.join(store.SKILLS_DIR, slug)) ) def p_free_slug(base: str) -> str: base = base or "skill" if not p_slug_taken(base): return base cand = f"{base}-imported" if not p_slug_taken(cand): return cand i = 2 while p_slug_taken(f"{base}-imported-{i}"): i += 1 return f"{base}-imported-{i}"