research-cli
Version:
AI-powered research assistant with web search capabilities and beautiful terminal UI
415 lines (413 loc) ⢠17 kB
JavaScript
/**
* 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