ClaudeCodePanel / config_io.py
smlflg's picture
Initial public upload from Projekte/ClaudeCodePanel
bbc65cd verified
Raw History Blame Contribute Delete
5.3 kB
"""Config I/O for Claude Code Control Panel.
Reads and writes ~/.claude/settings.json with atomic operations (backup + tmp + rename).
Also handles hook configuration and coaching script parameters.
"""
import json
import os
import shutil
import tempfile
from datetime import datetime
from pathlib import Path
SETTINGS_PATH = Path.home() / ".claude" / "settings.json"
COACH_SCRIPT = Path.home() / ".claude" / "hooks" / "coaching" / "coach-git-test.sh"
def read_settings() -> dict:
"""Read and parse ~/.claude/settings.json."""
if not SETTINGS_PATH.exists():
return {}
with SETTINGS_PATH.open() as f:
return json.load(f)
def write_settings(settings: dict) -> None:
"""Atomically write settings.json (backup + tmp + rename)."""
# Create backup
if SETTINGS_PATH.exists():
backup = SETTINGS_PATH.with_suffix(
f".json.backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
)
shutil.copy2(SETTINGS_PATH, backup)
# Write to temp file, then atomic rename
fd, tmp_path = tempfile.mkstemp(
dir=SETTINGS_PATH.parent, suffix=".tmp", prefix="settings-"
)
try:
with os.fdopen(fd, "w") as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
f.write("\n")
os.replace(tmp_path, SETTINGS_PATH)
except Exception:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
raise
def update_setting(key_path: str, value) -> dict:
"""Update a nested key in settings.json using dot notation.
Example: update_setting("env.SLASH_COMMAND_TOOL_CHAR_BUDGET", "15000")
"""
settings = read_settings()
keys = key_path.split(".")
obj = settings
for key in keys[:-1]:
if key not in obj:
obj[key] = {}
obj = obj[key]
obj[keys[-1]] = value
write_settings(settings)
return settings
def read_hook_list() -> list[dict]:
"""Extract all hooks from settings.json as a flat list.
Returns list of dicts with:
event, group_index, matcher, command, timeout, async_, short_name
"""
settings = read_settings()
hooks_config = settings.get("hooks", {})
result = []
for event_name, hook_groups in hooks_config.items():
for group_idx, group in enumerate(hook_groups):
matcher = group.get("matcher", "")
for hook in group.get("hooks", []):
command = hook.get("command", "")
short_name = _derive_hook_name(command)
result.append({
"event": event_name,
"group_index": group_idx,
"matcher": matcher,
"command": command,
"timeout": hook.get("timeout", 10),
"async_": hook.get("async", False),
"short_name": short_name,
})
return result
def _derive_hook_name(command: str) -> str:
"""Extract a readable name from a hook command path."""
parts = command.strip().split()
for part in reversed(parts):
if "/" in part:
name = Path(part).stem
return name.replace("_", " ").replace("-", " ").title()
return command[:30]
def update_hook(event: str, group_index: int, field: str, value) -> dict:
"""Update a specific hook parameter in settings.json.
field can be: 'timeout', 'async'
"""
settings = read_settings()
hooks_config = settings.get("hooks", {})
if event not in hooks_config:
raise KeyError(f"Event '{event}' not found in hooks config")
if group_index >= len(hooks_config[event]):
raise IndexError(f"Group index {group_index} out of range")
group = hooks_config[event][group_index]
for hook in group.get("hooks", []):
if field == "timeout":
hook["timeout"] = value
elif field == "async":
if value:
hook["async"] = True
elif "async" in hook:
del hook["async"]
write_settings(settings)
return settings
def read_coaching_rate_limit() -> int:
"""Read RATE_LIMIT_SECONDS from coach-git-test.sh."""
if not COACH_SCRIPT.exists():
return 300
with COACH_SCRIPT.open() as f:
for line in f:
if line.strip().startswith("RATE_LIMIT_SECONDS="):
try:
return int(line.split("=", 1)[1].strip().split("#")[0].strip())
except ValueError:
return 300
return 300
def write_coaching_rate_limit(seconds: int) -> None:
"""Update RATE_LIMIT_SECONDS in coach-git-test.sh."""
if not COACH_SCRIPT.exists():
return
lines = COACH_SCRIPT.read_text().splitlines(keepends=True)
new_lines = []
for line in lines:
if line.strip().startswith("RATE_LIMIT_SECONDS="):
new_lines.append(f"RATE_LIMIT_SECONDS={seconds} # {seconds // 60} minutes\n")
else:
new_lines.append(line)
# Atomic write
fd, tmp = tempfile.mkstemp(dir=COACH_SCRIPT.parent, suffix=".tmp")
try:
with os.fdopen(fd, "w") as f:
f.writelines(new_lines)
os.replace(tmp, COACH_SCRIPT)
except Exception:
if os.path.exists(tmp):
os.unlink(tmp)
raise