172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
|
|
def read_simple_yaml(path: Path) -> dict:
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
data: dict = {}
|
|
stack: list[tuple[int, dict]] = [(0, data)]
|
|
for raw_line in lines:
|
|
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
|
|
continue
|
|
indent = len(raw_line) - len(raw_line.lstrip(" "))
|
|
line = raw_line.strip()
|
|
while len(stack) > 1 and indent <= stack[-1][0]:
|
|
stack.pop()
|
|
parent = stack[-1][1]
|
|
if line.startswith("- "):
|
|
item = line[2:].strip().strip("'\"")
|
|
existing = parent.setdefault("__list__", [])
|
|
existing.append(item)
|
|
continue
|
|
if ":" not in line:
|
|
continue
|
|
key, value = line.split(":", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
if value == "":
|
|
child: dict = {}
|
|
parent[key] = child
|
|
stack.append((indent, child))
|
|
else:
|
|
parent[key] = value.strip("'\"")
|
|
return data
|
|
|
|
|
|
def read_frontmatter(skill_md: Path) -> dict:
|
|
text = skill_md.read_text(encoding="utf-8")
|
|
if not text.startswith("---"):
|
|
return {}
|
|
parts = text.split("---", 2)
|
|
if len(parts) < 3:
|
|
return {}
|
|
data = {}
|
|
for line in parts[1].splitlines():
|
|
if ":" not in line:
|
|
continue
|
|
key, value = line.split(":", 1)
|
|
data[key.strip()] = value.strip().strip("'\"")
|
|
return data
|
|
|
|
|
|
def read_interface(skill_dir: Path) -> dict:
|
|
path = skill_dir / "agents" / "interface.yaml"
|
|
if not path.exists():
|
|
return {}
|
|
raw = read_simple_yaml(path)
|
|
compatibility = raw.get("compatibility", {})
|
|
targets = compatibility.get("adapter_targets", {})
|
|
if isinstance(targets, dict) and "__list__" in targets:
|
|
compatibility["adapter_targets"] = targets["__list__"]
|
|
raw["compatibility"] = compatibility
|
|
return raw
|
|
|
|
|
|
def build_manifest(skill_dir: Path, platform: str) -> dict:
|
|
frontmatter = read_frontmatter(skill_dir / "SKILL.md")
|
|
interface = read_interface(skill_dir).get("interface", {})
|
|
compatibility = read_interface(skill_dir).get("compatibility", {})
|
|
return {
|
|
"name": frontmatter.get("name", skill_dir.name),
|
|
"description": frontmatter.get("description", ""),
|
|
"version": frontmatter.get("version", "1.0.0"),
|
|
"platform": platform,
|
|
"skill_root": skill_dir.name,
|
|
"display_name": interface.get("display_name", skill_dir.name),
|
|
"short_description": interface.get("short_description", ""),
|
|
"default_prompt": interface.get("default_prompt", ""),
|
|
"canonical_metadata": "agents/interface.yaml",
|
|
"adapter_targets": compatibility.get("adapter_targets", []),
|
|
}
|
|
|
|
|
|
def write_yaml_like(path: Path, payload: dict) -> None:
|
|
interface = payload.get("interface", {})
|
|
lines = ["interface:"]
|
|
for key in ("display_name", "short_description", "default_prompt"):
|
|
value = interface.get(key, "")
|
|
lines.append(f' {key}: "{value}"')
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def write_adapter(skill_dir: Path, out_dir: Path, platform: str) -> Path:
|
|
target_dir = out_dir / "targets" / platform
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
payload = build_manifest(skill_dir, platform)
|
|
if platform == "openai":
|
|
meta_dir = target_dir / "agents"
|
|
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
write_yaml_like(
|
|
meta_dir / "openai.yaml",
|
|
{
|
|
"interface": {
|
|
"display_name": payload["display_name"],
|
|
"short_description": payload["short_description"],
|
|
"default_prompt": payload["default_prompt"],
|
|
}
|
|
},
|
|
)
|
|
payload["install_hint"] = f"Use the packaged skill and include targets/openai/agents/openai.yaml when the client expects OpenAI-style interface metadata."
|
|
elif platform == "claude":
|
|
notes = target_dir / "README.md"
|
|
notes.write_text(
|
|
f"# Claude-Compatible Package\n\nUse `{skill_dir.name}` with its neutral source files. This target does not require vendor metadata by default.\n",
|
|
encoding="utf-8",
|
|
)
|
|
payload["install_hint"] = f"Use the packaged skill directly; this target relies on SKILL.md and optional neutral metadata."
|
|
else:
|
|
payload["install_hint"] = f"Use {skill_dir.name} as an Agent Skills compatible package."
|
|
path = target_dir / "adapter.json"
|
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def make_zip(skill_dir: Path, out_dir: Path) -> Path:
|
|
zip_path = out_dir / f"{skill_dir.name}.zip"
|
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
for path in skill_dir.rglob("*"):
|
|
if path.is_file():
|
|
zf.write(path, arcname=str(path.relative_to(skill_dir.parent)))
|
|
return zip_path
|
|
|
|
|
|
def copy_manifest(skill_dir: Path, out_dir: Path) -> Path:
|
|
manifest_path = out_dir / "manifest.json"
|
|
manifest_path.write_text(
|
|
json.dumps(build_manifest(skill_dir, "generic"), ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
return manifest_path
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Generate lightweight cross-platform packaging artifacts.")
|
|
parser.add_argument("skill_dir", help="Path to the skill directory")
|
|
parser.add_argument("--platform", action="append", default=[], help="Target platform: openai, claude, generic")
|
|
parser.add_argument("--output-dir", default="dist", help="Output directory")
|
|
parser.add_argument("--zip", action="store_true", help="Create a zip package")
|
|
args = parser.parse_args()
|
|
|
|
skill_dir = Path(args.skill_dir).resolve()
|
|
out_dir = Path(args.output_dir).resolve()
|
|
if out_dir.exists():
|
|
shutil.rmtree(out_dir)
|
|
out_dir.mkdir(parents=True)
|
|
|
|
manifest = copy_manifest(skill_dir, out_dir)
|
|
generated = [str(manifest)]
|
|
for platform in (args.platform or ["generic"]):
|
|
generated.append(str(write_adapter(skill_dir, out_dir, platform)))
|
|
if args.zip:
|
|
generated.append(str(make_zip(skill_dir, out_dir)))
|
|
|
|
print(json.dumps({"output_dir": str(out_dir), "generated": generated}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|