-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsettingsManager.js
More file actions
292 lines (259 loc) · 10.1 KB
/
settingsManager.js
File metadata and controls
292 lines (259 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
/**
* guIDE 2.0 — Settings Manager
*
* Centralized settings persistence with encrypted API key storage.
* Two stores:
* 1. settings.json — user preferences (plain JSON)
* 2. api-keys.enc — API keys (AES-256-GCM encrypted)
*
* Follows the same debounced-save pattern as MemoryStore/SessionStore.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const os = require('os');
const EventEmitter = require('events');
// ─── Defaults ────────────────────────────────────────────
const SETTINGS_DEFAULTS = {
// LLM / Inference
temperature: 0.4,
maxResponseTokens: 0, // 0 = auto (use all available context space)
contextSize: 0, // 0 = auto (hardware-aware ceiling); fixed values like 16000 also work
topP: 0.95,
topK: 40,
repeatPenalty: 1.1,
seed: -1,
// Thinking & Reasoning
thinkingBudget: 2048,
reasoningEffort: 'medium',
enableThinking: true, // Pass enable_thinking=true to chat template (Qwen 3.5 small models disable thinking by default; this activates it)
enableThinkingFilter: false,
// Agentic Behavior
maxIterations: 0,
generationTimeoutSec: 0,
enableGrammar: false,
// System Prompt
systemPrompt: '',
customInstructions: '',
// Hardware
gpuPreference: 'auto',
gpuLayers: -1,
requireMinContextForGpu: false,
gpuConstrainedContext: true, // When GPU layers < 30% of total, cap context to VRAM-bounded size for faster generation
vramBalance: 'balanced', // auto gpuLayers=-1: balanced | speed | context
kvCacheType: 'f16', // KV cache quantization — f16 matches llama.cpp upstream default and enables the fastest fused flash-attention path on consumer NVIDIA GPUs (matches LM Studio / llama-server). Lower-precision options (q8_0, q4_0) save VRAM at a measurable speed cost; user-overridable.
// Editor
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
tabSize: 2,
wordWrap: 'on',
minimap: true,
lineNumbers: 'on',
bracketPairColorization: true,
formatOnPaste: false,
formatOnType: false,
// Cloud AI
lastCloudProvider: null,
lastCloudModel: null,
// Setup
setupCompleted: false,
// Account
sessionToken: null,
accountUser: null,
licenseData: null,
// Auto-update — opt-in, off by default to honour the offline-first principle.
// 0 = disabled (no automatic check). >0 = check every N hours. The minimum sane
// value is 1; values below 1 are clamped to 1 by the periodic-check scheduler.
// The user can still manually trigger a check from the UI regardless of this value.
autoUpdateCheckHours: 0,
// UI State
lastModelPath: null,
lastProjectPath: null,
sidebarWidth: 260,
panelHeight: 200,
};
// ─── Encryption helpers ──────────────────────────────────
function deriveKey() {
// Machine-specific key derivation: hostname + username + static salt
// This means keys are only decryptable on the same machine by the same user
const identity = `${os.hostname()}:${os.userInfo().username}:guide-ide-keystore-v1`;
return crypto.pbkdf2Sync(identity, 'guide-ide-salt-2026', 100000, 32, 'sha256');
}
function encrypt(plaintext, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
// Format: iv (12) + tag (16) + ciphertext
return Buffer.concat([iv, tag, encrypted]).toString('base64');
}
function decrypt(encoded, key) {
const buf = Buffer.from(encoded, 'base64');
if (buf.length < 28) return null; // iv(12) + tag(16) minimum
const iv = buf.subarray(0, 12);
const tag = buf.subarray(12, 28);
const ciphertext = buf.subarray(28);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
try {
return decipher.update(ciphertext, null, 'utf8') + decipher.final('utf8');
} catch {
return null; // Decryption failed (wrong machine, corrupted, etc.)
}
}
// ─── SettingsManager class ───────────────────────────────
class SettingsManager extends EventEmitter {
/**
* @param {string} userDataPath — e.g. %APPDATA%/guide-ide
*/
constructor(userDataPath) {
super();
this._userDataPath = userDataPath;
this._settingsPath = path.join(userDataPath, 'settings.json');
this._keysPath = path.join(userDataPath, 'api-keys.enc');
this._encKey = deriveKey();
this._settings = { ...SETTINGS_DEFAULTS };
this._apiKeys = {};
this._saveTimer = null;
this._keysSaveTimer = null;
this._load();
}
/* ── Lifecycle ─────────────────────────────────────────── */
_load() {
// Ensure directory exists
try { fs.mkdirSync(this._userDataPath, { recursive: true }); } catch (_) {}
// Load settings
try {
if (fs.existsSync(this._settingsPath)) {
const raw = JSON.parse(fs.readFileSync(this._settingsPath, 'utf8'));
// Merge with defaults (new keys get defaults, removed keys get dropped)
this._settings = { ...SETTINGS_DEFAULTS, ...raw };
// Migration v2026-04-07: generationTimeoutSec default changed from 120→0.
// If stored value is exactly 120 (old default), reset to 0 (disabled).
// Preserves any custom timeout a user deliberately set (e.g. 60, 300).
if (this._settings.generationTimeoutSec === 120) {
this._settings.generationTimeoutSec = 0;
this._scheduleSave(); // write corrected value back to disk immediately
}
// Migration: fixed context used for server/dev testing (TEST_MAX_CONTEXT / manual 8k) → auto
const legacyFixedCtx = new Set([8000, 8192]);
if (legacyFixedCtx.has(this._settings.contextSize)) {
this._settings.contextSize = 0;
this._scheduleSave();
}
// Migration: legacy guIDE KV defaults (q3_0, q4_0) → f16. f16 matches llama.cpp upstream and
// enables the fastest fused flash-attention path on consumer NVIDIA GPUs (LM Studio / llama-server
// behaviour). Users who explicitly want lower-precision KV (e.g. for very long contexts on low-VRAM
// systems) can set q8_0 / q4_0 / etc. via settings — only legacy guIDE defaults are swept.
if (this._settings.kvCacheType === 'q3_0' || this._settings.kvCacheType === 'q4_0') {
this._settings.kvCacheType = 'f16';
this._scheduleSave();
}
}
} catch (e) {
console.warn('[SettingsManager] Failed to load settings:', e.message);
}
// Load encrypted API keys
try {
if (fs.existsSync(this._keysPath)) {
const encrypted = fs.readFileSync(this._keysPath, 'utf8').trim();
const decrypted = decrypt(encrypted, this._encKey);
if (decrypted) {
this._apiKeys = JSON.parse(decrypted);
} else {
console.warn('[SettingsManager] Could not decrypt API keys (machine changed?)');
}
}
} catch (e) {
console.warn('[SettingsManager] Failed to load API keys:', e.message);
}
}
/* ── Settings (plain JSON) ─────────────────────────────── */
get(key) {
return key in this._settings ? this._settings[key] : SETTINGS_DEFAULTS[key];
}
set(key, value) {
this._settings[key] = value;
this._scheduleSave();
this.emit('change', key, value);
}
getAll() {
return { ...this._settings };
}
setAll(obj) {
this._settings = { ...SETTINGS_DEFAULTS, ...obj };
this._scheduleSave();
this.emit('change', null, this._settings);
}
reset() {
this._settings = { ...SETTINGS_DEFAULTS };
this._scheduleSave();
this.emit('change', null, this._settings);
}
/* ── API Keys (encrypted) ──────────────────────────────── */
getApiKey(provider) {
return this._apiKeys[provider] || '';
}
setApiKey(provider, key) {
if (key && key.trim()) {
this._apiKeys[provider] = key;
} else {
delete this._apiKeys[provider];
}
this._scheduleKeysSave();
}
getAllApiKeys() {
return { ...this._apiKeys };
}
removeApiKey(provider) {
delete this._apiKeys[provider];
this._scheduleKeysSave();
}
hasApiKey(provider) {
return !!(this._apiKeys[provider] && this._apiKeys[provider].trim());
}
/* ── Persistence ───────────────────────────────────────── */
_scheduleSave() {
if (this._saveTimer) clearTimeout(this._saveTimer);
this._saveTimer = setTimeout(() => this._saveSettings(), 3000);
}
_scheduleKeysSave() {
if (this._keysSaveTimer) clearTimeout(this._keysSaveTimer);
this._keysSaveTimer = setTimeout(() => this._saveKeys(), 1000); // Keys save faster
}
_saveSettings() {
try {
fs.writeFileSync(this._settingsPath, JSON.stringify(this._settings, null, 2), 'utf8');
} catch (e) {
console.error('[SettingsManager] Failed to save settings:', e.message);
}
}
_saveKeys() {
try {
const json = JSON.stringify(this._apiKeys);
const encrypted = encrypt(json, this._encKey);
fs.writeFileSync(this._keysPath, encrypted, 'utf8');
} catch (e) {
console.error('[SettingsManager] Failed to save API keys:', e.message);
}
}
/** Flush all pending saves immediately (call on shutdown). */
flush() {
if (this._saveTimer) {
clearTimeout(this._saveTimer);
this._saveTimer = null;
this._saveSettings();
}
if (this._keysSaveTimer) {
clearTimeout(this._keysSaveTimer);
this._keysSaveTimer = null;
this._saveKeys();
}
}
/** Return the defaults for external use (e.g. reset endpoint). */
static get DEFAULTS() {
return { ...SETTINGS_DEFAULTS };
}
}
module.exports = { SettingsManager };