codemini-cli 0.1.12 → 0.1.14
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 +58 -0
- package/package.json +1 -1
- package/src/core/agent-loop.js +46 -0
- package/src/core/chat-runtime.js +364 -44
- package/src/core/config-store.js +49 -3
- package/src/core/reply-language.js +25 -0
- package/src/core/shell-profile.js +1 -1
- package/src/core/shell.js +135 -9
- package/src/core/soul.js +3 -1
- package/src/core/tools.js +1284 -10
- package/src/tui/chat-app.js +295 -36
package/src/core/tools.js
CHANGED
|
@@ -1,9 +1,66 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import {
|
|
7
|
+
hasReadyOutput,
|
|
8
|
+
isDangerousCommand,
|
|
9
|
+
isLikelyLongRunningCommand,
|
|
10
|
+
resolveShell,
|
|
11
|
+
runShellCommand,
|
|
12
|
+
terminateChild
|
|
13
|
+
} from './shell.js';
|
|
5
14
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
6
15
|
|
|
16
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.coder', '.codemini-cli', 'dist', 'coverage']);
|
|
17
|
+
const TEXT_EXTENSIONS = new Set([
|
|
18
|
+
'.js',
|
|
19
|
+
'.jsx',
|
|
20
|
+
'.ts',
|
|
21
|
+
'.tsx',
|
|
22
|
+
'.json',
|
|
23
|
+
'.md',
|
|
24
|
+
'.mjs',
|
|
25
|
+
'.cjs',
|
|
26
|
+
'.py',
|
|
27
|
+
'.rb',
|
|
28
|
+
'.go',
|
|
29
|
+
'.rs',
|
|
30
|
+
'.java',
|
|
31
|
+
'.cs',
|
|
32
|
+
'.css',
|
|
33
|
+
'.scss',
|
|
34
|
+
'.html',
|
|
35
|
+
'.yml',
|
|
36
|
+
'.yaml',
|
|
37
|
+
'.sh',
|
|
38
|
+
'.ps1'
|
|
39
|
+
]);
|
|
40
|
+
const LANGUAGE_FILE_TYPES = {
|
|
41
|
+
js: ['js', 'jsx', 'mjs', 'cjs'],
|
|
42
|
+
ts: ['ts', 'tsx'],
|
|
43
|
+
py: ['py'],
|
|
44
|
+
python: ['py'],
|
|
45
|
+
md: ['md'],
|
|
46
|
+
json: ['json'],
|
|
47
|
+
css: ['css', 'scss'],
|
|
48
|
+
html: ['html'],
|
|
49
|
+
java: ['java'],
|
|
50
|
+
csharp: ['cs'],
|
|
51
|
+
cs: ['cs'],
|
|
52
|
+
go: ['go'],
|
|
53
|
+
rust: ['rs'],
|
|
54
|
+
ruby: ['rb'],
|
|
55
|
+
shell: ['sh', 'ps1'],
|
|
56
|
+
yaml: ['yml', 'yaml']
|
|
57
|
+
};
|
|
58
|
+
const SERVICE_RECENT_LOG_LIMIT = 80;
|
|
59
|
+
const SERVICE_STARTUP_POLL_MS = 150;
|
|
60
|
+
const serviceRegistry = new Map();
|
|
61
|
+
let serviceCounter = 0;
|
|
62
|
+
let serviceLogCursorCounter = 0;
|
|
63
|
+
|
|
7
64
|
function resolveInWorkspace(root, targetPath = '.') {
|
|
8
65
|
const absRoot = path.resolve(root);
|
|
9
66
|
const absTarget = path.resolve(absRoot, targetPath);
|
|
@@ -13,11 +70,337 @@ function resolveInWorkspace(root, targetPath = '.') {
|
|
|
13
70
|
return absTarget;
|
|
14
71
|
}
|
|
15
72
|
|
|
73
|
+
function toWorkspaceRelative(root, absPath) {
|
|
74
|
+
return path.relative(path.resolve(root), absPath).replace(/\\/g, '/');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sha256(input) {
|
|
78
|
+
return `sha256:${crypto.createHash('sha256').update(String(input || '')).digest('hex')}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sha1(input) {
|
|
82
|
+
return crypto.createHash('sha1').update(String(input || '')).digest('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function trimLinePreview(line, maxLen = 180) {
|
|
86
|
+
const text = String(line || '').replace(/\t/g, ' ').trim();
|
|
87
|
+
if (text.length <= maxLen) return text;
|
|
88
|
+
return `${text.slice(0, maxLen - 3)}...`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function escapeRegex(value) {
|
|
92
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function splitLines(text) {
|
|
96
|
+
return String(text || '').split('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function detectTextFile(filePath) {
|
|
100
|
+
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeFileTypes(args = {}) {
|
|
104
|
+
const explicit = Array.isArray(args?.file_types) ? args.file_types.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : [];
|
|
105
|
+
const language = String(args?.language || '').trim().toLowerCase();
|
|
106
|
+
const languageTypes = LANGUAGE_FILE_TYPES[language] || [];
|
|
107
|
+
const merged = [...explicit, ...languageTypes];
|
|
108
|
+
return [...new Set(merged)];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
112
|
+
const abs = resolveInWorkspace(root, startPath);
|
|
113
|
+
const out = [];
|
|
114
|
+
const allowedExts = new Set((Array.isArray(fileTypes) ? fileTypes : []).map((item) => `.${String(item || '').replace(/^\./, '')}`));
|
|
115
|
+
|
|
116
|
+
async function visit(current) {
|
|
117
|
+
const stat = await fs.stat(current);
|
|
118
|
+
if (stat.isDirectory()) {
|
|
119
|
+
const name = path.basename(current);
|
|
120
|
+
if (SKIP_DIRS.has(name)) return;
|
|
121
|
+
const entries = await fs.readdir(current);
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
await visit(path.join(current, entry));
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!detectTextFile(current)) return;
|
|
128
|
+
if (allowedExts.size > 0 && !allowedExts.has(path.extname(current).toLowerCase())) return;
|
|
129
|
+
out.push(current);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await visit(abs);
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getLineColumnForMatch(line, query, caseSensitive = false) {
|
|
137
|
+
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
138
|
+
const needle = caseSensitive ? query : query.toLowerCase();
|
|
139
|
+
const index = haystack.indexOf(needle);
|
|
140
|
+
return index === -1 ? 1 : index + 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function classifyMatch(preview, query) {
|
|
144
|
+
const line = String(preview || '');
|
|
145
|
+
const escaped = escapeRegex(query);
|
|
146
|
+
const normalized = line.toLowerCase();
|
|
147
|
+
const queryLower = String(query || '').toLowerCase();
|
|
148
|
+
const definitionLeadPatterns = [
|
|
149
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\b/i,
|
|
150
|
+
/^\s*(?:export\s+)?class\b/i,
|
|
151
|
+
/^\s*(?:export\s+)?(?:const|let|var)\b/i,
|
|
152
|
+
/^\s*(?:export\s+)?(?:interface|type|enum)\b/i,
|
|
153
|
+
/^\s*def\b/i,
|
|
154
|
+
/^\s*(?:public|private|protected)\s+[A-Za-z0-9_<>,[\]\s?]+\s+[A-Za-z0-9_$]+\s*\(/i
|
|
155
|
+
];
|
|
156
|
+
if (definitionLeadPatterns.some((pattern) => pattern.test(line)) && normalized.includes(queryLower)) {
|
|
157
|
+
return 'definition';
|
|
158
|
+
}
|
|
159
|
+
if (new RegExp(String.raw`\b${escaped}\s*\(`, 'i').test(line)) return 'reference';
|
|
160
|
+
return 'text';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function matchSpecificity(preview, query) {
|
|
164
|
+
const line = String(preview || '');
|
|
165
|
+
const escaped = escapeRegex(query);
|
|
166
|
+
if (new RegExp(String.raw`\b${escaped}\b`, 'i').test(line)) return 0;
|
|
167
|
+
if (line.toLowerCase().includes(String(query || '').toLowerCase())) return 1;
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findSymbolDefinition(lines, symbol) {
|
|
172
|
+
const escaped = String(symbol || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
173
|
+
const patterns = [
|
|
174
|
+
new RegExp(String.raw`\bfunction\s+${escaped}\b`),
|
|
175
|
+
new RegExp(String.raw`\basync\s+function\s+${escaped}\b`),
|
|
176
|
+
new RegExp(String.raw`\bexport\s+function\s+${escaped}\b`),
|
|
177
|
+
new RegExp(String.raw`\bexport\s+async\s+function\s+${escaped}\b`),
|
|
178
|
+
new RegExp(String.raw`\bclass\s+${escaped}\b`),
|
|
179
|
+
new RegExp(String.raw`\bconst\s+${escaped}\b`),
|
|
180
|
+
new RegExp(String.raw`\blet\s+${escaped}\b`),
|
|
181
|
+
new RegExp(String.raw`\bvar\s+${escaped}\b`)
|
|
182
|
+
];
|
|
183
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
184
|
+
if (patterns.some((pattern) => pattern.test(lines[i]))) {
|
|
185
|
+
return i + 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
189
|
+
if (new RegExp(String.raw`\b${escaped}\b`).test(lines[i])) {
|
|
190
|
+
return i + 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function lineIndentSize(line) {
|
|
197
|
+
const match = String(line || '').match(/^\s*/);
|
|
198
|
+
return match ? match[0].length : 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function findBlockRange(lines, anchorLine) {
|
|
202
|
+
const total = lines.length;
|
|
203
|
+
const anchorIdx = Math.max(0, Math.min(total - 1, Number(anchorLine || 1) - 1));
|
|
204
|
+
|
|
205
|
+
let start = anchorIdx;
|
|
206
|
+
for (let i = anchorIdx; i >= 0; i -= 1) {
|
|
207
|
+
const line = String(lines[i] || '');
|
|
208
|
+
if (
|
|
209
|
+
/\b(function|class|interface|type|enum|const|let|var|export)\b/.test(line) ||
|
|
210
|
+
/=>\s*{/.test(line) ||
|
|
211
|
+
/<\w/.test(line)
|
|
212
|
+
) {
|
|
213
|
+
start = i;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let braceDepth = 0;
|
|
219
|
+
let seenBrace = false;
|
|
220
|
+
let end = anchorIdx;
|
|
221
|
+
for (let i = start; i < total; i += 1) {
|
|
222
|
+
const line = String(lines[i] || '');
|
|
223
|
+
for (const ch of line) {
|
|
224
|
+
if (ch === '{') {
|
|
225
|
+
braceDepth += 1;
|
|
226
|
+
seenBrace = true;
|
|
227
|
+
} else if (ch === '}') {
|
|
228
|
+
braceDepth -= 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
end = i;
|
|
232
|
+
if (seenBrace && braceDepth <= 0 && i > start) {
|
|
233
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const anchorText = String(lines[start] || '');
|
|
238
|
+
if (/^\s*def\b/.test(anchorText) || /:\s*$/.test(anchorText)) {
|
|
239
|
+
const baseIndent = lineIndentSize(anchorText);
|
|
240
|
+
end = start;
|
|
241
|
+
for (let i = start + 1; i < total; i += 1) {
|
|
242
|
+
const line = String(lines[i] || '');
|
|
243
|
+
if (!line.trim()) break;
|
|
244
|
+
if (lineIndentSize(line) <= baseIndent) break;
|
|
245
|
+
end = i;
|
|
246
|
+
}
|
|
247
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const baseIndent = lineIndentSize(lines[start]);
|
|
251
|
+
end = start;
|
|
252
|
+
for (let i = start + 1; i < total; i += 1) {
|
|
253
|
+
const line = String(lines[i] || '');
|
|
254
|
+
if (!line.trim()) {
|
|
255
|
+
end = i;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (lineIndentSize(line) <= baseIndent && i > anchorIdx) break;
|
|
259
|
+
end = i;
|
|
260
|
+
}
|
|
261
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function extractImports(lines) {
|
|
265
|
+
return lines.filter((line) => /^\s*import\b/.test(String(line || ''))).map((line) => trimLinePreview(line, 220));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractImportSignatures(lines, maxItems = 6) {
|
|
269
|
+
const imports = [];
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
const text = String(line || '').trim();
|
|
272
|
+
if (!/^import\b/.test(text)) continue;
|
|
273
|
+
imports.push(trimLinePreview(text, 96));
|
|
274
|
+
if (imports.length >= maxItems) break;
|
|
275
|
+
}
|
|
276
|
+
return imports;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function extractTypeSignatures(lines, maxItems = 6) {
|
|
280
|
+
const out = [];
|
|
281
|
+
const patterns = [
|
|
282
|
+
/^\s*(?:export\s+)?type\s+[A-Za-z0-9_$]+.*$/,
|
|
283
|
+
/^\s*(?:export\s+)?interface\s+[A-Za-z0-9_$]+.*$/,
|
|
284
|
+
/^\s*(?:export\s+)?enum\s+[A-Za-z0-9_$]+.*$/,
|
|
285
|
+
/^\s*(?:export\s+)?class\s+[A-Za-z0-9_$]+.*$/,
|
|
286
|
+
/^\s*import\s+type\b.*$/
|
|
287
|
+
];
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
const text = String(line || '').trim();
|
|
290
|
+
if (!patterns.some((pattern) => pattern.test(text))) continue;
|
|
291
|
+
out.push(trimLinePreview(text, 96));
|
|
292
|
+
if (out.length >= maxItems) break;
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractLocalSymbols(lines, sourceSymbol = '') {
|
|
298
|
+
const out = [];
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
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*=/;
|
|
301
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
302
|
+
const match = String(lines[i] || '').match(regex);
|
|
303
|
+
const name = match?.[1] || match?.[2] || match?.[3] || '';
|
|
304
|
+
if (!name || name === sourceSymbol || seen.has(name)) continue;
|
|
305
|
+
seen.add(name);
|
|
306
|
+
out.push({
|
|
307
|
+
name,
|
|
308
|
+
line: i + 1,
|
|
309
|
+
signature: trimLinePreview(lines[i], 220)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return out.slice(0, 8);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function extractDirectCalls(lines, symbol, maxItems = 3, excludeRange = null) {
|
|
316
|
+
const escaped = escapeRegex(symbol);
|
|
317
|
+
const out = [];
|
|
318
|
+
for (let i = 0; i < lines.length && out.length < maxItems; i += 1) {
|
|
319
|
+
if (excludeRange && i + 1 >= excludeRange.startLine && i + 1 <= excludeRange.endLine) continue;
|
|
320
|
+
const line = String(lines[i] || '');
|
|
321
|
+
if (!new RegExp(String.raw`\b${escaped}\s*\(`).test(line)) continue;
|
|
322
|
+
const blockLine = findEnclosingSymbol(lines, i + 1);
|
|
323
|
+
const owner = blockLine ? trimLinePreview(lines[blockLine - 1], 220) : trimLinePreview(line, 220);
|
|
324
|
+
const ownerName = blockLine ? extractSymbolName(lines[blockLine - 1]) : '';
|
|
325
|
+
if (ownerName === symbol) continue;
|
|
326
|
+
out.push({
|
|
327
|
+
symbol: ownerName || '(anonymous)',
|
|
328
|
+
line: blockLine || i + 1,
|
|
329
|
+
preview: owner
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function extractSymbolName(line) {
|
|
336
|
+
const text = String(line || '');
|
|
337
|
+
const match =
|
|
338
|
+
text.match(/\bfunction\s+([A-Za-z0-9_$]+)/) ||
|
|
339
|
+
text.match(/\bclass\s+([A-Za-z0-9_$]+)/) ||
|
|
340
|
+
text.match(/\bconst\s+([A-Za-z0-9_$]+)\s*=/) ||
|
|
341
|
+
text.match(/^\s*def\s+([A-Za-z0-9_]+)/);
|
|
342
|
+
return match?.[1] || '';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function findEnclosingSymbol(lines, anchorLine) {
|
|
346
|
+
for (let i = Math.max(0, anchorLine - 1); i >= 0; i -= 1) {
|
|
347
|
+
const name = extractSymbolName(lines[i]);
|
|
348
|
+
if (name) return i + 1;
|
|
349
|
+
}
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
|
|
354
|
+
const oldLines = splitLines(oldContent);
|
|
355
|
+
const newLines = splitLines(newContent);
|
|
356
|
+
let prefix = 0;
|
|
357
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
|
|
358
|
+
prefix += 1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let suffix = 0;
|
|
362
|
+
while (
|
|
363
|
+
suffix < oldLines.length - prefix &&
|
|
364
|
+
suffix < newLines.length - prefix &&
|
|
365
|
+
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
366
|
+
) {
|
|
367
|
+
suffix += 1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const oldChanged = oldLines.slice(prefix, oldLines.length - suffix);
|
|
371
|
+
const newChanged = newLines.slice(prefix, newLines.length - suffix);
|
|
372
|
+
const oldStart = prefix + 1;
|
|
373
|
+
const newStart = prefix + 1;
|
|
374
|
+
const oldCount = Math.max(1, oldChanged.length);
|
|
375
|
+
const newCount = Math.max(1, newChanged.length);
|
|
376
|
+
|
|
377
|
+
const body = [
|
|
378
|
+
`--- ${filePath}`,
|
|
379
|
+
`+++ ${filePath}`,
|
|
380
|
+
`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
|
|
381
|
+
...oldChanged.map((line) => `-${line}`),
|
|
382
|
+
...newChanged.map((line) => `+${line}`)
|
|
383
|
+
];
|
|
384
|
+
return body.join('\n');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function getFileState(root, relativePath) {
|
|
388
|
+
const target = resolveInWorkspace(root, relativePath);
|
|
389
|
+
const stat = await fs.stat(target);
|
|
390
|
+
const content = await fs.readFile(target, 'utf8');
|
|
391
|
+
return {
|
|
392
|
+
target,
|
|
393
|
+
content,
|
|
394
|
+
lines: splitLines(content),
|
|
395
|
+
stat
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
16
399
|
async function readFile(root, args) {
|
|
17
400
|
const target = resolveInWorkspace(root, args?.path);
|
|
18
401
|
const stat = await fs.stat(target);
|
|
19
402
|
const text = await fs.readFile(target, 'utf8');
|
|
20
|
-
const lines = text
|
|
403
|
+
const lines = splitLines(text);
|
|
21
404
|
const totalLines = lines.length;
|
|
22
405
|
const startLineRaw = Number(args?.start_line);
|
|
23
406
|
const endLineRaw = Number(args?.end_line);
|
|
@@ -34,7 +417,7 @@ async function readFile(root, args) {
|
|
|
34
417
|
endLine = Math.max(startLine, Math.min(endLine, totalLines));
|
|
35
418
|
|
|
36
419
|
const tokenSeed = `${args?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
|
|
37
|
-
const readToken =
|
|
420
|
+
const readToken = sha1(tokenSeed).slice(0, 16);
|
|
38
421
|
|
|
39
422
|
if (!includeContent) {
|
|
40
423
|
return {
|
|
@@ -98,9 +481,7 @@ async function writeFile(root, args) {
|
|
|
98
481
|
throw new Error(`write_file target is a directory: ${rawPath}`);
|
|
99
482
|
}
|
|
100
483
|
} catch (error) {
|
|
101
|
-
if (error?.code && error.code !== 'ENOENT')
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
484
|
+
if (error?.code && error.code !== 'ENOENT') throw error;
|
|
104
485
|
}
|
|
105
486
|
let before = '';
|
|
106
487
|
let existed = true;
|
|
@@ -116,8 +497,8 @@ async function writeFile(root, args) {
|
|
|
116
497
|
await fs.writeFile(target, args?.content || '', 'utf8');
|
|
117
498
|
}
|
|
118
499
|
const after = args?.append ? `${before}${args?.content || ''}` : args?.content || '';
|
|
119
|
-
const beforeLines = before
|
|
120
|
-
const afterLines = after
|
|
500
|
+
const beforeLines = splitLines(before);
|
|
501
|
+
const afterLines = splitLines(after);
|
|
121
502
|
let changeLine = 0;
|
|
122
503
|
const scanMax = Math.max(beforeLines.length, afterLines.length);
|
|
123
504
|
for (let i = 0; i < scanMax; i += 1) {
|
|
@@ -142,6 +523,9 @@ async function runCommand(root, config, args) {
|
|
|
142
523
|
if (!command.trim()) {
|
|
143
524
|
throw new Error('run_command requires command');
|
|
144
525
|
}
|
|
526
|
+
if (isLikelyLongRunningCommand(command)) {
|
|
527
|
+
throw new Error('Command looks like a long-running service. Use start_service instead of run_command.');
|
|
528
|
+
}
|
|
145
529
|
if (
|
|
146
530
|
!config.policy.allow_dangerous_commands &&
|
|
147
531
|
isDangerousCommand(command, config.policy.blocked_command_patterns)
|
|
@@ -165,8 +549,799 @@ async function runCommand(root, config, args) {
|
|
|
165
549
|
return { ...result, command };
|
|
166
550
|
}
|
|
167
551
|
|
|
168
|
-
|
|
552
|
+
function nextServiceId() {
|
|
553
|
+
serviceCounter += 1;
|
|
554
|
+
return `svc_${String(serviceCounter).padStart(3, '0')}`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function normalizeSuccessMatchers(items = []) {
|
|
558
|
+
if (!Array.isArray(items)) return [];
|
|
559
|
+
return items.map((item) => String(item || '').trim()).filter(Boolean);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function shellCommandForService(command, shellSpec) {
|
|
563
|
+
return process.platform !== 'win32' && /(?:^|\/)bash(?:\.exe)?$/i.test(shellSpec.command)
|
|
564
|
+
? `exec ${command}`
|
|
565
|
+
: command;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function appendRecentLogs(service, chunk) {
|
|
569
|
+
const lines = String(chunk || '')
|
|
570
|
+
.split(/\r?\n/)
|
|
571
|
+
.map((line) => trimLinePreview(line, 220))
|
|
572
|
+
.filter(Boolean);
|
|
573
|
+
if (lines.length === 0) return;
|
|
574
|
+
for (const line of lines) {
|
|
575
|
+
serviceLogCursorCounter += 1;
|
|
576
|
+
service.recentLogs.push({ cursor: serviceLogCursorCounter, line });
|
|
577
|
+
}
|
|
578
|
+
if (service.recentLogs.length > SERVICE_RECENT_LOG_LIMIT) {
|
|
579
|
+
service.recentLogs.splice(0, service.recentLogs.length - SERVICE_RECENT_LOG_LIMIT);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function matchesServiceSuccess(service, text) {
|
|
584
|
+
const value = String(text || '');
|
|
585
|
+
if (!value) return false;
|
|
586
|
+
if (hasReadyOutput(value)) return true;
|
|
587
|
+
return service.successMatchers.some((matcher) => value.toLowerCase().includes(matcher.toLowerCase()));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function markServiceReady(service, source = 'output') {
|
|
591
|
+
if (service.startupConfirmed) return;
|
|
592
|
+
service.startupConfirmed = true;
|
|
593
|
+
service.startupSource = source;
|
|
594
|
+
service.status = 'running';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function serviceUrlForPort(port) {
|
|
598
|
+
const portNumber = Number(port);
|
|
599
|
+
return Number.isInteger(portNumber) && portNumber > 0 ? `http://127.0.0.1:${portNumber}` : '';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function normalizeHttpProbe(value) {
|
|
603
|
+
if (!value || typeof value !== 'object') return null;
|
|
604
|
+
const url = String(value.url || '').trim();
|
|
605
|
+
if (!url) return null;
|
|
606
|
+
const expectStatus = Number(value.expect_status ?? value.expectStatus ?? 200);
|
|
607
|
+
return {
|
|
608
|
+
url,
|
|
609
|
+
expect_status: Number.isInteger(expectStatus) ? expectStatus : 200
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function snapshotService(service, tail = 12) {
|
|
614
|
+
const recentLogs = Array.isArray(service.recentLogs)
|
|
615
|
+
? service.recentLogs.slice(-Math.max(1, tail)).map((item) => item.line)
|
|
616
|
+
: [];
|
|
617
|
+
const latestCursor =
|
|
618
|
+
Array.isArray(service.recentLogs) && service.recentLogs.length > 0
|
|
619
|
+
? service.recentLogs[service.recentLogs.length - 1].cursor
|
|
620
|
+
: 0;
|
|
621
|
+
return {
|
|
622
|
+
task_id: service.taskId,
|
|
623
|
+
pid: service.child?.pid || null,
|
|
624
|
+
command: service.command,
|
|
625
|
+
cwd: service.cwd,
|
|
626
|
+
status: service.status,
|
|
627
|
+
startup_confirmed: service.startupConfirmed,
|
|
628
|
+
startup_source: service.startupSource || '',
|
|
629
|
+
http_probe: service.httpProbe || undefined,
|
|
630
|
+
url: serviceUrlForPort(service.portProbe) || undefined,
|
|
631
|
+
recent_logs: recentLogs,
|
|
632
|
+
log_cursor: latestCursor,
|
|
633
|
+
exit_code: service.exitCode ?? undefined,
|
|
634
|
+
signal: service.signal ?? undefined,
|
|
635
|
+
duration_ms: Date.now() - service.startedAt
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function listServiceSnapshots() {
|
|
640
|
+
return Array.from(serviceRegistry.values()).map((service) => snapshotService(service, 4));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function probePortOnce(port, host = '127.0.0.1', timeoutMs = 250) {
|
|
644
|
+
return new Promise((resolve) => {
|
|
645
|
+
const socket = new net.Socket();
|
|
646
|
+
let settled = false;
|
|
647
|
+
const finish = (value) => {
|
|
648
|
+
if (settled) return;
|
|
649
|
+
settled = true;
|
|
650
|
+
socket.destroy();
|
|
651
|
+
resolve(value);
|
|
652
|
+
};
|
|
653
|
+
socket.setTimeout(timeoutMs);
|
|
654
|
+
socket.once('connect', () => finish(true));
|
|
655
|
+
socket.once('timeout', () => finish(false));
|
|
656
|
+
socket.once('error', () => finish(false));
|
|
657
|
+
socket.connect(Number(port), host);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function probeHttpOnce(httpProbe, timeoutMs = 400) {
|
|
662
|
+
if (!httpProbe?.url) return false;
|
|
663
|
+
const controller = new AbortController();
|
|
664
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
665
|
+
try {
|
|
666
|
+
const response = await fetch(httpProbe.url, {
|
|
667
|
+
method: 'GET',
|
|
668
|
+
signal: controller.signal
|
|
669
|
+
});
|
|
670
|
+
return response.status === Number(httpProbe.expect_status || 200);
|
|
671
|
+
} catch {
|
|
672
|
+
return false;
|
|
673
|
+
} finally {
|
|
674
|
+
clearTimeout(timer);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function startService(root, config, args) {
|
|
679
|
+
const command = String(args?.command || args?.cmd || '').trim();
|
|
680
|
+
if (!command) throw new Error('start_service requires command');
|
|
681
|
+
if (
|
|
682
|
+
!config.policy.allow_dangerous_commands &&
|
|
683
|
+
isDangerousCommand(command, config.policy.blocked_command_patterns)
|
|
684
|
+
) {
|
|
685
|
+
throw new Error('Command blocked by policy');
|
|
686
|
+
}
|
|
687
|
+
const check = evaluateCommandPolicy(command, config, root);
|
|
688
|
+
if (!check.allowed) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`Command blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const shellSpec = resolveShell(config.shell.default);
|
|
695
|
+
const taskId = nextServiceId();
|
|
696
|
+
const startupTimeoutMs = Math.max(250, Number(args?.startup_timeout_ms || args?.startupTimeoutMs || 20000));
|
|
697
|
+
const successMatchers = normalizeSuccessMatchers(args?.success_matchers || args?.successMatchers);
|
|
698
|
+
const portProbe = Number(args?.port_probe || args?.portProbe || 0) || 0;
|
|
699
|
+
const httpProbe = normalizeHttpProbe(args?.http_probe || args?.httpProbe);
|
|
700
|
+
const service = {
|
|
701
|
+
taskId,
|
|
702
|
+
command,
|
|
703
|
+
cwd: root,
|
|
704
|
+
child: spawn(shellSpec.command, [...shellSpec.args, shellCommandForService(command, shellSpec)], {
|
|
705
|
+
cwd: root,
|
|
706
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
707
|
+
}),
|
|
708
|
+
startedAt: Date.now(),
|
|
709
|
+
status: 'starting',
|
|
710
|
+
startupConfirmed: false,
|
|
711
|
+
startupSource: '',
|
|
712
|
+
successMatchers,
|
|
713
|
+
portProbe,
|
|
714
|
+
httpProbe,
|
|
715
|
+
recentLogs: [],
|
|
716
|
+
exitCode: null,
|
|
717
|
+
signal: null
|
|
718
|
+
};
|
|
719
|
+
serviceRegistry.set(taskId, service);
|
|
720
|
+
|
|
721
|
+
service.closePromise = new Promise((resolve) => {
|
|
722
|
+
service.child.on('close', (code, signal) => {
|
|
723
|
+
service.exitCode = code;
|
|
724
|
+
service.signal = signal;
|
|
725
|
+
service.status = service.status === 'stopped' ? 'stopped' : 'exited';
|
|
726
|
+
resolve();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const onOutput = (chunk) => {
|
|
731
|
+
appendRecentLogs(service, chunk);
|
|
732
|
+
if (matchesServiceSuccess(service, chunk)) {
|
|
733
|
+
markServiceReady(service, 'output');
|
|
734
|
+
if (service._finishStartup) service._finishStartup();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
service.child.stdout.on('data', onOutput);
|
|
738
|
+
service.child.stderr.on('data', onOutput);
|
|
739
|
+
service.child.on('error', (error) => {
|
|
740
|
+
appendRecentLogs(service, error?.message || String(error));
|
|
741
|
+
service.status = 'exited';
|
|
742
|
+
if (service._finishStartup) service._finishStartup();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
await new Promise((resolve) => {
|
|
746
|
+
let settled = false;
|
|
747
|
+
const finish = () => {
|
|
748
|
+
if (settled) return;
|
|
749
|
+
settled = true;
|
|
750
|
+
clearTimeout(timeoutHandle);
|
|
751
|
+
clearInterval(portHandle);
|
|
752
|
+
clearInterval(httpHandle);
|
|
753
|
+
service._finishStartup = null;
|
|
754
|
+
resolve();
|
|
755
|
+
};
|
|
756
|
+
service._finishStartup = finish;
|
|
757
|
+
if (service.startupConfirmed || service.status === 'exited') {
|
|
758
|
+
finish();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const timeoutHandle = setTimeout(() => {
|
|
762
|
+
if (service.status === 'starting') {
|
|
763
|
+
if (!service.startupConfirmed) {
|
|
764
|
+
markServiceReady(service, 'startup_window');
|
|
765
|
+
} else {
|
|
766
|
+
service.status = 'running';
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
finish();
|
|
770
|
+
}, startupTimeoutMs);
|
|
771
|
+
const portHandle =
|
|
772
|
+
portProbe > 0
|
|
773
|
+
? setInterval(async () => {
|
|
774
|
+
const open = await probePortOnce(portProbe);
|
|
775
|
+
if (open) {
|
|
776
|
+
markServiceReady(service, 'port_probe');
|
|
777
|
+
finish();
|
|
778
|
+
}
|
|
779
|
+
}, SERVICE_STARTUP_POLL_MS)
|
|
780
|
+
: null;
|
|
781
|
+
const httpHandle =
|
|
782
|
+
httpProbe
|
|
783
|
+
? setInterval(async () => {
|
|
784
|
+
const ok = await probeHttpOnce(httpProbe);
|
|
785
|
+
if (ok) {
|
|
786
|
+
markServiceReady(service, 'http_probe');
|
|
787
|
+
finish();
|
|
788
|
+
}
|
|
789
|
+
}, SERVICE_STARTUP_POLL_MS)
|
|
790
|
+
: null;
|
|
791
|
+
service.child.once('close', () => finish());
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
if (service.status === 'starting') {
|
|
795
|
+
service.status = 'running';
|
|
796
|
+
}
|
|
797
|
+
return snapshotService(service);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getServiceOrThrow(taskId) {
|
|
801
|
+
const service = serviceRegistry.get(String(taskId || '').trim());
|
|
802
|
+
if (!service) throw new Error(`Unknown service task: ${taskId}`);
|
|
803
|
+
return service;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function getServiceStatus(_root, args) {
|
|
807
|
+
const service = getServiceOrThrow(args?.task_id || args?.taskId);
|
|
808
|
+
return snapshotService(service);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function listServices() {
|
|
812
|
+
return {
|
|
813
|
+
services: listServiceSnapshots()
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function getServiceLogs(_root, args) {
|
|
818
|
+
const service = getServiceOrThrow(args?.task_id || args?.taskId);
|
|
819
|
+
const tail = Math.max(1, Math.min(200, Number(args?.tail || 40)));
|
|
820
|
+
const afterCursor = Math.max(0, Number(args?.after_cursor || args?.afterCursor || 0));
|
|
821
|
+
const filtered = afterCursor > 0 ? service.recentLogs.filter((item) => item.cursor > afterCursor) : service.recentLogs;
|
|
822
|
+
const selected = filtered.slice(-tail);
|
|
823
|
+
return {
|
|
824
|
+
task_id: service.taskId,
|
|
825
|
+
status: service.status,
|
|
826
|
+
recent_logs: selected.map((item) => item.line),
|
|
827
|
+
next_cursor: selected.length > 0 ? selected[selected.length - 1].cursor : afterCursor
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function stopService(_root, args) {
|
|
832
|
+
const service = getServiceOrThrow(args?.task_id || args?.taskId);
|
|
833
|
+
if (service.status === 'stopped' || service.status === 'exited') {
|
|
834
|
+
return { ...snapshotService(service), stopped: true };
|
|
835
|
+
}
|
|
836
|
+
service.status = 'stopped';
|
|
837
|
+
terminateChild(service.child, 'SIGTERM');
|
|
838
|
+
setTimeout(() => terminateChild(service.child, 'SIGKILL'), 200);
|
|
839
|
+
await Promise.race([
|
|
840
|
+
service.closePromise,
|
|
841
|
+
new Promise((resolve) => setTimeout(resolve, 500))
|
|
842
|
+
]);
|
|
843
|
+
return { ...snapshotService(service), stopped: true };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function searchCode(root, args) {
|
|
847
|
+
const query = String(args?.query || args?.symbol || '').trim();
|
|
848
|
+
if (!query) throw new Error('search_code requires query');
|
|
849
|
+
const maxResults = Math.max(1, Math.min(50, Number(args?.max_results || 12)));
|
|
850
|
+
const caseSensitive = Boolean(args?.case_sensitive);
|
|
851
|
+
const files = await walkTextFiles(root, args?.path || '.', normalizeFileTypes(args));
|
|
852
|
+
const matches = [];
|
|
853
|
+
|
|
854
|
+
for (const filePath of files) {
|
|
855
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
856
|
+
const lines = splitLines(content);
|
|
857
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
858
|
+
const line = lines[idx];
|
|
859
|
+
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
860
|
+
const needle = caseSensitive ? query : query.toLowerCase();
|
|
861
|
+
if (!haystack.includes(needle)) continue;
|
|
862
|
+
matches.push({
|
|
863
|
+
file: toWorkspaceRelative(root, filePath),
|
|
864
|
+
line: idx + 1,
|
|
865
|
+
column: getLineColumnForMatch(line, query, caseSensitive),
|
|
866
|
+
preview: trimLinePreview(line),
|
|
867
|
+
kind: classifyMatch(line, query),
|
|
868
|
+
symbolHint: query
|
|
869
|
+
});
|
|
870
|
+
if (matches.length >= maxResults) {
|
|
871
|
+
matches.sort((left, right) => {
|
|
872
|
+
const kindRank = { definition: 0, reference: 1, text: 2 };
|
|
873
|
+
const specificity = matchSpecificity(left.preview, query) - matchSpecificity(right.preview, query);
|
|
874
|
+
if (specificity !== 0) return specificity;
|
|
875
|
+
if (kindRank[left.kind] !== kindRank[right.kind]) return kindRank[left.kind] - kindRank[right.kind];
|
|
876
|
+
return left.file.localeCompare(right.file) || left.line - right.line;
|
|
877
|
+
});
|
|
878
|
+
return {
|
|
879
|
+
query,
|
|
880
|
+
matches,
|
|
881
|
+
definitions: matches.filter((item) => item.kind === 'definition'),
|
|
882
|
+
references: matches.filter((item) => item.kind === 'reference'),
|
|
883
|
+
text_matches: matches.filter((item) => item.kind === 'text'),
|
|
884
|
+
truncated: true
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
matches.sort((left, right) => {
|
|
891
|
+
const kindRank = { definition: 0, reference: 1, text: 2 };
|
|
892
|
+
const specificity = matchSpecificity(left.preview, query) - matchSpecificity(right.preview, query);
|
|
893
|
+
if (specificity !== 0) return specificity;
|
|
894
|
+
if (kindRank[left.kind] !== kindRank[right.kind]) return kindRank[left.kind] - kindRank[right.kind];
|
|
895
|
+
return left.file.localeCompare(right.file) || left.line - right.line;
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
query,
|
|
900
|
+
matches,
|
|
901
|
+
definitions: matches.filter((item) => item.kind === 'definition'),
|
|
902
|
+
references: matches.filter((item) => item.kind === 'reference'),
|
|
903
|
+
text_matches: matches.filter((item) => item.kind === 'text'),
|
|
904
|
+
truncated: false
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function readBlock(root, args) {
|
|
909
|
+
const relativePath = String(args?.path || '').trim();
|
|
910
|
+
if (!relativePath) throw new Error('read_block requires path');
|
|
911
|
+
const { lines } = await getFileState(root, relativePath);
|
|
912
|
+
const symbol = String(args?.symbol || '').trim();
|
|
913
|
+
const anchorLine = symbol ? findSymbolDefinition(lines, symbol) : Number(args?.line || args?.anchor_line || 1);
|
|
914
|
+
const range = findBlockRange(lines, anchorLine);
|
|
915
|
+
return {
|
|
916
|
+
file: relativePath,
|
|
917
|
+
symbol: symbol || undefined,
|
|
918
|
+
mode: symbol ? 'symbol' : 'block',
|
|
919
|
+
start_line: range.startLine,
|
|
920
|
+
end_line: range.endLine,
|
|
921
|
+
content: lines.slice(range.startLine - 1, range.endLine).join('\n')
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function readSymbolContext(root, args) {
|
|
926
|
+
const relativePath = String(args?.path || '').trim();
|
|
927
|
+
const symbol = String(args?.symbol || '').trim();
|
|
928
|
+
if (!relativePath || !symbol) throw new Error('read_symbol_context requires path and symbol');
|
|
929
|
+
const { lines } = await getFileState(root, relativePath);
|
|
930
|
+
const mainBlock = await readBlock(root, { path: relativePath, symbol });
|
|
931
|
+
return {
|
|
932
|
+
file: relativePath,
|
|
933
|
+
symbol,
|
|
934
|
+
main_block: mainBlock,
|
|
935
|
+
related: {
|
|
936
|
+
imports: extractImports(lines),
|
|
937
|
+
import_signatures: extractImportSignatures(lines, Number(args?.max_related_imports || 4)),
|
|
938
|
+
type_signatures: extractTypeSignatures(lines, Number(args?.max_related_types || 4)),
|
|
939
|
+
local_symbols: extractLocalSymbols(lines, symbol),
|
|
940
|
+
calls: extractDirectCalls(lines, symbol, Number(args?.max_related_calls || 3), {
|
|
941
|
+
startLine: mainBlock.start_line,
|
|
942
|
+
endLine: mainBlock.end_line
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function validateEdit(root, args) {
|
|
949
|
+
const relativePath = String(args?.path || '').trim();
|
|
950
|
+
const kind = String(args?.kind || '').trim();
|
|
951
|
+
if (!relativePath || !kind) throw new Error('validate_edit requires path and kind');
|
|
952
|
+
const { content, lines } = await getFileState(root, relativePath);
|
|
953
|
+
|
|
954
|
+
if (kind === 'replace_block') {
|
|
955
|
+
const startLine = Number(args?.target?.start_line || args?.start_line);
|
|
956
|
+
const endLine = Number(args?.target?.end_line || args?.end_line);
|
|
957
|
+
if (!Number.isFinite(startLine) || !Number.isFinite(endLine) || startLine <= 0 || endLine < startLine) {
|
|
958
|
+
throw new Error('replace_block validation requires target.start_line and target.end_line');
|
|
959
|
+
}
|
|
960
|
+
const oldBlock = lines.slice(startLine - 1, endLine).join('\n');
|
|
961
|
+
return {
|
|
962
|
+
ok: true,
|
|
963
|
+
path: relativePath,
|
|
964
|
+
kind,
|
|
965
|
+
target: {
|
|
966
|
+
start_line: startLine,
|
|
967
|
+
end_line: endLine,
|
|
968
|
+
old_hash: sha256(oldBlock)
|
|
969
|
+
},
|
|
970
|
+
file_hash: sha256(content)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (kind === 'replace_text' || kind === 'insert_before' || kind === 'insert_after') {
|
|
975
|
+
const probe = String(args?.old_text || args?.anchor_text || '');
|
|
976
|
+
if (!probe) throw new Error(`${kind} validation requires old_text or anchor_text`);
|
|
977
|
+
const occurrences = content.split(probe).length - 1;
|
|
978
|
+
return {
|
|
979
|
+
ok: occurrences === 1,
|
|
980
|
+
path: relativePath,
|
|
981
|
+
kind,
|
|
982
|
+
occurrences,
|
|
983
|
+
reason: occurrences === 1 ? 'unique match' : occurrences === 0 ? 'anchor not found' : 'anchor not unique',
|
|
984
|
+
file_hash: sha256(content)
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
throw new Error(`validate_edit does not support kind: ${kind}`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
|
|
992
|
+
const afterLines = splitLines(afterContent);
|
|
993
|
+
const previewStart = Math.max(0, changedLine - 1);
|
|
994
|
+
const diffPreview = afterLines.slice(previewStart, previewStart + 6).map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n');
|
|
995
|
+
return {
|
|
996
|
+
ok: true,
|
|
997
|
+
path: pathText,
|
|
998
|
+
action,
|
|
999
|
+
changed_line: changedLine,
|
|
1000
|
+
diff_preview: diffPreview,
|
|
1001
|
+
diff: buildUnifiedDiff(beforeContent, afterContent, pathText),
|
|
1002
|
+
new_hash: sha256(afterContent)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function replaceBlock(root, args) {
|
|
1007
|
+
const relativePath = String(args?.path || '').trim();
|
|
1008
|
+
const newContent = String(args?.new_content || args?.content || '');
|
|
1009
|
+
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
|
+
const state = await getFileState(root, relativePath);
|
|
1014
|
+
const oldBlock = state.lines.slice(startLine - 1, endLine).join('\n');
|
|
1015
|
+
if (!oldHash || oldHash !== sha256(oldBlock)) {
|
|
1016
|
+
throw new Error('replace_block old_hash mismatch');
|
|
1017
|
+
}
|
|
1018
|
+
const nextLines = [...state.lines.slice(0, startLine - 1), ...splitLines(newContent), ...state.lines.slice(endLine)];
|
|
1019
|
+
const afterContent = nextLines.join('\n');
|
|
1020
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1021
|
+
return editResult(relativePath, 'replace_block', state.content, afterContent, startLine);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function replaceText(root, args) {
|
|
1025
|
+
const relativePath = String(args?.path || '').trim();
|
|
1026
|
+
const oldText = String(args?.old_text || '');
|
|
1027
|
+
const newText = String(args?.new_text || '');
|
|
1028
|
+
const state = await getFileState(root, relativePath);
|
|
1029
|
+
const occurrences = state.content.split(oldText).length - 1;
|
|
1030
|
+
if (occurrences !== 1) {
|
|
1031
|
+
throw new Error(occurrences === 0 ? 'replace_text old_text not found' : 'replace_text old_text not unique');
|
|
1032
|
+
}
|
|
1033
|
+
const afterContent = state.content.replace(oldText, newText);
|
|
1034
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1035
|
+
const changedLine = splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1036
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function insertRelative(root, args, mode) {
|
|
1040
|
+
const relativePath = String(args?.path || '').trim();
|
|
1041
|
+
const anchorText = String(args?.anchor_text || '');
|
|
1042
|
+
const content = String(args?.content || '');
|
|
1043
|
+
const state = await getFileState(root, relativePath);
|
|
1044
|
+
const occurrences = state.content.split(anchorText).length - 1;
|
|
1045
|
+
if (occurrences !== 1) {
|
|
1046
|
+
throw new Error(occurrences === 0 ? `${mode} anchor not found` : `${mode} anchor not unique`);
|
|
1047
|
+
}
|
|
1048
|
+
const replacement = mode === 'insert_before' ? `${content}${anchorText}` : `${anchorText}${content}`;
|
|
1049
|
+
const afterContent = state.content.replace(anchorText, replacement);
|
|
1050
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1051
|
+
const changedLine = splitLines(state.content.slice(0, state.content.indexOf(anchorText))).length;
|
|
1052
|
+
return editResult(relativePath, mode, state.content, afterContent, changedLine);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async function generateDiff(root, args) {
|
|
1056
|
+
const relativePath = String(args?.path || '').trim();
|
|
1057
|
+
if (!relativePath) throw new Error('generate_diff requires path');
|
|
1058
|
+
const state = await getFileState(root, relativePath);
|
|
1059
|
+
const newContent = String(args?.new_content || '');
|
|
1060
|
+
return {
|
|
1061
|
+
path: relativePath,
|
|
1062
|
+
old_hash: sha256(state.content),
|
|
1063
|
+
new_hash: sha256(newContent),
|
|
1064
|
+
diff: buildUnifiedDiff(state.content, newContent, relativePath)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
async function locate(root, args) {
|
|
1069
|
+
const result = await searchCode(root, args);
|
|
1070
|
+
return {
|
|
1071
|
+
query: result.query,
|
|
1072
|
+
matches: result.matches,
|
|
1073
|
+
definitions: result.definitions,
|
|
1074
|
+
references: result.references,
|
|
1075
|
+
text_matches: result.text_matches,
|
|
1076
|
+
truncated: result.truncated
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function openTarget(root, args) {
|
|
1081
|
+
const file = String(args?.file || args?.path || '').trim();
|
|
1082
|
+
if (!file) throw new Error('open_target requires file');
|
|
1083
|
+
const symbol = String(args?.symbol || '').trim();
|
|
1084
|
+
const line = Number(args?.line || 1);
|
|
1085
|
+
const mainBlock = symbol
|
|
1086
|
+
? await readSymbolContext(root, {
|
|
1087
|
+
path: file,
|
|
1088
|
+
symbol,
|
|
1089
|
+
max_related_calls: args?.max_related_calls,
|
|
1090
|
+
max_related_imports: args?.max_related_imports,
|
|
1091
|
+
max_related_types: args?.max_related_types
|
|
1092
|
+
})
|
|
1093
|
+
: { file, symbol: '', main_block: await readBlock(root, { path: file, line }), related: { imports: [], local_symbols: [] } };
|
|
1094
|
+
const block = mainBlock.main_block || mainBlock;
|
|
1095
|
+
return {
|
|
1096
|
+
file,
|
|
1097
|
+
symbol: symbol || undefined,
|
|
1098
|
+
main_block: block,
|
|
1099
|
+
related: mainBlock.related || { imports: [], local_symbols: [] },
|
|
1100
|
+
edit_target: {
|
|
1101
|
+
start_line: block.start_line,
|
|
1102
|
+
end_line: block.end_line,
|
|
1103
|
+
old_hash: sha256(block.content)
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function editTarget(root, args) {
|
|
1109
|
+
const file = String(args?.file || args?.path || '').trim();
|
|
1110
|
+
const edit = args?.edit || {};
|
|
1111
|
+
const kind = String(edit.kind || '').trim();
|
|
1112
|
+
if (!file || !kind) throw new Error('edit_target requires file and edit.kind');
|
|
1113
|
+
if (kind === 'replace_block') {
|
|
1114
|
+
return replaceBlock(root, {
|
|
1115
|
+
path: file,
|
|
1116
|
+
target: edit.target,
|
|
1117
|
+
new_content: edit.new_content
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (kind === 'replace_text') {
|
|
1121
|
+
return replaceText(root, {
|
|
1122
|
+
path: file,
|
|
1123
|
+
old_text: edit.old_text,
|
|
1124
|
+
new_text: edit.new_text
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (kind === 'insert_before') {
|
|
1128
|
+
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_before');
|
|
1129
|
+
}
|
|
1130
|
+
if (kind === 'insert_after') {
|
|
1131
|
+
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_after');
|
|
1132
|
+
}
|
|
1133
|
+
throw new Error(`edit_target does not support kind: ${kind}`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
169
1137
|
const definitions = [
|
|
1138
|
+
{
|
|
1139
|
+
type: 'function',
|
|
1140
|
+
function: {
|
|
1141
|
+
name: 'locate',
|
|
1142
|
+
description: 'High-level search that returns compact candidate code locations',
|
|
1143
|
+
parameters: {
|
|
1144
|
+
type: 'object',
|
|
1145
|
+
properties: {
|
|
1146
|
+
query: { type: 'string' },
|
|
1147
|
+
path: { type: 'string' },
|
|
1148
|
+
max_results: { type: 'number' },
|
|
1149
|
+
language: { type: 'string' },
|
|
1150
|
+
file_types: { type: 'array', items: { type: 'string' } }
|
|
1151
|
+
},
|
|
1152
|
+
required: ['query']
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
type: 'function',
|
|
1158
|
+
function: {
|
|
1159
|
+
name: 'open_target',
|
|
1160
|
+
description: 'Open a candidate location and return the smallest useful code block plus edit metadata',
|
|
1161
|
+
parameters: {
|
|
1162
|
+
type: 'object',
|
|
1163
|
+
properties: {
|
|
1164
|
+
file: { type: 'string' },
|
|
1165
|
+
path: { type: 'string' },
|
|
1166
|
+
line: { type: 'number' },
|
|
1167
|
+
symbol: { type: 'string' },
|
|
1168
|
+
max_related_calls: { type: 'number' },
|
|
1169
|
+
max_related_imports: { type: 'number' },
|
|
1170
|
+
max_related_types: { type: 'number' }
|
|
1171
|
+
},
|
|
1172
|
+
required: ['file']
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
type: 'function',
|
|
1178
|
+
function: {
|
|
1179
|
+
name: 'edit_target',
|
|
1180
|
+
description: 'Apply a validated high-level edit against an opened target',
|
|
1181
|
+
parameters: {
|
|
1182
|
+
type: 'object',
|
|
1183
|
+
properties: {
|
|
1184
|
+
file: { type: 'string' },
|
|
1185
|
+
path: { type: 'string' },
|
|
1186
|
+
edit: { type: 'object' }
|
|
1187
|
+
},
|
|
1188
|
+
required: ['file', 'edit']
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
type: 'function',
|
|
1194
|
+
function: {
|
|
1195
|
+
name: 'search_code',
|
|
1196
|
+
description: 'Search code and return structured top matches with file, line, preview, and basic match kind',
|
|
1197
|
+
parameters: {
|
|
1198
|
+
type: 'object',
|
|
1199
|
+
properties: {
|
|
1200
|
+
query: { type: 'string' },
|
|
1201
|
+
path: { type: 'string' },
|
|
1202
|
+
max_results: { type: 'number' },
|
|
1203
|
+
case_sensitive: { type: 'boolean' },
|
|
1204
|
+
language: { type: 'string' },
|
|
1205
|
+
file_types: { type: 'array', items: { type: 'string' } }
|
|
1206
|
+
},
|
|
1207
|
+
required: ['query']
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
type: 'function',
|
|
1213
|
+
function: {
|
|
1214
|
+
name: 'read_block',
|
|
1215
|
+
description: 'Read the smallest likely code block around a symbol or line from a file',
|
|
1216
|
+
parameters: {
|
|
1217
|
+
type: 'object',
|
|
1218
|
+
properties: {
|
|
1219
|
+
path: { type: 'string' },
|
|
1220
|
+
symbol: { type: 'string' },
|
|
1221
|
+
line: { type: 'number' },
|
|
1222
|
+
anchor_line: { type: 'number' }
|
|
1223
|
+
},
|
|
1224
|
+
required: ['path']
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
type: 'function',
|
|
1230
|
+
function: {
|
|
1231
|
+
name: 'read_symbol_context',
|
|
1232
|
+
description: 'Read a symbol block plus import and local symbol summaries',
|
|
1233
|
+
parameters: {
|
|
1234
|
+
type: 'object',
|
|
1235
|
+
properties: {
|
|
1236
|
+
path: { type: 'string' },
|
|
1237
|
+
symbol: { type: 'string' },
|
|
1238
|
+
max_related_calls: { type: 'number' },
|
|
1239
|
+
max_related_imports: { type: 'number' },
|
|
1240
|
+
max_related_types: { type: 'number' }
|
|
1241
|
+
},
|
|
1242
|
+
required: ['path', 'symbol']
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
type: 'function',
|
|
1248
|
+
function: {
|
|
1249
|
+
name: 'validate_edit',
|
|
1250
|
+
description: 'Validate whether an edit target is stable before applying it',
|
|
1251
|
+
parameters: {
|
|
1252
|
+
type: 'object',
|
|
1253
|
+
properties: {
|
|
1254
|
+
path: { type: 'string' },
|
|
1255
|
+
kind: { type: 'string' },
|
|
1256
|
+
target: { type: 'object' },
|
|
1257
|
+
start_line: { type: 'number' },
|
|
1258
|
+
end_line: { type: 'number' },
|
|
1259
|
+
old_text: { type: 'string' },
|
|
1260
|
+
anchor_text: { type: 'string' }
|
|
1261
|
+
},
|
|
1262
|
+
required: ['path', 'kind']
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
type: 'function',
|
|
1268
|
+
function: {
|
|
1269
|
+
name: 'replace_block',
|
|
1270
|
+
description: 'Replace a validated line block using an old_hash guard',
|
|
1271
|
+
parameters: {
|
|
1272
|
+
type: 'object',
|
|
1273
|
+
properties: {
|
|
1274
|
+
path: { type: 'string' },
|
|
1275
|
+
target: { type: 'object' },
|
|
1276
|
+
new_content: { type: 'string' }
|
|
1277
|
+
},
|
|
1278
|
+
required: ['path', 'target', 'new_content']
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
type: 'function',
|
|
1284
|
+
function: {
|
|
1285
|
+
name: 'replace_text',
|
|
1286
|
+
description: 'Replace a unique text fragment in a file',
|
|
1287
|
+
parameters: {
|
|
1288
|
+
type: 'object',
|
|
1289
|
+
properties: {
|
|
1290
|
+
path: { type: 'string' },
|
|
1291
|
+
old_text: { type: 'string' },
|
|
1292
|
+
new_text: { type: 'string' }
|
|
1293
|
+
},
|
|
1294
|
+
required: ['path', 'old_text', 'new_text']
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
type: 'function',
|
|
1300
|
+
function: {
|
|
1301
|
+
name: 'insert_before',
|
|
1302
|
+
description: 'Insert text before a unique anchor string',
|
|
1303
|
+
parameters: {
|
|
1304
|
+
type: 'object',
|
|
1305
|
+
properties: {
|
|
1306
|
+
path: { type: 'string' },
|
|
1307
|
+
anchor_text: { type: 'string' },
|
|
1308
|
+
content: { type: 'string' }
|
|
1309
|
+
},
|
|
1310
|
+
required: ['path', 'anchor_text', 'content']
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
type: 'function',
|
|
1316
|
+
function: {
|
|
1317
|
+
name: 'insert_after',
|
|
1318
|
+
description: 'Insert text after a unique anchor string',
|
|
1319
|
+
parameters: {
|
|
1320
|
+
type: 'object',
|
|
1321
|
+
properties: {
|
|
1322
|
+
path: { type: 'string' },
|
|
1323
|
+
anchor_text: { type: 'string' },
|
|
1324
|
+
content: { type: 'string' }
|
|
1325
|
+
},
|
|
1326
|
+
required: ['path', 'anchor_text', 'content']
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
type: 'function',
|
|
1332
|
+
function: {
|
|
1333
|
+
name: 'generate_diff',
|
|
1334
|
+
description: 'Generate a unified diff between the current file and proposed content',
|
|
1335
|
+
parameters: {
|
|
1336
|
+
type: 'object',
|
|
1337
|
+
properties: {
|
|
1338
|
+
path: { type: 'string' },
|
|
1339
|
+
new_content: { type: 'string' }
|
|
1340
|
+
},
|
|
1341
|
+
required: ['path', 'new_content']
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
170
1345
|
{
|
|
171
1346
|
type: 'function',
|
|
172
1347
|
function: {
|
|
@@ -207,7 +1382,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, session
|
|
|
207
1382
|
type: 'function',
|
|
208
1383
|
function: {
|
|
209
1384
|
name: 'run_command',
|
|
210
|
-
description: 'Execute a shell command in workspace',
|
|
1385
|
+
description: 'Execute a one-shot shell command in workspace. Do not use for long-running services.',
|
|
211
1386
|
parameters: {
|
|
212
1387
|
type: 'object',
|
|
213
1388
|
properties: {
|
|
@@ -216,10 +1391,109 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, session
|
|
|
216
1391
|
required: ['command']
|
|
217
1392
|
}
|
|
218
1393
|
}
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
type: 'function',
|
|
1397
|
+
function: {
|
|
1398
|
+
name: 'start_service',
|
|
1399
|
+
description: 'Start a long-running local service and return a compact service handle instead of blocking on process exit.',
|
|
1400
|
+
parameters: {
|
|
1401
|
+
type: 'object',
|
|
1402
|
+
properties: {
|
|
1403
|
+
command: { type: 'string' },
|
|
1404
|
+
startup_timeout_ms: { type: 'number' },
|
|
1405
|
+
success_matchers: {
|
|
1406
|
+
type: 'array',
|
|
1407
|
+
items: { type: 'string' }
|
|
1408
|
+
},
|
|
1409
|
+
port_probe: { type: 'number' },
|
|
1410
|
+
http_probe: {
|
|
1411
|
+
type: 'object',
|
|
1412
|
+
properties: {
|
|
1413
|
+
url: { type: 'string' },
|
|
1414
|
+
expect_status: { type: 'number' }
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
required: ['command']
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
type: 'function',
|
|
1424
|
+
function: {
|
|
1425
|
+
name: 'list_services',
|
|
1426
|
+
description: 'List all tracked local services and their compact current status.',
|
|
1427
|
+
parameters: {
|
|
1428
|
+
type: 'object',
|
|
1429
|
+
properties: {}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
type: 'function',
|
|
1435
|
+
function: {
|
|
1436
|
+
name: 'get_service_status',
|
|
1437
|
+
description: 'Get the current status of a previously started service.',
|
|
1438
|
+
parameters: {
|
|
1439
|
+
type: 'object',
|
|
1440
|
+
properties: {
|
|
1441
|
+
task_id: { type: 'string' }
|
|
1442
|
+
},
|
|
1443
|
+
required: ['task_id']
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
type: 'function',
|
|
1449
|
+
function: {
|
|
1450
|
+
name: 'get_service_logs',
|
|
1451
|
+
description: 'Read recent logs from a previously started service.',
|
|
1452
|
+
parameters: {
|
|
1453
|
+
type: 'object',
|
|
1454
|
+
properties: {
|
|
1455
|
+
task_id: { type: 'string' },
|
|
1456
|
+
tail: { type: 'number' },
|
|
1457
|
+
after_cursor: { type: 'number' }
|
|
1458
|
+
},
|
|
1459
|
+
required: ['task_id']
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
type: 'function',
|
|
1465
|
+
function: {
|
|
1466
|
+
name: 'stop_service',
|
|
1467
|
+
description: 'Stop a previously started service.',
|
|
1468
|
+
parameters: {
|
|
1469
|
+
type: 'object',
|
|
1470
|
+
properties: {
|
|
1471
|
+
task_id: { type: 'string' }
|
|
1472
|
+
},
|
|
1473
|
+
required: ['task_id']
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
219
1476
|
}
|
|
220
1477
|
];
|
|
221
1478
|
|
|
222
1479
|
const handlers = {
|
|
1480
|
+
locate: (args) => locate(workspaceRoot, args),
|
|
1481
|
+
open_target: (args) => openTarget(workspaceRoot, args),
|
|
1482
|
+
edit_target: (args) => editTarget(workspaceRoot, args),
|
|
1483
|
+
search_code: (args) => searchCode(workspaceRoot, args),
|
|
1484
|
+
read_block: (args) => readBlock(workspaceRoot, args),
|
|
1485
|
+
read_symbol_context: (args) => readSymbolContext(workspaceRoot, args),
|
|
1486
|
+
validate_edit: (args) => validateEdit(workspaceRoot, args),
|
|
1487
|
+
replace_block: (args) => replaceBlock(workspaceRoot, args),
|
|
1488
|
+
replace_text: (args) => replaceText(workspaceRoot, args),
|
|
1489
|
+
insert_before: (args) => insertRelative(workspaceRoot, args, 'insert_before'),
|
|
1490
|
+
insert_after: (args) => insertRelative(workspaceRoot, args, 'insert_after'),
|
|
1491
|
+
generate_diff: (args) => generateDiff(workspaceRoot, args),
|
|
1492
|
+
start_service: (args) => startService(workspaceRoot, config, args),
|
|
1493
|
+
list_services: () => listServices(workspaceRoot),
|
|
1494
|
+
get_service_status: (args) => getServiceStatus(workspaceRoot, args),
|
|
1495
|
+
get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
|
|
1496
|
+
stop_service: (args) => stopService(workspaceRoot, args),
|
|
223
1497
|
read_file: (args) =>
|
|
224
1498
|
readFile(workspaceRoot, {
|
|
225
1499
|
...args,
|