@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.
@@ -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
+ };