Download Sidecar/plugin_loader.py from smlflg/Sidecar-Workspace: direct link, hf CLI and curl.
- Browser
- Download file 3.66 kB
-
https://huggingface.co/smlflg/Sidecar-Workspace/resolve/main/Sidecar/plugin_loader.py
- Command line
-
hf download hf://smlflg/Sidecar-Workspace/Sidecar/plugin_loader.py
-
curl -L -o plugin_loader.py https://huggingface.co/smlflg/Sidecar-Workspace/resolve/main/Sidecar/plugin_loader.py
3.66 kB
| #!/usr/bin/env python3 | |
| """PluginLoader — Discovers and loads RulePlugin subclasses from a directory. | |
| Supports hot-reload: call load() again after SIGHUP to re-import all plugins. | |
| """ | |
| import importlib | |
| import importlib.util | |
| import os | |
| import sys | |
| import time | |
| # Import from sibling module | |
| try: | |
| from .rule_plugin import RulePlugin | |
| except ImportError: | |
| from rule_plugin import RulePlugin | |
| def _log(msg: str) -> None: | |
| """Minimal logger — same pattern as sidecar.py.""" | |
| try: | |
| with open("/tmp/claude-sidecar.log", "a") as f: | |
| f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [plugin_loader] {msg}\n") | |
| except Exception: | |
| pass | |
| class PluginLoader: | |
| """Loads RulePlugin subclasses from a directory of .py files.""" | |
| def __init__(self, rules_dir: str): | |
| self.rules_dir = rules_dir | |
| self.plugins: list[RulePlugin] = [] | |
| self._last_injection: str = "" # delta-dedup cache | |
| def load(self) -> list[RulePlugin]: | |
| """(Re)load all plugins from rules_dir. Returns list of instances.""" | |
| self.plugins = [] | |
| if not os.path.isdir(self.rules_dir): | |
| _log(f"Rules directory not found: {self.rules_dir}") | |
| return self.plugins | |
| for filename in sorted(os.listdir(self.rules_dir)): | |
| if not filename.endswith(".py") or filename.startswith("_"): | |
| continue | |
| filepath = os.path.join(self.rules_dir, filename) | |
| module_name = f"sidecar_rules_{filename[:-3]}" | |
| try: | |
| # Force re-import for hot-reload | |
| if module_name in sys.modules: | |
| del sys.modules[module_name] | |
| spec = importlib.util.spec_from_file_location(module_name, filepath) | |
| if spec is None or spec.loader is None: | |
| continue | |
| module = importlib.util.module_from_spec(spec) | |
| sys.modules[module_name] = module | |
| spec.loader.exec_module(module) | |
| # Find all RulePlugin subclasses in the module | |
| for attr_name in dir(module): | |
| attr = getattr(module, attr_name) | |
| if ( | |
| isinstance(attr, type) | |
| and issubclass(attr, RulePlugin) | |
| and attr is not RulePlugin | |
| ): | |
| instance = attr() | |
| self.plugins.append(instance) | |
| _log(f"Loaded plugin: {instance.name} from {filename}") | |
| except Exception as e: | |
| _log(f"Error loading plugin {filename}: {e}") | |
| _log(f"Total plugins loaded: {len(self.plugins)}") | |
| return self.plugins | |
| def match_rules(self, prompt: str, session_state: dict) -> str: | |
| """Run all plugins, return combined injection text for matching rules.""" | |
| injections = [] | |
| for plugin in self.plugins: | |
| try: | |
| if plugin.match(prompt, session_state): | |
| text = plugin.inject() | |
| if text: | |
| injections.append(text) | |
| _log(f"Rule fired: {plugin.name}") | |
| except Exception as e: | |
| _log(f"Error in plugin {plugin.name}.match/inject: {e}") | |
| result = "\n\n".join(injections) if injections else "" | |
| # Delta-dedup: suppress if identical to last injection | |
| if result and result == self._last_injection: | |
| _log(f"plugin-dedup: suppressed identical injection ({len(result)} chars)") | |
| return "" | |
| self._last_injection = result | |
| return result | |