@modelrelay/sdk 1.25.0 → 1.28.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/chunk-G5H7EY4F.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.cjs
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/node.ts
|
|
31
|
+
var node_exports = {};
|
|
32
|
+
__export(node_exports, {
|
|
33
|
+
DEFAULT_IGNORE_DIRS: () => DEFAULT_IGNORE_DIRS,
|
|
34
|
+
FSDefaults: () => FSDefaults,
|
|
35
|
+
FSToolNames: () => ToolNames,
|
|
36
|
+
LocalFSToolPack: () => LocalFSToolPack,
|
|
37
|
+
createLocalFSToolPack: () => createLocalFSToolPack,
|
|
38
|
+
createLocalFSTools: () => createLocalFSTools
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(node_exports);
|
|
41
|
+
|
|
42
|
+
// src/tools_local_fs.ts
|
|
43
|
+
var import_fs = require("fs");
|
|
44
|
+
var path = __toESM(require("path"), 1);
|
|
45
|
+
var import_child_process = require("child_process");
|
|
46
|
+
|
|
47
|
+
// package.json
|
|
48
|
+
var package_default = {
|
|
49
|
+
name: "@modelrelay/sdk",
|
|
50
|
+
version: "1.28.0",
|
|
51
|
+
description: "TypeScript SDK for the ModelRelay API",
|
|
52
|
+
type: "module",
|
|
53
|
+
main: "dist/index.cjs",
|
|
54
|
+
module: "dist/index.js",
|
|
55
|
+
types: "dist/index.d.ts",
|
|
56
|
+
exports: {
|
|
57
|
+
".": {
|
|
58
|
+
types: "./dist/index.d.ts",
|
|
59
|
+
import: "./dist/index.js",
|
|
60
|
+
require: "./dist/index.cjs"
|
|
61
|
+
},
|
|
62
|
+
"./node": {
|
|
63
|
+
types: "./dist/node.d.ts",
|
|
64
|
+
import: "./dist/node.js",
|
|
65
|
+
require: "./dist/node.cjs"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
publishConfig: {
|
|
69
|
+
access: "public"
|
|
70
|
+
},
|
|
71
|
+
files: [
|
|
72
|
+
"dist"
|
|
73
|
+
],
|
|
74
|
+
scripts: {
|
|
75
|
+
build: "tsup src/index.ts src/node.ts --format esm,cjs --dts --external playwright",
|
|
76
|
+
dev: "tsup src/index.ts src/node.ts --format esm,cjs --dts --watch",
|
|
77
|
+
lint: "tsc --noEmit --project tsconfig.lint.json",
|
|
78
|
+
test: "vitest run",
|
|
79
|
+
"generate:types": "openapi-typescript ../../api/openapi/api.json -o src/generated/api.ts"
|
|
80
|
+
},
|
|
81
|
+
keywords: [
|
|
82
|
+
"modelrelay",
|
|
83
|
+
"llm",
|
|
84
|
+
"sdk",
|
|
85
|
+
"typescript"
|
|
86
|
+
],
|
|
87
|
+
author: "Shane Vitarana",
|
|
88
|
+
license: "Apache-2.0",
|
|
89
|
+
dependencies: {
|
|
90
|
+
"fast-json-patch": "^3.1.1",
|
|
91
|
+
zod: "^3.23.0"
|
|
92
|
+
},
|
|
93
|
+
peerDependencies: {
|
|
94
|
+
playwright: ">=1.40.0"
|
|
95
|
+
},
|
|
96
|
+
peerDependenciesMeta: {
|
|
97
|
+
playwright: {
|
|
98
|
+
optional: true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
devDependencies: {
|
|
102
|
+
"@types/node": "^25.0.3",
|
|
103
|
+
"openapi-typescript": "^7.4.4",
|
|
104
|
+
playwright: "^1.49.0",
|
|
105
|
+
tsup: "^8.2.4",
|
|
106
|
+
typescript: "^5.6.3",
|
|
107
|
+
vitest: "^2.1.4"
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/types.ts
|
|
112
|
+
var SDK_VERSION = package_default.version || "0.0.0";
|
|
113
|
+
var DEFAULT_CLIENT_HEADER = `modelrelay-ts/${SDK_VERSION}`;
|
|
114
|
+
|
|
115
|
+
// src/errors.ts
|
|
116
|
+
var ModelRelayError = class extends Error {
|
|
117
|
+
constructor(message, opts) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.name = this.constructor.name;
|
|
120
|
+
this.category = opts.category;
|
|
121
|
+
this.status = opts.status;
|
|
122
|
+
this.code = opts.code;
|
|
123
|
+
this.requestId = opts.requestId;
|
|
124
|
+
this.fields = opts.fields;
|
|
125
|
+
this.data = opts.data;
|
|
126
|
+
this.retries = opts.retries;
|
|
127
|
+
this.cause = opts.cause;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var ToolArgumentError = class extends ModelRelayError {
|
|
131
|
+
constructor(opts) {
|
|
132
|
+
super(opts.message, {
|
|
133
|
+
category: "config",
|
|
134
|
+
status: 400,
|
|
135
|
+
cause: opts.cause
|
|
136
|
+
});
|
|
137
|
+
this.toolCallId = opts.toolCallId;
|
|
138
|
+
this.toolName = opts.toolName;
|
|
139
|
+
this.rawArguments = opts.rawArguments;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var PathEscapeError = class extends ModelRelayError {
|
|
143
|
+
constructor(opts) {
|
|
144
|
+
super(`path escapes sandbox: ${opts.requestedPath}`, {
|
|
145
|
+
category: "config",
|
|
146
|
+
status: 403
|
|
147
|
+
});
|
|
148
|
+
this.requestedPath = opts.requestedPath;
|
|
149
|
+
this.resolvedPath = opts.resolvedPath;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/tools.ts
|
|
154
|
+
function toolResultMessage(toolCallId, result) {
|
|
155
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
156
|
+
return {
|
|
157
|
+
type: "message",
|
|
158
|
+
role: "tool",
|
|
159
|
+
toolCallId,
|
|
160
|
+
content: [{ type: "text", text: content }]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
var ToolArgsError = class extends Error {
|
|
164
|
+
constructor(message, toolCallId, toolName, rawArguments) {
|
|
165
|
+
super(message);
|
|
166
|
+
this.name = "ToolArgsError";
|
|
167
|
+
this.toolCallId = toolCallId;
|
|
168
|
+
this.toolName = toolName;
|
|
169
|
+
this.rawArguments = rawArguments;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var ToolRegistry = class {
|
|
173
|
+
constructor() {
|
|
174
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Registers a handler function for a tool name.
|
|
178
|
+
* @param name - The tool name (must match the function name in the tool definition)
|
|
179
|
+
* @param handler - Function to execute when this tool is called
|
|
180
|
+
* @returns this for chaining
|
|
181
|
+
*/
|
|
182
|
+
register(name, handler) {
|
|
183
|
+
this.handlers.set(name, handler);
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Unregisters a tool handler.
|
|
188
|
+
* @param name - The tool name to unregister
|
|
189
|
+
* @returns true if the handler was removed, false if it didn't exist
|
|
190
|
+
*/
|
|
191
|
+
unregister(name) {
|
|
192
|
+
return this.handlers.delete(name);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Checks if a handler is registered for the given tool name.
|
|
196
|
+
*/
|
|
197
|
+
has(name) {
|
|
198
|
+
return this.handlers.has(name);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns the list of registered tool names.
|
|
202
|
+
*/
|
|
203
|
+
getRegisteredTools() {
|
|
204
|
+
return Array.from(this.handlers.keys());
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Executes a single tool call.
|
|
208
|
+
* @param call - The tool call to execute
|
|
209
|
+
* @returns The execution result
|
|
210
|
+
*/
|
|
211
|
+
async execute(call) {
|
|
212
|
+
const toolName = call.function?.name ?? "";
|
|
213
|
+
const handler = this.handlers.get(toolName);
|
|
214
|
+
if (!handler) {
|
|
215
|
+
return {
|
|
216
|
+
toolCallId: call.id,
|
|
217
|
+
toolName,
|
|
218
|
+
result: null,
|
|
219
|
+
error: `Unknown tool: '${toolName}'. Available tools: ${this.getRegisteredTools().join(", ") || "none"}`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
let args;
|
|
223
|
+
try {
|
|
224
|
+
args = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
|
225
|
+
} catch (err) {
|
|
226
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
227
|
+
return {
|
|
228
|
+
toolCallId: call.id,
|
|
229
|
+
toolName,
|
|
230
|
+
result: null,
|
|
231
|
+
error: `Invalid JSON in arguments: ${errorMessage}`,
|
|
232
|
+
isRetryable: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const result = await handler(args, call);
|
|
237
|
+
return {
|
|
238
|
+
toolCallId: call.id,
|
|
239
|
+
toolName,
|
|
240
|
+
result
|
|
241
|
+
};
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const isRetryable = err instanceof ToolArgsError || err instanceof ToolArgumentError;
|
|
244
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
245
|
+
return {
|
|
246
|
+
toolCallId: call.id,
|
|
247
|
+
toolName,
|
|
248
|
+
result: null,
|
|
249
|
+
error: errorMessage,
|
|
250
|
+
isRetryable
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Executes multiple tool calls in parallel.
|
|
256
|
+
* @param calls - Array of tool calls to execute
|
|
257
|
+
* @returns Array of execution results in the same order as input
|
|
258
|
+
*/
|
|
259
|
+
async executeAll(calls) {
|
|
260
|
+
return Promise.all(calls.map((call) => this.execute(call)));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Converts execution results to tool result messages.
|
|
264
|
+
* Useful for appending to the conversation history.
|
|
265
|
+
* @param results - Array of execution results
|
|
266
|
+
* @returns Array of tool result input items (role "tool")
|
|
267
|
+
*/
|
|
268
|
+
resultsToMessages(results) {
|
|
269
|
+
return results.map((r) => {
|
|
270
|
+
const content = r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result);
|
|
271
|
+
return toolResultMessage(r.toolCallId, content);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/tools_local_fs.ts
|
|
277
|
+
var ToolNames = {
|
|
278
|
+
FS_READ_FILE: "fs.read_file",
|
|
279
|
+
FS_LIST_FILES: "fs.list_files",
|
|
280
|
+
FS_SEARCH: "fs.search"
|
|
281
|
+
};
|
|
282
|
+
var FSDefaults = {
|
|
283
|
+
MAX_READ_BYTES: 64e3,
|
|
284
|
+
HARD_MAX_READ_BYTES: 1e6,
|
|
285
|
+
MAX_LIST_ENTRIES: 2e3,
|
|
286
|
+
HARD_MAX_LIST_ENTRIES: 2e4,
|
|
287
|
+
MAX_SEARCH_MATCHES: 100,
|
|
288
|
+
HARD_MAX_SEARCH_MATCHES: 2e3,
|
|
289
|
+
SEARCH_TIMEOUT_MS: 5e3,
|
|
290
|
+
MAX_SEARCH_BYTES_PER_FILE: 1e6
|
|
291
|
+
};
|
|
292
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
293
|
+
".git",
|
|
294
|
+
"node_modules",
|
|
295
|
+
"vendor",
|
|
296
|
+
"dist",
|
|
297
|
+
"build",
|
|
298
|
+
".next",
|
|
299
|
+
"target",
|
|
300
|
+
".idea",
|
|
301
|
+
".vscode",
|
|
302
|
+
"__pycache__",
|
|
303
|
+
".pytest_cache",
|
|
304
|
+
"coverage"
|
|
305
|
+
]);
|
|
306
|
+
var LocalFSToolPack = class {
|
|
307
|
+
constructor(options) {
|
|
308
|
+
this.rgPath = null;
|
|
309
|
+
this.rgChecked = false;
|
|
310
|
+
const root = options.root?.trim();
|
|
311
|
+
if (!root) {
|
|
312
|
+
throw new Error("LocalFSToolPack: root directory required");
|
|
313
|
+
}
|
|
314
|
+
this.rootAbs = path.resolve(root);
|
|
315
|
+
this.cfg = {
|
|
316
|
+
ignoreDirs: options.ignoreDirs ?? new Set(DEFAULT_IGNORE_DIRS),
|
|
317
|
+
maxReadBytes: options.maxReadBytes ?? FSDefaults.MAX_READ_BYTES,
|
|
318
|
+
hardMaxReadBytes: options.hardMaxReadBytes ?? FSDefaults.HARD_MAX_READ_BYTES,
|
|
319
|
+
maxListEntries: options.maxListEntries ?? FSDefaults.MAX_LIST_ENTRIES,
|
|
320
|
+
hardMaxListEntries: options.hardMaxListEntries ?? FSDefaults.HARD_MAX_LIST_ENTRIES,
|
|
321
|
+
maxSearchMatches: options.maxSearchMatches ?? FSDefaults.MAX_SEARCH_MATCHES,
|
|
322
|
+
hardMaxSearchMatches: options.hardMaxSearchMatches ?? FSDefaults.HARD_MAX_SEARCH_MATCHES,
|
|
323
|
+
searchTimeoutMs: options.searchTimeoutMs ?? FSDefaults.SEARCH_TIMEOUT_MS,
|
|
324
|
+
maxSearchBytesPerFile: options.maxSearchBytesPerFile ?? FSDefaults.MAX_SEARCH_BYTES_PER_FILE
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Returns the tool definitions for LLM requests.
|
|
329
|
+
* Use these when constructing the tools array for /responses requests.
|
|
330
|
+
*/
|
|
331
|
+
getToolDefinitions() {
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
type: "function",
|
|
335
|
+
function: {
|
|
336
|
+
name: ToolNames.FS_READ_FILE,
|
|
337
|
+
description: "Read the contents of a file. Returns the file contents as UTF-8 text.",
|
|
338
|
+
parameters: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
path: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: "Workspace-relative path to the file (e.g., 'src/index.ts')"
|
|
344
|
+
},
|
|
345
|
+
max_bytes: {
|
|
346
|
+
type: "integer",
|
|
347
|
+
description: `Maximum bytes to read. Default: ${this.cfg.maxReadBytes}, max: ${this.cfg.hardMaxReadBytes}`
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
required: ["path"]
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
type: "function",
|
|
356
|
+
function: {
|
|
357
|
+
name: ToolNames.FS_LIST_FILES,
|
|
358
|
+
description: "List files recursively in a directory. Returns newline-separated workspace-relative paths.",
|
|
359
|
+
parameters: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
path: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "Workspace-relative directory path. Default: '.' (workspace root)"
|
|
365
|
+
},
|
|
366
|
+
max_entries: {
|
|
367
|
+
type: "integer",
|
|
368
|
+
description: `Maximum files to list. Default: ${this.cfg.maxListEntries}, max: ${this.cfg.hardMaxListEntries}`
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
type: "function",
|
|
376
|
+
function: {
|
|
377
|
+
name: ToolNames.FS_SEARCH,
|
|
378
|
+
description: "Search for text matching a regex pattern. Returns matches as 'path:line:content' format.",
|
|
379
|
+
parameters: {
|
|
380
|
+
type: "object",
|
|
381
|
+
properties: {
|
|
382
|
+
query: {
|
|
383
|
+
type: "string",
|
|
384
|
+
description: "Regex pattern to search for"
|
|
385
|
+
},
|
|
386
|
+
path: {
|
|
387
|
+
type: "string",
|
|
388
|
+
description: "Workspace-relative directory to search. Default: '.' (workspace root)"
|
|
389
|
+
},
|
|
390
|
+
max_matches: {
|
|
391
|
+
type: "integer",
|
|
392
|
+
description: `Maximum matches to return. Default: ${this.cfg.maxSearchMatches}, max: ${this.cfg.hardMaxSearchMatches}`
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
required: ["query"]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Registers handlers into an existing ToolRegistry.
|
|
403
|
+
* @param registry - The registry to register into
|
|
404
|
+
* @returns The registry for chaining
|
|
405
|
+
*/
|
|
406
|
+
registerInto(registry) {
|
|
407
|
+
registry.register(ToolNames.FS_READ_FILE, this.readFile.bind(this));
|
|
408
|
+
registry.register(ToolNames.FS_LIST_FILES, this.listFiles.bind(this));
|
|
409
|
+
registry.register(ToolNames.FS_SEARCH, this.search.bind(this));
|
|
410
|
+
return registry;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Creates a new ToolRegistry with fs.* tools pre-registered.
|
|
414
|
+
*/
|
|
415
|
+
toRegistry() {
|
|
416
|
+
return this.registerInto(new ToolRegistry());
|
|
417
|
+
}
|
|
418
|
+
// ========================================================================
|
|
419
|
+
// Tool Handlers
|
|
420
|
+
// ========================================================================
|
|
421
|
+
async readFile(_args, call) {
|
|
422
|
+
const args = this.parseArgs(call, ["path"]);
|
|
423
|
+
const func = call.function;
|
|
424
|
+
const relPath = this.requireString(args, "path", call);
|
|
425
|
+
const requestedMax = this.optionalPositiveInt(args, "max_bytes", call);
|
|
426
|
+
let maxBytes = this.cfg.maxReadBytes;
|
|
427
|
+
if (requestedMax !== void 0) {
|
|
428
|
+
if (requestedMax > this.cfg.hardMaxReadBytes) {
|
|
429
|
+
throw new ToolArgumentError({
|
|
430
|
+
message: `max_bytes exceeds hard cap (${this.cfg.hardMaxReadBytes})`,
|
|
431
|
+
toolCallId: call.id,
|
|
432
|
+
toolName: func.name,
|
|
433
|
+
rawArguments: func.arguments
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
maxBytes = requestedMax;
|
|
437
|
+
}
|
|
438
|
+
const absPath = await this.resolveAndValidatePath(relPath, call);
|
|
439
|
+
const stat = await import_fs.promises.stat(absPath);
|
|
440
|
+
if (stat.isDirectory()) {
|
|
441
|
+
throw new Error(`fs.read_file: path is a directory: ${relPath}`);
|
|
442
|
+
}
|
|
443
|
+
if (stat.size > maxBytes) {
|
|
444
|
+
throw new Error(`fs.read_file: file exceeds max_bytes (${maxBytes})`);
|
|
445
|
+
}
|
|
446
|
+
const data = await import_fs.promises.readFile(absPath);
|
|
447
|
+
if (!this.isValidUtf8(data)) {
|
|
448
|
+
throw new Error(`fs.read_file: file is not valid UTF-8: ${relPath}`);
|
|
449
|
+
}
|
|
450
|
+
return data.toString("utf-8");
|
|
451
|
+
}
|
|
452
|
+
async listFiles(_args, call) {
|
|
453
|
+
const args = this.parseArgs(call, []);
|
|
454
|
+
const func = call.function;
|
|
455
|
+
const startPath = this.optionalString(args, "path", call)?.trim() || ".";
|
|
456
|
+
let maxEntries = this.cfg.maxListEntries;
|
|
457
|
+
const requestedMax = this.optionalPositiveInt(args, "max_entries", call);
|
|
458
|
+
if (requestedMax !== void 0) {
|
|
459
|
+
if (requestedMax > this.cfg.hardMaxListEntries) {
|
|
460
|
+
throw new ToolArgumentError({
|
|
461
|
+
message: `max_entries exceeds hard cap (${this.cfg.hardMaxListEntries})`,
|
|
462
|
+
toolCallId: call.id,
|
|
463
|
+
toolName: func.name,
|
|
464
|
+
rawArguments: func.arguments
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
maxEntries = requestedMax;
|
|
468
|
+
}
|
|
469
|
+
const absPath = await this.resolveAndValidatePath(startPath, call);
|
|
470
|
+
let rootReal;
|
|
471
|
+
try {
|
|
472
|
+
rootReal = await import_fs.promises.realpath(this.rootAbs);
|
|
473
|
+
} catch {
|
|
474
|
+
rootReal = this.rootAbs;
|
|
475
|
+
}
|
|
476
|
+
const stat = await import_fs.promises.stat(absPath);
|
|
477
|
+
if (!stat.isDirectory()) {
|
|
478
|
+
throw new Error(`fs.list_files: path is not a directory: ${startPath}`);
|
|
479
|
+
}
|
|
480
|
+
const files = [];
|
|
481
|
+
await this.walkDir(absPath, async (filePath, dirent) => {
|
|
482
|
+
if (files.length >= maxEntries) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (dirent.isDirectory()) {
|
|
486
|
+
if (this.cfg.ignoreDirs.has(dirent.name)) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
if (dirent.isFile()) {
|
|
492
|
+
const relPath = path.relative(rootReal, filePath);
|
|
493
|
+
files.push(relPath.split(path.sep).join("/"));
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
});
|
|
497
|
+
return files.join("\n");
|
|
498
|
+
}
|
|
499
|
+
async search(_args, call) {
|
|
500
|
+
const args = this.parseArgs(call, ["query"]);
|
|
501
|
+
const func = call.function;
|
|
502
|
+
const query = this.requireString(args, "query", call);
|
|
503
|
+
const startPath = this.optionalString(args, "path", call)?.trim() || ".";
|
|
504
|
+
let maxMatches = this.cfg.maxSearchMatches;
|
|
505
|
+
const requestedMax = this.optionalPositiveInt(args, "max_matches", call);
|
|
506
|
+
if (requestedMax !== void 0) {
|
|
507
|
+
if (requestedMax > this.cfg.hardMaxSearchMatches) {
|
|
508
|
+
throw new ToolArgumentError({
|
|
509
|
+
message: `max_matches exceeds hard cap (${this.cfg.hardMaxSearchMatches})`,
|
|
510
|
+
toolCallId: call.id,
|
|
511
|
+
toolName: func.name,
|
|
512
|
+
rawArguments: func.arguments
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
maxMatches = requestedMax;
|
|
516
|
+
}
|
|
517
|
+
const absPath = await this.resolveAndValidatePath(startPath, call);
|
|
518
|
+
const rgPath = await this.detectRipgrep();
|
|
519
|
+
if (rgPath) {
|
|
520
|
+
return this.searchWithRipgrep(
|
|
521
|
+
rgPath,
|
|
522
|
+
query,
|
|
523
|
+
absPath,
|
|
524
|
+
maxMatches
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return this.searchWithJS(query, absPath, maxMatches, call);
|
|
528
|
+
}
|
|
529
|
+
// ========================================================================
|
|
530
|
+
// Path Safety
|
|
531
|
+
// ========================================================================
|
|
532
|
+
/**
|
|
533
|
+
* Resolves a workspace-relative path and validates it stays within the sandbox.
|
|
534
|
+
* @throws {ToolArgumentError} if path is invalid
|
|
535
|
+
* @throws {PathEscapeError} if resolved path escapes root
|
|
536
|
+
*/
|
|
537
|
+
async resolveAndValidatePath(relPath, call) {
|
|
538
|
+
const func = call.function;
|
|
539
|
+
const cleanRel = relPath.trim();
|
|
540
|
+
if (!cleanRel) {
|
|
541
|
+
throw new ToolArgumentError({
|
|
542
|
+
message: "path cannot be empty",
|
|
543
|
+
toolCallId: call.id,
|
|
544
|
+
toolName: func.name,
|
|
545
|
+
rawArguments: func.arguments
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (path.isAbsolute(cleanRel)) {
|
|
549
|
+
throw new ToolArgumentError({
|
|
550
|
+
message: "path must be workspace-relative (not absolute)",
|
|
551
|
+
toolCallId: call.id,
|
|
552
|
+
toolName: func.name,
|
|
553
|
+
rawArguments: func.arguments
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const normalized = path.normalize(cleanRel);
|
|
557
|
+
if (normalized.startsWith("..") || normalized.startsWith(`.${path.sep}..`)) {
|
|
558
|
+
throw new ToolArgumentError({
|
|
559
|
+
message: "path must not escape the workspace root",
|
|
560
|
+
toolCallId: call.id,
|
|
561
|
+
toolName: func.name,
|
|
562
|
+
rawArguments: func.arguments
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const target = path.join(this.rootAbs, normalized);
|
|
566
|
+
let rootReal;
|
|
567
|
+
try {
|
|
568
|
+
rootReal = await import_fs.promises.realpath(this.rootAbs);
|
|
569
|
+
} catch {
|
|
570
|
+
rootReal = this.rootAbs;
|
|
571
|
+
}
|
|
572
|
+
let resolved;
|
|
573
|
+
try {
|
|
574
|
+
resolved = await import_fs.promises.realpath(target);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
resolved = path.join(rootReal, normalized);
|
|
577
|
+
}
|
|
578
|
+
const relFromRoot = path.relative(rootReal, resolved);
|
|
579
|
+
if (relFromRoot.startsWith("..") || relFromRoot.startsWith(`.${path.sep}..`) || path.isAbsolute(relFromRoot)) {
|
|
580
|
+
throw new PathEscapeError({
|
|
581
|
+
requestedPath: relPath,
|
|
582
|
+
resolvedPath: resolved
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return resolved;
|
|
586
|
+
}
|
|
587
|
+
// ========================================================================
|
|
588
|
+
// Ripgrep Search
|
|
589
|
+
// ========================================================================
|
|
590
|
+
async detectRipgrep() {
|
|
591
|
+
if (this.rgChecked) {
|
|
592
|
+
return this.rgPath;
|
|
593
|
+
}
|
|
594
|
+
this.rgChecked = true;
|
|
595
|
+
return new Promise((resolve2) => {
|
|
596
|
+
const proc = (0, import_child_process.spawn)("rg", ["--version"], { stdio: "ignore" });
|
|
597
|
+
proc.on("error", () => {
|
|
598
|
+
this.rgPath = null;
|
|
599
|
+
resolve2(null);
|
|
600
|
+
});
|
|
601
|
+
proc.on("close", (code) => {
|
|
602
|
+
if (code === 0) {
|
|
603
|
+
this.rgPath = "rg";
|
|
604
|
+
resolve2("rg");
|
|
605
|
+
} else {
|
|
606
|
+
this.rgPath = null;
|
|
607
|
+
resolve2(null);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async searchWithRipgrep(rgPath, query, dirAbs, maxMatches) {
|
|
613
|
+
return new Promise((resolve2, reject) => {
|
|
614
|
+
const args = ["--line-number", "--no-heading", "--color=never"];
|
|
615
|
+
for (const name of this.cfg.ignoreDirs) {
|
|
616
|
+
args.push("--glob", `!**/${name}/**`);
|
|
617
|
+
}
|
|
618
|
+
args.push(query, dirAbs);
|
|
619
|
+
const proc = (0, import_child_process.spawn)(rgPath, args, {
|
|
620
|
+
timeout: this.cfg.searchTimeoutMs
|
|
621
|
+
});
|
|
622
|
+
const lines = [];
|
|
623
|
+
let stderr = "";
|
|
624
|
+
let killed = false;
|
|
625
|
+
proc.stdout.on("data", (chunk) => {
|
|
626
|
+
if (killed) return;
|
|
627
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
628
|
+
const newLines = text.split("\n").filter((l) => l.trim());
|
|
629
|
+
for (const line of newLines) {
|
|
630
|
+
lines.push(this.normalizeRipgrepLine(line));
|
|
631
|
+
if (lines.length >= maxMatches) {
|
|
632
|
+
killed = true;
|
|
633
|
+
proc.kill();
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
proc.stderr.on("data", (chunk) => {
|
|
639
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
640
|
+
});
|
|
641
|
+
proc.on("error", (err) => {
|
|
642
|
+
reject(new Error(`fs.search: ripgrep error: ${err.message}`));
|
|
643
|
+
});
|
|
644
|
+
proc.on("close", (code) => {
|
|
645
|
+
if (killed) {
|
|
646
|
+
resolve2(lines.join("\n"));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (code === 0 || code === 1) {
|
|
650
|
+
resolve2(lines.join("\n"));
|
|
651
|
+
} else if (code === 2 && stderr.toLowerCase().includes("regex")) {
|
|
652
|
+
reject(
|
|
653
|
+
new ToolArgumentError({
|
|
654
|
+
message: `invalid query regex: ${stderr.trim()}`,
|
|
655
|
+
toolCallId: "",
|
|
656
|
+
toolName: ToolNames.FS_SEARCH,
|
|
657
|
+
rawArguments: ""
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
} else if (stderr) {
|
|
661
|
+
reject(new Error(`fs.search: ripgrep failed: ${stderr.trim()}`));
|
|
662
|
+
} else {
|
|
663
|
+
resolve2(lines.join("\n"));
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
normalizeRipgrepLine(line) {
|
|
669
|
+
const trimmed = line.trim();
|
|
670
|
+
if (!trimmed || !trimmed.includes(":")) {
|
|
671
|
+
return trimmed;
|
|
672
|
+
}
|
|
673
|
+
const colonIdx = trimmed.indexOf(":");
|
|
674
|
+
const filePath = trimmed.slice(0, colonIdx);
|
|
675
|
+
const rest = trimmed.slice(colonIdx + 1);
|
|
676
|
+
if (path.isAbsolute(filePath)) {
|
|
677
|
+
const rel = path.relative(this.rootAbs, filePath);
|
|
678
|
+
if (!rel.startsWith("..")) {
|
|
679
|
+
return rel.split(path.sep).join("/") + ":" + rest;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return filePath.split(path.sep).join("/") + ":" + rest;
|
|
683
|
+
}
|
|
684
|
+
// ========================================================================
|
|
685
|
+
// JavaScript Fallback Search
|
|
686
|
+
// ========================================================================
|
|
687
|
+
async searchWithJS(query, dirAbs, maxMatches, call) {
|
|
688
|
+
const func = call.function;
|
|
689
|
+
let regex;
|
|
690
|
+
try {
|
|
691
|
+
regex = new RegExp(query);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
throw new ToolArgumentError({
|
|
694
|
+
message: `invalid query regex: ${err.message}`,
|
|
695
|
+
toolCallId: call.id,
|
|
696
|
+
toolName: func.name,
|
|
697
|
+
rawArguments: func.arguments
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const matches = [];
|
|
701
|
+
const deadline = Date.now() + this.cfg.searchTimeoutMs;
|
|
702
|
+
await this.walkDir(dirAbs, async (filePath, dirent) => {
|
|
703
|
+
if (Date.now() > deadline) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
if (matches.length >= maxMatches) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
if (dirent.isDirectory()) {
|
|
710
|
+
if (this.cfg.ignoreDirs.has(dirent.name)) {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
if (!dirent.isFile()) {
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
const stat = await import_fs.promises.stat(filePath);
|
|
720
|
+
if (stat.size > this.cfg.maxSearchBytesPerFile) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const content = await import_fs.promises.readFile(filePath, "utf-8");
|
|
728
|
+
const lines = content.split("\n");
|
|
729
|
+
for (let i = 0; i < lines.length && matches.length < maxMatches; i++) {
|
|
730
|
+
if (regex.test(lines[i])) {
|
|
731
|
+
const relPath = path.relative(this.rootAbs, filePath);
|
|
732
|
+
const normalizedPath = relPath.split(path.sep).join("/");
|
|
733
|
+
matches.push(`${normalizedPath}:${i + 1}:${lines[i]}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
return true;
|
|
739
|
+
});
|
|
740
|
+
return matches.join("\n");
|
|
741
|
+
}
|
|
742
|
+
// ========================================================================
|
|
743
|
+
// Helpers
|
|
744
|
+
// ========================================================================
|
|
745
|
+
parseArgs(call, required) {
|
|
746
|
+
const func = call.function;
|
|
747
|
+
if (!func) {
|
|
748
|
+
throw new ToolArgumentError({
|
|
749
|
+
message: "tool call missing function",
|
|
750
|
+
toolCallId: call.id,
|
|
751
|
+
toolName: "",
|
|
752
|
+
rawArguments: ""
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
const rawArgs = func.arguments || "{}";
|
|
756
|
+
let parsed;
|
|
757
|
+
try {
|
|
758
|
+
parsed = JSON.parse(rawArgs);
|
|
759
|
+
} catch (err) {
|
|
760
|
+
throw new ToolArgumentError({
|
|
761
|
+
message: `invalid JSON arguments: ${err.message}`,
|
|
762
|
+
toolCallId: call.id,
|
|
763
|
+
toolName: func.name,
|
|
764
|
+
rawArguments: rawArgs
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
768
|
+
throw new ToolArgumentError({
|
|
769
|
+
message: "arguments must be an object",
|
|
770
|
+
toolCallId: call.id,
|
|
771
|
+
toolName: func.name,
|
|
772
|
+
rawArguments: rawArgs
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const args = parsed;
|
|
776
|
+
for (const key of required) {
|
|
777
|
+
const value = args[key];
|
|
778
|
+
if (value === void 0 || value === null || value === "") {
|
|
779
|
+
throw new ToolArgumentError({
|
|
780
|
+
message: `${key} is required`,
|
|
781
|
+
toolCallId: call.id,
|
|
782
|
+
toolName: func.name,
|
|
783
|
+
rawArguments: rawArgs
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return args;
|
|
788
|
+
}
|
|
789
|
+
toolArgumentError(call, message) {
|
|
790
|
+
const func = call.function;
|
|
791
|
+
throw new ToolArgumentError({
|
|
792
|
+
message,
|
|
793
|
+
toolCallId: call.id,
|
|
794
|
+
toolName: func?.name ?? "",
|
|
795
|
+
rawArguments: func?.arguments ?? ""
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
requireString(args, key, call) {
|
|
799
|
+
const value = args[key];
|
|
800
|
+
if (typeof value !== "string") {
|
|
801
|
+
this.toolArgumentError(call, `${key} must be a string`);
|
|
802
|
+
}
|
|
803
|
+
if (value.trim() === "") {
|
|
804
|
+
this.toolArgumentError(call, `${key} is required`);
|
|
805
|
+
}
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
optionalString(args, key, call) {
|
|
809
|
+
const value = args[key];
|
|
810
|
+
if (value === void 0 || value === null) {
|
|
811
|
+
return void 0;
|
|
812
|
+
}
|
|
813
|
+
if (typeof value !== "string") {
|
|
814
|
+
this.toolArgumentError(call, `${key} must be a string`);
|
|
815
|
+
}
|
|
816
|
+
const trimmed = value.trim();
|
|
817
|
+
if (trimmed === "") {
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
return value;
|
|
821
|
+
}
|
|
822
|
+
optionalPositiveInt(args, key, call) {
|
|
823
|
+
const value = args[key];
|
|
824
|
+
if (value === void 0 || value === null) {
|
|
825
|
+
return void 0;
|
|
826
|
+
}
|
|
827
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
828
|
+
this.toolArgumentError(call, `${key} must be an integer`);
|
|
829
|
+
}
|
|
830
|
+
if (value <= 0) {
|
|
831
|
+
this.toolArgumentError(call, `${key} must be > 0`);
|
|
832
|
+
}
|
|
833
|
+
return value;
|
|
834
|
+
}
|
|
835
|
+
isValidUtf8(buffer) {
|
|
836
|
+
try {
|
|
837
|
+
const text = buffer.toString("utf-8");
|
|
838
|
+
return !text.includes("\uFFFD");
|
|
839
|
+
} catch {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Recursively walks a directory, calling visitor for each entry.
|
|
845
|
+
* Visitor returns true to continue, false to skip (for dirs) or stop.
|
|
846
|
+
*/
|
|
847
|
+
async walkDir(dir, visitor) {
|
|
848
|
+
const entries = await import_fs.promises.readdir(dir, { withFileTypes: true });
|
|
849
|
+
for (const entry of entries) {
|
|
850
|
+
const fullPath = path.join(dir, entry.name);
|
|
851
|
+
const shouldContinue = await visitor(fullPath, entry);
|
|
852
|
+
if (!shouldContinue) {
|
|
853
|
+
if (entry.isDirectory()) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (entry.isDirectory()) {
|
|
859
|
+
await this.walkDir(fullPath, visitor);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
function createLocalFSToolPack(options) {
|
|
865
|
+
return new LocalFSToolPack(options);
|
|
866
|
+
}
|
|
867
|
+
function createLocalFSTools(options) {
|
|
868
|
+
return createLocalFSToolPack(options).toRegistry();
|
|
869
|
+
}
|
|
870
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
871
|
+
0 && (module.exports = {
|
|
872
|
+
DEFAULT_IGNORE_DIRS,
|
|
873
|
+
FSDefaults,
|
|
874
|
+
FSToolNames,
|
|
875
|
+
LocalFSToolPack,
|
|
876
|
+
createLocalFSToolPack,
|
|
877
|
+
createLocalFSTools
|
|
878
|
+
});
|