UNPKG

research-cli

Version:

AI-powered research assistant with web search capabilities and beautiful terminal UI

234 lines (233 loc) • 9.57 kB
/** * Easymode command - Clipanion implementation * * This provides a complete interactive setup flow that walks new users * through configuring their first AI provider with a delightful UX. */ import { Command, Option } from "clipanion"; import { apiKeyManager, } from "../auth/keyring-manager.js"; import { InteractiveUI } from "../ui/interactive.js"; export class EasymodeCommand extends Command { static paths = [["easymode"], ["easy"], ["setup"]]; static usage = Command.Usage({ category: "Setup", description: "🎯 Guided setup for new users - get started in minutes!", details: ` Interactive guided setup that walks you through: - Selecting an AI provider (OpenAI, Claude, Gemini, Perplexity) - Securely storing your API key - Testing your connection - Optional quick demo Perfect for first-time users or adding new providers. `, examples: [ ["Run interactive setup", "$0 easymode"], ["Use pre-selected provider", "$0 easymode --provider openai"], ["Skip connection test", "$0 easymode --skip-test"], ], }); provider = Option.String("--provider", { description: "Skip provider selection and use this provider", }); skipTest = Option.Boolean("--skip-test", false, { description: "Skip API key testing", }); async execute() { const options = {}; if (this.provider !== undefined) { options.provider = this.provider; } if (this.skipTest !== undefined) { options.skipTest = this.skipTest; } await handleEasymode(options); return 0; } } /** * Main easymode handler - orchestrates the entire setup flow */ async function handleEasymode(options) { // Setup graceful exit handling InteractiveUI.setupGracefulExit(); try { // Step 1: Welcome InteractiveUI.showWelcomeBanner(); await sleep(1000); // Brief pause for dramatic effect // Step 2: Check if already set up const existingProviders = await apiKeyManager.listStoredProviders(); if (existingProviders.length > 0) { const shouldContinue = await InteractiveUI.confirmAction(`You already have ${existingProviders.length} provider(s) configured: ${existingProviders.join(", ")}.\nSet up another provider?`, false); if (!shouldContinue) { InteractiveUI.showExistingSetupInfo(existingProviders); return; } } InteractiveUI.showProgress(1, 4, "Selecting AI provider"); // Step 3: Provider Selection let selectedProvider; if (options.provider) { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(options.provider)) { InteractiveUI.showErrorMessage(`Invalid provider: ${options.provider}. Valid options: ${validProviders.join(", ")}`); return; } selectedProvider = options.provider; console.log(`\n🎯 Using pre-selected provider: ${selectedProvider}\n`); } else { const choice = await InteractiveUI.selectProvider(); if (!choice) { console.log("\n👋 Setup cancelled. You can run this again anytime!\n"); return; } selectedProvider = choice; } InteractiveUI.showProgress(2, 4, `Getting ${selectedProvider} API key`); // Step 4: API Key Setup const providerUrls = { openai: "https://platform.openai.com/api-keys", claude: "https://console.anthropic.com/", gemini: "https://makersuite.google.com/app/apikey", perplexity: "https://www.perplexity.ai/settings/api", }; const apiKey = await InteractiveUI.getApiKey(selectedProvider, providerUrls[selectedProvider]); if (!apiKey) { InteractiveUI.showErrorMessage("API key is required to continue"); return; } // Basic format validation if (!validateApiKeyFormat(selectedProvider, apiKey)) { const shouldContinue = await InteractiveUI.confirmAction("⚠️ API key format doesn't match expected pattern. Continue anyway?", false); if (!shouldContinue) { console.log("\n👋 Setup cancelled. Double-check your API key and try again!\n"); return; } } InteractiveUI.showProgress(3, 4, "Saving credentials securely"); // Step 5: Save API Key const shouldSave = await InteractiveUI.confirmSaveKey(); let keyStored = false; if (shouldSave) { keyStored = await apiKeyManager.storeApiKey(selectedProvider, apiKey); if (!keyStored) { console.log("\n⚠️ Could not save to secure storage. You can still use the API key for this session.\n"); } } InteractiveUI.showProgress(4, 4, "Testing API connection"); // Step 6: Test API Key (unless skipped) if (!options.skipTest) { const testResult = await InteractiveUI.testApiKeyWithSpinner(async () => { return await apiKeyManager.testApiKey(selectedProvider, apiKey); }); if (!testResult.valid) { InteractiveUI.showErrorMessage(testResult.error || "API key test failed", selectedProvider); // If we stored the key but it's invalid, ask if they want to remove it if (keyStored) { const shouldRemove = await InteractiveUI.confirmAction("Remove the invalid API key from storage?", true); if (shouldRemove) { await apiKeyManager.removeApiKey(selectedProvider); console.log("\n✅ Invalid key removed from storage\n"); } } return; } console.log("\n✅ API key test successful!\n"); } // Step 7: Success! InteractiveUI.showSuccessMessage(selectedProvider); // Bonus: Quick demo suggestion const shouldDemo = await InteractiveUI.confirmAction("🎉 Want to try a quick test query?", true); if (shouldDemo) { await runQuickDemo(selectedProvider, keyStored ? undefined : apiKey); } } catch (error) { InteractiveUI.showErrorMessage(error instanceof Error ? error.message : "Unknown error occurred"); process.exit(1); } finally { InteractiveUI.cleanup(); } } /** * Validate API key format for basic sanity checking */ function validateApiKeyFormat(provider, apiKey) { const patterns = { openai: /^sk-(proj-)?[a-zA-Z0-9]{20,}$/, claude: /^sk-ant-[a-zA-Z0-9-_]{50,}$/, gemini: /^[a-zA-Z0-9-_]{39}$/, perplexity: /^pplx-[a-zA-Z0-9]{32,}$/, }; const pattern = patterns[provider]; return pattern ? pattern.test(apiKey) : true; } /** * Run a quick demo query to show the user how it works */ async function runQuickDemo(provider, temporaryApiKey) { console.log("\n🚀 Running quick demo...\n"); try { // Import the query command dynamically to avoid circular deps const { runQuery } = await import("./query.js"); // Set temporary env var if we have a key that wasn't stored const originalEnv = process.env[`${provider.toUpperCase()}_API_KEY`]; if (temporaryApiKey) { process.env[`${provider.toUpperCase()}_API_KEY`] = temporaryApiKey; } await runQuery("What is artificial intelligence in one sentence?", { provider, model: undefined, web: false, webSearchContextSize: undefined, stream: true, format: "md", maxTokens: 100, // Keep it short for demo temperature: undefined, timeout: undefined, dryRun: false, output: undefined, verbose: false, }); // Restore original env if (temporaryApiKey) { if (originalEnv !== undefined) { process.env[`${provider.toUpperCase()}_API_KEY`] = originalEnv; } else { delete process.env[`${provider.toUpperCase()}_API_KEY`]; } } console.log("\n✨ Demo complete! You're all set to start researching.\n"); } catch (error) { console.log("\n⚠️ Demo failed, but your setup is still complete!"); console.log(`Error: ${error instanceof Error ? error.message : "Unknown error"}\n`); } } /** * Utility function for dramatic pauses */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // Extend InteractiveUI with easymode-specific methods InteractiveUI.showExistingSetupInfo = (providers) => { console.log("\n✅ You're already set up!\n"); console.log("🔑 Configured providers:"); providers.forEach((provider) => { console.log(` • ${provider}`); }); console.log("\n🚀 Ready to research:"); console.log(' research-cli "your question here"'); console.log(' research-cli "question with web search" --web'); console.log(" research-cli auth list # view all providers"); console.log(" research-cli --help # see all options\n"); }; //# sourceMappingURL=easymode-clipanion.js.map