.claude/skills/klytos-terminal/SKILL.md
Guide for developing and extending the Klytos web terminal. Use when modifying terminal commands, adding terminal commands from plugins, fixing terminal bugs, extending the pseudo-terminal, working with TerminalExecutor class, registering custom permissions, adding custom category labels, or managing terminal UI and security.
npx skillsauth add joseconti/klytos klytos-terminalInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Klytos terminal is a pseudo-terminal integrated into the admin panel that executes exclusively Klytos CLI commands from the browser. It is NOT a system shell -- it runs 100% in PHP without exec/shell_exec/proc_open.
terminal.access permission (owner role only).rate-limits storage collection.|, >, <, &, ;, `, $, etc.).klytos_log() with user_id, command, IP, result.| File | Purpose |
|------|---------|
| core/terminal-executor.php | TerminalExecutor class: command registry, parsing, execution, security |
| admin/terminal.php | Admin page with xterm.js UI |
| admin/api/terminal.php | POST endpoint for command execution |
| admin/api/terminal-autocomplete.php | GET endpoint for Tab completion |
| admin/api/terminal-revalidate.php | POST endpoint for 2FA revalidation |
| admin/assets/vendor/xterm/ | Bundled xterm.js + fit addon |
Location: installer/core/terminal-executor.php
Namespace: Klytos\Core
Two execution methods:
// Web terminal (with 2FA, rate limiting, history, audit log):
$executor = $app->getTerminalExecutor();
$result = $executor->execute('build', $userId);
// Returns: ['success' => bool, 'output' => string, 'command' => string, 'timestamp' => int, 'requires_2fa' => bool]
// CLI / direct dispatch (no web security layers):
$result = $executor->dispatch('build', ['my-page'], ['period' => '30d']);
// Returns: ['success' => bool, 'output' => string]
// Get command metadata (safe for JSON serialization, no handlers):
$meta = $executor->getCommandsMetadata();
// Returns: ['build' => ['description' => '...', 'usage' => '...', 'category' => 'build'], ...]
installer/cli.php is a thin adapter over TerminalExecutor. It boots the app, parses $argv, and calls $executor->dispatch(). Plugin commands registered via terminal.commands filter are automatically available in both the web terminal AND the CLI.
Command history is persisted to storage (collection terminal-history). On each successful execution, the command + truncated output is saved. History survives sessions.
$executor->getHistory( 50 ); // Last 50 entries (default)
$executor->getHistory( 0 ); // All entries (max 100 stored)
// Returns: [['command' => '...', 'output' => '...', 'timestamp' => int], ...]
| Command | Category | Permission | Description |
|---------|----------|------------|-------------|
| help [cmd] | general | null | Show help or command details |
| clear | general | null | Clear terminal screen |
| build | build | build.run | Rebuild entire static site |
| build:page <slug> | build | build.run | Rebuild a single page |
| pages [--status=all] | content | pages.view | List all pages |
| pages:count | content | pages.view | Count pages by status |
| tasks [--status=all] | content | tasks.manage | List tasks |
| tasks:count | content | tasks.manage | Count tasks by status |
| status | system | site.configure | System status report |
| version | system | null | Show Klytos version |
| cache:clear | system | site.configure | Clear rate-limit and cron caches |
| cron:run | system | site.configure | Run pending scheduled tasks |
| logs [--date=Y-m-d] [--lines=50] | system | site.configure | View system log entries |
| webhooks | system | site.configure | List configured webhooks |
| users | users | users.manage | List admin users |
| plugins | plugins | plugins.manage | List installed plugins |
| plugins:activate <id> | plugins | plugins.manage | Activate a plugin |
| plugins:deactivate <id> | plugins | plugins.manage | Deactivate a plugin |
| analytics [--period=7d] | system | analytics.view | Analytics summary |
| backup:create [--label=x] | backup | site.configure | Create a manual backup |
| backup:list | backup | site.configure | List available backups |
| backup:restore <name> | backup | site.configure | Restore a backup |
| update:check | update | site.configure | Check for Klytos updates |
| update:run | update | site.configure | Download and install update |
| config:get <key> | config | site.configure | Show a config value |
| config:set <key> <value> | config | site.configure | Set a config value |
If your plugin terminal commands need permissions that don't already exist in Klytos (e.g., orders.manage, shipping.configure), you MUST register them first via the auth.capabilities filter. Without this step, klytos_has_permission() will always return false for your custom permission and the command will be blocked.
// In your plugin's init.php -- register BEFORE terminal commands
klytos_add_filter( 'auth.capabilities', function ( array $capabilities ): array {
// Define which roles can use each permission.
$capabilities['orders.manage'] = [ 'owner', 'admin' ];
$capabilities['orders.view'] = [ 'owner', 'admin', 'editor' ];
$capabilities['shipping.configure'] = [ 'owner' ];
$capabilities['stock.manage'] = [ 'owner', 'admin' ];
return $capabilities;
} );
Existing permissions you can reuse (no need to register again):
pages.view, pages.create, pages.edit, pages.delete, build.run, assets.manage, tasks.create, tasks.manage, users.manage, mcp.manage, site.configure, plugins.manage, analytics.view, forms.manage, webhooks.manage, updates.manage, terminal.access.
// In your plugin's init.php
klytos_add_filter( 'terminal.commands', function ( array $commands ): array {
$commands['orders'] = [
'description' => 'List recent orders',
'usage' => 'orders [--limit=20]',
'category' => 'ecommerce',
'permission' => 'orders.view', // Uses custom permission registered above
'handler' => function ( array $args, array $flags ) {
$limit = (int) ( $flags['limit'] ?? 20 );
$orders = klytos_storage()->list( 'orders', [], $limit );
$output = "Last {$limit} orders:\n\n";
foreach ( $orders as $order ) {
$total = number_format( $order['total'] ?? 0, 2 );
$status = $order['status'] ?? 'pending';
$output .= " #{$order['order_number']} [{$status}] {$total}EUR\n";
}
return $output;
},
];
$commands['stock:update'] = [
'description' => 'Update product stock',
'usage' => 'stock:update <product-slug> <quantity>',
'category' => 'ecommerce',
'permission' => 'stock.manage', // Uses custom permission registered above
'handler' => function ( array $args, array $flags ) {
if ( count( $args ) < 2 ) {
return "Usage: stock:update <product-slug> <quantity>\nExample: stock:update blue-shirt 50";
}
$slug = $args[0];
$quantity = (int) $args[1];
$storage = klytos_storage();
$results = $storage->list( 'products', [ 'slug' => $slug ], 1 );
$product = $results[0] ?? null;
if ( ! $product ) {
return "Product not found: {$slug}";
}
$id = $product['_id'] ?? $product['id'] ?? '';
$product['stock'] = $quantity;
$storage->delete( 'products', (string) $id );
$storage->write( 'products', (string) $id, $product );
return "Stock for '{$product['title']}' updated to {$quantity} units.";
},
];
return $commands;
} );
klytos_add_action( 'klytos.init', function () {
$app = klytos_app();
if ( method_exists( $app, 'getTerminalExecutor' ) ) {
$app->getTerminalExecutor()->registerCommand( 'shipping:rates', [
'description' => 'Show configured shipping rates',
'usage' => 'shipping:rates',
'category' => 'ecommerce',
'permission' => 'shipping.configure', // Custom permission
'handler' => function ( array $args, array $flags ) {
return "Shipping rates:\n ...";
},
] );
}
} );
klytos_add_filter( 'terminal.category_labels', function ( array $labels ): array {
$labels['ecommerce'] = 'E-Commerce';
return $labels;
} );
// init.php of a plugin that adds terminal commands
// 1. Register custom permissions FIRST.
klytos_add_filter( 'auth.capabilities', function ( array $caps ): array {
$caps['orders.view'] = [ 'owner', 'admin', 'editor' ];
$caps['orders.manage'] = [ 'owner', 'admin' ];
return $caps;
} );
// 2. Register terminal commands.
klytos_add_filter( 'terminal.commands', function ( array $commands ): array {
$commands['orders'] = [
'description' => 'List orders',
'usage' => 'orders',
'category' => 'ecommerce',
'permission' => 'orders.view',
'handler' => fn() => 'Orders list here...',
];
return $commands;
} );
// 3. Register category label.
klytos_add_filter( 'terminal.category_labels', function ( array $labels ): array {
$labels['ecommerce'] = 'E-Commerce';
return $labels;
} );
function ( array $args, array $flags, TerminalExecutor $terminal ): string
$args: Positional arguments (e.g., build:page mi-pagina -> ['mi-pagina'])$flags: Named flags (e.g., --period=30d -> ['period' => '30d'])$terminal: The TerminalExecutor instanceadmin/assets/vendor/xterm/)X-CSRF-Token header with fetch to admin/api/terminal*.php__CLEAR__ return value triggers screen clear on frontendTerminal appears at position 95 in the system section, between Plugins (90) and Updates (98). Only shown when user has 2FA enabled AND terminal.access permission. Defined conditionally in admin/templates/sidebar.php.
| Hook | Type | Description |
|------|------|-------------|
| terminal.commands | filter | Modify the command registry (add/remove commands) |
| terminal.category_labels | filter | Add custom category labels for help output |
| auth.capabilities | filter | Register custom permissions for plugin commands (MANDATORY before using new permissions) |
When a plugin adds terminal commands with custom permissions (not existing ones), it MUST register those permissions via auth.capabilities BEFORE registering the commands via terminal.commands. Both filters run during init, but auth.capabilities is checked at command execution time by klytos_has_permission(). If the permission is not registered, the command will always return "No tienes permiso para ejecutar este comando."
development
Guide for working with dates, times, and timezones in Klytos CMS. Use when formatting dates, converting timezones, scheduling actions with timestamps, displaying local time, working with UTC storage, building timezone selectors, or using any klytos_date/klytos_gmdate/klytos_timezone functions.
development
--- name: klytos-site-builder description: Guide for building a complete website from scratch with Klytos CMS. Use when creating a new site, configuring a site after installation, setting up design/content/SEO/navigation, or when the user pastes the post-install prompt. Covers 9 phases: discovery, design reference, global config, theme, content structure, templates, content creation, additional features, and launch. --- # Klytos Site Builder ## Overview The Site Builder is a conversational AI
development
Use when creating or editing page content in Klytos CMS. Ensures every page has proper SEO structure, HTML semantics, meta tags, structured data, accessibility for maximum search engine visibility. Apply when writing page titles, descriptions, content, headings, images, internal links, JSON-LD schema, or following the SEO checklist before publishing pages.
development
Guide for SEO, sitemap.xml, llms.txt, and search engine and AI indexing in Klytos CMS. Use when asking about SEO, sitemap, robots.txt, llms.txt, meta tags, Open Graph, structured data, search engine optimization, AI indexing, page indexing, canonical URLs, hreflang tags, JSON-LD schema, or the build engine SEO process.