🔑 Feat: Gestion interactive des clés API avec stockage sécurisé

Nouvelles fonctionnalités:
- Demande interactive des clés API au premier lancement
- Commandes pour gérer les clés: key set/remove, setup, keys
- Stockage sécurisé des clés dans ~/.neuraterm/keys.json
- Support variables d'environnement et rechargement à chaud
- Gestion intelligente du provider par défaut selon les clés disponibles

Commandes ajoutées:
- `keys` - Afficher le statut des clés API
- `key set <provider>` - Configurer une clé (openai/mistral)
- `key remove <provider>` - Supprimer une clé
- `setup` - Configuration interactive complète

Plus besoin de configurer manuellement les clés avant le lancement!

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Network Monitor Bot 2025-08-19 19:35:33 +02:00
parent 0b9bab45a8
commit b8369b89e6
10 changed files with 7447 additions and 40 deletions

View File

@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(git reset:*)"
],
"deny": [],
"ask": []

6
.gitignore vendored
View File

@ -1 +1,5 @@
.divers
.divers
node_modules/
dist/
*.log
.env

View File

@ -28,26 +28,50 @@ npm start
## ⚙️ Configuration
### Variables d'environnement
### 🚀 Configuration automatique (recommandée)
Au premier lancement, NeuraTerm vous demandera vos clés API de manière interactive :
```bash
neuraterm
# Suivez les instructions pour configurer vos clés API
```
### 🔑 Gestion des clés API
```bash
# Afficher le statut des clés
keys
# Configurer une nouvelle clé
key set openai
key set mistral
# Supprimer une clé
key remove openai
# Reconfiguration complète
setup
```
### Variables d'environnement (optionnel)
```bash
export OPENAI_API_KEY="votre_clé_openai"
export MISTRAL_API_KEY="votre_clé_mistral"
```
### Fichier de configuration
### Fichier de configuration avancée
Créez `~/.neuraterm/config.json` :
Créez `~/.neuraterm/config.json` pour une configuration avancée :
```json
{
"ai": {
"openai": {
"apiKey": "votre_clé_openai",
"model": "gpt-4o-mini"
},
"mistral": {
"apiKey": "votre_clé_mistral",
"model": "mistral-large-latest"
},
"defaultProvider": "openai"
@ -61,6 +85,8 @@ Créez `~/.neuraterm/config.json` :
}
```
> **Note**: Les clés API sont stockées séparément dans `~/.neuraterm/keys.json` pour plus de sécurité.
## 🎯 Utilisation
### Commandes de base
@ -72,20 +98,27 @@ neuraterm
# Aide
help
# Gestion des clés API
keys # Statut des clés
key set openai # Configurer OpenAI
key set mistral # Configurer Mistral
setup # Configuration interactive
# Poser une question à l'IA
Comment optimiser mon code Python ?
# Changer de provider
provider mistral
provider openai
# Voir les statistiques
stats
stats openai
cost
stats # Toutes les stats
stats openai # Stats OpenAI uniquement
cost # Coût total
# Configuration
config
providers
config # Voir la configuration
providers # Lister les providers
```
### Exemples d'usage professionnel

6974
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,20 +6,28 @@ import { AIClient } from './client.js';
import { ProviderConfig } from './types.js';
export async function initAI(config: any, auth: any): Promise<AIClient> {
const keyManager = auth.getKeyManager();
const keys = keyManager.loadKeys();
const providerConfig: ProviderConfig = {
openai: {
apiKey: config.ai.openai?.apiKey || process.env.OPENAI_API_KEY || '',
apiKey: keys.openai || '',
model: config.ai.openai?.model || 'gpt-4o-mini',
baseUrl: config.ai.openai?.baseUrl
},
mistral: {
apiKey: config.ai.mistral?.apiKey || process.env.MISTRAL_API_KEY || '',
apiKey: keys.mistral || '',
model: config.ai.mistral?.model || 'mistral-large-latest',
baseUrl: config.ai.mistral?.baseUrl
},
defaultProvider: config.ai.defaultProvider || 'openai'
defaultProvider: keys.openai ? 'openai' : 'mistral'
};
// Ajuster le provider par défaut selon les clés disponibles
if (config.ai.defaultProvider && keys[config.ai.defaultProvider as keyof typeof keys]) {
providerConfig.defaultProvider = config.ai.defaultProvider;
}
return new AIClient(providerConfig);
}

View File

@ -1,19 +1,40 @@
/**
* Gestion de l'authentification
* Gestion de l'authentification et des clés API
*/
import { KeyManager } from './keyManager.js';
export class AuthManager {
constructor(private config: any) {}
private keyManager: KeyManager;
constructor(private config: any) {
this.keyManager = new KeyManager();
}
isAuthenticated(): boolean {
return true;
return this.keyManager.hasValidKeys();
}
async authenticate(): Promise<void> {
return;
if (!this.keyManager.hasValidKeys()) {
await this.keyManager.promptForMissingKeys();
}
}
getKeyManager(): KeyManager {
return this.keyManager;
}
}
export async function initAuthentication(config: any): Promise<AuthManager> {
return new AuthManager(config);
}
const authManager = new AuthManager(config);
// Vérifier les clés au démarrage
if (!authManager.isAuthenticated()) {
await authManager.authenticate();
}
return authManager;
}
export { KeyManager } from './keyManager.js';

264
src/auth/keyManager.ts Normal file
View File

@ -0,0 +1,264 @@
/**
* Gestionnaire des clés API avec stockage sécurisé
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { join, dirname } from 'path';
import * as readline from 'readline';
import { logger } from '../utils/logger.js';
export interface APIKeys {
openai?: string;
mistral?: string;
}
export class KeyManager {
private configPath: string;
private keysPath: string;
constructor() {
const configDir = join(homedir(), '.neuraterm');
this.configPath = join(configDir, 'config.json');
this.keysPath = join(configDir, 'keys.json');
// Créer le dossier de configuration s'il n'existe pas
if (!existsSync(configDir)) {
mkdirSync(configDir, { recursive: true });
}
}
/**
* Charge les clés API depuis le fichier de configuration
*/
loadKeys(): APIKeys {
const keys: APIKeys = {};
// Priorité 1: Variables d'environnement
if (process.env.OPENAI_API_KEY) {
keys.openai = process.env.OPENAI_API_KEY;
}
if (process.env.MISTRAL_API_KEY) {
keys.mistral = process.env.MISTRAL_API_KEY;
}
// Priorité 2: Fichier de clés
if (existsSync(this.keysPath)) {
try {
const fileKeys = JSON.parse(readFileSync(this.keysPath, 'utf8'));
if (!keys.openai && fileKeys.openai) {
keys.openai = fileKeys.openai;
}
if (!keys.mistral && fileKeys.mistral) {
keys.mistral = fileKeys.mistral;
}
} catch (error) {
logger.warn('Erreur lors du chargement des clés:', error);
}
}
return keys;
}
/**
* Sauvegarde les clés API dans le fichier de configuration
*/
saveKeys(keys: APIKeys): void {
try {
// Ne sauvegarder que les clés non définies en variable d'environnement
const keysToSave: APIKeys = {};
if (keys.openai && !process.env.OPENAI_API_KEY) {
keysToSave.openai = keys.openai;
}
if (keys.mistral && !process.env.MISTRAL_API_KEY) {
keysToSave.mistral = keys.mistral;
}
writeFileSync(this.keysPath, JSON.stringify(keysToSave, null, 2));
logger.info('Clés API sauvegardées avec succès');
} catch (error) {
logger.error('Erreur lors de la sauvegarde des clés:', error);
throw error;
}
}
/**
* Vérifie si au moins une clé API est disponible
*/
hasValidKeys(): boolean {
const keys = this.loadKeys();
return !!(keys.openai || keys.mistral);
}
/**
* Demande interactivement les clés API manquantes
*/
async promptForMissingKeys(): Promise<APIKeys> {
const keys = this.loadKeys();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
};
try {
console.log('\n🔑 Configuration des clés API');
console.log('─'.repeat(40));
if (!keys.openai) {
console.log('\nOpenAI (ChatGPT) non configuré.');
const wantOpenAI = await question('Voulez-vous configurer OpenAI ? (o/n): ');
if (wantOpenAI.toLowerCase() === 'o' || wantOpenAI.toLowerCase() === 'oui') {
const openaiKey = await question('Entrez votre clé API OpenAI: ');
if (openaiKey.trim()) {
keys.openai = openaiKey.trim();
}
}
} else {
console.log('✅ OpenAI configuré');
}
if (!keys.mistral) {
console.log('\nMistral AI non configuré.');
const wantMistral = await question('Voulez-vous configurer Mistral AI ? (o/n): ');
if (wantMistral.toLowerCase() === 'o' || wantMistral.toLowerCase() === 'oui') {
const mistralKey = await question('Entrez votre clé API Mistral: ');
if (mistralKey.trim()) {
keys.mistral = mistralKey.trim();
}
}
} else {
console.log('✅ Mistral AI configuré');
}
// Vérifier qu'au moins une clé est disponible
if (!keys.openai && !keys.mistral) {
console.log('\n❌ Erreur: Au moins une clé API est requise pour utiliser NeuraTerm.');
console.log('Vous pouvez aussi définir les variables d\'environnement:');
console.log(' export OPENAI_API_KEY="votre_clé"');
console.log(' export MISTRAL_API_KEY="votre_clé"');
process.exit(1);
}
// Sauvegarder les nouvelles clés
this.saveKeys(keys);
console.log('\n✅ Configuration terminée !');
return keys;
} finally {
rl.close();
}
}
/**
* Met à jour une clé API spécifique
*/
async updateKey(provider: 'openai' | 'mistral'): Promise<void> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
};
try {
console.log(`\n🔑 Mise à jour de la clé ${provider.toUpperCase()}`);
if (process.env[`${provider.toUpperCase()}_API_KEY`]) {
console.log(`⚠️ Attention: La clé ${provider.toUpperCase()} est définie en variable d'environnement.`);
console.log('Cette modification ne sera effective que pour cette session.');
}
const newKey = await question(`Entrez la nouvelle clé API ${provider}: `);
if (!newKey.trim()) {
console.log('❌ Clé vide, annulation.');
return;
}
const keys = this.loadKeys();
keys[provider] = newKey.trim();
this.saveKeys(keys);
console.log(`✅ Clé ${provider.toUpperCase()} mise à jour avec succès !`);
} finally {
rl.close();
}
}
/**
* Supprime une clé API
*/
async removeKey(provider: 'openai' | 'mistral'): Promise<void> {
const keys = this.loadKeys();
if (process.env[`${provider.toUpperCase()}_API_KEY`]) {
console.log(`⚠️ La clé ${provider.toUpperCase()} est définie en variable d'environnement et ne peut pas être supprimée.`);
return;
}
if (existsSync(this.keysPath)) {
try {
const fileKeys = JSON.parse(readFileSync(this.keysPath, 'utf8'));
delete fileKeys[provider];
writeFileSync(this.keysPath, JSON.stringify(fileKeys, null, 2));
console.log(`✅ Clé ${provider.toUpperCase()} supprimée avec succès !`);
} catch (error) {
logger.error('Erreur lors de la suppression de la clé:', error);
}
}
}
/**
* Affiche le statut des clés API
*/
displayKeysStatus(): void {
const keys = this.loadKeys();
console.log('\n🔑 Statut des clés API:');
console.log('─'.repeat(30));
// OpenAI
if (keys.openai) {
const source = process.env.OPENAI_API_KEY ? '(var. env.)' : '(fichier)';
const masked = this.maskKey(keys.openai);
console.log(`✅ OpenAI: ${masked} ${source}`);
} else {
console.log('❌ OpenAI: Non configuré');
}
// Mistral
if (keys.mistral) {
const source = process.env.MISTRAL_API_KEY ? '(var. env.)' : '(fichier)';
const masked = this.maskKey(keys.mistral);
console.log(`✅ Mistral: ${masked} ${source}`);
} else {
console.log('❌ Mistral: Non configuré');
}
console.log('');
}
/**
* Masque une clé API pour l'affichage
*/
private maskKey(key: string): string {
if (key.length <= 8) return '*'.repeat(key.length);
return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4);
}
}

View File

@ -98,6 +98,19 @@ export class CommandProcessor {
this.showConfig();
break;
// Nouvelles commandes pour les clés API
case 'keys':
this.showKeys();
break;
case 'key':
await this.manageKey(args);
break;
case 'setup':
await this.setupKeys();
break;
default:
// Traiter comme une question à l'IA
await this.handleAIQuery(command);
@ -157,6 +170,100 @@ export class CommandProcessor {
console.log(JSON.stringify(this.config, null, 2));
}
private showKeys(): void {
const keyManager = this.dependencies.auth.getKeyManager();
keyManager.displayKeysStatus();
}
private async manageKey(args: string[]): Promise<void> {
const keyManager = this.dependencies.auth.getKeyManager();
if (args.length === 0) {
console.log('\n🔑 Gestion des clés API:');
console.log('Usage:');
console.log(' key set <provider> - Définir/mettre à jour une clé (openai, mistral)');
console.log(' key remove <provider> - Supprimer une clé');
console.log(' key status - Afficher le statut des clés');
return;
}
const [action, provider] = args;
switch (action.toLowerCase()) {
case 'set':
case 'update':
if (!provider || !['openai', 'mistral'].includes(provider.toLowerCase())) {
console.log('❌ Provider invalide. Utilisez: openai ou mistral');
return;
}
await keyManager.updateKey(provider.toLowerCase() as 'openai' | 'mistral');
// Redémarrer le client IA avec les nouvelles clés
await this.reloadAIClient();
break;
case 'remove':
case 'delete':
if (!provider || !['openai', 'mistral'].includes(provider.toLowerCase())) {
console.log('❌ Provider invalide. Utilisez: openai ou mistral');
return;
}
await keyManager.removeKey(provider.toLowerCase() as 'openai' | 'mistral');
await this.reloadAIClient();
break;
case 'status':
keyManager.displayKeysStatus();
break;
default:
console.log('❌ Action invalide. Utilisez: set, remove, ou status');
break;
}
}
private async setupKeys(): Promise<void> {
const keyManager = this.dependencies.auth.getKeyManager();
await keyManager.promptForMissingKeys();
await this.reloadAIClient();
console.log('\n✅ Configuration terminée! Vous pouvez maintenant utiliser NeuraTerm.');
}
private async reloadAIClient(): Promise<void> {
try {
// Recréer le client IA avec les nouvelles clés
const keyManager = this.dependencies.auth.getKeyManager();
const keys = keyManager.loadKeys();
// Vérifier qu'au moins une clé est disponible
if (!keys.openai && !keys.mistral) {
console.log('⚠️ Aucune clé API disponible. Le client IA ne peut pas être rechargé.');
return;
}
const providerConfig = {
openai: {
apiKey: keys.openai || '',
model: this.config.ai.openai?.model || 'gpt-4o-mini',
baseUrl: this.config.ai.openai?.baseUrl
},
mistral: {
apiKey: keys.mistral || '',
model: this.config.ai.mistral?.model || 'mistral-large-latest',
baseUrl: this.config.ai.mistral?.baseUrl
},
defaultProvider: keys.openai ? 'openai' : 'mistral'
};
// Recréer le client IA
const { AIClient } = await import('../ai/client.js');
this.dependencies.ai = new AIClient(providerConfig);
console.log('✅ Client IA rechargé avec les nouvelles clés');
} catch (error) {
console.log('❌ Erreur lors du rechargement:', error instanceof Error ? error.message : String(error));
}
}
private async handleAIQuery(query: string): Promise<void> {
try {
const response = await this.dependencies.ai.generateResponse({

View File

@ -62,27 +62,12 @@ export async function loadConfig(options: any = {}): Promise<Config> {
}
}
// Charger depuis les variables d'environnement
if (process.env.OPENAI_API_KEY) {
config.ai.openai = {
...config.ai.openai,
apiKey: process.env.OPENAI_API_KEY
};
}
if (process.env.MISTRAL_API_KEY) {
config.ai.mistral = {
...config.ai.mistral,
apiKey: process.env.MISTRAL_API_KEY
};
}
// Les clés API seront gérées par KeyManager
// On ne fait plus la validation ici car elle sera faite par AuthManager
// Fusionner avec les options passées en paramètre
config = { ...config, ...options };
// Validation
validateConfig(config);
return config;
}

View File

@ -71,10 +71,16 @@ Commandes générales:
exit, quit - Quitter NeuraTerm
clear - Effacer l'écran
Gestion des clés API:
keys - Afficher le statut des clés API
key set <provider> - Définir/mettre à jour une clé (openai, mistral)
key remove <provider> - Supprimer une clé
key status - Afficher le statut des clés
setup - Configuration interactive des clés
Gestion des providers:
providers - Lister les providers disponibles
provider <nom> - Changer de provider (openai, mistral)
models - Lister les modèles disponibles
Statistiques:
stats - Afficher les statistiques d'utilisation
@ -84,9 +90,13 @@ Statistiques:
Configuration:
config - Afficher la configuration actuelle
set <param> <value> - Modifier un paramètre
Pour poser une question à l'IA, tapez simplement votre message.
Exemples:
key set openai - Configurer votre clé OpenAI
provider mistral - Basculer vers Mistral AI
Comment optimiser ce code Python ?
`);
}
}