family-ai-agent 1.0.2 → 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,343 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import boxen from 'boxen';
5
+
6
+ import {
7
+ saveUserConfig,
8
+ getAllModels,
9
+ getConfigPath,
10
+ type PartialUserConfig,
11
+ } from '../config/user-config.js';
12
+
13
+ /**
14
+ * Display welcome banner for setup
15
+ */
16
+ function showSetupBanner(): void {
17
+ const banner = boxen(
18
+ chalk.bold.cyan('Family AI Agent Setup') +
19
+ '\n\n' +
20
+ chalk.white('Configure your AI agent in a few simple steps') +
21
+ '\n' +
22
+ chalk.gray('Your settings will be saved locally'),
23
+ {
24
+ padding: 1,
25
+ margin: 1,
26
+ borderStyle: 'round',
27
+ borderColor: 'cyan',
28
+ }
29
+ );
30
+ console.log(banner);
31
+ }
32
+
33
+ /**
34
+ * Test API connection
35
+ */
36
+ async function testConnection(
37
+ apiKey: string,
38
+ baseUrl: string
39
+ ): Promise<{ success: boolean; error?: string }> {
40
+ try {
41
+ const response = await fetch(`${baseUrl}/models`, {
42
+ headers: {
43
+ Authorization: `Bearer ${apiKey}`,
44
+ 'HTTP-Referer': 'https://family-ai-agent.local',
45
+ 'X-Title': 'Family AI Agent Setup',
46
+ },
47
+ });
48
+
49
+ if (response.ok) {
50
+ return { success: true };
51
+ }
52
+
53
+ const errorData = await response.json().catch(() => ({}));
54
+ return {
55
+ success: false,
56
+ error: (errorData as Record<string, unknown>)?.error?.toString() || `HTTP ${response.status}`,
57
+ };
58
+ } catch (error) {
59
+ return {
60
+ success: false,
61
+ error: error instanceof Error ? error.message : 'Connection failed',
62
+ };
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Run the interactive setup wizard
68
+ */
69
+ export async function runSetupWizard(): Promise<boolean> {
70
+ showSetupBanner();
71
+
72
+ try {
73
+ // Step 1: API Key
74
+ console.log(chalk.bold('\nšŸ“ Step 1: API Configuration\n'));
75
+
76
+ const { apiKey } = await inquirer.prompt<{ apiKey: string }>([
77
+ {
78
+ type: 'password',
79
+ name: 'apiKey',
80
+ message: 'Enter your OpenRouter API Key:',
81
+ mask: '*',
82
+ validate: (input: string) => {
83
+ if (!input || input.trim().length === 0) {
84
+ return 'API key is required';
85
+ }
86
+ if (input.length < 10) {
87
+ return 'API key seems too short';
88
+ }
89
+ return true;
90
+ },
91
+ },
92
+ ]);
93
+
94
+ // Step 2: Base URL
95
+ const { baseUrl } = await inquirer.prompt<{ baseUrl: string }>([
96
+ {
97
+ type: 'input',
98
+ name: 'baseUrl',
99
+ message: 'API Base URL:',
100
+ default: 'https://openrouter.ai/api/v1',
101
+ validate: (input: string) => {
102
+ try {
103
+ new URL(input);
104
+ return true;
105
+ } catch {
106
+ return 'Please enter a valid URL';
107
+ }
108
+ },
109
+ },
110
+ ]);
111
+
112
+ // Test connection
113
+ const spinner = ora('Testing API connection...').start();
114
+ const testResult = await testConnection(apiKey, baseUrl);
115
+
116
+ if (!testResult.success) {
117
+ spinner.fail(`Connection failed: ${testResult.error}`);
118
+
119
+ const { continueAnyway } = await inquirer.prompt<{ continueAnyway: boolean }>([
120
+ {
121
+ type: 'confirm',
122
+ name: 'continueAnyway',
123
+ message: 'Continue with setup anyway?',
124
+ default: false,
125
+ },
126
+ ]);
127
+
128
+ if (!continueAnyway) {
129
+ console.log(chalk.yellow('\nSetup cancelled. Please check your API key and try again.\n'));
130
+ return false;
131
+ }
132
+ } else {
133
+ spinner.succeed('API connection successful!');
134
+ }
135
+
136
+ // Step 3: Model Selection
137
+ console.log(chalk.bold('\nšŸ¤– Step 2: Model Configuration\n'));
138
+
139
+ const models = getAllModels();
140
+ const modelChoices = [
141
+ ...models.map((m) => ({
142
+ name: `${m.name} (${m.id})`,
143
+ value: m.id,
144
+ })),
145
+ { name: chalk.gray('Enter custom model ID...'), value: '__custom__' },
146
+ ];
147
+
148
+ const { defaultModel } = await inquirer.prompt<{ defaultModel: string }>([
149
+ {
150
+ type: 'list',
151
+ name: 'defaultModel',
152
+ message: 'Select your default model:',
153
+ choices: modelChoices,
154
+ default: 'anthropic/claude-3.5-sonnet',
155
+ },
156
+ ]);
157
+
158
+ let finalDefaultModel = defaultModel;
159
+
160
+ if (defaultModel === '__custom__') {
161
+ const { customModel } = await inquirer.prompt<{ customModel: string }>([
162
+ {
163
+ type: 'input',
164
+ name: 'customModel',
165
+ message: 'Enter custom model ID (e.g., provider/model-name):',
166
+ validate: (input: string) => {
167
+ if (!input || input.trim().length === 0) {
168
+ return 'Model ID is required';
169
+ }
170
+ return true;
171
+ },
172
+ },
173
+ ]);
174
+ finalDefaultModel = customModel;
175
+ }
176
+
177
+ // Step 4: Fast Model (optional)
178
+ const { configureFastModel } = await inquirer.prompt<{ configureFastModel: boolean }>([
179
+ {
180
+ type: 'confirm',
181
+ name: 'configureFastModel',
182
+ message: 'Configure a separate fast model for quick tasks?',
183
+ default: true,
184
+ },
185
+ ]);
186
+
187
+ let fastModel = 'anthropic/claude-3-haiku';
188
+
189
+ if (configureFastModel) {
190
+ const { selectedFastModel } = await inquirer.prompt<{ selectedFastModel: string }>([
191
+ {
192
+ type: 'list',
193
+ name: 'selectedFastModel',
194
+ message: 'Select fast model:',
195
+ choices: modelChoices,
196
+ default: 'anthropic/claude-3-haiku',
197
+ },
198
+ ]);
199
+
200
+ if (selectedFastModel === '__custom__') {
201
+ const { customFastModel } = await inquirer.prompt<{ customFastModel: string }>([
202
+ {
203
+ type: 'input',
204
+ name: 'customFastModel',
205
+ message: 'Enter custom fast model ID:',
206
+ },
207
+ ]);
208
+ fastModel = customFastModel;
209
+ } else {
210
+ fastModel = selectedFastModel;
211
+ }
212
+ }
213
+
214
+ // Step 5: Safety Settings
215
+ console.log(chalk.bold('\nšŸ›”ļø Step 3: Safety Settings\n'));
216
+
217
+ const { enableSafetyFilters, enableAuditLogging } = await inquirer.prompt<{
218
+ enableSafetyFilters: boolean;
219
+ enableAuditLogging: boolean;
220
+ }>([
221
+ {
222
+ type: 'confirm',
223
+ name: 'enableSafetyFilters',
224
+ message: 'Enable content safety filters?',
225
+ default: true,
226
+ },
227
+ {
228
+ type: 'confirm',
229
+ name: 'enableAuditLogging',
230
+ message: 'Enable audit logging?',
231
+ default: true,
232
+ },
233
+ ]);
234
+
235
+ // Save configuration
236
+ console.log();
237
+ const saveSpinner = ora('Saving configuration...').start();
238
+
239
+ try {
240
+ const config: PartialUserConfig = {
241
+ apiKey,
242
+ baseUrl,
243
+ defaultModel: finalDefaultModel,
244
+ fastModel,
245
+ embeddingModel: 'openai/text-embedding-3-small',
246
+ enableSafetyFilters,
247
+ enableAuditLogging,
248
+ customModels: [],
249
+ };
250
+
251
+ saveUserConfig(config);
252
+ saveSpinner.succeed('Configuration saved!');
253
+
254
+ // Show summary
255
+ console.log(
256
+ boxen(
257
+ chalk.bold.green('Setup Complete!') +
258
+ '\n\n' +
259
+ chalk.white('Configuration saved to:') +
260
+ '\n' +
261
+ chalk.gray(getConfigPath()) +
262
+ '\n\n' +
263
+ chalk.white('Default Model: ') +
264
+ chalk.cyan(finalDefaultModel) +
265
+ '\n' +
266
+ chalk.white('Fast Model: ') +
267
+ chalk.cyan(fastModel) +
268
+ '\n\n' +
269
+ chalk.gray('Run `family-ai-agent` to start chatting!'),
270
+ {
271
+ padding: 1,
272
+ margin: 1,
273
+ borderStyle: 'round',
274
+ borderColor: 'green',
275
+ }
276
+ )
277
+ );
278
+
279
+ return true;
280
+ } catch (error) {
281
+ saveSpinner.fail('Failed to save configuration');
282
+ console.error(
283
+ chalk.red(error instanceof Error ? error.message : 'Unknown error')
284
+ );
285
+ return false;
286
+ }
287
+ } catch (error) {
288
+ // Handle Ctrl+C gracefully
289
+ if ((error as { name?: string }).name === 'ExitPromptError') {
290
+ console.log(chalk.yellow('\n\nSetup cancelled.\n'));
291
+ return false;
292
+ }
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Quick setup with minimal prompts
299
+ */
300
+ export async function runQuickSetup(apiKey: string): Promise<boolean> {
301
+ const spinner = ora('Configuring with defaults...').start();
302
+
303
+ try {
304
+ const config: PartialUserConfig = {
305
+ apiKey,
306
+ baseUrl: 'https://openrouter.ai/api/v1',
307
+ defaultModel: 'anthropic/claude-3.5-sonnet',
308
+ fastModel: 'anthropic/claude-3-haiku',
309
+ embeddingModel: 'openai/text-embedding-3-small',
310
+ enableSafetyFilters: true,
311
+ enableAuditLogging: true,
312
+ customModels: [],
313
+ };
314
+
315
+ saveUserConfig(config);
316
+ spinner.succeed('Configuration saved with defaults!');
317
+ return true;
318
+ } catch (error) {
319
+ spinner.fail('Failed to save configuration');
320
+ console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
321
+ return false;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Check if setup is needed and run wizard if so
327
+ */
328
+ export async function checkAndRunSetup(): Promise<boolean> {
329
+ const { configExists } = await import('../config/user-config.js');
330
+
331
+ if (configExists()) {
332
+ return true; // Already configured
333
+ }
334
+
335
+ console.log(chalk.yellow('\nāš ļø No configuration found. Starting setup wizard...\n'));
336
+ return runSetupWizard();
337
+ }
338
+
339
+ export default {
340
+ runSetupWizard,
341
+ runQuickSetup,
342
+ checkAndRunSetup,
343
+ };
@@ -1,11 +1,119 @@
1
1
  import { config as dotenvConfig } from 'dotenv';
2
2
  import { z } from 'zod';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
3
6
 
4
7
  dotenvConfig();
5
8
 
9
+ // User config path
10
+ const USER_CONFIG_PATH = join(homedir(), '.family-ai-agent', 'config.json');
11
+
12
+ // Try to load user config
13
+ function loadUserConfigFile(): Record<string, unknown> | null {
14
+ if (!existsSync(USER_CONFIG_PATH)) {
15
+ return null;
16
+ }
17
+
18
+ try {
19
+ const content = readFileSync(USER_CONFIG_PATH, 'utf-8');
20
+ return JSON.parse(content);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ // Get config value with priority: user config > env > default
27
+ function getConfigValue<T>(
28
+ userConfig: Record<string, unknown> | null,
29
+ userKey: string,
30
+ envKey: string,
31
+ envValue: string | undefined
32
+ ): string | undefined {
33
+ // Priority 1: User config
34
+ if (userConfig && userKey in userConfig) {
35
+ const value = userConfig[userKey];
36
+ if (typeof value === 'string') return value;
37
+ if (typeof value === 'number') return String(value);
38
+ if (typeof value === 'boolean') return String(value);
39
+ }
40
+
41
+ // Priority 2: Environment variable
42
+ return envValue;
43
+ }
44
+
45
+ // Load user config if available
46
+ const userConfig = loadUserConfigFile();
47
+
48
+ // Build merged environment for Zod schema
49
+ const mergedEnv = {
50
+ ...process.env,
51
+
52
+ // Override with user config if available
53
+ OPENROUTER_API_KEY: getConfigValue(
54
+ userConfig,
55
+ 'apiKey',
56
+ 'OPENROUTER_API_KEY',
57
+ process.env.OPENROUTER_API_KEY
58
+ ),
59
+ OPENROUTER_BASE_URL: getConfigValue(
60
+ userConfig,
61
+ 'baseUrl',
62
+ 'OPENROUTER_BASE_URL',
63
+ process.env.OPENROUTER_BASE_URL
64
+ ),
65
+ DEFAULT_MODEL: getConfigValue(
66
+ userConfig,
67
+ 'defaultModel',
68
+ 'DEFAULT_MODEL',
69
+ process.env.DEFAULT_MODEL
70
+ ),
71
+ FAST_MODEL: getConfigValue(
72
+ userConfig,
73
+ 'fastModel',
74
+ 'FAST_MODEL',
75
+ process.env.FAST_MODEL
76
+ ),
77
+ EMBEDDING_MODEL: getConfigValue(
78
+ userConfig,
79
+ 'embeddingModel',
80
+ 'EMBEDDING_MODEL',
81
+ process.env.EMBEDDING_MODEL
82
+ ),
83
+ ENABLE_CONTENT_FILTER: getConfigValue(
84
+ userConfig,
85
+ 'enableSafetyFilters',
86
+ 'ENABLE_CONTENT_FILTER',
87
+ process.env.ENABLE_CONTENT_FILTER
88
+ ),
89
+ ENABLE_AUDIT_LOGGING: getConfigValue(
90
+ userConfig,
91
+ 'enableAuditLogging',
92
+ 'ENABLE_AUDIT_LOGGING',
93
+ process.env.ENABLE_AUDIT_LOGGING
94
+ ),
95
+
96
+ // Database from user config
97
+ DB_HOST: userConfig?.database && typeof userConfig.database === 'object'
98
+ ? (userConfig.database as Record<string, unknown>).host as string
99
+ : process.env.DB_HOST,
100
+ DB_PORT: userConfig?.database && typeof userConfig.database === 'object'
101
+ ? String((userConfig.database as Record<string, unknown>).port)
102
+ : process.env.DB_PORT,
103
+ DB_USER: userConfig?.database && typeof userConfig.database === 'object'
104
+ ? (userConfig.database as Record<string, unknown>).user as string
105
+ : process.env.DB_USER,
106
+ DB_PASSWORD: userConfig?.database && typeof userConfig.database === 'object'
107
+ ? (userConfig.database as Record<string, unknown>).password as string
108
+ : process.env.DB_PASSWORD,
109
+ DB_NAME: userConfig?.database && typeof userConfig.database === 'object'
110
+ ? (userConfig.database as Record<string, unknown>).name as string
111
+ : process.env.DB_NAME,
112
+ };
113
+
6
114
  const envSchema = z.object({
7
- // OpenRouter
8
- OPENROUTER_API_KEY: z.string().min(1, 'OpenRouter API key is required'),
115
+ // OpenRouter - now optional (can be set via user config)
116
+ OPENROUTER_API_KEY: z.string().min(1).optional(),
9
117
  OPENROUTER_BASE_URL: z.string().url().default('https://openrouter.ai/api/v1'),
10
118
 
11
119
  // Models
@@ -58,12 +166,48 @@ const envSchema = z.object({
58
166
  type EnvConfig = z.infer<typeof envSchema>;
59
167
 
60
168
  function loadConfig(): EnvConfig {
61
- const parsed = envSchema.safeParse(process.env);
169
+ const parsed = envSchema.safeParse(mergedEnv);
62
170
 
63
171
  if (!parsed.success) {
172
+ // More helpful error message
173
+ const errors = parsed.error.format();
64
174
  console.error('Configuration validation failed:');
65
- console.error(parsed.error.format());
66
- throw new Error('Invalid configuration. Check your .env file.');
175
+ console.error(errors);
176
+
177
+ // Check if it's just missing API key
178
+ if (!mergedEnv.OPENROUTER_API_KEY) {
179
+ // Don't throw - let CLI handle setup
180
+ return {
181
+ OPENROUTER_API_KEY: undefined,
182
+ OPENROUTER_BASE_URL: 'https://openrouter.ai/api/v1',
183
+ DEFAULT_MODEL: 'anthropic/claude-3.5-sonnet',
184
+ FAST_MODEL: 'anthropic/claude-3-haiku',
185
+ EMBEDDING_MODEL: 'openai/text-embedding-3-small',
186
+ DB_HOST: 'localhost',
187
+ DB_PORT: 5432,
188
+ DB_USER: 'familyai',
189
+ DB_PASSWORD: 'familyai123',
190
+ DB_NAME: 'familyai',
191
+ REDIS_HOST: 'localhost',
192
+ REDIS_PORT: 6379,
193
+ API_PORT: 3000,
194
+ API_HOST: '0.0.0.0',
195
+ ENABLE_CONTENT_FILTER: true,
196
+ ENABLE_PII_DETECTION: true,
197
+ ENABLE_AUDIT_LOGGING: true,
198
+ MAX_TOKENS_PER_REQUEST: 4096,
199
+ RATE_LIMIT_MAX: 100,
200
+ RATE_LIMIT_WINDOW_MS: 60000,
201
+ SANDBOX_ENABLED: true,
202
+ SANDBOX_TIMEOUT_MS: 30000,
203
+ SANDBOX_MEMORY_LIMIT_MB: 256,
204
+ LOG_LEVEL: 'info',
205
+ LOG_FORMAT: 'json',
206
+ NODE_ENV: 'development',
207
+ };
208
+ }
209
+
210
+ throw new Error('Invalid configuration. Check your settings.');
67
211
  }
68
212
 
69
213
  return parsed.data;
@@ -71,6 +215,21 @@ function loadConfig(): EnvConfig {
71
215
 
72
216
  export const config = loadConfig();
73
217
 
218
+ // Check if config is complete (has API key)
219
+ export const isConfigured = (): boolean => {
220
+ return Boolean(config.OPENROUTER_API_KEY);
221
+ };
222
+
223
+ // Check if using user config file
224
+ export const hasUserConfig = (): boolean => {
225
+ return userConfig !== null;
226
+ };
227
+
228
+ // Get user config path
229
+ export const getUserConfigPath = (): string => {
230
+ return USER_CONFIG_PATH;
231
+ };
232
+
74
233
  export const getDatabaseUrl = (): string => {
75
234
  if (config.DATABASE_URL) return config.DATABASE_URL;
76
235
  return `postgresql://${config.DB_USER}:${config.DB_PASSWORD}@${config.DB_HOST}:${config.DB_PORT}/${config.DB_NAME}`;