Add secure credential storage (GNOME Keyring + file fallback), LLM provider config in ~/.mcpctl/config.json, interactive setup wizard (mcpctl config setup), and wire configured provider into mcplocal for smart pagination summaries. - Secret store: SecretStore interface, GnomeKeyringStore, FileSecretStore - Config schema: LlmConfigSchema with provider/model/url/binaryPath - Setup wizard: arrow-key provider/model selection, dynamic model fetch - Provider factory: creates ProviderRegistry from config + secrets - Status: shows LLM line with hint when not configured - 572 tests passing across all packages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64 lines
1.6 KiB
TypeScript
64 lines
1.6 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
import type { SecretStore, SecretStoreDeps } from './types.js';
|
|
|
|
function defaultConfigDir(): string {
|
|
return join(homedir(), '.mcpctl');
|
|
}
|
|
|
|
function secretsPath(configDir: string): string {
|
|
return join(configDir, 'secrets');
|
|
}
|
|
|
|
export class FileSecretStore implements SecretStore {
|
|
private readonly configDir: string;
|
|
|
|
constructor(deps?: SecretStoreDeps) {
|
|
this.configDir = deps?.configDir ?? defaultConfigDir();
|
|
}
|
|
|
|
backend(): string {
|
|
return 'file';
|
|
}
|
|
|
|
async get(key: string): Promise<string | null> {
|
|
const data = this.readAll();
|
|
return data[key] ?? null;
|
|
}
|
|
|
|
async set(key: string, value: string): Promise<void> {
|
|
const data = this.readAll();
|
|
data[key] = value;
|
|
this.writeAll(data);
|
|
}
|
|
|
|
async delete(key: string): Promise<boolean> {
|
|
const data = this.readAll();
|
|
if (!(key in data)) return false;
|
|
delete data[key];
|
|
this.writeAll(data);
|
|
return true;
|
|
}
|
|
|
|
private readAll(): Record<string, string> {
|
|
const path = secretsPath(this.configDir);
|
|
if (!existsSync(path)) return {};
|
|
try {
|
|
const raw = readFileSync(path, 'utf-8');
|
|
return JSON.parse(raw) as Record<string, string>;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
private writeAll(data: Record<string, string>): void {
|
|
if (!existsSync(this.configDir)) {
|
|
mkdirSync(this.configDir, { recursive: true });
|
|
}
|
|
const path = secretsPath(this.configDir);
|
|
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
chmodSync(path, 0o600);
|
|
}
|
|
}
|