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