# Adding New Plugins This guide shows how to add a new plugin to Penelope. Plugins are auto-discovered and exposed as API endpoints with zero configuration. ## Quick Start 1. Create folder: `plugins/your_plugin/` 2. Add file: `plugin.py` with a class ending in `Plugin` 3. Restart server 4. Done. Plugin available at: `/api/v1//plugin/your-plugin` ## How Plugins Are Discovered On server startup, `PluginManager` scans the `plugins/` directory: ```python # lib/plugin_loader.py plugins/ your_plugin/ # 1. Find subdirectories plugin.py # 2. Look for plugin.py # 3. Import module dynamically # 4. Find classes ending with "Plugin" # 5. Check: has metadata + execute() # 6. Instantiate and register # 7. Create route: /api/v1//plugin/ ``` **Requirements for discovery:** - File must be named `plugin.py` - Must contain a class ending with `Plugin` (e.g., `WikipediaPlugin`, `FormFillerPlugin`) - Class must have `metadata` attribute (PluginMetadata instance) - Class must have `execute()` async method **Example discovery log:** ``` ✅ Loaded plugin: wikipedia-search from wikipedia_search ✅ Loaded plugin: form-filler from form_filler 📡 Registered route: ['POST'] /api/v1//plugin/wikipedia-search 📡 Registered route: ['POST'] /api/v1//plugin/form-filler 🔌 Loaded 2 plugins ``` ## Plugin Template ```python from dataclasses import dataclass from typing import Dict, Any from lib.plugin_validator import PluginMetadata, PluginParameter class YourPlugin: """Brief description of what this plugin does.""" metadata = PluginMetadata( name="your-plugin", # URL-safe name description="Button Label", # Shown in UI methods=["POST"], # HTTP methods (default: POST) bg_color=["#4CAF50"], # Optional: custom button color or gradient parameters=[ PluginParameter( name="field_name", type="text", # text, url, number, checkbox, textarea, select label="Display Label", required=True, placeholder="Enter value", help_text="Description" ) ] ) def validate_params(self, params: Dict[str, Any]) -> str | None: """Optional custom validation. Return error message or None.""" return None async def execute(self, browser_navigator, page_id: str, params: Dict[str, Any]) -> Dict[str, Any]: """ Plugin implementation. Args: browser_navigator: Access to browser and primitives page_id: Page to operate on params: Validated request parameters Returns: Dict with "success": bool and your data """ managed_page = await browser_navigator._get_page_to_use(page_id) try: await managed_page.set_busy("doing something") # Your implementation here return {"success": True, "data": "result"} except Exception as e: await managed_page.set_error(str(e)) raise finally: await managed_page.set_idle() ``` ## File Structure ``` plugins/ your_plugin/ # Any folder name plugin.py # Must be "plugin.py" # Must contain *Plugin class # Class must have: metadata, execute() ``` ## Parameter Types | Type | UI Input | Example Use | |------|----------|-------------| | `text` | Text input | Names, queries | | `url` | URL input | Web addresses | | `number` | Number input | Counts, delays | | `checkbox` | Checkbox | Boolean flags | | `textarea` | Text area | Long text, JSON | | `select` | Dropdown | Predefined choices | ## Custom Background Color/Gradient Plugins can specify custom background colors or gradients for their buttons: ```python # Single color (solid background) bg_color=["#ff6600"] # Gradient (left-to-right) bg_color=["#4CAF50", "#2E7D32"] bg_color=["#667eea", "#764ba2", "#f093fb"] # Multi-stop gradient # Any CSS color format bg_color=["rgb(255, 102, 0)", "#ff8800"] bg_color=["orange", "darkorange"] ``` If not specified, default button styling is used. ## Using Primitives Primitives provide human-like browser interactions to avoid bot detection: ```python from lib.primitive.click_element import ClickElement from lib.primitive.type_text import TypeText from lib.primitive.paste_text import PasteText # In execute() clicker = ClickElement() typer = TypeText() # Human-like click with Bézier curve mouse movement await clicker.click_selector(managed_page, '#button', slowmo=100) # Human-like typing with natural rhythm and typos await typer.type(managed_page.page, "text", base_delay=50) ``` **Available primitives:** ClickElement, TypeText, PasteText, PressEnter, PressTab, ScrollPage, ZoomPage ## Complete Example ```python from typing import Dict, Any from lib.plugin_validator import PluginMetadata, PluginParameter from lib.primitive.click_element import ClickElement from lib.primitive.type_text import TypeText class WikipediaSearchPlugin: """Search Wikipedia for a term.""" metadata = PluginMetadata( name="wikipedia-search", description="Search Wikipedia", parameters=[ PluginParameter( name="query", type="text", label="Search Term", required=True, placeholder="Enter search term" ) ] ) async def execute(self, browser_navigator, page_id: str, params: Dict[str, Any]) -> Dict[str, Any]: query = params['query'] managed_page = await browser_navigator._get_page_to_use(page_id) try: await managed_page.set_busy(f"searching wikipedia for {query}") await managed_page.page.goto("https://en.wikipedia.org") clicker = ClickElement() typer = TypeText() await clicker.click_selector(managed_page, 'input#searchInput', slowmo=100) await typer.type(managed_page.page, query, base_delay=50) await managed_page.page.press('input#searchInput', 'Enter') await managed_page.page.wait_for_load_state('networkidle') return {"success": True, "query": query, "url": managed_page.page.url} except Exception as e: await managed_page.set_error(str(e)) raise finally: await managed_page.set_idle() ``` ## Request Flow When a plugin is called, here's what happens: ``` 1. Client sends: POST /api/v1/page_1/plugin/wikipedia-search Body: {"query": "Python"} 2. @require_api_key decorator validates API key 3. PluginManager validates parameters against metadata.parameters schema 4. plugin.execute() is called with: - browser_navigator: Access to browser - page_id: "page_1" - params: {"query": "Python"} 5. Plugin returns: {"success": true, "data": "..."} 6. Server responds with JSON ``` ## Testing Your Plugin ```bash # 1. Create plugin mkdir -p plugins/wikipedia_search # Add plugin.py # 2. Restart server python server.py # 3. Test endpoint curl -X POST http://localhost:8888/api/v1/page_1/plugin/wikipedia-search \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "Python programming"}' # 4. Check UI config curl http://localhost:8888/api/v1/plugins/ui-config ``` ## Best Practices - **Atomic**: One focused operation per plugin - **Self-contained**: All logic in single file - **State management**: Always use set_busy/set_idle/set_error - **Error handling**: Wrap in try/except/finally - **Return format**: Always include `"success": bool` - **Human-like**: Use primitives to avoid bot detection when needed