winter-super-cli 2026.5.21 → 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/tools/executor.js +75 -8
- package/src/tools/executor.test.js +49 -0
package/package.json
CHANGED
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 });
|