dave-code 1.0.3 → 1.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/bin/cliMenu.js CHANGED
@@ -8,7 +8,56 @@
8
8
  */
9
9
 
10
10
  import readline from 'readline';
11
- import ora from 'ora';
11
+
12
+ function stripAnsi(str) {
13
+ return String(str || '').replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '');
14
+ }
15
+
16
+ function getStringWidth(str) {
17
+ const cleanStr = stripAnsi(str);
18
+ let width = 0;
19
+ for (let i = 0; i < cleanStr.length; i++) {
20
+ const code = cleanStr.charCodeAt(i);
21
+ if (
22
+ (code >= 0x4e00 && code <= 0x9fff) ||
23
+ (code >= 0x3400 && code <= 0x4dbf) ||
24
+ (code >= 0x3000 && code <= 0x303f) ||
25
+ (code >= 0xff00 && code <= 0xffef)
26
+ ) {
27
+ width += 2;
28
+ } else {
29
+ width += 1;
30
+ }
31
+ }
32
+ return width;
33
+ }
34
+
35
+ function truncateDisplay(str, maxWidth) {
36
+ const text = String(str || '');
37
+ if (getStringWidth(text) <= maxWidth) return text;
38
+ let width = 0;
39
+ let output = '';
40
+ for (const char of text) {
41
+ const charWidth = getStringWidth(char);
42
+ if (width + charWidth > maxWidth - 3) break;
43
+ output += char;
44
+ width += charWidth;
45
+ }
46
+ return output + '...';
47
+ }
48
+
49
+ function enterMenuScreen(stdout) {
50
+ stdout.write('\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J');
51
+ }
52
+
53
+ function redrawMenuScreen(stdout, draw) {
54
+ stdout.write('\x1b[H\x1b[2J');
55
+ draw();
56
+ }
57
+
58
+ function exitMenuScreen(stdout) {
59
+ stdout.write('\x1b[?25h\x1b[?1049l');
60
+ }
12
61
 
13
62
  export function selectMenu(title, description, options, defaultIndex = 0) {
14
63
  return new Promise((resolve) => {
@@ -23,51 +72,54 @@ export function selectMenu(title, description, options, defaultIndex = 0) {
23
72
  }
24
73
  stdin.resume();
25
74
 
26
- // Hide terminal cursor
27
- stdout.write('\x1b[?25l');
75
+ enterMenuScreen(stdout);
28
76
 
29
77
  function draw() {
30
- stdout.write(`\n\x1b[1;36m${title}\x1b[0m\n`);
31
- stdout.write(`\x1b[90m${description}\x1b[0m\n\n`);
78
+ const width = Math.max(24, Math.min(stdout.columns || 100, 120));
79
+ const compact = width < 64;
80
+ const nameWidth = Math.max(12, Math.floor(width * 0.42));
81
+ const descWidth = Math.max(10, width - nameWidth - 10);
82
+
83
+ stdout.write(`\x1b[1;36m${truncateDisplay(title, width)}\x1b[0m\n`);
84
+ stdout.write(`\x1b[90m${truncateDisplay(description, width)}\x1b[0m\n\n`);
32
85
 
33
86
  for (let i = 0; i < options.length; i++) {
34
87
  const opt = options[i];
88
+ const name = truncateDisplay(opt.name, compact ? width - 8 : nameWidth);
89
+ const desc = truncateDisplay(opt.desc, compact ? width - 7 : descWidth);
35
90
  if (i === cursor) {
36
- stdout.write(` \x1b[32m›\x1b[0m \x1b[1;32m${i + 1}. ${opt.name}\x1b[0m \x1b[90m${opt.desc}\x1b[0m\n`);
91
+ stdout.write(` \x1b[32m›\x1b[0m \x1b[1;32m${i + 1}. ${name}\x1b[0m${compact ? '\n \x1b[90m' + desc + '\x1b[0m' : ' \x1b[90m' + desc + '\x1b[0m'}\n`);
37
92
  } else {
38
- stdout.write(` ${i + 1}. ${opt.name} \x1b[90m${opt.desc}\x1b[0m\n`);
93
+ stdout.write(` ${i + 1}. ${name}${compact ? '\n \x1b[90m' + desc + '\x1b[0m' : ' \x1b[90m' + desc + '\x1b[0m'}\n`);
39
94
  }
40
95
  }
41
- stdout.write('\n\x1b[90mUse Arrow Keys (↑/↓) to navigate, Enter to select, Esc to cancel\x1b[0m\n');
96
+ stdout.write('\n\x1b[90mUse ↑/↓ or number keys, Enter to select, Esc to cancel\x1b[0m\n');
42
97
  }
43
98
 
44
- function clearLines(count) {
45
- for (let i = 0; i < count; i++) {
46
- stdout.write('\x1b[1A\x1b[2K');
47
- }
99
+ function redrawMenu() {
100
+ redrawMenuScreen(stdout, draw);
48
101
  }
49
102
 
50
- const totalLines = options.length + 6;
51
- draw();
103
+ redrawMenu();
52
104
 
53
105
  function onKeypress(str, key) {
54
106
  if (!key) return;
55
107
 
56
108
  if (key.name === 'up') {
57
109
  cursor = (cursor - 1 + options.length) % options.length;
58
- clearLines(totalLines);
59
- draw();
110
+ redrawMenu();
60
111
  } else if (key.name === 'down') {
61
112
  cursor = (cursor + 1) % options.length;
62
- clearLines(totalLines);
63
- draw();
113
+ redrawMenu();
114
+ } else if (/^[1-9]$/.test(str || '') && Number(str) <= options.length) {
115
+ cursor = Number(str) - 1;
116
+ cleanup();
117
+ resolve(cursor);
64
118
  } else if (key.name === 'return') {
65
119
  cleanup();
66
- stdout.write('\x1b[?25h'); // Show cursor
67
120
  resolve(cursor);
68
121
  } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
69
122
  cleanup();
70
- stdout.write('\x1b[?25h');
71
123
  resolve(-1);
72
124
  }
73
125
  }
@@ -78,6 +130,7 @@ export function selectMenu(title, description, options, defaultIndex = 0) {
78
130
  stdin.setRawMode(oldRawMode);
79
131
  }
80
132
  stdin.pause();
133
+ exitMenuScreen(stdout);
81
134
  }
82
135
 
83
136
  stdin.on('keypress', onKeypress);
@@ -153,12 +206,13 @@ export function chatInputPrompt(placeholder, currentLang) {
153
206
  const stdout = process.stdout;
154
207
 
155
208
  const footerText = currentLang === 'cn'
156
- ? '? 获取帮助 · Ctrl+C 退出'
157
- : '? for help · Ctrl+C to exit';
209
+ ? '/help 获取帮助 · Ctrl+C 退出'
210
+ : '/help for help · Ctrl+C to exit';
158
211
 
159
- stdout.write('\x1b[90m───────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\n');
160
- stdout.write(`› \x1b[90m${placeholder}\x1b[0m\n`);
161
- stdout.write(`\x1b[90m${footerText}\x1b[0m`);
212
+ const promptWidth = () => Math.max(24, Math.min(stdout.columns || 96, 120));
213
+ stdout.write(`\x1b[90m${'─'.repeat(promptWidth())}\x1b[0m\n`);
214
+ stdout.write(`› \x1b[90m${truncateDisplay(placeholder, promptWidth() - 3)}\x1b[0m\n`);
215
+ stdout.write(`\x1b[90m${truncateDisplay(footerText, promptWidth())}\x1b[0m`);
162
216
 
163
217
  stdout.write('\x1b[1A\x1b[3G');
164
218
 
@@ -172,23 +226,22 @@ export function chatInputPrompt(placeholder, currentLang) {
172
226
  let chars = [];
173
227
  let cursorIndex = 0;
174
228
 
175
- function getStringWidth(str) {
176
- const cleanStr = str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
177
- let width = 0;
178
- for (let i = 0; i < cleanStr.length; i++) {
179
- const code = cleanStr.charCodeAt(i);
180
- if (
181
- (code >= 0x4e00 && code <= 0x9fff) ||
182
- (code >= 0x3400 && code <= 0x4dbf) ||
183
- (code >= 0x3000 && code <= 0x303f) ||
184
- (code >= 0xff00 && code <= 0xffef)
185
- ) {
186
- width += 2;
187
- } else {
188
- width += 1;
189
- }
229
+ function getInputView() {
230
+ const available = Math.max(8, promptWidth() - 3);
231
+ let start = 0;
232
+ while (start < cursorIndex && getStringWidth(chars.slice(start, cursorIndex).join('')) > available - 1) {
233
+ start++;
234
+ }
235
+ const prefix = start > 0 ? '…' : '';
236
+ let visible = prefix;
237
+ for (let i = start; i < chars.length; i++) {
238
+ if (getStringWidth(visible + chars[i]) > available) break;
239
+ visible += chars[i];
190
240
  }
191
- return width;
241
+ return {
242
+ visible,
243
+ cursorLeft: prefix + chars.slice(start, cursorIndex).join('')
244
+ };
192
245
  }
193
246
 
194
247
  function redraw() {
@@ -197,16 +250,15 @@ export function chatInputPrompt(placeholder, currentLang) {
197
250
 
198
251
  const valueStr = chars.join('');
199
252
  if (valueStr.length === 0) {
200
- stdout.write(`› \x1b[90m${placeholder}\x1b[0m\n`);
253
+ stdout.write(`› \x1b[90m${truncateDisplay(placeholder, promptWidth() - 3)}\x1b[0m\n`);
201
254
  } else {
202
- stdout.write(`› ${valueStr}\n`);
255
+ stdout.write(`› ${getInputView().visible}\n`);
203
256
  }
204
257
 
205
- stdout.write(`\x1b[90m${footerText}\x1b[0m`);
258
+ stdout.write(`\x1b[90m${truncateDisplay(footerText, promptWidth())}\x1b[0m`);
206
259
  stdout.write('\x1b[1A');
207
260
 
208
- const leftPart = chars.slice(0, cursorIndex).join('');
209
- const cursorCol = 3 + (chars.length === 0 ? 0 : getStringWidth(leftPart));
261
+ const cursorCol = 3 + (chars.length === 0 ? 0 : getStringWidth(getInputView().cursorLeft));
210
262
  stdout.write(`\x1b[${cursorCol}G`);
211
263
  }
212
264
 
@@ -287,6 +339,10 @@ export function waitForEnter(promptText) {
287
339
  cleanup();
288
340
  stdout.write('\n');
289
341
  resolve();
342
+ } else if (key && (key.name === 'escape' || (key.ctrl && key.name === 'c'))) {
343
+ cleanup();
344
+ stdout.write('\n');
345
+ resolve();
290
346
  }
291
347
  }
292
348
 
@@ -302,144 +358,32 @@ export function waitForEnter(promptText) {
302
358
  });
303
359
  }
304
360
 
305
- export function startSpinner(prefixText) {
306
- const spinner = ora({
307
- text: prefixText,
308
- color: 'cyan',
309
- spinner: 'dots'
310
- }).start();
311
-
312
- return () => {
313
- spinner.stop();
314
- };
315
- }
316
-
317
- export function selectEffortMenu(currentEffort, currentLang) {
361
+ export async function selectEffortMenu(currentEffort, currentLang) {
318
362
  const levels = ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
319
- let cursor = levels.indexOf(currentEffort);
320
- if (cursor === -1) cursor = 2; // Default to 'high'
321
-
322
- return new Promise((resolve) => {
323
- const stdout = process.stdout;
324
- const stdin = process.stdin;
325
-
326
- readline.emitKeypressEvents(stdin);
327
- const oldRawMode = stdin.isRawMode;
328
- if (stdin.setRawMode) {
329
- stdin.setRawMode(true);
330
- }
331
- stdin.resume();
332
-
333
- // Hide terminal cursor
334
- stdout.write('\x1b[?25l');
335
-
336
- const title = currentLang === 'cn' ? '调整努力模式 (Adjust Effort Mode)' : 'Adjust Effort Mode';
337
- const description = currentLang === 'cn'
338
- ? '选择模型的思考努力程度。努力程度越高,循环上限和阅读长度越大;ultracode 模式会执行大文件消化工作流以完全读完。'
339
- : 'Select thinking effort level. Higher levels increase loop limits and file read sizes; ultracode triggers digestion workflows to read large files fully.';
340
-
341
- const footer = currentLang === 'cn'
342
- ? '使用左右方向键 (←/→) 移动滑块, 回车 (Enter) 确认, Esc/Ctrl+C 取消'
343
- : 'Use Arrow Keys (←/→) to navigate, Enter to select, Esc/Ctrl+C to cancel';
344
-
345
- function draw() {
346
- stdout.write(`\n\x1b[1;36m${title}\x1b[0m\n`);
347
- stdout.write(`\x1b[90m${description}\x1b[0m\n\n`);
348
-
349
- stdout.write(`Faster Smarter\n`);
350
- stdout.write(`───────────────────────────────────────────────────┋───────────────────────\n`);
351
-
352
- // Calculate Pointer line (triangle)
353
- const centers = [2, 12, 23, 36, 46, 60];
354
- const selectedCenter = centers[cursor];
355
-
356
- let pointerChar = '▲';
357
- // Triangle color: purple if ultracode, otherwise yellow
358
- let pointerColor = cursor === 5 ? '\x1b[1;38;2;140;90;240m' : '\x1b[1;33m';
359
-
360
- let rawPointerLine = ' '.repeat(selectedCenter) + '▲' + ' '.repeat(76 - selectedCenter - 1);
361
- let rawPointerLineChars = [...rawPointerLine];
362
- rawPointerLineChars[50] = '┋';
363
-
364
- let coloredPointerLine = '';
365
- for (let i = 0; i < rawPointerLineChars.length; i++) {
366
- if (i === selectedCenter) {
367
- coloredPointerLine += pointerColor + '▲\x1b[0m';
368
- } else if (i === 50) {
369
- coloredPointerLine += '\x1b[90m┋\x1b[0m';
370
- } else {
371
- coloredPointerLine += rawPointerLineChars[i];
372
- }
373
- }
374
- stdout.write(`${coloredPointerLine}\n`);
375
-
376
- // Calculate Labels line
377
- const optColors = levels.map((lvl, idx) => {
378
- if (idx === cursor) {
379
- return idx === 5 ? '\x1b[1;38;2;140;90;240m' : '\x1b[1;33m';
380
- }
381
- return '\x1b[90m';
382
- });
383
-
384
- stdout.write(` `);
385
- stdout.write(`${optColors[0]}low\x1b[0m` + ` `);
386
- stdout.write(`${optColors[1]}medium\x1b[0m` + ` `);
387
- stdout.write(`${optColors[2]}high\x1b[0m` + ` `);
388
- stdout.write(`${optColors[3]}xhigh\x1b[0m` + ` `);
389
- stdout.write(`${optColors[4]}max\x1b[0m` + ` \x1b[90m┋\x1b[0m `);
390
- stdout.write(`${optColors[5]}ultracode\x1b[0m\n`);
391
-
392
- // Print subtext line (spacing with divider)
393
- stdout.write(' '.repeat(50) + '\x1b[90m┋\x1b[0m\n\n');
394
-
395
- stdout.write(`\x1b[90m${footer}\x1b[0m\n`);
396
- }
397
-
398
- function clearLines(count) {
399
- for (let i = 0; i < count; i++) {
400
- stdout.write('\x1b[1A\x1b[2K');
401
- }
402
- }
403
-
404
- const totalLines = 10;
405
- draw();
406
-
407
- function onKeypress(str, key) {
408
- if (!key) return;
409
-
410
- if (key.name === 'left') {
411
- if (cursor > 0) {
412
- cursor--;
413
- clearLines(totalLines);
414
- draw();
415
- }
416
- } else if (key.name === 'right') {
417
- if (cursor < 5) {
418
- cursor++;
419
- clearLines(totalLines);
420
- draw();
421
- }
422
- } else if (key.name === 'return') {
423
- cleanup();
424
- stdout.write('\x1b[?25h'); // Show cursor
425
- resolve(levels[cursor]);
426
- } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
427
- cleanup();
428
- stdout.write('\x1b[?25h');
429
- resolve(null);
430
- }
431
- }
432
-
433
- function cleanup() {
434
- stdin.removeListener('keypress', onKeypress);
435
- if (stdin.setRawMode) {
436
- stdin.setRawMode(oldRawMode);
437
- }
438
- stdin.pause();
439
- }
440
-
441
- stdin.on('keypress', onKeypress);
442
- });
363
+ const descriptions = currentLang === 'cn'
364
+ ? [
365
+ '快速回答 · 最多 3 步 · 单次读取 150 行',
366
+ '轻量任务 · 最多 6 步 · 单次读取 300 行',
367
+ '日常编程(推荐)· 最多 12 步 · 单次读取 600 行',
368
+ '深入分析 · 最多 25 步 · 单次读取 1200 行',
369
+ '复杂任务 · 最多 50 步 · 单次读取 2500 行',
370
+ '超长任务 · 最多 150 步 · 大文件消化前会确认'
371
+ ]
372
+ : [
373
+ 'Quick answers · up to 3 steps · 150 lines per read',
374
+ 'Light tasks · up to 6 steps · 300 lines per read',
375
+ 'Daily coding (recommended) · up to 12 steps · 600 lines per read',
376
+ 'Deep analysis · up to 25 steps · 1200 lines per read',
377
+ 'Complex tasks · up to 50 steps · 2500 lines per read',
378
+ 'Long-running tasks · up to 150 steps · confirms large-file digestion'
379
+ ];
380
+ const selected = await selectMenu(
381
+ currentLang === 'cn' ? '调整努力模式' : 'Adjust Effort Mode',
382
+ currentLang === 'cn' ? '选择速度、上下文容量与连续工具步骤之间的平衡。' : 'Balance speed, context capacity, and consecutive tool steps.',
383
+ levels.map((level, index) => ({ name: level, desc: descriptions[index] })),
384
+ Math.max(0, levels.indexOf(currentEffort))
385
+ );
386
+ return selected === -1 ? null : levels[selected];
443
387
  }
444
388
 
445
389
  export function confirmPrompt(promptText) {
@@ -486,5 +430,3 @@ export function confirmPrompt(promptText) {
486
430
  stdin.on('keypress', onKeypress);
487
431
  });
488
432
  }
489
-
490
-
@@ -0,0 +1,50 @@
1
+ function exactCommand(input, command) {
2
+ return input === command || input.startsWith(`${command} `);
3
+ }
4
+
5
+ export function parsePlanCreation(argument) {
6
+ const separator = argument.indexOf(':');
7
+ if (separator === -1) {
8
+ return { error: 'Use /plan <name>: <request>.' };
9
+ }
10
+ const name = argument.slice(0, separator).trim();
11
+ const request = argument.slice(separator + 1).trim();
12
+ if (!name || !request) return { error: 'Plan name and request are both required.' };
13
+ if ([...name].length > 60) return { error: 'Plan name must be 60 characters or fewer.' };
14
+ if (/[\u0000-\u001f\u007f]/.test(name)) return { error: 'Plan name cannot contain control characters.' };
15
+ return { name, request };
16
+ }
17
+
18
+ export function parseInputCommand(input, { findPlan = () => null } = {}) {
19
+ const trimmed = String(input || '').trim();
20
+ if (trimmed === '/plan') return { type: 'plan.menu' };
21
+ if (exactCommand(trimmed, '/plan')) {
22
+ const parsed = parsePlanCreation(trimmed.slice('/plan'.length).trim());
23
+ return parsed.error ? { type: 'error', error: parsed.error } : { type: 'plan.create', ...parsed };
24
+ }
25
+
26
+ if (trimmed === '/code') return { type: 'code.usage' };
27
+ if (exactCommand(trimmed, '/code')) {
28
+ let argument = trimmed.slice('/code'.length).trim();
29
+ if (!argument) return { type: 'code.usage' };
30
+ if (argument.startsWith('--prompt ')) {
31
+ argument = argument.slice('--prompt '.length).trim();
32
+ return argument
33
+ ? { type: 'agent', mode: 'code', prompt: argument, plan: null }
34
+ : { type: 'code.usage' };
35
+ }
36
+ if (argument.startsWith('@')) {
37
+ const name = argument.slice(1).trim();
38
+ const plan = findPlan(name);
39
+ return plan
40
+ ? { type: 'agent', mode: 'code', prompt: plan.content, plan }
41
+ : { type: 'error', error: `Plan not found: ${name}` };
42
+ }
43
+ const plan = findPlan(argument);
44
+ return plan
45
+ ? { type: 'agent', mode: 'code', prompt: plan.content, plan }
46
+ : { type: 'agent', mode: 'code', prompt: argument, plan: null };
47
+ }
48
+
49
+ return { type: 'agent', mode: 'chat', prompt: trimmed, plan: null };
50
+ }
@@ -10,40 +10,118 @@
10
10
  import fs from 'fs';
11
11
  import path from 'path';
12
12
  import os from 'os';
13
- import { exec } from 'child_process';
13
+ import crypto from 'crypto';
14
+ import { spawn } from 'child_process';
14
15
 
15
16
  export let CONFIG_FILE = path.join(os.homedir(), '.dave-code-config.json');
17
+ export let INITIALIZED_MARKER = path.join(os.homedir(), '.dave-code-initialized');
18
+ export let lastConfigError = null;
19
+
20
+ function atomicWriteFile(filePath, content, mode = 0o600) {
21
+ const directory = path.dirname(filePath);
22
+ fs.mkdirSync(directory, { recursive: true });
23
+ const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(5).toString('hex')}.tmp`);
24
+ fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode });
25
+ try {
26
+ fs.chmodSync(tempPath, mode);
27
+ } catch {
28
+ // Best effort on Windows.
29
+ }
30
+ try {
31
+ fs.renameSync(tempPath, filePath);
32
+ } catch (error) {
33
+ if (process.platform !== 'win32' || !fs.existsSync(filePath)) throw error;
34
+ fs.unlinkSync(filePath);
35
+ fs.renameSync(tempPath, filePath);
36
+ } finally {
37
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
38
+ }
39
+ }
40
+
41
+ function normalizeProfile(profile, index) {
42
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
43
+ throw new Error(`Profile ${index + 1} must be an object.`);
44
+ }
45
+ const model = String(profile.model || '').trim();
46
+ if (!model) throw new Error(`Profile ${index + 1} is missing "model".`);
47
+ const provider = profile.provider === undefined ? undefined : String(profile.provider).toLowerCase();
48
+ if (provider && !['openai', 'anthropic', 'gemini'].includes(provider)) {
49
+ throw new Error(`Profile ${index + 1} has invalid provider "${provider}".`);
50
+ }
51
+ const toolMode = String(profile.toolMode || 'native').toLowerCase();
52
+ if (!['native', 'legacy'].includes(toolMode)) {
53
+ throw new Error(`Profile ${index + 1} has invalid toolMode "${toolMode}".`);
54
+ }
55
+ const maxOutputTokens = Number(profile.maxOutputTokens ?? 4096);
56
+ const temperature = Number(profile.temperature ?? 0.2);
57
+ if (!Number.isInteger(maxOutputTokens) || maxOutputTokens < 1 || maxOutputTokens > 200000) {
58
+ throw new Error(`Profile ${index + 1} has invalid maxOutputTokens.`);
59
+ }
60
+ if (!Number.isFinite(temperature) || temperature < 0 || temperature > 2) {
61
+ throw new Error(`Profile ${index + 1} has invalid temperature.`);
62
+ }
63
+ return {
64
+ ...profile,
65
+ model,
66
+ apiKey: String(profile.apiKey || ''),
67
+ apiBase: String(profile.apiBase || '').replace(/\/+$/, ''),
68
+ proxyUrl: String(profile.proxyUrl || ''),
69
+ ...(provider ? { provider } : {}),
70
+ toolMode,
71
+ maxOutputTokens,
72
+ temperature
73
+ };
74
+ }
16
75
 
17
76
  export function setConfigFilePathForTesting(filePath) {
18
77
  CONFIG_FILE = filePath;
19
78
  }
20
79
 
80
+ export function isFirstLaunch() {
81
+ return !fs.existsSync(INITIALIZED_MARKER);
82
+ }
83
+
84
+ export function setInitialized() {
85
+ try {
86
+ fs.writeFileSync(INITIALIZED_MARKER, 'true', { encoding: 'utf8', mode: 0o600 });
87
+ return true;
88
+ } catch (e) {
89
+ return false;
90
+ }
91
+ }
92
+
21
93
  const TEMPLATE_CONFIG = [
22
94
  {
23
95
  "model": "deepseek-chat",
24
96
  "apiKey": "YOUR_API_KEY",
25
97
  "apiBase": "https://api.deepseek.com/v1",
26
- "proxyUrl": ""
98
+ "proxyUrl": "",
99
+ "provider": "openai",
100
+ "toolMode": "native",
101
+ "maxOutputTokens": 4096,
102
+ "temperature": 0.2
27
103
  }
28
104
  ];
29
105
 
30
106
  export function loadConfig() {
107
+ lastConfigError = null;
31
108
  try {
32
109
  if (fs.existsSync(CONFIG_FILE)) {
33
110
  const data = fs.readFileSync(CONFIG_FILE, 'utf8');
34
111
  const parsed = JSON.parse(data);
35
- return Array.isArray(parsed) ? parsed : [];
112
+ if (!Array.isArray(parsed)) throw new Error('Configuration root must be a JSON array.');
113
+ return parsed.map(normalizeProfile);
36
114
  }
37
115
  } catch (e) {
38
- // Ignore and fallback to empty array
116
+ lastConfigError = e.message;
39
117
  }
40
118
  return [];
41
119
  }
42
120
 
43
121
  export function saveConfig(config) {
44
122
  try {
45
- const data = Array.isArray(config) ? config : [];
46
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
123
+ const data = Array.isArray(config) ? config.map(normalizeProfile) : [];
124
+ atomicWriteFile(CONFIG_FILE, JSON.stringify(data, null, 2), 0o600);
47
125
  return true;
48
126
  } catch (e) {
49
127
  return false;
@@ -126,11 +204,15 @@ export function openConfigFileInEditor() {
126
204
  // But since parsed.length > 0, they have some configs. We don't overwrite if they have active configurations.
127
205
  }
128
206
  } else {
129
- shouldWriteTemplate = true;
207
+ lastConfigError = 'Configuration root must be a JSON array.';
208
+ console.error(`Configuration is invalid and was not overwritten: ${lastConfigError}`);
209
+ shouldWriteTemplate = false;
130
210
  }
131
211
  }
132
212
  } catch (e) {
133
- shouldWriteTemplate = true;
213
+ lastConfigError = e.message;
214
+ console.error(`Configuration is invalid and was not overwritten: ${e.message}`);
215
+ shouldWriteTemplate = false;
134
216
  }
135
217
  }
136
218
 
@@ -143,16 +225,13 @@ export function openConfigFileInEditor() {
143
225
  }
144
226
 
145
227
  const command = process.platform === 'win32'
146
- ? `start "" "${CONFIG_FILE}"`
147
- : (process.platform === 'darwin' ? `open "${CONFIG_FILE}"` : `xdg-open "${CONFIG_FILE}"`);
148
-
149
- exec(command, (err) => {
150
- if (err) {
151
- if (process.platform === 'win32') {
152
- exec(`notepad "${CONFIG_FILE}"`);
153
- }
154
- }
155
- });
228
+ ? { file: 'explorer.exe', args: [CONFIG_FILE] }
229
+ : process.platform === 'darwin'
230
+ ? { file: 'open', args: [CONFIG_FILE] }
231
+ : { file: 'xdg-open', args: [CONFIG_FILE] };
232
+ const child = spawn(command.file, command.args, { detached: true, stdio: 'ignore', windowsHide: true });
233
+ child.on('error', () => {});
234
+ child.unref();
156
235
  }
157
236
 
158
237
  export function getModel() {
@@ -210,6 +289,7 @@ export function getActiveEffort() {
210
289
  }
211
290
 
212
291
  export function setActiveEffort(effort) {
292
+ if (!Object.prototype.hasOwnProperty.call(EFFORT_PRESETS, effort)) return false;
213
293
  const active = getActiveProfile();
214
294
  if (active) {
215
295
  updateProfile(active.model, { effort });
@@ -217,4 +297,3 @@ export function setActiveEffort(effort) {
217
297
  }
218
298
  return false;
219
299
  }
220
-