HarnessEcosystem / sidebyside /scripts /extract_claude.py
smlflg's picture
Initial public upload from Projekte/HarnessEcosystem
5134cea verified
Raw History Blame Contribute Delete
20.4 kB
from __future__ import annotations
from collections import Counter
from pathlib import Path
import json
import re
import shlex
__all__ = ["extract_claude"]
MAX_DOC_CHARS = 12000
MAX_SUMMARY_CHARS = 400
MAX_SOURCE_SNIPPET = 240
_FRONTMATTER_RE = re.compile(r"^---\s*$", re.M)
_HEADING_RE = re.compile(r"^#{1,6}\s+(.*\S)\s*$")
_BULLET_COMMAND_RE = re.compile(r"`/([A-Za-z0-9_.-]+)`")
_TOKEN_RE = re.compile(r"[A-Za-z0-9_.:/@+-]+")
def extract_claude(home: Path) -> dict:
root = _resolve_claude_root(home)
backup_root = Path("/home/smlflg/ClaudeSystemBackup")
backup_present = backup_root.exists()
settings = _load_json(root / "settings.json")
permissions = settings.get("permissions", {}) if isinstance(settings, dict) else {}
hooks = settings.get("hooks", {}) if isinstance(settings, dict) else {}
command_items = _collect_markdown_surface(root / "commands", "command")
agent_items = _collect_markdown_surface(root / "agents", "agent")
skill_items = _collect_markdown_surface(root / "skills", "skill")
command_count = len(command_items)
agent_count = len(agent_items)
skill_count = len(skill_items)
session_count = _count_files(root / "sessions", include_hidden=False)
session_log_count = _count_files(root / "session-logs", include_hidden=False)
telemetry_count = _count_files(root / "telemetry", include_hidden=False)
project_transcript_count = _count_files(root / "projects", include_hidden=False)
project_tool_usage = _sample_project_tool_usage(root / "projects")
root_group_counts = _command_group_counts(command_items)
settings_allow = _as_list(permissions.get("allow"))
settings_deny = _as_list(permissions.get("deny"))
mcp_allow = _extract_mcp_allow_entries(settings_allow)
hook_groups = _summarize_hooks(hooks)
instruction_layers = _build_instruction_layers(root)
changelog = _load_text(root / "cache" / "changelog.md")
changelog_candidates = _extract_changelog_command_candidates(
changelog,
{item["name"] for item in command_items},
)
backup_metadata = _backup_metadata(backup_root) if backup_present else {}
discoveries = []
if _plugins_dormant(root / "plugins"):
discoveries.append(
{
"kind": "dormant_surface",
"title": "Plugins directory looks dormant",
"reason": "Only blocklist.json was found under ~/.claude/plugins; no installed plugin manifests or plugin trees were present.",
}
)
if changelog_candidates:
discoveries.append(
{
"kind": "feature_gap",
"title": "Changelog-advertised commands/features have no local file evidence",
"items": changelog_candidates,
"reason": "Recent changelog entries mention slash commands or features that do not appear in the local command docs, settings, or hooks.",
}
)
if session_count == 0:
discoveries.append(
{
"kind": "sparse_history",
"title": "Sessions directory is empty",
"reason": "No files were found under ~/.claude/sessions.",
}
)
if command_count >= 40 or agent_count >= 15:
discoveries.append(
{
"kind": "lightly_used_surface",
"title": "Large command and agent surfaces with a thin history footprint",
"reason": (
f"{command_count} command docs, {agent_count} agent docs, and only "
f"{session_count} session file(s) / {session_log_count} session-log file(s) suggest a broad but lightly evidenced surface."
),
}
)
if session_count and session_count <= 1:
discoveries.append(
{
"kind": "sparse_history",
"title": "Sessions directory is effectively sparse",
"reason": "Exactly one session JSON file was present under ~/.claude/sessions, so the empty-dir candidate does not apply but the footprint is still minimal.",
}
)
warnings = [
"Skipped ~/.claude/.env, ~/.claude/.credentials.json, ~/.claude/settings.local.json, and other secret-bearing files.",
"Backup data at /home/smlflg/ClaudeSystemBackup was read as filenames and counts only; no file contents were read there.",
]
if not mcp_allow:
warnings.append("No explicit MCP tool allow entries were found in settings.json permissions.allow.")
return {
"id": "claude-code",
"name": "Claude Code",
"status": {
"home": str(root),
"backup_home": str(backup_root) if backup_present else None,
"settings_present": (root / "settings.json").exists(),
"backup_present": backup_present,
"model": _settings_model(settings),
"default_mode": _permission_default_mode(permissions),
"counts": {
"commands": command_count,
"agents": agent_count,
"skills": skill_count,
"sessions": session_count,
"session_logs": session_log_count,
"project_transcripts": project_transcript_count,
"telemetry": telemetry_count,
},
"backup_metadata": backup_metadata,
},
"config": {
"model": _settings_model(settings),
"permissions": {
"default_mode": _permission_default_mode(permissions),
"allow_count": len(settings_allow),
"deny_count": len(settings_deny),
"additional_directories": _as_list(permissions.get("additionalDirectories")),
},
"hooks": {
"groups": [group["name"] for group in hook_groups],
"group_count": len(hook_groups),
},
"mcp_tools": mcp_allow,
},
"instruction_layers": instruction_layers,
"extensions": {
"skills": skill_items,
"commands": command_items,
"agents": agent_items,
"hooks": hook_groups,
"mcp_tools": mcp_allow,
},
"usage": {
"counts": {
"commands": command_count,
"agents": agent_count,
"skills": skill_count,
"session_files": session_count,
"session_log_files": session_log_count,
"project_transcripts": project_transcript_count,
"telemetry_files": telemetry_count,
"tools_observed": sum(item["count"] for item in project_tool_usage),
},
"command_groups": root_group_counts,
"history": {
"session_file_count": session_count,
"session_log_file_count": session_log_count,
"telemetry_file_count": telemetry_count,
"project_transcript_count": project_transcript_count,
},
"tools": {
"observed_count": sum(item["count"] for item in project_tool_usage),
"top": project_tool_usage,
},
"surface_pressure": {
"commands_vs_history": _surface_pressure_label(command_count, session_log_count),
"agents_vs_history": _surface_pressure_label(agent_count, session_log_count),
},
},
"discoveries": discoveries,
"sources": _sources(root, backup_present),
"warnings": warnings,
}
def _resolve_claude_root(home: Path) -> Path:
home = Path(home).expanduser()
candidates = [home]
if home.name != ".claude":
candidates.insert(0, home / ".claude")
for candidate in candidates:
if (candidate / "settings.json").exists() or (candidate / "CLAUDE.md").exists():
return candidate
return candidates[0]
def _load_json(path: Path):
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
def _load_text(path: Path) -> str:
if not path.exists() or not path.is_file():
return ""
try:
with path.open("r", encoding="utf-8", errors="replace") as handle:
chunks = []
total = 0
for line in handle:
chunks.append(line)
total += len(line)
if total >= MAX_DOC_CHARS:
break
return "".join(chunks)
except OSError:
return ""
def _count_files(path: Path, include_hidden: bool = False) -> int:
if not path.exists():
return 0
count = 0
for item in path.rglob("*"):
if not item.is_file():
continue
if ".git" in item.parts:
continue
relative = item.relative_to(path)
if not include_hidden and any(part.startswith(".") for part in relative.parts):
# The tree contains operational dotfiles we do not want to over-report.
continue
count += 1
return count
def _sample_project_tool_usage(path: Path, max_files: int = 140) -> list[dict]:
if not path.exists():
return []
counter = Counter()
files = sorted(
(item for item in path.rglob("*.jsonl") if item.is_file() and ".git" not in item.parts),
key=lambda item: item.stat().st_mtime if item.exists() else 0,
reverse=True,
)[:max_files]
for file_path in files:
try:
with file_path.open("r", encoding="utf-8", errors="replace") as handle:
for line in handle:
parsed = _safe_json_line(line)
if parsed:
counter.update(_tool_names_from_json(parsed))
except OSError:
continue
return [{"name": name, "count": count} for name, count in counter.most_common(16)]
def _safe_json_line(line: str):
try:
parsed = json.loads(line)
except json.JSONDecodeError:
return None
return parsed if isinstance(parsed, dict) else None
def _tool_names_from_json(value) -> list[str]:
names = []
if isinstance(value, dict):
tool_type = value.get("type")
name = value.get("name") or value.get("tool_name") or value.get("tool")
if isinstance(name, str) and (tool_type in {"tool_use", "tool", "function"} or "tool" in str(tool_type).lower()):
names.append(name)
for nested in value.values():
names.extend(_tool_names_from_json(nested))
elif isinstance(value, list):
for item in value:
names.extend(_tool_names_from_json(item))
return names
def _as_list(value):
if isinstance(value, list):
return value
if value is None:
return []
return [value]
def _settings_model(settings: dict) -> str | None:
model = settings.get("model")
return model if isinstance(model, str) else None
def _permission_default_mode(permissions: dict) -> str | None:
mode = permissions.get("defaultMode")
return mode if isinstance(mode, str) else None
def _surface_pressure_label(surface_count: int, history_count: int) -> str:
if history_count == 0:
return "high"
ratio = surface_count / history_count
if ratio >= 10:
return "high"
if ratio >= 3:
return "medium"
return "low"
def _collect_markdown_surface(base: Path, kind: str) -> list[dict]:
if not base.exists():
return []
items = []
for path in sorted(p for p in base.rglob("*.md") if ".git" not in p.parts and p.is_file()):
rel = path.relative_to(base.parent)
if kind == "command" and rel.parts[0] != "commands":
continue
if kind == "agent" and rel.parts[0] != "agents":
continue
if kind == "skill" and rel.parts[0] != "skills":
continue
title, summary = _summarize_markdown(path)
group = _surface_group(rel)
items.append(
{
"name": _surface_name(path, title),
"path": str(rel),
"group": group,
"summary": summary,
}
)
return items
def _surface_group(rel_path: Path) -> str:
parts = rel_path.parts
if len(parts) <= 2:
return "root"
if parts[1] in {"gsd", "_archived"}:
return parts[1]
if parts[0] == "skills" and len(parts) > 2:
return parts[1]
return "root"
def _surface_name(path: Path, title: str) -> str:
stem = path.stem
if stem == "SKILL" and path.parent.name:
return path.parent.name
if title and title != path.name:
return title
return stem
def _summarize_markdown(path: Path) -> tuple[str, str]:
text = _load_text(path)
if not text:
return path.stem, ""
frontmatter, body = _split_frontmatter(text)
metadata = _parse_frontmatter(frontmatter)
title = _first_non_empty(
metadata.get("name"),
metadata.get("title"),
_first_heading(text),
path.stem,
)
lead = metadata.get("description", "")
if not lead:
lead = _extract_prose_summary(body)
summary = _compact_summary(" ".join(part for part in [title, lead] if part))
return title, summary
def _split_frontmatter(text: str) -> tuple[str, str]:
if not text.startswith("---"):
return "", text
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return "", text
for index in range(1, len(lines)):
if lines[index].strip() == "---":
frontmatter = "\n".join(lines[1:index])
body = "\n".join(lines[index + 1 :])
return frontmatter, body
return "", text
def _parse_frontmatter(text: str) -> dict[str, str]:
if not text:
return {}
data = {}
for raw in text.splitlines():
if ":" not in raw:
continue
key, value = raw.split(":", 1)
key = key.strip()
value = value.strip().strip("'\"")
if key:
data[key] = value
return data
def _first_heading(text: str) -> str:
for line in text.splitlines():
match = _HEADING_RE.match(line)
if match:
return match.group(1).strip()
return ""
def _extract_prose_summary(text: str) -> str:
lines = []
in_fence = False
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("```"):
in_fence = not in_fence
continue
if in_fence:
continue
if not stripped:
if lines:
break
continue
if stripped.startswith("#"):
continue
lines.append(stripped)
if len(" ".join(lines)) >= MAX_SUMMARY_CHARS:
break
return _compact_summary(" ".join(lines))
def _compact_summary(text: str) -> str:
text = re.sub(r"\s+", " ", text).strip()
if len(text) > MAX_SUMMARY_CHARS:
return text[: MAX_SUMMARY_CHARS - 1].rstrip() + "…"
return text
def _first_non_empty(*values) -> str:
for value in values:
if isinstance(value, str) and value.strip():
return value.strip()
return ""
def _command_group_counts(items: list[dict]) -> dict[str, int]:
counter = Counter()
for item in items:
group = item.get("group") or "root"
counter[group] += 1
return dict(sorted(counter.items()))
def _summarize_hooks(hooks: dict) -> list[dict]:
if not isinstance(hooks, dict):
return []
items = []
for name in sorted(hooks):
rules = hooks.get(name)
if not isinstance(rules, list):
continue
command_names = []
matchers = []
for rule in rules:
if not isinstance(rule, dict):
continue
matcher = rule.get("matcher")
if isinstance(matcher, str) and matcher:
matchers.append(matcher)
for hook in _as_list(rule.get("hooks")):
if not isinstance(hook, dict):
continue
command = hook.get("command")
if isinstance(command, str) and command:
command_names.append(_hook_command_name(command))
items.append(
{
"name": name,
"rule_count": len(rules),
"matchers": sorted({m for m in matchers if m}),
"commands": sorted({c for c in command_names if c}),
}
)
return items
def _hook_command_name(command: str) -> str:
try:
parts = shlex.split(command)
except ValueError:
parts = command.split()
if not parts:
return command.strip()
candidate = parts[-1] if len(parts) > 1 else parts[0]
return Path(candidate.strip("\"'")).name
def _extract_mcp_allow_entries(allow_entries) -> list[str]:
if not isinstance(allow_entries, list):
return []
items = []
for entry in allow_entries:
if isinstance(entry, str) and ("mcp__" in entry or entry.startswith("mcp:")):
items.append(entry)
return sorted(set(items))
def _build_instruction_layers(root: Path) -> list[dict]:
ordered = [
root / "CLAUDE.md",
root / "WelcheFehlerVermeiden.md",
root / "WieArbeitestDuMitSamuel.md",
root / "Skilluebersicht.md",
root / "README.md",
]
layers = []
for path in ordered:
if not path.exists():
continue
title, summary = _summarize_markdown(path)
layers.append(
{
"name": title,
"path": str(path),
"summary": summary,
}
)
return layers
def _extract_changelog_command_candidates(changelog: str, known_commands: set[str]) -> list[str]:
if not changelog:
return []
seen = []
for match in _BULLET_COMMAND_RE.finditer(changelog):
name = match.group(1)
if name not in known_commands and name not in seen:
seen.append(name)
return seen[:8]
def _backup_metadata(root: Path) -> dict:
if not root.exists():
return {}
entries = []
file_counts = Counter()
top_level = []
for child in sorted(root.iterdir(), key=lambda p: p.name):
top_level.append(child.name)
if child.is_file():
file_counts["."] += 1
entries.append({"name": child.name, "kind": "file"})
elif child.is_dir():
count = _count_files(child, include_hidden=True)
file_counts[child.name] += count
entries.append({"name": child.name, "kind": "dir", "file_count": count})
return {
"top_level": top_level[:40],
"entries": entries[:40],
"file_counts": dict(sorted(file_counts.items())),
}
def _plugins_dormant(path: Path) -> bool:
if not path.exists():
return False
files = [p for p in path.iterdir() if p.is_file() and ".git" not in p.parts]
subdirs = [p for p in path.iterdir() if p.is_dir() and not p.name.startswith(".")]
return len(files) == 1 and files[0].name == "blocklist.json" and not subdirs
def _sources(root: Path, backup_present: bool) -> list[dict]:
entries = [
{"kind": "config", "path": str(root / "settings.json")},
{"kind": "instruction", "path": str(root / "CLAUDE.md")},
{"kind": "instruction", "path": str(root / "WelcheFehlerVermeiden.md")},
{"kind": "instruction", "path": str(root / "WieArbeitestDuMitSamuel.md")},
{"kind": "instruction", "path": str(root / "Skilluebersicht.md")},
{"kind": "instruction", "path": str(root / "README.md")},
{"kind": "surface", "path": str(root / "commands")},
{"kind": "surface", "path": str(root / "agents")},
{"kind": "surface", "path": str(root / "skills")},
{"kind": "surface", "path": str(root / "sessions")},
{"kind": "surface", "path": str(root / "session-logs")},
{"kind": "surface", "path": str(root / "telemetry")},
{"kind": "surface", "path": str(root / "cache" / "changelog.md")},
]
if backup_present:
entries.append({"kind": "metadata_only", "path": "/home/smlflg/ClaudeSystemBackup"})
return entries