@modelrelay/sdk 1.25.0 → 1.27.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/dist/chunk-BL7GWXRZ.js +1196 -0
- package/dist/index.cjs +75 -681
- package/dist/index.d.cts +3 -1315
- package/dist/index.d.ts +3 -1315
- package/dist/index.js +627 -2276
- package/dist/node.cjs +878 -0
- package/dist/node.d.cts +149 -0
- package/dist/node.d.ts +149 -0
- package/dist/node.js +611 -0
- package/dist/tools-Db-F5rIL.d.cts +1169 -0
- package/dist/tools-Db-F5rIL.d.ts +1169 -0
- package/package.json +8 -3
package/dist/node.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PathEscapeError,
|
|
3
|
+
ToolArgumentError,
|
|
4
|
+
ToolRegistry
|
|
5
|
+
} from "./chunk-BL7GWXRZ.js";
|
|
6
|
+
|
|
7
|
+
// src/tools_local_fs.ts
|
|
8
|
+
import { promises as fs } from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
var ToolNames = {
|
|
12
|
+
FS_READ_FILE: "fs.read_file",
|
|
13
|
+
FS_LIST_FILES: "fs.list_files",
|
|
14
|
+
FS_SEARCH: "fs.search"
|
|
15
|
+
};
|
|
16
|
+
var FSDefaults = {
|
|
17
|
+
MAX_READ_BYTES: 64e3,
|
|
18
|
+
HARD_MAX_READ_BYTES: 1e6,
|
|
19
|
+
MAX_LIST_ENTRIES: 2e3,
|
|
20
|
+
HARD_MAX_LIST_ENTRIES: 2e4,
|
|
21
|
+
MAX_SEARCH_MATCHES: 100,
|
|
22
|
+
HARD_MAX_SEARCH_MATCHES: 2e3,
|
|
23
|
+
SEARCH_TIMEOUT_MS: 5e3,
|
|
24
|
+
MAX_SEARCH_BYTES_PER_FILE: 1e6
|
|
25
|
+
};
|
|
26
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
27
|
+
".git",
|
|
28
|
+
"node_modules",
|
|
29
|
+
"vendor",
|
|
30
|
+
"dist",
|
|
31
|
+
"build",
|
|
32
|
+
".next",
|
|
33
|
+
"target",
|
|
34
|
+
".idea",
|
|
35
|
+
".vscode",
|
|
36
|
+
"__pycache__",
|
|
37
|
+
".pytest_cache",
|
|
38
|
+
"coverage"
|
|
39
|
+
]);
|
|
40
|
+
var LocalFSToolPack = class {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.rgPath = null;
|
|
43
|
+
this.rgChecked = false;
|
|
44
|
+
const root = options.root?.trim();
|
|
45
|
+
if (!root) {
|
|
46
|
+
throw new Error("LocalFSToolPack: root directory required");
|
|
47
|
+
}
|
|
48
|
+
this.rootAbs = path.resolve(root);
|
|
49
|
+
this.cfg = {
|
|
50
|
+
ignoreDirs: options.ignoreDirs ?? new Set(DEFAULT_IGNORE_DIRS),
|
|
51
|
+
maxReadBytes: options.maxReadBytes ?? FSDefaults.MAX_READ_BYTES,
|
|
52
|
+
hardMaxReadBytes: options.hardMaxReadBytes ?? FSDefaults.HARD_MAX_READ_BYTES,
|
|
53
|
+
maxListEntries: options.maxListEntries ?? FSDefaults.MAX_LIST_ENTRIES,
|
|
54
|
+
hardMaxListEntries: options.hardMaxListEntries ?? FSDefaults.HARD_MAX_LIST_ENTRIES,
|
|
55
|
+
maxSearchMatches: options.maxSearchMatches ?? FSDefaults.MAX_SEARCH_MATCHES,
|
|
56
|
+
hardMaxSearchMatches: options.hardMaxSearchMatches ?? FSDefaults.HARD_MAX_SEARCH_MATCHES,
|
|
57
|
+
searchTimeoutMs: options.searchTimeoutMs ?? FSDefaults.SEARCH_TIMEOUT_MS,
|
|
58
|
+
maxSearchBytesPerFile: options.maxSearchBytesPerFile ?? FSDefaults.MAX_SEARCH_BYTES_PER_FILE
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns the tool definitions for LLM requests.
|
|
63
|
+
* Use these when constructing the tools array for /responses requests.
|
|
64
|
+
*/
|
|
65
|
+
getToolDefinitions() {
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
type: "function",
|
|
69
|
+
function: {
|
|
70
|
+
name: ToolNames.FS_READ_FILE,
|
|
71
|
+
description: "Read the contents of a file. Returns the file contents as UTF-8 text.",
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
path: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Workspace-relative path to the file (e.g., 'src/index.ts')"
|
|
78
|
+
},
|
|
79
|
+
max_bytes: {
|
|
80
|
+
type: "integer",
|
|
81
|
+
description: `Maximum bytes to read. Default: ${this.cfg.maxReadBytes}, max: ${this.cfg.hardMaxReadBytes}`
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
required: ["path"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "function",
|
|
90
|
+
function: {
|
|
91
|
+
name: ToolNames.FS_LIST_FILES,
|
|
92
|
+
description: "List files recursively in a directory. Returns newline-separated workspace-relative paths.",
|
|
93
|
+
parameters: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
path: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Workspace-relative directory path. Default: '.' (workspace root)"
|
|
99
|
+
},
|
|
100
|
+
max_entries: {
|
|
101
|
+
type: "integer",
|
|
102
|
+
description: `Maximum files to list. Default: ${this.cfg.maxListEntries}, max: ${this.cfg.hardMaxListEntries}`
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "function",
|
|
110
|
+
function: {
|
|
111
|
+
name: ToolNames.FS_SEARCH,
|
|
112
|
+
description: "Search for text matching a regex pattern. Returns matches as 'path:line:content' format.",
|
|
113
|
+
parameters: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
query: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "Regex pattern to search for"
|
|
119
|
+
},
|
|
120
|
+
path: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Workspace-relative directory to search. Default: '.' (workspace root)"
|
|
123
|
+
},
|
|
124
|
+
max_matches: {
|
|
125
|
+
type: "integer",
|
|
126
|
+
description: `Maximum matches to return. Default: ${this.cfg.maxSearchMatches}, max: ${this.cfg.hardMaxSearchMatches}`
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
required: ["query"]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Registers handlers into an existing ToolRegistry.
|
|
137
|
+
* @param registry - The registry to register into
|
|
138
|
+
* @returns The registry for chaining
|
|
139
|
+
*/
|
|
140
|
+
registerInto(registry) {
|
|
141
|
+
registry.register(ToolNames.FS_READ_FILE, this.readFile.bind(this));
|
|
142
|
+
registry.register(ToolNames.FS_LIST_FILES, this.listFiles.bind(this));
|
|
143
|
+
registry.register(ToolNames.FS_SEARCH, this.search.bind(this));
|
|
144
|
+
return registry;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Creates a new ToolRegistry with fs.* tools pre-registered.
|
|
148
|
+
*/
|
|
149
|
+
toRegistry() {
|
|
150
|
+
return this.registerInto(new ToolRegistry());
|
|
151
|
+
}
|
|
152
|
+
// ========================================================================
|
|
153
|
+
// Tool Handlers
|
|
154
|
+
// ========================================================================
|
|
155
|
+
async readFile(_args, call) {
|
|
156
|
+
const args = this.parseArgs(call, ["path"]);
|
|
157
|
+
const func = call.function;
|
|
158
|
+
const relPath = this.requireString(args, "path", call);
|
|
159
|
+
const requestedMax = this.optionalPositiveInt(args, "max_bytes", call);
|
|
160
|
+
let maxBytes = this.cfg.maxReadBytes;
|
|
161
|
+
if (requestedMax !== void 0) {
|
|
162
|
+
if (requestedMax > this.cfg.hardMaxReadBytes) {
|
|
163
|
+
throw new ToolArgumentError({
|
|
164
|
+
message: `max_bytes exceeds hard cap (${this.cfg.hardMaxReadBytes})`,
|
|
165
|
+
toolCallId: call.id,
|
|
166
|
+
toolName: func.name,
|
|
167
|
+
rawArguments: func.arguments
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
maxBytes = requestedMax;
|
|
171
|
+
}
|
|
172
|
+
const absPath = await this.resolveAndValidatePath(relPath, call);
|
|
173
|
+
const stat = await fs.stat(absPath);
|
|
174
|
+
if (stat.isDirectory()) {
|
|
175
|
+
throw new Error(`fs.read_file: path is a directory: ${relPath}`);
|
|
176
|
+
}
|
|
177
|
+
if (stat.size > maxBytes) {
|
|
178
|
+
throw new Error(`fs.read_file: file exceeds max_bytes (${maxBytes})`);
|
|
179
|
+
}
|
|
180
|
+
const data = await fs.readFile(absPath);
|
|
181
|
+
if (!this.isValidUtf8(data)) {
|
|
182
|
+
throw new Error(`fs.read_file: file is not valid UTF-8: ${relPath}`);
|
|
183
|
+
}
|
|
184
|
+
return data.toString("utf-8");
|
|
185
|
+
}
|
|
186
|
+
async listFiles(_args, call) {
|
|
187
|
+
const args = this.parseArgs(call, []);
|
|
188
|
+
const func = call.function;
|
|
189
|
+
const startPath = this.optionalString(args, "path", call)?.trim() || ".";
|
|
190
|
+
let maxEntries = this.cfg.maxListEntries;
|
|
191
|
+
const requestedMax = this.optionalPositiveInt(args, "max_entries", call);
|
|
192
|
+
if (requestedMax !== void 0) {
|
|
193
|
+
if (requestedMax > this.cfg.hardMaxListEntries) {
|
|
194
|
+
throw new ToolArgumentError({
|
|
195
|
+
message: `max_entries exceeds hard cap (${this.cfg.hardMaxListEntries})`,
|
|
196
|
+
toolCallId: call.id,
|
|
197
|
+
toolName: func.name,
|
|
198
|
+
rawArguments: func.arguments
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
maxEntries = requestedMax;
|
|
202
|
+
}
|
|
203
|
+
const absPath = await this.resolveAndValidatePath(startPath, call);
|
|
204
|
+
let rootReal;
|
|
205
|
+
try {
|
|
206
|
+
rootReal = await fs.realpath(this.rootAbs);
|
|
207
|
+
} catch {
|
|
208
|
+
rootReal = this.rootAbs;
|
|
209
|
+
}
|
|
210
|
+
const stat = await fs.stat(absPath);
|
|
211
|
+
if (!stat.isDirectory()) {
|
|
212
|
+
throw new Error(`fs.list_files: path is not a directory: ${startPath}`);
|
|
213
|
+
}
|
|
214
|
+
const files = [];
|
|
215
|
+
await this.walkDir(absPath, async (filePath, dirent) => {
|
|
216
|
+
if (files.length >= maxEntries) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (dirent.isDirectory()) {
|
|
220
|
+
if (this.cfg.ignoreDirs.has(dirent.name)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
if (dirent.isFile()) {
|
|
226
|
+
const relPath = path.relative(rootReal, filePath);
|
|
227
|
+
files.push(relPath.split(path.sep).join("/"));
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
return files.join("\n");
|
|
232
|
+
}
|
|
233
|
+
async search(_args, call) {
|
|
234
|
+
const args = this.parseArgs(call, ["query"]);
|
|
235
|
+
const func = call.function;
|
|
236
|
+
const query = this.requireString(args, "query", call);
|
|
237
|
+
const startPath = this.optionalString(args, "path", call)?.trim() || ".";
|
|
238
|
+
let maxMatches = this.cfg.maxSearchMatches;
|
|
239
|
+
const requestedMax = this.optionalPositiveInt(args, "max_matches", call);
|
|
240
|
+
if (requestedMax !== void 0) {
|
|
241
|
+
if (requestedMax > this.cfg.hardMaxSearchMatches) {
|
|
242
|
+
throw new ToolArgumentError({
|
|
243
|
+
message: `max_matches exceeds hard cap (${this.cfg.hardMaxSearchMatches})`,
|
|
244
|
+
toolCallId: call.id,
|
|
245
|
+
toolName: func.name,
|
|
246
|
+
rawArguments: func.arguments
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
maxMatches = requestedMax;
|
|
250
|
+
}
|
|
251
|
+
const absPath = await this.resolveAndValidatePath(startPath, call);
|
|
252
|
+
const rgPath = await this.detectRipgrep();
|
|
253
|
+
if (rgPath) {
|
|
254
|
+
return this.searchWithRipgrep(
|
|
255
|
+
rgPath,
|
|
256
|
+
query,
|
|
257
|
+
absPath,
|
|
258
|
+
maxMatches
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return this.searchWithJS(query, absPath, maxMatches, call);
|
|
262
|
+
}
|
|
263
|
+
// ========================================================================
|
|
264
|
+
// Path Safety
|
|
265
|
+
// ========================================================================
|
|
266
|
+
/**
|
|
267
|
+
* Resolves a workspace-relative path and validates it stays within the sandbox.
|
|
268
|
+
* @throws {ToolArgumentError} if path is invalid
|
|
269
|
+
* @throws {PathEscapeError} if resolved path escapes root
|
|
270
|
+
*/
|
|
271
|
+
async resolveAndValidatePath(relPath, call) {
|
|
272
|
+
const func = call.function;
|
|
273
|
+
const cleanRel = relPath.trim();
|
|
274
|
+
if (!cleanRel) {
|
|
275
|
+
throw new ToolArgumentError({
|
|
276
|
+
message: "path cannot be empty",
|
|
277
|
+
toolCallId: call.id,
|
|
278
|
+
toolName: func.name,
|
|
279
|
+
rawArguments: func.arguments
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (path.isAbsolute(cleanRel)) {
|
|
283
|
+
throw new ToolArgumentError({
|
|
284
|
+
message: "path must be workspace-relative (not absolute)",
|
|
285
|
+
toolCallId: call.id,
|
|
286
|
+
toolName: func.name,
|
|
287
|
+
rawArguments: func.arguments
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const normalized = path.normalize(cleanRel);
|
|
291
|
+
if (normalized.startsWith("..") || normalized.startsWith(`.${path.sep}..`)) {
|
|
292
|
+
throw new ToolArgumentError({
|
|
293
|
+
message: "path must not escape the workspace root",
|
|
294
|
+
toolCallId: call.id,
|
|
295
|
+
toolName: func.name,
|
|
296
|
+
rawArguments: func.arguments
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const target = path.join(this.rootAbs, normalized);
|
|
300
|
+
let rootReal;
|
|
301
|
+
try {
|
|
302
|
+
rootReal = await fs.realpath(this.rootAbs);
|
|
303
|
+
} catch {
|
|
304
|
+
rootReal = this.rootAbs;
|
|
305
|
+
}
|
|
306
|
+
let resolved;
|
|
307
|
+
try {
|
|
308
|
+
resolved = await fs.realpath(target);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
resolved = path.join(rootReal, normalized);
|
|
311
|
+
}
|
|
312
|
+
const relFromRoot = path.relative(rootReal, resolved);
|
|
313
|
+
if (relFromRoot.startsWith("..") || relFromRoot.startsWith(`.${path.sep}..`) || path.isAbsolute(relFromRoot)) {
|
|
314
|
+
throw new PathEscapeError({
|
|
315
|
+
requestedPath: relPath,
|
|
316
|
+
resolvedPath: resolved
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return resolved;
|
|
320
|
+
}
|
|
321
|
+
// ========================================================================
|
|
322
|
+
// Ripgrep Search
|
|
323
|
+
// ========================================================================
|
|
324
|
+
async detectRipgrep() {
|
|
325
|
+
if (this.rgChecked) {
|
|
326
|
+
return this.rgPath;
|
|
327
|
+
}
|
|
328
|
+
this.rgChecked = true;
|
|
329
|
+
return new Promise((resolve2) => {
|
|
330
|
+
const proc = spawn("rg", ["--version"], { stdio: "ignore" });
|
|
331
|
+
proc.on("error", () => {
|
|
332
|
+
this.rgPath = null;
|
|
333
|
+
resolve2(null);
|
|
334
|
+
});
|
|
335
|
+
proc.on("close", (code) => {
|
|
336
|
+
if (code === 0) {
|
|
337
|
+
this.rgPath = "rg";
|
|
338
|
+
resolve2("rg");
|
|
339
|
+
} else {
|
|
340
|
+
this.rgPath = null;
|
|
341
|
+
resolve2(null);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async searchWithRipgrep(rgPath, query, dirAbs, maxMatches) {
|
|
347
|
+
return new Promise((resolve2, reject) => {
|
|
348
|
+
const args = ["--line-number", "--no-heading", "--color=never"];
|
|
349
|
+
for (const name of this.cfg.ignoreDirs) {
|
|
350
|
+
args.push("--glob", `!**/${name}/**`);
|
|
351
|
+
}
|
|
352
|
+
args.push(query, dirAbs);
|
|
353
|
+
const proc = spawn(rgPath, args, {
|
|
354
|
+
timeout: this.cfg.searchTimeoutMs
|
|
355
|
+
});
|
|
356
|
+
const lines = [];
|
|
357
|
+
let stderr = "";
|
|
358
|
+
let killed = false;
|
|
359
|
+
proc.stdout.on("data", (chunk) => {
|
|
360
|
+
if (killed) return;
|
|
361
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
362
|
+
const newLines = text.split("\n").filter((l) => l.trim());
|
|
363
|
+
for (const line of newLines) {
|
|
364
|
+
lines.push(this.normalizeRipgrepLine(line));
|
|
365
|
+
if (lines.length >= maxMatches) {
|
|
366
|
+
killed = true;
|
|
367
|
+
proc.kill();
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
proc.stderr.on("data", (chunk) => {
|
|
373
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
374
|
+
});
|
|
375
|
+
proc.on("error", (err) => {
|
|
376
|
+
reject(new Error(`fs.search: ripgrep error: ${err.message}`));
|
|
377
|
+
});
|
|
378
|
+
proc.on("close", (code) => {
|
|
379
|
+
if (killed) {
|
|
380
|
+
resolve2(lines.join("\n"));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (code === 0 || code === 1) {
|
|
384
|
+
resolve2(lines.join("\n"));
|
|
385
|
+
} else if (code === 2 && stderr.toLowerCase().includes("regex")) {
|
|
386
|
+
reject(
|
|
387
|
+
new ToolArgumentError({
|
|
388
|
+
message: `invalid query regex: ${stderr.trim()}`,
|
|
389
|
+
toolCallId: "",
|
|
390
|
+
toolName: ToolNames.FS_SEARCH,
|
|
391
|
+
rawArguments: ""
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
} else if (stderr) {
|
|
395
|
+
reject(new Error(`fs.search: ripgrep failed: ${stderr.trim()}`));
|
|
396
|
+
} else {
|
|
397
|
+
resolve2(lines.join("\n"));
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
normalizeRipgrepLine(line) {
|
|
403
|
+
const trimmed = line.trim();
|
|
404
|
+
if (!trimmed || !trimmed.includes(":")) {
|
|
405
|
+
return trimmed;
|
|
406
|
+
}
|
|
407
|
+
const colonIdx = trimmed.indexOf(":");
|
|
408
|
+
const filePath = trimmed.slice(0, colonIdx);
|
|
409
|
+
const rest = trimmed.slice(colonIdx + 1);
|
|
410
|
+
if (path.isAbsolute(filePath)) {
|
|
411
|
+
const rel = path.relative(this.rootAbs, filePath);
|
|
412
|
+
if (!rel.startsWith("..")) {
|
|
413
|
+
return rel.split(path.sep).join("/") + ":" + rest;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return filePath.split(path.sep).join("/") + ":" + rest;
|
|
417
|
+
}
|
|
418
|
+
// ========================================================================
|
|
419
|
+
// JavaScript Fallback Search
|
|
420
|
+
// ========================================================================
|
|
421
|
+
async searchWithJS(query, dirAbs, maxMatches, call) {
|
|
422
|
+
const func = call.function;
|
|
423
|
+
let regex;
|
|
424
|
+
try {
|
|
425
|
+
regex = new RegExp(query);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
throw new ToolArgumentError({
|
|
428
|
+
message: `invalid query regex: ${err.message}`,
|
|
429
|
+
toolCallId: call.id,
|
|
430
|
+
toolName: func.name,
|
|
431
|
+
rawArguments: func.arguments
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const matches = [];
|
|
435
|
+
const deadline = Date.now() + this.cfg.searchTimeoutMs;
|
|
436
|
+
await this.walkDir(dirAbs, async (filePath, dirent) => {
|
|
437
|
+
if (Date.now() > deadline) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if (matches.length >= maxMatches) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if (dirent.isDirectory()) {
|
|
444
|
+
if (this.cfg.ignoreDirs.has(dirent.name)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (!dirent.isFile()) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const stat = await fs.stat(filePath);
|
|
454
|
+
if (stat.size > this.cfg.maxSearchBytesPerFile) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
462
|
+
const lines = content.split("\n");
|
|
463
|
+
for (let i = 0; i < lines.length && matches.length < maxMatches; i++) {
|
|
464
|
+
if (regex.test(lines[i])) {
|
|
465
|
+
const relPath = path.relative(this.rootAbs, filePath);
|
|
466
|
+
const normalizedPath = relPath.split(path.sep).join("/");
|
|
467
|
+
matches.push(`${normalizedPath}:${i + 1}:${lines[i]}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
});
|
|
474
|
+
return matches.join("\n");
|
|
475
|
+
}
|
|
476
|
+
// ========================================================================
|
|
477
|
+
// Helpers
|
|
478
|
+
// ========================================================================
|
|
479
|
+
parseArgs(call, required) {
|
|
480
|
+
const func = call.function;
|
|
481
|
+
if (!func) {
|
|
482
|
+
throw new ToolArgumentError({
|
|
483
|
+
message: "tool call missing function",
|
|
484
|
+
toolCallId: call.id,
|
|
485
|
+
toolName: "",
|
|
486
|
+
rawArguments: ""
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const rawArgs = func.arguments || "{}";
|
|
490
|
+
let parsed;
|
|
491
|
+
try {
|
|
492
|
+
parsed = JSON.parse(rawArgs);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
throw new ToolArgumentError({
|
|
495
|
+
message: `invalid JSON arguments: ${err.message}`,
|
|
496
|
+
toolCallId: call.id,
|
|
497
|
+
toolName: func.name,
|
|
498
|
+
rawArguments: rawArgs
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
502
|
+
throw new ToolArgumentError({
|
|
503
|
+
message: "arguments must be an object",
|
|
504
|
+
toolCallId: call.id,
|
|
505
|
+
toolName: func.name,
|
|
506
|
+
rawArguments: rawArgs
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const args = parsed;
|
|
510
|
+
for (const key of required) {
|
|
511
|
+
const value = args[key];
|
|
512
|
+
if (value === void 0 || value === null || value === "") {
|
|
513
|
+
throw new ToolArgumentError({
|
|
514
|
+
message: `${key} is required`,
|
|
515
|
+
toolCallId: call.id,
|
|
516
|
+
toolName: func.name,
|
|
517
|
+
rawArguments: rawArgs
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return args;
|
|
522
|
+
}
|
|
523
|
+
toolArgumentError(call, message) {
|
|
524
|
+
const func = call.function;
|
|
525
|
+
throw new ToolArgumentError({
|
|
526
|
+
message,
|
|
527
|
+
toolCallId: call.id,
|
|
528
|
+
toolName: func?.name ?? "",
|
|
529
|
+
rawArguments: func?.arguments ?? ""
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
requireString(args, key, call) {
|
|
533
|
+
const value = args[key];
|
|
534
|
+
if (typeof value !== "string") {
|
|
535
|
+
this.toolArgumentError(call, `${key} must be a string`);
|
|
536
|
+
}
|
|
537
|
+
if (value.trim() === "") {
|
|
538
|
+
this.toolArgumentError(call, `${key} is required`);
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
optionalString(args, key, call) {
|
|
543
|
+
const value = args[key];
|
|
544
|
+
if (value === void 0 || value === null) {
|
|
545
|
+
return void 0;
|
|
546
|
+
}
|
|
547
|
+
if (typeof value !== "string") {
|
|
548
|
+
this.toolArgumentError(call, `${key} must be a string`);
|
|
549
|
+
}
|
|
550
|
+
const trimmed = value.trim();
|
|
551
|
+
if (trimmed === "") {
|
|
552
|
+
return void 0;
|
|
553
|
+
}
|
|
554
|
+
return value;
|
|
555
|
+
}
|
|
556
|
+
optionalPositiveInt(args, key, call) {
|
|
557
|
+
const value = args[key];
|
|
558
|
+
if (value === void 0 || value === null) {
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
561
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
562
|
+
this.toolArgumentError(call, `${key} must be an integer`);
|
|
563
|
+
}
|
|
564
|
+
if (value <= 0) {
|
|
565
|
+
this.toolArgumentError(call, `${key} must be > 0`);
|
|
566
|
+
}
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
isValidUtf8(buffer) {
|
|
570
|
+
try {
|
|
571
|
+
const text = buffer.toString("utf-8");
|
|
572
|
+
return !text.includes("\uFFFD");
|
|
573
|
+
} catch {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Recursively walks a directory, calling visitor for each entry.
|
|
579
|
+
* Visitor returns true to continue, false to skip (for dirs) or stop.
|
|
580
|
+
*/
|
|
581
|
+
async walkDir(dir, visitor) {
|
|
582
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
583
|
+
for (const entry of entries) {
|
|
584
|
+
const fullPath = path.join(dir, entry.name);
|
|
585
|
+
const shouldContinue = await visitor(fullPath, entry);
|
|
586
|
+
if (!shouldContinue) {
|
|
587
|
+
if (entry.isDirectory()) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (entry.isDirectory()) {
|
|
593
|
+
await this.walkDir(fullPath, visitor);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function createLocalFSToolPack(options) {
|
|
599
|
+
return new LocalFSToolPack(options);
|
|
600
|
+
}
|
|
601
|
+
function createLocalFSTools(options) {
|
|
602
|
+
return createLocalFSToolPack(options).toRegistry();
|
|
603
|
+
}
|
|
604
|
+
export {
|
|
605
|
+
DEFAULT_IGNORE_DIRS,
|
|
606
|
+
FSDefaults,
|
|
607
|
+
ToolNames as FSToolNames,
|
|
608
|
+
LocalFSToolPack,
|
|
609
|
+
createLocalFSToolPack,
|
|
610
|
+
createLocalFSTools
|
|
611
|
+
};
|