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",
|
"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",
|
||||||
|
|||||||
@@ -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
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