UNPKG

@toolprint/hypertool-mcp

Version:

HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers

1,262 lines • 54.1 kB
/**
 * PersonaManager Implementation
 *
 * This module implements the main PersonaManager class that orchestrates persona lifecycle,
 * activation/deactivation, and state management with event emission. The manager ensures
 * only one persona is active at a time and handles the complete activation workflow
 * including toolset application and MCP configuration integration.
 *
 * @fileoverview Main persona management orchestration with state and event handling
 */
import { EventEmitter } from "events";
import { createChildLogger } from "../utils/logging.js";
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import { PersonaEvents, PersonaErrorCode } from "./types.js";
import { PersonaLoader } from "./loader.js";
import { PersonaCache } from "./cache.js";
import { PersonaDiscovery } from "./discovery.js";
import { PersonaError, createPersonaNotFoundError, createActivationFailedError, createToolsetNotFoundError, isPersonaError, } from "./errors.js";
import { PersonaToolsetBridge } from "./toolset-bridge.js";
import { PersonaMcpIntegration, personaHasMcpConfig, } from "./mcp-integration.js";
import { tokenCounter } from "../server/tools/utils/token-counter.js";
/**
 * Main PersonaManager class for lifecycle orchestration
 *
 * Manages persona activation, deactivation, state tracking, and event emission.
 * Ensures only one persona is active at a time and provides comprehensive
 * state management with cleanup and restoration capabilities.
 */
export class PersonaManager extends EventEmitter {
    logger = createChildLogger({ module: "persona/manager" });
    loader;
    cache;
    discovery;
    toolsetBridge;
    mcpIntegration;
    config;
    activeState = null;
    discoveredPersonas = [];
    lastDiscovery = null;
    initializationPromise = null;
    constructor(config = {}) {
        super();
        // Apply default configuration
        this.config = {
            getToolDiscoveryEngine: config.getToolDiscoveryEngine,
            toolsetManager: config.toolsetManager,
            mcpConfigHandlers: config.mcpConfigHandlers,
            cacheConfig: config.cacheConfig || {},
            discoveryConfig: config.discoveryConfig || {},
            defaultLoadOptions: config.defaultLoadOptions || {},
            autoDiscover: config.autoDiscover ?? true,
            validateOnActivation: config.validateOnActivation ?? true,
            persistState: config.persistState ?? false,
            stateKey: config.stateKey ?? "hypertool-mcp-persona-state",
            bridgeOptions: config.bridgeOptions || {},
            mcpMergeOptions: config.mcpMergeOptions || {},
        };
        // Initialize components
        this.cache = new PersonaCache(this.config.cacheConfig);
        this.discovery = new PersonaDiscovery(this.config.cacheConfig);
        this.loader = new PersonaLoader(this.config.getToolDiscoveryEngine?.(), this.discovery);
        this.toolsetBridge = new PersonaToolsetBridge(this.config.getToolDiscoveryEngine, this.config.bridgeOptions);
        // Initialize MCP integration
        if (this.config.mcpConfigHandlers) {
            this.mcpIntegration = new PersonaMcpIntegration(this.config.mcpConfigHandlers.getCurrentConfig, this.config.mcpConfigHandlers.setCurrentConfig, this.config.mcpConfigHandlers.restartConnections, this.config.mcpMergeOptions);
        }
        else {
            // Create null integration for personas without MCP config handlers
            this.mcpIntegration = PersonaMcpIntegration.createNullIntegration();
        }
        this.setupEventHandling();
    }
    /**
     * Initialize the persona manager
     */
    async initialize() {
        if (this.initializationPromise) {
            return this.initializationPromise;
        }
        this.initializationPromise = this.performInitialization();
        return this.initializationPromise;
    }
    /**
     * Get MCP tools from the active persona's active toolset
     * This makes PersonaManager a ToolsProvider like ToolsetManager
     */
    getMcpTools() {
        // If no persona is active, return empty array
        if (!this.activeState) {
            return [];
        }
        // Get discovery engine to resolve tools
        const discoveryEngine = this.config.getToolDiscoveryEngine?.();
        if (!discoveryEngine) {
            return [];
        }
        // Get the active toolset name (or default toolset if none specified)
        const activePersona = this.activeState.persona;
        const activeToolsetName = this.activeState.activeToolset || activePersona.config.defaultToolset;
        // If no toolset is active, return empty array
        if (!activeToolsetName) {
            return [];
        }
        // Find the active toolset in the persona's toolsets
        const personaToolsets = activePersona.config.toolsets || [];
        const activeToolset = personaToolsets.find((ts) => ts.name === activeToolsetName);
        if (!activeToolset) {
            return [];
        }
        // Resolve tool IDs to actual Tool objects
        const tools = [];
        for (const toolId of activeToolset.toolIds) {
            // Parse the namespacedName format (e.g., "git.status")
            const parts = toolId.split(".");
            if (parts.length < 2) {
                continue; // Skip invalid tool IDs
            }
            const serverName = parts[0];
            const toolName = parts.slice(1).join("."); // Handle tools with dots in their names
            // Try to find the tool in the discovery engine
            const discoveredTool = discoveryEngine
                .getAvailableTools()
                .find((dt) => dt.serverName === serverName && dt.tool.name === toolName);
            if (discoveredTool) {
                // Return the tool in MCP format
                tools.push(discoveredTool.tool);
            }
        }
        return tools;
    }
    /**
     * Activate a persona by name with optional toolset selection
     */
    async activatePersona(personaName, options = {}) {
        try {
            await this.initialize();
            // Deactivate current persona if different
            if (this.activeState &&
                this.activeState.persona.config.name !== personaName) {
                await this.deactivatePersona({ silent: options.silent });
            }
            // Return early if same persona is already active
            if (this.activeState &&
                this.activeState.persona.config.name === personaName) {
                // Check if we need to switch toolsets
                if (options.toolsetName &&
                    this.activeState.activeToolset !== options.toolsetName) {
                    return this.switchToolset(options.toolsetName, options.silent);
                }
                return {
                    success: true,
                    personaName,
                    activatedToolset: this.activeState.activeToolset,
                };
            }
            // Find and load the persona
            const persona = await this.findAndLoadPersona(personaName);
            if (!persona) {
                throw createPersonaNotFoundError(personaName, [
                    "Persona not found in discovered personas",
                    "Try refreshing discovery or check persona name spelling",
                ]);
            }
            // Perform activation
            const result = await this.performActivation(persona, options);
            if (!options.silent) {
                this.emit(PersonaEvents.PERSONA_ACTIVATED, {
                    persona: persona.config,
                    toolset: result.activatedToolset,
                    timestamp: new Date(),
                    previousPersona: null,
                });
            }
            return result;
        }
        catch (error) {
            const personaError = isPersonaError(error)
                ? error
                : createActivationFailedError(personaName, error instanceof Error ? error.message : String(error));
            if (!options.silent) {
                this.emit(PersonaEvents.PERSONA_VALIDATION_FAILED, {
                    personaName,
                    error: personaError,
                    timestamp: new Date(),
                });
            }
            return {
                success: false,
                personaName,
                errors: [personaError.message],
            };
        }
    }
    /**
     * Deactivate the currently active persona
     */
    async deactivatePersona(options = {}) {
        if (!this.activeState) {
            return {
                success: true,
                personaName: "(none)",
                warnings: ["No persona is currently active"],
            };
        }
        const previousPersona = this.activeState.persona;
        const previousToolset = this.activeState.activeToolset;
        try {
            // Restore MCP configuration if it was applied
            if (this.activeState.metadata.mcpConfigApplied &&
                this.mcpIntegration.hasBackup()) {
                try {
                    await this.mcpIntegration.restoreOriginalConfig();
                }
                catch (error) {
                    this.logger.warn("Failed to restore MCP configuration during deactivation:", error);
                    // Don't fail the entire deactivation for this
                }
            }
            // Restore previous state if available
            if (this.activeState.previousState) {
                await this.restorePreviousState(this.activeState.previousState);
            }
            // Clear active state
            this.activeState = null;
            // Clear persisted state if enabled
            if (this.config.persistState) {
                await this.clearPersistedState();
            }
            if (!options.silent) {
                this.emit(PersonaEvents.PERSONA_DEACTIVATED, {
                    persona: previousPersona.config,
                    toolset: previousToolset,
                    timestamp: new Date(),
                });
            }
            return {
                success: true,
                personaName: previousPersona.config.name,
                activatedToolset: previousToolset,
            };
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return {
                success: false,
                personaName: previousPersona.config.name,
                errors: [`Failed to deactivate persona: ${errorMessage}`],
            };
        }
    }
    /**
     * Get currently active persona state
     */
    getActivePersona() {
        return this.activeState;
    }
    /**
     * Check if a persona is currently active
     */
    isPersonaActive(personaName) {
        if (!this.activeState) {
            return false;
        }
        if (personaName) {
            return this.activeState.persona.config.name === personaName;
        }
        return true;
    }
    /**
     * List available personas with filtering options
     */
    async listPersonas(options = {}) {
        try {
            await this.initialize();
            // Refresh discovery if requested
            if (options.refresh) {
                await this.refreshDiscovery();
            }
            let personas = [...this.discoveredPersonas];
            // Apply filters
            if (!options.includeInvalid) {
                personas = personas.filter((p) => p.isValid);
            }
            if (options.namePattern) {
                const pattern = new RegExp(options.namePattern, "i");
                personas = personas.filter((p) => pattern.test(p.name));
            }
            if (options.tags && options.tags.length > 0) {
                // Would need persona metadata loading for tag filtering
                // For now, skip this filter
            }
            // Apply sorting
            if (options.sortBy) {
                personas.sort((a, b) => {
                    let aVal;
                    let bVal;
                    switch (options.sortBy) {
                        case "name":
                            aVal = a.name;
                            bVal = b.name;
                            break;
                        default:
                            aVal = a.name;
                            bVal = b.name;
                    }
                    const comparison = aVal.localeCompare(bVal);
                    return options.sortDirection === "desc" ? -comparison : comparison;
                });
            }
            // Apply limit
            if (options.limit && options.limit > 0) {
                personas = personas.slice(0, options.limit);
            }
            return personas;
        }
        catch (error) {
            this.emit(PersonaEvents.PERSONA_VALIDATION_FAILED, {
                error: error,
                timestamp: new Date(),
            });
            return [];
        }
    }
    /**
     * Refresh persona discovery
     */
    async refreshDiscovery() {
        const result = await this.discovery.refreshDiscovery(this.config.discoveryConfig);
        this.discoveredPersonas = result.personas;
        this.lastDiscovery = new Date();
        this.emit(PersonaEvents.PERSONA_DISCOVERED, {
            count: result.personas.length,
            fromCache: false,
            timestamp: new Date(),
        });
        return result;
    }
    /**
     * Get discovery and manager statistics
     */
    getStats() {
        return {
            discovery: this.discovery.getDiscoveryStats(),
            cache: this.cache.getStats(),
            activePersona: this.activeState?.persona.config.name || null,
            discoveredCount: this.discoveredPersonas.length,
            lastDiscovery: this.lastDiscovery,
            bridge: this.toolsetBridge.getConfiguration(),
        };
    }
    /**
     * Dispose of the manager and clean up resources
     */
    async dispose() {
        // Deactivate current persona
        if (this.activeState) {
            await this.deactivatePersona({ silent: true });
        }
        // Clean up components
        this.cache.destroy();
        this.discovery.dispose();
        this.mcpIntegration.dispose();
        // Clear state
        this.discoveredPersonas = [];
        this.lastDiscovery = null;
        this.initializationPromise = null;
        // Remove all listeners
        this.removeAllListeners();
    }
    /**
     * Perform manager initialization
     */
    async performInitialization() {
        try {
            // Restore persisted state if enabled
            if (this.config.persistState) {
                await this.restorePersistedState();
            }
            // Auto-discover personas if enabled
            if (this.config.autoDiscover) {
                const result = await this.discovery.discoverPersonas(this.config.discoveryConfig);
                this.discoveredPersonas = result.personas;
                this.lastDiscovery = new Date();
                this.emit(PersonaEvents.PERSONA_DISCOVERED, {
                    count: result.personas.length,
                    fromCache: false,
                    timestamp: new Date(),
                });
            }
        }
        catch (error) {
            // Initialization errors should not prevent usage
            this.emit("error", error);
        }
    }
    /**
     * Find and load a persona by name
     */
    async findAndLoadPersona(personaName) {
        // Check cache first
        const cached = this.cache.get(personaName);
        if (cached) {
            return cached;
        }
        // Find persona reference
        const personaRef = this.discoveredPersonas.find((p) => p.name === personaName);
        if (!personaRef) {
            return null;
        }
        // Load persona
        const loadResult = await this.loader.loadPersonaFromReference(personaRef, this.config.defaultLoadOptions);
        if (!loadResult.success || !loadResult.persona) {
            throw new PersonaError(PersonaErrorCode.ACTIVATION_FAILED, `Failed to load persona "${personaName}": ${this.formatLoadErrors(loadResult.errors)}`, {
                details: { personaName, errors: loadResult.errors },
                recoverable: false,
            });
        }
        // Cache the loaded persona
        this.cache.set(loadResult.persona);
        return loadResult.persona;
    }
    /**
     * Perform persona activation workflow
     */
    async performActivation(persona, options) {
        const warnings = [];
        // Validate persona if required
        if (this.config.validateOnActivation && !options.force) {
            if (!persona.validation.isValid) {
                throw createActivationFailedError(persona.config.name, `Persona validation failed: ${persona.validation.errors.map((e) => e.message).join(", ")}`);
            }
            if (persona.validation.warnings.length > 0) {
                warnings.push(...persona.validation.warnings.map((w) => w.message));
            }
        }
        // Determine toolset to activate
        const toolsetName = options.toolsetName || persona.config.defaultToolset;
        let selectedToolset;
        if (toolsetName) {
            selectedToolset = persona.config.toolsets?.find((t) => t.name === toolsetName);
            if (!selectedToolset) {
                throw createToolsetNotFoundError(toolsetName, persona.config.toolsets?.map((t) => t.name) || []);
            }
        }
        // Backup current state if requested
        const previousState = options.backupState
            ? this.captureCurrentState()
            : undefined;
        // Apply toolset configuration (placeholder - would integrate with toolset system)
        let toolsResolved = 0;
        if (selectedToolset) {
            try {
                toolsResolved = await this.applyToolset(selectedToolset, persona);
            }
            catch (error) {
                warnings.push(`Some tools could not be resolved: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        // Note: MCP configuration is now handled during boot sequence phase 2
        // This ensures all MCP servers are collected and connected in a single pass
        let mcpConfigPresent = personaHasMcpConfig(persona.assets);
        if (mcpConfigPresent) {
            this.logger.info(`Persona "${persona.config.name}" has MCP configuration - servers were loaded during boot sequence`);
        }
        // Validate tool availability if we have a discovery engine and persona has MCP config
        // Note: MCP servers should already be connected via boot sequence if present
        const discoveryEngine = this.config.getToolDiscoveryEngine?.();
        if (mcpConfigPresent && discoveryEngine) {
            try {
                this.logger.info("MCP config applied, waiting for servers to connect before validating tools...");
                // Wait for servers to fully connect and tools to be discovered
                this.logger.debug("Waiting for tools to be discovered from MCP servers...");
                let retries = 0;
                let availableTools = [];
                while (retries < 20) {
                    // Max 10 seconds (20 * 500ms)
                    // Force a refresh of the discovery engine
                    this.logger.debug(`Tool discovery attempt ${retries + 1}/20`);
                    await discoveryEngine.refreshCache();
                    availableTools = discoveryEngine.getAvailableTools(true);
                    this.logger.debug(`Found ${availableTools.length} tools on attempt ${retries + 1}`);
                    if (availableTools.length > 0) {
                        this.logger.info(`Tools discovered successfully after ${retries + 1} attempts`);
                        break;
                    }
                    await new Promise((resolve) => setTimeout(resolve, 500));
                    retries++;
                }
                if (availableTools.length === 0) {
                    this.logger.warn("No tools discovered after waiting 10 seconds - continuing anyway");
                }
                this.logger.debug("Getting final available tools from discovery engine");
                // Get more detailed info about the discovery engine state
                this.logger.debug("Discovery engine details:", {
                    engineType: discoveryEngine.constructor.name,
                    hasGetStats: typeof discoveryEngine.getStats === "function",
                    stats: typeof discoveryEngine.getStats === "function"
                        ? discoveryEngine.getStats()
                        : "No stats method",
                });
                // Log all tools grouped by server for debugging
                const toolsByServer = {};
                availableTools.forEach((tool) => {
                    const serverName = tool.serverName || "unknown";
                    if (!toolsByServer[serverName])
                        toolsByServer[serverName] = [];
                    toolsByServer[serverName].push(tool.namespacedName);
                });
                this.logger.debug(`Available tools after MCP config: ${availableTools.length}`, {
                    toolsByServer,
                });
                const { PersonaValidator } = await import("./validator.js");
                const validator = new PersonaValidator(discoveryEngine);
                const toolValidationResult = await validator.validatePersonaConfig(persona.config, {
                    personaPath: persona.sourcePath,
                    checkToolAvailability: true,
                    validateMcpConfig: false, // Already validated
                    toolDiscoveryEngine: discoveryEngine,
                }, {
                    checkToolAvailability: true,
                    validateMcpConfig: false,
                    includeWarnings: true,
                });
                // Reapply toolset now that MCP servers are connected and tools are available
                if (selectedToolset) {
                    try {
                        this.logger.info(`Reapplying persona toolset '${selectedToolset.name}' with newly available tools...`);
                        const toolsReapplied = await this.applyToolset(selectedToolset, persona);
                        this.logger.info(`Successfully applied persona toolset '${selectedToolset.name}' with ${toolsReapplied} tools`);
                    }
                    catch (error) {
                        const errorMessage = `Failed to reapply toolset after MCP connection: ${error instanceof Error ? error.message : String(error)}`;
                        warnings.push(errorMessage);
                        this.logger.warn(errorMessage);
                    }
                }
                if (!toolValidationResult.isValid) {
                    const toolErrors = toolValidationResult.errors
                        .filter((e) => e.type === "tool-resolution")
                        .map((e) => e.message);
                    if (toolErrors.length > 0) {
                        // Convert tool resolution errors to warnings instead of failing activation
                        warnings.push(...toolErrors);
                        this.logger.warn("Some tools could not be resolved, but persona activation will continue", {
                            errors: toolErrors,
                        });
                    }
                }
                if (toolValidationResult.warnings.length > 0) {
                    warnings.push(...toolValidationResult.warnings.map((w) => w.message));
                }
            }
            catch (error) {
                if (error instanceof PersonaError) {
                    throw error;
                }
                const errorMessage = `Tool validation failed: ${error instanceof Error ? error.message : String(error)}`;
                warnings.push(errorMessage);
            }
        }
        // Create active state
        this.activeState = {
            persona,
            activeToolset: toolsetName,
            activatedAt: new Date(),
            previousState,
            metadata: {
                activationSource: "manual",
                validationPassed: persona.validation.isValid,
                toolsResolved,
                warnings,
                mcpConfigApplied: mcpConfigPresent, // MCP config was applied via boot sequence
                mcpConfigWarnings: mcpConfigPresent
                    ? ["MCP config applied during boot sequence"]
                    : undefined,
            },
        };
        // Persist state if enabled
        if (this.config.persistState) {
            await this.persistActiveState();
        }
        return {
            success: true,
            personaName: persona.config.name,
            activatedToolset: toolsetName,
            warnings: warnings.length > 0 ? warnings : undefined,
        };
    }
    /**
     * Switch active toolset within current persona
     */
    async switchToolset(toolsetName, silent) {
        if (!this.activeState) {
            throw new Error("No active persona to switch toolset for");
        }
        const persona = this.activeState.persona;
        const toolset = persona.config.toolsets?.find((t) => t.name === toolsetName);
        if (!toolset) {
            throw createToolsetNotFoundError(toolsetName, persona.config.toolsets?.map((t) => t.name) || []);
        }
        try {
            // Apply new toolset
            const toolsResolved = await this.applyToolset(toolset, persona);
            // Update active state
            this.activeState.activeToolset = toolsetName;
            this.activeState.metadata.toolsResolved = toolsResolved;
            if (!silent) {
                this.emit(PersonaEvents.PERSONA_TOOLSET_CHANGED, {
                    persona: persona.config,
                    previousToolset: this.activeState.activeToolset,
                    newToolset: toolsetName,
                    timestamp: new Date(),
                });
            }
            return {
                success: true,
                personaName: persona.config.name,
                activatedToolset: toolsetName,
            };
        }
        catch (error) {
            return {
                success: false,
                personaName: persona.config.name,
                errors: [
                    `Failed to switch toolset: ${error instanceof Error ? error.message : String(error)}`,
                ],
            };
        }
    }
    /**
     * Apply toolset configuration via the toolset bridge
     */
    async applyToolset(toolset, persona) {
        const personaName = persona.config.name;
        try {
            // Convert persona toolset to ToolsetConfig format using the bridge
            const conversionResult = await this.toolsetBridge.convertPersonaToolset(toolset, personaName);
            if (!conversionResult.success || !conversionResult.toolsetConfig) {
                throw new Error(`Failed to convert persona toolset: ${conversionResult.error}`);
            }
            // Apply the toolset through the toolset manager if available
            if (this.config.toolsetManager) {
                const validation = this.config.toolsetManager.setCurrentToolset(conversionResult.toolsetConfig);
                if (!validation.valid) {
                    throw new Error(`Invalid toolset configuration: ${validation.errors.join(", ")}`);
                }
                // Return the number of successfully resolved tools
                return (conversionResult.stats?.resolvedTools ||
                    conversionResult.toolsetConfig.tools.length);
            }
            else {
                // No toolset manager available - fall back to basic tool resolution count
                return conversionResult.stats?.resolvedTools || toolset.toolIds.length;
            }
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to apply persona toolset: ${errorMessage}`);
        }
    }
    /**
     * Capture current state for restoration
     */
    captureCurrentState() {
        // This would capture the current toolset and MCP configuration state
        // Placeholder implementation
        return {
            toolsetName: undefined,
            mcpConfig: undefined,
            customState: {},
        };
    }
    /**
     * Restore previous state
     */
    async restorePreviousState(previousState) {
        try {
            // Restore previous toolset if available
            if (previousState?.toolsetName && this.config.toolsetManager) {
                // Try to restore previous toolset (this is a simplified implementation)
                // In a full implementation, you'd need to restore the exact previous state
                await this.config.toolsetManager.unequipToolset();
            }
            else if (this.config.toolsetManager) {
                // No previous toolset, just unequip current one
                await this.config.toolsetManager.unequipToolset();
            }
            // Note: MCP configuration restoration is handled by the mcpIntegration.restoreOriginalConfig()
            // during deactivatePersona(), not here
        }
        catch (error) {
            // Log error but don't fail deactivation
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to restore previous state: ${errorMessage}`);
        }
    }
    /**
     * Persist active state for session restoration
     */
    async persistActiveState() {
        if (!this.activeState) {
            return;
        }
        try {
            const stateData = {
                personaName: this.activeState.persona.config.name,
                personaPath: this.activeState.persona.sourcePath,
                activeToolset: this.activeState.activeToolset,
                activatedAt: this.activeState.activatedAt.toISOString(),
                metadata: this.activeState.metadata,
            };
            const stateFilePath = this.getStateFilePath();
            await fs.mkdir(path.dirname(stateFilePath), { recursive: true });
            await fs.writeFile(stateFilePath, JSON.stringify(stateData, null, 2));
        }
        catch (error) {
            this.logger.debug("Failed to persist persona state", { error });
        }
    }
    /**
     * Restore persisted state
     */
    async restorePersistedState() {
        try {
            const stateFilePath = this.getStateFilePath();
            // Check if state file exists
            try {
                await fs.access(stateFilePath);
            }
            catch {
                // State file doesn't exist, nothing to restore
                return;
            }
            const stateJson = await fs.readFile(stateFilePath, "utf8");
            const stateData = JSON.parse(stateJson);
            this.logger.debug(`Restoring persisted persona: ${stateData.personaName}`);
            // Try to restore the persona
            await this.activatePersona(stateData.personaName, {
                toolsetName: stateData.activeToolset,
                silent: true,
            });
            // Update activation source
            if (this.activeState) {
                this.activeState.metadata.activationSource = "restored";
            }
        }
        catch (error) {
            this.logger.debug("Failed to restore persisted state", { error });
            // Clear invalid persisted state
            await this.clearPersistedState();
        }
    }
    /**
     * Clear persisted state
     */
    async clearPersistedState() {
        try {
            const stateFilePath = this.getStateFilePath();
            await fs.unlink(stateFilePath);
        }
        catch {
            // Ignore cleanup errors (file might not exist)
        }
    }
    /**
     * Get the file path for persisted state
     */
    getStateFilePath() {
        const stateDir = path.join(os.homedir(), ".toolprint", "hypertool-mcp");
        return path.join(stateDir, `${this.config.stateKey}.json`);
    }
    /**
     * Setup event handling between components
     */
    setupEventHandling() {
        // Forward cache events
        this.cache.on("cache:evicted", (event) => {
            if (this.activeState &&
                this.activeState.persona.config.name === event.name) {
                // Active persona was evicted, reload it
                this.findAndLoadPersona(event.name).catch(() => {
                    // If reload fails, deactivate the persona
                    this.deactivatePersona({ silent: true });
                });
            }
        });
        // Forward discovery events
        this.discovery.on(PersonaEvents.PERSONA_DISCOVERED, (event) => {
            this.emit(PersonaEvents.PERSONA_DISCOVERED, event);
        });
    }
    /**
     * Format load errors for better readability
     */
    formatLoadErrors(errors) {
        if (errors.length === 0) {
            return "Unknown error";
        }
        if (errors.length === 1) {
            return errors[0];
        }
        // Group similar errors (especially tool ID format errors)
        const errorGroups = new Map();
        for (const error of errors) {
            if (error.includes("Tool ID must follow namespacedName format")) {
                const key = "Tool ID format errors";
                if (!errorGroups.has(key)) {
                    errorGroups.set(key, { count: 0, details: new Set() });
                }
                errorGroups.get(key).count++;
            }
            else {
                // Other errors - group by exact message
                const key = error;
                if (!errorGroups.has(key)) {
                    errorGroups.set(key, { count: 1, details: new Set() });
                }
                else {
                    errorGroups.get(key).count++;
                }
            }
        }
        // Format grouped errors
        const parts = [];
        for (const [errorType, info] of errorGroups) {
            if (errorType === "Tool ID format errors") {
                parts.push(`${info.count} tool ID(s) must follow namespacedName format (e.g., 'server.tool-name')`);
            }
            else {
                if (info.count > 1) {
                    parts.push(`${errorType} (${info.count} instances)`);
                }
                else {
                    parts.push(errorType);
                }
            }
        }
        return parts.join(", ");
    }
    /**
     * Get MCP server configurations for a persona without applying them
     * This method loads a persona and extracts its MCP server configs for inclusion
     * in the unified boot sequence, rather than applying them directly.
     */
    async getPersonaMcpServers(personaName) {
        try {
            await this.initialize();
            // Find and load the persona
            const persona = await this.findAndLoadPersona(personaName);
            if (!persona) {
                return {
                    success: false,
                    error: `Persona "${personaName}" not found in discovered personas`,
                };
            }
            // Check if persona has MCP config
            if (!personaHasMcpConfig(persona.assets)) {
                return {
                    success: true,
                    serverConfigs: {}, // Empty config is valid
                };
            }
            // Load the MCP config file directly without applying it
            const mcpConfigFile = persona.assets.mcpConfigFile;
            try {
                // Use the MCP config parser to load the config
                const configContent = await fs.readFile(mcpConfigFile, "utf-8");
                const mcpConfig = JSON.parse(configContent);
                // Validate basic structure
                if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") {
                    return {
                        success: false,
                        error: "Invalid MCP config: missing 'mcpServers' field",
                    };
                }
                // Transform the config format from persona format to internal format
                const serverConfigs = {};
                for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
                    const config = serverConfig;
                    // Transform 'transport' field to 'type' if present
                    if (config.transport) {
                        config.type = config.transport;
                        delete config.transport;
                    }
                    // Set default type to 'stdio' if command is present but no type specified
                    if (!config.type && config.command) {
                        config.type = "stdio";
                    }
                    serverConfigs[serverName] = config;
                }
                const serverNames = Object.keys(serverConfigs);
                this.logger.info(`Extracted ${serverNames.length} MCP server${serverNames.length !== 1 ? "s" : ""} from persona "${personaName}": ${serverNames.join(", ")}`);
                return {
                    success: true,
                    serverConfigs,
                };
            }
            catch (error) {
                return {
                    success: false,
                    error: `Failed to parse MCP config: ${error instanceof Error ? error.message : String(error)}`,
                };
            }
        }
        catch (error) {
            return {
                success: false,
                error: `Failed to load persona: ${error instanceof Error ? error.message : String(error)}`,
            };
        }
    }
    /**
     * Get toolsets for a specific persona
     * @param personaName - Name of the persona to get toolsets for
     * @returns Array of persona toolsets
     */
    getPersonaToolsets(personaName) {
        const activePersona = this.getActivePersona();
        // Only return toolsets if the requested persona is currently active
        if (!activePersona || activePersona.persona.config.name !== personaName) {
            return [];
        }
        return activePersona.persona.config.toolsets || [];
    }
    /**
     * Helper function to extract server and tool names from namespacedName
     * @param namespacedName - Tool name in format {server_name}.{tool_name}
     * @returns Object with serverName and toolName
     */
    extractToolInfo(namespacedName) {
        if (namespacedName.includes(".")) {
            const parts = namespacedName.split(".");
            const serverName = parts[0];
            const toolName = parts.slice(1).join(".");
            return { serverName, toolName };
        }
        // Fallback for tools without proper namespacing
        return { serverName: "unknown", toolName: namespacedName };
    }
    // =============================================================================
    // IToolsetDelegate Implementation
    // =============================================================================
    /**
     * List available toolsets for the active persona
     * Returns persona toolsets when a persona is active, empty array otherwise
     */
    async listSavedToolsets() {
        try {
            const activePersona = this.getActivePersona();
            if (!activePersona) {
                // No active persona, return empty list
                return {
                    success: true,
                    toolsets: [],
                };
            }
            // Get persona toolsets and convert them to ToolsetInfo format
            const personaToolsets = this.getPersonaToolsets(activePersona.persona.config.name);
            const convertedToolsets = [];
            for (const toolset of personaToolsets) {
                try {
                    // Use the bridge to convert to ToolsetConfig, then to ToolsetInfo
                    const conversionResult = await this.toolsetBridge.convertPersonaToolset(toolset, activePersona.persona.config.name);
                    if (conversionResult.success && conversionResult.toolsetConfig) {
                        const toolsetConfig = conversionResult.toolsetConfig;
                        const createdAt = toolsetConfig.createdAt instanceof Date
                            ? toolsetConfig.createdAt.toISOString()
                            : toolsetConfig.createdAt;
                        convertedToolsets.push({
                            name: toolset.name,
                            description: toolsetConfig.description,
                            version: toolsetConfig.version,
                            createdAt,
                            location: activePersona.persona.sourcePath,
                            toolCount: toolsetConfig.tools.length,
                            active: true, // Persona toolsets are active when persona is active
                            totalServers: 0, // Personas don't track servers this way
                            enabledServers: 0,
                            totalTools: toolsetConfig.tools.length,
                            servers: [], // Personas don't have server breakdown
                            tools: toolsetConfig.tools.map((tool) => {
                                const namespacedName = tool.namespacedName || tool.refId || "unknown";
                                const { serverName } = this.extractToolInfo(namespacedName);
                                return {
                                    namespacedName,
                                    refId: namespacedName, // Use namespacedName as refId for persona toolsets
                                    server: serverName,
                                    active: true, // All tools in persona toolsets are considered active
                                };
                            }),
                        });
                    }
                }
                catch (error) {
                    this.logger.warn(`Failed to convert persona toolset ${toolset.name}:`, error);
                    // Continue with other toolsets even if one fails
                }
            }
            return {
                success: true,
                toolsets: convertedToolsets,
            };
        }
        catch (error) {
            return {
                success: false,
                toolsets: [],
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    /**
     * Equip a persona toolset by name
     * This activates the specified toolset within the currently active persona
     */
    async equipToolset(name) {
        try {
            const activePersona = this.getActivePersona();
            if (!activePersona) {
                return {
                    success: false,
                    error: "No persona is currently active. Cannot equip persona toolset.",
                };
            }
            // Check if the toolset exists for this persona
            const personaToolsets = this.getPersonaToolsets(activePersona.persona.config.name);
            const targetToolset = personaToolsets.find((ts) => ts.name === name);
            if (!targetToolset) {
                return {
                    success: false,
                    error: `Toolset "${name}" not found in active persona "${activePersona.persona.config.name}".`,
                };
            }
            // Update the active toolset in the persona state
            this.activeState.activeToolset = targetToolset.name;
            this.activeState.metadata.toolsResolved = targetToolset.toolIds.length;
            // Persist the state if configured
            if (this.config.persistState) {
                await this.persistActiveState();
            }
            // Convert to IToolsetInfo format for response
            try {
                const conversionResult = await this.toolsetBridge.convertPersonaToolset(targetToolset, activePersona.persona.config.name);
                if (conversionResult.success && conversionResult.toolsetConfig) {
                    const toolsetConfig = conversionResult.toolsetConfig;
                    const createdAt = toolsetConfig.createdAt instanceof Date
                        ? toolsetConfig.createdAt.toISOString()
                        : toolsetConfig.createdAt;
                    const toolsetInfo = {
                        name: targetToolset.name,
                        description: toolsetConfig.description,
                        version: toolsetConfig.version,
                        createdAt,
                        location: activePersona.persona.sourcePath,
                        toolCount: toolsetConfig.tools.length,
                        active: true,
                        totalServers: 0,
                        enabledServers: 0,
                        totalTools: toolsetConfig.tools.length,
                        servers: [],
                        tools: toolsetConfig.tools.map((tool) => {
                            const namespacedName = tool.namespacedName || tool.refId || "unknown";
                            const { serverName } = this.extractToolInfo(namespacedName);
                            return {
                                namespacedName,
                                refId: namespacedName, // Use namespacedName as refId for persona toolsets
                                server: serverName,
                                active: true,
                            };
                        }),
                    };
                    // Emit toolset change event (check if the event exists)
                    this.emit("toolset-changed", {
                        personaName: activePersona.persona.config.name,
                        toolsetName: targetToolset.name,
                        toolCount: targetToolset.toolIds.length,
                    });
                    return {
                        success: true,
                        toolset: toolsetInfo,
                    };
                }
            }
            catch (conversionError) {
                this.logger.warn(`Failed to convert equipped toolset for response:`, conversionError);
            }
            // Fallback response if conversion fails
            return {
                success: true,
                toolset: undefined, // Equipped but couldn't convert for response
            };
        }
        catch (error) {
            return {
                success: false,
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    /**
     * Get information about the currently active persona toolset
     */
    async getActiveToolset() {
        try {
            const activePersona = this.getActivePersona();
            if (!activePersona) {
                return {
                    equipped: false,
                    toolset: undefined,
                    serverStatus: undefined,
                    toolSummary: undefined,
                    exposedTools: {},
                    unavailableServers: [],
                    warnings: [],
                };
            }
            // Get the active toolset (or default toolset if none specified)
            const activeToolsetName = this.activeState.activeToolset ||
                activePersona.persona.config.defaultToolset;
            const personaToolsets = this.getPersonaToolsets(activePersona.persona.config.name);
            const activeToolset = activeToolsetName
                ? personaToolsets.find((ts) => ts.name === activeToolsetName)
                : personaToolsets[0]; // Use first toolset if no specific one is active
            if (!activeToolset) {
                return {
                    equipped: false,
                    toolset: undefined,
                    serverStatus: undefined,
                    toolSummary: undefined,
                    exposedTools: {},
                    unavailableServers: [],
                    warnings: ["No toolsets available in active persona"],
                };
            }
            // Convert to IToolsetInfo format
            const conversionResult = await this.toolsetBridge.convertPersonaToolset(activeToolset, activePersona.persona.config.name);
            if (!conversionResult.success || !conversionResult.toolsetConfig) {
                return {
                    equipped: false,
                    toolset: undefined,
                    serverStatus: undefined,
                    toolSummary: undefined,
                    exposedTools: {},
                    unavailableServers: [],
                    warnings: ["Failed to convert active persona toolset"],
                };
            }
            const toolsetConfig = conversionResult.toolsetConfig;
            const createdAt = toolsetConfig.createdAt instanceof Date
                ? toolsetConfig.createdAt.toISOString()
                : toolsetConfig.createdAt;
            const toolsetInfo = {
                name: activeToolset.name,
                description: toolsetConfig.description,
                version: toolsetConfig.version,
                createdAt,
                location: activePersona.persona.sourcePath,
                toolCount: toolsetConfig.tools.length,
                active: true,
                totalServers: 0, // Personas don't track individual servers
                enabledServers: 0,
                totalTools: toolsetConfig.tools.length,
                servers: [],
                tools: toolsetConfig.tools.map((tool) => {
                    const namespacedName = tool.namespacedName || tool.refId || "unknown";
                    const { serverName } = this.extractToolInfo(namespacedName);
                    return {
                        namespacedName,
                        refId: namespacedName, // Use namespacedName as refId for persona toolsets
                        server: serverName,
                        active: true,
                    };
                }),
            };
            // Get available tool count from discovery engine
            const discoveryEngine = this.config.getToolDiscoveryEngine?.();
            const allDiscoveredTools = discoveryEngine?.getAvailableTools(true) || [];
            // Calculate context for exposed tools
            // We need to get the actual discovered tools that match our toolset
            const exposedDiscoveredTools = allDiscoveredTools.filter((discoveredTool) => {
                return toolsetConfig.tools.some((tool) => {
                    const namespacedName = tool.namespacedName || tool.refId || "";
                    return discoveredTool.namespacedName === namespacedName;
                });
            });
            const totalTokens = exposedDiscoveredTools.length > 0
                ? tokenCounter.calculateToolsetTokens(exposedDiscoveredTools)
                : 0;
            // Group tools by server for exposedTools with full details including context
            const exposedTools = {};
            for (const discoveredTool of exposedDiscoveredTools) {
                const { serverName } = this.extractToolInfo(discoveredTool.namespacedName);
                if (!exposedTools[serverName]) {
                    exposedTools[serverName] = [];
                }
                // Convert discovered tool to ToolInfoResponse with context
                exposedTools[serverName].push(tokenCounter.convertToToolInfoResponse(discoveredTool, totalTokens));
            }
            return {
                equipped: true,
                toolset: toolsetInfo,
                serverStatus: {
                    totalConfigured: 1, // Simplified for personas
                    enabled: 1,
                    available: 1,
                    unavailable: 0,
                    disabled: 0,
                },
                toolSummary: {
                    currentlyExposed: toolsetConfig.tools.length,
                    totalDiscovered: allDiscoveredTools.length,
                    filteredOut: Math.max(0, allDiscoveredTools.length - toolsetConfig.tools.length),
                },
                exposedTools,
                unavailableServers: [],
                warnings: this.activeState.metadata.warnings,
                // Add context at top level (for get-active-toolset only)
                context: {
                    tokens: totalTokens,
                    percentTotal: null, // Not applicable for get-active-toolset
                },
            };
        }
        catch (error) {
            return {
                equipped: false,
                toolset: undefined,
                serverStatus: undefined,
                toolSummary: undefined,
                exposedTools: {},
                unavailableServers: [],
                warnings: [error instanceof Error ? error.message : String(error)],
            };
        }
    }
    /**
     * Check if a persona toolset is currently active
     */
    hasActiveToolset() {
        return this.activeState !== null;
    }
    /**
     * Get the delegate type for routing context
     */
    getDelegateType() {
        return "persona";
    }
}
/**
 * Create a PersonaManager instance with default configuration
 */
export function createPersonaManager(config) {
    return new PersonaManager(config);
}
/**
 * Default PersonaManager instance for application-wide use
 */
export const defaultPersonaManager = new PersonaManager();
//# sourceMappingURL=manager.js.map