UNPKG

research-cli

Version:

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

415 lines (413 loc) • 17 kB
/** * Auth commands - Clipanion implementation * * Authentication management commands for API keys and provider setup */ import * as os from "node:os"; import { Command, Option } from "clipanion"; import { apiKeyManager, } from "../auth/keyring-manager.js"; import { getOpenAIApiKey, hasStoredApiKey, removeStoredApiKey, } from "../utils/credentials.js"; // import * as t from "typanion"; // Not used in this file // Base auth command (shows help when used alone) export class AuthCommand extends Command { static paths = [["auth"]]; static usage = Command.Usage({ category: "Authentication", description: "šŸ” Manage API keys and authentication", details: ` Manage API keys for various LLM providers using secure system keychain storage. Supports OpenAI, Claude, Gemini, and Perplexity providers with secure storage, testing, and migration capabilities. `, examples: [ ["Check authentication status", "$0 auth list"], ["Set up OpenAI API key", "$0 auth login openai"], ["Test API connection", "$0 auth test openai"], ["Remove stored key", "$0 auth logout openai"], ], }); async execute() { this.context.stdout.write("šŸ” Authentication Management\n\n"); this.context.stdout.write("Available commands:\n"); this.context.stdout.write(" auth list Show authentication status for all providers\n"); this.context.stdout.write(" auth login <provider> Set up API key for a provider\n"); this.context.stdout.write(" auth logout <provider> Remove stored API key\n"); this.context.stdout.write(" auth test <provider> Test API connection\n"); this.context.stdout.write(" auth status Check legacy OpenAI key status\n"); this.context.stdout.write(" auth migrate Migrate keys from config file\n\n"); this.context.stdout.write("Use '$0 auth <command> --help' for detailed information.\n"); return 0; } } // Legacy status command export class AuthStatusCommand extends Command { static paths = [["auth", "status"]]; static usage = Command.Usage({ category: "Authentication", description: "Check API key status (legacy command)", }); async execute() { try { const hasStored = await hasStoredApiKey(); const hasEnv = !!process.env["OPENAI_API_KEY"]; this.context.stdout.write("šŸ”‘ API Key Status:\n"); if (hasEnv) { this.context.stdout.write(" āœ… Environment variable: OPENAI_API_KEY is set\n"); } else { this.context.stdout.write(" āŒ Environment variable: OPENAI_API_KEY not set\n"); } if (hasStored) { this.context.stdout.write(" āœ… Keychain: API key is stored securely\n"); } else { this.context.stdout.write(" āŒ Keychain: No API key stored\n"); } if (!hasEnv && !hasStored) { this.context.stdout.write("\nšŸ’” Run a query to be prompted for your API key, or set OPENAI_API_KEY environment variable\n"); } return 0; } catch (error) { this.context.stderr.write(`Error checking API key status: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Legacy setup command export class AuthSetupCommand extends Command { static paths = [["auth", "setup"]]; static usage = Command.Usage({ category: "Authentication", description: "Set up OpenAI API key interactively (legacy command)", }); async execute() { try { this.context.stdout.write("šŸ”§ Setting up OpenAI API key...\n"); await getOpenAIApiKey(); this.context.stdout.write("āœ… API key setup complete!\n"); return 0; } catch (error) { this.context.stderr.write(`Error setting up API key: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Legacy remove command export class AuthRemoveCommand extends Command { static paths = [["auth", "remove"]]; static usage = Command.Usage({ category: "Authentication", description: "Remove stored API key from keychain (legacy command)", }); async execute() { try { const hasStored = await hasStoredApiKey(); if (!hasStored) { this.context.stdout.write("āŒ No API key is stored in keychain\n"); return 0; } const removed = await removeStoredApiKey(); if (removed) { this.context.stdout.write("āœ… API key removed from keychain\n"); } else { this.context.stdout.write("āŒ Failed to remove API key from keychain\n"); } return 0; } catch (error) { this.context.stderr.write(`Error removing API key: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Modern login command export class AuthLoginCommand extends Command { static paths = [["auth", "login"]]; static usage = Command.Usage({ category: "Authentication", description: "Set up API key for a provider", details: ` Authenticate with a supported LLM provider by storing your API key securely. The API key will be stored in your system's secure keychain and can be tested to verify it's working correctly. `, examples: [ ["Interactive setup", "$0 auth login openai"], ["Provide key directly", "$0 auth login openai --key sk-..."], ], }); provider = Option.String({ required: true }); key = Option.String("--key", { description: "API key to store (will prompt if not provided)", }); async execute() { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(this.provider)) { this.context.stderr.write(`āŒ Invalid provider: ${this.provider}\n`); this.context.stdout.write(`Valid providers: ${validProviders.join(", ")}\n`); return 1; } const providerKey = this.provider; try { if (this.key) { // Store provided key const success = await apiKeyManager.storeApiKey(providerKey, this.key); if (!success) { this.context.stderr.write(`āŒ Failed to store API key for ${this.provider}\n`); return 1; } } else { // Interactive setup const apiKey = await apiKeyManager.setupApiKey(providerKey); if (!apiKey) { this.context.stderr.write(`āŒ Failed to set up API key for ${this.provider}\n`); return 1; } } this.context.stdout.write(`āœ… Successfully authenticated with ${this.provider}\n`); // Test the key const testResult = await apiKeyManager.testApiKey(providerKey); if (testResult.valid) { this.context.stdout.write("āœ… API key verified and working\n"); } else { this.context.stdout.write(`āš ļø API key stored but validation failed: ${testResult.error}\n`); } return 0; } catch (error) { this.context.stderr.write(`Error setting up API key: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Modern logout command export class AuthLogoutCommand extends Command { static paths = [["auth", "logout"]]; static usage = Command.Usage({ category: "Authentication", description: "Remove stored API key for a provider", examples: [ ["Remove with confirmation", "$0 auth logout openai"], ["Remove without confirmation", "$0 auth logout openai --force"], ], }); provider = Option.String({ required: true }); force = Option.Boolean("--force", false, { description: "Skip confirmation prompt", }); async execute() { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(this.provider)) { this.context.stderr.write(`āŒ Invalid provider: ${this.provider}\n`); this.context.stdout.write(`Valid providers: ${validProviders.join(", ")}\n`); return 1; } const providerKey = this.provider; try { const hasKey = await apiKeyManager.getApiKey(providerKey); if (!hasKey) { this.context.stdout.write(`ā„¹ļø No API key stored for ${this.provider}\n`); return 0; } if (!this.force) { const readline = await import("node:readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const answer = await new Promise((resolve) => { rl.question(`Remove API key for ${this.provider}? (y/N): `, resolve); }); rl.close(); if (!answer.toLowerCase().startsWith("y")) { this.context.stdout.write("Cancelled\n"); return 0; } } const success = await apiKeyManager.removeApiKey(providerKey); if (!success) { this.context.stderr.write(`āŒ Failed to remove API key for ${this.provider}\n`); return 1; } this.context.stdout.write(`āœ… API key removed for ${this.provider}\n`); return 0; } catch (error) { this.context.stderr.write(`Error removing API key: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // List command export class AuthListCommand extends Command { static paths = [["auth", "list"]]; static usage = Command.Usage({ category: "Authentication", description: "Show authentication status for all providers", }); async execute() { try { this.context.stdout.write("\nšŸ”‘ Authentication Status:\n"); const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; const storedProviders = await apiKeyManager.listStoredProviders(); if (storedProviders.length === 0) { this.context.stdout.write("āŒ No API keys stored\n"); this.context.stdout.write("Run: research-cli auth login <provider>\n"); return 0; } for (const provider of validProviders) { const hasKey = storedProviders.includes(provider); const status = hasKey ? "āœ… Configured" : "āŒ Not configured"; this.context.stdout.write(` ${provider.padEnd(12)} ${status}\n`); } this.context.stdout.write(`\nTotal providers configured: ${storedProviders.length}\n`); return 0; } catch (error) { this.context.stderr.write(`Error checking authentication status: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Test command export class AuthTestCommand extends Command { static paths = [["auth", "test"]]; static usage = Command.Usage({ category: "Authentication", description: "Test API connection for a provider", examples: [["Test OpenAI connection", "$0 auth test openai"]], }); provider = Option.String({ required: true }); async execute() { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(this.provider)) { this.context.stderr.write(`āŒ Invalid provider: ${this.provider}\n`); this.context.stdout.write(`Valid providers: ${validProviders.join(", ")}\n`); return 1; } const providerKey = this.provider; try { const apiKey = await apiKeyManager.getApiKey(providerKey); if (!apiKey) { this.context.stderr.write(`āŒ No API key stored for ${this.provider}\n`); this.context.stdout.write(`Run: research-cli auth login ${this.provider}\n`); return 1; } this.context.stdout.write(`Testing ${this.provider} API connection...\n`); const testResult = await apiKeyManager.testApiKey(providerKey); if (testResult.valid) { this.context.stdout.write(`āœ… ${this.provider} API key is valid\n`); return 0; } else { this.context.stderr.write(`āŒ ${this.provider} API test failed: ${testResult.error}\n`); this.context.stdout.write(`Run: research-cli auth login ${this.provider} to update your key\n`); return 1; } } catch (error) { this.context.stderr.write(`Error testing API key: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Migrate command export class AuthMigrateCommand extends Command { static paths = [["auth", "migrate"]]; static usage = Command.Usage({ category: "Authentication", description: "Migrate API keys from config file to secure storage", examples: [ ["Migrate from default location", "$0 auth migrate"], [ "Migrate from custom location", "$0 auth migrate --config /path/to/config.json", ], ], }); config = Option.String("--config", "~/.research-cli/config.json", { description: "Config file to migrate from", }); async execute() { try { const configPath = this.config.replace("~", os.homedir()); this.context.stdout.write(`Migrating API keys from: ${configPath}\n`); const result = await apiKeyManager.migrateFromConfig(configPath); if (result.migrated > 0) { this.context.stdout.write(`āœ… Successfully migrated ${result.migrated} API keys\n`); if (result.errors.length > 0) { this.context.stdout.write("āš ļø Some migrations failed:\n"); result.errors.forEach((error) => this.context.stdout.write(` - ${error}\n`)); } } else { this.context.stdout.write("ā„¹ļø No API keys found to migrate\n"); if (result.errors.length > 0) { this.context.stdout.write("Errors:\n"); result.errors.forEach((error) => this.context.stdout.write(` - ${error}\n`)); } } return 0; } catch (error) { this.context.stderr.write(`Error migrating API keys: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } // Keyring status command export class AuthKeyringStatusCommand extends Command { static paths = [["auth", "keyring-status"]]; static usage = Command.Usage({ category: "Authentication", description: "Check system keyring availability", }); async execute() { try { this.context.stdout.write("šŸ” Checking system keyring status...\n"); const testResult = await apiKeyManager.testKeyringAvailability(); if (testResult.available) { this.context.stdout.write("āœ… System keyring is available and working\n"); this.context.stdout.write(`Backend: ${testResult.backend}\n`); } else { this.context.stdout.write("āŒ System keyring is not available\n"); if (testResult.error) { this.context.stdout.write(`Error: ${testResult.error}\n`); } this.context.stdout.write("Will use encrypted fallback storage\n"); } return 0; } catch (error) { this.context.stderr.write(`Error checking keyring status: ${error instanceof Error ? error.message : "Unknown error"}\n`); return 1; } } } //# sourceMappingURL=auth.js.map