winter-super-cli 2026.5.19 → 2026.5.21

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.19",
3
+ "version": "2026.5.21",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -397,7 +397,7 @@ export class AIProviderManager {
397
397
  }
398
398
 
399
399
  getSystemPrompt() {
400
- return `You are Winter ❄️, an expert AI coding assistant.
400
+ return `You are Winter, an expert AI coding assistant.
401
401
 
402
402
  ## Core Principles
403
403
  1. **Think Before Coding** - State assumptions, ask when unclear
@@ -280,7 +280,7 @@ export class CommandParser {
280
280
  }
281
281
 
282
282
  getWinterSystemPrompt() {
283
- return `You are Winter ❄️, an expert AI coding assistant.
283
+ return `You are Winter, an expert AI coding assistant.
284
284
 
285
285
  Follow these principles:
286
286
 
@@ -356,4 +356,4 @@ ${colors.white}Configuration:${colors.reset}
356
356
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
357
357
  `);
358
358
  }
359
- }
359
+ }
package/src/cli/repl.js CHANGED
@@ -295,7 +295,7 @@ export class WinterREPL {
295
295
  this.rl = readline.createInterface({
296
296
  input: process.stdin,
297
297
  output: process.stdout,
298
- prompt: `${colors.bright}${colors.cyan}winter ${colors.reset} `,
298
+ prompt: `${colors.bright}${colors.cyan}winter > ${colors.reset}`,
299
299
  completer: this.completer.bind(this),
300
300
  });
301
301
  this.installSlashSuggestions();
@@ -1487,6 +1487,7 @@ ${colors.reset}
1487
1487
  const toolCallParts = [];
1488
1488
  let finishReason = null;
1489
1489
  let printed = false;
1490
+ const bufferToolModeContent = options?.enableTools === true;
1490
1491
 
1491
1492
  for await (const chunk of this.ai.streamRequest(messages, options)) {
1492
1493
  if (chunk.usage) this.addUsage(totalUsage, chunk.usage);
@@ -1515,40 +1516,57 @@ ${colors.reset}
1515
1516
  if (chunk.content) {
1516
1517
  if (!printed) {
1517
1518
  if (this.spinner) this.spinner.stop();
1518
- process.stdout.write(`\n${colors.white}`);
1519
- printed = true;
1519
+ if (!bufferToolModeContent) {
1520
+ process.stdout.write(`\n${colors.white}`);
1521
+ printed = true;
1522
+ }
1520
1523
  }
1521
1524
  content += chunk.content;
1522
- process.stdout.write(chunk.content);
1525
+ if (!bufferToolModeContent) {
1526
+ process.stdout.write(chunk.content);
1527
+ }
1523
1528
  }
1524
1529
  }
1525
1530
 
1526
1531
  if (this.spinner) this.spinner.stop();
1527
1532
  if (printed) process.stdout.write(colors.reset);
1528
1533
 
1529
- const rawToolCalls = toolCallParts
1530
- .filter(Boolean)
1531
- .map((toolCall, index) => ({
1534
+ const inlineToolExtraction = this.extractInlineToolCalls(content);
1535
+ const rawToolCalls = [
1536
+ ...toolCallParts.filter(Boolean).map((toolCall, index) => ({
1532
1537
  ...toolCall,
1533
1538
  id: toolCall.id || `call-${index}`,
1534
- }));
1539
+ })),
1540
+ ...inlineToolExtraction.toolCalls,
1541
+ ];
1535
1542
  const toolCalls = this.normalizeToolCalls(rawToolCalls);
1543
+ const visibleContent = inlineToolExtraction.content || content;
1544
+
1545
+ if (bufferToolModeContent && toolCalls.length === 0 && visibleContent) {
1546
+ this.printAssistantAnswer(visibleContent, startedAt, totalUsage);
1547
+ return {
1548
+ assistantMsg: { content: visibleContent },
1549
+ toolCalls,
1550
+ finalContent: visibleContent,
1551
+ finishReason,
1552
+ };
1553
+ }
1536
1554
 
1537
- if (toolCalls.length === 0 && content) {
1555
+ if (toolCalls.length === 0 && visibleContent) {
1538
1556
  console.log(`\n${colors.dim}${this.formatAnswerFooter(startedAt, totalUsage)}${colors.reset}\n`);
1539
1557
  return {
1540
- assistantMsg: { content },
1558
+ assistantMsg: { content: visibleContent },
1541
1559
  toolCalls,
1542
- finalContent: content,
1560
+ finalContent: visibleContent,
1543
1561
  finishReason,
1544
1562
  };
1545
- } else if (content) {
1563
+ } else if (printed && visibleContent) {
1546
1564
  process.stdout.write('\n');
1547
1565
  }
1548
1566
 
1549
1567
  return {
1550
1568
  assistantMsg: {
1551
- content,
1569
+ content: visibleContent,
1552
1570
  tool_calls: this.formatToolCallsForMessage(toolCalls),
1553
1571
  },
1554
1572
  toolCalls,
@@ -1924,6 +1942,42 @@ ${colors.reset}
1924
1942
  });
1925
1943
  }
1926
1944
 
1945
+ extractInlineToolCalls(content) {
1946
+ const text = String(content || '');
1947
+ const toolCalls = [];
1948
+ let cleaned = text;
1949
+ const callPattern = /<minimax:tool_call>\s*<invoke\s+name=["']([^"']+)["']>([\s\S]*?)<\/invoke>\s*<\/minimax:tool_call>/gi;
1950
+
1951
+ cleaned = cleaned.replace(callPattern, (_match, name, body) => {
1952
+ const args = {};
1953
+ const paramPattern = /<parameter\s+name=["']([^"']+)["']>([\s\S]*?)<\/parameter>/gi;
1954
+ let param;
1955
+ while ((param = paramPattern.exec(body))) {
1956
+ args[param[1]] = this.decodeXmlEntities(param[2].trim());
1957
+ }
1958
+ toolCalls.push({
1959
+ id: `inline-${Date.now()}-${toolCalls.length}`,
1960
+ type: 'function',
1961
+ function: {
1962
+ name,
1963
+ arguments: JSON.stringify(args),
1964
+ },
1965
+ });
1966
+ return '';
1967
+ }).trim();
1968
+
1969
+ return { content: cleaned, toolCalls };
1970
+ }
1971
+
1972
+ decodeXmlEntities(value) {
1973
+ return String(value || '')
1974
+ .replace(/&lt;/g, '<')
1975
+ .replace(/&gt;/g, '>')
1976
+ .replace(/&quot;/g, '"')
1977
+ .replace(/&apos;/g, "'")
1978
+ .replace(/&amp;/g, '&');
1979
+ }
1980
+
1927
1981
  parseToolArguments(rawArgs) {
1928
1982
  if (!rawArgs) return {};
1929
1983
  if (typeof rawArgs === 'object') return rawArgs;
@@ -2127,7 +2181,7 @@ ${colors.reset}
2127
2181
  let memoryStr = memories.length > 0 ? `\n## Memories (Important Context)\n${memories.map(m => `- ${m.text}`).join('\n')}` : '';
2128
2182
  let plansStr = plans.length > 0 ? `\n## Active Plans & Tasks\n${plans.map(p => `- [${p.status}] ${p.title}: ${p.description}`).join('\n')}` : '';
2129
2183
 
2130
- return `You are Winter ❄️, an expert AI coding assistant.
2184
+ return `You are Winter, an expert AI coding assistant.
2131
2185
 
2132
2186
  ## CRITICAL AI RULES (MUST FOLLOW STRICTLY):
2133
2187
  1. [THINKING BEFORE CODING]: Always output your thought process briefly before generating code. Think about edge cases, design structure, and syntax correctness.
@@ -2149,7 +2203,7 @@ ${colors.reset}
2149
2203
  - Read, Write, Edit - File operations
2150
2204
  - Write - Create/overwrite files directly. Use this instead of Bash echo/cat/heredoc for writing code.
2151
2205
  - Edit - Replace exact text in existing files.
2152
- - Bash - Execute shell commands. Current OS is ${process.platform === 'win32' ? 'Windows; Bash runs through PowerShell, not Linux bash. Use PowerShell-compatible commands.' : process.platform}.
2206
+ - Bash - Execute shell commands. Current OS is ${process.platform === 'win32' ? 'Windows; Bash auto-detects PowerShell and cmd.exe syntax. Use shell="powershell" or shell="cmd" when needed.' : process.platform}.
2153
2207
  - Glob - Find files
2154
2208
  - Grep - Search content
2155
2209
  - TaskCreate, TaskUpdate, TaskList - Task management
@@ -2160,8 +2214,11 @@ ${colors.reset}
2160
2214
  - Call tools when they help - be proactive
2161
2215
  - You DO have file write tools. Never say "there is no write tool"; use Write or Edit.
2162
2216
  - If a tool name fails, call the canonical tool name next: Write, Edit, Read, Bash, Glob, or Grep.
2163
- - Do not use Linux-only heredoc/echo chains to write files on Windows. Prefer Write with full content.
2217
+ - On Windows, Bash accepts both PowerShell and cmd.exe commands. Prefer Write with full content for file writes.
2164
2218
  - After using tools, always provide a direct final answer to the user.
2219
+ - Never claim that you changed files unless a Write, Edit, Bash, or equivalent tool result shows the change succeeded in this turn.
2220
+ - Never emit XML or provider-specific pseudo tool syntax like <minimax:tool_call>. Use the actual tool-calling API only.
2221
+ - If a file path is unknown, search with Glob/Grep first instead of inventing names like Nav.tsx or Footer.tsx.
2165
2222
  - Answer normal questions directly without unnecessary legal or policy disclaimers.
2166
2223
  - If a request is illegal, unsafe, or harmful, refuse briefly and offer a safe alternative.
2167
2224
  - Read files before modifying
@@ -2246,8 +2303,8 @@ ${rolePrompt}
2246
2303
 
2247
2304
  ## Tool Rules
2248
2305
  - Canonical tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, BrowserDebug, WebFetch, WebSearch.
2249
- - Current OS is ${process.platform === 'win32' ? 'Windows; Bash runs through PowerShell, not Linux bash. Use PowerShell-compatible commands.' : process.platform}.
2250
- - Prefer Write/Edit for writing files. Do not use Linux-only heredoc or long echo chains for code files.
2306
+ - Current OS is ${process.platform === 'win32' ? 'Windows; Bash auto-detects PowerShell and cmd.exe syntax. Use shell="powershell" or shell="cmd" when needed.' : process.platform}.
2307
+ - Prefer Write/Edit for writing files. Bash accepts both PowerShell and cmd.exe on Windows, but do not use long echo chains for code files.
2251
2308
  - If a tool call fails because of an unknown alias, call the canonical tool name next.
2252
2309
 
2253
2310
  ## Project
@@ -215,3 +215,61 @@ test('runConversation reports malformed tool arguments instead of executing empt
215
215
  assert.equal(answer, 'Recovered');
216
216
  assert.deepEqual(executed, []);
217
217
  });
218
+
219
+ test('runConversation executes inline XML tool calls without printing pseudo syntax', async () => {
220
+ const repl = new WinterREPL({ projectPath: process.cwd() });
221
+
222
+ let streamCount = 0;
223
+ const executed = [];
224
+ const writes = [];
225
+ const originalWrite = process.stdout.write;
226
+ repl.ai = {
227
+ tools: [],
228
+ providers: { custom: { model: 'test-model' } },
229
+ getActiveProvider: () => 'custom',
230
+ setTools(tools) {
231
+ this.tools = tools;
232
+ },
233
+ async *streamRequest() {
234
+ streamCount++;
235
+ if (streamCount === 1) {
236
+ yield {
237
+ content: 'Để tôi đọc file:\n<minimax:tool_call><invoke name="Read"><parameter name="path">README.md</parameter></invoke></minimax:tool_call>',
238
+ raw: { choices: [{ delta: { content: 'inline xml' }, finish_reason: 'stop' }] },
239
+ };
240
+ return;
241
+ }
242
+
243
+ yield { content: 'Đã đọc xong' };
244
+ },
245
+ };
246
+ repl.tools = {
247
+ normalizeToolName: name => name,
248
+ async execute(name, args) {
249
+ executed.push({ name, args });
250
+ return { success: true, path: args.path, lines: 1, size: 2, content: 'ok' };
251
+ },
252
+ };
253
+
254
+ process.stdout.write = (chunk) => {
255
+ writes.push(String(chunk));
256
+ return true;
257
+ };
258
+
259
+ try {
260
+ const answer = await repl.runConversation([{ role: 'user', content: 'read it' }], 'Test', [{ name: 'Read' }]);
261
+
262
+ assert.equal(answer, 'Đã đọc xong');
263
+ assert.deepEqual(executed, [{ name: 'Read', args: { path: 'README.md' } }]);
264
+ assert.doesNotMatch(writes.join(''), /minimax:tool_call/);
265
+ } finally {
266
+ process.stdout.write = originalWrite;
267
+ }
268
+ });
269
+
270
+ test('interactive prompt and system prompt do not brand Winter with emoji', () => {
271
+ const repl = new WinterREPL({ projectPath: process.cwd() });
272
+
273
+ assert.match(repl.getSystemPrompt(''), /You are Winter, an expert AI coding assistant/);
274
+ assert.doesNotMatch(repl.getSystemPrompt(''), /You are Winter ❄️/);
275
+ });
@@ -64,13 +64,14 @@ export class ToolExecutor {
64
64
  {
65
65
  type: 'function',
66
66
  name: 'Bash',
67
- description: 'Execute a shell command. On Windows this runs in PowerShell, not Linux bash. Prefer Write/Edit for file writes.',
67
+ description: 'Execute a shell command. On Windows, shell auto-detects PowerShell or cmd.exe syntax. Prefer Write/Edit for file writes.',
68
68
  parameters: {
69
69
  type: 'object',
70
70
  properties: {
71
71
  command: { type: 'string', description: 'Shell command to execute' },
72
72
  cwd: { type: 'string', description: 'Working directory' },
73
- timeout: { type: 'number', description: 'Timeout in ms (default: 60000)' }
73
+ timeout: { type: 'number', description: 'Timeout in ms (default: 60000)' },
74
+ shell: { type: 'string', description: 'Windows shell: auto, powershell, or cmd' }
74
75
  },
75
76
  required: ['command']
76
77
  }
@@ -216,7 +217,7 @@ export class ToolExecutor {
216
217
  newStr
217
218
  );
218
219
  case 'Bash':
219
- return await this.bash(input.command ?? input.cmd, input.cwd || cwd, input.timeout);
220
+ return await this.bash(input.command ?? input.cmd, input.cwd || cwd, input.timeout, input.shell);
220
221
  case 'Glob':
221
222
  return await this.glob(input.pattern ?? input.glob ?? '**/*', input.cwd || input.path || cwd);
222
223
  case 'Grep':
@@ -444,7 +445,7 @@ export class ToolExecutor {
444
445
  }
445
446
  }
446
447
 
447
- async bash(command, cwd, timeout = 60000) {
448
+ async bash(command, cwd, timeout = 60000, shell = 'auto') {
448
449
  if (typeof command !== 'string' || command.trim() === '') {
449
450
  return { success: false, error: 'command is required', exitCode: 1 };
450
451
  }
@@ -455,7 +456,10 @@ export class ToolExecutor {
455
456
  const heredocResult = await this.tryHandleHeredocWrite(command, cwd);
456
457
  if (heredocResult) return heredocResult;
457
458
 
458
- command = await this.translateWindowsCommand(command);
459
+ let requestedShell = this.normalizeWindowsShell(shell);
460
+ const translated = await this.translateWindowsCommand(command);
461
+ command = translated.command;
462
+ if (requestedShell === 'auto' && translated.shell) requestedShell = translated.shell;
459
463
 
460
464
  // Security check
461
465
  const lowerCommand = command.toLowerCase();
@@ -467,12 +471,7 @@ export class ToolExecutor {
467
471
 
468
472
  try {
469
473
  const { stdout, stderr } = process.platform === 'win32'
470
- ? await execFileAsync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command], {
471
- cwd,
472
- timeout,
473
- windowsHide: true,
474
- maxBuffer: 10 * 1024 * 1024,
475
- })
474
+ ? await this.execWindowsCommand(command, cwd, timeout, requestedShell)
476
475
  : await execAsync(command, { cwd, timeout, shell: true, maxBuffer: 10 * 1024 * 1024 });
477
476
  return {
478
477
  success: true,
@@ -491,6 +490,50 @@ export class ToolExecutor {
491
490
  }
492
491
  }
493
492
 
493
+ normalizeWindowsShell(shell) {
494
+ const value = String(shell || 'auto').toLowerCase();
495
+ if (['cmd', 'cmd.exe', 'commandprompt', 'command-prompt'].includes(value)) return 'cmd';
496
+ if (['powershell', 'pwsh', 'ps', 'powershell.exe'].includes(value)) return 'powershell';
497
+ return 'auto';
498
+ }
499
+
500
+ async execWindowsCommand(command, cwd, timeout, requestedShell = 'auto') {
501
+ const shell = requestedShell === 'auto' ? this.detectWindowsShell(command) : requestedShell;
502
+ if (shell === 'cmd') {
503
+ return await execFileAsync('cmd.exe', ['/d', '/s', '/c', command], {
504
+ cwd,
505
+ timeout,
506
+ windowsHide: true,
507
+ maxBuffer: 10 * 1024 * 1024,
508
+ });
509
+ }
510
+
511
+ return await execFileAsync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command], {
512
+ cwd,
513
+ timeout,
514
+ windowsHide: true,
515
+ maxBuffer: 10 * 1024 * 1024,
516
+ });
517
+ }
518
+
519
+ detectWindowsShell(command) {
520
+ const trimmed = String(command || '').trim();
521
+ if (this.looksLikePowerShell(trimmed)) return 'powershell';
522
+ if (this.looksLikeCmd(trimmed)) return 'cmd';
523
+ return 'powershell';
524
+ }
525
+
526
+ looksLikePowerShell(command) {
527
+ return /(^|[;&|]\s*)(Get-|Set-|New-|Remove-|Copy-|Move-|Select-|Where-|ForEach-|Test-Path|Out-File|Write-Host|powershell(?:\.exe)?\b|pwsh\b)/i.test(command);
528
+ }
529
+
530
+ looksLikeCmd(command) {
531
+ return /\s(&&|\|\|)\s/.test(command)
532
+ || /(^|[&]\s*)(dir|copy|xcopy|del|erase|move|ren|type|echo|set|if|for|mkdir|rmdir)\b/i.test(command)
533
+ || /^\s*@?echo\s+/i.test(command)
534
+ || /(^|\s)(\/b|\/s|\/q|\/y)\b/i.test(command);
535
+ }
536
+
494
537
  async glob(pattern, cwd) {
495
538
  const normalizedRequest = await this.normalizeGlobRequest(pattern, cwd);
496
539
  if (normalizedRequest.file) {
@@ -571,7 +614,7 @@ export class ToolExecutor {
571
614
  }
572
615
 
573
616
  async translateWindowsCommand(command) {
574
- if (process.platform !== 'win32') return command;
617
+ if (process.platform !== 'win32') return { command };
575
618
 
576
619
  const trimmed = command.trim();
577
620
  const ls = trimmed.match(/^ls(?:\s+(.+))?$/i);
@@ -584,7 +627,10 @@ export class ToolExecutor {
584
627
  const fields = long
585
628
  ? 'Mode,Length,LastWriteTime,Name'
586
629
  : 'Name';
587
- return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)}${recurse ? ' -Recurse' : ''}${force ? ' -Force' : ''} | Select-Object ${fields}`;
630
+ return {
631
+ command: `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)}${recurse ? ' -Recurse' : ''}${force ? ' -Force' : ''} | Select-Object ${fields}`,
632
+ shell: 'powershell',
633
+ };
588
634
  }
589
635
 
590
636
  const cat = trimmed.match(/^cat\s+(.+)$/i);
@@ -594,13 +640,19 @@ export class ToolExecutor {
594
640
  try {
595
641
  const stat = await fs.stat(normalizedTarget);
596
642
  if (stat.isDirectory()) {
597
- return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Force | Select-Object -ExpandProperty Name`;
643
+ return {
644
+ command: `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Force | Select-Object -ExpandProperty Name`,
645
+ shell: 'powershell',
646
+ };
598
647
  }
599
648
  } catch {}
600
- return `Get-Content -LiteralPath ${this.quotePowerShellString(target)}`;
649
+ return {
650
+ command: `Get-Content -LiteralPath ${this.quotePowerShellString(target)}`,
651
+ shell: 'powershell',
652
+ };
601
653
  }
602
654
 
603
- return command;
655
+ return { command };
604
656
  }
605
657
 
606
658
  async tryHandleHeredocWrite(command, cwd) {
@@ -72,6 +72,46 @@ test('Windows Bash runs PowerShell commands and translates ls flags', async (t)
72
72
  assert.match(ps.stdout, /a\.txt/);
73
73
  });
74
74
 
75
+ test('Windows Bash accepts explicit cmd and PowerShell shells', async (t) => {
76
+ if (process.platform !== 'win32') {
77
+ t.skip('Windows-only shell behavior');
78
+ return;
79
+ }
80
+
81
+ const root = await mkdtemp(path.join(tmpdir(), 'winter-shells-'));
82
+ const tools = new ToolExecutor({ projectPath: root });
83
+
84
+ const cmd = await tools.execute('Bash', {
85
+ shell: 'cmd',
86
+ command: 'echo hello>cmd.txt && type cmd.txt',
87
+ });
88
+ assert.equal(cmd.success, true);
89
+ assert.match(cmd.stdout, /hello/);
90
+
91
+ const ps = await tools.execute('Bash', {
92
+ shell: 'powershell',
93
+ command: "Set-Content -LiteralPath ps.txt -Value 'world'; Get-Content -LiteralPath ps.txt",
94
+ });
95
+ assert.equal(ps.success, true);
96
+ assert.match(ps.stdout, /world/);
97
+ });
98
+
99
+ test('Windows Bash auto-detects cmd chaining syntax', async (t) => {
100
+ if (process.platform !== 'win32') {
101
+ t.skip('Windows-only shell behavior');
102
+ return;
103
+ }
104
+
105
+ const root = await mkdtemp(path.join(tmpdir(), 'winter-cmd-auto-'));
106
+ const tools = new ToolExecutor({ projectPath: root });
107
+ const result = await tools.execute('Bash', {
108
+ command: 'echo auto>auto.txt && type auto.txt',
109
+ });
110
+
111
+ assert.equal(result.success, true);
112
+ assert.match(result.stdout, /auto/);
113
+ });
114
+
75
115
  test('Read lists directories instead of failing on directory paths', async () => {
76
116
  const root = await mkdtemp(path.join(tmpdir(), 'winter-tools-'));
77
117
  await mkdir(path.join(root, 'sub'));