family-ai-agent 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
4
+ import { z } from 'zod';
5
+ import { createLogger } from '../utils/logger.js';
6
+
7
+ const logger = createLogger('UserConfig');
8
+
9
+ // Config directory and file paths
10
+ const CONFIG_DIR_NAME = '.family-ai-agent';
11
+ const CONFIG_FILE_NAME = 'config.json';
12
+
13
+ // Custom model schema
14
+ const CustomModelSchema = z.object({
15
+ id: z.string().min(1),
16
+ name: z.string().min(1),
17
+ contextWindow: z.number().positive().default(8192),
18
+ maxOutput: z.number().positive().default(4096),
19
+ description: z.string().optional(),
20
+ });
21
+
22
+ export type CustomModel = z.infer<typeof CustomModelSchema>;
23
+
24
+ // User config schema
25
+ const UserConfigSchema = z.object({
26
+ // API Configuration
27
+ apiKey: z.string().min(1, 'API key is required'),
28
+ baseUrl: z.string().url().default('https://openrouter.ai/api/v1'),
29
+
30
+ // Model Configuration
31
+ defaultModel: z.string().default('anthropic/claude-3.5-sonnet'),
32
+ fastModel: z.string().default('anthropic/claude-3-haiku'),
33
+ embeddingModel: z.string().default('openai/text-embedding-3-small'),
34
+
35
+ // Custom Models
36
+ customModels: z.array(CustomModelSchema).default([]),
37
+
38
+ // Optional Settings
39
+ temperature: z.number().min(0).max(2).optional(),
40
+ maxTokens: z.number().positive().optional(),
41
+
42
+ // Feature Flags
43
+ enableSafetyFilters: z.boolean().default(true),
44
+ enableAuditLogging: z.boolean().default(true),
45
+
46
+ // Database (optional - uses defaults if not set)
47
+ database: z.object({
48
+ host: z.string().default('localhost'),
49
+ port: z.number().default(5432),
50
+ user: z.string().default('familyai'),
51
+ password: z.string().default('familyai123'),
52
+ name: z.string().default('familyai'),
53
+ }).optional(),
54
+
55
+ // Metadata
56
+ createdAt: z.string().datetime().optional(),
57
+ updatedAt: z.string().datetime().optional(),
58
+ });
59
+
60
+ export type UserConfig = z.infer<typeof UserConfigSchema>;
61
+
62
+ // Partial config for updates
63
+ export type PartialUserConfig = Partial<Omit<UserConfig, 'createdAt' | 'updatedAt'>>;
64
+
65
+ /**
66
+ * Get the config directory path
67
+ */
68
+ export function getConfigDir(): string {
69
+ return join(homedir(), CONFIG_DIR_NAME);
70
+ }
71
+
72
+ /**
73
+ * Get the config file path
74
+ */
75
+ export function getConfigPath(): string {
76
+ return join(getConfigDir(), CONFIG_FILE_NAME);
77
+ }
78
+
79
+ /**
80
+ * Check if config file exists
81
+ */
82
+ export function configExists(): boolean {
83
+ return existsSync(getConfigPath());
84
+ }
85
+
86
+ /**
87
+ * Ensure config directory exists
88
+ */
89
+ export function ensureConfigDir(): void {
90
+ const configDir = getConfigDir();
91
+ if (!existsSync(configDir)) {
92
+ mkdirSync(configDir, { recursive: true });
93
+ logger.debug('Config directory created', { path: configDir });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Load user config from file
99
+ */
100
+ export function loadUserConfig(): UserConfig | null {
101
+ const configPath = getConfigPath();
102
+
103
+ if (!existsSync(configPath)) {
104
+ logger.debug('Config file not found', { path: configPath });
105
+ return null;
106
+ }
107
+
108
+ try {
109
+ const fileContent = readFileSync(configPath, 'utf-8');
110
+ const rawConfig = JSON.parse(fileContent);
111
+ const parsed = UserConfigSchema.safeParse(rawConfig);
112
+
113
+ if (!parsed.success) {
114
+ logger.warn('Config file validation failed', {
115
+ errors: parsed.error.format(),
116
+ });
117
+ return null;
118
+ }
119
+
120
+ logger.debug('Config loaded successfully');
121
+ return parsed.data;
122
+ } catch (error) {
123
+ logger.error('Failed to load config file', {
124
+ error: error instanceof Error ? error.message : 'Unknown error',
125
+ });
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Save user config to file
132
+ */
133
+ export function saveUserConfig(config: PartialUserConfig): UserConfig {
134
+ ensureConfigDir();
135
+ const configPath = getConfigPath();
136
+
137
+ // Load existing config if present
138
+ const existingConfig = loadUserConfig();
139
+
140
+ // Merge with existing config
141
+ const mergedConfig = {
142
+ ...existingConfig,
143
+ ...config,
144
+ updatedAt: new Date().toISOString(),
145
+ };
146
+
147
+ // Add createdAt if new config
148
+ if (!existingConfig) {
149
+ mergedConfig.createdAt = new Date().toISOString();
150
+ }
151
+
152
+ // Validate merged config
153
+ const parsed = UserConfigSchema.safeParse(mergedConfig);
154
+ if (!parsed.success) {
155
+ throw new Error(`Invalid config: ${JSON.stringify(parsed.error.format())}`);
156
+ }
157
+
158
+ // Write to file
159
+ writeFileSync(configPath, JSON.stringify(parsed.data, null, 2), 'utf-8');
160
+ logger.info('Config saved successfully', { path: configPath });
161
+
162
+ return parsed.data;
163
+ }
164
+
165
+ /**
166
+ * Update specific config values
167
+ */
168
+ export function updateUserConfig(updates: PartialUserConfig): UserConfig {
169
+ const existingConfig = loadUserConfig();
170
+
171
+ if (!existingConfig) {
172
+ throw new Error('No config file found. Run setup first.');
173
+ }
174
+
175
+ return saveUserConfig({
176
+ ...existingConfig,
177
+ ...updates,
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Delete config file
183
+ */
184
+ export function deleteUserConfig(): boolean {
185
+ const configPath = getConfigPath();
186
+
187
+ if (!existsSync(configPath)) {
188
+ return false;
189
+ }
190
+
191
+ try {
192
+ unlinkSync(configPath);
193
+ logger.info('Config deleted', { path: configPath });
194
+ return true;
195
+ } catch (error) {
196
+ logger.error('Failed to delete config', {
197
+ error: error instanceof Error ? error.message : 'Unknown error',
198
+ });
199
+ return false;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get a specific config value
205
+ */
206
+ export function getConfigValue<K extends keyof UserConfig>(
207
+ key: K
208
+ ): UserConfig[K] | undefined {
209
+ const config = loadUserConfig();
210
+ return config?.[key];
211
+ }
212
+
213
+ /**
214
+ * Add a custom model
215
+ */
216
+ export function addCustomModel(model: CustomModel): UserConfig {
217
+ const config = loadUserConfig();
218
+
219
+ if (!config) {
220
+ throw new Error('No config file found. Run setup first.');
221
+ }
222
+
223
+ // Check if model already exists
224
+ const existingIndex = config.customModels.findIndex((m) => m.id === model.id);
225
+
226
+ if (existingIndex >= 0) {
227
+ // Update existing model
228
+ config.customModels[existingIndex] = model;
229
+ } else {
230
+ // Add new model
231
+ config.customModels.push(model);
232
+ }
233
+
234
+ return saveUserConfig(config);
235
+ }
236
+
237
+ /**
238
+ * Remove a custom model
239
+ */
240
+ export function removeCustomModel(modelId: string): UserConfig {
241
+ const config = loadUserConfig();
242
+
243
+ if (!config) {
244
+ throw new Error('No config file found. Run setup first.');
245
+ }
246
+
247
+ config.customModels = config.customModels.filter((m) => m.id !== modelId);
248
+
249
+ return saveUserConfig(config);
250
+ }
251
+
252
+ /**
253
+ * Get all available models (built-in + custom)
254
+ */
255
+ export function getAllModels(): Array<{ id: string; name: string; isCustom: boolean }> {
256
+ const config = loadUserConfig();
257
+
258
+ // Built-in models from OpenRouter
259
+ const builtInModels = [
260
+ { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', isCustom: false },
261
+ { id: 'anthropic/claude-3-haiku', name: 'Claude 3 Haiku', isCustom: false },
262
+ { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', isCustom: false },
263
+ { id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', isCustom: false },
264
+ { id: 'openai/gpt-4o', name: 'GPT-4o', isCustom: false },
265
+ { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini', isCustom: false },
266
+ { id: 'google/gemini-pro-1.5', name: 'Gemini Pro 1.5', isCustom: false },
267
+ { id: 'meta-llama/llama-3.1-70b-instruct', name: 'Llama 3.1 70B', isCustom: false },
268
+ { id: 'meta-llama/llama-3.1-8b-instruct', name: 'Llama 3.1 8B', isCustom: false },
269
+ { id: 'mistralai/mistral-large', name: 'Mistral Large', isCustom: false },
270
+ { id: 'deepseek/deepseek-chat', name: 'DeepSeek Chat', isCustom: false },
271
+ ];
272
+
273
+ // Add custom models
274
+ const customModels = (config?.customModels || []).map((m) => ({
275
+ id: m.id,
276
+ name: m.name,
277
+ isCustom: true,
278
+ }));
279
+
280
+ return [...builtInModels, ...customModels];
281
+ }
282
+
283
+ /**
284
+ * Mask API key for display
285
+ */
286
+ export function maskApiKey(apiKey: string): string {
287
+ if (apiKey.length <= 8) {
288
+ return '****';
289
+ }
290
+ return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
291
+ }
292
+
293
+ /**
294
+ * Get config for display (with masked API key)
295
+ */
296
+ export function getDisplayConfig(): Record<string, unknown> | null {
297
+ const config = loadUserConfig();
298
+
299
+ if (!config) {
300
+ return null;
301
+ }
302
+
303
+ return {
304
+ ...config,
305
+ apiKey: maskApiKey(config.apiKey),
306
+ };
307
+ }
308
+
309
+ export default {
310
+ getConfigDir,
311
+ getConfigPath,
312
+ configExists,
313
+ loadUserConfig,
314
+ saveUserConfig,
315
+ updateUserConfig,
316
+ deleteUserConfig,
317
+ getConfigValue,
318
+ addCustomModel,
319
+ removeCustomModel,
320
+ getAllModels,
321
+ maskApiKey,
322
+ getDisplayConfig,
323
+ };