claude-autopm 1.30.1 → 2.1.0
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.
- package/autopm/.claude/mcp/test-server.md +10 -0
- package/autopm/.claude/scripts/github/dependency-tracker.js +554 -0
- package/autopm/.claude/scripts/github/dependency-validator.js +545 -0
- package/autopm/.claude/scripts/github/dependency-visualizer.js +477 -0
- package/autopm/.claude/scripts/pm/lib/epic-discovery.js +119 -0
- package/autopm/.claude/scripts/pm/next.js +56 -58
- package/bin/autopm-poc.js +348 -0
- package/bin/autopm.js +6 -0
- package/lib/ai-providers/AbstractAIProvider.js +524 -0
- package/lib/ai-providers/ClaudeProvider.js +423 -0
- package/lib/ai-providers/TemplateProvider.js +432 -0
- package/lib/cli/commands/agent.js +206 -0
- package/lib/cli/commands/config.js +488 -0
- package/lib/cli/commands/prd.js +345 -0
- package/lib/cli/commands/task.js +206 -0
- package/lib/config/ConfigManager.js +531 -0
- package/lib/errors/AIProviderError.js +164 -0
- package/lib/services/AgentService.js +557 -0
- package/lib/services/EpicService.js +609 -0
- package/lib/services/PRDService.js +1003 -0
- package/lib/services/TaskService.js +760 -0
- package/lib/services/interfaces.js +753 -0
- package/lib/utils/CircuitBreaker.js +165 -0
- package/lib/utils/Encryption.js +201 -0
- package/lib/utils/RateLimiter.js +241 -0
- package/lib/utils/ServiceFactory.js +165 -0
- package/package.json +9 -5
- package/scripts/config/get.js +108 -0
- package/scripts/config/init.js +100 -0
- package/scripts/config/list-providers.js +93 -0
- package/scripts/config/set-api-key.js +107 -0
- package/scripts/config/set-provider.js +201 -0
- package/scripts/config/set.js +139 -0
- package/scripts/config/show.js +181 -0
- package/autopm/.claude/.env +0 -158
- package/autopm/.claude/settings.local.json +0 -9
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration manager for standalone AutoPM
|
|
3
|
+
* Handles hierarchical configuration, provider management, and encrypted API keys
|
|
4
|
+
*
|
|
5
|
+
* Based on Context7 documentation patterns:
|
|
6
|
+
* - node-config: Hierarchical configuration (default → environment → runtime)
|
|
7
|
+
* - Node.js crypto: Secure encryption for API keys
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const manager = new ConfigManager('/path/to/config.json');
|
|
11
|
+
* manager.setMasterPassword('password');
|
|
12
|
+
* manager.setApiKey('claude', 'sk-...');
|
|
13
|
+
* manager.save();
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const Encryption = require('../utils/Encryption');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration manager for AutoPM
|
|
23
|
+
*/
|
|
24
|
+
class ConfigManager {
|
|
25
|
+
/**
|
|
26
|
+
* Create a ConfigManager instance
|
|
27
|
+
*
|
|
28
|
+
* @param {string} [configPath] - Path to config file (defaults to ~/.autopm/config.json)
|
|
29
|
+
*/
|
|
30
|
+
constructor(configPath) {
|
|
31
|
+
this.configPath = configPath || path.join(os.homedir(), '.autopm', 'config.json');
|
|
32
|
+
this.encryption = new Encryption();
|
|
33
|
+
this.masterPassword = null;
|
|
34
|
+
this.modified = false;
|
|
35
|
+
|
|
36
|
+
// Default configuration
|
|
37
|
+
this.config = this.getDefaultConfig();
|
|
38
|
+
|
|
39
|
+
// Load existing config if available
|
|
40
|
+
this.load();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get default configuration structure
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
* @returns {Object} Default configuration
|
|
48
|
+
*/
|
|
49
|
+
getDefaultConfig() {
|
|
50
|
+
return {
|
|
51
|
+
version: '1.0.0',
|
|
52
|
+
defaultProvider: 'claude',
|
|
53
|
+
environment: 'development',
|
|
54
|
+
providers: {
|
|
55
|
+
claude: {
|
|
56
|
+
model: 'claude-sonnet-4-20250514',
|
|
57
|
+
temperature: 0.7,
|
|
58
|
+
maxTokens: 4096,
|
|
59
|
+
rateLimit: {
|
|
60
|
+
tokensPerInterval: 60,
|
|
61
|
+
interval: 'minute'
|
|
62
|
+
},
|
|
63
|
+
circuitBreaker: {
|
|
64
|
+
failureThreshold: 5,
|
|
65
|
+
successThreshold: 2,
|
|
66
|
+
timeout: 60000
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
apiKeys: {}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get configuration value by key (supports dot notation)
|
|
76
|
+
*
|
|
77
|
+
* @param {string} [key] - Configuration key (e.g., 'providers.claude.model')
|
|
78
|
+
* @param {*} [defaultValue] - Default value if key doesn't exist
|
|
79
|
+
* @returns {*} Configuration value
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* manager.getConfig('version'); // '1.0.0'
|
|
83
|
+
* manager.getConfig('providers.claude.model'); // 'claude-sonnet-4-20250514'
|
|
84
|
+
* manager.getConfig('nonexistent', 'default'); // 'default'
|
|
85
|
+
*/
|
|
86
|
+
getConfig(key, defaultValue) {
|
|
87
|
+
if (!key) {
|
|
88
|
+
return this.config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const keys = key.split('.');
|
|
92
|
+
let value = this.config;
|
|
93
|
+
|
|
94
|
+
for (const k of keys) {
|
|
95
|
+
if (value && typeof value === 'object' && k in value) {
|
|
96
|
+
value = value[k];
|
|
97
|
+
} else {
|
|
98
|
+
return defaultValue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set configuration value by key (supports dot notation)
|
|
107
|
+
*
|
|
108
|
+
* @param {string} key - Configuration key
|
|
109
|
+
* @param {*} value - Configuration value
|
|
110
|
+
* @throws {Error} If key is invalid
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* manager.setConfig('version', '2.0.0');
|
|
114
|
+
* manager.setConfig('providers.claude.temperature', 0.9);
|
|
115
|
+
*/
|
|
116
|
+
setConfig(key, value) {
|
|
117
|
+
if (!key || typeof key !== 'string') {
|
|
118
|
+
throw new Error('Key is required');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Prevent circular references
|
|
122
|
+
if (typeof value === 'object' && value !== null) {
|
|
123
|
+
try {
|
|
124
|
+
JSON.stringify(value);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error('Cannot set value with circular references');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const keys = key.split('.');
|
|
131
|
+
let current = this.config;
|
|
132
|
+
|
|
133
|
+
// Navigate to parent object
|
|
134
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
135
|
+
const k = keys[i];
|
|
136
|
+
if (!(k in current) || typeof current[k] !== 'object') {
|
|
137
|
+
current[k] = {};
|
|
138
|
+
}
|
|
139
|
+
current = current[k];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Set value
|
|
143
|
+
current[keys[keys.length - 1]] = value;
|
|
144
|
+
this.modified = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Remove configuration value by key (supports dot notation)
|
|
149
|
+
*
|
|
150
|
+
* @param {string} key - Configuration key
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* manager.removeConfig('custom.setting');
|
|
154
|
+
*/
|
|
155
|
+
removeConfig(key) {
|
|
156
|
+
if (!key) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const keys = key.split('.');
|
|
161
|
+
let current = this.config;
|
|
162
|
+
|
|
163
|
+
// Navigate to parent object
|
|
164
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
165
|
+
const k = keys[i];
|
|
166
|
+
if (!(k in current) || typeof current[k] !== 'object') {
|
|
167
|
+
return; // Key doesn't exist
|
|
168
|
+
}
|
|
169
|
+
current = current[k];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Delete key
|
|
173
|
+
delete current[keys[keys.length - 1]];
|
|
174
|
+
this.modified = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if configuration key exists
|
|
179
|
+
*
|
|
180
|
+
* @param {string} key - Configuration key
|
|
181
|
+
* @returns {boolean} True if key exists
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* manager.hasConfig('version'); // true
|
|
185
|
+
* manager.hasConfig('nonexistent'); // false
|
|
186
|
+
*/
|
|
187
|
+
hasConfig(key) {
|
|
188
|
+
return this.getConfig(key) !== undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get default provider name
|
|
193
|
+
*
|
|
194
|
+
* @returns {string} Default provider name
|
|
195
|
+
*/
|
|
196
|
+
getDefaultProvider() {
|
|
197
|
+
return this.getConfig('defaultProvider');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Set default provider
|
|
202
|
+
*
|
|
203
|
+
* @param {string} name - Provider name
|
|
204
|
+
*/
|
|
205
|
+
setDefaultProvider(name) {
|
|
206
|
+
this.setConfig('defaultProvider', name);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get provider configuration
|
|
211
|
+
*
|
|
212
|
+
* @param {string} name - Provider name
|
|
213
|
+
* @returns {Object|undefined} Provider configuration
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const claude = manager.getProvider('claude');
|
|
217
|
+
* // { model: '...', temperature: 0.7, maxTokens: 4096 }
|
|
218
|
+
*/
|
|
219
|
+
getProvider(name) {
|
|
220
|
+
return this.getConfig(`providers.${name}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Set provider configuration (merges with existing)
|
|
225
|
+
*
|
|
226
|
+
* @param {string} name - Provider name
|
|
227
|
+
* @param {Object} config - Provider configuration
|
|
228
|
+
* @throws {Error} If configuration is invalid
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* manager.setProvider('openai', {
|
|
232
|
+
* model: 'gpt-4',
|
|
233
|
+
* temperature: 0.8,
|
|
234
|
+
* maxTokens: 2000
|
|
235
|
+
* });
|
|
236
|
+
*/
|
|
237
|
+
setProvider(name, config) {
|
|
238
|
+
// Merge with existing configuration
|
|
239
|
+
const existing = this.getProvider(name) || {};
|
|
240
|
+
const merged = { ...existing, ...config };
|
|
241
|
+
|
|
242
|
+
// Validate merged configuration
|
|
243
|
+
this.validateProvider(merged);
|
|
244
|
+
|
|
245
|
+
this.setConfig(`providers.${name}`, merged);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* List all provider names
|
|
250
|
+
*
|
|
251
|
+
* @returns {string[]} Provider names
|
|
252
|
+
*/
|
|
253
|
+
listProviders() {
|
|
254
|
+
const providers = this.getConfig('providers') || {};
|
|
255
|
+
return Object.keys(providers);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Remove provider configuration
|
|
260
|
+
*
|
|
261
|
+
* @param {string} name - Provider name
|
|
262
|
+
* @throws {Error} If trying to remove default provider
|
|
263
|
+
*/
|
|
264
|
+
removeProvider(name) {
|
|
265
|
+
if (name === this.getDefaultProvider()) {
|
|
266
|
+
throw new Error('Cannot remove default provider without setting new default first');
|
|
267
|
+
}
|
|
268
|
+
this.removeConfig(`providers.${name}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate provider configuration
|
|
273
|
+
*
|
|
274
|
+
* @param {Object} config - Provider configuration
|
|
275
|
+
* @throws {Error} If configuration is invalid
|
|
276
|
+
*/
|
|
277
|
+
validateProvider(config) {
|
|
278
|
+
if (!config.model) {
|
|
279
|
+
throw new Error('Provider configuration must have model');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (config.temperature !== undefined) {
|
|
283
|
+
if (typeof config.temperature !== 'number' || config.temperature < 0 || config.temperature > 1) {
|
|
284
|
+
throw new Error('Temperature must be between 0 and 1');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (config.maxTokens !== undefined) {
|
|
289
|
+
if (typeof config.maxTokens !== 'number' || config.maxTokens <= 0) {
|
|
290
|
+
throw new Error('maxTokens must be positive');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (config.rateLimit) {
|
|
295
|
+
const validIntervals = ['second', 'minute', 'hour'];
|
|
296
|
+
if (!validIntervals.includes(config.rateLimit.interval)) {
|
|
297
|
+
throw new Error('Rate limit interval must be one of: second, minute, hour');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validate entire configuration
|
|
304
|
+
*
|
|
305
|
+
* @throws {Error} If configuration is invalid
|
|
306
|
+
*/
|
|
307
|
+
validateConfig() {
|
|
308
|
+
// Validate each provider
|
|
309
|
+
const providers = this.getConfig('providers') || {};
|
|
310
|
+
for (const [name, config] of Object.entries(providers)) {
|
|
311
|
+
try {
|
|
312
|
+
this.validateProvider(config);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(`Invalid configuration for provider '${name}': ${error.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Validate default provider exists
|
|
319
|
+
const defaultProvider = this.getDefaultProvider();
|
|
320
|
+
if (defaultProvider && !this.getProvider(defaultProvider)) {
|
|
321
|
+
throw new Error(`Default provider '${defaultProvider}' is not configured`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Set master password for API key encryption
|
|
327
|
+
*
|
|
328
|
+
* @param {string} password - Master password
|
|
329
|
+
*/
|
|
330
|
+
setMasterPassword(password) {
|
|
331
|
+
this.masterPassword = password;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if master password is set
|
|
336
|
+
*
|
|
337
|
+
* @returns {boolean} True if master password is set
|
|
338
|
+
*/
|
|
339
|
+
hasMasterPassword() {
|
|
340
|
+
return this.masterPassword !== null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Set encrypted API key for provider
|
|
345
|
+
*
|
|
346
|
+
* @param {string} provider - Provider name
|
|
347
|
+
* @param {string} apiKey - API key to encrypt
|
|
348
|
+
* @throws {Error} If master password is not set
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* manager.setMasterPassword('password');
|
|
352
|
+
* manager.setApiKey('claude', 'sk-...');
|
|
353
|
+
*/
|
|
354
|
+
setApiKey(provider, apiKey) {
|
|
355
|
+
if (!this.hasMasterPassword()) {
|
|
356
|
+
throw new Error('Master password not set. Call setMasterPassword() first.');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Encrypt API key
|
|
360
|
+
const encrypted = this.encryption.encrypt(apiKey, this.masterPassword);
|
|
361
|
+
|
|
362
|
+
// Store encrypted data
|
|
363
|
+
if (!this.config.apiKeys) {
|
|
364
|
+
this.config.apiKeys = {};
|
|
365
|
+
}
|
|
366
|
+
this.config.apiKeys[provider] = encrypted;
|
|
367
|
+
this.modified = true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get decrypted API key for provider
|
|
372
|
+
*
|
|
373
|
+
* @param {string} provider - Provider name
|
|
374
|
+
* @returns {string|null} Decrypted API key or null if not found
|
|
375
|
+
* @throws {Error} If master password is not set or decryption fails
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* const apiKey = manager.getApiKey('claude');
|
|
379
|
+
*/
|
|
380
|
+
getApiKey(provider) {
|
|
381
|
+
if (!this.hasMasterPassword()) {
|
|
382
|
+
throw new Error('Master password not set. Call setMasterPassword() first.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const encrypted = this.config.apiKeys?.[provider];
|
|
386
|
+
if (!encrypted) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Decrypt API key
|
|
391
|
+
return this.encryption.decrypt(encrypted, this.masterPassword);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Remove API key for provider
|
|
396
|
+
*
|
|
397
|
+
* @param {string} provider - Provider name
|
|
398
|
+
*/
|
|
399
|
+
removeApiKey(provider) {
|
|
400
|
+
if (this.config.apiKeys && this.config.apiKeys[provider]) {
|
|
401
|
+
delete this.config.apiKeys[provider];
|
|
402
|
+
this.modified = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check if API key exists for provider
|
|
408
|
+
*
|
|
409
|
+
* @param {string} provider - Provider name
|
|
410
|
+
* @returns {boolean} True if API key exists
|
|
411
|
+
*/
|
|
412
|
+
hasApiKey(provider) {
|
|
413
|
+
return !!(this.config.apiKeys && this.config.apiKeys[provider]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Change master password and re-encrypt all API keys
|
|
418
|
+
*
|
|
419
|
+
* @param {string} oldPassword - Current password
|
|
420
|
+
* @param {string} newPassword - New password
|
|
421
|
+
* @throws {Error} If old password is incorrect
|
|
422
|
+
*/
|
|
423
|
+
changeMasterPassword(oldPassword, newPassword) {
|
|
424
|
+
// Verify old password by trying to decrypt all keys
|
|
425
|
+
const apiKeys = {};
|
|
426
|
+
const providers = Object.keys(this.config.apiKeys || {});
|
|
427
|
+
|
|
428
|
+
for (const provider of providers) {
|
|
429
|
+
const encrypted = this.config.apiKeys[provider];
|
|
430
|
+
try {
|
|
431
|
+
apiKeys[provider] = this.encryption.decrypt(encrypted, oldPassword);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw new Error('Old password is incorrect');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Re-encrypt all keys with new password
|
|
438
|
+
this.masterPassword = newPassword;
|
|
439
|
+
for (const [provider, apiKey] of Object.entries(apiKeys)) {
|
|
440
|
+
this.setApiKey(provider, apiKey);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Check if configuration has been modified
|
|
446
|
+
*
|
|
447
|
+
* @returns {boolean} True if modified
|
|
448
|
+
*/
|
|
449
|
+
isModified() {
|
|
450
|
+
return this.modified;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Save configuration to file (atomic write)
|
|
455
|
+
*
|
|
456
|
+
* @throws {Error} If file write fails
|
|
457
|
+
*/
|
|
458
|
+
save() {
|
|
459
|
+
// Ensure directory exists
|
|
460
|
+
const dir = path.dirname(this.configPath);
|
|
461
|
+
if (!fs.existsSync(dir)) {
|
|
462
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Atomic write: write to temp file, then rename
|
|
466
|
+
const tempPath = `${this.configPath}.tmp`;
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
fs.writeFileSync(tempPath, JSON.stringify(this.config, null, 2), 'utf8');
|
|
470
|
+
fs.renameSync(tempPath, this.configPath);
|
|
471
|
+
this.modified = false;
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// Clean up temp file on error
|
|
474
|
+
if (fs.existsSync(tempPath)) {
|
|
475
|
+
fs.unlinkSync(tempPath);
|
|
476
|
+
}
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Load configuration from file
|
|
483
|
+
*
|
|
484
|
+
* Merges loaded config with defaults (defaults have lower priority)
|
|
485
|
+
*/
|
|
486
|
+
load() {
|
|
487
|
+
if (!fs.existsSync(this.configPath)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const content = fs.readFileSync(this.configPath, 'utf8');
|
|
493
|
+
const loaded = JSON.parse(content);
|
|
494
|
+
|
|
495
|
+
// Merge with defaults (loaded config takes priority)
|
|
496
|
+
this.config = this.mergeConfig(this.getDefaultConfig(), loaded);
|
|
497
|
+
this.modified = false;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// If config is corrupted, keep defaults
|
|
500
|
+
console.error(`Error loading config: ${error.message}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Merge configuration objects (deep merge)
|
|
506
|
+
*
|
|
507
|
+
* @private
|
|
508
|
+
* @param {Object} defaults - Default configuration
|
|
509
|
+
* @param {Object} overrides - Override configuration
|
|
510
|
+
* @returns {Object} Merged configuration
|
|
511
|
+
*/
|
|
512
|
+
mergeConfig(defaults, overrides) {
|
|
513
|
+
const result = { ...defaults };
|
|
514
|
+
|
|
515
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
516
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
517
|
+
if (result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
|
518
|
+
result[key] = this.mergeConfig(result[key], value);
|
|
519
|
+
} else {
|
|
520
|
+
result[key] = value;
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
result[key] = value;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = ConfigManager;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIProviderError - Custom Error for AI Provider Operations
|
|
3
|
+
*
|
|
4
|
+
* Following Node.js best practices for custom error classes:
|
|
5
|
+
* - Extends native Error class
|
|
6
|
+
* - Restores prototype chain with Object.setPrototypeOf
|
|
7
|
+
* - Captures stack trace with Error.captureStackTrace
|
|
8
|
+
* - Provides operational/programming error distinction
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/goldbergyoni/nodebestpractices#-21-use-async-await-or-promises-for-async-error-handling
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom error class for AI provider operations
|
|
15
|
+
*
|
|
16
|
+
* @class AIProviderError
|
|
17
|
+
* @extends {Error}
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Creating an error
|
|
21
|
+
* throw new AIProviderError('INVALID_API_KEY', 'API key is missing or invalid');
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Creating a programming error (not operational)
|
|
25
|
+
* throw new AIProviderError('UNKNOWN_ERROR', 'Unexpected system error', false);
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Creating an error with HTTP status
|
|
29
|
+
* throw new AIProviderError('RATE_LIMIT', 'Too many requests', true, 429);
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Catching and checking error type
|
|
33
|
+
* try {
|
|
34
|
+
* await provider.complete(prompt);
|
|
35
|
+
* } catch (error) {
|
|
36
|
+
* if (error instanceof AIProviderError && error.isOperational) {
|
|
37
|
+
* // Handle gracefully
|
|
38
|
+
* } else {
|
|
39
|
+
* // Log and crash
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
class AIProviderError extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* Creates an instance of AIProviderError
|
|
46
|
+
*
|
|
47
|
+
* @param {string} code - Error code (e.g., 'INVALID_API_KEY', 'RATE_LIMIT')
|
|
48
|
+
* @param {string} message - Human-readable error message
|
|
49
|
+
* @param {boolean} [isOperational=true] - Whether error is operational (expected) or programming error
|
|
50
|
+
* @param {number} [httpStatus] - Optional HTTP status code
|
|
51
|
+
*
|
|
52
|
+
* @throws {AIProviderError}
|
|
53
|
+
*/
|
|
54
|
+
constructor(code, message, isOperational = true, httpStatus = undefined) {
|
|
55
|
+
// Call parent constructor
|
|
56
|
+
super(message);
|
|
57
|
+
|
|
58
|
+
// Restore prototype chain
|
|
59
|
+
// Required for proper instanceof checks in ES6+ with transpilation
|
|
60
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
61
|
+
|
|
62
|
+
// Set error name
|
|
63
|
+
this.name = 'AIProviderError';
|
|
64
|
+
|
|
65
|
+
// Set custom properties
|
|
66
|
+
this.code = code;
|
|
67
|
+
this.isOperational = isOperational;
|
|
68
|
+
|
|
69
|
+
// Optional HTTP status
|
|
70
|
+
if (httpStatus !== undefined) {
|
|
71
|
+
this.httpStatus = httpStatus;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Capture stack trace, excluding constructor call from stack
|
|
75
|
+
Error.captureStackTrace(this, this.constructor);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns string representation of error
|
|
80
|
+
*
|
|
81
|
+
* @returns {string} Formatted error string
|
|
82
|
+
*/
|
|
83
|
+
toString() {
|
|
84
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Custom JSON serialization
|
|
89
|
+
* Ensures all custom properties are included when JSON.stringify is called
|
|
90
|
+
*
|
|
91
|
+
* @returns {Object} JSON-serializable object
|
|
92
|
+
*/
|
|
93
|
+
toJSON() {
|
|
94
|
+
return {
|
|
95
|
+
name: this.name,
|
|
96
|
+
code: this.code,
|
|
97
|
+
message: this.message,
|
|
98
|
+
isOperational: this.isOperational,
|
|
99
|
+
httpStatus: this.httpStatus,
|
|
100
|
+
stack: this.stack
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Static error code constants
|
|
106
|
+
// These provide type-safe error code references throughout the codebase
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Invalid or missing API key
|
|
110
|
+
* @type {string}
|
|
111
|
+
* @static
|
|
112
|
+
*/
|
|
113
|
+
AIProviderError.INVALID_API_KEY = 'INVALID_API_KEY';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Rate limit exceeded
|
|
117
|
+
* @type {string}
|
|
118
|
+
* @static
|
|
119
|
+
*/
|
|
120
|
+
AIProviderError.RATE_LIMIT = 'RATE_LIMIT';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Service temporarily unavailable
|
|
124
|
+
* @type {string}
|
|
125
|
+
* @static
|
|
126
|
+
*/
|
|
127
|
+
AIProviderError.SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Network connection error
|
|
131
|
+
* @type {string}
|
|
132
|
+
* @static
|
|
133
|
+
*/
|
|
134
|
+
AIProviderError.NETWORK_ERROR = 'NETWORK_ERROR';
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Invalid request parameters
|
|
138
|
+
* @type {string}
|
|
139
|
+
* @static
|
|
140
|
+
*/
|
|
141
|
+
AIProviderError.INVALID_REQUEST = 'INVALID_REQUEST';
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Context length exceeded (prompt too long)
|
|
145
|
+
* @type {string}
|
|
146
|
+
* @static
|
|
147
|
+
*/
|
|
148
|
+
AIProviderError.CONTEXT_LENGTH_EXCEEDED = 'CONTEXT_LENGTH_EXCEEDED';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Content policy violation
|
|
152
|
+
* @type {string}
|
|
153
|
+
* @static
|
|
154
|
+
*/
|
|
155
|
+
AIProviderError.CONTENT_POLICY_VIOLATION = 'CONTENT_POLICY_VIOLATION';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Unknown or unexpected error
|
|
159
|
+
* @type {string}
|
|
160
|
+
* @static
|
|
161
|
+
*/
|
|
162
|
+
AIProviderError.UNKNOWN_ERROR = 'UNKNOWN_ERROR';
|
|
163
|
+
|
|
164
|
+
module.exports = AIProviderError;
|