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).
188 lines
6.1 KiB
Python
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()
|