feat(setup): add config builder and summary renderer
Add ConfigBuilder class to accumulate wizard answers into config objects with YAML serialization, and renderSummary function to display configuration summary. Includes 9 test cases covering provider setup, channel configuration, and feature flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ConfigBuilder } from './config.js';
|
||||
|
||||
describe('ConfigBuilder', () => {
|
||||
it('creates minimal config with anthropic + webchat', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setProvider('default', { provider: 'anthropic', model: 'claude-sonnet-4-20250514', api_key: 'sk-ant-test' });
|
||||
builder.setGatewayPort(3777);
|
||||
const obj = builder.build();
|
||||
expect(obj.models.default.provider).toBe('anthropic');
|
||||
expect(obj.models.default.api_key).toBe('sk-ant-test');
|
||||
expect(obj.server.port).toBe(3777);
|
||||
});
|
||||
|
||||
it('adds telegram channel', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' });
|
||||
builder.setTelegram('123:ABC', [12345]);
|
||||
const obj = builder.build();
|
||||
expect(obj.telegram.bot_token).toBe('123:ABC');
|
||||
expect(obj.telegram.allowed_chat_ids).toEqual([12345]);
|
||||
});
|
||||
|
||||
it('adds discord channel', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' });
|
||||
builder.setDiscord('MTIz.test', ['guild1']);
|
||||
const obj = builder.build();
|
||||
expect(obj.discord.bot_token).toBe('MTIz.test');
|
||||
expect(obj.discord.allowed_guild_ids).toEqual(['guild1']);
|
||||
});
|
||||
|
||||
it('adds fast tier', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' });
|
||||
builder.setProvider('fast', { provider: 'anthropic', model: 'claude-haiku-4-5-20251001', api_key: 'k' });
|
||||
const obj = builder.build();
|
||||
expect(obj.models.fast.provider).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('serializes to valid YAML string', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setProvider('default', { provider: 'ollama', model: 'llama3.3', endpoint: 'http://localhost:11434' });
|
||||
builder.setGatewayPort(3777);
|
||||
const yaml = builder.toYaml();
|
||||
expect(yaml).toContain('provider: ollama');
|
||||
expect(yaml).toContain('model: llama3.3');
|
||||
expect(yaml).toContain('port: 3777');
|
||||
});
|
||||
|
||||
it('loads from existing config object', () => {
|
||||
const existing = {
|
||||
models: { default: { provider: 'openai', model: 'gpt-4.1', api_key: 'sk-test' } },
|
||||
server: { port: 9999 },
|
||||
telegram: { bot_token: '123:ABC', allowed_chat_ids: [111] },
|
||||
};
|
||||
const builder = ConfigBuilder.fromObject(existing);
|
||||
const obj = builder.build();
|
||||
expect(obj.models.default.provider).toBe('openai');
|
||||
expect(obj.server.port).toBe(9999);
|
||||
expect(obj.telegram.bot_token).toBe('123:ABC');
|
||||
});
|
||||
|
||||
it('sets memory embedding config', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' });
|
||||
builder.setMemoryEmbedding({ provider: 'openai', api_key: 'sk-emb' });
|
||||
const obj = builder.build();
|
||||
expect(obj.memory.embedding.enabled).toBe(true);
|
||||
expect(obj.memory.embedding.provider).toBe('openai');
|
||||
expect(obj.memory.embedding.api_key).toBe('sk-emb');
|
||||
});
|
||||
|
||||
it('sets sandbox enabled', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setSandboxEnabled(true);
|
||||
const obj = builder.build();
|
||||
expect(obj.sandbox.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('sets gateway auth token', () => {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.setGatewayToken('my-secret-token');
|
||||
const obj = builder.build();
|
||||
expect(obj.server.token).toBe('my-secret-token');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
interface ProviderConfig {
|
||||
provider: string;
|
||||
model: string;
|
||||
api_key?: string;
|
||||
auth_token?: string;
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
interface EmbeddingConfig {
|
||||
provider: string;
|
||||
api_key?: string;
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export class ConfigBuilder {
|
||||
private config: Record<string, unknown>;
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
log_level: 'info',
|
||||
models: {},
|
||||
server: { port: 18800, localhost: true },
|
||||
hooks: {
|
||||
confirm: ['shell.*', 'file.write', 'file.patch'],
|
||||
log: ['web.*', 'file.read'],
|
||||
silent: ['notify'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static fromObject(obj: Record<string, unknown>): ConfigBuilder {
|
||||
const builder = new ConfigBuilder();
|
||||
builder.config = structuredClone(obj);
|
||||
return builder;
|
||||
}
|
||||
|
||||
setProvider(tier: 'default' | 'fast' | 'complex' | 'local', cfg: ProviderConfig): void {
|
||||
const models = (this.config.models ?? {}) as Record<string, unknown>;
|
||||
const entry: Record<string, unknown> = { provider: cfg.provider, model: cfg.model };
|
||||
if (cfg.api_key) entry.api_key = cfg.api_key;
|
||||
if (cfg.auth_token) entry.auth_token = cfg.auth_token;
|
||||
if (cfg.endpoint) entry.endpoint = cfg.endpoint;
|
||||
models[tier] = entry;
|
||||
this.config.models = models;
|
||||
}
|
||||
|
||||
setTelegram(botToken: string, chatIds: number[]): void {
|
||||
this.config.telegram = { bot_token: botToken, allowed_chat_ids: chatIds };
|
||||
}
|
||||
|
||||
setDiscord(botToken: string, guildIds: string[]): void {
|
||||
this.config.discord = { bot_token: botToken, allowed_guild_ids: guildIds };
|
||||
}
|
||||
|
||||
setSlack(botToken: string, appToken: string, signingSecret: string, channelIds: string[]): void {
|
||||
this.config.slack = { bot_token: botToken, app_token: appToken, signing_secret: signingSecret, allowed_channel_ids: channelIds };
|
||||
}
|
||||
|
||||
setWhatsApp(allowedNumbers: string[]): void {
|
||||
this.config.whatsapp = { allowed_numbers: allowedNumbers };
|
||||
}
|
||||
|
||||
setGatewayPort(port: number): void {
|
||||
const server = (this.config.server ?? {}) as Record<string, unknown>;
|
||||
server.port = port;
|
||||
this.config.server = server;
|
||||
}
|
||||
|
||||
setGatewayToken(token: string): void {
|
||||
const server = (this.config.server ?? {}) as Record<string, unknown>;
|
||||
server.token = token;
|
||||
this.config.server = server;
|
||||
}
|
||||
|
||||
setGatewayLock(enabled: boolean): void {
|
||||
const server = (this.config.server ?? {}) as Record<string, unknown>;
|
||||
server.lock = enabled;
|
||||
this.config.server = server;
|
||||
}
|
||||
|
||||
setTailscaleServe(enabled: boolean): void {
|
||||
const server = (this.config.server ?? {}) as Record<string, unknown>;
|
||||
const tailscale = (server.tailscale ?? {}) as Record<string, unknown>;
|
||||
tailscale.serve = enabled;
|
||||
server.tailscale = tailscale;
|
||||
this.config.server = server;
|
||||
}
|
||||
|
||||
setMemoryEmbedding(cfg: EmbeddingConfig): void {
|
||||
const memory = (this.config.memory ?? {}) as Record<string, unknown>;
|
||||
const embedding: Record<string, unknown> = { enabled: true, provider: cfg.provider };
|
||||
if (cfg.api_key) embedding.api_key = cfg.api_key;
|
||||
if (cfg.endpoint) embedding.endpoint = cfg.endpoint;
|
||||
memory.embedding = embedding;
|
||||
this.config.memory = memory;
|
||||
}
|
||||
|
||||
setSandboxEnabled(enabled: boolean): void {
|
||||
this.config.sandbox = { enabled };
|
||||
}
|
||||
|
||||
setPairingEnabled(enabled: boolean): void {
|
||||
this.config.pairing = { enabled };
|
||||
}
|
||||
|
||||
setToolProfile(profile: string): void {
|
||||
this.config.tools = { profile };
|
||||
}
|
||||
|
||||
setWebhooksEnabled(secret?: string): void {
|
||||
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
|
||||
if (secret) {
|
||||
automation.webhooks = [{ name: 'default', secret, message: '{{body}}', output: { channel: 'webchat', peer: 'webhook' }, enabled: true }];
|
||||
} else {
|
||||
automation.webhooks = automation.webhooks ?? [];
|
||||
}
|
||||
this.config.automation = automation;
|
||||
}
|
||||
|
||||
setGmailEnabled(credentialsFile: string, outputChannel: string, outputPeer: string): void {
|
||||
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
|
||||
automation.gmail = { enabled: true, credentials_file: credentialsFile, output: { channel: outputChannel, peer: outputPeer } };
|
||||
this.config.automation = automation;
|
||||
}
|
||||
|
||||
setCronEnabled(): void {
|
||||
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
|
||||
if (!automation.cron) automation.cron = [];
|
||||
this.config.automation = automation;
|
||||
}
|
||||
|
||||
build(): Record<string, any> {
|
||||
return structuredClone(this.config) as Record<string, any>;
|
||||
}
|
||||
|
||||
toYaml(): string {
|
||||
return stringify(this.config, { lineWidth: 120 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export function renderSummary(config: Record<string, any>): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
const models = config.models ?? {};
|
||||
const tiers = ['default', 'fast', 'complex', 'local']
|
||||
.filter(t => models[t])
|
||||
.map(t => `${t}:${models[t].provider}`)
|
||||
.join(', ');
|
||||
lines.push(` Models: ${tiers || 'none configured'}`);
|
||||
|
||||
const channels: string[] = [];
|
||||
if (config.server?.port) channels.push('webchat');
|
||||
if (config.telegram) channels.push('telegram');
|
||||
if (config.discord) channels.push('discord');
|
||||
if (config.slack) channels.push('slack');
|
||||
if (config.whatsapp) channels.push('whatsapp');
|
||||
lines.push(` Channels: ${channels.join(', ') || 'none'}`);
|
||||
|
||||
const embedding = config.memory?.embedding;
|
||||
const memoryStatus = embedding?.enabled ? `vector search (${embedding.provider})` : 'keyword search (no embeddings)';
|
||||
lines.push(` Memory: ${memoryStatus}`);
|
||||
|
||||
const auto = config.automation ?? {};
|
||||
const autoFeatures: string[] = [];
|
||||
if (auto.cron?.length > 0) autoFeatures.push(`${auto.cron.length} cron jobs`);
|
||||
if (auto.webhooks?.length > 0) autoFeatures.push('webhooks');
|
||||
if (auto.gmail?.enabled) autoFeatures.push('gmail');
|
||||
if (auto.heartbeat?.enabled) autoFeatures.push('heartbeat');
|
||||
lines.push(` Automation: ${autoFeatures.join(', ') || 'none'}`);
|
||||
|
||||
const secFeatures: string[] = [];
|
||||
secFeatures.push(`tools:${config.tools?.profile ?? 'full'}`);
|
||||
if (config.sandbox?.enabled) secFeatures.push('sandbox');
|
||||
if (config.pairing?.enabled) secFeatures.push('pairing');
|
||||
lines.push(` Security: ${secFeatures.join(', ')}`);
|
||||
|
||||
const gw: string[] = [];
|
||||
gw.push(`port ${config.server?.port ?? 18800}`);
|
||||
if (config.server?.token) gw.push('auth');
|
||||
if (config.server?.lock) gw.push('locked');
|
||||
if (config.server?.tailscale?.serve) gw.push('tailscale');
|
||||
lines.push(` Gateway: ${gw.join(', ')}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user