mrmd-server 0.1.0 → 0.1.1

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,782 @@
1
+ /**
2
+ * Settings API routes
3
+ *
4
+ * Mirrors electronAPI.settings.*
5
+ * Settings are stored at ~/.config/mrmd/settings.json (same as Electron)
6
+ */
7
+
8
+ import { Router } from 'express';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ // Configuration
14
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'mrmd');
15
+ const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.json');
16
+
17
+ /**
18
+ * Default settings schema
19
+ */
20
+ const DEFAULT_SETTINGS = {
21
+ version: 1,
22
+
23
+ // API keys for various providers
24
+ apiKeys: {
25
+ anthropic: '',
26
+ openai: '',
27
+ groq: '',
28
+ gemini: '',
29
+ openrouter: '',
30
+ },
31
+
32
+ // Quality level to model mappings (1-5)
33
+ qualityLevels: {
34
+ 1: {
35
+ model: 'groq/moonshotai/kimi-k2-instruct-0905',
36
+ reasoningDefault: 0,
37
+ name: 'Quick',
38
+ },
39
+ 2: {
40
+ model: 'anthropic/claude-sonnet-4-5',
41
+ reasoningDefault: 1,
42
+ name: 'Balanced',
43
+ },
44
+ 3: {
45
+ model: 'gemini/gemini-3-pro-preview',
46
+ reasoningDefault: 2,
47
+ name: 'Deep',
48
+ },
49
+ 4: {
50
+ model: 'anthropic/claude-opus-4-5',
51
+ reasoningDefault: 3,
52
+ name: 'Maximum',
53
+ },
54
+ 5: {
55
+ type: 'multi',
56
+ models: [
57
+ 'openrouter/x-ai/grok-4',
58
+ 'openai/gpt-5.2',
59
+ 'gemini/gemini-3-pro-preview',
60
+ 'anthropic/claude-opus-4-5',
61
+ ],
62
+ synthesizer: 'gemini/gemini-3-pro-preview',
63
+ name: 'Ultimate',
64
+ },
65
+ },
66
+
67
+ // Custom AI command sections
68
+ customSections: [],
69
+
70
+ // Default preferences
71
+ defaults: {
72
+ juiceLevel: 2,
73
+ reasoningLevel: 1,
74
+ },
75
+ };
76
+
77
+ /**
78
+ * Available API providers with metadata
79
+ */
80
+ const API_PROVIDERS = {
81
+ anthropic: {
82
+ name: 'Anthropic',
83
+ keyPrefix: 'sk-ant-',
84
+ envVar: 'ANTHROPIC_API_KEY',
85
+ testEndpoint: 'https://api.anthropic.com/v1/messages',
86
+ },
87
+ openai: {
88
+ name: 'OpenAI',
89
+ keyPrefix: 'sk-',
90
+ envVar: 'OPENAI_API_KEY',
91
+ testEndpoint: 'https://api.openai.com/v1/models',
92
+ },
93
+ groq: {
94
+ name: 'Groq',
95
+ keyPrefix: 'gsk_',
96
+ envVar: 'GROQ_API_KEY',
97
+ testEndpoint: 'https://api.groq.com/openai/v1/models',
98
+ },
99
+ gemini: {
100
+ name: 'Google Gemini',
101
+ keyPrefix: '',
102
+ envVar: 'GEMINI_API_KEY',
103
+ testEndpoint: 'https://generativelanguage.googleapis.com/v1/models',
104
+ },
105
+ openrouter: {
106
+ name: 'OpenRouter',
107
+ keyPrefix: 'sk-or-',
108
+ envVar: 'OPENROUTER_API_KEY',
109
+ testEndpoint: 'https://openrouter.ai/api/v1/models',
110
+ },
111
+ };
112
+
113
+ // In-memory cache
114
+ let settingsCache = null;
115
+
116
+ /**
117
+ * Ensure config directory exists
118
+ */
119
+ function ensureConfigDir() {
120
+ if (!fs.existsSync(CONFIG_DIR)) {
121
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Merge loaded settings with defaults (for schema upgrades)
127
+ */
128
+ function mergeWithDefaults(loaded) {
129
+ const merged = { ...DEFAULT_SETTINGS };
130
+
131
+ for (const key of Object.keys(DEFAULT_SETTINGS)) {
132
+ if (loaded[key] !== undefined) {
133
+ if (typeof DEFAULT_SETTINGS[key] === 'object' && !Array.isArray(DEFAULT_SETTINGS[key])) {
134
+ merged[key] = { ...DEFAULT_SETTINGS[key], ...loaded[key] };
135
+ } else {
136
+ merged[key] = loaded[key];
137
+ }
138
+ }
139
+ }
140
+
141
+ if (loaded.version && loaded.version > merged.version) {
142
+ merged.version = loaded.version;
143
+ }
144
+
145
+ return merged;
146
+ }
147
+
148
+ /**
149
+ * Load settings from disk
150
+ */
151
+ function loadSettings() {
152
+ if (settingsCache) {
153
+ return settingsCache;
154
+ }
155
+
156
+ ensureConfigDir();
157
+
158
+ try {
159
+ if (fs.existsSync(SETTINGS_FILE)) {
160
+ const content = fs.readFileSync(SETTINGS_FILE, 'utf8');
161
+ const loaded = JSON.parse(content);
162
+ settingsCache = mergeWithDefaults(loaded);
163
+ } else {
164
+ settingsCache = { ...DEFAULT_SETTINGS };
165
+ saveSettings();
166
+ }
167
+ } catch (e) {
168
+ console.error('[settings] Error loading settings:', e.message);
169
+ settingsCache = { ...DEFAULT_SETTINGS };
170
+ }
171
+
172
+ return settingsCache;
173
+ }
174
+
175
+ /**
176
+ * Save settings to disk
177
+ */
178
+ function saveSettings() {
179
+ ensureConfigDir();
180
+ try {
181
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settingsCache, null, 2));
182
+ return true;
183
+ } catch (e) {
184
+ console.error('[settings] Error saving settings:', e.message);
185
+ return false;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Get a value by dot-notation path
191
+ */
192
+ function getByPath(obj, keyPath, defaultValue = undefined) {
193
+ const parts = keyPath.split('.');
194
+ let value = obj;
195
+
196
+ for (const part of parts) {
197
+ if (value === undefined || value === null) {
198
+ return defaultValue;
199
+ }
200
+ value = value[part];
201
+ }
202
+
203
+ return value !== undefined ? value : defaultValue;
204
+ }
205
+
206
+ /**
207
+ * Set a value by dot-notation path
208
+ */
209
+ function setByPath(obj, keyPath, value) {
210
+ const parts = keyPath.split('.');
211
+ let current = obj;
212
+
213
+ for (let i = 0; i < parts.length - 1; i++) {
214
+ const part = parts[i];
215
+ if (current[part] === undefined) {
216
+ current[part] = {};
217
+ }
218
+ current = current[part];
219
+ }
220
+
221
+ current[parts[parts.length - 1]] = value;
222
+ }
223
+
224
+ /**
225
+ * Create settings routes
226
+ * @param {import('../server.js').ServerContext} ctx
227
+ */
228
+ export function createSettingsRoutes(ctx) {
229
+ const router = Router();
230
+
231
+ /**
232
+ * GET /api/settings
233
+ * Get all settings
234
+ */
235
+ router.get('/', (req, res) => {
236
+ try {
237
+ res.json(loadSettings());
238
+ } catch (err) {
239
+ console.error('[settings:getAll]', err);
240
+ res.status(500).json({ error: err.message });
241
+ }
242
+ });
243
+
244
+ /**
245
+ * GET /api/settings/key?path=...
246
+ * Get a specific setting by path
247
+ */
248
+ router.get('/key', (req, res) => {
249
+ try {
250
+ const { path: keyPath, default: defaultValue } = req.query;
251
+ if (!keyPath) {
252
+ return res.status(400).json({ error: 'path query parameter required' });
253
+ }
254
+
255
+ const settings = loadSettings();
256
+ const value = getByPath(settings, keyPath, defaultValue);
257
+ res.json({ value });
258
+ } catch (err) {
259
+ console.error('[settings:get]', err);
260
+ res.status(500).json({ error: err.message });
261
+ }
262
+ });
263
+
264
+ /**
265
+ * POST /api/settings/key
266
+ * Set a specific setting by path
267
+ */
268
+ router.post('/key', (req, res) => {
269
+ try {
270
+ const { key, value } = req.body;
271
+ if (!key) {
272
+ return res.status(400).json({ error: 'key required' });
273
+ }
274
+
275
+ const settings = loadSettings();
276
+ setByPath(settings, key, value);
277
+ const success = saveSettings();
278
+ res.json({ success });
279
+ } catch (err) {
280
+ console.error('[settings:set]', err);
281
+ res.status(500).json({ error: err.message });
282
+ }
283
+ });
284
+
285
+ /**
286
+ * POST /api/settings/update
287
+ * Update multiple settings at once
288
+ */
289
+ router.post('/update', (req, res) => {
290
+ try {
291
+ const { updates } = req.body;
292
+ if (!updates || typeof updates !== 'object') {
293
+ return res.status(400).json({ error: 'updates object required' });
294
+ }
295
+
296
+ const settings = loadSettings();
297
+ for (const [keyPath, value] of Object.entries(updates)) {
298
+ setByPath(settings, keyPath, value);
299
+ }
300
+ const success = saveSettings();
301
+ res.json({ success });
302
+ } catch (err) {
303
+ console.error('[settings:update]', err);
304
+ res.status(500).json({ error: err.message });
305
+ }
306
+ });
307
+
308
+ /**
309
+ * POST /api/settings/reset
310
+ * Reset settings to defaults
311
+ */
312
+ router.post('/reset', (req, res) => {
313
+ try {
314
+ settingsCache = { ...DEFAULT_SETTINGS };
315
+ const success = saveSettings();
316
+ res.json({ success });
317
+ } catch (err) {
318
+ console.error('[settings:reset]', err);
319
+ res.status(500).json({ error: err.message });
320
+ }
321
+ });
322
+
323
+ // ==========================================================================
324
+ // API KEYS
325
+ // ==========================================================================
326
+
327
+ /**
328
+ * GET /api/settings/api-keys?masked=true
329
+ * Get all API keys (masked for display by default)
330
+ */
331
+ router.get('/api-keys', (req, res) => {
332
+ try {
333
+ const masked = req.query.masked !== 'false';
334
+ const settings = loadSettings();
335
+ const keys = settings.apiKeys || {};
336
+
337
+ if (!masked) {
338
+ return res.json(keys);
339
+ }
340
+
341
+ // Mask keys for display
342
+ const maskedKeys = {};
343
+ for (const [provider, key] of Object.entries(keys)) {
344
+ if (key && key.length > 12) {
345
+ maskedKeys[provider] = `${key.slice(0, 8)}${'•'.repeat(key.length - 12)}${key.slice(-4)}`;
346
+ } else if (key) {
347
+ maskedKeys[provider] = '•'.repeat(key.length);
348
+ } else {
349
+ maskedKeys[provider] = '';
350
+ }
351
+ }
352
+
353
+ res.json(maskedKeys);
354
+ } catch (err) {
355
+ console.error('[settings:getApiKeys]', err);
356
+ res.status(500).json({ error: err.message });
357
+ }
358
+ });
359
+
360
+ /**
361
+ * POST /api/settings/api-key
362
+ * Set an API key for a provider
363
+ */
364
+ router.post('/api-key', (req, res) => {
365
+ try {
366
+ const { provider, key } = req.body;
367
+ if (!provider) {
368
+ return res.status(400).json({ error: 'provider required' });
369
+ }
370
+
371
+ const settings = loadSettings();
372
+ if (!settings.apiKeys) {
373
+ settings.apiKeys = {};
374
+ }
375
+ settings.apiKeys[provider] = key || '';
376
+ const success = saveSettings();
377
+ res.json({ success });
378
+ } catch (err) {
379
+ console.error('[settings:setApiKey]', err);
380
+ res.status(500).json({ error: err.message });
381
+ }
382
+ });
383
+
384
+ /**
385
+ * GET /api/settings/api-key/:provider
386
+ * Get a single API key (unmasked)
387
+ */
388
+ router.get('/api-key/:provider', (req, res) => {
389
+ try {
390
+ const { provider } = req.params;
391
+ const settings = loadSettings();
392
+ const key = settings.apiKeys?.[provider] || '';
393
+ res.json({ key });
394
+ } catch (err) {
395
+ console.error('[settings:getApiKey]', err);
396
+ res.status(500).json({ error: err.message });
397
+ }
398
+ });
399
+
400
+ /**
401
+ * GET /api/settings/api-key/:provider/exists
402
+ * Check if a provider has a key configured
403
+ */
404
+ router.get('/api-key/:provider/exists', (req, res) => {
405
+ try {
406
+ const { provider } = req.params;
407
+ const settings = loadSettings();
408
+ const key = settings.apiKeys?.[provider] || '';
409
+ res.json({ hasKey: key.length > 0 });
410
+ } catch (err) {
411
+ console.error('[settings:hasApiKey]', err);
412
+ res.status(500).json({ error: err.message });
413
+ }
414
+ });
415
+
416
+ /**
417
+ * GET /api/settings/api-providers
418
+ * Get API provider metadata
419
+ */
420
+ router.get('/api-providers', (req, res) => {
421
+ res.json(API_PROVIDERS);
422
+ });
423
+
424
+ // ==========================================================================
425
+ // QUALITY LEVELS
426
+ // ==========================================================================
427
+
428
+ /**
429
+ * GET /api/settings/quality-levels
430
+ * Get all quality level configurations
431
+ */
432
+ router.get('/quality-levels', (req, res) => {
433
+ try {
434
+ const settings = loadSettings();
435
+ res.json(settings.qualityLevels || DEFAULT_SETTINGS.qualityLevels);
436
+ } catch (err) {
437
+ console.error('[settings:getQualityLevels]', err);
438
+ res.status(500).json({ error: err.message });
439
+ }
440
+ });
441
+
442
+ /**
443
+ * POST /api/settings/quality-level/:level/model
444
+ * Set the model for a quality level
445
+ */
446
+ router.post('/quality-level/:level/model', (req, res) => {
447
+ try {
448
+ const level = parseInt(req.params.level, 10);
449
+ const { model } = req.body;
450
+
451
+ if (isNaN(level) || level < 1 || level > 5) {
452
+ return res.status(400).json({ error: 'level must be 1-5' });
453
+ }
454
+
455
+ const settings = loadSettings();
456
+ if (!settings.qualityLevels) {
457
+ settings.qualityLevels = { ...DEFAULT_SETTINGS.qualityLevels };
458
+ }
459
+
460
+ const current = settings.qualityLevels[level] || {};
461
+ settings.qualityLevels[level] = { ...current, model };
462
+
463
+ const success = saveSettings();
464
+ res.json({ success });
465
+ } catch (err) {
466
+ console.error('[settings:setQualityLevelModel]', err);
467
+ res.status(500).json({ error: err.message });
468
+ }
469
+ });
470
+
471
+ // ==========================================================================
472
+ // CUSTOM COMMANDS
473
+ // ==========================================================================
474
+
475
+ /**
476
+ * GET /api/settings/custom-sections
477
+ * Get all custom sections with their commands
478
+ */
479
+ router.get('/custom-sections', (req, res) => {
480
+ try {
481
+ const settings = loadSettings();
482
+ res.json(settings.customSections || []);
483
+ } catch (err) {
484
+ console.error('[settings:getCustomSections]', err);
485
+ res.status(500).json({ error: err.message });
486
+ }
487
+ });
488
+
489
+ /**
490
+ * POST /api/settings/custom-section
491
+ * Add a new custom section
492
+ */
493
+ router.post('/custom-section', (req, res) => {
494
+ try {
495
+ const { name } = req.body;
496
+ if (!name) {
497
+ return res.status(400).json({ error: 'name required' });
498
+ }
499
+
500
+ const settings = loadSettings();
501
+ if (!settings.customSections) {
502
+ settings.customSections = [];
503
+ }
504
+
505
+ const id = `section-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
506
+ const section = { id, name, commands: [] };
507
+ settings.customSections.push(section);
508
+ saveSettings();
509
+
510
+ res.json(section);
511
+ } catch (err) {
512
+ console.error('[settings:addCustomSection]', err);
513
+ res.status(500).json({ error: err.message });
514
+ }
515
+ });
516
+
517
+ /**
518
+ * DELETE /api/settings/custom-section/:id
519
+ * Remove a custom section
520
+ */
521
+ router.delete('/custom-section/:id', (req, res) => {
522
+ try {
523
+ const { id } = req.params;
524
+
525
+ const settings = loadSettings();
526
+ const sections = settings.customSections || [];
527
+ const filtered = sections.filter(s => s.id !== id);
528
+
529
+ if (filtered.length === sections.length) {
530
+ return res.status(404).json({ error: 'Section not found' });
531
+ }
532
+
533
+ settings.customSections = filtered;
534
+ const success = saveSettings();
535
+ res.json({ success });
536
+ } catch (err) {
537
+ console.error('[settings:removeCustomSection]', err);
538
+ res.status(500).json({ error: err.message });
539
+ }
540
+ });
541
+
542
+ /**
543
+ * POST /api/settings/custom-command
544
+ * Add a custom command to a section
545
+ */
546
+ router.post('/custom-command', (req, res) => {
547
+ try {
548
+ const { sectionId, command } = req.body;
549
+ if (!sectionId || !command) {
550
+ return res.status(400).json({ error: 'sectionId and command required' });
551
+ }
552
+
553
+ const settings = loadSettings();
554
+ const sections = settings.customSections || [];
555
+ const section = sections.find(s => s.id === sectionId);
556
+
557
+ if (!section) {
558
+ return res.status(404).json({ error: 'Section not found' });
559
+ }
560
+
561
+ const id = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
562
+ const newCommand = {
563
+ id,
564
+ ...command,
565
+ program: `Custom_${id.replace(/-/g, '_')}`,
566
+ resultField: command.resultField || 'result',
567
+ };
568
+
569
+ section.commands.push(newCommand);
570
+ saveSettings();
571
+
572
+ res.json(newCommand);
573
+ } catch (err) {
574
+ console.error('[settings:addCustomCommand]', err);
575
+ res.status(500).json({ error: err.message });
576
+ }
577
+ });
578
+
579
+ /**
580
+ * PUT /api/settings/custom-command
581
+ * Update a custom command
582
+ */
583
+ router.put('/custom-command', (req, res) => {
584
+ try {
585
+ const { sectionId, commandId, updates } = req.body;
586
+ if (!sectionId || !commandId || !updates) {
587
+ return res.status(400).json({ error: 'sectionId, commandId, and updates required' });
588
+ }
589
+
590
+ const settings = loadSettings();
591
+ const sections = settings.customSections || [];
592
+ const section = sections.find(s => s.id === sectionId);
593
+
594
+ if (!section) {
595
+ return res.status(404).json({ error: 'Section not found' });
596
+ }
597
+
598
+ const command = section.commands.find(c => c.id === commandId);
599
+ if (!command) {
600
+ return res.status(404).json({ error: 'Command not found' });
601
+ }
602
+
603
+ Object.assign(command, updates);
604
+ const success = saveSettings();
605
+ res.json({ success });
606
+ } catch (err) {
607
+ console.error('[settings:updateCustomCommand]', err);
608
+ res.status(500).json({ error: err.message });
609
+ }
610
+ });
611
+
612
+ /**
613
+ * DELETE /api/settings/custom-command
614
+ * Remove a custom command
615
+ */
616
+ router.delete('/custom-command', (req, res) => {
617
+ try {
618
+ const { sectionId, commandId } = req.body;
619
+ if (!sectionId || !commandId) {
620
+ return res.status(400).json({ error: 'sectionId and commandId required' });
621
+ }
622
+
623
+ const settings = loadSettings();
624
+ const sections = settings.customSections || [];
625
+ const section = sections.find(s => s.id === sectionId);
626
+
627
+ if (!section) {
628
+ return res.status(404).json({ error: 'Section not found' });
629
+ }
630
+
631
+ const originalLength = section.commands.length;
632
+ section.commands = section.commands.filter(c => c.id !== commandId);
633
+
634
+ if (section.commands.length === originalLength) {
635
+ return res.status(404).json({ error: 'Command not found' });
636
+ }
637
+
638
+ const success = saveSettings();
639
+ res.json({ success });
640
+ } catch (err) {
641
+ console.error('[settings:removeCustomCommand]', err);
642
+ res.status(500).json({ error: err.message });
643
+ }
644
+ });
645
+
646
+ /**
647
+ * GET /api/settings/custom-commands
648
+ * Get all custom commands as a flat list
649
+ */
650
+ router.get('/custom-commands', (req, res) => {
651
+ try {
652
+ const settings = loadSettings();
653
+ const sections = settings.customSections || [];
654
+ const commands = [];
655
+
656
+ for (const section of sections) {
657
+ for (const command of section.commands || []) {
658
+ commands.push({
659
+ ...command,
660
+ sectionId: section.id,
661
+ sectionName: section.name,
662
+ });
663
+ }
664
+ }
665
+
666
+ res.json(commands);
667
+ } catch (err) {
668
+ console.error('[settings:getAllCustomCommands]', err);
669
+ res.status(500).json({ error: err.message });
670
+ }
671
+ });
672
+
673
+ // ==========================================================================
674
+ // DEFAULTS
675
+ // ==========================================================================
676
+
677
+ /**
678
+ * GET /api/settings/defaults
679
+ * Get default juice and reasoning levels
680
+ */
681
+ router.get('/defaults', (req, res) => {
682
+ try {
683
+ const settings = loadSettings();
684
+ res.json({
685
+ juiceLevel: settings.defaults?.juiceLevel ?? 2,
686
+ reasoningLevel: settings.defaults?.reasoningLevel ?? 1,
687
+ });
688
+ } catch (err) {
689
+ console.error('[settings:getDefaults]', err);
690
+ res.status(500).json({ error: err.message });
691
+ }
692
+ });
693
+
694
+ /**
695
+ * POST /api/settings/defaults
696
+ * Set default juice and/or reasoning levels
697
+ */
698
+ router.post('/defaults', (req, res) => {
699
+ try {
700
+ const { juiceLevel, reasoningLevel } = req.body;
701
+
702
+ const settings = loadSettings();
703
+ if (!settings.defaults) {
704
+ settings.defaults = {};
705
+ }
706
+
707
+ if (juiceLevel !== undefined) {
708
+ settings.defaults.juiceLevel = juiceLevel;
709
+ }
710
+ if (reasoningLevel !== undefined) {
711
+ settings.defaults.reasoningLevel = reasoningLevel;
712
+ }
713
+
714
+ const success = saveSettings();
715
+ res.json({ success });
716
+ } catch (err) {
717
+ console.error('[settings:setDefaults]', err);
718
+ res.status(500).json({ error: err.message });
719
+ }
720
+ });
721
+
722
+ // ==========================================================================
723
+ // EXPORT/IMPORT
724
+ // ==========================================================================
725
+
726
+ /**
727
+ * GET /api/settings/export?includeKeys=false
728
+ * Export settings to JSON string
729
+ */
730
+ router.get('/export', (req, res) => {
731
+ try {
732
+ const includeKeys = req.query.includeKeys === 'true';
733
+ const settings = loadSettings();
734
+
735
+ if (!includeKeys) {
736
+ const exported = { ...settings };
737
+ exported.apiKeys = {};
738
+ return res.json({ json: JSON.stringify(exported, null, 2) });
739
+ }
740
+
741
+ res.json({ json: JSON.stringify(settings, null, 2) });
742
+ } catch (err) {
743
+ console.error('[settings:export]', err);
744
+ res.status(500).json({ error: err.message });
745
+ }
746
+ });
747
+
748
+ /**
749
+ * POST /api/settings/import
750
+ * Import settings from JSON string
751
+ */
752
+ router.post('/import', (req, res) => {
753
+ try {
754
+ const { json, mergeKeys } = req.body;
755
+ if (!json) {
756
+ return res.status(400).json({ error: 'json required' });
757
+ }
758
+
759
+ const imported = JSON.parse(json);
760
+
761
+ if (typeof imported !== 'object') {
762
+ return res.status(400).json({ error: 'Invalid settings format' });
763
+ }
764
+
765
+ // Preserve existing keys if not merging
766
+ if (!mergeKeys) {
767
+ const currentSettings = loadSettings();
768
+ imported.apiKeys = currentSettings.apiKeys || {};
769
+ }
770
+
771
+ // Merge with defaults and save
772
+ settingsCache = mergeWithDefaults(imported);
773
+ const success = saveSettings();
774
+ res.json({ success });
775
+ } catch (err) {
776
+ console.error('[settings:import]', err);
777
+ res.status(500).json({ error: err.message });
778
+ }
779
+ });
780
+
781
+ return router;
782
+ }