104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from context_sizer import estimate_tokens, read_text, TEXT_EXTS
|
|
|
|
|
|
OPTIONAL_DIRS = ("references", "scripts", "assets", "evals", "templates")
|
|
CANONICAL_PATHS = ("SKILL.md", "manifest.json", "agents", "references", "scripts", "assets", "evals", "templates")
|
|
|
|
|
|
def has_files(path: Path) -> bool:
|
|
return path.exists() and any(child.is_file() for child in path.rglob("*"))
|
|
|
|
|
|
def iter_relevant_files(root: Path) -> list[Path]:
|
|
files = []
|
|
for entry in CANONICAL_PATHS:
|
|
path = root / entry
|
|
if path.is_file():
|
|
files.append(path)
|
|
elif path.is_dir():
|
|
files.extend(sorted(file for file in path.rglob("*") if file.is_file()))
|
|
return files
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Check whether a skill package keeps resource boundaries under control.")
|
|
parser.add_argument("skill_dir")
|
|
parser.add_argument("--max-initial-tokens", type=int, default=1800)
|
|
parser.add_argument("--warn-skill-body-tokens", type=int, default=1400)
|
|
args = parser.parse_args()
|
|
|
|
root = Path(args.skill_dir).resolve()
|
|
skill_md = root / "SKILL.md"
|
|
failures = []
|
|
warnings = []
|
|
|
|
if not skill_md.exists():
|
|
failures.append("Missing SKILL.md")
|
|
report = {"ok": False, "failures": failures, "warnings": warnings}
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
raise SystemExit(2)
|
|
|
|
files = iter_relevant_files(root)
|
|
skill_body_tokens = 0
|
|
other_tokens = 0
|
|
initial_load_tokens = 0
|
|
total_text_tokens = 0
|
|
for path in files:
|
|
if path.suffix and path.suffix not in TEXT_EXTS and path.name != "SKILL.md":
|
|
continue
|
|
text = read_text(path)
|
|
tokens = estimate_tokens(text)
|
|
total_text_tokens += tokens
|
|
rel = path.relative_to(root)
|
|
if rel.name == "SKILL.md":
|
|
skill_body_tokens += tokens
|
|
initial_load_tokens += tokens
|
|
else:
|
|
other_tokens += tokens
|
|
if rel.parts[0] in {"agents"}:
|
|
initial_load_tokens += tokens
|
|
|
|
if initial_load_tokens > args.max_initial_tokens:
|
|
failures.append(
|
|
f"Estimated initial-load tokens exceed budget: {initial_load_tokens} > {args.max_initial_tokens}"
|
|
)
|
|
if skill_body_tokens > args.warn_skill_body_tokens:
|
|
warnings.append(f"SKILL.md is getting heavy: {skill_body_tokens} estimated tokens.")
|
|
|
|
skill_text = skill_md.read_text(encoding="utf-8")
|
|
for dirname in OPTIONAL_DIRS:
|
|
path = root / dirname
|
|
if path.exists() and not has_files(path):
|
|
warnings.append(f"{dirname}/ exists but is empty.")
|
|
continue
|
|
if has_files(path) and dirname not in skill_text and dirname.capitalize() not in skill_text:
|
|
warnings.append(f"{dirname}/ contains files but is not referenced explicitly in SKILL.md.")
|
|
|
|
if other_tokens and skill_body_tokens / (skill_body_tokens + other_tokens) > 0.75:
|
|
warnings.append("Most text still lives in SKILL.md; consider moving detail into references/ or scripts/.")
|
|
|
|
report = {
|
|
"ok": not failures,
|
|
"failures": failures,
|
|
"warnings": warnings,
|
|
"stats": {
|
|
"skill_body_tokens": skill_body_tokens,
|
|
"other_text_tokens": other_tokens,
|
|
"estimated_initial_load_tokens": initial_load_tokens,
|
|
"estimated_total_text_tokens": total_text_tokens,
|
|
"relevant_file_count": len(files),
|
|
},
|
|
}
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
if failures:
|
|
raise SystemExit(2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|