winter-super-cli 2026.5.21 → 2026.5.24
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/package.json +1 -1
- package/rules/default.md +1 -0
- package/src/cli/repl.js +37 -7
- package/src/tools/executor.js +75 -8
- package/src/tools/executor.test.js +49 -0
package/package.json
CHANGED
package/rules/default.md
CHANGED
|
@@ -80,6 +80,7 @@ Check for these files in order:
|
|
|
80
80
|
|
|
81
81
|
### Tool Guidelines
|
|
82
82
|
- Call tools proactively - don't just describe, DO
|
|
83
|
+
- CHỦ ĐỘNG TÌM KIẾM: Khi người dùng yêu cầu sửa đổi hoặc thêm tính năng mà không chỉ định file cụ thể, AI PHẢI TỰ DÙNG các công cụ như `list_dir` hoặc `grep_search` để tìm kiếm file liên quan trong dự án. TUYỆT ĐỐI KHÔNG ĐƯỢC hỏi xin code hoặc xin đường dẫn file từ người dùng nếu có thể tự tìm thấy!
|
|
83
84
|
- Prefer Read over describing code
|
|
84
85
|
- Use Edit for small changes, Write for new files
|
|
85
86
|
- Verify changes after execution
|
package/src/cli/repl.js
CHANGED
|
@@ -1422,17 +1422,25 @@ ${colors.reset}
|
|
|
1422
1422
|
const maxLen = BOX_WIDTH - 8;
|
|
1423
1423
|
const lines = summary.split('\n');
|
|
1424
1424
|
for (const line of lines) {
|
|
1425
|
-
|
|
1426
|
-
|
|
1425
|
+
// Loại bỏ mã màu ANSI để tính toán độ dài và cắt dòng chuẩn xác
|
|
1426
|
+
const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1427
|
+
|
|
1428
|
+
if (cleanLine.length <= maxLen) {
|
|
1429
|
+
const padLen = BOX_WIDTH - 7 - cleanLine.length;
|
|
1430
|
+
const padding = ' '.repeat(Math.max(0, padLen));
|
|
1431
|
+
console.log(`${colors.magenta}│${colors.reset} ${statusIcon} ${colors.dim}${cleanLine}${colors.reset}${padding}${colors.magenta}│${colors.reset}`);
|
|
1427
1432
|
} else {
|
|
1428
1433
|
// Word wrap
|
|
1429
|
-
let remaining =
|
|
1434
|
+
let remaining = cleanLine;
|
|
1430
1435
|
let first = true;
|
|
1431
1436
|
while (remaining.length > 0) {
|
|
1432
1437
|
const chunk = remaining.substring(0, maxLen);
|
|
1433
1438
|
remaining = remaining.substring(maxLen);
|
|
1434
1439
|
const prefix = first ? statusIcon : ' ';
|
|
1435
|
-
|
|
1440
|
+
|
|
1441
|
+
const padLen = BOX_WIDTH - 7 - chunk.length;
|
|
1442
|
+
const padding = ' '.repeat(Math.max(0, padLen));
|
|
1443
|
+
console.log(`${colors.magenta}│${colors.reset} ${prefix} ${colors.dim}${chunk}${colors.reset}${padding}${colors.magenta}│${colors.reset}`);
|
|
1436
1444
|
first = false;
|
|
1437
1445
|
}
|
|
1438
1446
|
}
|
|
@@ -1676,19 +1684,41 @@ ${colors.reset}
|
|
|
1676
1684
|
|
|
1677
1685
|
if (this.slashMenu.printedLines) {
|
|
1678
1686
|
readline.moveCursor(process.stdout, 0, -this.slashMenu.printedLines);
|
|
1679
|
-
readline.clearScreenDown(process.stdout);
|
|
1680
1687
|
}
|
|
1681
1688
|
|
|
1682
1689
|
process.stdout.write('\n');
|
|
1690
|
+
readline.clearLine(process.stdout, 1);
|
|
1683
1691
|
process.stdout.write(`${colors.dim}Commands${colors.reset}\n`);
|
|
1684
|
-
|
|
1692
|
+
|
|
1693
|
+
const maxDisplay = 5;
|
|
1694
|
+
const displayedMatches = matches.slice(0, maxDisplay);
|
|
1695
|
+
|
|
1696
|
+
displayedMatches.forEach((item, index) => {
|
|
1697
|
+
readline.clearLine(process.stdout, 1);
|
|
1685
1698
|
const usage = item.usage ? ` ${colors.dim}${item.usage}${colors.reset}` : '';
|
|
1686
1699
|
const pointer = index === this.slashMenu.selected ? `${colors.green}>${colors.reset}` : ' ';
|
|
1687
1700
|
process.stdout.write(`${pointer} ${colors.cyan}${item.cmd}${colors.reset} ${colors.dim}${item.desc}${colors.reset}${usage}\n`);
|
|
1688
1701
|
});
|
|
1702
|
+
|
|
1703
|
+
if (matches.length > maxDisplay) {
|
|
1704
|
+
readline.clearLine(process.stdout, 1);
|
|
1705
|
+
process.stdout.write(` ${colors.dim}... và ${matches.length - maxDisplay} lệnh khác (gõ tiếp để lọc)${colors.reset}\n`);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
readline.clearLine(process.stdout, 1);
|
|
1689
1709
|
process.stdout.write(`${colors.dim}↑/↓ chọn · Enter/Tab dùng · Esc đóng${colors.reset}\n`);
|
|
1690
1710
|
|
|
1691
|
-
|
|
1711
|
+
// Xóa các dòng thừa nếu số lượng dòng mới ít hơn số lượng dòng cũ
|
|
1712
|
+
const currentLines = Math.min(matches.length, maxDisplay) + 3 + (matches.length > maxDisplay ? 1 : 0);
|
|
1713
|
+
if (this.slashMenu.printedLines > currentLines) {
|
|
1714
|
+
for (let i = 0; i < this.slashMenu.printedLines - currentLines; i++) {
|
|
1715
|
+
readline.clearLine(process.stdout, 1);
|
|
1716
|
+
process.stdout.write('\n');
|
|
1717
|
+
}
|
|
1718
|
+
readline.moveCursor(process.stdout, 0, -(this.slashMenu.printedLines - currentLines));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
this.slashMenu.printedLines = currentLines;
|
|
1692
1722
|
this.rl.prompt(true);
|
|
1693
1723
|
}
|
|
1694
1724
|
|
package/src/tools/executor.js
CHANGED
|
@@ -209,13 +209,7 @@ export class ToolExecutor {
|
|
|
209
209
|
case 'Write':
|
|
210
210
|
return await this.writeFile(this.resolveInputPath(input.file_path ?? input.path ?? input.file, cwd), input.content);
|
|
211
211
|
case 'Edit':
|
|
212
|
-
|
|
213
|
-
const newStr = input.new_string ?? input.newString ?? input.new_text ?? input.newText ?? input.replace ?? input.content;
|
|
214
|
-
return await this.editFile(
|
|
215
|
-
this.resolveInputPath(input.file_path ?? input.path ?? input.file, cwd),
|
|
216
|
-
oldStr,
|
|
217
|
-
newStr
|
|
218
|
-
);
|
|
212
|
+
return await this.executeEdit(input, cwd);
|
|
219
213
|
case 'Bash':
|
|
220
214
|
return await this.bash(input.command ?? input.cmd, input.cwd || cwd, input.timeout, input.shell);
|
|
221
215
|
case 'Glob':
|
|
@@ -410,12 +404,85 @@ export class ToolExecutor {
|
|
|
410
404
|
}
|
|
411
405
|
}
|
|
412
406
|
|
|
407
|
+
async executeEdit(input, cwd) {
|
|
408
|
+
const request = this.unwrapToolInput(input);
|
|
409
|
+
const batch = request.edits ?? request.replacements ?? request.changes;
|
|
410
|
+
|
|
411
|
+
if (Array.isArray(batch)) {
|
|
412
|
+
const results = [];
|
|
413
|
+
for (const item of batch) {
|
|
414
|
+
const edit = this.normalizeEditArgs({ ...request, ...this.unwrapToolInput(item) }, cwd);
|
|
415
|
+
const result = await this.editFile(edit.filePath, edit.oldString, edit.newString);
|
|
416
|
+
results.push(result);
|
|
417
|
+
if (result.success === false) {
|
|
418
|
+
return { ...result, batchResults: results };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
success: true,
|
|
424
|
+
path: results[results.length - 1]?.path,
|
|
425
|
+
replacements: results.reduce((sum, result) => sum + (result.replacements || 0), 0),
|
|
426
|
+
batchResults: results,
|
|
427
|
+
diff: results.map(result => result.diff).filter(Boolean).join('\n'),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const edit = this.normalizeEditArgs(request, cwd);
|
|
432
|
+
return await this.editFile(edit.filePath, edit.oldString, edit.newString);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
unwrapToolInput(input) {
|
|
436
|
+
let current = input && typeof input === 'object' ? input : {};
|
|
437
|
+
for (const key of ['input', 'args', 'arguments', 'parameters']) {
|
|
438
|
+
if (
|
|
439
|
+
current[key]
|
|
440
|
+
&& typeof current[key] === 'object'
|
|
441
|
+
&& !Array.isArray(current[key])
|
|
442
|
+
&& Object.keys(current).length === 1
|
|
443
|
+
) {
|
|
444
|
+
current = current[key];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return current;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
normalizeEditArgs(input, cwd) {
|
|
451
|
+
const pick = (keys) => {
|
|
452
|
+
for (const key of keys) {
|
|
453
|
+
if (typeof input[key] === 'string') return input[key];
|
|
454
|
+
}
|
|
455
|
+
return undefined;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const filePath = this.resolveInputPath(pick([
|
|
459
|
+
'file_path', 'filepath', 'filePath', 'path', 'file', 'filename', 'target_file', 'targetFile',
|
|
460
|
+
]), cwd);
|
|
461
|
+
const oldString = pick([
|
|
462
|
+
'old_string', 'oldString', 'old_text', 'oldText', 'old_str', 'oldStr',
|
|
463
|
+
'search', 'search_string', 'searchString', 'find', 'find_text', 'findText',
|
|
464
|
+
'target', 'target_string', 'targetString', 'text_to_replace', 'textToReplace',
|
|
465
|
+
'pattern', 'original', 'before',
|
|
466
|
+
]);
|
|
467
|
+
const newString = pick([
|
|
468
|
+
'new_string', 'newString', 'new_text', 'newText', 'new_str', 'newStr',
|
|
469
|
+
'replace', 'replacement', 'replace_with', 'replaceWith',
|
|
470
|
+
'new_content', 'newContent', 'content', 'value', 'after',
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
return { filePath, oldString, newString };
|
|
474
|
+
}
|
|
475
|
+
|
|
413
476
|
async editFile(filePath, oldString, newString) {
|
|
414
477
|
if (!filePath) {
|
|
415
478
|
return { success: false, error: 'file_path is required' };
|
|
416
479
|
}
|
|
417
480
|
if (typeof oldString !== 'string' || typeof newString !== 'string') {
|
|
418
|
-
return {
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: 'old_string and new_string are required. Accepted aliases: oldString/old_str/search/find/text_to_replace and newString/new_str/replace/replacement/replace_with. For full-file replacement use Write instead of Edit.',
|
|
484
|
+
path: filePath,
|
|
485
|
+
};
|
|
419
486
|
}
|
|
420
487
|
|
|
421
488
|
try {
|
|
@@ -38,6 +38,55 @@ test('tool names accept common model aliases', () => {
|
|
|
38
38
|
assert.equal(tools.normalizeToolName('web-search'), 'WebSearch');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test('Edit accepts common model argument aliases', async () => {
|
|
42
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-edit-alias-'));
|
|
43
|
+
await writeFile(path.join(root, 'file.txt'), 'hello world\n');
|
|
44
|
+
|
|
45
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
46
|
+
const result = await tools.execute('Edit', {
|
|
47
|
+
path: 'file.txt',
|
|
48
|
+
find: 'hello',
|
|
49
|
+
replacement: 'hi',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
assert.equal(result.success, true);
|
|
53
|
+
const read = await tools.execute('Read', { path: 'file.txt' });
|
|
54
|
+
assert.equal(read.content, 'hi world\n');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('Edit accepts nested and batch edit arguments', async () => {
|
|
58
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-edit-batch-'));
|
|
59
|
+
await writeFile(path.join(root, 'file.txt'), 'alpha beta gamma\n');
|
|
60
|
+
|
|
61
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
62
|
+
const result = await tools.execute('replace_in_file', {
|
|
63
|
+
arguments: {
|
|
64
|
+
file_path: 'file.txt',
|
|
65
|
+
edits: [
|
|
66
|
+
{ old_str: 'alpha', new_str: 'one' },
|
|
67
|
+
{ text_to_replace: 'gamma', replace_with: 'three' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert.equal(result.success, true);
|
|
73
|
+
assert.equal(result.replacements, 2);
|
|
74
|
+
const read = await tools.execute('Read', { path: 'file.txt' });
|
|
75
|
+
assert.equal(read.content, 'one beta three\n');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('Edit missing strings returns recovery guidance', async () => {
|
|
79
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-edit-recovery-'));
|
|
80
|
+
await writeFile(path.join(root, 'file.txt'), 'hello\n');
|
|
81
|
+
|
|
82
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
83
|
+
const result = await tools.execute('Edit', { path: 'file.txt' });
|
|
84
|
+
|
|
85
|
+
assert.equal(result.success, false);
|
|
86
|
+
assert.match(result.error, /Accepted aliases/);
|
|
87
|
+
assert.match(result.error, /Write instead of Edit/);
|
|
88
|
+
});
|
|
89
|
+
|
|
41
90
|
test('Bash supports model-style heredoc file writes', async () => {
|
|
42
91
|
const root = await mkdtemp(path.join(tmpdir(), 'winter-heredoc-'));
|
|
43
92
|
const tools = new ToolExecutor({ projectPath: root });
|