feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
251
dexto/packages/agent-management/src/plugins/validate-plugin.ts
Normal file
251
dexto/packages/agent-management/src/plugins/validate-plugin.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Plugin Validation
|
||||
*
|
||||
* Validates plugin directory structure and manifest.
|
||||
* Checks for required files, valid JSON, and schema compliance.
|
||||
*
|
||||
* Supports two plugin formats:
|
||||
* - .claude-plugin/plugin.json: Claude Code compatible format
|
||||
* - .dexto-plugin/plugin.json: Dexto-native format with extended features (preferred)
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { existsSync, readFileSync, readdirSync } from 'fs';
|
||||
import { PluginManifestSchema, DextoPluginManifestSchema } from './schemas.js';
|
||||
import type {
|
||||
PluginValidationResult,
|
||||
PluginManifest,
|
||||
DextoPluginManifest,
|
||||
PluginFormat,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Extended validation result with plugin format
|
||||
*/
|
||||
export interface ExtendedPluginValidationResult extends PluginValidationResult {
|
||||
/** Plugin format detected */
|
||||
format?: PluginFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a plugin directory structure and manifest.
|
||||
*
|
||||
* Checks:
|
||||
* 1. Directory exists
|
||||
* 2. .dexto-plugin/plugin.json OR .claude-plugin/plugin.json exists (Dexto format preferred)
|
||||
* 3. plugin.json is valid JSON
|
||||
* 4. plugin.json matches schema (name is required)
|
||||
* 5. At least one command or skill exists (warning if none)
|
||||
*
|
||||
* @param pluginPath Absolute or relative path to plugin directory
|
||||
* @returns Validation result with manifest (if valid), errors, and warnings
|
||||
*/
|
||||
export function validatePluginDirectory(pluginPath: string): ExtendedPluginValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
let manifest: PluginManifest | DextoPluginManifest | undefined;
|
||||
let format: PluginFormat | undefined;
|
||||
|
||||
// Resolve to absolute path
|
||||
const absolutePath = path.isAbsolute(pluginPath) ? pluginPath : path.resolve(pluginPath);
|
||||
|
||||
// Check directory exists
|
||||
if (!existsSync(absolutePath)) {
|
||||
errors.push(`Directory does not exist: ${absolutePath}`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Check for plugin manifest (prefer .dexto-plugin over .claude-plugin)
|
||||
const dextoPluginDir = path.join(absolutePath, '.dexto-plugin');
|
||||
const claudePluginDir = path.join(absolutePath, '.claude-plugin');
|
||||
|
||||
let manifestPath: string;
|
||||
if (existsSync(dextoPluginDir)) {
|
||||
manifestPath = path.join(dextoPluginDir, 'plugin.json');
|
||||
format = 'dexto';
|
||||
} else if (existsSync(claudePluginDir)) {
|
||||
manifestPath = path.join(claudePluginDir, 'plugin.json');
|
||||
format = 'claude-code';
|
||||
} else {
|
||||
errors.push('Missing .dexto-plugin or .claude-plugin directory');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Check plugin.json exists
|
||||
if (!existsSync(manifestPath)) {
|
||||
errors.push(
|
||||
`Missing ${format === 'dexto' ? '.dexto-plugin' : '.claude-plugin'}/plugin.json`
|
||||
);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Parse and validate manifest
|
||||
try {
|
||||
const content = readFileSync(manifestPath, 'utf-8');
|
||||
let parsed: unknown;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
errors.push(
|
||||
`Invalid JSON in plugin.json: ${parseError instanceof Error ? parseError.message : String(parseError)}`
|
||||
);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Validate against appropriate schema
|
||||
const schema = format === 'dexto' ? DextoPluginManifestSchema : PluginManifestSchema;
|
||||
const result = schema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
|
||||
errors.push(`Schema validation failed: ${issues.join('; ')}`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
manifest = result.data;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Failed to read plugin.json: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Check for commands or skills (warning if none)
|
||||
const hasCommands = checkDirectoryHasFiles(path.join(absolutePath, 'commands'), '.md');
|
||||
const hasSkills = checkDirectoryHasSkills(path.join(absolutePath, 'skills'));
|
||||
|
||||
if (!hasCommands && !hasSkills) {
|
||||
warnings.push('Plugin has no commands or skills');
|
||||
}
|
||||
|
||||
// Check for unsupported features (warnings only)
|
||||
if (existsSync(path.join(absolutePath, 'hooks'))) {
|
||||
warnings.push('hooks/ directory found - hooks are not supported (security risk)');
|
||||
}
|
||||
|
||||
if (existsSync(path.join(absolutePath, '.lsp.json'))) {
|
||||
warnings.push('.lsp.json found - LSP configuration is not supported');
|
||||
}
|
||||
|
||||
// Check for MCP config
|
||||
const mcpPath = path.join(absolutePath, '.mcp.json');
|
||||
if (existsSync(mcpPath)) {
|
||||
try {
|
||||
const mcpContent = readFileSync(mcpPath, 'utf-8');
|
||||
JSON.parse(mcpContent);
|
||||
} catch {
|
||||
warnings.push('.mcp.json exists but contains invalid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
manifest,
|
||||
format,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a directory has files with a specific extension.
|
||||
*/
|
||||
function checkDirectoryHasFiles(dirPath: string, extension: string): boolean {
|
||||
if (!existsSync(dirPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||
return entries.some((entry) => entry.isFile() && entry.name.endsWith(extension));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a skills directory has valid SKILL.md files.
|
||||
* Skills are subdirectories containing SKILL.md.
|
||||
*/
|
||||
function checkDirectoryHasSkills(skillsDir: string): boolean {
|
||||
if (!existsSync(skillsDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
||||
return entries.some((entry) => {
|
||||
if (!entry.isDirectory()) return false;
|
||||
const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
|
||||
return existsSync(skillMdPath);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of manifest loading with format information
|
||||
*/
|
||||
export interface LoadedManifestResult {
|
||||
manifest: PluginManifest | DextoPluginManifest;
|
||||
format: PluginFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load and validate a plugin manifest from a directory.
|
||||
* Returns null if the manifest doesn't exist, is invalid JSON, or fails schema validation.
|
||||
*
|
||||
* Checks for .dexto-plugin first (preferred), then falls back to .claude-plugin.
|
||||
*
|
||||
* This is a shared utility used by discover-plugins, list-plugins, and import-plugin.
|
||||
*
|
||||
* @param pluginPath Absolute path to the plugin directory
|
||||
* @returns Validated manifest with format or null if not a valid plugin
|
||||
*/
|
||||
export function tryLoadManifest(pluginPath: string): PluginManifest | null;
|
||||
export function tryLoadManifest(
|
||||
pluginPath: string,
|
||||
returnFormat: true
|
||||
): LoadedManifestResult | null;
|
||||
export function tryLoadManifest(
|
||||
pluginPath: string,
|
||||
returnFormat?: boolean
|
||||
): PluginManifest | LoadedManifestResult | null {
|
||||
// Check for .dexto-plugin first (preferred), then .claude-plugin
|
||||
const dextoManifestPath = path.join(pluginPath, '.dexto-plugin', 'plugin.json');
|
||||
const claudeManifestPath = path.join(pluginPath, '.claude-plugin', 'plugin.json');
|
||||
|
||||
let manifestPath: string;
|
||||
let format: PluginFormat;
|
||||
|
||||
if (existsSync(dextoManifestPath)) {
|
||||
manifestPath = dextoManifestPath;
|
||||
format = 'dexto';
|
||||
} else if (existsSync(claudeManifestPath)) {
|
||||
manifestPath = claudeManifestPath;
|
||||
format = 'claude-code';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(manifestPath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
// Use appropriate schema based on format
|
||||
const schema = format === 'dexto' ? DextoPluginManifestSchema : PluginManifestSchema;
|
||||
const result = schema.safeParse(parsed);
|
||||
|
||||
if (!result.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (returnFormat) {
|
||||
return { manifest: result.data, format };
|
||||
}
|
||||
return result.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user