codemini-cli 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/agent-loop.js +17 -0
- package/src/core/chat-runtime.js +364 -44
- package/src/core/config-store.js +39 -3
- package/src/core/reply-language.js +25 -0
- package/src/core/shell-profile.js +1 -1
- package/src/core/soul.js +3 -1
- package/src/core/tools.js +884 -8
- package/src/tui/chat-app.js +89 -15
package/src/core/tools.js
CHANGED
|
@@ -4,6 +4,49 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { isDangerousCommand, runShellCommand } from './shell.js';
|
|
5
5
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
6
6
|
|
|
7
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.coder', '.codemini-cli', 'dist', 'coverage']);
|
|
8
|
+
const TEXT_EXTENSIONS = new Set([
|
|
9
|
+
'.js',
|
|
10
|
+
'.jsx',
|
|
11
|
+
'.ts',
|
|
12
|
+
'.tsx',
|
|
13
|
+
'.json',
|
|
14
|
+
'.md',
|
|
15
|
+
'.mjs',
|
|
16
|
+
'.cjs',
|
|
17
|
+
'.py',
|
|
18
|
+
'.rb',
|
|
19
|
+
'.go',
|
|
20
|
+
'.rs',
|
|
21
|
+
'.java',
|
|
22
|
+
'.cs',
|
|
23
|
+
'.css',
|
|
24
|
+
'.scss',
|
|
25
|
+
'.html',
|
|
26
|
+
'.yml',
|
|
27
|
+
'.yaml',
|
|
28
|
+
'.sh',
|
|
29
|
+
'.ps1'
|
|
30
|
+
]);
|
|
31
|
+
const LANGUAGE_FILE_TYPES = {
|
|
32
|
+
js: ['js', 'jsx', 'mjs', 'cjs'],
|
|
33
|
+
ts: ['ts', 'tsx'],
|
|
34
|
+
py: ['py'],
|
|
35
|
+
python: ['py'],
|
|
36
|
+
md: ['md'],
|
|
37
|
+
json: ['json'],
|
|
38
|
+
css: ['css', 'scss'],
|
|
39
|
+
html: ['html'],
|
|
40
|
+
java: ['java'],
|
|
41
|
+
csharp: ['cs'],
|
|
42
|
+
cs: ['cs'],
|
|
43
|
+
go: ['go'],
|
|
44
|
+
rust: ['rs'],
|
|
45
|
+
ruby: ['rb'],
|
|
46
|
+
shell: ['sh', 'ps1'],
|
|
47
|
+
yaml: ['yml', 'yaml']
|
|
48
|
+
};
|
|
49
|
+
|
|
7
50
|
function resolveInWorkspace(root, targetPath = '.') {
|
|
8
51
|
const absRoot = path.resolve(root);
|
|
9
52
|
const absTarget = path.resolve(absRoot, targetPath);
|
|
@@ -13,11 +56,337 @@ function resolveInWorkspace(root, targetPath = '.') {
|
|
|
13
56
|
return absTarget;
|
|
14
57
|
}
|
|
15
58
|
|
|
59
|
+
function toWorkspaceRelative(root, absPath) {
|
|
60
|
+
return path.relative(path.resolve(root), absPath).replace(/\\/g, '/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sha256(input) {
|
|
64
|
+
return `sha256:${crypto.createHash('sha256').update(String(input || '')).digest('hex')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sha1(input) {
|
|
68
|
+
return crypto.createHash('sha1').update(String(input || '')).digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trimLinePreview(line, maxLen = 180) {
|
|
72
|
+
const text = String(line || '').replace(/\t/g, ' ').trim();
|
|
73
|
+
if (text.length <= maxLen) return text;
|
|
74
|
+
return `${text.slice(0, maxLen - 3)}...`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function escapeRegex(value) {
|
|
78
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function splitLines(text) {
|
|
82
|
+
return String(text || '').split('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectTextFile(filePath) {
|
|
86
|
+
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeFileTypes(args = {}) {
|
|
90
|
+
const explicit = Array.isArray(args?.file_types) ? args.file_types.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : [];
|
|
91
|
+
const language = String(args?.language || '').trim().toLowerCase();
|
|
92
|
+
const languageTypes = LANGUAGE_FILE_TYPES[language] || [];
|
|
93
|
+
const merged = [...explicit, ...languageTypes];
|
|
94
|
+
return [...new Set(merged)];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
98
|
+
const abs = resolveInWorkspace(root, startPath);
|
|
99
|
+
const out = [];
|
|
100
|
+
const allowedExts = new Set((Array.isArray(fileTypes) ? fileTypes : []).map((item) => `.${String(item || '').replace(/^\./, '')}`));
|
|
101
|
+
|
|
102
|
+
async function visit(current) {
|
|
103
|
+
const stat = await fs.stat(current);
|
|
104
|
+
if (stat.isDirectory()) {
|
|
105
|
+
const name = path.basename(current);
|
|
106
|
+
if (SKIP_DIRS.has(name)) return;
|
|
107
|
+
const entries = await fs.readdir(current);
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
await visit(path.join(current, entry));
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!detectTextFile(current)) return;
|
|
114
|
+
if (allowedExts.size > 0 && !allowedExts.has(path.extname(current).toLowerCase())) return;
|
|
115
|
+
out.push(current);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await visit(abs);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getLineColumnForMatch(line, query, caseSensitive = false) {
|
|
123
|
+
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
124
|
+
const needle = caseSensitive ? query : query.toLowerCase();
|
|
125
|
+
const index = haystack.indexOf(needle);
|
|
126
|
+
return index === -1 ? 1 : index + 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function classifyMatch(preview, query) {
|
|
130
|
+
const line = String(preview || '');
|
|
131
|
+
const escaped = escapeRegex(query);
|
|
132
|
+
const normalized = line.toLowerCase();
|
|
133
|
+
const queryLower = String(query || '').toLowerCase();
|
|
134
|
+
const definitionLeadPatterns = [
|
|
135
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\b/i,
|
|
136
|
+
/^\s*(?:export\s+)?class\b/i,
|
|
137
|
+
/^\s*(?:export\s+)?(?:const|let|var)\b/i,
|
|
138
|
+
/^\s*(?:export\s+)?(?:interface|type|enum)\b/i,
|
|
139
|
+
/^\s*def\b/i,
|
|
140
|
+
/^\s*(?:public|private|protected)\s+[A-Za-z0-9_<>,[\]\s?]+\s+[A-Za-z0-9_$]+\s*\(/i
|
|
141
|
+
];
|
|
142
|
+
if (definitionLeadPatterns.some((pattern) => pattern.test(line)) && normalized.includes(queryLower)) {
|
|
143
|
+
return 'definition';
|
|
144
|
+
}
|
|
145
|
+
if (new RegExp(String.raw`\b${escaped}\s*\(`, 'i').test(line)) return 'reference';
|
|
146
|
+
return 'text';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function matchSpecificity(preview, query) {
|
|
150
|
+
const line = String(preview || '');
|
|
151
|
+
const escaped = escapeRegex(query);
|
|
152
|
+
if (new RegExp(String.raw`\b${escaped}\b`, 'i').test(line)) return 0;
|
|
153
|
+
if (line.toLowerCase().includes(String(query || '').toLowerCase())) return 1;
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findSymbolDefinition(lines, symbol) {
|
|
158
|
+
const escaped = String(symbol || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
159
|
+
const patterns = [
|
|
160
|
+
new RegExp(String.raw`\bfunction\s+${escaped}\b`),
|
|
161
|
+
new RegExp(String.raw`\basync\s+function\s+${escaped}\b`),
|
|
162
|
+
new RegExp(String.raw`\bexport\s+function\s+${escaped}\b`),
|
|
163
|
+
new RegExp(String.raw`\bexport\s+async\s+function\s+${escaped}\b`),
|
|
164
|
+
new RegExp(String.raw`\bclass\s+${escaped}\b`),
|
|
165
|
+
new RegExp(String.raw`\bconst\s+${escaped}\b`),
|
|
166
|
+
new RegExp(String.raw`\blet\s+${escaped}\b`),
|
|
167
|
+
new RegExp(String.raw`\bvar\s+${escaped}\b`)
|
|
168
|
+
];
|
|
169
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
170
|
+
if (patterns.some((pattern) => pattern.test(lines[i]))) {
|
|
171
|
+
return i + 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
175
|
+
if (new RegExp(String.raw`\b${escaped}\b`).test(lines[i])) {
|
|
176
|
+
return i + 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function lineIndentSize(line) {
|
|
183
|
+
const match = String(line || '').match(/^\s*/);
|
|
184
|
+
return match ? match[0].length : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findBlockRange(lines, anchorLine) {
|
|
188
|
+
const total = lines.length;
|
|
189
|
+
const anchorIdx = Math.max(0, Math.min(total - 1, Number(anchorLine || 1) - 1));
|
|
190
|
+
|
|
191
|
+
let start = anchorIdx;
|
|
192
|
+
for (let i = anchorIdx; i >= 0; i -= 1) {
|
|
193
|
+
const line = String(lines[i] || '');
|
|
194
|
+
if (
|
|
195
|
+
/\b(function|class|interface|type|enum|const|let|var|export)\b/.test(line) ||
|
|
196
|
+
/=>\s*{/.test(line) ||
|
|
197
|
+
/<\w/.test(line)
|
|
198
|
+
) {
|
|
199
|
+
start = i;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let braceDepth = 0;
|
|
205
|
+
let seenBrace = false;
|
|
206
|
+
let end = anchorIdx;
|
|
207
|
+
for (let i = start; i < total; i += 1) {
|
|
208
|
+
const line = String(lines[i] || '');
|
|
209
|
+
for (const ch of line) {
|
|
210
|
+
if (ch === '{') {
|
|
211
|
+
braceDepth += 1;
|
|
212
|
+
seenBrace = true;
|
|
213
|
+
} else if (ch === '}') {
|
|
214
|
+
braceDepth -= 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
end = i;
|
|
218
|
+
if (seenBrace && braceDepth <= 0 && i > start) {
|
|
219
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const anchorText = String(lines[start] || '');
|
|
224
|
+
if (/^\s*def\b/.test(anchorText) || /:\s*$/.test(anchorText)) {
|
|
225
|
+
const baseIndent = lineIndentSize(anchorText);
|
|
226
|
+
end = start;
|
|
227
|
+
for (let i = start + 1; i < total; i += 1) {
|
|
228
|
+
const line = String(lines[i] || '');
|
|
229
|
+
if (!line.trim()) break;
|
|
230
|
+
if (lineIndentSize(line) <= baseIndent) break;
|
|
231
|
+
end = i;
|
|
232
|
+
}
|
|
233
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const baseIndent = lineIndentSize(lines[start]);
|
|
237
|
+
end = start;
|
|
238
|
+
for (let i = start + 1; i < total; i += 1) {
|
|
239
|
+
const line = String(lines[i] || '');
|
|
240
|
+
if (!line.trim()) {
|
|
241
|
+
end = i;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (lineIndentSize(line) <= baseIndent && i > anchorIdx) break;
|
|
245
|
+
end = i;
|
|
246
|
+
}
|
|
247
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function extractImports(lines) {
|
|
251
|
+
return lines.filter((line) => /^\s*import\b/.test(String(line || ''))).map((line) => trimLinePreview(line, 220));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function extractImportSignatures(lines, maxItems = 6) {
|
|
255
|
+
const imports = [];
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
const text = String(line || '').trim();
|
|
258
|
+
if (!/^import\b/.test(text)) continue;
|
|
259
|
+
imports.push(trimLinePreview(text, 96));
|
|
260
|
+
if (imports.length >= maxItems) break;
|
|
261
|
+
}
|
|
262
|
+
return imports;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function extractTypeSignatures(lines, maxItems = 6) {
|
|
266
|
+
const out = [];
|
|
267
|
+
const patterns = [
|
|
268
|
+
/^\s*(?:export\s+)?type\s+[A-Za-z0-9_$]+.*$/,
|
|
269
|
+
/^\s*(?:export\s+)?interface\s+[A-Za-z0-9_$]+.*$/,
|
|
270
|
+
/^\s*(?:export\s+)?enum\s+[A-Za-z0-9_$]+.*$/,
|
|
271
|
+
/^\s*(?:export\s+)?class\s+[A-Za-z0-9_$]+.*$/,
|
|
272
|
+
/^\s*import\s+type\b.*$/
|
|
273
|
+
];
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
const text = String(line || '').trim();
|
|
276
|
+
if (!patterns.some((pattern) => pattern.test(text))) continue;
|
|
277
|
+
out.push(trimLinePreview(text, 96));
|
|
278
|
+
if (out.length >= maxItems) break;
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractLocalSymbols(lines, sourceSymbol = '') {
|
|
284
|
+
const out = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
const regex = /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)|^\s*(?:export\s+)?class\s+([A-Za-z0-9_$]+)|^\s*(?:export\s+)?const\s+([A-Za-z0-9_$]+)\s*=/;
|
|
287
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
288
|
+
const match = String(lines[i] || '').match(regex);
|
|
289
|
+
const name = match?.[1] || match?.[2] || match?.[3] || '';
|
|
290
|
+
if (!name || name === sourceSymbol || seen.has(name)) continue;
|
|
291
|
+
seen.add(name);
|
|
292
|
+
out.push({
|
|
293
|
+
name,
|
|
294
|
+
line: i + 1,
|
|
295
|
+
signature: trimLinePreview(lines[i], 220)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return out.slice(0, 8);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function extractDirectCalls(lines, symbol, maxItems = 3, excludeRange = null) {
|
|
302
|
+
const escaped = escapeRegex(symbol);
|
|
303
|
+
const out = [];
|
|
304
|
+
for (let i = 0; i < lines.length && out.length < maxItems; i += 1) {
|
|
305
|
+
if (excludeRange && i + 1 >= excludeRange.startLine && i + 1 <= excludeRange.endLine) continue;
|
|
306
|
+
const line = String(lines[i] || '');
|
|
307
|
+
if (!new RegExp(String.raw`\b${escaped}\s*\(`).test(line)) continue;
|
|
308
|
+
const blockLine = findEnclosingSymbol(lines, i + 1);
|
|
309
|
+
const owner = blockLine ? trimLinePreview(lines[blockLine - 1], 220) : trimLinePreview(line, 220);
|
|
310
|
+
const ownerName = blockLine ? extractSymbolName(lines[blockLine - 1]) : '';
|
|
311
|
+
if (ownerName === symbol) continue;
|
|
312
|
+
out.push({
|
|
313
|
+
symbol: ownerName || '(anonymous)',
|
|
314
|
+
line: blockLine || i + 1,
|
|
315
|
+
preview: owner
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function extractSymbolName(line) {
|
|
322
|
+
const text = String(line || '');
|
|
323
|
+
const match =
|
|
324
|
+
text.match(/\bfunction\s+([A-Za-z0-9_$]+)/) ||
|
|
325
|
+
text.match(/\bclass\s+([A-Za-z0-9_$]+)/) ||
|
|
326
|
+
text.match(/\bconst\s+([A-Za-z0-9_$]+)\s*=/) ||
|
|
327
|
+
text.match(/^\s*def\s+([A-Za-z0-9_]+)/);
|
|
328
|
+
return match?.[1] || '';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findEnclosingSymbol(lines, anchorLine) {
|
|
332
|
+
for (let i = Math.max(0, anchorLine - 1); i >= 0; i -= 1) {
|
|
333
|
+
const name = extractSymbolName(lines[i]);
|
|
334
|
+
if (name) return i + 1;
|
|
335
|
+
}
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
|
|
340
|
+
const oldLines = splitLines(oldContent);
|
|
341
|
+
const newLines = splitLines(newContent);
|
|
342
|
+
let prefix = 0;
|
|
343
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
|
|
344
|
+
prefix += 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let suffix = 0;
|
|
348
|
+
while (
|
|
349
|
+
suffix < oldLines.length - prefix &&
|
|
350
|
+
suffix < newLines.length - prefix &&
|
|
351
|
+
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
352
|
+
) {
|
|
353
|
+
suffix += 1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const oldChanged = oldLines.slice(prefix, oldLines.length - suffix);
|
|
357
|
+
const newChanged = newLines.slice(prefix, newLines.length - suffix);
|
|
358
|
+
const oldStart = prefix + 1;
|
|
359
|
+
const newStart = prefix + 1;
|
|
360
|
+
const oldCount = Math.max(1, oldChanged.length);
|
|
361
|
+
const newCount = Math.max(1, newChanged.length);
|
|
362
|
+
|
|
363
|
+
const body = [
|
|
364
|
+
`--- ${filePath}`,
|
|
365
|
+
`+++ ${filePath}`,
|
|
366
|
+
`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
|
|
367
|
+
...oldChanged.map((line) => `-${line}`),
|
|
368
|
+
...newChanged.map((line) => `+${line}`)
|
|
369
|
+
];
|
|
370
|
+
return body.join('\n');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function getFileState(root, relativePath) {
|
|
374
|
+
const target = resolveInWorkspace(root, relativePath);
|
|
375
|
+
const stat = await fs.stat(target);
|
|
376
|
+
const content = await fs.readFile(target, 'utf8');
|
|
377
|
+
return {
|
|
378
|
+
target,
|
|
379
|
+
content,
|
|
380
|
+
lines: splitLines(content),
|
|
381
|
+
stat
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
16
385
|
async function readFile(root, args) {
|
|
17
386
|
const target = resolveInWorkspace(root, args?.path);
|
|
18
387
|
const stat = await fs.stat(target);
|
|
19
388
|
const text = await fs.readFile(target, 'utf8');
|
|
20
|
-
const lines = text
|
|
389
|
+
const lines = splitLines(text);
|
|
21
390
|
const totalLines = lines.length;
|
|
22
391
|
const startLineRaw = Number(args?.start_line);
|
|
23
392
|
const endLineRaw = Number(args?.end_line);
|
|
@@ -34,7 +403,7 @@ async function readFile(root, args) {
|
|
|
34
403
|
endLine = Math.max(startLine, Math.min(endLine, totalLines));
|
|
35
404
|
|
|
36
405
|
const tokenSeed = `${args?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
|
|
37
|
-
const readToken =
|
|
406
|
+
const readToken = sha1(tokenSeed).slice(0, 16);
|
|
38
407
|
|
|
39
408
|
if (!includeContent) {
|
|
40
409
|
return {
|
|
@@ -98,9 +467,7 @@ async function writeFile(root, args) {
|
|
|
98
467
|
throw new Error(`write_file target is a directory: ${rawPath}`);
|
|
99
468
|
}
|
|
100
469
|
} catch (error) {
|
|
101
|
-
if (error?.code && error.code !== 'ENOENT')
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
470
|
+
if (error?.code && error.code !== 'ENOENT') throw error;
|
|
104
471
|
}
|
|
105
472
|
let before = '';
|
|
106
473
|
let existed = true;
|
|
@@ -116,8 +483,8 @@ async function writeFile(root, args) {
|
|
|
116
483
|
await fs.writeFile(target, args?.content || '', 'utf8');
|
|
117
484
|
}
|
|
118
485
|
const after = args?.append ? `${before}${args?.content || ''}` : args?.content || '';
|
|
119
|
-
const beforeLines = before
|
|
120
|
-
const afterLines = after
|
|
486
|
+
const beforeLines = splitLines(before);
|
|
487
|
+
const afterLines = splitLines(after);
|
|
121
488
|
let changeLine = 0;
|
|
122
489
|
const scanMax = Math.max(beforeLines.length, afterLines.length);
|
|
123
490
|
for (let i = 0; i < scanMax; i += 1) {
|
|
@@ -165,8 +532,505 @@ async function runCommand(root, config, args) {
|
|
|
165
532
|
return { ...result, command };
|
|
166
533
|
}
|
|
167
534
|
|
|
168
|
-
|
|
535
|
+
async function searchCode(root, args) {
|
|
536
|
+
const query = String(args?.query || args?.symbol || '').trim();
|
|
537
|
+
if (!query) throw new Error('search_code requires query');
|
|
538
|
+
const maxResults = Math.max(1, Math.min(50, Number(args?.max_results || 12)));
|
|
539
|
+
const caseSensitive = Boolean(args?.case_sensitive);
|
|
540
|
+
const files = await walkTextFiles(root, args?.path || '.', normalizeFileTypes(args));
|
|
541
|
+
const matches = [];
|
|
542
|
+
|
|
543
|
+
for (const filePath of files) {
|
|
544
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
545
|
+
const lines = splitLines(content);
|
|
546
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
547
|
+
const line = lines[idx];
|
|
548
|
+
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
549
|
+
const needle = caseSensitive ? query : query.toLowerCase();
|
|
550
|
+
if (!haystack.includes(needle)) continue;
|
|
551
|
+
matches.push({
|
|
552
|
+
file: toWorkspaceRelative(root, filePath),
|
|
553
|
+
line: idx + 1,
|
|
554
|
+
column: getLineColumnForMatch(line, query, caseSensitive),
|
|
555
|
+
preview: trimLinePreview(line),
|
|
556
|
+
kind: classifyMatch(line, query),
|
|
557
|
+
symbolHint: query
|
|
558
|
+
});
|
|
559
|
+
if (matches.length >= maxResults) {
|
|
560
|
+
matches.sort((left, right) => {
|
|
561
|
+
const kindRank = { definition: 0, reference: 1, text: 2 };
|
|
562
|
+
const specificity = matchSpecificity(left.preview, query) - matchSpecificity(right.preview, query);
|
|
563
|
+
if (specificity !== 0) return specificity;
|
|
564
|
+
if (kindRank[left.kind] !== kindRank[right.kind]) return kindRank[left.kind] - kindRank[right.kind];
|
|
565
|
+
return left.file.localeCompare(right.file) || left.line - right.line;
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
query,
|
|
569
|
+
matches,
|
|
570
|
+
definitions: matches.filter((item) => item.kind === 'definition'),
|
|
571
|
+
references: matches.filter((item) => item.kind === 'reference'),
|
|
572
|
+
text_matches: matches.filter((item) => item.kind === 'text'),
|
|
573
|
+
truncated: true
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
matches.sort((left, right) => {
|
|
580
|
+
const kindRank = { definition: 0, reference: 1, text: 2 };
|
|
581
|
+
const specificity = matchSpecificity(left.preview, query) - matchSpecificity(right.preview, query);
|
|
582
|
+
if (specificity !== 0) return specificity;
|
|
583
|
+
if (kindRank[left.kind] !== kindRank[right.kind]) return kindRank[left.kind] - kindRank[right.kind];
|
|
584
|
+
return left.file.localeCompare(right.file) || left.line - right.line;
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
query,
|
|
589
|
+
matches,
|
|
590
|
+
definitions: matches.filter((item) => item.kind === 'definition'),
|
|
591
|
+
references: matches.filter((item) => item.kind === 'reference'),
|
|
592
|
+
text_matches: matches.filter((item) => item.kind === 'text'),
|
|
593
|
+
truncated: false
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function readBlock(root, args) {
|
|
598
|
+
const relativePath = String(args?.path || '').trim();
|
|
599
|
+
if (!relativePath) throw new Error('read_block requires path');
|
|
600
|
+
const { lines } = await getFileState(root, relativePath);
|
|
601
|
+
const symbol = String(args?.symbol || '').trim();
|
|
602
|
+
const anchorLine = symbol ? findSymbolDefinition(lines, symbol) : Number(args?.line || args?.anchor_line || 1);
|
|
603
|
+
const range = findBlockRange(lines, anchorLine);
|
|
604
|
+
return {
|
|
605
|
+
file: relativePath,
|
|
606
|
+
symbol: symbol || undefined,
|
|
607
|
+
mode: symbol ? 'symbol' : 'block',
|
|
608
|
+
start_line: range.startLine,
|
|
609
|
+
end_line: range.endLine,
|
|
610
|
+
content: lines.slice(range.startLine - 1, range.endLine).join('\n')
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function readSymbolContext(root, args) {
|
|
615
|
+
const relativePath = String(args?.path || '').trim();
|
|
616
|
+
const symbol = String(args?.symbol || '').trim();
|
|
617
|
+
if (!relativePath || !symbol) throw new Error('read_symbol_context requires path and symbol');
|
|
618
|
+
const { lines } = await getFileState(root, relativePath);
|
|
619
|
+
const mainBlock = await readBlock(root, { path: relativePath, symbol });
|
|
620
|
+
return {
|
|
621
|
+
file: relativePath,
|
|
622
|
+
symbol,
|
|
623
|
+
main_block: mainBlock,
|
|
624
|
+
related: {
|
|
625
|
+
imports: extractImports(lines),
|
|
626
|
+
import_signatures: extractImportSignatures(lines, Number(args?.max_related_imports || 4)),
|
|
627
|
+
type_signatures: extractTypeSignatures(lines, Number(args?.max_related_types || 4)),
|
|
628
|
+
local_symbols: extractLocalSymbols(lines, symbol),
|
|
629
|
+
calls: extractDirectCalls(lines, symbol, Number(args?.max_related_calls || 3), {
|
|
630
|
+
startLine: mainBlock.start_line,
|
|
631
|
+
endLine: mainBlock.end_line
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function validateEdit(root, args) {
|
|
638
|
+
const relativePath = String(args?.path || '').trim();
|
|
639
|
+
const kind = String(args?.kind || '').trim();
|
|
640
|
+
if (!relativePath || !kind) throw new Error('validate_edit requires path and kind');
|
|
641
|
+
const { content, lines } = await getFileState(root, relativePath);
|
|
642
|
+
|
|
643
|
+
if (kind === 'replace_block') {
|
|
644
|
+
const startLine = Number(args?.target?.start_line || args?.start_line);
|
|
645
|
+
const endLine = Number(args?.target?.end_line || args?.end_line);
|
|
646
|
+
if (!Number.isFinite(startLine) || !Number.isFinite(endLine) || startLine <= 0 || endLine < startLine) {
|
|
647
|
+
throw new Error('replace_block validation requires target.start_line and target.end_line');
|
|
648
|
+
}
|
|
649
|
+
const oldBlock = lines.slice(startLine - 1, endLine).join('\n');
|
|
650
|
+
return {
|
|
651
|
+
ok: true,
|
|
652
|
+
path: relativePath,
|
|
653
|
+
kind,
|
|
654
|
+
target: {
|
|
655
|
+
start_line: startLine,
|
|
656
|
+
end_line: endLine,
|
|
657
|
+
old_hash: sha256(oldBlock)
|
|
658
|
+
},
|
|
659
|
+
file_hash: sha256(content)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (kind === 'replace_text' || kind === 'insert_before' || kind === 'insert_after') {
|
|
664
|
+
const probe = String(args?.old_text || args?.anchor_text || '');
|
|
665
|
+
if (!probe) throw new Error(`${kind} validation requires old_text or anchor_text`);
|
|
666
|
+
const occurrences = content.split(probe).length - 1;
|
|
667
|
+
return {
|
|
668
|
+
ok: occurrences === 1,
|
|
669
|
+
path: relativePath,
|
|
670
|
+
kind,
|
|
671
|
+
occurrences,
|
|
672
|
+
reason: occurrences === 1 ? 'unique match' : occurrences === 0 ? 'anchor not found' : 'anchor not unique',
|
|
673
|
+
file_hash: sha256(content)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
throw new Error(`validate_edit does not support kind: ${kind}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
|
|
681
|
+
const afterLines = splitLines(afterContent);
|
|
682
|
+
const previewStart = Math.max(0, changedLine - 1);
|
|
683
|
+
const diffPreview = afterLines.slice(previewStart, previewStart + 6).map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n');
|
|
684
|
+
return {
|
|
685
|
+
ok: true,
|
|
686
|
+
path: pathText,
|
|
687
|
+
action,
|
|
688
|
+
changed_line: changedLine,
|
|
689
|
+
diff_preview: diffPreview,
|
|
690
|
+
diff: buildUnifiedDiff(beforeContent, afterContent, pathText),
|
|
691
|
+
new_hash: sha256(afterContent)
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function replaceBlock(root, args) {
|
|
696
|
+
const relativePath = String(args?.path || '').trim();
|
|
697
|
+
const newContent = String(args?.new_content || args?.content || '');
|
|
698
|
+
const target = args?.target || {};
|
|
699
|
+
const startLine = Number(target.start_line);
|
|
700
|
+
const endLine = Number(target.end_line);
|
|
701
|
+
const oldHash = String(target.old_hash || '');
|
|
702
|
+
const state = await getFileState(root, relativePath);
|
|
703
|
+
const oldBlock = state.lines.slice(startLine - 1, endLine).join('\n');
|
|
704
|
+
if (!oldHash || oldHash !== sha256(oldBlock)) {
|
|
705
|
+
throw new Error('replace_block old_hash mismatch');
|
|
706
|
+
}
|
|
707
|
+
const nextLines = [...state.lines.slice(0, startLine - 1), ...splitLines(newContent), ...state.lines.slice(endLine)];
|
|
708
|
+
const afterContent = nextLines.join('\n');
|
|
709
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
710
|
+
return editResult(relativePath, 'replace_block', state.content, afterContent, startLine);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function replaceText(root, args) {
|
|
714
|
+
const relativePath = String(args?.path || '').trim();
|
|
715
|
+
const oldText = String(args?.old_text || '');
|
|
716
|
+
const newText = String(args?.new_text || '');
|
|
717
|
+
const state = await getFileState(root, relativePath);
|
|
718
|
+
const occurrences = state.content.split(oldText).length - 1;
|
|
719
|
+
if (occurrences !== 1) {
|
|
720
|
+
throw new Error(occurrences === 0 ? 'replace_text old_text not found' : 'replace_text old_text not unique');
|
|
721
|
+
}
|
|
722
|
+
const afterContent = state.content.replace(oldText, newText);
|
|
723
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
724
|
+
const changedLine = splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
725
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function insertRelative(root, args, mode) {
|
|
729
|
+
const relativePath = String(args?.path || '').trim();
|
|
730
|
+
const anchorText = String(args?.anchor_text || '');
|
|
731
|
+
const content = String(args?.content || '');
|
|
732
|
+
const state = await getFileState(root, relativePath);
|
|
733
|
+
const occurrences = state.content.split(anchorText).length - 1;
|
|
734
|
+
if (occurrences !== 1) {
|
|
735
|
+
throw new Error(occurrences === 0 ? `${mode} anchor not found` : `${mode} anchor not unique`);
|
|
736
|
+
}
|
|
737
|
+
const replacement = mode === 'insert_before' ? `${content}${anchorText}` : `${anchorText}${content}`;
|
|
738
|
+
const afterContent = state.content.replace(anchorText, replacement);
|
|
739
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
740
|
+
const changedLine = splitLines(state.content.slice(0, state.content.indexOf(anchorText))).length;
|
|
741
|
+
return editResult(relativePath, mode, state.content, afterContent, changedLine);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function generateDiff(root, args) {
|
|
745
|
+
const relativePath = String(args?.path || '').trim();
|
|
746
|
+
if (!relativePath) throw new Error('generate_diff requires path');
|
|
747
|
+
const state = await getFileState(root, relativePath);
|
|
748
|
+
const newContent = String(args?.new_content || '');
|
|
749
|
+
return {
|
|
750
|
+
path: relativePath,
|
|
751
|
+
old_hash: sha256(state.content),
|
|
752
|
+
new_hash: sha256(newContent),
|
|
753
|
+
diff: buildUnifiedDiff(state.content, newContent, relativePath)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function locate(root, args) {
|
|
758
|
+
const result = await searchCode(root, args);
|
|
759
|
+
return {
|
|
760
|
+
query: result.query,
|
|
761
|
+
matches: result.matches,
|
|
762
|
+
definitions: result.definitions,
|
|
763
|
+
references: result.references,
|
|
764
|
+
text_matches: result.text_matches,
|
|
765
|
+
truncated: result.truncated
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function openTarget(root, args) {
|
|
770
|
+
const file = String(args?.file || args?.path || '').trim();
|
|
771
|
+
if (!file) throw new Error('open_target requires file');
|
|
772
|
+
const symbol = String(args?.symbol || '').trim();
|
|
773
|
+
const line = Number(args?.line || 1);
|
|
774
|
+
const mainBlock = symbol
|
|
775
|
+
? await readSymbolContext(root, {
|
|
776
|
+
path: file,
|
|
777
|
+
symbol,
|
|
778
|
+
max_related_calls: args?.max_related_calls,
|
|
779
|
+
max_related_imports: args?.max_related_imports,
|
|
780
|
+
max_related_types: args?.max_related_types
|
|
781
|
+
})
|
|
782
|
+
: { file, symbol: '', main_block: await readBlock(root, { path: file, line }), related: { imports: [], local_symbols: [] } };
|
|
783
|
+
const block = mainBlock.main_block || mainBlock;
|
|
784
|
+
return {
|
|
785
|
+
file,
|
|
786
|
+
symbol: symbol || undefined,
|
|
787
|
+
main_block: block,
|
|
788
|
+
related: mainBlock.related || { imports: [], local_symbols: [] },
|
|
789
|
+
edit_target: {
|
|
790
|
+
start_line: block.start_line,
|
|
791
|
+
end_line: block.end_line,
|
|
792
|
+
old_hash: sha256(block.content)
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function editTarget(root, args) {
|
|
798
|
+
const file = String(args?.file || args?.path || '').trim();
|
|
799
|
+
const edit = args?.edit || {};
|
|
800
|
+
const kind = String(edit.kind || '').trim();
|
|
801
|
+
if (!file || !kind) throw new Error('edit_target requires file and edit.kind');
|
|
802
|
+
if (kind === 'replace_block') {
|
|
803
|
+
return replaceBlock(root, {
|
|
804
|
+
path: file,
|
|
805
|
+
target: edit.target,
|
|
806
|
+
new_content: edit.new_content
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (kind === 'replace_text') {
|
|
810
|
+
return replaceText(root, {
|
|
811
|
+
path: file,
|
|
812
|
+
old_text: edit.old_text,
|
|
813
|
+
new_text: edit.new_text
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (kind === 'insert_before') {
|
|
817
|
+
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_before');
|
|
818
|
+
}
|
|
819
|
+
if (kind === 'insert_after') {
|
|
820
|
+
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_after');
|
|
821
|
+
}
|
|
822
|
+
throw new Error(`edit_target does not support kind: ${kind}`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
169
826
|
const definitions = [
|
|
827
|
+
{
|
|
828
|
+
type: 'function',
|
|
829
|
+
function: {
|
|
830
|
+
name: 'locate',
|
|
831
|
+
description: 'High-level search that returns compact candidate code locations',
|
|
832
|
+
parameters: {
|
|
833
|
+
type: 'object',
|
|
834
|
+
properties: {
|
|
835
|
+
query: { type: 'string' },
|
|
836
|
+
path: { type: 'string' },
|
|
837
|
+
max_results: { type: 'number' },
|
|
838
|
+
language: { type: 'string' },
|
|
839
|
+
file_types: { type: 'array', items: { type: 'string' } }
|
|
840
|
+
},
|
|
841
|
+
required: ['query']
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
type: 'function',
|
|
847
|
+
function: {
|
|
848
|
+
name: 'open_target',
|
|
849
|
+
description: 'Open a candidate location and return the smallest useful code block plus edit metadata',
|
|
850
|
+
parameters: {
|
|
851
|
+
type: 'object',
|
|
852
|
+
properties: {
|
|
853
|
+
file: { type: 'string' },
|
|
854
|
+
path: { type: 'string' },
|
|
855
|
+
line: { type: 'number' },
|
|
856
|
+
symbol: { type: 'string' },
|
|
857
|
+
max_related_calls: { type: 'number' },
|
|
858
|
+
max_related_imports: { type: 'number' },
|
|
859
|
+
max_related_types: { type: 'number' }
|
|
860
|
+
},
|
|
861
|
+
required: ['file']
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
type: 'function',
|
|
867
|
+
function: {
|
|
868
|
+
name: 'edit_target',
|
|
869
|
+
description: 'Apply a validated high-level edit against an opened target',
|
|
870
|
+
parameters: {
|
|
871
|
+
type: 'object',
|
|
872
|
+
properties: {
|
|
873
|
+
file: { type: 'string' },
|
|
874
|
+
path: { type: 'string' },
|
|
875
|
+
edit: { type: 'object' }
|
|
876
|
+
},
|
|
877
|
+
required: ['file', 'edit']
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
type: 'function',
|
|
883
|
+
function: {
|
|
884
|
+
name: 'search_code',
|
|
885
|
+
description: 'Search code and return structured top matches with file, line, preview, and basic match kind',
|
|
886
|
+
parameters: {
|
|
887
|
+
type: 'object',
|
|
888
|
+
properties: {
|
|
889
|
+
query: { type: 'string' },
|
|
890
|
+
path: { type: 'string' },
|
|
891
|
+
max_results: { type: 'number' },
|
|
892
|
+
case_sensitive: { type: 'boolean' },
|
|
893
|
+
language: { type: 'string' },
|
|
894
|
+
file_types: { type: 'array', items: { type: 'string' } }
|
|
895
|
+
},
|
|
896
|
+
required: ['query']
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
type: 'function',
|
|
902
|
+
function: {
|
|
903
|
+
name: 'read_block',
|
|
904
|
+
description: 'Read the smallest likely code block around a symbol or line from a file',
|
|
905
|
+
parameters: {
|
|
906
|
+
type: 'object',
|
|
907
|
+
properties: {
|
|
908
|
+
path: { type: 'string' },
|
|
909
|
+
symbol: { type: 'string' },
|
|
910
|
+
line: { type: 'number' },
|
|
911
|
+
anchor_line: { type: 'number' }
|
|
912
|
+
},
|
|
913
|
+
required: ['path']
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
type: 'function',
|
|
919
|
+
function: {
|
|
920
|
+
name: 'read_symbol_context',
|
|
921
|
+
description: 'Read a symbol block plus import and local symbol summaries',
|
|
922
|
+
parameters: {
|
|
923
|
+
type: 'object',
|
|
924
|
+
properties: {
|
|
925
|
+
path: { type: 'string' },
|
|
926
|
+
symbol: { type: 'string' },
|
|
927
|
+
max_related_calls: { type: 'number' },
|
|
928
|
+
max_related_imports: { type: 'number' },
|
|
929
|
+
max_related_types: { type: 'number' }
|
|
930
|
+
},
|
|
931
|
+
required: ['path', 'symbol']
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
type: 'function',
|
|
937
|
+
function: {
|
|
938
|
+
name: 'validate_edit',
|
|
939
|
+
description: 'Validate whether an edit target is stable before applying it',
|
|
940
|
+
parameters: {
|
|
941
|
+
type: 'object',
|
|
942
|
+
properties: {
|
|
943
|
+
path: { type: 'string' },
|
|
944
|
+
kind: { type: 'string' },
|
|
945
|
+
target: { type: 'object' },
|
|
946
|
+
start_line: { type: 'number' },
|
|
947
|
+
end_line: { type: 'number' },
|
|
948
|
+
old_text: { type: 'string' },
|
|
949
|
+
anchor_text: { type: 'string' }
|
|
950
|
+
},
|
|
951
|
+
required: ['path', 'kind']
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
type: 'function',
|
|
957
|
+
function: {
|
|
958
|
+
name: 'replace_block',
|
|
959
|
+
description: 'Replace a validated line block using an old_hash guard',
|
|
960
|
+
parameters: {
|
|
961
|
+
type: 'object',
|
|
962
|
+
properties: {
|
|
963
|
+
path: { type: 'string' },
|
|
964
|
+
target: { type: 'object' },
|
|
965
|
+
new_content: { type: 'string' }
|
|
966
|
+
},
|
|
967
|
+
required: ['path', 'target', 'new_content']
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
type: 'function',
|
|
973
|
+
function: {
|
|
974
|
+
name: 'replace_text',
|
|
975
|
+
description: 'Replace a unique text fragment in a file',
|
|
976
|
+
parameters: {
|
|
977
|
+
type: 'object',
|
|
978
|
+
properties: {
|
|
979
|
+
path: { type: 'string' },
|
|
980
|
+
old_text: { type: 'string' },
|
|
981
|
+
new_text: { type: 'string' }
|
|
982
|
+
},
|
|
983
|
+
required: ['path', 'old_text', 'new_text']
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
type: 'function',
|
|
989
|
+
function: {
|
|
990
|
+
name: 'insert_before',
|
|
991
|
+
description: 'Insert text before a unique anchor string',
|
|
992
|
+
parameters: {
|
|
993
|
+
type: 'object',
|
|
994
|
+
properties: {
|
|
995
|
+
path: { type: 'string' },
|
|
996
|
+
anchor_text: { type: 'string' },
|
|
997
|
+
content: { type: 'string' }
|
|
998
|
+
},
|
|
999
|
+
required: ['path', 'anchor_text', 'content']
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
type: 'function',
|
|
1005
|
+
function: {
|
|
1006
|
+
name: 'insert_after',
|
|
1007
|
+
description: 'Insert text after a unique anchor string',
|
|
1008
|
+
parameters: {
|
|
1009
|
+
type: 'object',
|
|
1010
|
+
properties: {
|
|
1011
|
+
path: { type: 'string' },
|
|
1012
|
+
anchor_text: { type: 'string' },
|
|
1013
|
+
content: { type: 'string' }
|
|
1014
|
+
},
|
|
1015
|
+
required: ['path', 'anchor_text', 'content']
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
type: 'function',
|
|
1021
|
+
function: {
|
|
1022
|
+
name: 'generate_diff',
|
|
1023
|
+
description: 'Generate a unified diff between the current file and proposed content',
|
|
1024
|
+
parameters: {
|
|
1025
|
+
type: 'object',
|
|
1026
|
+
properties: {
|
|
1027
|
+
path: { type: 'string' },
|
|
1028
|
+
new_content: { type: 'string' }
|
|
1029
|
+
},
|
|
1030
|
+
required: ['path', 'new_content']
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
170
1034
|
{
|
|
171
1035
|
type: 'function',
|
|
172
1036
|
function: {
|
|
@@ -220,6 +1084,18 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, session
|
|
|
220
1084
|
];
|
|
221
1085
|
|
|
222
1086
|
const handlers = {
|
|
1087
|
+
locate: (args) => locate(workspaceRoot, args),
|
|
1088
|
+
open_target: (args) => openTarget(workspaceRoot, args),
|
|
1089
|
+
edit_target: (args) => editTarget(workspaceRoot, args),
|
|
1090
|
+
search_code: (args) => searchCode(workspaceRoot, args),
|
|
1091
|
+
read_block: (args) => readBlock(workspaceRoot, args),
|
|
1092
|
+
read_symbol_context: (args) => readSymbolContext(workspaceRoot, args),
|
|
1093
|
+
validate_edit: (args) => validateEdit(workspaceRoot, args),
|
|
1094
|
+
replace_block: (args) => replaceBlock(workspaceRoot, args),
|
|
1095
|
+
replace_text: (args) => replaceText(workspaceRoot, args),
|
|
1096
|
+
insert_before: (args) => insertRelative(workspaceRoot, args, 'insert_before'),
|
|
1097
|
+
insert_after: (args) => insertRelative(workspaceRoot, args, 'insert_after'),
|
|
1098
|
+
generate_diff: (args) => generateDiff(workspaceRoot, args),
|
|
223
1099
|
read_file: (args) =>
|
|
224
1100
|
readFile(workspaceRoot, {
|
|
225
1101
|
...args,
|