UNPKG

@toolprint/hypertool-mcp

Version:

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

288 lines (283 loc) • 12.7 kB
/** * Persona Add Command * * CLI command to install personas from folder paths or .htp archives * into the standard personas directory (~/.toolprint/hypertool-mcp/personas). * * @fileoverview Persona installation CLI command */ import { Command } from "commander"; import { resolve } from "path"; import { promises as fs } from "fs"; import inquirer from "inquirer"; import { theme, semantic } from "../../utils/theme.js"; import { createChildLogger } from "../../utils/logging.js"; import { installPersona, analyzeSource, checkPersonaExists, getStandardPersonasDir, } from "../../persona/installer.js"; import { isPersonaError } from "../../persona/errors.js"; import { PersonaErrorCode } from "../../persona/types.js"; const logger = createChildLogger({ module: "persona-add-cli" }); /** * Check for environment variables in MCP config that need configuration */ async function checkEnvironmentVariables(installPath) { const mcpConfigPath = `${installPath}/mcp.json`; try { const configContent = await fs.readFile(mcpConfigPath, "utf-8"); const config = JSON.parse(configContent); const envVars = []; if (config.mcpServers) { for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { if (typeof serverConfig === "object" && serverConfig !== null && "env" in serverConfig) { const env = serverConfig.env; if (env && typeof env === "object") { const vars = Object.keys(env); if (vars.length > 0) { envVars.push({ server: serverName, vars: vars, }); } } } } } return { hasEnvVars: envVars.length > 0, envVars, configPath: mcpConfigPath, }; } catch (error) { // If we can't read the MCP config, that's okay return { hasEnvVars: false, envVars: [], configPath: mcpConfigPath, }; } } /** * Interactive environment variable configuration using inquirer (simplified) */ async function interactiveEnvVarConfig(envCheck, personaName) { const { shouldConfigure } = await inquirer.prompt([ { type: "confirm", name: "shouldConfigure", message: "Configure now?", default: true, }, ]); if (!shouldConfigure) { console.log(theme.warning(`⏭️ Configure later with: ${theme.info("hypertool persona inspect " + personaName)}`)); return; } const envValues = {}; // Collect values for all environment variables for (const serverEnv of envCheck.envVars) { for (const envVar of serverEnv.vars) { const { value } = await inquirer.prompt([ { type: "input", name: "value", message: `${serverEnv.server}.${envVar}:`, validate: (input) => (input.trim() ? true : "Required"), }, ]); envValues[envVar] = value; } } // Update the MCP config file with the new values try { const configContent = await fs.readFile(envCheck.configPath, "utf-8"); const config = JSON.parse(configContent); // Update environment variables in the config for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { if (typeof serverConfig === "object" && serverConfig !== null && "env" in serverConfig) { const env = serverConfig.env; if (env && typeof env === "object") { for (const [envVar, currentValue] of Object.entries(env)) { if (envValues[envVar]) { env[envVar] = envValues[envVar]; } } } } } // Write the updated config back to file await fs.writeFile(envCheck.configPath, JSON.stringify(config, null, 2)); console.log(theme.success(`✅ Configured! Run: ${theme.warning(`hypertool persona activate ${personaName}`)}`)); console.log(` ${theme.muted("Config:")} ${envCheck.configPath}`); } catch (error) { console.error(theme.error(`❌ Config failed. Edit manually: ${envCheck.configPath}`)); } } /** * Display installation progress and results (simplified and compact) */ async function displayInstallationResult(sourcePath, result, sourceType) { if (result.success) { console.log(theme.success(`✅ Installed "${result.personaName}"`)); console.log(` ${theme.muted("Location:")} ${result.installPath}`); console.log(` ${theme.muted("Config:")} ${result.installPath}/mcp.json`); // Check for environment variables that need configuration const envCheck = await checkEnvironmentVariables(result.installPath); if (envCheck.hasEnvVars) { console.log(); console.log(theme.warning("⚙️ Configuration needed:")); for (const serverEnv of envCheck.envVars) { const vars = serverEnv.vars.join(", "); console.log(` ${theme.info(serverEnv.server)}: ${theme.warning(vars)}`); } // Offer interactive configuration await interactiveEnvVarConfig(envCheck, result.personaName); } else { console.log(); console.log(theme.success(`🚀 Ready to use! Run: ${theme.warning(`hypertool persona activate ${result.personaName}`)}`)); } } else { console.error(semantic.messageError("❌ Installation failed")); if (result.errors && result.errors.length > 0) { for (const error of result.errors) { console.error(` ${theme.error(error)}`); } } } } /** * Display helpful error messages based on error type */ function displayError(error, sourcePath) { console.error(semantic.messageError("❌ Failed to install persona:")); if (isPersonaError(error)) { console.error(` ${theme.error(error.message)}`); // Provide specific guidance based on error type switch (error.code) { case PersonaErrorCode.PERSONA_NOT_FOUND: console.log(); console.error(theme.info("💡 Check that the path exists and is accessible")); console.error(theme.info("💡 Use absolute paths to avoid confusion")); break; case PersonaErrorCode.INVALID_SCHEMA: console.log(); console.error(theme.info("💡 Use 'hypertool persona validate <path>' to see detailed validation errors")); console.error(theme.info("💡 Ensure persona.yaml file has correct structure")); break; case PersonaErrorCode.DUPLICATE_PERSONA_NAME: console.log(); console.error(theme.info("💡 Use --force to overwrite existing persona")); console.error(theme.info("💡 Use --backup to create backup before overwriting")); console.error(theme.info("💡 Use 'hypertool persona list' to see existing personas")); break; case PersonaErrorCode.ARCHIVE_EXTRACTION_FAILED: console.log(); console.error(theme.info("💡 Ensure the .htp file is not corrupted")); console.error(theme.info("💡 Check that you have write permissions to the installation directory")); break; case PersonaErrorCode.FILE_SYSTEM_ERROR: console.log(); console.error(theme.info("💡 Check file permissions and disk space")); console.error(theme.info("💡 Ensure installation directory is writable")); break; default: console.log(); console.error(theme.info("💡 Check the persona structure and try again")); } if (error.suggestions && error.suggestions.length > 0) { console.log(); console.error(theme.info("💡 Suggestions:")); for (const suggestion of error.suggestions) { console.error(` • ${theme.info(suggestion)}`); } } } else { console.error(` ${theme.error(error.message)}`); } } /** * Create the add subcommand */ export function createAddCommand() { return new Command("add") .description("Install a persona from a folder path or .htp archive") .argument("<path>", "Path to persona folder or .htp archive file") .option("--force", "Overwrite existing persona if it already exists", false) .option("--backup", "Create backup of existing persona before overwriting", false) .option("--skip-validation", "Skip persona validation during installation", false) .option("--install-dir <dir>", "Custom installation directory (defaults to ~/.toolprint/hypertool-mcp/personas)") .action(async (sourcePath, options) => { try { const resolvedSourcePath = resolve(sourcePath); // Analyze the source to understand what we're installing const sourceInfo = await analyzeSource(resolvedSourcePath); if (!sourceInfo.accessible) { throw new Error(`Source path is not accessible: ${resolvedSourcePath}`); } // Check if persona already exists const installDir = options.installDir || getStandardPersonasDir(); if (sourceInfo.personaName) { const exists = await checkPersonaExists(sourceInfo.personaName, installDir); if (exists) { if (!options.force) { console.log(theme.warning(`⚠️ Persona '${sourceInfo.personaName}' already exists`)); console.log(); console.log(theme.info("Use --force to overwrite or --backup to create a backup first")); process.exit(1); return; // Prevent further execution in test environment } else { console.log(theme.warning(`⚠️ Will overwrite existing persona '${sourceInfo.personaName}'`)); if (options.backup) { console.log(theme.info(" Creating backup before overwrite...")); } console.log(); } } } // Prepare installation options const installOptions = { force: options.force, backup: options.backup, skipValidation: options.skipValidation, installDir: options.installDir, }; // Install the persona const result = await installPersona(resolvedSourcePath, installOptions); // Display results await displayInstallationResult(resolvedSourcePath, result, sourceInfo.type); // Exit with appropriate code process.exit(result.success ? 0 : 1); } catch (error) { displayError(error, sourcePath); process.exit(1); } }) .addHelpText("after", ` ${theme.label("Examples:")} ${theme.muted("# Install from folder")} ${theme.muted("hypertool persona add ./my-persona-folder")} ${theme.muted("# Install from archive")} ${theme.muted("hypertool persona add ./awesome-persona.htp")} ${theme.muted("# Force overwrite existing persona")} ${theme.muted("hypertool persona add ./updated-persona.htp --force")} ${theme.muted("# Create backup before overwriting")} ${theme.muted("hypertool persona add ./updated-persona.htp --force --backup")} ${theme.muted("# Install to custom directory")} ${theme.muted("hypertool persona add ./persona --install-dir /custom/path")} ${theme.label("Notes:")} ${theme.muted("• Persona folders must contain a valid persona.yaml file")} ${theme.muted("• Archive files must have .htp extension")} ${theme.muted("• Installation validates persona structure by default")} ${theme.muted("• Use --skip-validation to bypass validation checks")} ${theme.muted("• Backups are created with timestamp: persona.backup.YYYY-MM-DD...")}`); } //# sourceMappingURL=add.js.map