codemini-cli 0.1.17 → 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/cli.js +1 -1
- 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 +541 -210
- package/src/tui/chat-app.js +281 -124
package/src/core/tools.js
CHANGED
|
@@ -37,6 +37,25 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
37
37
|
'.sh',
|
|
38
38
|
'.ps1'
|
|
39
39
|
]);
|
|
40
|
+
const CODE_WRITE_GUARD_EXTENSIONS = new Set([
|
|
41
|
+
'.js',
|
|
42
|
+
'.jsx',
|
|
43
|
+
'.ts',
|
|
44
|
+
'.tsx',
|
|
45
|
+
'.mjs',
|
|
46
|
+
'.cjs',
|
|
47
|
+
'.py',
|
|
48
|
+
'.rb',
|
|
49
|
+
'.go',
|
|
50
|
+
'.rs',
|
|
51
|
+
'.java',
|
|
52
|
+
'.cs',
|
|
53
|
+
'.css',
|
|
54
|
+
'.scss',
|
|
55
|
+
'.html',
|
|
56
|
+
'.sh',
|
|
57
|
+
'.ps1'
|
|
58
|
+
]);
|
|
40
59
|
const LANGUAGE_FILE_TYPES = {
|
|
41
60
|
js: ['js', 'jsx', 'mjs', 'cjs'],
|
|
42
61
|
ts: ['ts', 'tsx'],
|
|
@@ -96,10 +115,75 @@ function splitLines(text) {
|
|
|
96
115
|
return String(text || '').split('\n');
|
|
97
116
|
}
|
|
98
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
|
+
|
|
99
179
|
function detectTextFile(filePath) {
|
|
100
180
|
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
101
181
|
}
|
|
102
182
|
|
|
183
|
+
function isCodeLikePath(filePath) {
|
|
184
|
+
return CODE_WRITE_GUARD_EXTENSIONS.has(path.extname(String(filePath || '')).toLowerCase());
|
|
185
|
+
}
|
|
186
|
+
|
|
103
187
|
function normalizeFileTypes(args = {}) {
|
|
104
188
|
const explicit = Array.isArray(args?.file_types) ? args.file_types.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : [];
|
|
105
189
|
const language = String(args?.language || '').trim().toLowerCase();
|
|
@@ -133,6 +217,63 @@ async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
|
133
217
|
return out;
|
|
134
218
|
}
|
|
135
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
|
+
|
|
136
277
|
function getLineColumnForMatch(line, query, caseSensitive = false) {
|
|
137
278
|
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
138
279
|
const needle = caseSensitive ? query : query.toLowerCase();
|
|
@@ -384,6 +525,129 @@ function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
|
|
|
384
525
|
return body.join('\n');
|
|
385
526
|
}
|
|
386
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
|
+
|
|
387
651
|
async function getFileState(root, relativePath) {
|
|
388
652
|
const target = resolveInWorkspace(root, relativePath);
|
|
389
653
|
const stat = await fs.stat(target);
|
|
@@ -429,7 +693,7 @@ async function readFile(root, args) {
|
|
|
429
693
|
suggested_start_line: startLine,
|
|
430
694
|
suggested_end_line: endLine,
|
|
431
695
|
read_token: readToken,
|
|
432
|
-
next: 'Call
|
|
696
|
+
next: 'Call read again with include_content=true and this read_token'
|
|
433
697
|
};
|
|
434
698
|
}
|
|
435
699
|
|
|
@@ -469,16 +733,16 @@ async function readFile(root, args) {
|
|
|
469
733
|
async function writeFile(root, args) {
|
|
470
734
|
const rawPath = String(args?.path || '').trim();
|
|
471
735
|
if (!rawPath) {
|
|
472
|
-
throw new Error('
|
|
736
|
+
throw new Error('write requires a file path like weather/WeatherForecast.js');
|
|
473
737
|
}
|
|
474
738
|
if (rawPath === '.' || rawPath === './') {
|
|
475
|
-
throw new Error('
|
|
739
|
+
throw new Error('write requires a file path, not the workspace root');
|
|
476
740
|
}
|
|
477
741
|
const target = resolveInWorkspace(root, rawPath);
|
|
478
742
|
try {
|
|
479
743
|
const stat = await fs.stat(target);
|
|
480
744
|
if (stat.isDirectory()) {
|
|
481
|
-
throw new Error(`
|
|
745
|
+
throw new Error(`write target is a directory: ${rawPath}`);
|
|
482
746
|
}
|
|
483
747
|
} catch (error) {
|
|
484
748
|
if (error?.code && error.code !== 'ENOENT') throw error;
|
|
@@ -490,6 +754,11 @@ async function writeFile(root, args) {
|
|
|
490
754
|
} catch {
|
|
491
755
|
existed = false;
|
|
492
756
|
}
|
|
757
|
+
if (existed && !args?.append && !args?.full_file_rewrite && isCodeLikePath(rawPath)) {
|
|
758
|
+
throw new Error(
|
|
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.'
|
|
760
|
+
);
|
|
761
|
+
}
|
|
493
762
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
494
763
|
if (args?.append) {
|
|
495
764
|
await fs.appendFile(target, args?.content || '', 'utf8');
|
|
@@ -521,10 +790,10 @@ async function writeFile(root, args) {
|
|
|
521
790
|
async function runCommand(root, config, args) {
|
|
522
791
|
const command = args?.command || '';
|
|
523
792
|
if (!command.trim()) {
|
|
524
|
-
throw new Error('
|
|
793
|
+
throw new Error('run requires command');
|
|
525
794
|
}
|
|
526
795
|
if (isLikelyLongRunningCommand(command)) {
|
|
527
|
-
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.');
|
|
528
797
|
}
|
|
529
798
|
if (
|
|
530
799
|
!config.policy.allow_dangerous_commands &&
|
|
@@ -905,6 +1174,81 @@ async function searchCode(root, args) {
|
|
|
905
1174
|
};
|
|
906
1175
|
}
|
|
907
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
|
+
|
|
908
1252
|
async function readBlock(root, args) {
|
|
909
1253
|
const relativePath = String(args?.path || '').trim();
|
|
910
1254
|
if (!relativePath) throw new Error('read_block requires path');
|
|
@@ -957,17 +1301,25 @@ async function validateEdit(root, args) {
|
|
|
957
1301
|
if (!Number.isFinite(startLine) || !Number.isFinite(endLine) || startLine <= 0 || endLine < startLine) {
|
|
958
1302
|
throw new Error('replace_block validation requires target.start_line and target.end_line');
|
|
959
1303
|
}
|
|
960
|
-
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');
|
|
961
1311
|
return {
|
|
962
1312
|
ok: true,
|
|
963
1313
|
path: relativePath,
|
|
964
1314
|
kind,
|
|
965
1315
|
target: {
|
|
966
|
-
start_line: startLine,
|
|
967
|
-
end_line: endLine,
|
|
968
|
-
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
|
|
969
1320
|
},
|
|
970
|
-
file_hash: sha256(content)
|
|
1321
|
+
file_hash: sha256(content),
|
|
1322
|
+
relocated: Boolean(resolved?.relocated)
|
|
971
1323
|
};
|
|
972
1324
|
}
|
|
973
1325
|
|
|
@@ -1007,18 +1359,19 @@ async function replaceBlock(root, args) {
|
|
|
1007
1359
|
const relativePath = String(args?.path || '').trim();
|
|
1008
1360
|
const newContent = String(args?.new_content || args?.content || '');
|
|
1009
1361
|
const target = args?.target || {};
|
|
1010
|
-
const startLine = Number(target.start_line);
|
|
1011
|
-
const endLine = Number(target.end_line);
|
|
1012
|
-
const oldHash = String(target.old_hash || '');
|
|
1013
1362
|
const state = await getFileState(root, relativePath);
|
|
1014
|
-
const
|
|
1015
|
-
if (!
|
|
1016
|
-
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');
|
|
1017
1366
|
}
|
|
1018
|
-
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
|
+
];
|
|
1019
1372
|
const afterContent = nextLines.join('\n');
|
|
1020
1373
|
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1021
|
-
return editResult(relativePath, 'replace_block', state.content, afterContent,
|
|
1374
|
+
return editResult(relativePath, 'replace_block', state.content, afterContent, resolved.start_line);
|
|
1022
1375
|
}
|
|
1023
1376
|
|
|
1024
1377
|
async function replaceText(root, args) {
|
|
@@ -1028,7 +1381,11 @@ async function replaceText(root, args) {
|
|
|
1028
1381
|
const state = await getFileState(root, relativePath);
|
|
1029
1382
|
const occurrences = state.content.split(oldText).length - 1;
|
|
1030
1383
|
if (occurrences !== 1) {
|
|
1031
|
-
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
|
+
);
|
|
1032
1389
|
}
|
|
1033
1390
|
const afterContent = state.content.replace(oldText, newText);
|
|
1034
1391
|
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
@@ -1065,16 +1422,55 @@ async function generateDiff(root, args) {
|
|
|
1065
1422
|
};
|
|
1066
1423
|
}
|
|
1067
1424
|
|
|
1068
|
-
async function
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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 };
|
|
1078
1474
|
}
|
|
1079
1475
|
|
|
1080
1476
|
async function openTarget(root, args) {
|
|
@@ -1097,10 +1493,11 @@ async function openTarget(root, args) {
|
|
|
1097
1493
|
symbol: symbol || undefined,
|
|
1098
1494
|
main_block: block,
|
|
1099
1495
|
related: mainBlock.related || { imports: [], local_symbols: [] },
|
|
1100
|
-
|
|
1496
|
+
edit: {
|
|
1101
1497
|
start_line: block.start_line,
|
|
1102
1498
|
end_line: block.end_line,
|
|
1103
|
-
old_hash: sha256(block.content)
|
|
1499
|
+
old_hash: sha256(block.content),
|
|
1500
|
+
old_content: block.content
|
|
1104
1501
|
}
|
|
1105
1502
|
};
|
|
1106
1503
|
}
|
|
@@ -1109,9 +1506,16 @@ function normalizeEditTargetArgs(args = {}) {
|
|
|
1109
1506
|
const file = String(args?.file || args?.path || '').trim();
|
|
1110
1507
|
const nestedEdit = args?.edit && typeof args.edit === 'object' ? args.edit : null;
|
|
1111
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
|
+
}
|
|
1112
1516
|
return {
|
|
1113
1517
|
file,
|
|
1114
|
-
edit:
|
|
1518
|
+
edit: normalizedEdit
|
|
1115
1519
|
};
|
|
1116
1520
|
}
|
|
1117
1521
|
return {
|
|
@@ -1132,13 +1536,35 @@ async function editTarget(root, args) {
|
|
|
1132
1536
|
const normalized = normalizeEditTargetArgs(args);
|
|
1133
1537
|
const file = normalized.file;
|
|
1134
1538
|
const edit = normalized.edit || {};
|
|
1135
|
-
|
|
1136
|
-
|
|
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');
|
|
1137
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;
|
|
1138
1564
|
try {
|
|
1139
1565
|
return await replaceBlock(root, {
|
|
1140
1566
|
path: file,
|
|
1141
|
-
target:
|
|
1567
|
+
target: resolvedTarget,
|
|
1142
1568
|
new_content: edit.new_content
|
|
1143
1569
|
});
|
|
1144
1570
|
} catch (error) {
|
|
@@ -1146,7 +1572,7 @@ async function editTarget(root, args) {
|
|
|
1146
1572
|
const validation = await validateEdit(root, {
|
|
1147
1573
|
path: file,
|
|
1148
1574
|
kind: 'replace_block',
|
|
1149
|
-
target:
|
|
1575
|
+
target: resolvedTarget
|
|
1150
1576
|
});
|
|
1151
1577
|
return replaceBlock(root, {
|
|
1152
1578
|
path: file,
|
|
@@ -1168,7 +1594,14 @@ async function editTarget(root, args) {
|
|
|
1168
1594
|
if (kind === 'insert_after') {
|
|
1169
1595
|
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_after');
|
|
1170
1596
|
}
|
|
1171
|
-
|
|
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}`);
|
|
1172
1605
|
}
|
|
1173
1606
|
|
|
1174
1607
|
export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
@@ -1176,192 +1609,131 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1176
1609
|
{
|
|
1177
1610
|
type: 'function',
|
|
1178
1611
|
function: {
|
|
1179
|
-
name: '
|
|
1180
|
-
description:
|
|
1181
|
-
|
|
1182
|
-
type: 'object',
|
|
1183
|
-
properties: {
|
|
1184
|
-
query: { type: 'string' },
|
|
1185
|
-
path: { type: 'string' },
|
|
1186
|
-
max_results: { type: 'number' },
|
|
1187
|
-
language: { type: 'string' },
|
|
1188
|
-
file_types: { type: 'array', items: { type: 'string' } }
|
|
1189
|
-
},
|
|
1190
|
-
required: ['query']
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
},
|
|
1194
|
-
{
|
|
1195
|
-
type: 'function',
|
|
1196
|
-
function: {
|
|
1197
|
-
name: 'open_target',
|
|
1198
|
-
description: 'Open a candidate location and return the smallest useful code block plus edit metadata',
|
|
1199
|
-
parameters: {
|
|
1200
|
-
type: 'object',
|
|
1201
|
-
properties: {
|
|
1202
|
-
file: { type: 'string' },
|
|
1203
|
-
path: { type: 'string' },
|
|
1204
|
-
line: { type: 'number' },
|
|
1205
|
-
symbol: { type: 'string' },
|
|
1206
|
-
max_related_calls: { type: 'number' },
|
|
1207
|
-
max_related_imports: { type: 'number' },
|
|
1208
|
-
max_related_types: { type: 'number' }
|
|
1209
|
-
},
|
|
1210
|
-
required: ['file']
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
},
|
|
1214
|
-
{
|
|
1215
|
-
type: 'function',
|
|
1216
|
-
function: {
|
|
1217
|
-
name: 'edit_target',
|
|
1218
|
-
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',
|
|
1219
1615
|
parameters: {
|
|
1220
1616
|
type: 'object',
|
|
1221
1617
|
properties: {
|
|
1222
|
-
file: { type: 'string' },
|
|
1223
1618
|
path: { type: 'string' },
|
|
1224
|
-
|
|
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' }
|
|
1225
1624
|
},
|
|
1226
|
-
required: ['
|
|
1625
|
+
required: ['path']
|
|
1227
1626
|
}
|
|
1228
1627
|
}
|
|
1229
1628
|
},
|
|
1230
1629
|
{
|
|
1231
1630
|
type: 'function',
|
|
1232
1631
|
function: {
|
|
1233
|
-
name: '
|
|
1234
|
-
description: 'Search
|
|
1632
|
+
name: 'grep',
|
|
1633
|
+
description: 'Search file contents using a plain string or regex pattern and return compact matches',
|
|
1235
1634
|
parameters: {
|
|
1236
1635
|
type: 'object',
|
|
1237
1636
|
properties: {
|
|
1637
|
+
pattern: { type: 'string' },
|
|
1238
1638
|
query: { type: 'string' },
|
|
1239
1639
|
path: { type: 'string' },
|
|
1240
|
-
|
|
1640
|
+
regex: { type: 'boolean' },
|
|
1241
1641
|
case_sensitive: { type: 'boolean' },
|
|
1642
|
+
max_results: { type: 'number' },
|
|
1242
1643
|
language: { type: 'string' },
|
|
1243
1644
|
file_types: { type: 'array', items: { type: 'string' } }
|
|
1244
1645
|
},
|
|
1245
|
-
required: ['
|
|
1646
|
+
required: ['pattern']
|
|
1246
1647
|
}
|
|
1247
1648
|
}
|
|
1248
1649
|
},
|
|
1249
1650
|
{
|
|
1250
1651
|
type: 'function',
|
|
1251
1652
|
function: {
|
|
1252
|
-
name: '
|
|
1253
|
-
description: '
|
|
1653
|
+
name: 'glob',
|
|
1654
|
+
description: 'Find files by glob pattern such as **/*.ts or src/**/*.tsx',
|
|
1254
1655
|
parameters: {
|
|
1255
1656
|
type: 'object',
|
|
1256
1657
|
properties: {
|
|
1658
|
+
pattern: { type: 'string' },
|
|
1257
1659
|
path: { type: 'string' },
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
anchor_line: { type: 'number' }
|
|
1660
|
+
include_hidden: { type: 'boolean' },
|
|
1661
|
+
max_results: { type: 'number' }
|
|
1261
1662
|
},
|
|
1262
|
-
required: ['
|
|
1663
|
+
required: ['pattern']
|
|
1263
1664
|
}
|
|
1264
1665
|
}
|
|
1265
1666
|
},
|
|
1266
1667
|
{
|
|
1267
1668
|
type: 'function',
|
|
1268
1669
|
function: {
|
|
1269
|
-
name: '
|
|
1270
|
-
description: '
|
|
1670
|
+
name: 'list',
|
|
1671
|
+
description: 'List files and directories in a workspace path',
|
|
1271
1672
|
parameters: {
|
|
1272
1673
|
type: 'object',
|
|
1273
1674
|
properties: {
|
|
1274
1675
|
path: { type: 'string' },
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
max_related_imports: { type: 'number' },
|
|
1278
|
-
max_related_types: { type: 'number' }
|
|
1279
|
-
},
|
|
1280
|
-
required: ['path', 'symbol']
|
|
1676
|
+
include_hidden: { type: 'boolean' }
|
|
1677
|
+
}
|
|
1281
1678
|
}
|
|
1282
1679
|
}
|
|
1283
1680
|
},
|
|
1284
1681
|
{
|
|
1285
1682
|
type: 'function',
|
|
1286
1683
|
function: {
|
|
1287
|
-
name: '
|
|
1288
|
-
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.',
|
|
1289
1687
|
parameters: {
|
|
1290
1688
|
type: 'object',
|
|
1291
1689
|
properties: {
|
|
1690
|
+
file: { type: 'string' },
|
|
1292
1691
|
path: { type: 'string' },
|
|
1293
|
-
|
|
1294
|
-
target: { type: 'object' },
|
|
1295
|
-
start_line: { type: 'number' },
|
|
1296
|
-
end_line: { type: 'number' },
|
|
1692
|
+
new_content: { type: 'string' },
|
|
1297
1693
|
old_text: { type: 'string' },
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
},
|
|
1304
|
-
{
|
|
1305
|
-
type: 'function',
|
|
1306
|
-
function: {
|
|
1307
|
-
name: 'replace_block',
|
|
1308
|
-
description: 'Replace a validated line block using an old_hash guard',
|
|
1309
|
-
parameters: {
|
|
1310
|
-
type: 'object',
|
|
1311
|
-
properties: {
|
|
1312
|
-
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' },
|
|
1313
1699
|
target: { type: 'object' },
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
},
|
|
1320
|
-
{
|
|
1321
|
-
type: 'function',
|
|
1322
|
-
function: {
|
|
1323
|
-
name: 'replace_text',
|
|
1324
|
-
description: 'Replace a unique text fragment in a file',
|
|
1325
|
-
parameters: {
|
|
1326
|
-
type: 'object',
|
|
1327
|
-
properties: {
|
|
1328
|
-
path: { type: 'string' },
|
|
1329
|
-
old_text: { type: 'string' },
|
|
1330
|
-
new_text: { type: 'string' }
|
|
1700
|
+
symbol: { type: 'string' },
|
|
1701
|
+
line: { type: 'number' },
|
|
1702
|
+
edit: { type: 'object' },
|
|
1331
1703
|
},
|
|
1332
|
-
required: ['
|
|
1704
|
+
required: ['file']
|
|
1333
1705
|
}
|
|
1334
1706
|
}
|
|
1335
1707
|
},
|
|
1336
1708
|
{
|
|
1337
1709
|
type: 'function',
|
|
1338
1710
|
function: {
|
|
1339
|
-
name: '
|
|
1340
|
-
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.',
|
|
1341
1714
|
parameters: {
|
|
1342
1715
|
type: 'object',
|
|
1343
1716
|
properties: {
|
|
1344
1717
|
path: { type: 'string' },
|
|
1345
|
-
|
|
1346
|
-
|
|
1718
|
+
content: { type: 'string' },
|
|
1719
|
+
append: { type: 'boolean' },
|
|
1720
|
+
full_file_rewrite: { type: 'boolean' }
|
|
1347
1721
|
},
|
|
1348
|
-
required: ['path', '
|
|
1722
|
+
required: ['path', 'content']
|
|
1349
1723
|
}
|
|
1350
1724
|
}
|
|
1351
1725
|
},
|
|
1352
1726
|
{
|
|
1353
1727
|
type: 'function',
|
|
1354
1728
|
function: {
|
|
1355
|
-
name: '
|
|
1356
|
-
description: '
|
|
1729
|
+
name: 'run',
|
|
1730
|
+
description: 'Primary run tool. Execute a one-shot shell command in workspace. Do not use for long-running services.',
|
|
1357
1731
|
parameters: {
|
|
1358
1732
|
type: 'object',
|
|
1359
1733
|
properties: {
|
|
1360
|
-
|
|
1361
|
-
anchor_text: { type: 'string' },
|
|
1362
|
-
content: { type: 'string' }
|
|
1734
|
+
command: { type: 'string' }
|
|
1363
1735
|
},
|
|
1364
|
-
required: ['
|
|
1736
|
+
required: ['command']
|
|
1365
1737
|
}
|
|
1366
1738
|
}
|
|
1367
1739
|
},
|
|
@@ -1383,50 +1755,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1383
1755
|
{
|
|
1384
1756
|
type: 'function',
|
|
1385
1757
|
function: {
|
|
1386
|
-
name: '
|
|
1387
|
-
description:
|
|
1388
|
-
'Two-phase read: first call returns metadata+read_token; second call with include_content=true and matching read_token returns content',
|
|
1389
|
-
parameters: {
|
|
1390
|
-
type: 'object',
|
|
1391
|
-
properties: {
|
|
1392
|
-
path: { type: 'string' },
|
|
1393
|
-
start_line: { type: 'number' },
|
|
1394
|
-
end_line: { type: 'number' },
|
|
1395
|
-
max_chars: { type: 'number' },
|
|
1396
|
-
include_content: { type: 'boolean' },
|
|
1397
|
-
read_token: { type: 'string' }
|
|
1398
|
-
},
|
|
1399
|
-
required: ['path']
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
},
|
|
1403
|
-
{
|
|
1404
|
-
type: 'function',
|
|
1405
|
-
function: {
|
|
1406
|
-
name: 'write_file',
|
|
1407
|
-
description: 'Write a UTF-8 text file in workspace. Always provide a full file path, not a directory.',
|
|
1408
|
-
parameters: {
|
|
1409
|
-
type: 'object',
|
|
1410
|
-
properties: {
|
|
1411
|
-
path: { type: 'string' },
|
|
1412
|
-
content: { type: 'string' },
|
|
1413
|
-
append: { type: 'boolean' }
|
|
1414
|
-
},
|
|
1415
|
-
required: ['path', 'content']
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
},
|
|
1419
|
-
{
|
|
1420
|
-
type: 'function',
|
|
1421
|
-
function: {
|
|
1422
|
-
name: 'run_command',
|
|
1423
|
-
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',
|
|
1424
1760
|
parameters: {
|
|
1425
1761
|
type: 'object',
|
|
1426
1762
|
properties: {
|
|
1427
|
-
|
|
1763
|
+
patch: { type: 'string' },
|
|
1764
|
+
content: { type: 'string' }
|
|
1428
1765
|
},
|
|
1429
|
-
required: ['
|
|
1766
|
+
required: ['patch']
|
|
1430
1767
|
}
|
|
1431
1768
|
}
|
|
1432
1769
|
},
|
|
@@ -1512,27 +1849,10 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1512
1849
|
}
|
|
1513
1850
|
}
|
|
1514
1851
|
}
|
|
1515
|
-
];
|
|
1852
|
+
].filter(Boolean);
|
|
1516
1853
|
|
|
1517
1854
|
const handlers = {
|
|
1518
|
-
|
|
1519
|
-
open_target: (args) => openTarget(workspaceRoot, args),
|
|
1520
|
-
edit_target: (args) => editTarget(workspaceRoot, args),
|
|
1521
|
-
search_code: (args) => searchCode(workspaceRoot, args),
|
|
1522
|
-
read_block: (args) => readBlock(workspaceRoot, args),
|
|
1523
|
-
read_symbol_context: (args) => readSymbolContext(workspaceRoot, args),
|
|
1524
|
-
validate_edit: (args) => validateEdit(workspaceRoot, args),
|
|
1525
|
-
replace_block: (args) => replaceBlock(workspaceRoot, args),
|
|
1526
|
-
replace_text: (args) => replaceText(workspaceRoot, args),
|
|
1527
|
-
insert_before: (args) => insertRelative(workspaceRoot, args, 'insert_before'),
|
|
1528
|
-
insert_after: (args) => insertRelative(workspaceRoot, args, 'insert_after'),
|
|
1529
|
-
generate_diff: (args) => generateDiff(workspaceRoot, args),
|
|
1530
|
-
start_service: (args) => startService(workspaceRoot, config, args),
|
|
1531
|
-
list_services: () => listServices(workspaceRoot),
|
|
1532
|
-
get_service_status: (args) => getServiceStatus(workspaceRoot, args),
|
|
1533
|
-
get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
|
|
1534
|
-
stop_service: (args) => stopService(workspaceRoot, args),
|
|
1535
|
-
read_file: (args) =>
|
|
1855
|
+
read: (args) =>
|
|
1536
1856
|
readFile(workspaceRoot, {
|
|
1537
1857
|
...args,
|
|
1538
1858
|
default_lines: config.context?.read_file_default_lines ?? 220,
|
|
@@ -1541,8 +1861,19 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1541
1861
|
? args.max_chars
|
|
1542
1862
|
: config.context?.read_file_max_chars ?? 24000
|
|
1543
1863
|
}),
|
|
1544
|
-
|
|
1545
|
-
|
|
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)
|
|
1546
1877
|
};
|
|
1547
1878
|
|
|
1548
1879
|
return { definitions, handlers };
|