winter-super-cli 2026.5.17 → 2026.5.19
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/cli/repl.js +13 -1
- package/src/tools/executor.js +102 -17
- package/src/tools/executor.test.js +45 -0
package/package.json
CHANGED
package/src/cli/repl.js
CHANGED
|
@@ -2147,7 +2147,9 @@ ${colors.reset}
|
|
|
2147
2147
|
|
|
2148
2148
|
## Tools Available
|
|
2149
2149
|
- Read, Write, Edit - File operations
|
|
2150
|
-
-
|
|
2150
|
+
- Write - Create/overwrite files directly. Use this instead of Bash echo/cat/heredoc for writing code.
|
|
2151
|
+
- 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}.
|
|
2151
2153
|
- Glob - Find files
|
|
2152
2154
|
- Grep - Search content
|
|
2153
2155
|
- TaskCreate, TaskUpdate, TaskList - Task management
|
|
@@ -2156,6 +2158,9 @@ ${colors.reset}
|
|
|
2156
2158
|
|
|
2157
2159
|
## Guidelines
|
|
2158
2160
|
- Call tools when they help - be proactive
|
|
2161
|
+
- You DO have file write tools. Never say "there is no write tool"; use Write or Edit.
|
|
2162
|
+
- 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.
|
|
2159
2164
|
- After using tools, always provide a direct final answer to the user.
|
|
2160
2165
|
- Answer normal questions directly without unnecessary legal or policy disclaimers.
|
|
2161
2166
|
- If a request is illegal, unsafe, or harmful, refuse briefly and offer a safe alternative.
|
|
@@ -2235,9 +2240,16 @@ Nếu người dùng yêu cầu sửa file/chạy lệnh/đọc dự án thì n
|
|
|
2235
2240
|
2. [DESIGN EXCELLENCE]: Use rich aesthetics. Default to modern UI frameworks if applicable. Never output plain, ugly HTML/CSS. Ensure responsive, premium feel with micro-animations.
|
|
2236
2241
|
3. [CODE QUALITY]: Write clean, modular, SOLID code. Check for syntax errors carefully. Do not generate incomplete code blocks.
|
|
2237
2242
|
4. [NO HALLUCINATION]: If you don't know, use tools (Grep/Read/Web) to find out. Do not guess file paths or APIs.
|
|
2243
|
+
5. [TOOL EXECUTION FIRST]: You DO have file tools. Use Write to create/overwrite files and Edit to patch files. Never say there is no write tool.
|
|
2238
2244
|
|
|
2239
2245
|
${rolePrompt}
|
|
2240
2246
|
|
|
2247
|
+
## Tool Rules
|
|
2248
|
+
- 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.
|
|
2251
|
+
- If a tool call fails because of an unknown alias, call the canonical tool name next.
|
|
2252
|
+
|
|
2241
2253
|
## Project
|
|
2242
2254
|
Working directory: ${this.projectPath}
|
|
2243
2255
|
Current session: ${this.session.getSessionId().substring(0, 8)}
|
package/src/tools/executor.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { promises as fs } from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { exec } from 'child_process';
|
|
8
|
+
import { exec, execFile } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import { diffLines } from 'diff';
|
|
11
11
|
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
13
14
|
|
|
14
15
|
export class ToolExecutor {
|
|
15
16
|
constructor(repl) {
|
|
@@ -36,7 +37,7 @@ export class ToolExecutor {
|
|
|
36
37
|
{
|
|
37
38
|
type: 'function',
|
|
38
39
|
name: 'Write',
|
|
39
|
-
description: 'Create or overwrite file with content.',
|
|
40
|
+
description: 'Create or overwrite a file with content. Prefer this for file creation instead of Bash echo/cat/heredoc. Also handles model aliases like write_to_file.',
|
|
40
41
|
parameters: {
|
|
41
42
|
type: 'object',
|
|
42
43
|
properties: {
|
|
@@ -49,7 +50,7 @@ export class ToolExecutor {
|
|
|
49
50
|
{
|
|
50
51
|
type: 'function',
|
|
51
52
|
name: 'Edit',
|
|
52
|
-
description: 'Make surgical changes. Replace exact old_string with new_string.',
|
|
53
|
+
description: 'Make surgical changes. Replace exact old_string with new_string. Also handles model aliases like replace_in_file and str_replace_editor.',
|
|
53
54
|
parameters: {
|
|
54
55
|
type: 'object',
|
|
55
56
|
properties: {
|
|
@@ -63,7 +64,7 @@ export class ToolExecutor {
|
|
|
63
64
|
{
|
|
64
65
|
type: 'function',
|
|
65
66
|
name: 'Bash',
|
|
66
|
-
description: 'Execute shell command.
|
|
67
|
+
description: 'Execute a shell command. On Windows this runs in PowerShell, not Linux bash. Prefer Write/Edit for file writes.',
|
|
67
68
|
parameters: {
|
|
68
69
|
type: 'object',
|
|
69
70
|
properties: {
|
|
@@ -235,7 +236,12 @@ export class ToolExecutor {
|
|
|
235
236
|
case 'WebSearch':
|
|
236
237
|
return await this.webSearch(input.query ?? input.q);
|
|
237
238
|
default:
|
|
238
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: `Unknown tool: ${toolName}`,
|
|
242
|
+
availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'BrowserDebug', 'WebFetch', 'WebSearch'],
|
|
243
|
+
recovery: 'Call one of the available tools. For file writes use Write with { "file_path": "...", "content": "..." }. For shell commands use Bash with { "command": "..." }.',
|
|
244
|
+
};
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -262,6 +268,7 @@ export class ToolExecutor {
|
|
|
262
268
|
bash: 'Bash',
|
|
263
269
|
shell: 'Bash',
|
|
264
270
|
command: 'Bash',
|
|
271
|
+
commandexecutor: 'Bash',
|
|
265
272
|
executecommand: 'Bash',
|
|
266
273
|
runcommand: 'Bash',
|
|
267
274
|
terminal: 'Bash',
|
|
@@ -445,6 +452,9 @@ export class ToolExecutor {
|
|
|
445
452
|
timeout = parseInt(timeout, 10);
|
|
446
453
|
if (isNaN(timeout) || timeout < 0) timeout = 60000;
|
|
447
454
|
|
|
455
|
+
const heredocResult = await this.tryHandleHeredocWrite(command, cwd);
|
|
456
|
+
if (heredocResult) return heredocResult;
|
|
457
|
+
|
|
448
458
|
command = await this.translateWindowsCommand(command);
|
|
449
459
|
|
|
450
460
|
// Security check
|
|
@@ -456,7 +466,14 @@ export class ToolExecutor {
|
|
|
456
466
|
}
|
|
457
467
|
|
|
458
468
|
try {
|
|
459
|
-
const { stdout, stderr } =
|
|
469
|
+
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
|
+
})
|
|
476
|
+
: await execAsync(command, { cwd, timeout, shell: true, maxBuffer: 10 * 1024 * 1024 });
|
|
460
477
|
return {
|
|
461
478
|
success: true,
|
|
462
479
|
stdout: stdout || '',
|
|
@@ -557,16 +574,17 @@ export class ToolExecutor {
|
|
|
557
574
|
if (process.platform !== 'win32') return command;
|
|
558
575
|
|
|
559
576
|
const trimmed = command.trim();
|
|
560
|
-
const
|
|
561
|
-
if (lsRecursive) {
|
|
562
|
-
const target = lsRecursive[1].trim();
|
|
563
|
-
return `powershell -NoProfile -Command "Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Recurse -Force | Select-Object -ExpandProperty FullName"`;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const ls = trimmed.match(/^ls\s+(.+)$/i);
|
|
577
|
+
const ls = trimmed.match(/^ls(?:\s+(.+))?$/i);
|
|
567
578
|
if (ls) {
|
|
568
|
-
const
|
|
569
|
-
|
|
579
|
+
const parsed = this.parseLsArgs(ls[1] || '');
|
|
580
|
+
const target = parsed.target || '.';
|
|
581
|
+
const recurse = parsed.flags.has('r') || parsed.flags.has('recursive');
|
|
582
|
+
const long = parsed.flags.has('l') || parsed.flags.has('la') || parsed.flags.has('al');
|
|
583
|
+
const force = parsed.flags.has('a') || parsed.flags.has('force') || parsed.flags.has('la') || parsed.flags.has('al');
|
|
584
|
+
const fields = long
|
|
585
|
+
? 'Mode,Length,LastWriteTime,Name'
|
|
586
|
+
: 'Name';
|
|
587
|
+
return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)}${recurse ? ' -Recurse' : ''}${force ? ' -Force' : ''} | Select-Object ${fields}`;
|
|
570
588
|
}
|
|
571
589
|
|
|
572
590
|
const cat = trimmed.match(/^cat\s+(.+)$/i);
|
|
@@ -576,15 +594,82 @@ export class ToolExecutor {
|
|
|
576
594
|
try {
|
|
577
595
|
const stat = await fs.stat(normalizedTarget);
|
|
578
596
|
if (stat.isDirectory()) {
|
|
579
|
-
return `
|
|
597
|
+
return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Force | Select-Object -ExpandProperty Name`;
|
|
580
598
|
}
|
|
581
599
|
} catch {}
|
|
582
|
-
return `
|
|
600
|
+
return `Get-Content -LiteralPath ${this.quotePowerShellString(target)}`;
|
|
583
601
|
}
|
|
584
602
|
|
|
585
603
|
return command;
|
|
586
604
|
}
|
|
587
605
|
|
|
606
|
+
async tryHandleHeredocWrite(command, cwd) {
|
|
607
|
+
const match = String(command || '').match(/^cat\s*>\s*(.+?)\s*<<\s*['"]?([A-Za-z0-9_-]+)['"]?\r?\n([\s\S]*?)\r?\n\2\s*$/);
|
|
608
|
+
if (!match) return null;
|
|
609
|
+
|
|
610
|
+
const [, rawTarget, , content] = match;
|
|
611
|
+
const target = this.resolveInputPath(rawTarget.trim(), cwd);
|
|
612
|
+
return await this.writeFile(target, content.endsWith('\n') ? content : `${content}\n`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
parseLsArgs(rawArgs) {
|
|
616
|
+
const tokens = this.splitShellLike(rawArgs);
|
|
617
|
+
const flags = new Set();
|
|
618
|
+
const paths = [];
|
|
619
|
+
|
|
620
|
+
for (const token of tokens) {
|
|
621
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
622
|
+
const flag = token.replace(/^-+/, '').toLowerCase();
|
|
623
|
+
flags.add(flag);
|
|
624
|
+
if (!flag.includes('=')) {
|
|
625
|
+
for (const ch of flag) flags.add(ch);
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
paths.push(token);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return { flags, target: paths.join(' ') };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
splitShellLike(text) {
|
|
636
|
+
const tokens = [];
|
|
637
|
+
let current = '';
|
|
638
|
+
let quote = null;
|
|
639
|
+
let escaped = false;
|
|
640
|
+
|
|
641
|
+
for (const ch of String(text || '')) {
|
|
642
|
+
if (escaped) {
|
|
643
|
+
current += ch;
|
|
644
|
+
escaped = false;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (ch === '\\') {
|
|
648
|
+
escaped = true;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if ((ch === '"' || ch === "'") && !quote) {
|
|
652
|
+
quote = ch;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (ch === quote) {
|
|
656
|
+
quote = null;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (/\s/.test(ch) && !quote) {
|
|
660
|
+
if (current) {
|
|
661
|
+
tokens.push(current);
|
|
662
|
+
current = '';
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
current += ch;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (current) tokens.push(current);
|
|
670
|
+
return tokens;
|
|
671
|
+
}
|
|
672
|
+
|
|
588
673
|
quotePowerShellString(value) {
|
|
589
674
|
const unquoted = value.replace(/^['"]|['"]$/g, '');
|
|
590
675
|
return `'${unquoted.replace(/'/g, "''")}'`;
|
|
@@ -14,12 +14,23 @@ test('Bash validates missing command instead of throwing', async () => {
|
|
|
14
14
|
assert.equal(result.error, 'command is required');
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
test('unknown tools return recovery guidance instead of a bare failure', async () => {
|
|
18
|
+
const tools = new ToolExecutor({ projectPath: process.cwd() });
|
|
19
|
+
const result = await tools.execute('bad_tool_name', {});
|
|
20
|
+
|
|
21
|
+
assert.equal(result.success, false);
|
|
22
|
+
assert.equal(result.error, 'Unknown tool: bad_tool_name');
|
|
23
|
+
assert(result.availableTools.includes('Write'));
|
|
24
|
+
assert.match(result.recovery, /Write/);
|
|
25
|
+
});
|
|
26
|
+
|
|
17
27
|
test('tool names accept common model aliases', () => {
|
|
18
28
|
const tools = new ToolExecutor({ projectPath: process.cwd() });
|
|
19
29
|
|
|
20
30
|
assert.equal(tools.normalizeToolName('read_file'), 'Read');
|
|
21
31
|
assert.equal(tools.normalizeToolName('write_to_file'), 'Write');
|
|
22
32
|
assert.equal(tools.normalizeToolName('replace_in_file'), 'Edit');
|
|
33
|
+
assert.equal(tools.normalizeToolName('command_executor'), 'Bash');
|
|
23
34
|
assert.equal(tools.normalizeToolName('execute_command'), 'Bash');
|
|
24
35
|
assert.equal(tools.normalizeToolName('list_files'), 'Glob');
|
|
25
36
|
assert.equal(tools.normalizeToolName('search_files'), 'Grep');
|
|
@@ -27,6 +38,40 @@ test('tool names accept common model aliases', () => {
|
|
|
27
38
|
assert.equal(tools.normalizeToolName('web-search'), 'WebSearch');
|
|
28
39
|
});
|
|
29
40
|
|
|
41
|
+
test('Bash supports model-style heredoc file writes', async () => {
|
|
42
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-heredoc-'));
|
|
43
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
44
|
+
const result = await tools.execute('command_executor', {
|
|
45
|
+
command: "cat > src/components/InstallSection.tsx << 'EOF'\nconst ok = true;\nEOF",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.equal(result.success, true);
|
|
49
|
+
assert.equal(result.path, path.join(root, 'src', 'components', 'InstallSection.tsx'));
|
|
50
|
+
|
|
51
|
+
const read = await tools.execute('Read', { file_path: 'src/components/InstallSection.tsx' });
|
|
52
|
+
assert.equal(read.content, 'const ok = true;\n');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('Windows Bash runs PowerShell commands and translates ls flags', async (t) => {
|
|
56
|
+
if (process.platform !== 'win32') {
|
|
57
|
+
t.skip('Windows-only shell behavior');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-powershell-'));
|
|
62
|
+
await mkdir(path.join(root, 'src', 'components'), { recursive: true });
|
|
63
|
+
await writeFile(path.join(root, 'src', 'components', 'a.txt'), 'hello');
|
|
64
|
+
|
|
65
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
66
|
+
const ls = await tools.execute('Bash', { command: 'ls -la src/components/' });
|
|
67
|
+
assert.equal(ls.success, true);
|
|
68
|
+
assert.match(ls.stdout, /a\.txt/);
|
|
69
|
+
|
|
70
|
+
const ps = await tools.execute('Bash', { command: 'Get-ChildItem -Path src/components/ -Name' });
|
|
71
|
+
assert.equal(ps.success, true);
|
|
72
|
+
assert.match(ps.stdout, /a\.txt/);
|
|
73
|
+
});
|
|
74
|
+
|
|
30
75
|
test('Read lists directories instead of failing on directory paths', async () => {
|
|
31
76
|
const root = await mkdtemp(path.join(tmpdir(), 'winter-tools-'));
|
|
32
77
|
await mkdir(path.join(root, 'sub'));
|