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