@agentscope-ai/agentscope 0.0.2
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/dist/agent/index.d.mts +234 -0
- package/dist/agent/index.d.ts +234 -0
- package/dist/agent/index.js +1412 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/index.mjs +1375 -0
- package/dist/agent/index.mjs.map +1 -0
- package/dist/base-BOx3UzOl.d.mts +41 -0
- package/dist/base-BoIps2RL.d.ts +41 -0
- package/dist/base-C7jwyH4Z.d.mts +52 -0
- package/dist/base-Cwi4bjze.d.ts +127 -0
- package/dist/base-DYlBMCy_.d.mts +127 -0
- package/dist/base-NX-knWOv.d.ts +52 -0
- package/dist/block-VsnHrllL.d.mts +48 -0
- package/dist/block-VsnHrllL.d.ts +48 -0
- package/dist/event/index.d.mts +181 -0
- package/dist/event/index.d.ts +181 -0
- package/dist/event/index.js +58 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/index.mjs +33 -0
- package/dist/event/index.mjs.map +1 -0
- package/dist/formatter/index.d.mts +187 -0
- package/dist/formatter/index.d.ts +187 -0
- package/dist/formatter/index.js +647 -0
- package/dist/formatter/index.js.map +1 -0
- package/dist/formatter/index.mjs +616 -0
- package/dist/formatter/index.mjs.map +1 -0
- package/dist/index-BTJDlKvQ.d.mts +195 -0
- package/dist/index-BcatlwXQ.d.ts +195 -0
- package/dist/index-CAxQAkiP.d.mts +21 -0
- package/dist/index-CAxQAkiP.d.ts +21 -0
- package/dist/mcp/index.d.mts +9 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.js +432 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/index.mjs +408 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/message/index.d.mts +10 -0
- package/dist/message/index.d.ts +10 -0
- package/dist/message/index.js +67 -0
- package/dist/message/index.js.map +1 -0
- package/dist/message/index.mjs +37 -0
- package/dist/message/index.mjs.map +1 -0
- package/dist/message-CkN21KaY.d.mts +99 -0
- package/dist/message-CzLeTlua.d.ts +99 -0
- package/dist/model/index.d.mts +377 -0
- package/dist/model/index.d.ts +377 -0
- package/dist/model/index.js +1880 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/index.mjs +1849 -0
- package/dist/model/index.mjs.map +1 -0
- package/dist/storage/index.d.mts +68 -0
- package/dist/storage/index.d.ts +68 -0
- package/dist/storage/index.js +250 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +212 -0
- package/dist/storage/index.mjs.map +1 -0
- package/dist/tool/index.d.mts +311 -0
- package/dist/tool/index.d.ts +311 -0
- package/dist/tool/index.js +1494 -0
- package/dist/tool/index.js.map +1 -0
- package/dist/tool/index.mjs +1447 -0
- package/dist/tool/index.mjs.map +1 -0
- package/dist/toolkit-CEpulFi0.d.ts +99 -0
- package/dist/toolkit-CGEZSZPa.d.mts +99 -0
- package/jest.config.js +11 -0
- package/package.json +92 -0
- package/src/_utils/common.ts +104 -0
- package/src/_utils/index.ts +1 -0
- package/src/agent/agent-base.ts +0 -0
- package/src/agent/agent.test.ts +1028 -0
- package/src/agent/agent.ts +1032 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/interfaces.ts +23 -0
- package/src/agent/test-compression.ts +72 -0
- package/src/event/index.ts +250 -0
- package/src/formatter/base.ts +133 -0
- package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
- package/src/formatter/dashscope-chat-formatter.ts +163 -0
- package/src/formatter/deepseek-chat-formatter.ts +130 -0
- package/src/formatter/index.ts +5 -0
- package/src/formatter/ollama-chat-formatter.ts +67 -0
- package/src/formatter/openai-chat-formatter.test.ts +263 -0
- package/src/formatter/openai-chat-formatter.ts +301 -0
- package/src/formatter/openai.md +767 -0
- package/src/mcp/base.ts +114 -0
- package/src/mcp/http.test.ts +303 -0
- package/src/mcp/http.ts +224 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/stdio.test.ts +91 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/message/block.ts +60 -0
- package/src/message/enums.ts +4 -0
- package/src/message/index.ts +12 -0
- package/src/message/message.test.ts +80 -0
- package/src/message/message.ts +131 -0
- package/src/model/base.ts +226 -0
- package/src/model/dashscope-model.test.ts +335 -0
- package/src/model/dashscope-model.ts +441 -0
- package/src/model/deepseek-model.test.ts +279 -0
- package/src/model/deepseek-model.ts +401 -0
- package/src/model/index.ts +7 -0
- package/src/model/ollama-model.test.ts +307 -0
- package/src/model/ollama-model.ts +356 -0
- package/src/model/openai-model.ts +327 -0
- package/src/model/response.ts +22 -0
- package/src/model/usage.ts +12 -0
- package/src/storage/base.ts +52 -0
- package/src/storage/file-system.test.ts +587 -0
- package/src/storage/file-system.ts +269 -0
- package/src/storage/index.ts +2 -0
- package/src/tool/base.ts +23 -0
- package/src/tool/bash.test.ts +174 -0
- package/src/tool/bash.ts +152 -0
- package/src/tool/edit.test.ts +83 -0
- package/src/tool/edit.ts +95 -0
- package/src/tool/glob.test.ts +63 -0
- package/src/tool/glob.ts +166 -0
- package/src/tool/grep.test.ts +74 -0
- package/src/tool/grep.ts +256 -0
- package/src/tool/index.ts +10 -0
- package/src/tool/read.test.ts +77 -0
- package/src/tool/read.ts +117 -0
- package/src/tool/response.ts +82 -0
- package/src/tool/task.test.ts +299 -0
- package/src/tool/task.ts +399 -0
- package/src/tool/toolkit.test.ts +636 -0
- package/src/tool/toolkit.ts +601 -0
- package/src/tool/write.test.ts +52 -0
- package/src/tool/write.ts +57 -0
- package/src/type/index.ts +52 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +20 -0
- package/typedoc.json +52 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
import { Tool } from './base';
|
|
6
|
+
import { Edit } from './edit';
|
|
7
|
+
|
|
8
|
+
describe('Edit', () => {
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
let edit: Tool;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
edit = Edit();
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'edit-test-'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('replaces a unique string', () => {
|
|
22
|
+
const filePath = path.join(tmpDir, 'test.ts');
|
|
23
|
+
fs.writeFileSync(filePath, 'const x = 1;\nconst y = 2;');
|
|
24
|
+
edit.call!({
|
|
25
|
+
file_path: filePath,
|
|
26
|
+
old_string: 'const x = 1;',
|
|
27
|
+
new_string: 'const x = 42;',
|
|
28
|
+
});
|
|
29
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('const x = 42;\nconst y = 2;');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('throws when old_string not found', () => {
|
|
33
|
+
const filePath = path.join(tmpDir, 'test.ts');
|
|
34
|
+
fs.writeFileSync(filePath, 'hello world');
|
|
35
|
+
expect(() =>
|
|
36
|
+
edit.call!({ file_path: filePath, old_string: 'not here', new_string: 'x' })
|
|
37
|
+
).toThrow('not found');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('throws when old_string is not unique and replace_all is false', () => {
|
|
41
|
+
const filePath = path.join(tmpDir, 'test.ts');
|
|
42
|
+
fs.writeFileSync(filePath, 'foo foo foo');
|
|
43
|
+
expect(() =>
|
|
44
|
+
edit.call!({ file_path: filePath, old_string: 'foo', new_string: 'bar' })
|
|
45
|
+
).toThrow('not unique');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('replaces all occurrences when replace_all is true', () => {
|
|
49
|
+
const filePath = path.join(tmpDir, 'test.ts');
|
|
50
|
+
fs.writeFileSync(filePath, 'foo foo foo');
|
|
51
|
+
edit.call!({
|
|
52
|
+
file_path: filePath,
|
|
53
|
+
old_string: 'foo',
|
|
54
|
+
new_string: 'bar',
|
|
55
|
+
replace_all: true,
|
|
56
|
+
});
|
|
57
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('bar bar bar');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('throws when old_string equals new_string', () => {
|
|
61
|
+
const filePath = path.join(tmpDir, 'test.ts');
|
|
62
|
+
fs.writeFileSync(filePath, 'hello');
|
|
63
|
+
expect(() =>
|
|
64
|
+
edit.call!({ file_path: filePath, old_string: 'hello', new_string: 'hello' })
|
|
65
|
+
).toThrow('must be different');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws on relative path', () => {
|
|
69
|
+
expect(() =>
|
|
70
|
+
edit.call!({ file_path: 'relative.ts', old_string: 'a', new_string: 'b' })
|
|
71
|
+
).toThrow('absolute path');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws on non-existent file', () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
edit.call!({
|
|
77
|
+
file_path: path.join(tmpDir, 'nope.ts'),
|
|
78
|
+
old_string: 'a',
|
|
79
|
+
new_string: 'b',
|
|
80
|
+
})
|
|
81
|
+
).toThrow('File not found');
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/tool/edit.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tool for performing exact string replacements in files.
|
|
8
|
+
* Requires the file to have been read at least once before editing.
|
|
9
|
+
*
|
|
10
|
+
* @returns A Tool object with a call method that performs the edit operation based on the provided parameters, ensuring uniqueness of the old_string unless replace_all is true.
|
|
11
|
+
*/
|
|
12
|
+
export function Edit() {
|
|
13
|
+
return {
|
|
14
|
+
name: 'Edit',
|
|
15
|
+
description: `Performs exact string replacements in files.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
- You must use your Read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
19
|
+
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
20
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
21
|
+
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
22
|
+
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
23
|
+
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`,
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
file_path: z.string().describe('The absolute path to the file to modify'),
|
|
26
|
+
old_string: z.string().describe('The text to replace'),
|
|
27
|
+
new_string: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe('The text to replace it with (must be different from old_string)'),
|
|
30
|
+
replace_all: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.default(false)
|
|
34
|
+
.describe('Replace all occurrences of old_string (default false)'),
|
|
35
|
+
}),
|
|
36
|
+
requireUserConfirm: true,
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Performs an exact string replacement in the specified file.
|
|
40
|
+
*
|
|
41
|
+
* @param root0 - The parameters object
|
|
42
|
+
* @param root0.file_path - Absolute path to the file to modify
|
|
43
|
+
* @param root0.old_string - The exact text to find and replace
|
|
44
|
+
* @param root0.new_string - The text to replace old_string with
|
|
45
|
+
* @param root0.replace_all - If true, replaces all occurrences; otherwise only the first
|
|
46
|
+
* @returns A success message indicating the file was updated
|
|
47
|
+
* @throws If the path is not absolute, file does not exist, strings are identical,
|
|
48
|
+
* old_string is not found, or old_string is not unique and replace_all is false
|
|
49
|
+
*/
|
|
50
|
+
call({
|
|
51
|
+
file_path,
|
|
52
|
+
old_string,
|
|
53
|
+
new_string,
|
|
54
|
+
replace_all = false,
|
|
55
|
+
}: {
|
|
56
|
+
file_path: string;
|
|
57
|
+
old_string: string;
|
|
58
|
+
new_string: string;
|
|
59
|
+
replace_all?: boolean;
|
|
60
|
+
}): string {
|
|
61
|
+
if (!path.isAbsolute(file_path)) {
|
|
62
|
+
throw new Error(`file_path must be an absolute path, got: ${file_path}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(file_path)) {
|
|
66
|
+
throw new Error(`File not found: ${file_path}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (old_string === new_string) {
|
|
70
|
+
throw new Error('old_string and new_string must be different');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(file_path, 'utf-8');
|
|
74
|
+
const occurrences = content.split(old_string).length - 1;
|
|
75
|
+
|
|
76
|
+
if (occurrences === 0) {
|
|
77
|
+
throw new Error(`old_string not found in file: ${file_path}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!replace_all && occurrences > 1) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`old_string is not unique in the file (found ${occurrences} occurrences). ` +
|
|
83
|
+
`Provide more surrounding context to make it unique, or use replace_all=true.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const newContent = replace_all
|
|
88
|
+
? content.split(old_string).join(new_string)
|
|
89
|
+
: content.replace(old_string, new_string);
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(file_path, newContent, 'utf-8');
|
|
92
|
+
return `The file ${file_path} has been updated successfully.`;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
import { Glob } from './glob';
|
|
6
|
+
|
|
7
|
+
describe('Glob', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let glob: ReturnType<typeof Glob>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
glob = Glob();
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'glob-test-'));
|
|
14
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), '');
|
|
15
|
+
fs.writeFileSync(path.join(tmpDir, 'b.ts'), '');
|
|
16
|
+
fs.writeFileSync(path.join(tmpDir, 'c.js'), '');
|
|
17
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
18
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'd.ts'), '');
|
|
19
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'e.tsx'), '');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('matches *.ts files in root', () => {
|
|
27
|
+
const result = glob.call!({ pattern: '*.ts', path: tmpDir });
|
|
28
|
+
expect(result).toContain('a.ts');
|
|
29
|
+
expect(result).toContain('b.ts');
|
|
30
|
+
expect(result).not.toContain('c.js');
|
|
31
|
+
expect(result).not.toContain('d.ts');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('matches **/*.ts recursively', () => {
|
|
35
|
+
const result = glob.call!({ pattern: '**/*.ts', path: tmpDir });
|
|
36
|
+
expect(result).toContain('a.ts');
|
|
37
|
+
expect(result).toContain('b.ts');
|
|
38
|
+
expect(result).toContain('d.ts');
|
|
39
|
+
expect(result).not.toContain('c.js');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('matches **/*.tsx recursively', () => {
|
|
43
|
+
const result = glob.call!({ pattern: '**/*.tsx', path: tmpDir });
|
|
44
|
+
expect(result).toContain('e.tsx');
|
|
45
|
+
expect(result).not.toContain('a.ts');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns message when no files match', () => {
|
|
49
|
+
const result = glob.call!({ pattern: '*.py', path: tmpDir });
|
|
50
|
+
expect(result).toContain('No files found');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws on non-existent directory', () => {
|
|
54
|
+
expect(() => glob.call!({ pattern: '*.ts', path: '/nonexistent/path' })).toThrow(
|
|
55
|
+
'Directory not found'
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('uses cwd when path is not specified', () => {
|
|
60
|
+
const result = glob.call!({ pattern: '*.json' });
|
|
61
|
+
expect(typeof result).toBe('string');
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/tool/glob.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tool for fast file pattern matching across a codebase.
|
|
8
|
+
* Supports glob patterns and returns results sorted by modification time.
|
|
9
|
+
*
|
|
10
|
+
* @returns A Tool object with a call method that performs glob matching based on the provided pattern and path.
|
|
11
|
+
*/
|
|
12
|
+
export function Glob() {
|
|
13
|
+
/**
|
|
14
|
+
* Matches files against a glob pattern starting from the given base directory.
|
|
15
|
+
* @param pattern - The glob pattern to match against.
|
|
16
|
+
* @param baseDir - The base directory to search from.
|
|
17
|
+
* @returns An array of matched file paths.
|
|
18
|
+
*/
|
|
19
|
+
const globMatch = (pattern: string, baseDir: string): string[] => {
|
|
20
|
+
const results: string[] = [];
|
|
21
|
+
const parts = pattern.split('/');
|
|
22
|
+
matchParts(parts, 0, baseDir, results);
|
|
23
|
+
return results;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively matches path parts against directory entries.
|
|
28
|
+
* @param parts - The split glob pattern parts.
|
|
29
|
+
* @param partIndex - The current index in the parts array.
|
|
30
|
+
* @param currentDir - The current directory being traversed.
|
|
31
|
+
* @param results - The accumulator array for matched file paths.
|
|
32
|
+
*/
|
|
33
|
+
const matchParts = (
|
|
34
|
+
parts: string[],
|
|
35
|
+
partIndex: number,
|
|
36
|
+
currentDir: string,
|
|
37
|
+
results: string[]
|
|
38
|
+
): void => {
|
|
39
|
+
if (partIndex >= parts.length) return;
|
|
40
|
+
|
|
41
|
+
const part = parts[partIndex];
|
|
42
|
+
const isLast = partIndex === parts.length - 1;
|
|
43
|
+
|
|
44
|
+
if (part === '**') {
|
|
45
|
+
if (isLast) {
|
|
46
|
+
collectAll(currentDir, results);
|
|
47
|
+
} else {
|
|
48
|
+
matchParts(parts, partIndex + 1, currentDir, results);
|
|
49
|
+
try {
|
|
50
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
const subDir = path.join(currentDir, entry.name);
|
|
54
|
+
matchParts(parts, partIndex, subDir, results);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// skip unreadable dirs
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const regex = globPartToRegex(part);
|
|
63
|
+
try {
|
|
64
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (regex.test(entry.name)) {
|
|
67
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
68
|
+
if (isLast) {
|
|
69
|
+
if (entry.isFile()) results.push(fullPath);
|
|
70
|
+
} else if (entry.isDirectory()) {
|
|
71
|
+
matchParts(parts, partIndex + 1, fullPath, results);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// skip unreadable dirs
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Recursively collects all files under a directory.
|
|
83
|
+
* @param dir - The directory to collect files from.
|
|
84
|
+
* @param results - The accumulator array for collected file paths.
|
|
85
|
+
*/
|
|
86
|
+
const collectAll = (dir: string, results: string[]): void => {
|
|
87
|
+
try {
|
|
88
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const fullPath = path.join(dir, entry.name);
|
|
91
|
+
if (entry.isFile()) {
|
|
92
|
+
results.push(fullPath);
|
|
93
|
+
} else if (entry.isDirectory()) {
|
|
94
|
+
collectAll(fullPath, results);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// skip unreadable dirs
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Converts a single glob pattern part to a RegExp.
|
|
104
|
+
* @param part - The glob pattern part to convert.
|
|
105
|
+
* @returns A RegExp that matches the pattern part.
|
|
106
|
+
*/
|
|
107
|
+
const globPartToRegex = (part: string): RegExp => {
|
|
108
|
+
const escaped = part
|
|
109
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
110
|
+
.replace(/\*/g, '.*')
|
|
111
|
+
.replace(/\?/g, '.');
|
|
112
|
+
return new RegExp(`^${escaped}$`);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name: 'Glob',
|
|
117
|
+
description: `Fast file pattern matching tool that works with any codebase size.
|
|
118
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
119
|
+
- Returns matching file paths sorted by modification time
|
|
120
|
+
- Use this tool when you need to find files by name patterns
|
|
121
|
+
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
|
|
122
|
+
- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.`,
|
|
123
|
+
inputSchema: z.object({
|
|
124
|
+
pattern: z.string().describe('The glob pattern to match files against'),
|
|
125
|
+
path: z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe(
|
|
129
|
+
'The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.'
|
|
130
|
+
),
|
|
131
|
+
}),
|
|
132
|
+
requireUserConfirm: true,
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Finds files matching a glob pattern under the given directory.
|
|
136
|
+
*
|
|
137
|
+
* @param root0 - The parameters object
|
|
138
|
+
* @param root0.pattern - The glob pattern to match files against
|
|
139
|
+
* @param root0.path - The base directory to search in; defaults to cwd
|
|
140
|
+
* @returns A newline-separated list of matching file paths sorted by modification time,
|
|
141
|
+
* or a no-matches message if nothing is found
|
|
142
|
+
* @throws If the base directory does not exist
|
|
143
|
+
*/
|
|
144
|
+
call({ pattern, path: searchPath }: { pattern: string; path?: string }): string {
|
|
145
|
+
const baseDir = searchPath ? searchPath : process.cwd();
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(baseDir)) {
|
|
148
|
+
throw new Error(`Directory not found: ${baseDir}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const matches = globMatch(pattern, baseDir);
|
|
152
|
+
|
|
153
|
+
matches.sort((a, b) => {
|
|
154
|
+
const statA = fs.statSync(a);
|
|
155
|
+
const statB = fs.statSync(b);
|
|
156
|
+
return statB.mtimeMs - statA.mtimeMs;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (matches.length === 0) {
|
|
160
|
+
return `No files found matching pattern: ${pattern}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return matches.join('\n');
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
import { Tool } from './base';
|
|
6
|
+
import { Grep } from './grep';
|
|
7
|
+
|
|
8
|
+
describe('Grep', () => {
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
let grep: Tool;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
grep = Grep();
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'grep-test-'));
|
|
15
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), 'function hello() {}\nconst world = 1;');
|
|
16
|
+
fs.writeFileSync(path.join(tmpDir, 'b.ts'), 'function greet() {}\nconst hello = "hi";');
|
|
17
|
+
fs.writeFileSync(path.join(tmpDir, 'c.js'), 'var hello = true;');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns files_with_matches by default', () => {
|
|
25
|
+
const result = grep.call!({ pattern: 'hello', path: tmpDir });
|
|
26
|
+
expect(result).toContain('a.ts');
|
|
27
|
+
expect(result).toContain('b.ts');
|
|
28
|
+
expect(result).toContain('c.js');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('filters by type', () => {
|
|
32
|
+
const result = grep.call!({ pattern: 'hello', path: tmpDir, type: 'ts' });
|
|
33
|
+
expect(result).toContain('a.ts');
|
|
34
|
+
expect(result).toContain('b.ts');
|
|
35
|
+
expect(result).not.toContain('c.js');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns content with matching lines', () => {
|
|
39
|
+
const result = grep.call!({
|
|
40
|
+
pattern: 'function',
|
|
41
|
+
path: tmpDir,
|
|
42
|
+
output_mode: 'content',
|
|
43
|
+
type: 'ts',
|
|
44
|
+
});
|
|
45
|
+
expect(result).toContain('function hello');
|
|
46
|
+
expect(result).toContain('function greet');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns count mode', () => {
|
|
50
|
+
const result = grep.call!({ pattern: 'hello', path: tmpDir, output_mode: 'count' });
|
|
51
|
+
expect(result).toMatch(/\d+/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('is case insensitive when flag set', () => {
|
|
55
|
+
const result = grep.call!({ pattern: 'HELLO', path: tmpDir, case_insensitive: true });
|
|
56
|
+
expect(result).toContain('a.ts');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns no matches message when nothing found', () => {
|
|
60
|
+
const result = grep.call!({ pattern: 'zzznomatch', path: tmpDir });
|
|
61
|
+
expect(result).toContain('No matches found');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('respects head_limit', () => {
|
|
65
|
+
const result = grep.call!({ pattern: 'hello', path: tmpDir, head_limit: 1 });
|
|
66
|
+
expect((result as string).split('\n').length).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('searches a single file directly', () => {
|
|
70
|
+
const filePath = path.join(tmpDir, 'a.ts');
|
|
71
|
+
const result = grep.call!({ pattern: 'hello', path: filePath });
|
|
72
|
+
expect(result).toContain('a.ts');
|
|
73
|
+
});
|
|
74
|
+
});
|