@cuylabs/agent-code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -0
- package/dist/chunk-IOXLKDEB.js +815 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +45 -0
- package/dist/tools/index.d.ts +360 -0
- package/dist/tools/index.js +42 -0
- package/package.json +75 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
// src/tools/bash.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Tool, truncateOutput } from "@cuylabs/agent-core";
|
|
4
|
+
var DEFAULT_TIMEOUT = 2 * 60 * 1e3;
|
|
5
|
+
var bashParameters = z.object({
|
|
6
|
+
command: z.string().describe("The shell command to execute"),
|
|
7
|
+
timeout: z.number().optional().describe("Timeout in milliseconds (default: 120000)"),
|
|
8
|
+
workdir: z.string().optional().describe("Working directory to run the command in. Defaults to current working directory."),
|
|
9
|
+
description: z.string().describe("Clear, concise description of what this command does in 5-10 words")
|
|
10
|
+
});
|
|
11
|
+
async function executeBash(params, host, cwd, abort) {
|
|
12
|
+
const workdir = params.workdir || cwd;
|
|
13
|
+
const timeout = params.timeout ?? DEFAULT_TIMEOUT;
|
|
14
|
+
if (!await host.exists(workdir)) {
|
|
15
|
+
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
16
|
+
}
|
|
17
|
+
return host.exec(params.command, {
|
|
18
|
+
cwd: workdir,
|
|
19
|
+
timeout,
|
|
20
|
+
signal: abort,
|
|
21
|
+
env: {
|
|
22
|
+
// Force color output
|
|
23
|
+
FORCE_COLOR: "1",
|
|
24
|
+
TERM: "xterm-256color"
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
var bashTool = Tool.define("bash", {
|
|
29
|
+
description: `Execute a shell command in the working directory.
|
|
30
|
+
|
|
31
|
+
IMPORTANT:
|
|
32
|
+
- Commands run in a non-interactive shell
|
|
33
|
+
- Long-running commands will timeout after 2 minutes by default
|
|
34
|
+
- Use 'workdir' parameter instead of 'cd' commands
|
|
35
|
+
- Output is automatically truncated if too long
|
|
36
|
+
- For pager commands, output is piped through 'cat' automatically`,
|
|
37
|
+
parameters: bashParameters,
|
|
38
|
+
async execute(params, ctx) {
|
|
39
|
+
const host = ctx.host;
|
|
40
|
+
const result = await executeBash(params, host, ctx.cwd, ctx.abort);
|
|
41
|
+
let output = "";
|
|
42
|
+
if (result.stdout) {
|
|
43
|
+
output += result.stdout;
|
|
44
|
+
}
|
|
45
|
+
if (result.stderr) {
|
|
46
|
+
if (output) output += "\n";
|
|
47
|
+
output += `[stderr]
|
|
48
|
+
${result.stderr}`;
|
|
49
|
+
}
|
|
50
|
+
if (result.timedOut) {
|
|
51
|
+
output += `
|
|
52
|
+
|
|
53
|
+
[Command timed out after ${params.timeout ?? DEFAULT_TIMEOUT}ms]`;
|
|
54
|
+
} else if (result.exitCode !== 0) {
|
|
55
|
+
output += `
|
|
56
|
+
|
|
57
|
+
[Exit code: ${result.exitCode}]`;
|
|
58
|
+
}
|
|
59
|
+
const truncated = truncateOutput(output);
|
|
60
|
+
return {
|
|
61
|
+
title: params.description || params.command.slice(0, 50),
|
|
62
|
+
output: truncated.content,
|
|
63
|
+
metadata: {
|
|
64
|
+
exitCode: result.exitCode,
|
|
65
|
+
timedOut: result.timedOut,
|
|
66
|
+
truncated: truncated.truncated,
|
|
67
|
+
outputPath: truncated.outputPath
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/tools/read.ts
|
|
74
|
+
import { z as z2 } from "zod";
|
|
75
|
+
import * as path from "path";
|
|
76
|
+
import { Tool as Tool2 } from "@cuylabs/agent-core";
|
|
77
|
+
var DEFAULT_READ_LIMIT = 2e3;
|
|
78
|
+
var MAX_LINE_LENGTH = 2e3;
|
|
79
|
+
var MAX_BYTES = 50 * 1024;
|
|
80
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
81
|
+
".zip",
|
|
82
|
+
".tar",
|
|
83
|
+
".gz",
|
|
84
|
+
".exe",
|
|
85
|
+
".dll",
|
|
86
|
+
".so",
|
|
87
|
+
".class",
|
|
88
|
+
".jar",
|
|
89
|
+
".war",
|
|
90
|
+
".7z",
|
|
91
|
+
".doc",
|
|
92
|
+
".docx",
|
|
93
|
+
".xls",
|
|
94
|
+
".xlsx",
|
|
95
|
+
".ppt",
|
|
96
|
+
".pptx",
|
|
97
|
+
".odt",
|
|
98
|
+
".ods",
|
|
99
|
+
".odp",
|
|
100
|
+
".bin",
|
|
101
|
+
".dat",
|
|
102
|
+
".obj",
|
|
103
|
+
".o",
|
|
104
|
+
".a",
|
|
105
|
+
".lib",
|
|
106
|
+
".wasm",
|
|
107
|
+
".pyc",
|
|
108
|
+
".pyo",
|
|
109
|
+
".png",
|
|
110
|
+
".jpg",
|
|
111
|
+
".jpeg",
|
|
112
|
+
".gif",
|
|
113
|
+
".bmp",
|
|
114
|
+
".ico",
|
|
115
|
+
".webp",
|
|
116
|
+
".mp3",
|
|
117
|
+
".mp4",
|
|
118
|
+
".avi",
|
|
119
|
+
".mov",
|
|
120
|
+
".mkv",
|
|
121
|
+
".wav",
|
|
122
|
+
".flac"
|
|
123
|
+
]);
|
|
124
|
+
async function isBinaryFile(filepath, host) {
|
|
125
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
126
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const buffer = await host.readBytes(filepath, 0, 4096);
|
|
131
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
132
|
+
if (buffer[i] === 0) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
var readParameters = z2.object({
|
|
142
|
+
filePath: z2.string().describe("The path to the file to read"),
|
|
143
|
+
offset: z2.number().optional().describe("The line number to start reading from (0-based)"),
|
|
144
|
+
limit: z2.number().optional().describe(`The number of lines to read (defaults to ${DEFAULT_READ_LIMIT})`)
|
|
145
|
+
});
|
|
146
|
+
var readTool = Tool2.define("read", {
|
|
147
|
+
description: `Read the contents of a file.
|
|
148
|
+
|
|
149
|
+
IMPORTANT:
|
|
150
|
+
- Returns content with line numbers for easy reference
|
|
151
|
+
- Large files are automatically truncated
|
|
152
|
+
- Use 'offset' parameter to read beyond the default limit
|
|
153
|
+
- Binary files cannot be read
|
|
154
|
+
- Images and PDFs are returned as attachments`,
|
|
155
|
+
parameters: readParameters,
|
|
156
|
+
async execute(params, ctx) {
|
|
157
|
+
const host = ctx.host;
|
|
158
|
+
let filepath = params.filePath;
|
|
159
|
+
if (!path.isAbsolute(filepath)) {
|
|
160
|
+
filepath = path.resolve(ctx.cwd, filepath);
|
|
161
|
+
}
|
|
162
|
+
const title = path.relative(ctx.cwd, filepath);
|
|
163
|
+
if (!await host.exists(filepath)) {
|
|
164
|
+
const dir = path.dirname(filepath);
|
|
165
|
+
const base = path.basename(filepath);
|
|
166
|
+
let suggestions = [];
|
|
167
|
+
try {
|
|
168
|
+
const entries = await host.readdir(dir);
|
|
169
|
+
suggestions = entries.filter(
|
|
170
|
+
(entry) => entry.name.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.name.toLowerCase())
|
|
171
|
+
).map((entry) => path.join(dir, entry.name)).slice(0, 3);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
if (suggestions.length > 0) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`File not found: ${filepath}
|
|
177
|
+
|
|
178
|
+
Did you mean one of these?
|
|
179
|
+
${suggestions.join("\n")}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`File not found: ${filepath}`);
|
|
183
|
+
}
|
|
184
|
+
if (await isBinaryFile(filepath, host)) {
|
|
185
|
+
throw new Error(`Cannot read binary file: ${filepath}`);
|
|
186
|
+
}
|
|
187
|
+
const content = await host.readFile(filepath);
|
|
188
|
+
const allLines = content.split("\n");
|
|
189
|
+
const limit = params.limit ?? DEFAULT_READ_LIMIT;
|
|
190
|
+
const offset = params.offset || 0;
|
|
191
|
+
const lines = [];
|
|
192
|
+
let bytes = 0;
|
|
193
|
+
let truncatedByBytes = false;
|
|
194
|
+
for (let i = offset; i < Math.min(allLines.length, offset + limit); i++) {
|
|
195
|
+
let line = allLines[i];
|
|
196
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
197
|
+
line = line.substring(0, MAX_LINE_LENGTH) + "...";
|
|
198
|
+
}
|
|
199
|
+
const size = Buffer.byteLength(line, "utf-8") + (lines.length > 0 ? 1 : 0);
|
|
200
|
+
if (bytes + size > MAX_BYTES) {
|
|
201
|
+
truncatedByBytes = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
lines.push(line);
|
|
205
|
+
bytes += size;
|
|
206
|
+
}
|
|
207
|
+
const formatted = lines.map((line, index) => {
|
|
208
|
+
const lineNum = (index + offset + 1).toString().padStart(5, "0");
|
|
209
|
+
return `${lineNum}| ${line}`;
|
|
210
|
+
});
|
|
211
|
+
let output = "<file>\n";
|
|
212
|
+
output += formatted.join("\n");
|
|
213
|
+
const totalLines = allLines.length;
|
|
214
|
+
const lastReadLine = offset + lines.length;
|
|
215
|
+
const hasMoreLines = totalLines > lastReadLine;
|
|
216
|
+
const truncated = hasMoreLines || truncatedByBytes;
|
|
217
|
+
if (truncatedByBytes) {
|
|
218
|
+
output += `
|
|
219
|
+
|
|
220
|
+
(Output truncated at ${MAX_BYTES} bytes. Use 'offset' parameter to read beyond line ${lastReadLine})`;
|
|
221
|
+
} else if (hasMoreLines) {
|
|
222
|
+
output += `
|
|
223
|
+
|
|
224
|
+
(File has more lines. Use 'offset' parameter to read beyond line ${lastReadLine})`;
|
|
225
|
+
} else {
|
|
226
|
+
output += `
|
|
227
|
+
|
|
228
|
+
(End of file - total ${totalLines} lines)`;
|
|
229
|
+
}
|
|
230
|
+
output += "\n</file>";
|
|
231
|
+
return {
|
|
232
|
+
title,
|
|
233
|
+
output,
|
|
234
|
+
metadata: {
|
|
235
|
+
totalLines,
|
|
236
|
+
linesRead: lines.length,
|
|
237
|
+
offset,
|
|
238
|
+
truncated,
|
|
239
|
+
preview: lines.slice(0, 20).join("\n")
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// src/tools/edit.ts
|
|
246
|
+
import { z as z3 } from "zod";
|
|
247
|
+
import * as path2 from "path";
|
|
248
|
+
import { Tool as Tool3 } from "@cuylabs/agent-core";
|
|
249
|
+
function normalizeLineEndings(text) {
|
|
250
|
+
return text.replaceAll("\r\n", "\n");
|
|
251
|
+
}
|
|
252
|
+
function createDiff(filepath, oldContent, newContent) {
|
|
253
|
+
const oldLines = normalizeLineEndings(oldContent).split("\n");
|
|
254
|
+
const newLines = normalizeLineEndings(newContent).split("\n");
|
|
255
|
+
const diff = [];
|
|
256
|
+
diff.push(`--- ${filepath}`);
|
|
257
|
+
diff.push(`+++ ${filepath}`);
|
|
258
|
+
let i = 0, j = 0;
|
|
259
|
+
let chunk = [];
|
|
260
|
+
let chunkStart = 0;
|
|
261
|
+
let inChunk = false;
|
|
262
|
+
while (i < oldLines.length || j < newLines.length) {
|
|
263
|
+
if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
|
|
264
|
+
if (inChunk && chunk.length > 0) {
|
|
265
|
+
const context = oldLines.slice(i, Math.min(i + 3, oldLines.length));
|
|
266
|
+
chunk.push(...context.map((l) => ` ${l}`));
|
|
267
|
+
diff.push(`@@ -${chunkStart + 1} +${chunkStart + 1} @@`);
|
|
268
|
+
diff.push(...chunk);
|
|
269
|
+
chunk = [];
|
|
270
|
+
inChunk = false;
|
|
271
|
+
}
|
|
272
|
+
i++;
|
|
273
|
+
j++;
|
|
274
|
+
} else {
|
|
275
|
+
if (!inChunk) {
|
|
276
|
+
chunkStart = Math.max(0, i - 3);
|
|
277
|
+
const context = oldLines.slice(chunkStart, i);
|
|
278
|
+
chunk = context.map((l) => ` ${l}`);
|
|
279
|
+
inChunk = true;
|
|
280
|
+
}
|
|
281
|
+
if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
|
|
282
|
+
chunk.push(`-${oldLines[i]}`);
|
|
283
|
+
i++;
|
|
284
|
+
}
|
|
285
|
+
if (j < newLines.length && (i >= oldLines.length || oldLines[i - 1] !== newLines[j])) {
|
|
286
|
+
chunk.push(`+${newLines[j]}`);
|
|
287
|
+
j++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (chunk.length > 0) {
|
|
292
|
+
diff.push(`@@ -${chunkStart + 1} +${chunkStart + 1} @@`);
|
|
293
|
+
diff.push(...chunk);
|
|
294
|
+
}
|
|
295
|
+
return diff.join("\n");
|
|
296
|
+
}
|
|
297
|
+
function findTextFuzzy(content, searchText) {
|
|
298
|
+
const exactIndex = content.indexOf(searchText);
|
|
299
|
+
if (exactIndex !== -1) {
|
|
300
|
+
return { index: exactIndex, match: searchText };
|
|
301
|
+
}
|
|
302
|
+
const normalizedSearch = searchText.replace(/\s+/g, " ").trim();
|
|
303
|
+
const normalizedContent = content.replace(/\s+/g, " ");
|
|
304
|
+
const normalizedIndex = normalizedContent.indexOf(normalizedSearch);
|
|
305
|
+
if (normalizedIndex !== -1) {
|
|
306
|
+
let origIndex = 0;
|
|
307
|
+
let normIndex = 0;
|
|
308
|
+
while (normIndex < normalizedIndex && origIndex < content.length) {
|
|
309
|
+
if (/\s/.test(content[origIndex])) {
|
|
310
|
+
while (origIndex < content.length && /\s/.test(content[origIndex])) {
|
|
311
|
+
origIndex++;
|
|
312
|
+
}
|
|
313
|
+
normIndex++;
|
|
314
|
+
} else {
|
|
315
|
+
origIndex++;
|
|
316
|
+
normIndex++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
let endOrigIndex = origIndex;
|
|
320
|
+
let endNormIndex = normIndex;
|
|
321
|
+
while (endNormIndex < normalizedIndex + normalizedSearch.length && endOrigIndex < content.length) {
|
|
322
|
+
if (/\s/.test(content[endOrigIndex])) {
|
|
323
|
+
while (endOrigIndex < content.length && /\s/.test(content[endOrigIndex])) {
|
|
324
|
+
endOrigIndex++;
|
|
325
|
+
}
|
|
326
|
+
endNormIndex++;
|
|
327
|
+
} else {
|
|
328
|
+
endOrigIndex++;
|
|
329
|
+
endNormIndex++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
index: origIndex,
|
|
334
|
+
match: content.slice(origIndex, endOrigIndex)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
var editParameters = z3.object({
|
|
340
|
+
filePath: z3.string().describe("The absolute path to the file to modify"),
|
|
341
|
+
oldString: z3.string().describe("The text to replace (must match exactly or with minor whitespace differences)"),
|
|
342
|
+
newString: z3.string().describe("The text to replace it with (must be different from oldString)"),
|
|
343
|
+
replaceAll: z3.boolean().optional().describe("Replace all occurrences of oldString (default: false)")
|
|
344
|
+
});
|
|
345
|
+
var editTool = Tool3.define("edit", {
|
|
346
|
+
description: `Edit a file by replacing exact text with new text.
|
|
347
|
+
|
|
348
|
+
IMPORTANT:
|
|
349
|
+
- The oldString must match text in the file exactly (whitespace matters)
|
|
350
|
+
- oldString and newString must be different
|
|
351
|
+
- By default, only the first occurrence is replaced
|
|
352
|
+
- Use replaceAll: true to replace all occurrences
|
|
353
|
+
- For creating new files, use the write tool instead`,
|
|
354
|
+
parameters: editParameters,
|
|
355
|
+
// Enable automatic baseline capture for turn tracking
|
|
356
|
+
fileOps: {
|
|
357
|
+
pathArgs: ["filePath"],
|
|
358
|
+
operationType: "write"
|
|
359
|
+
},
|
|
360
|
+
async execute(params, ctx) {
|
|
361
|
+
if (params.oldString === params.newString) {
|
|
362
|
+
throw new Error("oldString and newString must be different");
|
|
363
|
+
}
|
|
364
|
+
let filepath = params.filePath;
|
|
365
|
+
if (!path2.isAbsolute(filepath)) {
|
|
366
|
+
filepath = path2.resolve(ctx.cwd, filepath);
|
|
367
|
+
}
|
|
368
|
+
const host = ctx.host;
|
|
369
|
+
const title = path2.relative(ctx.cwd, filepath);
|
|
370
|
+
if (params.oldString === "") {
|
|
371
|
+
await host.mkdir(path2.dirname(filepath));
|
|
372
|
+
await host.writeFile(filepath, params.newString);
|
|
373
|
+
return {
|
|
374
|
+
title,
|
|
375
|
+
output: `Created new file: ${filepath}`,
|
|
376
|
+
metadata: {
|
|
377
|
+
filepath,
|
|
378
|
+
created: true,
|
|
379
|
+
additions: params.newString.split("\n").length,
|
|
380
|
+
deletions: 0,
|
|
381
|
+
diff: ""
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
let content;
|
|
386
|
+
try {
|
|
387
|
+
content = await host.readFile(filepath);
|
|
388
|
+
} catch {
|
|
389
|
+
throw new Error(`File not found: ${filepath}`);
|
|
390
|
+
}
|
|
391
|
+
const found = findTextFuzzy(content, params.oldString);
|
|
392
|
+
if (!found) {
|
|
393
|
+
const lines = content.split("\n").slice(0, 50);
|
|
394
|
+
throw new Error(
|
|
395
|
+
`Could not find the specified text in the file.
|
|
396
|
+
|
|
397
|
+
First 50 lines of file:
|
|
398
|
+
${lines.map((l, i) => `${i + 1}: ${l}`).join("\n")}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
let newContent;
|
|
402
|
+
if (params.replaceAll) {
|
|
403
|
+
newContent = content.split(found.match).join(params.newString);
|
|
404
|
+
} else {
|
|
405
|
+
newContent = content.slice(0, found.index) + params.newString + content.slice(found.index + found.match.length);
|
|
406
|
+
}
|
|
407
|
+
await host.writeFile(filepath, newContent);
|
|
408
|
+
const diff = createDiff(filepath, content, newContent);
|
|
409
|
+
let additions = 0;
|
|
410
|
+
let deletions = 0;
|
|
411
|
+
const oldLines = content.split("\n").length;
|
|
412
|
+
const newLines = newContent.split("\n").length;
|
|
413
|
+
additions = Math.max(0, newLines - oldLines);
|
|
414
|
+
deletions = Math.max(0, oldLines - newLines);
|
|
415
|
+
return {
|
|
416
|
+
title,
|
|
417
|
+
output: `Edit applied successfully.
|
|
418
|
+
|
|
419
|
+
${diff}`,
|
|
420
|
+
metadata: {
|
|
421
|
+
filepath,
|
|
422
|
+
created: false,
|
|
423
|
+
additions,
|
|
424
|
+
deletions,
|
|
425
|
+
diff
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// src/tools/write.ts
|
|
432
|
+
import { z as z4 } from "zod";
|
|
433
|
+
import * as path3 from "path";
|
|
434
|
+
import { Tool as Tool4 } from "@cuylabs/agent-core";
|
|
435
|
+
var writeParameters = z4.object({
|
|
436
|
+
filePath: z4.string().describe("The absolute path to the file to write"),
|
|
437
|
+
content: z4.string().describe("The content to write to the file")
|
|
438
|
+
});
|
|
439
|
+
var writeTool = Tool4.define("write", {
|
|
440
|
+
description: `Write content to a file, creating it if it doesn't exist or overwriting if it does.
|
|
441
|
+
|
|
442
|
+
IMPORTANT:
|
|
443
|
+
- Use this for creating new files
|
|
444
|
+
- For modifying existing files, prefer the edit tool
|
|
445
|
+
- Parent directories are created automatically
|
|
446
|
+
- Existing content is completely replaced`,
|
|
447
|
+
parameters: writeParameters,
|
|
448
|
+
// Enable automatic baseline capture for turn tracking
|
|
449
|
+
fileOps: {
|
|
450
|
+
pathArgs: ["filePath"],
|
|
451
|
+
operationType: "write"
|
|
452
|
+
},
|
|
453
|
+
async execute(params, ctx) {
|
|
454
|
+
let filepath = params.filePath;
|
|
455
|
+
if (!path3.isAbsolute(filepath)) {
|
|
456
|
+
filepath = path3.resolve(ctx.cwd, filepath);
|
|
457
|
+
}
|
|
458
|
+
const title = path3.relative(ctx.cwd, filepath);
|
|
459
|
+
const host = ctx.host;
|
|
460
|
+
let exists = false;
|
|
461
|
+
let oldContent = "";
|
|
462
|
+
try {
|
|
463
|
+
oldContent = await host.readFile(filepath);
|
|
464
|
+
exists = true;
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
await host.mkdir(path3.dirname(filepath));
|
|
468
|
+
await host.writeFile(filepath, params.content);
|
|
469
|
+
const action = exists ? "Updated" : "Created";
|
|
470
|
+
let output = `${action} file: ${filepath}`;
|
|
471
|
+
if (exists && oldContent !== params.content) {
|
|
472
|
+
const oldLines = oldContent.split("\n").length;
|
|
473
|
+
const newLines = params.content.split("\n").length;
|
|
474
|
+
output += `
|
|
475
|
+
|
|
476
|
+
Changes: ${oldLines} \u2192 ${newLines} lines`;
|
|
477
|
+
} else if (!exists) {
|
|
478
|
+
const lines = params.content.split("\n").length;
|
|
479
|
+
output += `
|
|
480
|
+
|
|
481
|
+
Created with ${lines} lines`;
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
title,
|
|
485
|
+
output,
|
|
486
|
+
metadata: {
|
|
487
|
+
filepath,
|
|
488
|
+
created: !exists,
|
|
489
|
+
updated: exists,
|
|
490
|
+
lines: params.content.split("\n").length
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// src/tools/grep.ts
|
|
497
|
+
import { z as z5 } from "zod";
|
|
498
|
+
import * as path4 from "path";
|
|
499
|
+
import { Tool as Tool5 } from "@cuylabs/agent-core";
|
|
500
|
+
var MAX_LINE_LENGTH2 = 2e3;
|
|
501
|
+
var MAX_RESULTS = 100;
|
|
502
|
+
function sq(s) {
|
|
503
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
504
|
+
}
|
|
505
|
+
var grepParameters = z5.object({
|
|
506
|
+
pattern: z5.string().describe("The regex pattern to search for in file contents"),
|
|
507
|
+
path: z5.string().optional().describe("The directory to search in. Defaults to current working directory."),
|
|
508
|
+
include: z5.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")')
|
|
509
|
+
});
|
|
510
|
+
async function executeGrep(params, host, cwd, abort) {
|
|
511
|
+
const searchPath = params.path ? path4.isAbsolute(params.path) ? params.path : path4.resolve(cwd, params.path) : cwd;
|
|
512
|
+
const parts = [
|
|
513
|
+
"rg",
|
|
514
|
+
"-nH",
|
|
515
|
+
"--hidden",
|
|
516
|
+
"--no-messages",
|
|
517
|
+
sq("--field-match-separator=|"),
|
|
518
|
+
"--regexp",
|
|
519
|
+
sq(params.pattern)
|
|
520
|
+
];
|
|
521
|
+
if (params.include) {
|
|
522
|
+
parts.push("--glob", sq(params.include));
|
|
523
|
+
}
|
|
524
|
+
parts.push(sq(searchPath));
|
|
525
|
+
const result = await host.exec(parts.join(" "), {
|
|
526
|
+
cwd: searchPath,
|
|
527
|
+
signal: abort
|
|
528
|
+
});
|
|
529
|
+
if (result.exitCode === 1 || result.exitCode === 2 && !result.stdout.trim()) {
|
|
530
|
+
return { matches: [], truncated: false };
|
|
531
|
+
}
|
|
532
|
+
if (result.exitCode !== 0 && result.exitCode !== 2) {
|
|
533
|
+
throw new Error(`ripgrep failed: ${result.stderr}`);
|
|
534
|
+
}
|
|
535
|
+
const lines = result.stdout.trim().split(/\r?\n/);
|
|
536
|
+
const matches = [];
|
|
537
|
+
let truncated = false;
|
|
538
|
+
for (const line of lines) {
|
|
539
|
+
if (!line) continue;
|
|
540
|
+
const [filePath, lineNumStr, ...textParts] = line.split("|");
|
|
541
|
+
if (!filePath || !lineNumStr || textParts.length === 0) continue;
|
|
542
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
543
|
+
let lineText = textParts.join("|");
|
|
544
|
+
if (lineText.length > MAX_LINE_LENGTH2) {
|
|
545
|
+
lineText = lineText.slice(0, MAX_LINE_LENGTH2) + "...";
|
|
546
|
+
}
|
|
547
|
+
if (matches.length >= MAX_RESULTS) {
|
|
548
|
+
truncated = true;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
matches.push({
|
|
552
|
+
file: filePath,
|
|
553
|
+
line: lineNum,
|
|
554
|
+
text: lineText
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return { matches, truncated };
|
|
558
|
+
}
|
|
559
|
+
var grepTool = Tool5.define("grep", {
|
|
560
|
+
description: `Search for a pattern in file contents using ripgrep.
|
|
561
|
+
|
|
562
|
+
IMPORTANT:
|
|
563
|
+
- Uses regex pattern matching
|
|
564
|
+
- Searches recursively in the specified directory
|
|
565
|
+
- Use 'include' to filter by file extension
|
|
566
|
+
- Results are sorted by modification time (newest first)
|
|
567
|
+
- Maximum ${MAX_RESULTS} results returned`,
|
|
568
|
+
parameters: grepParameters,
|
|
569
|
+
async execute(params, ctx) {
|
|
570
|
+
if (!params.pattern) {
|
|
571
|
+
throw new Error("pattern is required");
|
|
572
|
+
}
|
|
573
|
+
const host = ctx.host;
|
|
574
|
+
const result = await executeGrep(params, host, ctx.cwd, ctx.abort);
|
|
575
|
+
if (result.matches.length === 0) {
|
|
576
|
+
return {
|
|
577
|
+
title: params.pattern,
|
|
578
|
+
output: "No matches found",
|
|
579
|
+
metadata: { matches: 0, truncated: false }
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const output = result.matches.map((m) => `${m.file}:${m.line}: ${m.text}`);
|
|
583
|
+
if (result.truncated) {
|
|
584
|
+
output.push("");
|
|
585
|
+
output.push(`(Results truncated to ${MAX_RESULTS} matches)`);
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
title: params.pattern,
|
|
589
|
+
output: output.join("\n"),
|
|
590
|
+
metadata: {
|
|
591
|
+
matches: result.matches.length,
|
|
592
|
+
truncated: result.truncated
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// src/tools/glob.ts
|
|
599
|
+
import { z as z6 } from "zod";
|
|
600
|
+
import * as path5 from "path";
|
|
601
|
+
import { Tool as Tool6 } from "@cuylabs/agent-core";
|
|
602
|
+
var MAX_RESULTS2 = 100;
|
|
603
|
+
function sq2(s) {
|
|
604
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
605
|
+
}
|
|
606
|
+
async function findFiles(pattern, cwd, host, abort) {
|
|
607
|
+
const files = [];
|
|
608
|
+
let truncated = false;
|
|
609
|
+
try {
|
|
610
|
+
const result = await host.exec(
|
|
611
|
+
`rg --files --glob ${sq2(pattern)}`,
|
|
612
|
+
{ cwd, signal: abort }
|
|
613
|
+
);
|
|
614
|
+
if (result.exitCode === 0 || result.exitCode === 1) {
|
|
615
|
+
const found = result.stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
616
|
+
for (const file of found) {
|
|
617
|
+
if (files.length >= MAX_RESULTS2) {
|
|
618
|
+
truncated = true;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
files.push(path5.resolve(cwd, file));
|
|
622
|
+
}
|
|
623
|
+
return { files, truncated };
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
const globPattern = new RegExp(
|
|
628
|
+
"^" + pattern.replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/<<<DOUBLESTAR>>>/g, ".*") + "$"
|
|
629
|
+
);
|
|
630
|
+
async function walk(dir, base = "") {
|
|
631
|
+
if (abort?.aborted) return;
|
|
632
|
+
if (files.length >= MAX_RESULTS2) {
|
|
633
|
+
truncated = true;
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const entries = await host.readdir(dir);
|
|
637
|
+
for (const entry of entries) {
|
|
638
|
+
if (abort?.aborted || files.length >= MAX_RESULTS2) {
|
|
639
|
+
truncated = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const relativePath = base ? `${base}/${entry.name}` : entry.name;
|
|
646
|
+
const fullPath = path5.join(dir, entry.name);
|
|
647
|
+
if (entry.isDirectory) {
|
|
648
|
+
await walk(fullPath, relativePath);
|
|
649
|
+
} else if (globPattern.test(relativePath)) {
|
|
650
|
+
files.push(fullPath);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
await walk(cwd);
|
|
655
|
+
return { files, truncated };
|
|
656
|
+
}
|
|
657
|
+
var globParameters = z6.object({
|
|
658
|
+
pattern: z6.string().describe('The glob pattern to match files (e.g. "**/*.ts", "src/**/*.js")'),
|
|
659
|
+
path: z6.string().optional().describe("The directory to search in. Defaults to current working directory.")
|
|
660
|
+
});
|
|
661
|
+
var globTool = Tool6.define("glob", {
|
|
662
|
+
description: `Find files matching a glob pattern.
|
|
663
|
+
|
|
664
|
+
IMPORTANT:
|
|
665
|
+
- Use ** to match any directory depth
|
|
666
|
+
- Use * to match any characters within a path segment
|
|
667
|
+
- Results are sorted by modification time (newest first)
|
|
668
|
+
- Maximum ${MAX_RESULTS2} results returned`,
|
|
669
|
+
parameters: globParameters,
|
|
670
|
+
async execute(params, ctx) {
|
|
671
|
+
const host = ctx.host;
|
|
672
|
+
const searchPath = params.path ? path5.isAbsolute(params.path) ? params.path : path5.resolve(ctx.cwd, params.path) : ctx.cwd;
|
|
673
|
+
const { files, truncated } = await findFiles(params.pattern, searchPath, host, ctx.abort);
|
|
674
|
+
if (files.length === 0) {
|
|
675
|
+
return {
|
|
676
|
+
title: params.pattern,
|
|
677
|
+
output: "No files found",
|
|
678
|
+
metadata: { count: 0, truncated: false }
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const filesWithMtime = await Promise.all(
|
|
682
|
+
files.map(async (f) => {
|
|
683
|
+
try {
|
|
684
|
+
const s = await host.stat(f);
|
|
685
|
+
return { path: f, mtime: s.mtime.getTime() };
|
|
686
|
+
} catch {
|
|
687
|
+
return { path: f, mtime: 0 };
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
filesWithMtime.sort((a, b) => b.mtime - a.mtime);
|
|
692
|
+
const output = filesWithMtime.map((f) => f.path);
|
|
693
|
+
if (truncated) {
|
|
694
|
+
output.push("");
|
|
695
|
+
output.push(`(Results truncated to ${MAX_RESULTS2} files)`);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
title: path5.relative(ctx.cwd, searchPath) || ".",
|
|
699
|
+
output: output.join("\n"),
|
|
700
|
+
metadata: {
|
|
701
|
+
count: files.length,
|
|
702
|
+
truncated
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// src/tools/defaults.ts
|
|
709
|
+
var defaultCodingTools = [
|
|
710
|
+
bashTool,
|
|
711
|
+
readTool,
|
|
712
|
+
editTool,
|
|
713
|
+
writeTool,
|
|
714
|
+
grepTool,
|
|
715
|
+
globTool
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
// src/tools/toolset.ts
|
|
719
|
+
import { defaultRegistry } from "@cuylabs/agent-core";
|
|
720
|
+
function setupToolRegistry() {
|
|
721
|
+
for (const tool of [bashTool, readTool, editTool, writeTool, grepTool, globTool]) {
|
|
722
|
+
defaultRegistry.set(tool);
|
|
723
|
+
}
|
|
724
|
+
defaultRegistry.registerGroup("all", ["bash", "read", "edit", "write", "grep", "glob"]);
|
|
725
|
+
defaultRegistry.registerGroup("read-only", ["read", "grep", "glob"]);
|
|
726
|
+
defaultRegistry.registerGroup("safe", ["read", "edit", "write", "grep", "glob"]);
|
|
727
|
+
defaultRegistry.registerGroup("minimal", ["read", "write"]);
|
|
728
|
+
}
|
|
729
|
+
var ToolsetBuilder = class {
|
|
730
|
+
tools;
|
|
731
|
+
constructor(base) {
|
|
732
|
+
setupToolRegistry();
|
|
733
|
+
this.tools = /* @__PURE__ */ new Map();
|
|
734
|
+
if (base !== void 0) {
|
|
735
|
+
const resolved = defaultRegistry.resolve(base);
|
|
736
|
+
for (const tool of resolved) {
|
|
737
|
+
this.tools.set(tool.id, tool);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/** Add a tool by ID (from registry) or by instance. */
|
|
742
|
+
add(tool) {
|
|
743
|
+
if (typeof tool === "string") {
|
|
744
|
+
if (defaultRegistry.hasGroup(tool)) {
|
|
745
|
+
const group = defaultRegistry.getGroup(tool);
|
|
746
|
+
for (const t of group) {
|
|
747
|
+
this.tools.set(t.id, t);
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
const resolved = defaultRegistry.get(tool);
|
|
751
|
+
if (resolved) {
|
|
752
|
+
this.tools.set(resolved.id, resolved);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
this.tools.set(tool.id, tool);
|
|
757
|
+
}
|
|
758
|
+
return this;
|
|
759
|
+
}
|
|
760
|
+
/** Remove a tool by ID. */
|
|
761
|
+
remove(id) {
|
|
762
|
+
if (defaultRegistry.hasGroup(id)) {
|
|
763
|
+
const group = defaultRegistry.getGroup(id);
|
|
764
|
+
for (const t of group) {
|
|
765
|
+
this.tools.delete(t.id);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
this.tools.delete(id);
|
|
769
|
+
}
|
|
770
|
+
return this;
|
|
771
|
+
}
|
|
772
|
+
/** Keep only the specified tool IDs (intersection). */
|
|
773
|
+
only(...ids) {
|
|
774
|
+
const keep = new Set(ids);
|
|
775
|
+
for (const id of Array.from(this.tools.keys())) {
|
|
776
|
+
if (!keep.has(id)) {
|
|
777
|
+
this.tools.delete(id);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return this;
|
|
781
|
+
}
|
|
782
|
+
/** Return the final tool array. */
|
|
783
|
+
build() {
|
|
784
|
+
return Array.from(this.tools.values());
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
function toolset(base) {
|
|
788
|
+
return new ToolsetBuilder(base);
|
|
789
|
+
}
|
|
790
|
+
function resolveTools(spec) {
|
|
791
|
+
setupToolRegistry();
|
|
792
|
+
return defaultRegistry.resolve(spec);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export {
|
|
796
|
+
bashParameters,
|
|
797
|
+
executeBash,
|
|
798
|
+
bashTool,
|
|
799
|
+
readParameters,
|
|
800
|
+
readTool,
|
|
801
|
+
editParameters,
|
|
802
|
+
editTool,
|
|
803
|
+
writeParameters,
|
|
804
|
+
writeTool,
|
|
805
|
+
grepParameters,
|
|
806
|
+
executeGrep,
|
|
807
|
+
grepTool,
|
|
808
|
+
globParameters,
|
|
809
|
+
globTool,
|
|
810
|
+
defaultCodingTools,
|
|
811
|
+
setupToolRegistry,
|
|
812
|
+
ToolsetBuilder,
|
|
813
|
+
toolset,
|
|
814
|
+
resolveTools
|
|
815
|
+
};
|