--- name: Klytos Plugin Development description: Complete guide for developing Klytos CMS plugins including structure, entry points, MCP tools, admin pages, hooks, routes, and best practices. Use when creating, modifying, extending Klytos functionality, adding MCP tools, admin pages, hooks, filters, or debugging plugins. --- # Klytos Plugin Development Guide ## Architecture Overview Klytos is an **AI-First CMS** controlled via MCP (Model Context Protocol). Plugins extend core functionality through a WordPress-inspired hook system (actions + filters) WITHOUT modifying core files. **Key principle**: Every feature should be exposed as an MCP tool FIRST, admin UI second. --- ## Plugin Identification (IMMUTABLE CONTRACT) A Klytos plugin is identified by a directory `plugins/{plugin-id}/` containing a PHP file named `{plugin-id}.php` with a `Plugin Name:` header in its docblock. This contract can NEVER change. ### Minimum Viable Plugin ```php 'my-plugin', 'title' => 'My Plugin', 'url' => klytos_admin_url('plugins/my-plugin/admin/settings.php'), 'icon' => 'P', 'position' => 86, ]; return $items; }); // 2. Register MCP tools klytos_add_filter('mcp.tools_list', function (array $tools): array { $tools[] = [ 'name' => 'my_plugin_do_something', 'description' => 'Does something useful.', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'param1' => ['type' => 'string', 'description' => 'First parameter.'], ], ], ]; return $tools; }); // 3. Handle MCP tool calls klytos_add_filter('mcp.handle_tool', function (mixed $result, string $toolName, array $params): mixed { if ($toolName !== 'my_plugin_do_something') { return $result; } return [ 'content' => [['type' => 'text', 'text' => 'Done!']], 'isError' => false, ]; }, 10); // 4. Register translations klytos_register_translations('my-plugin', klytos_plugin_path('my-plugin', 'lang')); // 5. Hook into page lifecycle klytos_add_action('page.after_save', function (array $page, string $action): void { klytos_log('info', 'My plugin: page saved', ['slug' => $page['slug']]); }); ``` --- ## Registering Admin Pages Use `klytos_register_admin_page()` to add sidebar items: ```php klytos_register_admin_page( 'my-plugin', [ 'id' => 'settings', 'title' => 'My Plugin Settings', 'icon' => 'P', 'position' => 86, 'capability' => 'plugins.manage', ] ); ``` The PHP file at `plugins/my-plugin/admin/settings.php` renders inside the admin layout automatically. It receives `$app`, `$auth`, `$pluginId`, `$pageName`, `$manifest`. --- ## Core Service Accessors ```php klytos_storage() → StorageInterface (read/write encrypted data) klytos_app() → App instance klytos_auth() → Auth instance klytos_config($key, $default) → Read config value (dot notation) klytos_version() → Current Klytos version klytos_is_admin() → True if in admin context klytos_is_mcp() → True if in MCP context klytos_current_user() → Current user array or null klytos_has_permission($perm) → Permission check klytos_log($level, $msg, $ctx) → Write to log file ``` --- ## Available Hooks ### Page Lifecycle - `page.before_save`, `page.after_save`, `page.before_delete`, `page.after_delete` - `page.content` (filter) — modify page HTML content ### Build Lifecycle - `build.before`, `build.after`, `build.page.before`, `build.page.after` (actions) - `build.head_html`, `build.body_end_html` (filters) — inject CSS/JS ### Admin Panel - `admin.sidebar_items` (filter) — add menu items - `admin.head`, `admin.footer` (actions) — inject into admin HTML - `admin.{page}.before`, `admin.{page}.after` (actions) — per-page hooks ### Blocks & Templates - `block.before_save`, `block.after_save`, `block.rendered_html` - `page_template.before_save`, `page_template.after_save` ### Plugins - `plugin.activated`, `plugin.deactivated`, `plugin.loaded` --- ## Internationalization (i18n) Place JSON translation files in `plugins/{plugin-id}/lang/`: ``` plugins/my-plugin/lang/ ├── en.json └── es.json ``` Register them: ```php klytos_register_translations('my-plugin', klytos_plugin_path('my-plugin', 'lang')); ``` Translation file format (flat recommended): ```json { "my_plugin.settings_title": "My Plugin Settings", "my_plugin.save": "Save Changes" } ``` Use translations: ```php echo __('my_plugin.settings_title'); // "My Plugin Settings" echo __('my_plugin.greeting', ['name' => 'Jose']); ``` --- ## Plugin Assets (CSS, JS, Images) Plugin static assets live in `plugins/{plugin-id}/assets/` and are publicly accessible via the web. ### Building Asset URLs ```php // CORRECT — full path from plugin root: klytos_plugin_url('my-plugin', 'assets/css/style.css') // → /admin/plugins/my-plugin/assets/css/style.css // Loading in admin pages: $cssUrl = klytos_plugin_url('my-plugin', 'assets/css/style.css'); ?> ``` ### CRITICAL: CSP Nonce Requirement All ` ``` --- ## Plugin Logging Plugins opt into logging by declaring `Logs: true` in the PHP header. When declared, an "Enable Logs" action appears in the plugin management page. ```php /** * Plugin Name: My Plugin * Logs: true */ ``` Writing logs: ```php klytos_log('info', 'Order processed', ['order_id' => 42], 'my-plugin'); klytos_log_error('Payment failed', ['gateway' => 'stripe'], 'my-plugin'); klytos_log_warning('Rate limit approaching', [], 'my-plugin'); klytos_log_info('Cache refreshed', [], 'my-plugin'); ``` --- ## Storage Pattern for Plugin Data ```php // Read/write plugin-specific data $storage = klytos_storage(); // Write plugin data to its own collection $storage->write('my-plugin-data', 'settings', [ 'api_key' => 'xxx', 'enabled' => true, ]); // Read it back $data = $storage->read('my-plugin-data', 'settings'); ``` --- ## Security Requirements 1. **Never access the filesystem directly** — use `klytos_storage()` 2. **Always sanitize HTML output** — use `htmlspecialchars()` or `Helpers::sanitizeHtml()` 3. **Always validate input** — check types, lengths, and formats 4. **Use capabilities for access control** — register via `auth.capabilities` filter 5. **Never store secrets in cleartext** — declare sensitivity with `klytos_register_option()` 6. **Include the GPL-3.0-or-later license header** in all PHP files if distributing ### Declaring Option Sensitivity When your plugin stores options, classify them by sensitivity so Klytos encrypts them appropriately based on the site's encryption level: ```php // In your plugin's main file ({plugin-id}.php): // API keys and secrets — ALWAYS encrypted klytos_register_option('my-plugin.api_key', true); klytos_register_option('my-plugin.webhook_secret', true); // Personal/GDPR data — encrypted from 'medium' level klytos_register_option('my-plugin.user_email', 'user_data'); // Non-sensitive settings — only encrypted at 'professional' level klytos_register_option('my-plugin.theme_color'); // false is the default ``` | Sensitivity | Encrypted at | Use for | |---|---|---| | `true` | Always (all levels) | API keys, tokens, passwords, secrets | | `'user_data'` | Medium + Professional | Emails, IPs, personal data (GDPR) | | `false` (default) | Professional only | Colors, toggles, non-sensitive config | See the **klytos-options-storage** skill for full documentation. --- ## Troubleshooting ### Error: "Requires Klytos X+, current: X-beta.Y" In semver, pre-release versions are lower than the release: - `0.14.0-beta.4 < 0.14.0` Always set `Requires Klytos` to the OLDEST version you actually need, not the current one. ```php /** * Plugin Name: My Plugin * Requires Klytos: 0.13.0 ← Use the last stable, not the current beta */ ``` ### Error: Plugin assets return 403 (Forbidden) The `.htaccess` in the `plugins/` directory blocks access to executable files. Plugin PHP files are only executed server-side by the PluginLoader (`require_once`), never accessed directly via URL. ### Error: Plugin JS blocked by Content-Security-Policy The `