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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.5.21",
3
+ "version": "2026.5.24",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
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
- if (line.length <= maxLen) {
1426
- console.log(`${colors.magenta}│${colors.reset} ${statusIcon} ${colors.dim}${line}${colors.reset}`);
1425
+ // Loại bỏ 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 = line;
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
- console.log(`${colors.magenta}│${colors.reset} ${prefix} ${colors.dim}${chunk}${colors.reset}`);
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
- matches.forEach((item, index) => {
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
- this.slashMenu.printedLines = matches.length + 3;
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
 
@@ -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
- const oldStr = input.old_string ?? input.oldString ?? input.old_text ?? input.oldText ?? input.search ?? input.target;
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 { success: false, error: 'old_string and new_string are required', path: filePath };
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 });