@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
package/src/tool/grep.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
type OutputMode = 'content' | 'files_with_matches' | 'count';
|
|
7
|
+
|
|
8
|
+
const TYPE_EXTENSIONS: Record<string, string[]> = {
|
|
9
|
+
js: ['.js', '.mjs', '.cjs'],
|
|
10
|
+
ts: ['.ts', '.mts', '.cts'],
|
|
11
|
+
tsx: ['.tsx'],
|
|
12
|
+
jsx: ['.jsx'],
|
|
13
|
+
py: ['.py'],
|
|
14
|
+
rust: ['.rs'],
|
|
15
|
+
go: ['.go'],
|
|
16
|
+
java: ['.java'],
|
|
17
|
+
cpp: ['.cpp', '.cc', '.cxx', '.h', '.hpp'],
|
|
18
|
+
c: ['.c', '.h'],
|
|
19
|
+
css: ['.css'],
|
|
20
|
+
html: ['.html', '.htm'],
|
|
21
|
+
json: ['.json'],
|
|
22
|
+
md: ['.md', '.markdown'],
|
|
23
|
+
yaml: ['.yaml', '.yml'],
|
|
24
|
+
toml: ['.toml'],
|
|
25
|
+
sh: ['.sh', '.bash'],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tool for searching file contents using regular expressions.
|
|
30
|
+
* Supports multiple output modes, file type filtering, and multiline matching.
|
|
31
|
+
*
|
|
32
|
+
* @returns A Tool object for performing regex searches across files, with a call method that executes the search and returns results based on the specified output mode.
|
|
33
|
+
*/
|
|
34
|
+
export function Grep() {
|
|
35
|
+
/**
|
|
36
|
+
* Collects all files under a base directory, optionally filtered by glob or type.
|
|
37
|
+
* @param baseDir - The base directory to search from.
|
|
38
|
+
* @param glob - Optional glob pattern to filter files by name.
|
|
39
|
+
* @param type - Optional file type key to filter by extension.
|
|
40
|
+
* @returns An array of matching file paths.
|
|
41
|
+
*/
|
|
42
|
+
const collectFiles = (baseDir: string, glob?: string, type?: string): string[] => {
|
|
43
|
+
const results: string[] = [];
|
|
44
|
+
const extensions = type ? TYPE_EXTENSIONS[type] : undefined;
|
|
45
|
+
|
|
46
|
+
const walk = (dir: string): void => {
|
|
47
|
+
let entries: fs.Dirent[];
|
|
48
|
+
try {
|
|
49
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
56
|
+
|
|
57
|
+
const fullPath = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
walk(fullPath);
|
|
60
|
+
} else if (entry.isFile()) {
|
|
61
|
+
if (extensions) {
|
|
62
|
+
const ext = path.extname(entry.name);
|
|
63
|
+
if (extensions.includes(ext)) results.push(fullPath);
|
|
64
|
+
} else if (glob) {
|
|
65
|
+
if (matchGlob(glob, entry.name)) results.push(fullPath);
|
|
66
|
+
} else {
|
|
67
|
+
results.push(fullPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(baseDir) && fs.statSync(baseDir).isFile()) {
|
|
74
|
+
results.push(baseDir);
|
|
75
|
+
} else {
|
|
76
|
+
walk(baseDir);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tests whether a filename matches a glob pattern.
|
|
84
|
+
* @param pattern - The glob pattern to match against.
|
|
85
|
+
* @param filename - The filename to test.
|
|
86
|
+
* @returns True if the filename matches the pattern, false otherwise.
|
|
87
|
+
*/
|
|
88
|
+
const matchGlob = (pattern: string, filename: string): boolean => {
|
|
89
|
+
const braceMatch = pattern.match(/\*\.\\{(.+)\\}/);
|
|
90
|
+
if (braceMatch) {
|
|
91
|
+
const exts = braceMatch[1].split(',').map(e => `.${e.trim()}`);
|
|
92
|
+
return exts.includes(path.extname(filename));
|
|
93
|
+
}
|
|
94
|
+
const escaped = pattern
|
|
95
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
96
|
+
.replace(/\*/g, '.*')
|
|
97
|
+
.replace(/\?/g, '.');
|
|
98
|
+
return new RegExp(`^${escaped}$`).test(filename);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
name: 'Grep',
|
|
103
|
+
description: `A powerful search tool built on regular expressions.
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
- ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command. The Grep tool has been optimized for correct permissions and access.
|
|
107
|
+
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
108
|
+
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
|
|
109
|
+
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
110
|
+
- Use Task tool for open-ended searches requiring multiple rounds
|
|
111
|
+
- Pattern syntax: Uses ripgrep-style regex - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
|
|
112
|
+
- Multiline matching: By default patterns match within single lines only. For cross-line patterns, use \`multiline: true\``,
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
pattern: z
|
|
115
|
+
.string()
|
|
116
|
+
.describe('The regular expression pattern to search for in file contents'),
|
|
117
|
+
path: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('File or directory to search in. Defaults to current working directory.'),
|
|
121
|
+
glob: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
|
|
125
|
+
type: z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe(
|
|
129
|
+
'File type to search (e.g., "js", "ts", "py"). More efficient than glob for standard file types.'
|
|
130
|
+
),
|
|
131
|
+
output_mode: z
|
|
132
|
+
.enum(['content', 'files_with_matches', 'count'])
|
|
133
|
+
.optional()
|
|
134
|
+
.describe(
|
|
135
|
+
'Output mode: "content" | "files_with_matches" | "count". Defaults to "files_with_matches".'
|
|
136
|
+
),
|
|
137
|
+
multiline: z
|
|
138
|
+
.boolean()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe(
|
|
141
|
+
'Enable multiline mode where . matches newlines and patterns can span lines. Default: false.'
|
|
142
|
+
),
|
|
143
|
+
case_insensitive: z
|
|
144
|
+
.boolean()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('Case insensitive search. Default: false.'),
|
|
147
|
+
context: z
|
|
148
|
+
.number()
|
|
149
|
+
.int()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe(
|
|
152
|
+
'Number of lines to show before and after each match. Requires output_mode: "content".'
|
|
153
|
+
),
|
|
154
|
+
head_limit: z
|
|
155
|
+
.number()
|
|
156
|
+
.int()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe('Limit output to first N lines/entries.'),
|
|
159
|
+
}),
|
|
160
|
+
requireUserConfirm: true,
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Searches files for a regex pattern and returns results in the specified output mode.
|
|
164
|
+
*
|
|
165
|
+
* @param root0 - The parameters object
|
|
166
|
+
* @param root0.pattern - The regular expression pattern to search for
|
|
167
|
+
* @param root0.path - File or directory to search in; defaults to cwd
|
|
168
|
+
* @param root0.glob - Glob pattern to filter which files are searched
|
|
169
|
+
* @param root0.type - File type shorthand (e.g. "ts", "py") to filter files
|
|
170
|
+
* @param root0.output_mode - How to format results: "content", "files_with_matches", or "count"
|
|
171
|
+
* @param root0.multiline - Whether the pattern can span multiple lines
|
|
172
|
+
* @param root0.case_insensitive - Whether the search is case-insensitive
|
|
173
|
+
* @param root0.context - Number of surrounding lines to include with each match
|
|
174
|
+
* @param root0.head_limit - Maximum number of result entries to return
|
|
175
|
+
* @returns A newline-separated string of results, or a no-matches message
|
|
176
|
+
*/
|
|
177
|
+
call({
|
|
178
|
+
pattern,
|
|
179
|
+
path: searchPath,
|
|
180
|
+
glob,
|
|
181
|
+
type,
|
|
182
|
+
output_mode = 'files_with_matches',
|
|
183
|
+
multiline = false,
|
|
184
|
+
case_insensitive = false,
|
|
185
|
+
context,
|
|
186
|
+
head_limit,
|
|
187
|
+
}: {
|
|
188
|
+
pattern: string;
|
|
189
|
+
path?: string;
|
|
190
|
+
glob?: string;
|
|
191
|
+
type?: string;
|
|
192
|
+
output_mode?: OutputMode;
|
|
193
|
+
multiline?: boolean;
|
|
194
|
+
case_insensitive?: boolean;
|
|
195
|
+
context?: number;
|
|
196
|
+
head_limit?: number;
|
|
197
|
+
}): string {
|
|
198
|
+
const baseDir = searchPath ? searchPath : process.cwd();
|
|
199
|
+
|
|
200
|
+
let flags = multiline ? 'gms' : 'gm';
|
|
201
|
+
if (case_insensitive) flags += 'i';
|
|
202
|
+
|
|
203
|
+
const regex = new RegExp(pattern, flags);
|
|
204
|
+
const files = collectFiles(baseDir, glob, type);
|
|
205
|
+
|
|
206
|
+
if (files.length === 0) {
|
|
207
|
+
return 'No files found to search.';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const results: string[] = [];
|
|
211
|
+
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
try {
|
|
214
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
215
|
+
|
|
216
|
+
if (output_mode === 'files_with_matches') {
|
|
217
|
+
if (regex.test(content)) {
|
|
218
|
+
results.push(file);
|
|
219
|
+
}
|
|
220
|
+
regex.lastIndex = 0;
|
|
221
|
+
} else if (output_mode === 'count') {
|
|
222
|
+
const matches = content.match(regex);
|
|
223
|
+
if (matches) {
|
|
224
|
+
results.push(`${file}: ${matches.length}`);
|
|
225
|
+
}
|
|
226
|
+
regex.lastIndex = 0;
|
|
227
|
+
} else if (output_mode === 'content') {
|
|
228
|
+
const lines = content.split('\n');
|
|
229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
const lineRegex = new RegExp(pattern, case_insensitive ? 'i' : '');
|
|
231
|
+
if (lineRegex.test(lines[i])) {
|
|
232
|
+
const start = context !== undefined ? Math.max(0, i - context) : i;
|
|
233
|
+
const end =
|
|
234
|
+
context !== undefined
|
|
235
|
+
? Math.min(lines.length - 1, i + context)
|
|
236
|
+
: i;
|
|
237
|
+
for (let j = start; j <= end; j++) {
|
|
238
|
+
results.push(`${file}:${j + 1}:${lines[j]}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// skip unreadable files
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (results.length === 0) {
|
|
249
|
+
return `No matches found for pattern: ${pattern}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const output = head_limit !== undefined ? results.slice(0, head_limit) : results;
|
|
253
|
+
return output.join('\n');
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ToolResponse } from './response';
|
|
2
|
+
export { Tool } from './base';
|
|
3
|
+
export { Toolkit } from './toolkit';
|
|
4
|
+
export { Bash } from './bash';
|
|
5
|
+
export { Read } from './read';
|
|
6
|
+
export { Write } from './write';
|
|
7
|
+
export { Edit } from './edit';
|
|
8
|
+
export { Glob } from './glob';
|
|
9
|
+
export { Grep } from './grep';
|
|
10
|
+
export { TaskCreate, TaskUpdate, TaskGet, TaskList } from './task';
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { Read } from './read';
|
|
7
|
+
import { ToolResponse } from './response';
|
|
8
|
+
|
|
9
|
+
describe('Read', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
let read: Tool;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
read = Read();
|
|
15
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'read-test-'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const getTextFromResponse = (response: ToolResponse): string => {
|
|
23
|
+
const textBlock = response.content.find(block => block.type === 'text');
|
|
24
|
+
return textBlock && 'text' in textBlock ? textBlock.text : '';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it('reads a file with line numbers', () => {
|
|
28
|
+
const filePath = path.join(tmpDir, 'test.txt');
|
|
29
|
+
fs.writeFileSync(filePath, 'line1\nline2\nline3');
|
|
30
|
+
const response = read.call!({ file_path: filePath }) as ToolResponse;
|
|
31
|
+
const result = getTextFromResponse(response);
|
|
32
|
+
expect(result).toContain('1\tline1');
|
|
33
|
+
expect(result).toContain('2\tline2');
|
|
34
|
+
expect(result).toContain('3\tline3');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('respects offset and limit', () => {
|
|
38
|
+
const filePath = path.join(tmpDir, 'test.txt');
|
|
39
|
+
fs.writeFileSync(filePath, 'a\nb\nc\nd\ne');
|
|
40
|
+
const response = read.call!({ file_path: filePath, offset: 2, limit: 2 }) as ToolResponse;
|
|
41
|
+
const result = getTextFromResponse(response);
|
|
42
|
+
expect(result).toContain('2\tb');
|
|
43
|
+
expect(result).toContain('3\tc');
|
|
44
|
+
expect(result).not.toContain('1\ta');
|
|
45
|
+
expect(result).not.toContain('4\td');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('throws on relative path', () => {
|
|
49
|
+
expect(() => read.call!({ file_path: 'relative.txt' })).toThrow('absolute path');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('throws on non-existent file', () => {
|
|
53
|
+
expect(() => read.call!({ file_path: path.join(tmpDir, 'nope.txt') })).toThrow(
|
|
54
|
+
'File not found'
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('throws on directory', () => {
|
|
59
|
+
expect(() => read.call!({ file_path: tmpDir })).toThrow('directory');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns warning for empty file', () => {
|
|
63
|
+
const filePath = path.join(tmpDir, 'empty.txt');
|
|
64
|
+
fs.writeFileSync(filePath, '');
|
|
65
|
+
const response = read.call!({ file_path: filePath }) as ToolResponse;
|
|
66
|
+
const result = getTextFromResponse(response);
|
|
67
|
+
expect(result).toBe('');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('truncates lines longer than 2000 characters', () => {
|
|
71
|
+
const filePath = path.join(tmpDir, 'long.txt');
|
|
72
|
+
fs.writeFileSync(filePath, 'x'.repeat(2100));
|
|
73
|
+
const response = read.call!({ file_path: filePath }) as ToolResponse;
|
|
74
|
+
const result = getTextFromResponse(response);
|
|
75
|
+
expect(result).toContain('[truncated]');
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/tool/read.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { createToolResponse, ToolResponse } from './response';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tool for reading files from the local filesystem.
|
|
10
|
+
* Returns file contents with line numbers in cat -n format.
|
|
11
|
+
*
|
|
12
|
+
* @returns A Tool object for reading files, with a call method that performs the read operation and returns the formatted contents or a warning if the file is empty.
|
|
13
|
+
*/
|
|
14
|
+
export function Read() {
|
|
15
|
+
return {
|
|
16
|
+
name: 'Read',
|
|
17
|
+
description: `Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
18
|
+
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
- The file_path parameter must be an absolute path, not a relative path
|
|
22
|
+
- By default, it reads up to 2000 lines starting from the beginning of the file
|
|
23
|
+
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
24
|
+
- Any lines longer than 2000 characters will be truncated
|
|
25
|
+
- Results are returned using cat -n format, with line numbers starting at 1
|
|
26
|
+
- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
|
|
27
|
+
- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.
|
|
28
|
+
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.`,
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
file_path: z.string().describe('The absolute path to the file to read'),
|
|
31
|
+
offset: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe(
|
|
37
|
+
'The line number to start reading from. Only provide if the file is too large to read at once'
|
|
38
|
+
),
|
|
39
|
+
limit: z
|
|
40
|
+
.number()
|
|
41
|
+
.int()
|
|
42
|
+
.positive()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
'The number of lines to read. Only provide if the file is too large to read at once'
|
|
46
|
+
),
|
|
47
|
+
}),
|
|
48
|
+
requireUserConfirm: true,
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads a file and returns its contents with line numbers.
|
|
52
|
+
*
|
|
53
|
+
* @param root0 - The parameters object
|
|
54
|
+
* @param root0.file_path - Absolute path to the file to read
|
|
55
|
+
* @param root0.offset - Line number to start reading from (1-based)
|
|
56
|
+
* @param root0.limit - Maximum number of lines to read (capped at 2000)
|
|
57
|
+
* @returns The file contents formatted with line numbers, or a warning if the file is empty
|
|
58
|
+
* @throws If the path is not absolute, the file does not exist, or the path is a directory
|
|
59
|
+
*/
|
|
60
|
+
call({
|
|
61
|
+
file_path,
|
|
62
|
+
offset,
|
|
63
|
+
limit,
|
|
64
|
+
}: {
|
|
65
|
+
file_path: string;
|
|
66
|
+
offset?: number;
|
|
67
|
+
limit?: number;
|
|
68
|
+
}): ToolResponse {
|
|
69
|
+
if (!path.isAbsolute(file_path)) {
|
|
70
|
+
throw new Error(`file_path must be an absolute path, got: ${file_path}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(file_path)) {
|
|
74
|
+
throw new Error(`File not found: ${file_path}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stat = fs.statSync(file_path);
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${file_path} is a directory, not a file. Use Bash with ls to read directories.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rawContent = fs.readFileSync(file_path, 'utf-8');
|
|
85
|
+
|
|
86
|
+
if (rawContent.length === 0) {
|
|
87
|
+
return createToolResponse({
|
|
88
|
+
content: [{ id: crypto.randomUUID(), type: 'text', text: rawContent }],
|
|
89
|
+
state: 'success',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const allLines = rawContent.split('\n');
|
|
94
|
+
const startLine = offset !== undefined ? offset - 1 : 0;
|
|
95
|
+
const maxLines = 2000;
|
|
96
|
+
const effectiveLimit = limit !== undefined ? Math.min(limit, maxLines) : maxLines;
|
|
97
|
+
const selectedLines = allLines.slice(startLine, startLine + effectiveLimit);
|
|
98
|
+
|
|
99
|
+
const maxLineLength = 2000;
|
|
100
|
+
const formatted = selectedLines
|
|
101
|
+
.map((line, i) => {
|
|
102
|
+
const lineNum = startLine + i + 1;
|
|
103
|
+
const truncated =
|
|
104
|
+
line.length > maxLineLength
|
|
105
|
+
? line.substring(0, maxLineLength) + '[truncated]'
|
|
106
|
+
: line;
|
|
107
|
+
return `${String(lineNum).padStart(6)}\t${truncated}`;
|
|
108
|
+
})
|
|
109
|
+
.join('\n');
|
|
110
|
+
|
|
111
|
+
return createToolResponse({
|
|
112
|
+
content: [{ id: crypto.randomUUID(), type: 'text', text: formatted }],
|
|
113
|
+
state: 'success',
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DataBlock, TextBlock } from '../message';
|
|
2
|
+
import { JSONSerializableObject } from '../type';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The tool response structure.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolResponse {
|
|
8
|
+
content: Array<TextBlock | DataBlock>;
|
|
9
|
+
id: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
metadata: Record<string, JSONSerializableObject>;
|
|
12
|
+
state: 'success' | 'error' | 'interrupted' | 'running';
|
|
13
|
+
isLast: boolean;
|
|
14
|
+
isInterrupted: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a tool response object with the given parameters.
|
|
19
|
+
*
|
|
20
|
+
* @param root0
|
|
21
|
+
* @param root0.content
|
|
22
|
+
* @param root0.state
|
|
23
|
+
* @param root0.id
|
|
24
|
+
* @param root0.createdAt
|
|
25
|
+
* @param root0.metadata
|
|
26
|
+
* @param root0.stream
|
|
27
|
+
* @param root0.isLast
|
|
28
|
+
* @param root0.isInterrupted
|
|
29
|
+
* @returns A ToolResponse object
|
|
30
|
+
*/
|
|
31
|
+
export function createToolResponse({
|
|
32
|
+
content,
|
|
33
|
+
state,
|
|
34
|
+
id = crypto.randomUUID(),
|
|
35
|
+
createdAt = new Date().toISOString(),
|
|
36
|
+
metadata = {},
|
|
37
|
+
stream = false,
|
|
38
|
+
isLast = true,
|
|
39
|
+
isInterrupted = false,
|
|
40
|
+
}: {
|
|
41
|
+
content: Array<TextBlock | DataBlock>;
|
|
42
|
+
state: 'success' | 'error' | 'interrupted' | 'running';
|
|
43
|
+
id?: string;
|
|
44
|
+
createdAt?: string;
|
|
45
|
+
metadata?: Record<string, JSONSerializableObject>;
|
|
46
|
+
stream?: boolean;
|
|
47
|
+
isLast?: boolean;
|
|
48
|
+
isInterrupted?: boolean;
|
|
49
|
+
}) {
|
|
50
|
+
return {
|
|
51
|
+
content,
|
|
52
|
+
id,
|
|
53
|
+
createdAt,
|
|
54
|
+
metadata,
|
|
55
|
+
state,
|
|
56
|
+
stream,
|
|
57
|
+
isLast,
|
|
58
|
+
isInterrupted,
|
|
59
|
+
} as ToolResponse;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* If the given object conforms to the ToolResponse structure.
|
|
64
|
+
*
|
|
65
|
+
* @param obj
|
|
66
|
+
* @returns True if the object is a ToolResponse, false otherwise
|
|
67
|
+
*/
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
export function isToolResponse(obj: any): boolean {
|
|
70
|
+
return (
|
|
71
|
+
obj &&
|
|
72
|
+
typeof obj === 'object' &&
|
|
73
|
+
typeof obj.id === 'string' &&
|
|
74
|
+
typeof obj.createdAt === 'string' &&
|
|
75
|
+
Array.isArray(obj.content) &&
|
|
76
|
+
typeof obj.metadata === 'object' &&
|
|
77
|
+
typeof obj.stream === 'boolean' &&
|
|
78
|
+
typeof obj.isLast === 'boolean' &&
|
|
79
|
+
typeof obj.isInterrupted === 'boolean' &&
|
|
80
|
+
['success', 'error', 'interrupted', 'running'].includes(obj.state)
|
|
81
|
+
);
|
|
82
|
+
}
|