Download plugins/hookify/core/config_loader.py from SaylorTwift/claude-code: direct link, hf CLI and curl.
- Browser
- Download file 9.69 kB
-
https://huggingface.co/SaylorTwift/claude-code/resolve/main/plugins/hookify/core/config_loader.py
- Command line
-
hf download hf://SaylorTwift/claude-code/plugins/hookify/core/config_loader.py
-
curl -L -o config_loader.py https://huggingface.co/SaylorTwift/claude-code/resolve/main/plugins/hookify/core/config_loader.py
9.69 kB
| #!/usr/bin/env python3 | |
| """Configuration loader for hookify plugin. | |
| Loads and parses .claude/hookify.*.local.md files. | |
| """ | |
| import os | |
| import sys | |
| import glob | |
| import re | |
| from typing import List, Optional, Dict, Any | |
| from dataclasses import dataclass, field | |
| class Condition: | |
| """A single condition for matching.""" | |
| field: str # "command", "new_text", "old_text", "file_path", etc. | |
| operator: str # "regex_match", "contains", "equals", etc. | |
| pattern: str # Pattern to match | |
| def from_dict(cls, data: Dict[str, Any]) -> 'Condition': | |
| """Create Condition from dict.""" | |
| return cls( | |
| field=data.get('field', ''), | |
| operator=data.get('operator', 'regex_match'), | |
| pattern=data.get('pattern', '') | |
| ) | |
| class Rule: | |
| """A hookify rule.""" | |
| name: str | |
| enabled: bool | |
| event: str # "bash", "file", "stop", "all", etc. | |
| pattern: Optional[str] = None # Simple pattern (legacy) | |
| conditions: List[Condition] = field(default_factory=list) | |
| action: str = "warn" # "warn" or "block" (future) | |
| tool_matcher: Optional[str] = None # Override tool matching | |
| message: str = "" # Message body from markdown | |
| def from_dict(cls, frontmatter: Dict[str, Any], message: str) -> 'Rule': | |
| """Create Rule from frontmatter dict and message body.""" | |
| # Handle both simple pattern and complex conditions | |
| conditions = [] | |
| # New style: explicit conditions list | |
| if 'conditions' in frontmatter: | |
| cond_list = frontmatter['conditions'] | |
| if isinstance(cond_list, list): | |
| conditions = [Condition.from_dict(c) for c in cond_list] | |
| # Legacy style: simple pattern field | |
| simple_pattern = frontmatter.get('pattern') | |
| if simple_pattern and not conditions: | |
| # Convert simple pattern to condition | |
| # Infer field from event | |
| event = frontmatter.get('event', 'all') | |
| if event == 'bash': | |
| field = 'command' | |
| elif event == 'file': | |
| field = 'new_text' | |
| else: | |
| field = 'content' | |
| conditions = [Condition( | |
| field=field, | |
| operator='regex_match', | |
| pattern=simple_pattern | |
| )] | |
| return cls( | |
| name=frontmatter.get('name', 'unnamed'), | |
| enabled=frontmatter.get('enabled', True), | |
| event=frontmatter.get('event', 'all'), | |
| pattern=simple_pattern, | |
| conditions=conditions, | |
| action=frontmatter.get('action', 'warn'), | |
| tool_matcher=frontmatter.get('tool_matcher'), | |
| message=message.strip() | |
| ) | |
| def extract_frontmatter(content: str) -> tuple[Dict[str, Any], str]: | |
| """Extract YAML frontmatter and message body from markdown. | |
| Returns (frontmatter_dict, message_body). | |
| Supports multi-line dictionary items in lists by preserving indentation. | |
| """ | |
| if not content.startswith('---'): | |
| return {}, content | |
| # Split on --- markers | |
| parts = content.split('---', 2) | |
| if len(parts) < 3: | |
| return {}, content | |
| frontmatter_text = parts[1] | |
| message = parts[2].strip() | |
| # Simple YAML parser that handles indented list items | |
| frontmatter = {} | |
| lines = frontmatter_text.split('\n') | |
| current_key = None | |
| current_list = [] | |
| current_dict = {} | |
| in_list = False | |
| in_dict_item = False | |
| for line in lines: | |
| # Skip empty lines and comments | |
| stripped = line.strip() | |
| if not stripped or stripped.startswith('#'): | |
| continue | |
| # Check indentation level | |
| indent = len(line) - len(line.lstrip()) | |
| # Top-level key (no indentation or minimal) | |
| if indent == 0 and ':' in line and not line.strip().startswith('-'): | |
| # Save previous list/dict if any | |
| if in_list and current_key: | |
| if in_dict_item and current_dict: | |
| current_list.append(current_dict) | |
| current_dict = {} | |
| frontmatter[current_key] = current_list | |
| in_list = False | |
| in_dict_item = False | |
| current_list = [] | |
| key, value = line.split(':', 1) | |
| key = key.strip() | |
| value = value.strip() | |
| if not value: | |
| # Empty value - list or nested structure follows | |
| current_key = key | |
| in_list = True | |
| current_list = [] | |
| else: | |
| # Simple key-value pair | |
| value = value.strip('"').strip("'") | |
| if value.lower() == 'true': | |
| value = True | |
| elif value.lower() == 'false': | |
| value = False | |
| frontmatter[key] = value | |
| # List item (starts with -) | |
| elif stripped.startswith('-') and in_list: | |
| # Save previous dict item if any | |
| if in_dict_item and current_dict: | |
| current_list.append(current_dict) | |
| current_dict = {} | |
| item_text = stripped[1:].strip() | |
| # Check if this is an inline dict (key: value on same line) | |
| if ':' in item_text and ',' in item_text: | |
| # Inline comma-separated dict: "- field: command, operator: regex_match" | |
| item_dict = {} | |
| for part in item_text.split(','): | |
| if ':' in part: | |
| k, v = part.split(':', 1) | |
| item_dict[k.strip()] = v.strip().strip('"').strip("'") | |
| current_list.append(item_dict) | |
| in_dict_item = False | |
| elif ':' in item_text: | |
| # Start of multi-line dict item: "- field: command" | |
| in_dict_item = True | |
| k, v = item_text.split(':', 1) | |
| current_dict = {k.strip(): v.strip().strip('"').strip("'")} | |
| else: | |
| # Simple list item | |
| current_list.append(item_text.strip('"').strip("'")) | |
| in_dict_item = False | |
| # Continuation of dict item (indented under list item) | |
| elif indent > 2 and in_dict_item and ':' in line: | |
| # This is a field of the current dict item | |
| k, v = stripped.split(':', 1) | |
| current_dict[k.strip()] = v.strip().strip('"').strip("'") | |
| # Save final list/dict if any | |
| if in_list and current_key: | |
| if in_dict_item and current_dict: | |
| current_list.append(current_dict) | |
| frontmatter[current_key] = current_list | |
| return frontmatter, message | |
| def load_rules(event: Optional[str] = None) -> List[Rule]: | |
| """Load all hookify rules from .claude directory. | |
| Args: | |
| event: Optional event filter ("bash", "file", "stop", etc.) | |
| Returns: | |
| List of enabled Rule objects matching the event. | |
| """ | |
| rules = [] | |
| # Find all hookify.*.local.md files | |
| pattern = os.path.join('.claude', 'hookify.*.local.md') | |
| files = glob.glob(pattern) | |
| for file_path in files: | |
| try: | |
| rule = load_rule_file(file_path) | |
| if not rule: | |
| continue | |
| # Filter by event if specified | |
| if event: | |
| if rule.event != 'all' and rule.event != event: | |
| continue | |
| # Only include enabled rules | |
| if rule.enabled: | |
| rules.append(rule) | |
| except (IOError, OSError, PermissionError) as e: | |
| # File I/O errors - log and continue | |
| print(f"Warning: Failed to read {file_path}: {e}", file=sys.stderr) | |
| continue | |
| except (ValueError, KeyError, AttributeError, TypeError) as e: | |
| # Parsing errors - log and continue | |
| print(f"Warning: Failed to parse {file_path}: {e}", file=sys.stderr) | |
| continue | |
| except Exception as e: | |
| # Unexpected errors - log with type details | |
| print(f"Warning: Unexpected error loading {file_path} ({type(e).__name__}): {e}", file=sys.stderr) | |
| continue | |
| return rules | |
| def load_rule_file(file_path: str) -> Optional[Rule]: | |
| """Load a single rule file. | |
| Returns: | |
| Rule object or None if file is invalid. | |
| """ | |
| try: | |
| with open(file_path, 'r') as f: | |
| content = f.read() | |
| frontmatter, message = extract_frontmatter(content) | |
| if not frontmatter: | |
| print(f"Warning: {file_path} missing YAML frontmatter (must start with ---)", file=sys.stderr) | |
| return None | |
| rule = Rule.from_dict(frontmatter, message) | |
| return rule | |
| except (IOError, OSError, PermissionError) as e: | |
| print(f"Error: Cannot read {file_path}: {e}", file=sys.stderr) | |
| return None | |
| except (ValueError, KeyError, AttributeError, TypeError) as e: | |
| print(f"Error: Malformed rule file {file_path}: {e}", file=sys.stderr) | |
| return None | |
| except UnicodeDecodeError as e: | |
| print(f"Error: Invalid encoding in {file_path}: {e}", file=sys.stderr) | |
| return None | |
| except Exception as e: | |
| print(f"Error: Unexpected error parsing {file_path} ({type(e).__name__}): {e}", file=sys.stderr) | |
| return None | |
| # For testing | |
| if __name__ == '__main__': | |
| import sys | |
| # Test frontmatter parsing | |
| test_content = """--- | |
| name: test-rule | |
| enabled: true | |
| event: bash | |
| pattern: "rm -rf" | |
| --- | |
| ⚠️ Dangerous command detected! | |
| """ | |
| fm, msg = extract_frontmatter(test_content) | |
| print("Frontmatter:", fm) | |
| print("Message:", msg) | |
| rule = Rule.from_dict(fm, msg) | |
| print("Rule:", rule) | |