mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
ce4b5d47a0
Signed-off-by: phernandez <paul@basicmachines.co>
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate Basic Memory SKILL.md source directories."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
PLAIN_SCALAR_MAPPING_VALUE = re.compile(r":(?:\s|$)")
|
|
|
|
|
|
def strip_matching_quotes(value: str) -> str:
|
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
return value[1:-1]
|
|
return value
|
|
|
|
|
|
def validate_plain_scalar(path: Path, line_number: int, key: str, value: str) -> None:
|
|
"""Catch invalid plain-scalar YAML that Codex rejects while loading skills."""
|
|
stripped = value.strip()
|
|
if not stripped or stripped[0] in {'"', "'"} or stripped in {"|", ">", "|-", ">-", "|+", ">+"}:
|
|
return
|
|
if PLAIN_SCALAR_MAPPING_VALUE.search(stripped):
|
|
raise SystemExit(
|
|
f"{path}:{line_number}: invalid YAML frontmatter for {key!r}: "
|
|
"unquoted ':' followed by whitespace; quote the value"
|
|
)
|
|
|
|
|
|
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
"""Extract top-level frontmatter keys from a Markdown file.
|
|
|
|
A deliberately minimal parser (no PyYAML — this runs under bare `python3` in
|
|
CI). It only captures **top-level** `key: value` lines. Indented lines are
|
|
skipped, so nested blocks (a schema note's `schema:`/`settings:` children) can't
|
|
overwrite a top-level key like `type` or `entity` via last-write-wins. It does
|
|
not interpret block scalars or multi-line values; callers rely on single-line
|
|
top-level fields (name, description, type, entity). Keep the Codex-facing YAML
|
|
guard here dependency-free so package checks work under bare `python3`.
|
|
"""
|
|
lines = path.read_text().splitlines()
|
|
if not lines or lines[0] != "---":
|
|
raise SystemExit(f"{path}: missing YAML frontmatter")
|
|
|
|
frontmatter: dict[str, str] = {}
|
|
for line_number, line in enumerate(lines[1:], start=2):
|
|
if line == "---":
|
|
break
|
|
if line[:1] in (" ", "\t"): # nested key — not a top-level field
|
|
continue
|
|
if ":" not in line:
|
|
continue
|
|
key, value = line.split(":", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
validate_plain_scalar(path, line_number, key, value)
|
|
frontmatter[key] = strip_matching_quotes(value)
|
|
else:
|
|
raise SystemExit(f"{path}: unclosed YAML frontmatter")
|
|
|
|
return frontmatter
|
|
|
|
|
|
def validate_skills(skills_root: Path) -> None:
|
|
if not skills_root.exists():
|
|
raise SystemExit(f"Skills directory not found: {skills_root}")
|
|
|
|
skill_dirs = sorted(path for path in skills_root.glob("memory-*") if path.is_dir())
|
|
if not skill_dirs:
|
|
raise SystemExit(f"No memory-* skill directories found in {skills_root}")
|
|
|
|
for skill_dir in skill_dirs:
|
|
skill_file = skill_dir / "SKILL.md"
|
|
if not skill_file.exists():
|
|
raise SystemExit(f"{skill_dir}: missing SKILL.md")
|
|
|
|
frontmatter = parse_frontmatter(skill_file)
|
|
name = frontmatter.get("name")
|
|
description = frontmatter.get("description")
|
|
if name != skill_dir.name:
|
|
raise SystemExit(f"{skill_file}: name {name!r} does not match directory")
|
|
if not description:
|
|
raise SystemExit(f"{skill_file}: missing description")
|
|
|
|
print(f"validated {len(skill_dirs)} skills in {skills_root}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("skills_root", nargs="?", default="skills")
|
|
args = parser.parse_args()
|
|
validate_skills((Path.cwd() / args.skills_root).resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|