Add YAML config system (types and io)
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -26,7 +26,8 @@
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"telegram-markdown-v2": "^0.0.4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"bin": {
|
||||
"lettabot": "dist/cli.js",
|
||||
@@ -6384,6 +6385,21 @@
|
||||
"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": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"telegram-markdown-v2": "^0.0.4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@slack/bolt": "^4.6.0",
|
||||
|
||||
2
src/config/index.ts
Normal file
2
src/config/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './types.js';
|
||||
export * from './io.js';
|
||||
220
src/config/io.ts
Normal file
220
src/config/io.ts
Normal 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
93
src/config/types.ts
Normal 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: {},
|
||||
};
|
||||
Reference in New Issue
Block a user