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/README.md +16 -1
- package/bin/aiClient.js +658 -164
- package/bin/cliMenu.js +127 -185
- package/bin/commandRouter.js +50 -0
- package/bin/configManager.js +98 -19
- package/bin/contextManager.js +151 -0
- package/bin/index.js +827 -450
- package/bin/install.js +3 -5
- package/bin/planManager.js +239 -0
- package/bin/runtimeEvents.js +62 -0
- package/bin/sessionManager.js +166 -0
- package/bin/terminalRenderer.js +283 -0
- package/bin/toolRuntime.js +1052 -0
- package/package.json +3 -2
package/bin/cliMenu.js
CHANGED
|
@@ -8,7 +8,56 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import readline from 'readline';
|
|
11
|
-
|
|
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
|
-
|
|
27
|
-
stdout.write('\x1b[?25l');
|
|
75
|
+
enterMenuScreen(stdout);
|
|
28
76
|
|
|
29
77
|
function draw() {
|
|
30
|
-
stdout.
|
|
31
|
-
|
|
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}. ${
|
|
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}. ${
|
|
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
|
|
96
|
+
stdout.write('\n\x1b[90mUse ↑/↓ or number keys, Enter to select, Esc to cancel\x1b[0m\n');
|
|
42
97
|
}
|
|
43
98
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
stdout.write('\x1b[1A\x1b[2K');
|
|
47
|
-
}
|
|
99
|
+
function redrawMenu() {
|
|
100
|
+
redrawMenuScreen(stdout, draw);
|
|
48
101
|
}
|
|
49
102
|
|
|
50
|
-
|
|
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
|
-
|
|
59
|
-
draw();
|
|
110
|
+
redrawMenu();
|
|
60
111
|
} else if (key.name === 'down') {
|
|
61
112
|
cursor = (cursor + 1) % options.length;
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
? '
|
|
157
|
-
: '
|
|
209
|
+
? '/help 获取帮助 · Ctrl+C 退出'
|
|
210
|
+
: '/help for help · Ctrl+C to exit';
|
|
158
211
|
|
|
159
|
-
stdout.
|
|
160
|
-
stdout.write(
|
|
161
|
-
stdout.write(
|
|
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
|
|
176
|
-
const
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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(`› ${
|
|
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
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
package/bin/configManager.js
CHANGED
|
@@ -10,40 +10,118 @@
|
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import os from 'os';
|
|
13
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
147
|
-
:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|