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).
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user