@auxiora/config 1.0.0 → 1.3.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/src/validator.ts DELETED
@@ -1,226 +0,0 @@
1
- /**
2
- * Configuration validation with detailed error messages
3
- */
4
-
5
- import { type Config } from './index.js';
6
- import type { ZodError } from 'zod';
7
-
8
- export interface ValidationResult {
9
- valid: boolean;
10
- errors: ValidationError[];
11
- warnings: ValidationWarning[];
12
- }
13
-
14
- export interface ValidationError {
15
- path: string;
16
- message: string;
17
- value?: any;
18
- suggestion?: string;
19
- }
20
-
21
- export interface ValidationWarning {
22
- path: string;
23
- message: string;
24
- suggestion: string;
25
- }
26
-
27
- /**
28
- * Validate configuration and provide helpful error messages
29
- */
30
- export function validateConfig(config: Config): ValidationResult {
31
- const errors: ValidationError[] = [];
32
- const warnings: ValidationWarning[] = [];
33
-
34
- // Validate gateway configuration
35
- if (config.gateway.host === '0.0.0.0' && config.auth.mode === 'none') {
36
- warnings.push({
37
- path: 'gateway.host',
38
- message: 'Gateway bound to 0.0.0.0 with no authentication',
39
- suggestion: 'Consider binding to 127.0.0.1 or enabling JWT authentication for security',
40
- });
41
- }
42
-
43
- if (config.gateway.port < 1024) {
44
- warnings.push({
45
- path: 'gateway.port',
46
- message: `Port ${config.gateway.port} requires elevated privileges`,
47
- suggestion: 'Use a port >= 1024 to avoid needing root/admin access',
48
- });
49
- }
50
-
51
- // Validate authentication configuration
52
- if (config.auth.mode === 'jwt') {
53
- if (!config.auth.jwtSecret) {
54
- errors.push({
55
- path: 'auth.jwtSecret',
56
- message: 'JWT secret required when auth mode is "jwt"',
57
- suggestion: 'Generate a secret: openssl rand -hex 32',
58
- });
59
- } else if (config.auth.jwtSecret.length < 32) {
60
- warnings.push({
61
- path: 'auth.jwtSecret',
62
- message: 'JWT secret should be at least 32 characters',
63
- suggestion: 'Use a longer secret for better security: openssl rand -hex 32',
64
- });
65
- }
66
- }
67
-
68
- if (config.auth.mode === 'password' && !config.auth.passwordHash) {
69
- errors.push({
70
- path: 'auth.passwordHash',
71
- message: 'Password hash required when auth mode is "password"',
72
- suggestion: 'Set a password using the CLI: auxiora auth set-password',
73
- });
74
- }
75
-
76
- // Validate rate limiting
77
- if (!config.rateLimit.enabled) {
78
- warnings.push({
79
- path: 'rateLimit.enabled',
80
- message: 'Rate limiting is disabled',
81
- suggestion: 'Enable rate limiting to prevent abuse',
82
- });
83
- }
84
-
85
- if (config.rateLimit.maxRequests > 1000) {
86
- warnings.push({
87
- path: 'rateLimit.maxRequests',
88
- message: `High rate limit (${config.rateLimit.maxRequests} requests)`,
89
- suggestion: 'Consider a lower limit to prevent API quota exhaustion',
90
- });
91
- }
92
-
93
- // Validate provider configuration
94
- if (config.provider.primary === config.provider.fallback) {
95
- warnings.push({
96
- path: 'provider.fallback',
97
- message: 'Fallback provider same as primary',
98
- suggestion: 'Use a different provider for fallback or remove fallback',
99
- });
100
- }
101
-
102
- // Validate session configuration
103
- if (config.session.maxContextTokens > 200000) {
104
- warnings.push({
105
- path: 'session.maxContextTokens',
106
- message: 'Very large context window may cause performance issues',
107
- suggestion: 'Consider reducing to 100000 tokens for better performance',
108
- });
109
- }
110
-
111
- if (config.session.ttlMinutes < 60) {
112
- warnings.push({
113
- path: 'session.ttlMinutes',
114
- message: 'Short session TTL may cause frequent session loss',
115
- suggestion: 'Consider at least 60 minutes for better user experience',
116
- });
117
- }
118
-
119
- // Validate channel configuration
120
- const enabledChannels = Object.entries(config.channels)
121
- .filter(([_, channelConfig]) => channelConfig.enabled)
122
- .map(([name]) => name);
123
-
124
- if (enabledChannels.length === 0) {
125
- warnings.push({
126
- path: 'channels',
127
- message: 'No channels enabled',
128
- suggestion: 'Enable at least webchat for testing',
129
- });
130
- }
131
-
132
- // Validate logging configuration
133
- if (config.logging.maxFileSizeMb > 100) {
134
- warnings.push({
135
- path: 'logging.maxFileSizeMb',
136
- message: 'Large log file size may consume significant disk space',
137
- suggestion: 'Consider a smaller size with log rotation',
138
- });
139
- }
140
-
141
- return {
142
- valid: errors.length === 0,
143
- errors,
144
- warnings,
145
- };
146
- }
147
-
148
- /**
149
- * Format validation errors for display
150
- */
151
- export function formatValidationErrors(result: ValidationResult): string {
152
- const lines: string[] = [];
153
-
154
- if (result.errors.length > 0) {
155
- lines.push('❌ Configuration Errors:\n');
156
- for (const error of result.errors) {
157
- lines.push(` ${error.path}: ${error.message}`);
158
- if (error.suggestion) {
159
- lines.push(` 💡 ${error.suggestion}`);
160
- }
161
- lines.push('');
162
- }
163
- }
164
-
165
- if (result.warnings.length > 0) {
166
- lines.push('⚠️ Configuration Warnings:\n');
167
- for (const warning of result.warnings) {
168
- lines.push(` ${warning.path}: ${warning.message}`);
169
- lines.push(` 💡 ${warning.suggestion}`);
170
- lines.push('');
171
- }
172
- }
173
-
174
- return lines.join('\n');
175
- }
176
-
177
- /**
178
- * Format Zod validation errors
179
- */
180
- export function formatZodError(error: ZodError): string {
181
- const lines: string[] = ['❌ Configuration Validation Failed:\n'];
182
-
183
- for (const issue of error.issues) {
184
- const path = issue.path.join('.');
185
- lines.push(` ${path || 'config'}: ${issue.message}`);
186
-
187
- // Add helpful suggestions based on error type
188
- if (issue.code === 'invalid_type') {
189
- lines.push(` 💡 Expected ${issue.expected}, got ${issue.received}`);
190
- } else if (issue.code === 'invalid_enum_value') {
191
- lines.push(` 💡 Valid options: ${issue.options.join(', ')}`);
192
- } else if (issue.code === 'too_small') {
193
- lines.push(` 💡 Minimum value: ${issue.minimum}`);
194
- } else if (issue.code === 'too_big') {
195
- lines.push(` 💡 Maximum value: ${issue.maximum}`);
196
- }
197
-
198
- lines.push('');
199
- }
200
-
201
- lines.push('See .env.example for configuration reference');
202
-
203
- return lines.join('\n');
204
- }
205
-
206
- /**
207
- * Validate and report configuration issues
208
- */
209
- export function validateAndReport(config: Config): boolean {
210
- const result = validateConfig(config);
211
-
212
- if (!result.valid || result.warnings.length > 0) {
213
- console.error(formatValidationErrors(result));
214
- }
215
-
216
- if (!result.valid) {
217
- console.error('\n⛔ Cannot start with invalid configuration\n');
218
- return false;
219
- }
220
-
221
- if (result.warnings.length > 0) {
222
- console.warn('⚠️ Starting with warnings (see above)\n');
223
- }
224
-
225
- return true;
226
- }
@@ -1,390 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { ConfigSchema, loadConfig, getDefaultConfig } from '../src/index.js';
3
-
4
- describe('Config', () => {
5
- describe('ConfigSchema', () => {
6
- it('should provide sensible defaults', () => {
7
- // Clear any env overrides
8
- delete process.env.AUXIORA_GATEWAY_HOST;
9
- delete process.env.AUXIORA_GATEWAY_PORT;
10
-
11
- const config = ConfigSchema.parse({});
12
-
13
- // Note: host might be overridden by existing config, just check it's valid
14
- expect(config.gateway.host).toBeDefined();
15
- expect(config.gateway.port).toBe(18800);
16
- expect(config.auth.mode).toBe('none'); // Default to open for initial setup
17
- expect(config.rateLimit.enabled).toBe(true);
18
- expect(config.pairing.enabled).toBe(true);
19
- expect(config.pairing.codeLength).toBe(6);
20
- expect(config.pairing.expiryMinutes).toBe(15);
21
- expect(config.provider.primary).toBe('anthropic');
22
- expect(config.channels.webchat.enabled).toBe(true);
23
- expect(config.channels.discord.enabled).toBe(false);
24
- });
25
-
26
- it('should validate port range', () => {
27
- expect(() => ConfigSchema.parse({ gateway: { port: 0 } })).toThrow();
28
- expect(() => ConfigSchema.parse({ gateway: { port: 70000 } })).toThrow();
29
- expect(() => ConfigSchema.parse({ gateway: { port: 8080 } })).not.toThrow();
30
- });
31
-
32
- it('should validate auth mode', () => {
33
- expect(() => ConfigSchema.parse({ auth: { mode: 'invalid' } })).toThrow();
34
- expect(() => ConfigSchema.parse({ auth: { mode: 'none' } })).not.toThrow();
35
- expect(() => ConfigSchema.parse({ auth: { mode: 'jwt' } })).not.toThrow();
36
- });
37
-
38
- it('should validate provider options', () => {
39
- // primary is now a string for extensibility (supports 'anthropic', 'openai', 'google', 'ollama', etc.)
40
- expect(() => ConfigSchema.parse({ provider: { primary: 'openai' } })).not.toThrow();
41
- expect(() => ConfigSchema.parse({ provider: { primary: 'google' } })).not.toThrow();
42
- expect(() => ConfigSchema.parse({ provider: { primary: 'ollama' } })).not.toThrow();
43
- });
44
-
45
- it('should merge partial config with defaults', () => {
46
- const config = ConfigSchema.parse({
47
- gateway: { port: 9000, host: '127.0.0.1' },
48
- auth: { mode: 'jwt' },
49
- });
50
-
51
- expect(config.gateway.port).toBe(9000);
52
- expect(config.gateway.host).toBe('127.0.0.1');
53
- expect(config.auth.mode).toBe('jwt');
54
- expect(config.auth.jwtExpiresIn).toBe('7d'); // default
55
- });
56
- });
57
-
58
- describe('getDefaultConfig', () => {
59
- it('should return valid default config', async () => {
60
- const config = await getDefaultConfig();
61
- expect(config).toBeDefined();
62
- expect(config.gateway.port).toBe(18800);
63
- });
64
- });
65
-
66
- describe('environment overrides', () => {
67
- it('should support env var format', () => {
68
- // This tests the pattern, not the actual loading
69
- // The actual env override is tested via applyEnvOverrides internal function
70
- // For now, just verify the schema works
71
- const config = ConfigSchema.parse({
72
- gateway: { port: 9999 },
73
- auth: { mode: 'jwt' },
74
- });
75
-
76
- expect(config.gateway.port).toBe(9999);
77
- expect(config.auth.mode).toBe('jwt');
78
- });
79
- });
80
-
81
- describe('channel config', () => {
82
- it('should have all channel types', () => {
83
- const config = ConfigSchema.parse({});
84
-
85
- expect(config.channels.discord).toBeDefined();
86
- expect(config.channels.telegram).toBeDefined();
87
- expect(config.channels.slack).toBeDefined();
88
- expect(config.channels.twilio).toBeDefined();
89
- expect(config.channels.webchat).toBeDefined();
90
- });
91
-
92
- it('should default channels to disabled except webchat', () => {
93
- const config = ConfigSchema.parse({});
94
-
95
- expect(config.channels.discord.enabled).toBe(false);
96
- expect(config.channels.telegram.enabled).toBe(false);
97
- expect(config.channels.slack.enabled).toBe(false);
98
- expect(config.channels.twilio.enabled).toBe(false);
99
- expect(config.channels.webchat.enabled).toBe(true);
100
- });
101
- });
102
-
103
- describe('voice config', () => {
104
- it('should default voice to disabled', () => {
105
- const config = ConfigSchema.parse({});
106
- expect(config.voice.enabled).toBe(false);
107
- expect(config.voice.sttProvider).toBe('openai-whisper');
108
- expect(config.voice.ttsProvider).toBe('openai-tts');
109
- expect(config.voice.defaultVoice).toBe('alloy');
110
- expect(config.voice.language).toBe('en');
111
- expect(config.voice.maxAudioDuration).toBe(30);
112
- expect(config.voice.sampleRate).toBe(16000);
113
- });
114
-
115
- it('should accept custom voice config', () => {
116
- const config = ConfigSchema.parse({
117
- voice: { enabled: true, defaultVoice: 'nova', language: 'fr' },
118
- });
119
- expect(config.voice.enabled).toBe(true);
120
- expect(config.voice.defaultVoice).toBe('nova');
121
- expect(config.voice.language).toBe('fr');
122
- });
123
- });
124
-
125
- describe('webhook config', () => {
126
- it('should default webhooks to disabled', () => {
127
- const config = ConfigSchema.parse({});
128
- expect(config.webhooks.enabled).toBe(false);
129
- expect(config.webhooks.basePath).toBe('/api/v1/webhooks');
130
- expect(config.webhooks.signatureHeader).toBe('x-webhook-signature');
131
- expect(config.webhooks.maxPayloadSize).toBe(65536);
132
- });
133
-
134
- it('should accept custom webhook config', () => {
135
- const config = ConfigSchema.parse({
136
- webhooks: { enabled: true, maxPayloadSize: 131072 },
137
- });
138
- expect(config.webhooks.enabled).toBe(true);
139
- expect(config.webhooks.maxPayloadSize).toBe(131072);
140
- });
141
- });
142
-
143
- describe('dashboard config', () => {
144
- it('should default dashboard to enabled', () => {
145
- const config = ConfigSchema.parse({});
146
- expect(config.dashboard.enabled).toBe(true);
147
- expect(config.dashboard.sessionTtlMs).toBe(86_400_000);
148
- });
149
-
150
- it('should accept custom dashboard config', () => {
151
- const config = ConfigSchema.parse({
152
- dashboard: { enabled: true, sessionTtlMs: 3_600_000 },
153
- });
154
- expect(config.dashboard.enabled).toBe(true);
155
- expect(config.dashboard.sessionTtlMs).toBe(3_600_000);
156
- });
157
- });
158
-
159
- describe('plugins config', () => {
160
- it('should default plugins to enabled', () => {
161
- const config = ConfigSchema.parse({});
162
- expect(config.plugins.enabled).toBe(true);
163
- expect(config.plugins.dir).toBeUndefined();
164
- });
165
-
166
- it('should accept custom plugins config', () => {
167
- const config = ConfigSchema.parse({
168
- plugins: { enabled: false, dir: '/custom/plugins' },
169
- });
170
- expect(config.plugins.enabled).toBe(false);
171
- expect(config.plugins.dir).toBe('/custom/plugins');
172
- });
173
- });
174
-
175
- describe('routing config', () => {
176
- it('should default routing to enabled with empty rules', () => {
177
- const config = ConfigSchema.parse({});
178
- expect(config.routing.enabled).toBe(true);
179
- expect(config.routing.rules).toEqual([]);
180
- expect(config.routing.costLimits.warnAt).toBe(0.8);
181
- expect(config.routing.preferences.preferLocal).toBe(false);
182
- expect(config.routing.preferences.preferCheap).toBe(false);
183
- expect(config.routing.preferences.sensitiveToLocal).toBe(false);
184
- });
185
-
186
- it('should accept routing rules', () => {
187
- const config = ConfigSchema.parse({
188
- routing: {
189
- rules: [
190
- { task: 'reasoning', provider: 'anthropic', model: 'claude-sonnet-4-20250514', priority: 1 },
191
- { task: 'fast', provider: 'openai', model: 'gpt-4o-mini', priority: 0 },
192
- ],
193
- },
194
- });
195
- expect(config.routing.rules).toHaveLength(2);
196
- expect(config.routing.rules[0].task).toBe('reasoning');
197
- expect(config.routing.rules[0].provider).toBe('anthropic');
198
- });
199
-
200
- it('should reject invalid task types', () => {
201
- expect(() => ConfigSchema.parse({
202
- routing: { rules: [{ task: 'invalid', provider: 'anthropic', model: 'x' }] },
203
- })).toThrow();
204
- });
205
-
206
- it('should accept cost limits', () => {
207
- const config = ConfigSchema.parse({
208
- routing: {
209
- costLimits: { dailyBudget: 10, monthlyBudget: 100, perMessageMax: 0.5, warnAt: 0.9 },
210
- },
211
- });
212
- expect(config.routing.costLimits.dailyBudget).toBe(10);
213
- expect(config.routing.costLimits.monthlyBudget).toBe(100);
214
- expect(config.routing.costLimits.warnAt).toBe(0.9);
215
- });
216
-
217
- it('should reject negative budgets', () => {
218
- expect(() => ConfigSchema.parse({
219
- routing: { costLimits: { dailyBudget: -5 } },
220
- })).toThrow();
221
- });
222
-
223
- it('should accept a default model override', () => {
224
- const config = ConfigSchema.parse({
225
- routing: { defaultModel: 'claude-sonnet-4-20250514' },
226
- });
227
- expect(config.routing.defaultModel).toBe('claude-sonnet-4-20250514');
228
- });
229
- });
230
-
231
- describe('expanded provider config', () => {
232
- it('should have google provider defaults', () => {
233
- const config = ConfigSchema.parse({});
234
- expect(config.provider.google.model).toBe('gemini-2.5-flash');
235
- expect(config.provider.google.maxTokens).toBe(4096);
236
- });
237
-
238
- it('should have ollama provider defaults', () => {
239
- const config = ConfigSchema.parse({});
240
- expect(config.provider.ollama.model).toBe('llama3');
241
- expect(config.provider.ollama.baseUrl).toBe('http://localhost:11434');
242
- });
243
-
244
- it('should have openaiCompatible provider defaults', () => {
245
- const config = ConfigSchema.parse({});
246
- expect(config.provider.openaiCompatible.model).toBe('');
247
- expect(config.provider.openaiCompatible.name).toBe('custom');
248
- });
249
-
250
- it('should accept custom ollama config', () => {
251
- const config = ConfigSchema.parse({
252
- provider: { ollama: { model: 'mistral', baseUrl: 'http://192.168.1.5:11434' } },
253
- });
254
- expect(config.provider.ollama.model).toBe('mistral');
255
- expect(config.provider.ollama.baseUrl).toBe('http://192.168.1.5:11434');
256
- });
257
- });
258
-
259
- describe('orchestration config', () => {
260
- it('should default orchestration to enabled', () => {
261
- const config = ConfigSchema.parse({});
262
- expect(config.orchestration.enabled).toBe(true);
263
- expect(config.orchestration.maxConcurrentAgents).toBe(5);
264
- expect(config.orchestration.defaultTimeout).toBe(60000);
265
- expect(config.orchestration.totalTimeout).toBe(300000);
266
- expect(config.orchestration.allowedPatterns).toEqual([
267
- 'parallel', 'sequential', 'debate', 'map-reduce', 'supervisor',
268
- ]);
269
- expect(config.orchestration.costMultiplierWarning).toBe(3);
270
- });
271
-
272
- it('should accept custom orchestration config', () => {
273
- const config = ConfigSchema.parse({
274
- orchestration: {
275
- enabled: false,
276
- maxConcurrentAgents: 3,
277
- defaultTimeout: 30000,
278
- allowedPatterns: ['parallel', 'sequential'],
279
- },
280
- });
281
- expect(config.orchestration.enabled).toBe(false);
282
- expect(config.orchestration.maxConcurrentAgents).toBe(3);
283
- expect(config.orchestration.defaultTimeout).toBe(30000);
284
- expect(config.orchestration.allowedPatterns).toEqual(['parallel', 'sequential']);
285
- });
286
-
287
- it('should reject invalid maxConcurrentAgents', () => {
288
- expect(() => ConfigSchema.parse({
289
- orchestration: { maxConcurrentAgents: 0 },
290
- })).toThrow();
291
- expect(() => ConfigSchema.parse({
292
- orchestration: { maxConcurrentAgents: 11 },
293
- })).toThrow();
294
- });
295
-
296
- it('should reject invalid pattern names', () => {
297
- expect(() => ConfigSchema.parse({
298
- orchestration: { allowedPatterns: ['invalid-pattern'] },
299
- })).toThrow();
300
- });
301
-
302
- it('should reject non-positive timeouts', () => {
303
- expect(() => ConfigSchema.parse({
304
- orchestration: { defaultTimeout: 0 },
305
- })).toThrow();
306
- expect(() => ConfigSchema.parse({
307
- orchestration: { totalTimeout: -1 },
308
- })).toThrow();
309
- });
310
- });
311
-
312
- describe('modes config', () => {
313
- it('should default modes to enabled with auto detection', () => {
314
- const config = ConfigSchema.parse({});
315
- expect(config.modes.enabled).toBe(true);
316
- expect(config.modes.defaultMode).toBe('auto');
317
- expect(config.modes.autoDetection).toBe(true);
318
- expect(config.modes.confirmationThreshold).toBe(0.4);
319
- });
320
-
321
- it('should accept custom preferences', () => {
322
- const config = ConfigSchema.parse({
323
- modes: {
324
- preferences: {
325
- verbosity: 0.8,
326
- formality: 0.2,
327
- humor: 0.9,
328
- feedbackStyle: 'sandwich',
329
- expertiseAssumption: 'expert',
330
- },
331
- },
332
- });
333
- expect(config.modes.preferences.verbosity).toBe(0.8);
334
- expect(config.modes.preferences.formality).toBe(0.2);
335
- expect(config.modes.preferences.humor).toBe(0.9);
336
- expect(config.modes.preferences.feedbackStyle).toBe('sandwich');
337
- expect(config.modes.preferences.expertiseAssumption).toBe('expert');
338
- });
339
-
340
- it('should accept valid mode names as defaultMode', () => {
341
- expect(() => ConfigSchema.parse({ modes: { defaultMode: 'operator' } })).not.toThrow();
342
- expect(() => ConfigSchema.parse({ modes: { defaultMode: 'analyst' } })).not.toThrow();
343
- expect(() => ConfigSchema.parse({ modes: { defaultMode: 'auto' } })).not.toThrow();
344
- expect(() => ConfigSchema.parse({ modes: { defaultMode: 'off' } })).not.toThrow();
345
- });
346
-
347
- it('should reject invalid mode names', () => {
348
- expect(() => ConfigSchema.parse({ modes: { defaultMode: 'invalid' } })).toThrow();
349
- });
350
-
351
- it('should reject out-of-range preference values', () => {
352
- expect(() => ConfigSchema.parse({
353
- modes: { preferences: { verbosity: 1.5 } },
354
- })).toThrow();
355
- expect(() => ConfigSchema.parse({
356
- modes: { preferences: { humor: -0.1 } },
357
- })).toThrow();
358
- });
359
-
360
- it('should reject invalid feedback style', () => {
361
- expect(() => ConfigSchema.parse({
362
- modes: { preferences: { feedbackStyle: 'harsh' } },
363
- })).toThrow();
364
- });
365
-
366
- it('should reject invalid expertise assumption', () => {
367
- expect(() => ConfigSchema.parse({
368
- modes: { preferences: { expertiseAssumption: 'guru' } },
369
- })).toThrow();
370
- });
371
- });
372
-
373
- describe('memory config', () => {
374
- it('should default memory to enabled with auto-extract', () => {
375
- const config = ConfigSchema.parse({});
376
- expect(config.memory.enabled).toBe(true);
377
- expect(config.memory.autoExtract).toBe(true);
378
- expect(config.memory.maxEntries).toBe(1000);
379
- });
380
-
381
- it('should accept custom memory config', () => {
382
- const config = ConfigSchema.parse({
383
- memory: { enabled: false, autoExtract: false, maxEntries: 100 },
384
- });
385
- expect(config.memory.enabled).toBe(false);
386
- expect(config.memory.autoExtract).toBe(false);
387
- expect(config.memory.maxEntries).toBe(100);
388
- });
389
- });
390
- });
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"],
8
- "references": [
9
- { "path": "../core" }
10
- ]
11
- }