CtrlK
BlogDocsLog inGet started
Tessl Logo

sporkwace/monkey-thought-translator

translate claude skills into chatgpt or codex skills only after auditing purpose, target host, capability parity, resource portability, tool assumptions, and unrealizable behavior. use when the user uploads or points to a claude skill, asks to port a claude skill to chatgpt, codex, or openai skills, or wants a compatibility review before translation. requires a compatibility report, risk score, capability matrix, and user approval before packaging when behavior cannot be preserved faithfully.

83

2.34x
Quality

90%

Does it follow best practices?

Impact

89%

2.34x

Average score across 2 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

inspect_skill.pyscripts/

#!/usr/bin/env python3
"""Inspect a Claude skill folder or zip before translation.

This script is intentionally conservative. It reports probable host assumptions,
file inventory, target-sensitive findings, and a rough translation risk score.
The assistant must still make the final compatibility call and capability matrix.
"""
from __future__ import annotations

import argparse
import json
import re
import tempfile
import zipfile
from pathlib import Path
from typing import Iterable

HOST_PATTERNS = {
    "claude_runtime": [r"\bClaude Desktop\b", r"\bClaude Code\b", r"\bAnthropic\b", r"\bclaude\.ai\b", r"\bArtifacts\b"],
    "mcp": [r"\bMCP\b", r"model context protocol", r"mcpServers", r"mcp_server", r"mcp\.json"],
    "connectors": [r"Google Drive", r"Notion", r"Slack", r"GitHub", r"Linear", r"Dropbox", r"SharePoint", r"email", r"Gmail"],
    "credentials": [r"API[_ -]?KEY", r"TOKEN", r"SECRET", r"OAuth", r"cookie", r"credential", r"login", r"session"],
    "filesystem": [r"/Users/", r"~/", r"C:\\\\", r"/home/", r"local file", r"filesystem"],
    "shell": [r"\bbash\b", r"\bzsh\b", r"\bnpm\b", r"\bpnpm\b", r"\bpip\b", r"\bbrew\b", r"\bdocker\b", r"\bmake\b"],
    "background_work": [r"background", r"later", r"scheduled", r"cron", r"daemon", r"watcher", r"monitor"],
}

RISK_BY_CATEGORY = {
    "claude_runtime": "medium",
    "mcp": "high",
    "connectors": "high",
    "credentials": "high",
    "filesystem": "medium",
    "shell": "medium",
    "background_work": "high",
}

TEXT_EXTS = {".md", ".txt", ".yaml", ".yml", ".json", ".py", ".js", ".ts", ".sh", ".toml", ".xml", ".html", ".css"}
BINARY_EXTS = {".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"}


def read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except Exception:
        return ""


def extract_frontmatter(text: str) -> dict[str, str]:
    if not text.startswith("---"):
        return {}
    end = text.find("\n---", 3)
    if end == -1:
        return {}
    raw = text[3:end].strip()
    data: dict[str, str] = {}
    for line in raw.splitlines():
        if ":" in line:
            k, v = line.split(":", 1)
            data[k.strip()] = v.strip().strip('"\'')
    return data


def max_risk(risks: Iterable[str]) -> str:
    order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
    best = "low"
    for risk in risks:
        if order[risk] > order[best]:
            best = risk
    return best




def normalize_kebab(value: str) -> str:
    """Normalize a skill/display name to lowercase kebab-case."""
    value = value.strip()
    value = re.sub(r"['’]", "", value)
    value = re.sub(r"[^A-Za-z0-9]+", "-", value)
    value = re.sub(r"-{2,}", "-", value).strip("-")
    return value.lower()


def humanize_kebab(value: str) -> str:
    """Produce a readable display name from a kebab-case skill name."""
    return " ".join(part.capitalize() for part in value.split("-") if part)

def inspect_dir(root: Path) -> dict:
    skill_files = [p for p in root.rglob("SKILL.md") if p.is_file()]
    files = []
    findings = []
    total_size = 0
    risks = []
    possible_capabilities = []

    for p in sorted(root.rglob("*")):
        if not p.is_file():
            continue

        rel = str(p.relative_to(root))
        size = p.stat().st_size
        total_size += size
        suffix = p.suffix.lower()
        file_entry = {"path": rel, "size": size, "kind": "text" if suffix in TEXT_EXTS else "binary" if suffix in BINARY_EXTS else "unknown"}
        files.append(file_entry)

        if rel.endswith("SKILL.md"):
            possible_capabilities.append("core instructions")
        elif rel.startswith("references/") and suffix in TEXT_EXTS:
            possible_capabilities.append(f"reference guidance: {rel}")
        elif rel.startswith("scripts/"):
            possible_capabilities.append(f"scripted operation: {rel}")
        elif rel.startswith("assets/"):
            possible_capabilities.append(f"asset-dependent output: {rel}")

        if suffix in TEXT_EXTS:
            text = read_text(p)
            for category, patterns in HOST_PATTERNS.items():
                for pattern in patterns:
                    if re.search(pattern, text, flags=re.IGNORECASE):
                        findings.append({"file": rel, "category": category, "pattern": pattern, "risk": RISK_BY_CATEGORY[category]})
                        risks.append(RISK_BY_CATEGORY[category])

    frontmatter = {}
    source_name = root.name
    if len(skill_files) == 1:
        frontmatter = extract_frontmatter(read_text(skill_files[0]))
        source_name = frontmatter.get("name") or source_name
    elif len(skill_files) == 0:
        risks.append("blocked")
    else:
        risks.append("blocked")

    suggested_name = normalize_kebab(source_name) if source_name else "translated-skill"
    if not suggested_name:
        suggested_name = "translated-skill"

    if total_size > 25 * 1024 * 1024:
        risks.append("blocked")
        findings.append({"file": "<package>", "category": "package_size", "pattern": ">25MB", "risk": "blocked"})

    return {
        "root": str(root),
        "skill_md_count": len(skill_files),
        "skill_md_paths": [str(p.relative_to(root)) for p in skill_files],
        "frontmatter": frontmatter,
        "source_name_for_default_translation": source_name,
        "suggested_translated_skill_name": suggested_name,
        "suggested_display_name": humanize_kebab(suggested_name),
        "file_count": len(files),
        "total_size_bytes": total_size,
        "files": files,
        "probable_host_assumptions": findings,
        "rough_translation_risk": max_risk(risks),
        "possible_capabilities_to_review": sorted(set(possible_capabilities)),
        "next_step": "Build the capability matrix and confirm user approval before packaging if risk is high or blocked.",
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Inspect a Claude skill folder or zip before translation.")
    parser.add_argument("source", help="Path to skill folder or .zip archive")
    args = parser.parse_args()

    source = Path(args.source).expanduser().resolve()
    if not source.exists():
        raise SystemExit(f"source does not exist: {source}")

    if source.is_file() and source.suffix.lower() == ".zip":
        with tempfile.TemporaryDirectory() as tmp:
            with zipfile.ZipFile(source) as zf:
                zf.extractall(tmp)
            report = inspect_dir(Path(tmp))
    elif source.is_dir():
        report = inspect_dir(source)
    else:
        raise SystemExit("source must be a directory or .zip file")

    print(json.dumps(report, indent=2, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SKILL.md

tessl.json

tile.json