Files
periscope/scripts/upload_skills.py
Siddharth Kothari 6672d2be57 Pinscope open-source core
Agentic schematic validation: datasheet extraction via Claude Console
Skills, netlist/BOM design graph, per-IC direct datasheet review with
page citations, capacitor derating, Next.js report UI.

Extracted from the Pinscope cloud codebase. Auth and billing live in the
private gateway repo behind stable seams (billing_hook.py, adapter files
listed in CLAUDE.md).
2026-07-16 21:29:45 -07:00

188 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Upload or update extraction skills on the Claude Console platform.
Usage:
python3 scripts/upload_skills.py # Create new skills
python3 scripts/upload_skills.py --update # Create new versions of existing skills
python3 scripts/upload_skills.py --list # List current skills
Requires ANTHROPIC_API_KEY environment variable.
Reads/writes skill IDs to backend/skills_manifest.json.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
import anthropic
PROJECT_ROOT = Path(__file__).resolve().parent.parent
# Load API key from backend .env if not already in environment
_env_file = PROJECT_ROOT / "backend" / ".env"
if _env_file.exists() and not os.environ.get("ANTHROPIC_API_KEY"):
for line in _env_file.read_text().splitlines():
line = line.strip()
if line.startswith("ANTHROPIC_API_KEY=") and not line.startswith("#"):
os.environ["ANTHROPIC_API_KEY"] = line.split("=", 1)[1].strip().strip("'\"")
break
SKILLS_DIR = PROJECT_ROOT / "skills"
MANIFEST_PATH = PROJECT_ROOT / "backend" / "skills_manifest.json"
SKILLS = [
{
"directory": "extract-pintable",
"display_title": "Extract Pin Table",
},
{
"directory": "extract-pattern",
"display_title": "Extract Passive Pattern",
},
{
"directory": "extract-specs",
"display_title": "Extract Component Specs",
},
]
def load_manifest() -> dict:
if MANIFEST_PATH.exists():
return json.loads(MANIFEST_PATH.read_text())
return {}
def save_manifest(manifest: dict) -> None:
MANIFEST_PATH.write_text(json.dumps(manifest, indent=2) + "\n")
print(f"\nManifest written to {MANIFEST_PATH}")
def skill_file_tuples(directory: str) -> list[tuple[str, bytes]]:
"""Return (directory/filename, content) tuples for all files in a skill directory.
The API requires files to be in a top-level directory with SKILL.md at its root.
"""
skill_dir = SKILLS_DIR / directory
files = []
for path in sorted(skill_dir.iterdir()):
if path.is_file():
files.append((f"{directory}/{path.name}", path.read_bytes()))
return files
def _bump_minor_version(v: str) -> str:
"""Bump the minor segment of a semver string, reset patch to 0."""
major, minor, patch = v.split(".")
return f"{major}.{int(minor) + 1}.0"
def create_skills(client: anthropic.Anthropic) -> None:
"""Create new skills on the platform."""
manifest = load_manifest()
# Initialize default_model_version if absent
if "default_model_version" not in manifest:
manifest["default_model_version"] = "1.0.0"
print(f" Initialized default_model_version: 1.0.0")
for skill in SKILLS:
name = skill["directory"]
if name in manifest:
print(f" {name}: already exists (skill_id={manifest[name]['skill_id']}), skipping. Use --update to create a new version.")
continue
print(f" Creating {name}...")
files = skill_file_tuples(name)
result = client.beta.skills.create(
display_title=skill["display_title"],
files=files,
)
manifest[name] = {
"skill_id": result.id,
"latest_version": result.latest_version,
"display_title": skill["display_title"],
}
print(f" skill_id: {result.id}")
print(f" version: {result.latest_version}")
save_manifest(manifest)
def update_skills(client: anthropic.Anthropic) -> None:
"""Create new versions for existing skills."""
manifest = load_manifest()
for skill in SKILLS:
name = skill["directory"]
if name not in manifest:
print(f" {name}: not yet created, run without --update first.")
continue
skill_id = manifest[name]["skill_id"]
print(f" Updating {name} (skill_id={skill_id})...")
files = skill_file_tuples(name)
result = client.beta.skills.versions.create(
skill_id=skill_id,
files=files,
)
manifest[name]["latest_version"] = result.version
print(f" new version: {result.version}")
# Bump default_model_version minor (new skill → new extraction schema)
old_v = manifest.get("default_model_version", "1.0.0")
new_v = _bump_minor_version(old_v)
manifest["default_model_version"] = new_v
print(f"\n default_model_version bumped: {old_v}{new_v}")
save_manifest(manifest)
def list_skills(client: anthropic.Anthropic) -> None:
"""List skills on the platform."""
manifest = load_manifest()
if not manifest:
print(" No skills in manifest. Run without flags to create them.")
return
for name, info in manifest.items():
skill_id = info["skill_id"]
print(f"\n {name}:")
print(f" skill_id: {skill_id}")
try:
skill = client.beta.skills.retrieve(skill_id)
print(f" display_title: {skill.display_title}")
print(f" latest_version: {skill.latest_version}")
versions = client.beta.skills.versions.list(skill_id=skill_id)
print(f" versions: {[v.version for v in versions.data]}")
except Exception as e:
print(f" error: {e}")
print(f"\n Manifest: {MANIFEST_PATH}")
def main() -> None:
parser = argparse.ArgumentParser(description="Upload extraction skills to Claude Console")
group = parser.add_mutually_exclusive_group()
group.add_argument("--update", action="store_true", help="Create new versions of existing skills")
group.add_argument("--list", action="store_true", help="List current skills and versions")
args = parser.parse_args()
client = anthropic.Anthropic() # uses ANTHROPIC_API_KEY env var
if args.list:
print("Listing skills...")
list_skills(client)
elif args.update:
print("Updating skills (new versions)...")
update_skills(client)
else:
print("Creating skills...")
create_skills(client)
if __name__ == "__main__":
main()