mrmd-server 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/bin/cli.js +4 -20
- package/package.json +20 -3
- package/src/api/bash.js +72 -189
- package/src/api/file.js +26 -20
- package/src/api/index.js +5 -0
- package/src/api/notebook.js +290 -0
- package/src/api/project.js +178 -12
- package/src/api/pty.js +73 -293
- package/src/api/r.js +337 -0
- package/src/api/session.js +96 -251
- package/src/api/settings.js +782 -0
- package/src/api/system.js +199 -1
- package/src/server.js +133 -8
- package/src/services.js +42 -0
- package/src/sync-manager.js +223 -0
- package/static/favicon.png +0 -0
- package/static/http-shim.js +172 -3
- package/static/index.html +1 -0
|
@@ -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
|
+
}
|