codemini-cli 0.1.18 → 0.1.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/README.md +6 -6
- package/package.json +1 -1
- package/skills/superpowers-lite/SKILL.md +10 -9
- package/src/core/agent-loop.js +56 -29
- package/src/core/chat-runtime.js +79 -16
- package/src/core/command-policy.js +4 -4
- package/src/core/config-store.js +16 -29
- package/src/core/shell-profile.js +1 -1
- package/src/core/tools.js +514 -213
- package/src/tui/chat-app.js +281 -124
package/src/core/tools.js
CHANGED
|
@@ -115,6 +115,67 @@ function splitLines(text) {
|
|
|
115
115
|
return String(text || '').split('\n');
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function findUniqueLineBlock(lines, blockContent) {
|
|
119
|
+
const probeLines = splitLines(blockContent);
|
|
120
|
+
if (probeLines.length === 0 || (probeLines.length === 1 && probeLines[0] === '')) return null;
|
|
121
|
+
const matches = [];
|
|
122
|
+
const lastStart = lines.length - probeLines.length;
|
|
123
|
+
for (let start = 0; start <= lastStart; start += 1) {
|
|
124
|
+
let ok = true;
|
|
125
|
+
for (let offset = 0; offset < probeLines.length; offset += 1) {
|
|
126
|
+
if (lines[start + offset] !== probeLines[offset]) {
|
|
127
|
+
ok = false;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (ok) {
|
|
132
|
+
matches.push({
|
|
133
|
+
start_line: start + 1,
|
|
134
|
+
end_line: start + probeLines.length,
|
|
135
|
+
content: probeLines.join('\n')
|
|
136
|
+
});
|
|
137
|
+
if (matches.length > 1) break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return matches.length === 1 ? matches[0] : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveReplaceBlockTarget(state, target) {
|
|
144
|
+
const startLine = Number(target?.start_line);
|
|
145
|
+
const endLine = Number(target?.end_line);
|
|
146
|
+
const oldHash = String(target?.old_hash || '');
|
|
147
|
+
const currentBlock =
|
|
148
|
+
Number.isFinite(startLine) && Number.isFinite(endLine) && startLine > 0 && endLine >= startLine
|
|
149
|
+
? state.lines.slice(startLine - 1, endLine).join('\n')
|
|
150
|
+
: '';
|
|
151
|
+
|
|
152
|
+
if (oldHash && currentBlock && oldHash === sha256(currentBlock)) {
|
|
153
|
+
return {
|
|
154
|
+
start_line: startLine,
|
|
155
|
+
end_line: endLine,
|
|
156
|
+
old_hash: oldHash,
|
|
157
|
+
old_content: currentBlock,
|
|
158
|
+
relocated: false
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const oldContent = String(target?.old_content || '');
|
|
163
|
+
if (oldContent) {
|
|
164
|
+
const relocated = findUniqueLineBlock(state.lines, oldContent);
|
|
165
|
+
if (relocated) {
|
|
166
|
+
return {
|
|
167
|
+
start_line: relocated.start_line,
|
|
168
|
+
end_line: relocated.end_line,
|
|
169
|
+
old_hash: sha256(relocated.content),
|
|
170
|
+
old_content: relocated.content,
|
|
171
|
+
relocated: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
118
179
|
function detectTextFile(filePath) {
|
|
119
180
|
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
120
181
|
}
|
|
@@ -156,6 +217,63 @@ async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
|
156
217
|
return out;
|
|
157
218
|
}
|
|
158
219
|
|
|
220
|
+
async function walkWorkspaceEntries(root, startPath = '.', { includeHidden = false } = {}) {
|
|
221
|
+
const abs = resolveInWorkspace(root, startPath);
|
|
222
|
+
const out = [];
|
|
223
|
+
|
|
224
|
+
async function visit(current) {
|
|
225
|
+
const stat = await fs.stat(current);
|
|
226
|
+
const relative = toWorkspaceRelative(root, current) || '.';
|
|
227
|
+
const name = path.basename(current);
|
|
228
|
+
|
|
229
|
+
if (!includeHidden && name.startsWith('.') && relative !== '.') return;
|
|
230
|
+
if (stat.isDirectory()) {
|
|
231
|
+
if (SKIP_DIRS.has(name) && relative !== '.') return;
|
|
232
|
+
out.push({ path: relative, name, type: 'dir' });
|
|
233
|
+
const entries = await fs.readdir(current);
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
await visit(path.join(current, entry));
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
out.push({ path: relative, name, type: 'file' });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await visit(abs);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function globToRegex(pattern) {
|
|
248
|
+
const normalized = String(pattern || '').replace(/\\/g, '/').trim();
|
|
249
|
+
let regexBody = '';
|
|
250
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
251
|
+
const ch = normalized[i];
|
|
252
|
+
const next = normalized[i + 1];
|
|
253
|
+
const afterNext = normalized[i + 2];
|
|
254
|
+
if (ch === '*' && next === '*' && afterNext === '/') {
|
|
255
|
+
regexBody += '(?:.*/)?';
|
|
256
|
+
i += 2;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (ch === '*' && next === '*') {
|
|
260
|
+
regexBody += '.*';
|
|
261
|
+
i += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (ch === '*') {
|
|
265
|
+
regexBody += '[^/]*';
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (ch === '?') {
|
|
269
|
+
regexBody += '[^/]';
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
regexBody += /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
|
|
273
|
+
}
|
|
274
|
+
return new RegExp(`^${regexBody}$`);
|
|
275
|
+
}
|
|
276
|
+
|
|
159
277
|
function getLineColumnForMatch(line, query, caseSensitive = false) {
|
|
160
278
|
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
161
279
|
const needle = caseSensitive ? query : query.toLowerCase();
|
|
@@ -407,6 +525,129 @@ function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
|
|
|
407
525
|
return body.join('\n');
|
|
408
526
|
}
|
|
409
527
|
|
|
528
|
+
function parseUnifiedPatch(patchText) {
|
|
529
|
+
const lines = splitLines(String(patchText || ''));
|
|
530
|
+
const files = [];
|
|
531
|
+
let current = null;
|
|
532
|
+
|
|
533
|
+
const pushCurrent = () => {
|
|
534
|
+
if (current) files.push(current);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
538
|
+
const line = lines[i];
|
|
539
|
+
if (line.startsWith('--- ')) {
|
|
540
|
+
pushCurrent();
|
|
541
|
+
current = {
|
|
542
|
+
oldPath: line.slice(4).trim(),
|
|
543
|
+
newPath: '',
|
|
544
|
+
hunks: []
|
|
545
|
+
};
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (!current) continue;
|
|
549
|
+
if (line.startsWith('+++ ')) {
|
|
550
|
+
current.newPath = line.slice(4).trim();
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (line.startsWith('@@ ')) {
|
|
554
|
+
const match = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
555
|
+
if (!match) {
|
|
556
|
+
throw new Error(`invalid patch hunk header: ${line}`);
|
|
557
|
+
}
|
|
558
|
+
const hunk = {
|
|
559
|
+
oldStart: Number(match[1]),
|
|
560
|
+
oldCount: Number(match[2] || '1'),
|
|
561
|
+
newStart: Number(match[3]),
|
|
562
|
+
newCount: Number(match[4] || '1'),
|
|
563
|
+
lines: []
|
|
564
|
+
};
|
|
565
|
+
i += 1;
|
|
566
|
+
while (i < lines.length) {
|
|
567
|
+
const hunkLine = lines[i];
|
|
568
|
+
if (hunkLine.startsWith('@@ ') || hunkLine.startsWith('--- ')) {
|
|
569
|
+
i -= 1;
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
if (hunkLine.startsWith('\')) {
|
|
573
|
+
i += 1;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (hunkLine === '') {
|
|
577
|
+
hunk.lines.push(' ');
|
|
578
|
+
i += 1;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (!/^[ +\-]/.test(hunkLine)) {
|
|
582
|
+
hunk.lines.push(` ${hunkLine}`);
|
|
583
|
+
i += 1;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (!/^[ +\-]/.test(hunkLine)) {
|
|
587
|
+
throw new Error(`invalid patch line: ${hunkLine}`);
|
|
588
|
+
}
|
|
589
|
+
hunk.lines.push(hunkLine);
|
|
590
|
+
i += 1;
|
|
591
|
+
}
|
|
592
|
+
current.hunks.push(hunk);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
pushCurrent();
|
|
597
|
+
return files.filter((file) => file.oldPath || file.newPath);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function applyHunkToLines(lines, hunk) {
|
|
601
|
+
const oldChunk = [];
|
|
602
|
+
const newChunk = [];
|
|
603
|
+
for (const line of hunk.lines) {
|
|
604
|
+
if (line.startsWith(' ')) {
|
|
605
|
+
const text = line.slice(1);
|
|
606
|
+
oldChunk.push(text);
|
|
607
|
+
newChunk.push(text);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (line.startsWith('-')) {
|
|
611
|
+
oldChunk.push(line.slice(1));
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (line.startsWith('+')) {
|
|
615
|
+
newChunk.push(line.slice(1));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (oldChunk.length === 0) {
|
|
620
|
+
const insertAt = Math.max(0, Number(hunk.oldStart || 1) - 1);
|
|
621
|
+
return [...lines.slice(0, insertAt), ...newChunk, ...lines.slice(insertAt)];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const lastStart = Math.max(0, lines.length - oldChunk.length);
|
|
625
|
+
const matches = [];
|
|
626
|
+
for (let start = 0; start <= lastStart; start += 1) {
|
|
627
|
+
let ok = true;
|
|
628
|
+
for (let offset = 0; offset < oldChunk.length; offset += 1) {
|
|
629
|
+
if (lines[start + offset] !== oldChunk[offset]) {
|
|
630
|
+
ok = false;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (ok) {
|
|
635
|
+
matches.push(start);
|
|
636
|
+
if (matches.length > 1) break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (matches.length === 0) {
|
|
641
|
+
throw new Error('patch hunk context not found');
|
|
642
|
+
}
|
|
643
|
+
if (matches.length > 1) {
|
|
644
|
+
throw new Error('patch hunk context not unique');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const start = matches[0];
|
|
648
|
+
return [...lines.slice(0, start), ...newChunk, ...lines.slice(start + oldChunk.length)];
|
|
649
|
+
}
|
|
650
|
+
|
|
410
651
|
async function getFileState(root, relativePath) {
|
|
411
652
|
const target = resolveInWorkspace(root, relativePath);
|
|
412
653
|
const stat = await fs.stat(target);
|
|
@@ -452,7 +693,7 @@ async function readFile(root, args) {
|
|
|
452
693
|
suggested_start_line: startLine,
|
|
453
694
|
suggested_end_line: endLine,
|
|
454
695
|
read_token: readToken,
|
|
455
|
-
next: 'Call
|
|
696
|
+
next: 'Call read again with include_content=true and this read_token'
|
|
456
697
|
};
|
|
457
698
|
}
|
|
458
699
|
|
|
@@ -492,16 +733,16 @@ async function readFile(root, args) {
|
|
|
492
733
|
async function writeFile(root, args) {
|
|
493
734
|
const rawPath = String(args?.path || '').trim();
|
|
494
735
|
if (!rawPath) {
|
|
495
|
-
throw new Error('
|
|
736
|
+
throw new Error('write requires a file path like weather/WeatherForecast.js');
|
|
496
737
|
}
|
|
497
738
|
if (rawPath === '.' || rawPath === './') {
|
|
498
|
-
throw new Error('
|
|
739
|
+
throw new Error('write requires a file path, not the workspace root');
|
|
499
740
|
}
|
|
500
741
|
const target = resolveInWorkspace(root, rawPath);
|
|
501
742
|
try {
|
|
502
743
|
const stat = await fs.stat(target);
|
|
503
744
|
if (stat.isDirectory()) {
|
|
504
|
-
throw new Error(`
|
|
745
|
+
throw new Error(`write target is a directory: ${rawPath}`);
|
|
505
746
|
}
|
|
506
747
|
} catch (error) {
|
|
507
748
|
if (error?.code && error.code !== 'ENOENT') throw error;
|
|
@@ -515,7 +756,7 @@ async function writeFile(root, args) {
|
|
|
515
756
|
}
|
|
516
757
|
if (existed && !args?.append && !args?.full_file_rewrite && isCodeLikePath(rawPath)) {
|
|
517
758
|
throw new Error(
|
|
518
|
-
'
|
|
759
|
+
'write blocks full overwrite for existing code files by default. Use grep/read -> edit for minimal edits, or pass full_file_rewrite=true when a whole-file rewrite is truly intended.'
|
|
519
760
|
);
|
|
520
761
|
}
|
|
521
762
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
@@ -549,10 +790,10 @@ async function writeFile(root, args) {
|
|
|
549
790
|
async function runCommand(root, config, args) {
|
|
550
791
|
const command = args?.command || '';
|
|
551
792
|
if (!command.trim()) {
|
|
552
|
-
throw new Error('
|
|
793
|
+
throw new Error('run requires command');
|
|
553
794
|
}
|
|
554
795
|
if (isLikelyLongRunningCommand(command)) {
|
|
555
|
-
throw new Error('Command looks like a long-running service. Use start_service instead of
|
|
796
|
+
throw new Error('Command looks like a long-running service. Use start_service instead of run.');
|
|
556
797
|
}
|
|
557
798
|
if (
|
|
558
799
|
!config.policy.allow_dangerous_commands &&
|
|
@@ -933,6 +1174,81 @@ async function searchCode(root, args) {
|
|
|
933
1174
|
};
|
|
934
1175
|
}
|
|
935
1176
|
|
|
1177
|
+
async function grep(root, args) {
|
|
1178
|
+
const pattern = String(args?.pattern || args?.query || '').trim();
|
|
1179
|
+
if (!pattern) throw new Error('grep requires pattern');
|
|
1180
|
+
const maxResults = Math.max(1, Math.min(200, Number(args?.max_results || 50)));
|
|
1181
|
+
const caseSensitive = Boolean(args?.case_sensitive);
|
|
1182
|
+
const files = await walkTextFiles(root, args?.path || '.', normalizeFileTypes(args));
|
|
1183
|
+
const regex = args?.regex
|
|
1184
|
+
? new RegExp(pattern, caseSensitive ? 'g' : 'gi')
|
|
1185
|
+
: new RegExp(escapeRegex(pattern), caseSensitive ? 'g' : 'gi');
|
|
1186
|
+
const matches = [];
|
|
1187
|
+
|
|
1188
|
+
for (const filePath of files) {
|
|
1189
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1190
|
+
const lines = splitLines(content);
|
|
1191
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
1192
|
+
const line = String(lines[idx] || '');
|
|
1193
|
+
regex.lastIndex = 0;
|
|
1194
|
+
const found = regex.exec(line);
|
|
1195
|
+
if (!found) continue;
|
|
1196
|
+
matches.push({
|
|
1197
|
+
path: toWorkspaceRelative(root, filePath),
|
|
1198
|
+
line: idx + 1,
|
|
1199
|
+
column: Math.max(1, Number(found.index || 0) + 1),
|
|
1200
|
+
preview: trimLinePreview(line)
|
|
1201
|
+
});
|
|
1202
|
+
if (matches.length >= maxResults) {
|
|
1203
|
+
return { pattern, matches, truncated: true };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return { pattern, matches, truncated: false };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function glob(root, args) {
|
|
1212
|
+
const pattern = String(args?.pattern || '').trim();
|
|
1213
|
+
if (!pattern) throw new Error('glob requires pattern');
|
|
1214
|
+
const maxResults = Math.max(1, Math.min(500, Number(args?.max_results || 200)));
|
|
1215
|
+
const regex = globToRegex(pattern);
|
|
1216
|
+
const entries = await walkWorkspaceEntries(root, args?.path || '.', {
|
|
1217
|
+
includeHidden: Boolean(args?.include_hidden)
|
|
1218
|
+
});
|
|
1219
|
+
const matches = entries
|
|
1220
|
+
.filter((entry) => entry.type === 'file' && regex.test(entry.path))
|
|
1221
|
+
.slice(0, maxResults)
|
|
1222
|
+
.map((entry) => entry.path);
|
|
1223
|
+
return {
|
|
1224
|
+
pattern,
|
|
1225
|
+
matches,
|
|
1226
|
+
truncated: entries.filter((entry) => entry.type === 'file' && regex.test(entry.path)).length > matches.length
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async function list(root, args) {
|
|
1231
|
+
const relativePath = String(args?.path || '.').trim() || '.';
|
|
1232
|
+
const target = resolveInWorkspace(root, relativePath);
|
|
1233
|
+
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
1234
|
+
const includeHidden = Boolean(args?.include_hidden);
|
|
1235
|
+
const items = entries
|
|
1236
|
+
.filter((entry) => includeHidden || !entry.name.startsWith('.'))
|
|
1237
|
+
.map((entry) => ({
|
|
1238
|
+
name: entry.name,
|
|
1239
|
+
path: path.posix.join(relativePath === '.' ? '' : relativePath.replace(/\\/g, '/'), entry.name) || entry.name,
|
|
1240
|
+
type: entry.isDirectory() ? 'dir' : 'file'
|
|
1241
|
+
}))
|
|
1242
|
+
.sort((left, right) => {
|
|
1243
|
+
if (left.type !== right.type) return left.type === 'dir' ? -1 : 1;
|
|
1244
|
+
return left.path.localeCompare(right.path);
|
|
1245
|
+
});
|
|
1246
|
+
return {
|
|
1247
|
+
path: relativePath,
|
|
1248
|
+
items
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
936
1252
|
async function readBlock(root, args) {
|
|
937
1253
|
const relativePath = String(args?.path || '').trim();
|
|
938
1254
|
if (!relativePath) throw new Error('read_block requires path');
|
|
@@ -985,17 +1301,25 @@ async function validateEdit(root, args) {
|
|
|
985
1301
|
if (!Number.isFinite(startLine) || !Number.isFinite(endLine) || startLine <= 0 || endLine < startLine) {
|
|
986
1302
|
throw new Error('replace_block validation requires target.start_line and target.end_line');
|
|
987
1303
|
}
|
|
988
|
-
const
|
|
1304
|
+
const resolved = resolveReplaceBlockTarget({ content, lines }, {
|
|
1305
|
+
start_line: startLine,
|
|
1306
|
+
end_line: endLine,
|
|
1307
|
+
old_hash: args?.target?.old_hash,
|
|
1308
|
+
old_content: args?.target?.old_content
|
|
1309
|
+
});
|
|
1310
|
+
const oldBlock = resolved?.old_content || lines.slice(startLine - 1, endLine).join('\n');
|
|
989
1311
|
return {
|
|
990
1312
|
ok: true,
|
|
991
1313
|
path: relativePath,
|
|
992
1314
|
kind,
|
|
993
1315
|
target: {
|
|
994
|
-
start_line: startLine,
|
|
995
|
-
end_line: endLine,
|
|
996
|
-
old_hash: sha256(oldBlock)
|
|
1316
|
+
start_line: resolved?.start_line || startLine,
|
|
1317
|
+
end_line: resolved?.end_line || endLine,
|
|
1318
|
+
old_hash: sha256(oldBlock),
|
|
1319
|
+
old_content: oldBlock
|
|
997
1320
|
},
|
|
998
|
-
file_hash: sha256(content)
|
|
1321
|
+
file_hash: sha256(content),
|
|
1322
|
+
relocated: Boolean(resolved?.relocated)
|
|
999
1323
|
};
|
|
1000
1324
|
}
|
|
1001
1325
|
|
|
@@ -1035,18 +1359,19 @@ async function replaceBlock(root, args) {
|
|
|
1035
1359
|
const relativePath = String(args?.path || '').trim();
|
|
1036
1360
|
const newContent = String(args?.new_content || args?.content || '');
|
|
1037
1361
|
const target = args?.target || {};
|
|
1038
|
-
const startLine = Number(target.start_line);
|
|
1039
|
-
const endLine = Number(target.end_line);
|
|
1040
|
-
const oldHash = String(target.old_hash || '');
|
|
1041
1362
|
const state = await getFileState(root, relativePath);
|
|
1042
|
-
const
|
|
1043
|
-
if (!
|
|
1044
|
-
throw new Error('replace_block old_hash mismatch');
|
|
1363
|
+
const resolved = resolveReplaceBlockTarget(state, target);
|
|
1364
|
+
if (!resolved) {
|
|
1365
|
+
throw new Error('replace_block old_hash mismatch; retry through edit with a symbol or line hint');
|
|
1045
1366
|
}
|
|
1046
|
-
const nextLines = [
|
|
1367
|
+
const nextLines = [
|
|
1368
|
+
...state.lines.slice(0, resolved.start_line - 1),
|
|
1369
|
+
...splitLines(newContent),
|
|
1370
|
+
...state.lines.slice(resolved.end_line)
|
|
1371
|
+
];
|
|
1047
1372
|
const afterContent = nextLines.join('\n');
|
|
1048
1373
|
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1049
|
-
return editResult(relativePath, 'replace_block', state.content, afterContent,
|
|
1374
|
+
return editResult(relativePath, 'replace_block', state.content, afterContent, resolved.start_line);
|
|
1050
1375
|
}
|
|
1051
1376
|
|
|
1052
1377
|
async function replaceText(root, args) {
|
|
@@ -1056,7 +1381,11 @@ async function replaceText(root, args) {
|
|
|
1056
1381
|
const state = await getFileState(root, relativePath);
|
|
1057
1382
|
const occurrences = state.content.split(oldText).length - 1;
|
|
1058
1383
|
if (occurrences !== 1) {
|
|
1059
|
-
throw new Error(
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
occurrences === 0
|
|
1386
|
+
? 'replace_text old_text not found; use edit with a symbol or line hint for block edits'
|
|
1387
|
+
: 'replace_text old_text not unique; use a larger unique fragment or retry through edit'
|
|
1388
|
+
);
|
|
1060
1389
|
}
|
|
1061
1390
|
const afterContent = state.content.replace(oldText, newText);
|
|
1062
1391
|
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
@@ -1093,16 +1422,55 @@ async function generateDiff(root, args) {
|
|
|
1093
1422
|
};
|
|
1094
1423
|
}
|
|
1095
1424
|
|
|
1096
|
-
async function
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1425
|
+
async function applyPatch(root, args) {
|
|
1426
|
+
const patchText = String(args?.patch || args?.content || '').trim();
|
|
1427
|
+
if (!patchText) throw new Error('patch requires patch content');
|
|
1428
|
+
const files = parseUnifiedPatch(patchText);
|
|
1429
|
+
if (files.length === 0) throw new Error('patch contains no file changes');
|
|
1430
|
+
|
|
1431
|
+
const results = [];
|
|
1432
|
+
for (const fileChange of files) {
|
|
1433
|
+
const newPath = String(fileChange.newPath || '').trim();
|
|
1434
|
+
const oldPath = String(fileChange.oldPath || '').trim();
|
|
1435
|
+
const targetPath = newPath && newPath !== '/dev/null' ? newPath : oldPath;
|
|
1436
|
+
if (!targetPath || targetPath === '/dev/null') {
|
|
1437
|
+
throw new Error('patch requires a target file path');
|
|
1438
|
+
}
|
|
1439
|
+
const absTarget = resolveInWorkspace(root, targetPath);
|
|
1440
|
+
let beforeContent = '';
|
|
1441
|
+
let beforeLines = [];
|
|
1442
|
+
try {
|
|
1443
|
+
beforeContent = await fs.readFile(absTarget, 'utf8');
|
|
1444
|
+
beforeLines = splitLines(beforeContent);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
if (!(error && error.code === 'ENOENT')) throw error;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
let nextLines = beforeLines;
|
|
1450
|
+
for (const hunk of fileChange.hunks) {
|
|
1451
|
+
nextLines = applyHunkToLines(nextLines, hunk);
|
|
1452
|
+
}
|
|
1453
|
+
const afterContent = nextLines.join('\n');
|
|
1454
|
+
|
|
1455
|
+
if (newPath === '/dev/null') {
|
|
1456
|
+
await fs.rm(absTarget, { force: true });
|
|
1457
|
+
results.push({
|
|
1458
|
+
path: targetPath,
|
|
1459
|
+
action: 'delete',
|
|
1460
|
+
changed_line: 1,
|
|
1461
|
+
diff_preview: `deleted ${targetPath}`,
|
|
1462
|
+
diff: buildUnifiedDiff(beforeContent, '', targetPath),
|
|
1463
|
+
new_hash: sha256('')
|
|
1464
|
+
});
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
await fs.mkdir(path.dirname(absTarget), { recursive: true });
|
|
1469
|
+
await fs.writeFile(absTarget, afterContent, 'utf8');
|
|
1470
|
+
results.push(editResult(targetPath, beforeContent ? 'patch' : 'create', beforeContent, afterContent, 1));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
return results.length === 1 ? results[0] : { ok: true, files: results };
|
|
1106
1474
|
}
|
|
1107
1475
|
|
|
1108
1476
|
async function openTarget(root, args) {
|
|
@@ -1125,10 +1493,11 @@ async function openTarget(root, args) {
|
|
|
1125
1493
|
symbol: symbol || undefined,
|
|
1126
1494
|
main_block: block,
|
|
1127
1495
|
related: mainBlock.related || { imports: [], local_symbols: [] },
|
|
1128
|
-
|
|
1496
|
+
edit: {
|
|
1129
1497
|
start_line: block.start_line,
|
|
1130
1498
|
end_line: block.end_line,
|
|
1131
|
-
old_hash: sha256(block.content)
|
|
1499
|
+
old_hash: sha256(block.content),
|
|
1500
|
+
old_content: block.content
|
|
1132
1501
|
}
|
|
1133
1502
|
};
|
|
1134
1503
|
}
|
|
@@ -1137,9 +1506,16 @@ function normalizeEditTargetArgs(args = {}) {
|
|
|
1137
1506
|
const file = String(args?.file || args?.path || '').trim();
|
|
1138
1507
|
const nestedEdit = args?.edit && typeof args.edit === 'object' ? args.edit : null;
|
|
1139
1508
|
if (nestedEdit) {
|
|
1509
|
+
const normalizedEdit = { ...nestedEdit };
|
|
1510
|
+
if (normalizedEdit.new_content == null && normalizedEdit.content != null) {
|
|
1511
|
+
normalizedEdit.new_content = normalizedEdit.content;
|
|
1512
|
+
}
|
|
1513
|
+
if (normalizedEdit.new_text == null && normalizedEdit.content != null && normalizedEdit.old_text != null) {
|
|
1514
|
+
normalizedEdit.new_text = normalizedEdit.content;
|
|
1515
|
+
}
|
|
1140
1516
|
return {
|
|
1141
1517
|
file,
|
|
1142
|
-
edit:
|
|
1518
|
+
edit: normalizedEdit
|
|
1143
1519
|
};
|
|
1144
1520
|
}
|
|
1145
1521
|
return {
|
|
@@ -1160,13 +1536,35 @@ async function editTarget(root, args) {
|
|
|
1160
1536
|
const normalized = normalizeEditTargetArgs(args);
|
|
1161
1537
|
const file = normalized.file;
|
|
1162
1538
|
const edit = normalized.edit || {};
|
|
1163
|
-
|
|
1164
|
-
|
|
1539
|
+
let kind = String(edit.kind || '').trim();
|
|
1540
|
+
const hasContent = edit.new_content != null || edit.content != null;
|
|
1541
|
+
const hasTargetHint = Boolean(edit.symbol || args?.symbol || edit.line || args?.line || edit.target);
|
|
1542
|
+
if (!kind) {
|
|
1543
|
+
if (hasContent && hasTargetHint) {
|
|
1544
|
+
kind = 'replace_block';
|
|
1545
|
+
} else if (edit.old_text != null && (edit.new_text != null || edit.content != null)) {
|
|
1546
|
+
kind = 'replace_text';
|
|
1547
|
+
} else if ((edit.anchor_text != null || edit.target_text != null) && (edit.content != null || edit.new_content != null)) {
|
|
1548
|
+
kind = String(edit.position || edit.mode || args?.position || '').trim() === 'after' ? 'insert_after' : 'insert_before';
|
|
1549
|
+
} else if (hasContent) {
|
|
1550
|
+
kind = 'rewrite_file';
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (!file || !kind) throw new Error('edit requires file and edit.kind');
|
|
1165
1554
|
if (kind === 'replace_block') {
|
|
1555
|
+
const resolvedTarget =
|
|
1556
|
+
edit.target ||
|
|
1557
|
+
(
|
|
1558
|
+
await openTarget(root, {
|
|
1559
|
+
file,
|
|
1560
|
+
symbol: edit.symbol || args?.symbol,
|
|
1561
|
+
line: edit.line || args?.line
|
|
1562
|
+
})
|
|
1563
|
+
).edit;
|
|
1166
1564
|
try {
|
|
1167
1565
|
return await replaceBlock(root, {
|
|
1168
1566
|
path: file,
|
|
1169
|
-
target:
|
|
1567
|
+
target: resolvedTarget,
|
|
1170
1568
|
new_content: edit.new_content
|
|
1171
1569
|
});
|
|
1172
1570
|
} catch (error) {
|
|
@@ -1174,7 +1572,7 @@ async function editTarget(root, args) {
|
|
|
1174
1572
|
const validation = await validateEdit(root, {
|
|
1175
1573
|
path: file,
|
|
1176
1574
|
kind: 'replace_block',
|
|
1177
|
-
target:
|
|
1575
|
+
target: resolvedTarget
|
|
1178
1576
|
});
|
|
1179
1577
|
return replaceBlock(root, {
|
|
1180
1578
|
path: file,
|
|
@@ -1196,7 +1594,14 @@ async function editTarget(root, args) {
|
|
|
1196
1594
|
if (kind === 'insert_after') {
|
|
1197
1595
|
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_after');
|
|
1198
1596
|
}
|
|
1199
|
-
|
|
1597
|
+
if (kind === 'rewrite_file') {
|
|
1598
|
+
return writeFile(root, {
|
|
1599
|
+
path: file,
|
|
1600
|
+
content: edit.new_content ?? edit.content ?? '',
|
|
1601
|
+
full_file_rewrite: true
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
throw new Error(`edit does not support kind: ${kind}`);
|
|
1200
1605
|
}
|
|
1201
1606
|
|
|
1202
1607
|
export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
@@ -1204,192 +1609,131 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1204
1609
|
{
|
|
1205
1610
|
type: 'function',
|
|
1206
1611
|
function: {
|
|
1207
|
-
name: '
|
|
1208
|
-
description:
|
|
1209
|
-
|
|
1210
|
-
type: 'object',
|
|
1211
|
-
properties: {
|
|
1212
|
-
query: { type: 'string' },
|
|
1213
|
-
path: { type: 'string' },
|
|
1214
|
-
max_results: { type: 'number' },
|
|
1215
|
-
language: { type: 'string' },
|
|
1216
|
-
file_types: { type: 'array', items: { type: 'string' } }
|
|
1217
|
-
},
|
|
1218
|
-
required: ['query']
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
},
|
|
1222
|
-
{
|
|
1223
|
-
type: 'function',
|
|
1224
|
-
function: {
|
|
1225
|
-
name: 'open_target',
|
|
1226
|
-
description: 'Open a candidate location and return the smallest useful code block plus edit metadata',
|
|
1227
|
-
parameters: {
|
|
1228
|
-
type: 'object',
|
|
1229
|
-
properties: {
|
|
1230
|
-
file: { type: 'string' },
|
|
1231
|
-
path: { type: 'string' },
|
|
1232
|
-
line: { type: 'number' },
|
|
1233
|
-
symbol: { type: 'string' },
|
|
1234
|
-
max_related_calls: { type: 'number' },
|
|
1235
|
-
max_related_imports: { type: 'number' },
|
|
1236
|
-
max_related_types: { type: 'number' }
|
|
1237
|
-
},
|
|
1238
|
-
required: ['file']
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
},
|
|
1242
|
-
{
|
|
1243
|
-
type: 'function',
|
|
1244
|
-
function: {
|
|
1245
|
-
name: 'edit_target',
|
|
1246
|
-
description: 'Apply a validated high-level edit against an opened target',
|
|
1612
|
+
name: 'read',
|
|
1613
|
+
description:
|
|
1614
|
+
'Primary read tool. First call returns metadata+read_token, second call with include_content=true and matching read_token returns content',
|
|
1247
1615
|
parameters: {
|
|
1248
1616
|
type: 'object',
|
|
1249
1617
|
properties: {
|
|
1250
|
-
file: { type: 'string' },
|
|
1251
1618
|
path: { type: 'string' },
|
|
1252
|
-
|
|
1619
|
+
start_line: { type: 'number' },
|
|
1620
|
+
end_line: { type: 'number' },
|
|
1621
|
+
max_chars: { type: 'number' },
|
|
1622
|
+
include_content: { type: 'boolean' },
|
|
1623
|
+
read_token: { type: 'string' }
|
|
1253
1624
|
},
|
|
1254
|
-
required: ['
|
|
1625
|
+
required: ['path']
|
|
1255
1626
|
}
|
|
1256
1627
|
}
|
|
1257
1628
|
},
|
|
1258
1629
|
{
|
|
1259
1630
|
type: 'function',
|
|
1260
1631
|
function: {
|
|
1261
|
-
name: '
|
|
1262
|
-
description: 'Search
|
|
1632
|
+
name: 'grep',
|
|
1633
|
+
description: 'Search file contents using a plain string or regex pattern and return compact matches',
|
|
1263
1634
|
parameters: {
|
|
1264
1635
|
type: 'object',
|
|
1265
1636
|
properties: {
|
|
1637
|
+
pattern: { type: 'string' },
|
|
1266
1638
|
query: { type: 'string' },
|
|
1267
1639
|
path: { type: 'string' },
|
|
1268
|
-
|
|
1640
|
+
regex: { type: 'boolean' },
|
|
1269
1641
|
case_sensitive: { type: 'boolean' },
|
|
1642
|
+
max_results: { type: 'number' },
|
|
1270
1643
|
language: { type: 'string' },
|
|
1271
1644
|
file_types: { type: 'array', items: { type: 'string' } }
|
|
1272
1645
|
},
|
|
1273
|
-
required: ['
|
|
1646
|
+
required: ['pattern']
|
|
1274
1647
|
}
|
|
1275
1648
|
}
|
|
1276
1649
|
},
|
|
1277
1650
|
{
|
|
1278
1651
|
type: 'function',
|
|
1279
1652
|
function: {
|
|
1280
|
-
name: '
|
|
1281
|
-
description: '
|
|
1653
|
+
name: 'glob',
|
|
1654
|
+
description: 'Find files by glob pattern such as **/*.ts or src/**/*.tsx',
|
|
1282
1655
|
parameters: {
|
|
1283
1656
|
type: 'object',
|
|
1284
1657
|
properties: {
|
|
1658
|
+
pattern: { type: 'string' },
|
|
1285
1659
|
path: { type: 'string' },
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
anchor_line: { type: 'number' }
|
|
1660
|
+
include_hidden: { type: 'boolean' },
|
|
1661
|
+
max_results: { type: 'number' }
|
|
1289
1662
|
},
|
|
1290
|
-
required: ['
|
|
1663
|
+
required: ['pattern']
|
|
1291
1664
|
}
|
|
1292
1665
|
}
|
|
1293
1666
|
},
|
|
1294
1667
|
{
|
|
1295
1668
|
type: 'function',
|
|
1296
1669
|
function: {
|
|
1297
|
-
name: '
|
|
1298
|
-
description: '
|
|
1670
|
+
name: 'list',
|
|
1671
|
+
description: 'List files and directories in a workspace path',
|
|
1299
1672
|
parameters: {
|
|
1300
1673
|
type: 'object',
|
|
1301
1674
|
properties: {
|
|
1302
1675
|
path: { type: 'string' },
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
max_related_imports: { type: 'number' },
|
|
1306
|
-
max_related_types: { type: 'number' }
|
|
1307
|
-
},
|
|
1308
|
-
required: ['path', 'symbol']
|
|
1676
|
+
include_hidden: { type: 'boolean' }
|
|
1677
|
+
}
|
|
1309
1678
|
}
|
|
1310
1679
|
}
|
|
1311
1680
|
},
|
|
1312
1681
|
{
|
|
1313
1682
|
type: 'function',
|
|
1314
1683
|
function: {
|
|
1315
|
-
name: '
|
|
1316
|
-
description:
|
|
1684
|
+
name: 'edit',
|
|
1685
|
+
description:
|
|
1686
|
+
'Preferred edit tool for existing files. Accepts natural forms such as file + new_content for whole-file rewrites, file + symbol/line + new_content for block edits, file + old_text + new_text for exact replacements, and file + anchor_text + content for anchored inserts. A nested edit object is also supported.',
|
|
1317
1687
|
parameters: {
|
|
1318
1688
|
type: 'object',
|
|
1319
1689
|
properties: {
|
|
1690
|
+
file: { type: 'string' },
|
|
1320
1691
|
path: { type: 'string' },
|
|
1321
|
-
|
|
1322
|
-
target: { type: 'object' },
|
|
1323
|
-
start_line: { type: 'number' },
|
|
1324
|
-
end_line: { type: 'number' },
|
|
1692
|
+
new_content: { type: 'string' },
|
|
1325
1693
|
old_text: { type: 'string' },
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
},
|
|
1332
|
-
{
|
|
1333
|
-
type: 'function',
|
|
1334
|
-
function: {
|
|
1335
|
-
name: 'replace_block',
|
|
1336
|
-
description: 'Replace a validated line block using an old_hash guard',
|
|
1337
|
-
parameters: {
|
|
1338
|
-
type: 'object',
|
|
1339
|
-
properties: {
|
|
1340
|
-
path: { type: 'string' },
|
|
1694
|
+
new_text: { type: 'string' },
|
|
1695
|
+
anchor_text: { type: 'string' },
|
|
1696
|
+
content: { type: 'string' },
|
|
1697
|
+
position: { type: 'string' },
|
|
1698
|
+
kind: { type: 'string' },
|
|
1341
1699
|
target: { type: 'object' },
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
},
|
|
1348
|
-
{
|
|
1349
|
-
type: 'function',
|
|
1350
|
-
function: {
|
|
1351
|
-
name: 'replace_text',
|
|
1352
|
-
description: 'Replace a unique text fragment in a file',
|
|
1353
|
-
parameters: {
|
|
1354
|
-
type: 'object',
|
|
1355
|
-
properties: {
|
|
1356
|
-
path: { type: 'string' },
|
|
1357
|
-
old_text: { type: 'string' },
|
|
1358
|
-
new_text: { type: 'string' }
|
|
1700
|
+
symbol: { type: 'string' },
|
|
1701
|
+
line: { type: 'number' },
|
|
1702
|
+
edit: { type: 'object' },
|
|
1359
1703
|
},
|
|
1360
|
-
required: ['
|
|
1704
|
+
required: ['file']
|
|
1361
1705
|
}
|
|
1362
1706
|
}
|
|
1363
1707
|
},
|
|
1364
1708
|
{
|
|
1365
1709
|
type: 'function',
|
|
1366
1710
|
function: {
|
|
1367
|
-
name: '
|
|
1368
|
-
description:
|
|
1711
|
+
name: 'write',
|
|
1712
|
+
description:
|
|
1713
|
+
'Primary write tool. Create a UTF-8 text file or overwrite an existing file. Existing code files require full_file_rewrite=true for whole-file overwrites.',
|
|
1369
1714
|
parameters: {
|
|
1370
1715
|
type: 'object',
|
|
1371
1716
|
properties: {
|
|
1372
1717
|
path: { type: 'string' },
|
|
1373
|
-
|
|
1374
|
-
|
|
1718
|
+
content: { type: 'string' },
|
|
1719
|
+
append: { type: 'boolean' },
|
|
1720
|
+
full_file_rewrite: { type: 'boolean' }
|
|
1375
1721
|
},
|
|
1376
|
-
required: ['path', '
|
|
1722
|
+
required: ['path', 'content']
|
|
1377
1723
|
}
|
|
1378
1724
|
}
|
|
1379
1725
|
},
|
|
1380
1726
|
{
|
|
1381
1727
|
type: 'function',
|
|
1382
1728
|
function: {
|
|
1383
|
-
name: '
|
|
1384
|
-
description: '
|
|
1729
|
+
name: 'run',
|
|
1730
|
+
description: 'Primary run tool. Execute a one-shot shell command in workspace. Do not use for long-running services.',
|
|
1385
1731
|
parameters: {
|
|
1386
1732
|
type: 'object',
|
|
1387
1733
|
properties: {
|
|
1388
|
-
|
|
1389
|
-
anchor_text: { type: 'string' },
|
|
1390
|
-
content: { type: 'string' }
|
|
1734
|
+
command: { type: 'string' }
|
|
1391
1735
|
},
|
|
1392
|
-
required: ['
|
|
1736
|
+
required: ['command']
|
|
1393
1737
|
}
|
|
1394
1738
|
}
|
|
1395
1739
|
},
|
|
@@ -1411,52 +1755,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1411
1755
|
{
|
|
1412
1756
|
type: 'function',
|
|
1413
1757
|
function: {
|
|
1414
|
-
name: '
|
|
1415
|
-
description:
|
|
1416
|
-
'Two-phase read: first call returns metadata+read_token; second call with include_content=true and matching read_token returns content',
|
|
1417
|
-
parameters: {
|
|
1418
|
-
type: 'object',
|
|
1419
|
-
properties: {
|
|
1420
|
-
path: { type: 'string' },
|
|
1421
|
-
start_line: { type: 'number' },
|
|
1422
|
-
end_line: { type: 'number' },
|
|
1423
|
-
max_chars: { type: 'number' },
|
|
1424
|
-
include_content: { type: 'boolean' },
|
|
1425
|
-
read_token: { type: 'string' }
|
|
1426
|
-
},
|
|
1427
|
-
required: ['path']
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
},
|
|
1431
|
-
{
|
|
1432
|
-
type: 'function',
|
|
1433
|
-
function: {
|
|
1434
|
-
name: 'write_file',
|
|
1435
|
-
description:
|
|
1436
|
-
'Write a UTF-8 text file in workspace. Always provide a full file path, not a directory. Existing code files require full_file_rewrite=true for whole-file overwrites.',
|
|
1437
|
-
parameters: {
|
|
1438
|
-
type: 'object',
|
|
1439
|
-
properties: {
|
|
1440
|
-
path: { type: 'string' },
|
|
1441
|
-
content: { type: 'string' },
|
|
1442
|
-
append: { type: 'boolean' },
|
|
1443
|
-
full_file_rewrite: { type: 'boolean' }
|
|
1444
|
-
},
|
|
1445
|
-
required: ['path', 'content']
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
},
|
|
1449
|
-
{
|
|
1450
|
-
type: 'function',
|
|
1451
|
-
function: {
|
|
1452
|
-
name: 'run_command',
|
|
1453
|
-
description: 'Execute a one-shot shell command in workspace. Do not use for long-running services.',
|
|
1758
|
+
name: 'patch',
|
|
1759
|
+
description: 'Apply one or more unified diff hunks to files in the workspace',
|
|
1454
1760
|
parameters: {
|
|
1455
1761
|
type: 'object',
|
|
1456
1762
|
properties: {
|
|
1457
|
-
|
|
1763
|
+
patch: { type: 'string' },
|
|
1764
|
+
content: { type: 'string' }
|
|
1458
1765
|
},
|
|
1459
|
-
required: ['
|
|
1766
|
+
required: ['patch']
|
|
1460
1767
|
}
|
|
1461
1768
|
}
|
|
1462
1769
|
},
|
|
@@ -1542,27 +1849,10 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1542
1849
|
}
|
|
1543
1850
|
}
|
|
1544
1851
|
}
|
|
1545
|
-
];
|
|
1852
|
+
].filter(Boolean);
|
|
1546
1853
|
|
|
1547
1854
|
const handlers = {
|
|
1548
|
-
|
|
1549
|
-
open_target: (args) => openTarget(workspaceRoot, args),
|
|
1550
|
-
edit_target: (args) => editTarget(workspaceRoot, args),
|
|
1551
|
-
search_code: (args) => searchCode(workspaceRoot, args),
|
|
1552
|
-
read_block: (args) => readBlock(workspaceRoot, args),
|
|
1553
|
-
read_symbol_context: (args) => readSymbolContext(workspaceRoot, args),
|
|
1554
|
-
validate_edit: (args) => validateEdit(workspaceRoot, args),
|
|
1555
|
-
replace_block: (args) => replaceBlock(workspaceRoot, args),
|
|
1556
|
-
replace_text: (args) => replaceText(workspaceRoot, args),
|
|
1557
|
-
insert_before: (args) => insertRelative(workspaceRoot, args, 'insert_before'),
|
|
1558
|
-
insert_after: (args) => insertRelative(workspaceRoot, args, 'insert_after'),
|
|
1559
|
-
generate_diff: (args) => generateDiff(workspaceRoot, args),
|
|
1560
|
-
start_service: (args) => startService(workspaceRoot, config, args),
|
|
1561
|
-
list_services: () => listServices(workspaceRoot),
|
|
1562
|
-
get_service_status: (args) => getServiceStatus(workspaceRoot, args),
|
|
1563
|
-
get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
|
|
1564
|
-
stop_service: (args) => stopService(workspaceRoot, args),
|
|
1565
|
-
read_file: (args) =>
|
|
1855
|
+
read: (args) =>
|
|
1566
1856
|
readFile(workspaceRoot, {
|
|
1567
1857
|
...args,
|
|
1568
1858
|
default_lines: config.context?.read_file_default_lines ?? 220,
|
|
@@ -1571,8 +1861,19 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1571
1861
|
? args.max_chars
|
|
1572
1862
|
: config.context?.read_file_max_chars ?? 24000
|
|
1573
1863
|
}),
|
|
1574
|
-
|
|
1575
|
-
|
|
1864
|
+
grep: (args) => grep(workspaceRoot, args),
|
|
1865
|
+
glob: (args) => glob(workspaceRoot, args),
|
|
1866
|
+
list: (args) => list(workspaceRoot, args),
|
|
1867
|
+
edit: (args) => editTarget(workspaceRoot, args),
|
|
1868
|
+
generate_diff: (args) => generateDiff(workspaceRoot, args),
|
|
1869
|
+
patch: (args) => applyPatch(workspaceRoot, args),
|
|
1870
|
+
write: (args) => writeFile(workspaceRoot, args),
|
|
1871
|
+
run: (args) => runCommand(workspaceRoot, config, args),
|
|
1872
|
+
start_service: (args) => startService(workspaceRoot, config, args),
|
|
1873
|
+
list_services: () => listServices(workspaceRoot),
|
|
1874
|
+
get_service_status: (args) => getServiceStatus(workspaceRoot, args),
|
|
1875
|
+
get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
|
|
1876
|
+
stop_service: (args) => stopService(workspaceRoot, args)
|
|
1576
1877
|
};
|
|
1577
1878
|
|
|
1578
1879
|
return { definitions, handlers };
|