winter-super-cli 2026.5.20 → 2026.5.22
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/src/ai/providers.js +1 -1
- package/src/cli/commands.js +2 -2
- package/src/cli/repl.js +71 -14
- package/src/cli/repl.test.js +58 -0
- package/src/tools/executor.js +75 -8
- package/src/tools/executor.test.js +49 -0
package/package.json
CHANGED
package/src/ai/providers.js
CHANGED
|
@@ -397,7 +397,7 @@ export class AIProviderManager {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
getSystemPrompt() {
|
|
400
|
-
return `You are Winter
|
|
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
|
package/src/cli/commands.js
CHANGED
|
@@ -280,7 +280,7 @@ export class CommandParser {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
getWinterSystemPrompt() {
|
|
283
|
-
return `You are Winter
|
|
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
|
|
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
|
-
|
|
1519
|
-
|
|
1519
|
+
if (!bufferToolModeContent) {
|
|
1520
|
+
process.stdout.write(`\n${colors.white}`);
|
|
1521
|
+
printed = true;
|
|
1522
|
+
}
|
|
1520
1523
|
}
|
|
1521
1524
|
content += chunk.content;
|
|
1522
|
-
|
|
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
|
|
1530
|
-
|
|
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 &&
|
|
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:
|
|
1560
|
+
finalContent: visibleContent,
|
|
1543
1561
|
finishReason,
|
|
1544
1562
|
};
|
|
1545
|
-
} else if (
|
|
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(/</g, '<')
|
|
1975
|
+
.replace(/>/g, '>')
|
|
1976
|
+
.replace(/"/g, '"')
|
|
1977
|
+
.replace(/'/g, "'")
|
|
1978
|
+
.replace(/&/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
|
|
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.
|
|
@@ -2162,6 +2216,9 @@ ${colors.reset}
|
|
|
2162
2216
|
- If a tool name fails, call the canonical tool name next: Write, Edit, Read, Bash, Glob, or Grep.
|
|
2163
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
|
package/src/cli/repl.test.js
CHANGED
|
@@ -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
|
+
});
|
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 });
|