Download config_io.py from smlflg/ClaudeCodePanel: direct link, hf CLI and curl.
- Browser
- Download file 5.3 kB
-
https://huggingface.co/smlflg/ClaudeCodePanel/resolve/main/config_io.py
- Command line
-
hf download hf://smlflg/ClaudeCodePanel/config_io.py
-
curl -L -o config_io.py https://huggingface.co/smlflg/ClaudeCodePanel/resolve/main/config_io.py
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 | |