Files
mcpctl/src/shared/src/secrets/file-store.ts
Michal 7c23da10c6
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled
feat: LLM provider configuration, secret store, and setup wizard
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>
2026-02-24 22:48:17 +00:00

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);
}
}