envManager.ts•7.96 kB
// Environment Variable Management for Agent-MCP
// Handles reading, writing, and managing .env file through TUI
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
import { join } from 'path';
import { getProjectDir } from './config.js';
export interface EnvVariable {
key: string;
value: string | undefined;
required: boolean;
description: string;
category: 'embedding' | 'cli' | 'system' | 'optional';
}
export const MANAGED_ENV_VARS: EnvVariable[] = [
// Embedding Provider Keys
{
key: 'OPENAI_API_KEY',
value: undefined,
required: false,
description: 'OpenAI API key for embeddings and chat completion',
category: 'embedding'
},
{
key: 'HUGGINGFACE_API_KEY',
value: undefined,
required: false,
description: 'HuggingFace API key for embedding models',
category: 'embedding'
},
{
key: 'GEMINI_API_KEY',
value: undefined,
required: false,
description: 'Google Gemini API key for embeddings',
category: 'embedding'
},
{
key: 'ANTHROPIC_API_KEY',
value: undefined,
required: false,
description: 'Anthropic Claude API key for CLI agents',
category: 'cli'
},
// System Configuration
{
key: 'EMBEDDING_PROVIDER',
value: undefined,
required: false,
description: 'Default embedding provider (openai, ollama, huggingface, gemini)',
category: 'system'
},
{
key: 'MCP_DEBUG',
value: undefined,
required: false,
description: 'Enable debug logging (true/false)',
category: 'system'
},
// Optional Services
{
key: 'OLLAMA_HOST',
value: undefined,
required: false,
description: 'Ollama service host URL (default: http://localhost:11434)',
category: 'optional'
},
{
key: 'LOCAL_SERVER_URL',
value: undefined,
required: false,
description: 'Local embedding server URL',
category: 'optional'
}
];
function getEnvFilePath(): string {
return join(getProjectDir(), '.env');
}
export function loadEnvFile(): Map<string, string> {
const envPath = getEnvFilePath();
const envVars = new Map<string, string>();
if (!existsSync(envPath)) {
return envVars;
}
try {
const content = readFileSync(envPath, 'utf-8');
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const equalIndex = trimmed.indexOf('=');
if (equalIndex > 0) {
const key = trimmed.substring(0, equalIndex).trim();
const value = trimmed.substring(equalIndex + 1).trim()
.replace(/^["']/, '') // Remove leading quote
.replace(/["']$/, ''); // Remove trailing quote
envVars.set(key, value);
}
}
}
} catch (error) {
console.warn(`Warning: Could not read .env file: ${error}`);
}
return envVars;
}
export function saveEnvFile(envVars: Map<string, string>): void {
const envPath = getEnvFilePath();
const lines: string[] = [];
// Add header comment
lines.push('# Agent-MCP Environment Configuration');
lines.push('# Generated by Agent-MCP TUI Configuration');
lines.push('# ' + new Date().toISOString());
lines.push('');
// Group by category
const categories = ['embedding', 'cli', 'system', 'optional'] as const;
for (const category of categories) {
const categoryVars = MANAGED_ENV_VARS.filter(v => v.category === category);
if (categoryVars.length === 0) continue;
// Category header
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
lines.push(`# ${categoryName} Configuration`);
for (const envVar of categoryVars) {
const value = envVars.get(envVar.key);
if (value !== undefined && value !== '') {
lines.push(`${envVar.key}="${value}"`);
} else {
lines.push(`# ${envVar.key}= # ${envVar.description}`);
}
}
lines.push('');
}
// Add any other existing vars that aren't managed
const existingVars = loadEnvFile();
const otherVars: string[] = [];
for (const [key, value] of existingVars) {
if (!MANAGED_ENV_VARS.find(v => v.key === key)) {
otherVars.push(`${key}="${value}"`);
}
}
if (otherVars.length > 0) {
lines.push('# Other Configuration');
lines.push(...otherVars);
lines.push('');
}
try {
writeFileSync(envPath, lines.join('\n'));
} catch (error) {
console.error(`Error writing .env file: ${error}`);
throw error;
}
}
export function getCurrentEnvValues(): Map<string, string> {
const current = new Map<string, string>();
const fileVars = loadEnvFile();
for (const envVar of MANAGED_ENV_VARS) {
// Priority: process.env > .env file
const processValue = process.env[envVar.key];
const fileValue = fileVars.get(envVar.key);
if (processValue) {
current.set(envVar.key, processValue);
} else if (fileValue) {
current.set(envVar.key, fileValue);
}
}
return current;
}
export function setEnvVariable(key: string, value: string): void {
const envVars = loadEnvFile();
envVars.set(key, value);
saveEnvFile(envVars);
// Also update the current process environment
process.env[key] = value;
}
export function removeEnvVariable(key: string): void {
const envVars = loadEnvFile();
envVars.delete(key);
saveEnvFile(envVars);
// Also remove from current process environment
delete process.env[key];
}
export function validateApiKey(key: string, value: string): { valid: boolean; message?: string } {
if (!value || value.trim() === '') {
return { valid: false, message: 'API key cannot be empty' };
}
// Basic validation patterns for different services
switch (key) {
case 'OPENAI_API_KEY':
if (!value.startsWith('sk-') || value.length < 20) {
return { valid: false, message: 'OpenAI API key should start with "sk-" and be at least 20 characters' };
}
break;
case 'ANTHROPIC_API_KEY':
if (!value.startsWith('sk-ant-') || value.length < 20) {
return { valid: false, message: 'Anthropic API key should start with "sk-ant-" and be at least 20 characters' };
}
break;
case 'HUGGINGFACE_API_KEY':
if (!value.startsWith('hf_') || value.length < 10) {
return { valid: false, message: 'HuggingFace API key should start with "hf_" and be at least 10 characters' };
}
break;
case 'GEMINI_API_KEY':
if (value.length < 10) {
return { valid: false, message: 'Gemini API key should be at least 10 characters' };
}
break;
}
return { valid: true };
}
export function maskApiKey(value: string): string {
if (!value || value.length < 8) {
return '***';
}
const start = value.substring(0, 4);
const end = value.substring(value.length - 4);
const middle = '*'.repeat(Math.max(4, value.length - 8));
return `${start}${middle}${end}`;
}
export function getEnvVariablesByCategory(category: 'embedding' | 'cli' | 'system' | 'optional'): EnvVariable[] {
return MANAGED_ENV_VARS.filter(v => v.category === category);
}
export function reloadEnvironmentVariables(): void {
// Reload environment variables from .env file into process.env
const envVars = loadEnvFile();
for (const [key, value] of envVars) {
process.env[key] = value;
}
}
export function isEnvVariableRequired(key: string, embeddingProvider?: string): boolean {
const envVar = MANAGED_ENV_VARS.find(v => v.key === key);
if (!envVar) return false;
// Dynamic requirements based on selected embedding provider
if (embeddingProvider) {
switch (key) {
case 'OPENAI_API_KEY':
return embeddingProvider === 'openai';
case 'HUGGINGFACE_API_KEY':
return embeddingProvider === 'huggingface';
case 'GEMINI_API_KEY':
return embeddingProvider === 'gemini';
}
}
return envVar.required;
}