Add YAML config system (types and io)

This commit is contained in:
Sarah Wooders
2026-01-28 22:50:49 -08:00
parent 2b5b1eda57
commit b72150c193
5 changed files with 334 additions and 2 deletions

18
package-lock.json generated
View File

@@ -26,7 +26,8 @@
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"telegram-markdown-v2": "^0.0.4", "telegram-markdown-v2": "^0.0.4",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3" "typescript": "^5.9.3",
"yaml": "^2.8.2"
}, },
"bin": { "bin": {
"lettabot": "dist/cli.js", "lettabot": "dist/cli.js",
@@ -6384,6 +6385,21 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yauzl": { "node_modules/yauzl": {
"version": "2.10.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",

View File

@@ -52,7 +52,8 @@
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"telegram-markdown-v2": "^0.0.4", "telegram-markdown-v2": "^0.0.4",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3" "typescript": "^5.9.3",
"yaml": "^2.8.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"@slack/bolt": "^4.6.0", "@slack/bolt": "^4.6.0",

2
src/config/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './types.js';
export * from './io.js';

220
src/config/io.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* LettaBot Configuration I/O
*
* Config file location: ~/.lettabot/config.yaml (or ./lettabot.yaml in project)
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import YAML from 'yaml';
import type { LettaBotConfig, ProviderConfig } from './types.js';
import { DEFAULT_CONFIG } from './types.js';
// Config file locations (checked in order)
const CONFIG_PATHS = [
resolve(process.cwd(), 'lettabot.yaml'), // Project-local
resolve(process.cwd(), 'lettabot.yml'), // Project-local alt
join(homedir(), '.lettabot', 'config.yaml'), // User global
join(homedir(), '.lettabot', 'config.yml'), // User global alt
];
const DEFAULT_CONFIG_PATH = join(homedir(), '.lettabot', 'config.yaml');
/**
* Find the config file path (first existing, or default)
*/
export function resolveConfigPath(): string {
for (const p of CONFIG_PATHS) {
if (existsSync(p)) {
return p;
}
}
return DEFAULT_CONFIG_PATH;
}
/**
* Load config from YAML file
*/
export function loadConfig(): LettaBotConfig {
const configPath = resolveConfigPath();
if (!existsSync(configPath)) {
return { ...DEFAULT_CONFIG };
}
try {
const content = readFileSync(configPath, 'utf-8');
const parsed = YAML.parse(content) as Partial<LettaBotConfig>;
// Merge with defaults
return {
...DEFAULT_CONFIG,
...parsed,
server: { ...DEFAULT_CONFIG.server, ...parsed.server },
agent: { ...DEFAULT_CONFIG.agent, ...parsed.agent },
channels: { ...DEFAULT_CONFIG.channels, ...parsed.channels },
};
} catch (err) {
console.error(`[Config] Failed to load ${configPath}:`, err);
return { ...DEFAULT_CONFIG };
}
}
/**
* Save config to YAML file
*/
export function saveConfig(config: LettaBotConfig, path?: string): void {
const configPath = path || resolveConfigPath();
// Ensure directory exists
const dir = dirname(configPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
// Convert to YAML with comments
const content = YAML.stringify(config, {
indent: 2,
lineWidth: 0, // Don't wrap lines
});
writeFileSync(configPath, content, 'utf-8');
console.log(`[Config] Saved to ${configPath}`);
}
/**
* Get environment variables from config (for backwards compatibility)
*/
export function configToEnv(config: LettaBotConfig): Record<string, string> {
const env: Record<string, string> = {};
// Server
if (config.server.mode === 'selfhosted' && config.server.baseUrl) {
env.LETTA_BASE_URL = config.server.baseUrl;
}
if (config.server.apiKey) {
env.LETTA_API_KEY = config.server.apiKey;
}
// Agent
if (config.agent.id) {
env.LETTA_AGENT_ID = config.agent.id;
}
if (config.agent.name) {
env.AGENT_NAME = config.agent.name;
}
if (config.agent.model) {
env.MODEL = config.agent.model;
}
// Channels
if (config.channels.telegram?.token) {
env.TELEGRAM_BOT_TOKEN = config.channels.telegram.token;
if (config.channels.telegram.dmPolicy) {
env.TELEGRAM_DM_POLICY = config.channels.telegram.dmPolicy;
}
}
if (config.channels.slack?.appToken) {
env.SLACK_APP_TOKEN = config.channels.slack.appToken;
}
if (config.channels.slack?.botToken) {
env.SLACK_BOT_TOKEN = config.channels.slack.botToken;
}
if (config.channels.whatsapp?.enabled) {
env.WHATSAPP_ENABLED = 'true';
if (config.channels.whatsapp.selfChat) {
env.WHATSAPP_SELF_CHAT_MODE = 'true';
}
}
if (config.channels.signal?.phone) {
env.SIGNAL_PHONE_NUMBER = config.channels.signal.phone;
}
// Features
if (config.features?.cron) {
env.CRON_ENABLED = 'true';
}
if (config.features?.heartbeat?.enabled) {
env.HEARTBEAT_INTERVAL_MIN = String(config.features.heartbeat.intervalMin || 30);
}
return env;
}
/**
* Apply config to process.env
*/
export function applyConfigToEnv(config: LettaBotConfig): void {
const env = configToEnv(config);
for (const [key, value] of Object.entries(env)) {
if (!process.env[key]) {
process.env[key] = value;
}
}
}
/**
* Create BYOK providers on Letta Cloud
*/
export async function syncProviders(config: LettaBotConfig): Promise<void> {
if (config.server.mode !== 'cloud' || !config.server.apiKey) {
return;
}
if (!config.providers || config.providers.length === 0) {
return;
}
const apiKey = config.server.apiKey;
const baseUrl = 'https://api.letta.com';
// List existing providers
const listResponse = await fetch(`${baseUrl}/v1/providers`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
});
const existingProviders = listResponse.ok
? await listResponse.json() as Array<{ id: string; name: string }>
: [];
// Create or update each provider
for (const provider of config.providers) {
const existing = existingProviders.find(p => p.name === provider.name);
try {
if (existing) {
// Update existing
await fetch(`${baseUrl}/v1/providers/${existing.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ api_key: provider.apiKey }),
});
console.log(`[Config] Updated provider: ${provider.name}`);
} else {
// Create new
await fetch(`${baseUrl}/v1/providers`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
name: provider.name,
provider_type: provider.type,
api_key: provider.apiKey,
}),
});
console.log(`[Config] Created provider: ${provider.name}`);
}
} catch (err) {
console.error(`[Config] Failed to sync provider ${provider.name}:`, err);
}
}
}

93
src/config/types.ts Normal file
View File

@@ -0,0 +1,93 @@
/**
* LettaBot Configuration Types
*
* Two modes:
* 1. Self-hosted: Uses baseUrl (e.g., http://localhost:8283), no API key
* 2. Letta Cloud: Uses apiKey, optional BYOK providers
*/
export interface LettaBotConfig {
// Server connection
server: {
// 'cloud' (api.letta.com) or 'selfhosted'
mode: 'cloud' | 'selfhosted';
// Only for selfhosted mode
baseUrl?: string;
// Only for cloud mode
apiKey?: string;
};
// Agent configuration
agent: {
id?: string;
name: string;
model: string;
};
// BYOK providers (cloud mode only)
providers?: ProviderConfig[];
// Channel configurations
channels: {
telegram?: TelegramConfig;
slack?: SlackConfig;
whatsapp?: WhatsAppConfig;
signal?: SignalConfig;
};
// Features
features?: {
cron?: boolean;
heartbeat?: {
enabled: boolean;
intervalMin?: number;
};
};
}
export interface ProviderConfig {
id: string; // e.g., 'anthropic', 'openai'
name: string; // e.g., 'lc-anthropic'
type: string; // e.g., 'anthropic', 'openai'
apiKey: string;
}
export interface TelegramConfig {
enabled: boolean;
token?: string;
dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[];
}
export interface SlackConfig {
enabled: boolean;
appToken?: string;
botToken?: string;
allowedUsers?: string[];
}
export interface WhatsAppConfig {
enabled: boolean;
selfChat?: boolean;
dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[];
}
export interface SignalConfig {
enabled: boolean;
phone?: string;
dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[];
}
// Default config
export const DEFAULT_CONFIG: LettaBotConfig = {
server: {
mode: 'cloud',
},
agent: {
name: 'LettaBot',
model: 'zai/glm-4.7', // Free model default
},
channels: {},
};