@ch4p/cli 0.3.0 → 0.3.3

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.
@@ -0,0 +1,3413 @@
1
+ import {
2
+ SecurityError,
3
+ ToolError
4
+ } from "./chunk-YSCX2QQQ.js";
5
+
6
+ // ../../packages/tools/dist/index.js
7
+ import { spawn } from "child_process";
8
+ import { resolve } from "path";
9
+ import { stat, readdir } from "fs/promises";
10
+ import { readFile, stat as stat2 } from "fs/promises";
11
+ import { resolve as resolve2, extname } from "path";
12
+ import { writeFile, mkdir } from "fs/promises";
13
+ import { resolve as resolve3, dirname } from "path";
14
+ import { stat as stat3, readFile as readFile2 } from "fs/promises";
15
+ import { createHash } from "crypto";
16
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
17
+ import { resolve as resolve4 } from "path";
18
+ import { readdir as readdir2, stat as stat4, readFile as readFile4 } from "fs/promises";
19
+ import { resolve as resolve5, join, relative, extname as extname2 } from "path";
20
+ import { readdir as readdir3, stat as stat5 } from "fs/promises";
21
+ import { resolve as resolve6, join as join2 } from "path";
22
+ import { randomBytes } from "crypto";
23
+ import { resolve4 as resolve42, resolve6 as resolve62 } from "dns/promises";
24
+ var DEFAULT_TIMEOUT_MS = 12e4;
25
+ var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
26
+ var MAX_BASH_OUTPUT_BYTES = 10 * 1024 * 1024;
27
+ var BashTool = class {
28
+ name = "bash";
29
+ description = "Execute a shell command. Commands are validated against the security policy. Output from stdout and stderr is captured and returned. Long-running commands can be cancelled via abort signal.";
30
+ weight = "heavyweight";
31
+ parameters = {
32
+ type: "object",
33
+ properties: {
34
+ command: {
35
+ type: "string",
36
+ description: "The shell command to execute.",
37
+ minLength: 1
38
+ },
39
+ timeout: {
40
+ type: "number",
41
+ description: "Timeout in milliseconds. Defaults to 120000 (2 minutes).",
42
+ minimum: 1,
43
+ maximum: 6e5
44
+ },
45
+ cwd: {
46
+ type: "string",
47
+ description: "Working directory for the command. Defaults to the session working directory."
48
+ }
49
+ },
50
+ required: ["command"],
51
+ additionalProperties: false
52
+ };
53
+ activeProcess = null;
54
+ validate(args) {
55
+ if (typeof args !== "object" || args === null) {
56
+ return { valid: false, errors: ["Arguments must be an object."] };
57
+ }
58
+ const { command, timeout, cwd } = args;
59
+ const errors = [];
60
+ if (typeof command !== "string" || command.trim().length === 0) {
61
+ errors.push("command must be a non-empty string.");
62
+ }
63
+ if (timeout !== void 0) {
64
+ if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout < 1) {
65
+ errors.push("timeout must be a positive number.");
66
+ }
67
+ if (typeof timeout === "number" && timeout > 6e5) {
68
+ errors.push("timeout cannot exceed 600000ms (10 minutes).");
69
+ }
70
+ }
71
+ if (cwd !== void 0 && typeof cwd !== "string") {
72
+ errors.push("cwd must be a string.");
73
+ }
74
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
75
+ }
76
+ async execute(args, context) {
77
+ const validation = this.validate(args);
78
+ if (!validation.valid) {
79
+ return {
80
+ success: false,
81
+ output: "",
82
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
83
+ };
84
+ }
85
+ const { command, timeout, cwd } = args;
86
+ const timeoutMs = timeout ?? DEFAULT_TIMEOUT_MS;
87
+ const workingDir = cwd ? resolve(context.cwd, cwd) : context.cwd;
88
+ const cwdValidation = context.securityPolicy.validatePath(workingDir, "read");
89
+ if (!cwdValidation.allowed) {
90
+ throw new SecurityError(
91
+ `Working directory blocked: ${cwdValidation.reason ?? workingDir}`,
92
+ { path: workingDir }
93
+ );
94
+ }
95
+ const commandValidation = context.securityPolicy.validateCommand("bash", [
96
+ "-c",
97
+ command
98
+ ]);
99
+ if (!commandValidation.allowed) {
100
+ throw new SecurityError(
101
+ `Command blocked: ${commandValidation.reason ?? command}`,
102
+ { command }
103
+ );
104
+ }
105
+ if (context.abortSignal.aborted) {
106
+ return {
107
+ success: false,
108
+ output: "",
109
+ error: "Command aborted before execution."
110
+ };
111
+ }
112
+ return this.runCommand(command, workingDir, timeoutMs, context);
113
+ }
114
+ abort(_reason) {
115
+ if (this.activeProcess && !this.activeProcess.killed) {
116
+ this.activeProcess.kill("SIGTERM");
117
+ setTimeout(() => {
118
+ if (this.activeProcess && !this.activeProcess.killed) {
119
+ this.activeProcess.kill("SIGKILL");
120
+ }
121
+ }, 5e3);
122
+ }
123
+ }
124
+ runCommand(command, cwd, timeoutMs, context) {
125
+ return new Promise((resolvePromise) => {
126
+ const child = spawn("bash", ["-c", command], {
127
+ cwd,
128
+ stdio: ["pipe", "pipe", "pipe"],
129
+ env: { ...process.env }
130
+ // Close stdin immediately — we never write to it
131
+ });
132
+ this.activeProcess = child;
133
+ child.stdin?.end();
134
+ const stdoutChunks = [];
135
+ const stderrChunks = [];
136
+ let totalBytes = 0;
137
+ let killed = false;
138
+ let outputCapped = false;
139
+ const killForOutputCap = () => {
140
+ if (outputCapped) return;
141
+ outputCapped = true;
142
+ child.kill("SIGTERM");
143
+ setTimeout(() => {
144
+ if (!child.killed) child.kill("SIGKILL");
145
+ }, 5e3);
146
+ };
147
+ child.stdout?.on("data", (chunk) => {
148
+ stdoutChunks.push(chunk);
149
+ totalBytes += chunk.length;
150
+ context.onProgress(`[stdout] ${chunk.toString("utf-8").trim()}`);
151
+ if (totalBytes > MAX_BASH_OUTPUT_BYTES) killForOutputCap();
152
+ });
153
+ child.stderr?.on("data", (chunk) => {
154
+ stderrChunks.push(chunk);
155
+ totalBytes += chunk.length;
156
+ context.onProgress(`[stderr] ${chunk.toString("utf-8").trim()}`);
157
+ if (totalBytes > MAX_BASH_OUTPUT_BYTES) killForOutputCap();
158
+ });
159
+ const timer = setTimeout(() => {
160
+ killed = true;
161
+ child.kill("SIGTERM");
162
+ setTimeout(() => {
163
+ if (!child.killed) {
164
+ child.kill("SIGKILL");
165
+ }
166
+ }, 5e3);
167
+ }, timeoutMs);
168
+ const onAbort = () => {
169
+ killed = true;
170
+ child.kill("SIGTERM");
171
+ setTimeout(() => {
172
+ if (!child.killed) {
173
+ child.kill("SIGKILL");
174
+ }
175
+ }, 5e3);
176
+ };
177
+ context.abortSignal.addEventListener("abort", onAbort, { once: true });
178
+ child.on("close", (code, signal) => {
179
+ clearTimeout(timer);
180
+ context.abortSignal.removeEventListener("abort", onAbort);
181
+ this.activeProcess = null;
182
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
183
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
184
+ let output = "";
185
+ if (stdout.length > 0) {
186
+ output += stdout;
187
+ }
188
+ if (stderr.length > 0) {
189
+ if (output.length > 0) output += "\n";
190
+ output += stderr;
191
+ }
192
+ if (output.length > DEFAULT_MAX_OUTPUT_LENGTH) {
193
+ const halfLen = Math.floor((DEFAULT_MAX_OUTPUT_LENGTH - 50) / 2);
194
+ output = output.slice(0, halfLen) + "\n\n... [output truncated] ...\n\n" + output.slice(output.length - halfLen);
195
+ }
196
+ if (outputCapped) {
197
+ resolvePromise({
198
+ success: false,
199
+ output,
200
+ error: `Command output exceeded ${MAX_BASH_OUTPUT_BYTES / 1024 / 1024}MiB limit and was killed.`,
201
+ metadata: { code, signal, outputCapped: true }
202
+ });
203
+ return;
204
+ }
205
+ if (killed && context.abortSignal.aborted) {
206
+ resolvePromise({
207
+ success: false,
208
+ output,
209
+ error: "Command was aborted.",
210
+ metadata: { code, signal }
211
+ });
212
+ return;
213
+ }
214
+ if (killed) {
215
+ resolvePromise({
216
+ success: false,
217
+ output,
218
+ error: `Command timed out after ${timeoutMs}ms.`,
219
+ metadata: { code, signal, timedOut: true }
220
+ });
221
+ return;
222
+ }
223
+ resolvePromise({
224
+ success: code === 0,
225
+ output,
226
+ error: code !== 0 ? `Command exited with code ${code}.` : void 0,
227
+ metadata: { code, signal }
228
+ });
229
+ });
230
+ child.on("error", (err) => {
231
+ clearTimeout(timer);
232
+ context.abortSignal.removeEventListener("abort", onAbort);
233
+ this.activeProcess = null;
234
+ resolvePromise({
235
+ success: false,
236
+ output: "",
237
+ error: `Failed to spawn command: ${err.message}`
238
+ });
239
+ });
240
+ });
241
+ }
242
+ async getStateSnapshot(args, context) {
243
+ const { command, cwd } = args ?? {};
244
+ const workingDir = cwd ? resolve(context.cwd, cwd) : context.cwd;
245
+ try {
246
+ const dirStats = await stat(workingDir);
247
+ const entries = dirStats.isDirectory() ? await readdir(workingDir).then((e) => e.length) : 0;
248
+ return {
249
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
250
+ state: {
251
+ cwd: workingDir,
252
+ cwdExists: true,
253
+ entryCount: entries,
254
+ command: command?.slice(0, 200)
255
+ },
256
+ description: `Working directory state for bash: ${workingDir}`
257
+ };
258
+ } catch {
259
+ return {
260
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
261
+ state: {
262
+ cwd: workingDir,
263
+ cwdExists: false,
264
+ command: command?.slice(0, 200)
265
+ },
266
+ description: `Working directory state for bash: ${workingDir} (does not exist)`
267
+ };
268
+ }
269
+ }
270
+ };
271
+ var DEFAULT_LINE_LIMIT = 2e3;
272
+ var MAX_LINE_LENGTH = 2e3;
273
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
274
+ ".png",
275
+ ".jpg",
276
+ ".jpeg",
277
+ ".gif",
278
+ ".bmp",
279
+ ".ico",
280
+ ".webp",
281
+ ".avif",
282
+ ".mp3",
283
+ ".mp4",
284
+ ".avi",
285
+ ".mov",
286
+ ".mkv",
287
+ ".flv",
288
+ ".wmv",
289
+ ".wav",
290
+ ".flac",
291
+ ".zip",
292
+ ".gz",
293
+ ".tar",
294
+ ".bz2",
295
+ ".xz",
296
+ ".7z",
297
+ ".rar",
298
+ ".exe",
299
+ ".dll",
300
+ ".so",
301
+ ".dylib",
302
+ ".bin",
303
+ ".woff",
304
+ ".woff2",
305
+ ".ttf",
306
+ ".otf",
307
+ ".eot",
308
+ ".pdf",
309
+ ".doc",
310
+ ".docx",
311
+ ".xls",
312
+ ".xlsx",
313
+ ".ppt",
314
+ ".pptx",
315
+ ".sqlite",
316
+ ".db",
317
+ ".sqlite3",
318
+ ".class",
319
+ ".pyc",
320
+ ".pyo",
321
+ ".o",
322
+ ".obj",
323
+ ".wasm"
324
+ ]);
325
+ var FileReadTool = class {
326
+ name = "file_read";
327
+ description = "Read the contents of a file from the filesystem. Lines are returned with line number prefixes. Supports offset and limit for reading portions of large files. Binary files are detected and rejected.";
328
+ weight = "lightweight";
329
+ parameters = {
330
+ type: "object",
331
+ properties: {
332
+ path: {
333
+ type: "string",
334
+ description: "Absolute or relative path to the file to read.",
335
+ minLength: 1
336
+ },
337
+ offset: {
338
+ type: "number",
339
+ description: "Line number to start reading from (1-based). Defaults to 1.",
340
+ minimum: 1
341
+ },
342
+ limit: {
343
+ type: "number",
344
+ description: "Maximum number of lines to read. Defaults to 2000.",
345
+ minimum: 1
346
+ }
347
+ },
348
+ required: ["path"],
349
+ additionalProperties: false
350
+ };
351
+ validate(args) {
352
+ if (typeof args !== "object" || args === null) {
353
+ return { valid: false, errors: ["Arguments must be an object."] };
354
+ }
355
+ const { path, offset, limit } = args;
356
+ const errors = [];
357
+ if (typeof path !== "string" || path.trim().length === 0) {
358
+ errors.push("path must be a non-empty string.");
359
+ }
360
+ if (offset !== void 0) {
361
+ if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1) {
362
+ errors.push("offset must be a positive integer (1-based).");
363
+ }
364
+ }
365
+ if (limit !== void 0) {
366
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1) {
367
+ errors.push("limit must be a positive integer.");
368
+ }
369
+ }
370
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
371
+ }
372
+ async execute(args, context) {
373
+ const validation = this.validate(args);
374
+ if (!validation.valid) {
375
+ return {
376
+ success: false,
377
+ output: "",
378
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
379
+ };
380
+ }
381
+ const { path: filePath, offset, limit } = args;
382
+ const absolutePath = resolve2(context.cwd, filePath);
383
+ const pathValidation = context.securityPolicy.validatePath(absolutePath, "read");
384
+ if (!pathValidation.allowed) {
385
+ throw new SecurityError(
386
+ `Path blocked: ${pathValidation.reason ?? absolutePath}`,
387
+ { path: absolutePath }
388
+ );
389
+ }
390
+ const resolvedPath = pathValidation.canonicalPath ?? absolutePath;
391
+ const ext = extname(resolvedPath).toLowerCase();
392
+ if (BINARY_EXTENSIONS.has(ext)) {
393
+ return {
394
+ success: false,
395
+ output: "",
396
+ error: `Cannot read binary file (${ext}). Use an appropriate tool for this file type.`,
397
+ metadata: { path: resolvedPath, extension: ext, binary: true }
398
+ };
399
+ }
400
+ let fileStats;
401
+ try {
402
+ fileStats = await stat2(resolvedPath);
403
+ } catch (err) {
404
+ const code = err.code;
405
+ if (code === "ENOENT") {
406
+ return {
407
+ success: false,
408
+ output: "",
409
+ error: `File not found: ${resolvedPath}`
410
+ };
411
+ }
412
+ return {
413
+ success: false,
414
+ output: "",
415
+ error: `Cannot access file: ${err.message}`
416
+ };
417
+ }
418
+ if (!fileStats.isFile()) {
419
+ return {
420
+ success: false,
421
+ output: "",
422
+ error: `Path is not a file: ${resolvedPath}. Use ls or glob for directories.`
423
+ };
424
+ }
425
+ let content;
426
+ try {
427
+ const buffer = await readFile(resolvedPath);
428
+ const sampleSize = Math.min(buffer.length, 8192);
429
+ for (let i = 0; i < sampleSize; i++) {
430
+ if (buffer[i] === 0) {
431
+ return {
432
+ success: false,
433
+ output: "",
434
+ error: "File appears to be binary (contains null bytes).",
435
+ metadata: { path: resolvedPath, size: fileStats.size, binary: true }
436
+ };
437
+ }
438
+ }
439
+ content = buffer.toString("utf-8");
440
+ } catch (err) {
441
+ return {
442
+ success: false,
443
+ output: "",
444
+ error: `Failed to read file: ${err.message}`
445
+ };
446
+ }
447
+ if (content.length === 0) {
448
+ return {
449
+ success: true,
450
+ output: "(empty file)",
451
+ metadata: { path: resolvedPath, lines: 0, size: 0 }
452
+ };
453
+ }
454
+ const allLines = content.split("\n");
455
+ const startLine = (offset ?? 1) - 1;
456
+ const lineLimit = limit ?? DEFAULT_LINE_LIMIT;
457
+ const endLine = Math.min(startLine + lineLimit, allLines.length);
458
+ const selectedLines = allLines.slice(startLine, endLine);
459
+ const maxLineNumWidth = String(endLine).length;
460
+ const formattedLines = selectedLines.map((line, idx) => {
461
+ const lineNum = String(startLine + idx + 1).padStart(maxLineNumWidth, " ");
462
+ const truncatedLine = line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + "...(truncated)" : line;
463
+ return `${lineNum} ${truncatedLine}`;
464
+ });
465
+ const output = formattedLines.join("\n");
466
+ const metadata = {
467
+ path: resolvedPath,
468
+ totalLines: allLines.length,
469
+ startLine: startLine + 1,
470
+ endLine,
471
+ size: fileStats.size
472
+ };
473
+ if (endLine < allLines.length) {
474
+ metadata.truncated = true;
475
+ metadata.remainingLines = allLines.length - endLine;
476
+ }
477
+ return {
478
+ success: true,
479
+ output,
480
+ metadata
481
+ };
482
+ }
483
+ };
484
+ async function captureFileState(absolutePath, description) {
485
+ try {
486
+ const fileStats = await stat3(absolutePath);
487
+ if (!fileStats.isFile()) {
488
+ return {
489
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
490
+ state: {
491
+ path: absolutePath,
492
+ exists: true,
493
+ isFile: false,
494
+ isDirectory: fileStats.isDirectory()
495
+ },
496
+ description: description ?? `State of ${absolutePath}`
497
+ };
498
+ }
499
+ const MAX_HASH_SIZE = 1048576;
500
+ let contentHash;
501
+ let lineCount;
502
+ if (fileStats.size <= MAX_HASH_SIZE) {
503
+ const content = await readFile2(absolutePath, "utf-8");
504
+ contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
505
+ lineCount = content.split("\n").length;
506
+ } else {
507
+ const buffer = await readFile2(absolutePath);
508
+ contentHash = createHash("sha256").update(buffer.subarray(0, MAX_HASH_SIZE)).digest("hex").slice(0, 16);
509
+ }
510
+ return {
511
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
512
+ state: {
513
+ path: absolutePath,
514
+ exists: true,
515
+ isFile: true,
516
+ size: fileStats.size,
517
+ mtime: fileStats.mtime.toISOString(),
518
+ contentHash,
519
+ ...lineCount !== void 0 ? { lineCount } : {}
520
+ },
521
+ description: description ?? `State of ${absolutePath}`
522
+ };
523
+ } catch (err) {
524
+ const code = err.code;
525
+ if (code === "ENOENT") {
526
+ return {
527
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
528
+ state: {
529
+ path: absolutePath,
530
+ exists: false
531
+ },
532
+ description: description ?? `State of ${absolutePath} (does not exist)`
533
+ };
534
+ }
535
+ return {
536
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
537
+ state: {
538
+ path: absolutePath,
539
+ exists: void 0,
540
+ error: `Cannot access: ${err.message}`
541
+ },
542
+ description: description ?? `State of ${absolutePath} (error)`
543
+ };
544
+ }
545
+ }
546
+ var FileWriteTool = class {
547
+ name = "file_write";
548
+ description = "Write content to a file. Creates the file if it does not exist, or overwrites it if it does. Parent directories are created automatically. Path is validated against the security policy.";
549
+ weight = "lightweight";
550
+ parameters = {
551
+ type: "object",
552
+ properties: {
553
+ path: {
554
+ type: "string",
555
+ description: "Absolute or relative path to the file to write.",
556
+ minLength: 1
557
+ },
558
+ content: {
559
+ type: "string",
560
+ description: "The content to write to the file."
561
+ }
562
+ },
563
+ required: ["path", "content"],
564
+ additionalProperties: false
565
+ };
566
+ validate(args) {
567
+ if (typeof args !== "object" || args === null) {
568
+ return { valid: false, errors: ["Arguments must be an object."] };
569
+ }
570
+ const { path, content } = args;
571
+ const errors = [];
572
+ if (typeof path !== "string" || path.trim().length === 0) {
573
+ errors.push("path must be a non-empty string.");
574
+ }
575
+ if (typeof content !== "string") {
576
+ errors.push("content must be a string.");
577
+ }
578
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
579
+ }
580
+ async execute(args, context) {
581
+ const validation = this.validate(args);
582
+ if (!validation.valid) {
583
+ return {
584
+ success: false,
585
+ output: "",
586
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
587
+ };
588
+ }
589
+ const { path: filePath, content } = args;
590
+ const absolutePath = resolve3(context.cwd, filePath);
591
+ const pathValidation = context.securityPolicy.validatePath(absolutePath, "write");
592
+ if (!pathValidation.allowed) {
593
+ throw new SecurityError(
594
+ `Path blocked for writing: ${pathValidation.reason ?? absolutePath}`,
595
+ { path: absolutePath }
596
+ );
597
+ }
598
+ const resolvedPath = pathValidation.canonicalPath ?? absolutePath;
599
+ const parentDir = dirname(resolvedPath);
600
+ try {
601
+ await mkdir(parentDir, { recursive: true });
602
+ } catch (err) {
603
+ return {
604
+ success: false,
605
+ output: "",
606
+ error: `Failed to create parent directory: ${err.message}`
607
+ };
608
+ }
609
+ try {
610
+ await writeFile(resolvedPath, content, "utf-8");
611
+ } catch (err) {
612
+ return {
613
+ success: false,
614
+ output: "",
615
+ error: `Failed to write file: ${err.message}`
616
+ };
617
+ }
618
+ const lineCount = content.split("\n").length;
619
+ const byteCount = Buffer.byteLength(content, "utf-8");
620
+ return {
621
+ success: true,
622
+ output: `File written successfully: ${resolvedPath} (${lineCount} lines, ${byteCount} bytes)`,
623
+ metadata: {
624
+ path: resolvedPath,
625
+ lines: lineCount,
626
+ bytes: byteCount
627
+ }
628
+ };
629
+ }
630
+ async getStateSnapshot(args, context) {
631
+ const { path: filePath } = args ?? {};
632
+ if (!filePath) {
633
+ return {
634
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
635
+ state: { error: "No path argument provided" }
636
+ };
637
+ }
638
+ const absolutePath = resolve3(context.cwd, filePath);
639
+ return captureFileState(absolutePath);
640
+ }
641
+ };
642
+ var FileEditTool = class {
643
+ name = "file_edit";
644
+ description = "Perform exact string replacements in a file. The old_string must be unique in the file unless replace_all is set to true. This ensures edits are unambiguous. The new_string must differ from old_string.";
645
+ weight = "lightweight";
646
+ parameters = {
647
+ type: "object",
648
+ properties: {
649
+ path: {
650
+ type: "string",
651
+ description: "Absolute or relative path to the file to edit.",
652
+ minLength: 1
653
+ },
654
+ old_string: {
655
+ type: "string",
656
+ description: "The exact string to find and replace."
657
+ },
658
+ new_string: {
659
+ type: "string",
660
+ description: "The replacement string."
661
+ },
662
+ replace_all: {
663
+ type: "boolean",
664
+ description: "If true, replace all occurrences. If false (default), old_string must be unique.",
665
+ default: false
666
+ }
667
+ },
668
+ required: ["path", "old_string", "new_string"],
669
+ additionalProperties: false
670
+ };
671
+ validate(args) {
672
+ if (typeof args !== "object" || args === null) {
673
+ return { valid: false, errors: ["Arguments must be an object."] };
674
+ }
675
+ const { path, old_string, new_string, replace_all } = args;
676
+ const errors = [];
677
+ if (typeof path !== "string" || path.trim().length === 0) {
678
+ errors.push("path must be a non-empty string.");
679
+ }
680
+ if (typeof old_string !== "string") {
681
+ errors.push("old_string must be a string.");
682
+ }
683
+ if (typeof new_string !== "string") {
684
+ errors.push("new_string must be a string.");
685
+ }
686
+ if (typeof old_string === "string" && typeof new_string === "string" && old_string === new_string) {
687
+ errors.push("new_string must be different from old_string.");
688
+ }
689
+ if (replace_all !== void 0 && typeof replace_all !== "boolean") {
690
+ errors.push("replace_all must be a boolean.");
691
+ }
692
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
693
+ }
694
+ async execute(args, context) {
695
+ const validation = this.validate(args);
696
+ if (!validation.valid) {
697
+ return {
698
+ success: false,
699
+ output: "",
700
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
701
+ };
702
+ }
703
+ const {
704
+ path: filePath,
705
+ old_string: oldString,
706
+ new_string: newString,
707
+ replace_all: replaceAll = false
708
+ } = args;
709
+ const absolutePath = resolve4(context.cwd, filePath);
710
+ const pathValidation = context.securityPolicy.validatePath(absolutePath, "write");
711
+ if (!pathValidation.allowed) {
712
+ throw new SecurityError(
713
+ `Path blocked for writing: ${pathValidation.reason ?? absolutePath}`,
714
+ { path: absolutePath }
715
+ );
716
+ }
717
+ const resolvedPath = pathValidation.canonicalPath ?? absolutePath;
718
+ let content;
719
+ try {
720
+ content = await readFile3(resolvedPath, "utf-8");
721
+ } catch (err) {
722
+ const code = err.code;
723
+ if (code === "ENOENT") {
724
+ return {
725
+ success: false,
726
+ output: "",
727
+ error: `File not found: ${resolvedPath}`
728
+ };
729
+ }
730
+ return {
731
+ success: false,
732
+ output: "",
733
+ error: `Failed to read file: ${err.message}`
734
+ };
735
+ }
736
+ const occurrences = countOccurrences(content, oldString);
737
+ if (occurrences === 0) {
738
+ return {
739
+ success: false,
740
+ output: "",
741
+ error: "old_string was not found in the file. Ensure you have the exact string including whitespace and indentation.",
742
+ metadata: { path: resolvedPath }
743
+ };
744
+ }
745
+ if (!replaceAll && occurrences > 1) {
746
+ return {
747
+ success: false,
748
+ output: "",
749
+ error: `old_string appears ${occurrences} times in the file. Provide more surrounding context to make it unique, or set replace_all to true.`,
750
+ metadata: { path: resolvedPath, occurrences }
751
+ };
752
+ }
753
+ let newContent;
754
+ let replacedCount;
755
+ if (replaceAll) {
756
+ newContent = content.split(oldString).join(newString);
757
+ replacedCount = occurrences;
758
+ } else {
759
+ const idx = content.indexOf(oldString);
760
+ newContent = content.slice(0, idx) + newString + content.slice(idx + oldString.length);
761
+ replacedCount = 1;
762
+ }
763
+ try {
764
+ await writeFile2(resolvedPath, newContent, "utf-8");
765
+ } catch (err) {
766
+ return {
767
+ success: false,
768
+ output: "",
769
+ error: `Failed to write file: ${err.message}`
770
+ };
771
+ }
772
+ return {
773
+ success: true,
774
+ output: `Replaced ${replacedCount} occurrence${replacedCount > 1 ? "s" : ""} in ${resolvedPath}.`,
775
+ metadata: {
776
+ path: resolvedPath,
777
+ replacements: replacedCount
778
+ }
779
+ };
780
+ }
781
+ async getStateSnapshot(args, context) {
782
+ const { path: filePath } = args ?? {};
783
+ if (!filePath) {
784
+ return {
785
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
786
+ state: { error: "No path argument provided" }
787
+ };
788
+ }
789
+ const absolutePath = resolve4(context.cwd, filePath);
790
+ return captureFileState(absolutePath);
791
+ }
792
+ };
793
+ function countOccurrences(haystack, needle) {
794
+ if (needle.length === 0) return 0;
795
+ let count = 0;
796
+ let pos = 0;
797
+ while (true) {
798
+ pos = haystack.indexOf(needle, pos);
799
+ if (pos === -1) break;
800
+ count++;
801
+ pos += needle.length;
802
+ }
803
+ return count;
804
+ }
805
+ var MAX_RESULTS = 500;
806
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
807
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
808
+ ".png",
809
+ ".jpg",
810
+ ".jpeg",
811
+ ".gif",
812
+ ".bmp",
813
+ ".ico",
814
+ ".webp",
815
+ ".avif",
816
+ ".mp3",
817
+ ".mp4",
818
+ ".avi",
819
+ ".mov",
820
+ ".mkv",
821
+ ".wav",
822
+ ".flac",
823
+ ".zip",
824
+ ".gz",
825
+ ".tar",
826
+ ".bz2",
827
+ ".xz",
828
+ ".7z",
829
+ ".rar",
830
+ ".exe",
831
+ ".dll",
832
+ ".so",
833
+ ".dylib",
834
+ ".bin",
835
+ ".woff",
836
+ ".woff2",
837
+ ".ttf",
838
+ ".otf",
839
+ ".eot",
840
+ ".pdf",
841
+ ".doc",
842
+ ".docx",
843
+ ".xls",
844
+ ".xlsx",
845
+ ".sqlite",
846
+ ".db",
847
+ ".sqlite3",
848
+ ".class",
849
+ ".pyc",
850
+ ".o",
851
+ ".obj",
852
+ ".wasm"
853
+ ]);
854
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
855
+ "node_modules",
856
+ ".git",
857
+ ".hg",
858
+ ".svn",
859
+ "dist",
860
+ "build",
861
+ ".next",
862
+ "__pycache__",
863
+ ".tox",
864
+ ".venv",
865
+ "venv",
866
+ ".cache",
867
+ "coverage"
868
+ ]);
869
+ var GrepTool = class {
870
+ name = "grep";
871
+ description = "Search file contents using regular expressions. Supports glob pattern filtering, multiple output modes (content, files_with_matches, count), and context lines around matches.";
872
+ weight = "lightweight";
873
+ parameters = {
874
+ type: "object",
875
+ properties: {
876
+ pattern: {
877
+ type: "string",
878
+ description: "Regular expression pattern to search for.",
879
+ minLength: 1
880
+ },
881
+ path: {
882
+ type: "string",
883
+ description: "File or directory to search in. Defaults to the session working directory."
884
+ },
885
+ glob: {
886
+ type: "string",
887
+ description: 'Glob pattern to filter files (e.g. "*.ts", "*.{js,jsx}"). Only files matching this pattern are searched.'
888
+ },
889
+ output_mode: {
890
+ type: "string",
891
+ enum: ["content", "files_with_matches", "count"],
892
+ description: 'Output mode: "content" shows matching lines, "files_with_matches" shows file paths (default), "count" shows match counts.',
893
+ default: "files_with_matches"
894
+ },
895
+ context_lines: {
896
+ type: "number",
897
+ description: 'Number of context lines to show before and after each match. Only used with output_mode "content".',
898
+ minimum: 0,
899
+ maximum: 20
900
+ }
901
+ },
902
+ required: ["pattern"],
903
+ additionalProperties: false
904
+ };
905
+ validate(args) {
906
+ if (typeof args !== "object" || args === null) {
907
+ return { valid: false, errors: ["Arguments must be an object."] };
908
+ }
909
+ const { pattern, path, glob, output_mode, context_lines } = args;
910
+ const errors = [];
911
+ if (typeof pattern !== "string" || pattern.trim().length === 0) {
912
+ errors.push("pattern must be a non-empty string.");
913
+ }
914
+ if (typeof pattern === "string") {
915
+ try {
916
+ new RegExp(pattern);
917
+ } catch {
918
+ errors.push(`Invalid regular expression: ${pattern}`);
919
+ }
920
+ }
921
+ if (path !== void 0 && typeof path !== "string") {
922
+ errors.push("path must be a string.");
923
+ }
924
+ if (glob !== void 0 && typeof glob !== "string") {
925
+ errors.push("glob must be a string.");
926
+ }
927
+ if (output_mode !== void 0 && !["content", "files_with_matches", "count"].includes(output_mode)) {
928
+ errors.push('output_mode must be "content", "files_with_matches", or "count".');
929
+ }
930
+ if (context_lines !== void 0) {
931
+ if (typeof context_lines !== "number" || !Number.isInteger(context_lines) || context_lines < 0) {
932
+ errors.push("context_lines must be a non-negative integer.");
933
+ }
934
+ if (typeof context_lines === "number" && context_lines > 20) {
935
+ errors.push("context_lines cannot exceed 20.");
936
+ }
937
+ }
938
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
939
+ }
940
+ async execute(args, context) {
941
+ const validation = this.validate(args);
942
+ if (!validation.valid) {
943
+ return {
944
+ success: false,
945
+ output: "",
946
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
947
+ };
948
+ }
949
+ const {
950
+ pattern,
951
+ path: searchPath,
952
+ glob: globPattern,
953
+ output_mode: outputMode = "files_with_matches",
954
+ context_lines: contextLines = 0
955
+ } = args;
956
+ const regex = new RegExp(pattern, "g");
957
+ const basePath = searchPath ? resolve5(context.cwd, searchPath) : context.cwd;
958
+ const pathValidation = context.securityPolicy.validatePath(basePath, "read");
959
+ if (!pathValidation.allowed) {
960
+ throw new SecurityError(
961
+ `Search path blocked: ${pathValidation.reason ?? basePath}`,
962
+ { path: basePath }
963
+ );
964
+ }
965
+ const resolvedBase = pathValidation.canonicalPath ?? basePath;
966
+ let fileStat;
967
+ try {
968
+ fileStat = await stat4(resolvedBase);
969
+ } catch {
970
+ return {
971
+ success: false,
972
+ output: "",
973
+ error: `Path not found: ${resolvedBase}`
974
+ };
975
+ }
976
+ const files = [];
977
+ if (fileStat.isFile()) {
978
+ files.push(resolvedBase);
979
+ } else if (fileStat.isDirectory()) {
980
+ await collectFiles(resolvedBase, files, globPattern, context);
981
+ } else {
982
+ return {
983
+ success: false,
984
+ output: "",
985
+ error: `Path is not a file or directory: ${resolvedBase}`
986
+ };
987
+ }
988
+ const mode = outputMode;
989
+ const results = [];
990
+ let totalMatches = 0;
991
+ let filesWithMatches = 0;
992
+ let resultLimitReached = false;
993
+ for (const file of files) {
994
+ if (context.abortSignal.aborted) break;
995
+ if (resultLimitReached) break;
996
+ const matches = await searchFile(file, regex, mode, contextLines);
997
+ if (matches === null) continue;
998
+ if (matches.matchCount > 0) {
999
+ filesWithMatches++;
1000
+ totalMatches += matches.matchCount;
1001
+ const relPath = relative(context.cwd, file);
1002
+ switch (mode) {
1003
+ case "files_with_matches":
1004
+ results.push(relPath);
1005
+ break;
1006
+ case "count":
1007
+ results.push(`${relPath}:${matches.matchCount}`);
1008
+ break;
1009
+ case "content":
1010
+ for (const line of matches.lines) {
1011
+ results.push(`${relPath}:${line}`);
1012
+ if (results.length >= MAX_RESULTS) {
1013
+ resultLimitReached = true;
1014
+ break;
1015
+ }
1016
+ }
1017
+ break;
1018
+ }
1019
+ if (results.length >= MAX_RESULTS) {
1020
+ resultLimitReached = true;
1021
+ }
1022
+ }
1023
+ }
1024
+ if (results.length === 0) {
1025
+ return {
1026
+ success: true,
1027
+ output: "No matches found.",
1028
+ metadata: { pattern, filesSearched: files.length, matches: 0 }
1029
+ };
1030
+ }
1031
+ let output = results.join("\n");
1032
+ if (resultLimitReached) {
1033
+ output += `
1034
+
1035
+ (results truncated at ${MAX_RESULTS} entries)`;
1036
+ }
1037
+ return {
1038
+ success: true,
1039
+ output,
1040
+ metadata: {
1041
+ pattern,
1042
+ filesSearched: files.length,
1043
+ filesWithMatches,
1044
+ totalMatches,
1045
+ truncated: resultLimitReached
1046
+ }
1047
+ };
1048
+ }
1049
+ };
1050
+ async function collectFiles(dir, files, globPattern, context) {
1051
+ let entries;
1052
+ try {
1053
+ entries = await readdir2(dir, { withFileTypes: true });
1054
+ } catch {
1055
+ return;
1056
+ }
1057
+ for (const entry of entries) {
1058
+ if (context.abortSignal.aborted) return;
1059
+ const fullPath = join(dir, entry.name);
1060
+ if (entry.isDirectory()) {
1061
+ if (SKIP_DIRS.has(entry.name)) continue;
1062
+ const dirValidation = context.securityPolicy.validatePath(fullPath, "read");
1063
+ if (!dirValidation.allowed) continue;
1064
+ await collectFiles(fullPath, files, globPattern, context);
1065
+ } else if (entry.isFile()) {
1066
+ const ext = extname2(entry.name).toLowerCase();
1067
+ if (SKIP_EXTENSIONS.has(ext)) continue;
1068
+ if (globPattern && !matchGlob(entry.name, globPattern)) continue;
1069
+ files.push(fullPath);
1070
+ }
1071
+ }
1072
+ }
1073
+ function matchGlob(filename, pattern) {
1074
+ const expanded = expandBraces(pattern);
1075
+ return expanded.some((p) => matchSimpleGlob(filename, p));
1076
+ }
1077
+ function matchSimpleGlob(str, pattern) {
1078
+ let regexStr = "^";
1079
+ for (let i = 0; i < pattern.length; i++) {
1080
+ const c = pattern[i];
1081
+ if (c === "*") {
1082
+ if (pattern[i + 1] === "*") {
1083
+ regexStr += ".*";
1084
+ i++;
1085
+ if (pattern[i + 1] === "/") i++;
1086
+ } else {
1087
+ regexStr += "[^/]*";
1088
+ }
1089
+ } else if (c === "?") {
1090
+ regexStr += "[^/]";
1091
+ } else if (c === ".") {
1092
+ regexStr += "\\.";
1093
+ } else {
1094
+ regexStr += c;
1095
+ }
1096
+ }
1097
+ regexStr += "$";
1098
+ try {
1099
+ return new RegExp(regexStr, "i").test(str);
1100
+ } catch {
1101
+ return false;
1102
+ }
1103
+ }
1104
+ function expandBraces(pattern) {
1105
+ const braceStart = pattern.indexOf("{");
1106
+ if (braceStart === -1) return [pattern];
1107
+ const braceEnd = pattern.indexOf("}", braceStart);
1108
+ if (braceEnd === -1) return [pattern];
1109
+ const prefix = pattern.slice(0, braceStart);
1110
+ const suffix = pattern.slice(braceEnd + 1);
1111
+ const alternatives = pattern.slice(braceStart + 1, braceEnd).split(",");
1112
+ const results = [];
1113
+ for (const alt of alternatives) {
1114
+ results.push(...expandBraces(prefix + alt.trim() + suffix));
1115
+ }
1116
+ return results;
1117
+ }
1118
+ async function searchFile(filePath, regex, mode, contextLines) {
1119
+ let fileSize;
1120
+ try {
1121
+ const stats = await stat4(filePath);
1122
+ fileSize = stats.size;
1123
+ } catch {
1124
+ return null;
1125
+ }
1126
+ if (fileSize > MAX_FILE_SIZE) return null;
1127
+ let content;
1128
+ try {
1129
+ const buffer = await readFile4(filePath);
1130
+ const sampleSize = Math.min(buffer.length, 512);
1131
+ for (let i = 0; i < sampleSize; i++) {
1132
+ if (buffer[i] === 0) return null;
1133
+ }
1134
+ content = buffer.toString("utf-8");
1135
+ } catch {
1136
+ return null;
1137
+ }
1138
+ const lines = content.split("\n");
1139
+ const matchingLineIndices = [];
1140
+ for (let i = 0; i < lines.length; i++) {
1141
+ regex.lastIndex = 0;
1142
+ if (regex.test(lines[i])) {
1143
+ matchingLineIndices.push(i);
1144
+ }
1145
+ }
1146
+ if (matchingLineIndices.length === 0) {
1147
+ return { matchCount: 0, lines: [] };
1148
+ }
1149
+ if (mode === "files_with_matches" || mode === "count") {
1150
+ return { matchCount: matchingLineIndices.length, lines: [] };
1151
+ }
1152
+ const outputLines = [];
1153
+ const includedLines = /* @__PURE__ */ new Set();
1154
+ for (const matchIdx of matchingLineIndices) {
1155
+ const start = Math.max(0, matchIdx - contextLines);
1156
+ const end = Math.min(lines.length - 1, matchIdx + contextLines);
1157
+ if (outputLines.length > 0 && !includedLines.has(start - 1)) {
1158
+ outputLines.push("--");
1159
+ }
1160
+ for (let i = start; i <= end; i++) {
1161
+ if (includedLines.has(i)) continue;
1162
+ includedLines.add(i);
1163
+ const lineNum = i + 1;
1164
+ const separator = i === matchIdx ? ":" : "-";
1165
+ outputLines.push(`${lineNum}${separator}${lines[i]}`);
1166
+ }
1167
+ }
1168
+ return { matchCount: matchingLineIndices.length, lines: outputLines };
1169
+ }
1170
+ var MAX_RESULTS2 = 1e3;
1171
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
1172
+ "node_modules",
1173
+ ".git",
1174
+ ".hg",
1175
+ ".svn",
1176
+ "dist",
1177
+ "build",
1178
+ ".next",
1179
+ "__pycache__",
1180
+ ".tox",
1181
+ ".venv",
1182
+ "venv",
1183
+ ".cache",
1184
+ "coverage"
1185
+ ]);
1186
+ var GlobTool = class {
1187
+ name = "glob";
1188
+ description = "Find files matching a glob pattern. Supports standard glob syntax including *, **, ?, and {a,b} brace expansion. Results are sorted by modification time (most recent first).";
1189
+ weight = "lightweight";
1190
+ parameters = {
1191
+ type: "object",
1192
+ properties: {
1193
+ pattern: {
1194
+ type: "string",
1195
+ description: 'Glob pattern to match files (e.g. "**/*.ts", "src/**/*.{js,jsx}").',
1196
+ minLength: 1
1197
+ },
1198
+ path: {
1199
+ type: "string",
1200
+ description: "Directory to search in. Defaults to the session working directory."
1201
+ }
1202
+ },
1203
+ required: ["pattern"],
1204
+ additionalProperties: false
1205
+ };
1206
+ validate(args) {
1207
+ if (typeof args !== "object" || args === null) {
1208
+ return { valid: false, errors: ["Arguments must be an object."] };
1209
+ }
1210
+ const { pattern, path } = args;
1211
+ const errors = [];
1212
+ if (typeof pattern !== "string" || pattern.trim().length === 0) {
1213
+ errors.push("pattern must be a non-empty string.");
1214
+ }
1215
+ if (path !== void 0 && typeof path !== "string") {
1216
+ errors.push("path must be a string.");
1217
+ }
1218
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
1219
+ }
1220
+ async execute(args, context) {
1221
+ const validation = this.validate(args);
1222
+ if (!validation.valid) {
1223
+ return {
1224
+ success: false,
1225
+ output: "",
1226
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
1227
+ };
1228
+ }
1229
+ const { pattern, path: searchPath } = args;
1230
+ const basePath = searchPath ? resolve6(context.cwd, searchPath) : context.cwd;
1231
+ const pathValidation = context.securityPolicy.validatePath(basePath, "read");
1232
+ if (!pathValidation.allowed) {
1233
+ throw new SecurityError(
1234
+ `Search path blocked: ${pathValidation.reason ?? basePath}`,
1235
+ { path: basePath }
1236
+ );
1237
+ }
1238
+ const resolvedBase = pathValidation.canonicalPath ?? basePath;
1239
+ const patternParts = pattern.split("/");
1240
+ const matcher = compileGlobPattern(pattern);
1241
+ const matches = [];
1242
+ await collectMatches(
1243
+ resolvedBase,
1244
+ "",
1245
+ patternParts,
1246
+ matcher,
1247
+ matches,
1248
+ context
1249
+ );
1250
+ if (matches.length === 0) {
1251
+ return {
1252
+ success: true,
1253
+ output: "No files matched the pattern.",
1254
+ metadata: { pattern, basePath: resolvedBase, matches: 0 }
1255
+ };
1256
+ }
1257
+ matches.sort((a, b) => b.mtime - a.mtime);
1258
+ const truncated = matches.length > MAX_RESULTS2;
1259
+ const displayMatches = truncated ? matches.slice(0, MAX_RESULTS2) : matches;
1260
+ let output = displayMatches.map((m) => m.path).join("\n");
1261
+ if (truncated) {
1262
+ output += `
1263
+
1264
+ (showing ${MAX_RESULTS2} of ${matches.length} matches)`;
1265
+ }
1266
+ return {
1267
+ success: true,
1268
+ output,
1269
+ metadata: {
1270
+ pattern,
1271
+ basePath: resolvedBase,
1272
+ matches: matches.length,
1273
+ truncated
1274
+ }
1275
+ };
1276
+ }
1277
+ };
1278
+ async function collectMatches(basePath, relativePath, patternParts, matcher, matches, context) {
1279
+ if (context.abortSignal.aborted) return;
1280
+ if (matches.length >= MAX_RESULTS2 * 2) return;
1281
+ const currentDir = join2(basePath, relativePath);
1282
+ let entries;
1283
+ try {
1284
+ entries = await readdir3(currentDir, { withFileTypes: true });
1285
+ } catch {
1286
+ return;
1287
+ }
1288
+ for (const entry of entries) {
1289
+ if (context.abortSignal.aborted) return;
1290
+ const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1291
+ const entryFull = join2(basePath, entryRelative);
1292
+ if (entry.isDirectory()) {
1293
+ if (SKIP_DIRS2.has(entry.name)) continue;
1294
+ const dirValidation = context.securityPolicy.validatePath(entryFull, "read");
1295
+ if (!dirValidation.allowed) continue;
1296
+ await collectMatches(
1297
+ basePath,
1298
+ entryRelative,
1299
+ patternParts,
1300
+ matcher,
1301
+ matches,
1302
+ context
1303
+ );
1304
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
1305
+ if (matcher(entryRelative)) {
1306
+ try {
1307
+ const fileStat = await stat5(entryFull);
1308
+ matches.push({
1309
+ path: entryFull,
1310
+ mtime: fileStat.mtimeMs
1311
+ });
1312
+ } catch {
1313
+ }
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+ function compileGlobPattern(pattern) {
1319
+ const expanded = expandBraces2(pattern);
1320
+ const regexes = expanded.map(globToRegex);
1321
+ return (path) => regexes.some((re) => re.test(path));
1322
+ }
1323
+ function globToRegex(pattern) {
1324
+ let regexStr = "^";
1325
+ let i = 0;
1326
+ while (i < pattern.length) {
1327
+ const c = pattern[i];
1328
+ if (c === "*") {
1329
+ if (pattern[i + 1] === "*") {
1330
+ if (pattern[i + 2] === "/") {
1331
+ regexStr += "(?:.+/)?";
1332
+ i += 3;
1333
+ } else {
1334
+ regexStr += ".*";
1335
+ i += 2;
1336
+ }
1337
+ } else {
1338
+ regexStr += "[^/]*";
1339
+ i++;
1340
+ }
1341
+ } else if (c === "?") {
1342
+ regexStr += "[^/]";
1343
+ i++;
1344
+ } else if (c === ".") {
1345
+ regexStr += "\\.";
1346
+ i++;
1347
+ } else if (c === "(" || c === ")" || c === "+" || c === "^" || c === "$" || c === "|") {
1348
+ regexStr += "\\" + c;
1349
+ i++;
1350
+ } else if (c === "[") {
1351
+ const closeBracket = pattern.indexOf("]", i + 1);
1352
+ if (closeBracket === -1) {
1353
+ regexStr += "\\[";
1354
+ i++;
1355
+ } else {
1356
+ regexStr += pattern.slice(i, closeBracket + 1);
1357
+ i = closeBracket + 1;
1358
+ }
1359
+ } else {
1360
+ regexStr += c;
1361
+ i++;
1362
+ }
1363
+ }
1364
+ regexStr += "$";
1365
+ try {
1366
+ return new RegExp(regexStr);
1367
+ } catch {
1368
+ return /(?!)/;
1369
+ }
1370
+ }
1371
+ function expandBraces2(pattern) {
1372
+ const braceStart = pattern.indexOf("{");
1373
+ if (braceStart === -1) return [pattern];
1374
+ const braceEnd = pattern.indexOf("}", braceStart);
1375
+ if (braceEnd === -1) return [pattern];
1376
+ const prefix = pattern.slice(0, braceStart);
1377
+ const suffix = pattern.slice(braceEnd + 1);
1378
+ const alternatives = pattern.slice(braceStart + 1, braceEnd).split(",");
1379
+ const results = [];
1380
+ for (const alt of alternatives) {
1381
+ results.push(...expandBraces2(prefix + alt.trim() + suffix));
1382
+ }
1383
+ return results;
1384
+ }
1385
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
1386
+ "169.254.169.254",
1387
+ // AWS / GCP / Azure instance metadata
1388
+ "metadata.google.internal",
1389
+ // GCP metadata alternative
1390
+ "metadata.internal"
1391
+ // Generic cloud metadata
1392
+ ]);
1393
+ function isPrivateIpV4(ip) {
1394
+ const parts = ip.split(".").map(Number);
1395
+ if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) {
1396
+ return true;
1397
+ }
1398
+ const [a, b] = parts;
1399
+ if (a === 127) return true;
1400
+ if (a === 10) return true;
1401
+ if (a === 172 && b >= 16 && b <= 31) return true;
1402
+ if (a === 192 && b === 168) return true;
1403
+ if (a === 169 && b === 254) return true;
1404
+ if (a === 0) return true;
1405
+ if (a === 100 && b >= 64 && b <= 127) return true;
1406
+ if (a === 192 && b === 0) return true;
1407
+ if (a === 198 && b === 51) return true;
1408
+ if (a === 203 && b === 0) return true;
1409
+ if (a >= 224) return true;
1410
+ return false;
1411
+ }
1412
+ function isPrivateIpV6(ip) {
1413
+ const lower = ip.toLowerCase();
1414
+ if (lower === "::1" || lower === "::") return true;
1415
+ if (lower === "0:0:0:0:0:0:0:1" || lower === "0:0:0:0:0:0:0:0") return true;
1416
+ const v4MappedHex = lower.match(/^::ffff:([0-9a-f]+):([0-9a-f]+)$/);
1417
+ if (v4MappedHex) {
1418
+ const hi = parseInt(v4MappedHex[1], 16);
1419
+ const lo = parseInt(v4MappedHex[2], 16);
1420
+ const a = hi >> 8 & 255;
1421
+ const b = hi & 255;
1422
+ const c = lo >> 8 & 255;
1423
+ const d = lo & 255;
1424
+ if (isPrivateIpV4(`${a}.${b}.${c}.${d}`)) return true;
1425
+ }
1426
+ if (lower.startsWith("fe80:")) return true;
1427
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1428
+ const v4Mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
1429
+ if (v4Mapped) {
1430
+ return isPrivateIpV4(v4Mapped[1]);
1431
+ }
1432
+ return false;
1433
+ }
1434
+ function isBlockedHostname(hostname) {
1435
+ if (BLOCKED_HOSTNAMES.has(hostname)) return true;
1436
+ if (hostname === "localhost") return true;
1437
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
1438
+ return isPrivateIpV4(hostname);
1439
+ }
1440
+ if (hostname.includes(":")) {
1441
+ return isPrivateIpV6(hostname);
1442
+ }
1443
+ return false;
1444
+ }
1445
+ async function resolveAndCheckPrivate(hostname) {
1446
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.includes(":")) {
1447
+ return { blocked: false };
1448
+ }
1449
+ try {
1450
+ const [v4Addrs, v6Addrs] = await Promise.all([
1451
+ resolve42(hostname).catch(() => []),
1452
+ resolve62(hostname).catch(() => [])
1453
+ ]);
1454
+ for (const addr of v4Addrs) {
1455
+ if (isPrivateIpV4(addr)) {
1456
+ return { blocked: true, reason: `DNS resolved to private IPv4 address ${addr}` };
1457
+ }
1458
+ }
1459
+ for (const addr of v6Addrs) {
1460
+ if (isPrivateIpV6(addr)) {
1461
+ return { blocked: true, reason: `DNS resolved to private IPv6 address ${addr}` };
1462
+ }
1463
+ }
1464
+ if (v4Addrs.length === 0 && v6Addrs.length === 0) {
1465
+ return { blocked: true, reason: `DNS resolution failed for ${hostname}` };
1466
+ }
1467
+ return { blocked: false };
1468
+ } catch {
1469
+ return { blocked: true, reason: `DNS resolution failed for ${hostname}` };
1470
+ }
1471
+ }
1472
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
1473
+ var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
1474
+ var MAX_OUTPUT_LENGTH = 5e4;
1475
+ var MAX_REDIRECTS = 5;
1476
+ var WebFetchTool = class {
1477
+ name = "web_fetch";
1478
+ description = "Fetch content from a URL. HTML is converted to plain text. Supports HTTP and HTTPS. An optional prompt can describe what information to focus on in the response.";
1479
+ weight = "heavyweight";
1480
+ parameters = {
1481
+ type: "object",
1482
+ properties: {
1483
+ url: {
1484
+ type: "string",
1485
+ description: "The URL to fetch content from. Must be a valid HTTP or HTTPS URL.",
1486
+ format: "uri",
1487
+ minLength: 1
1488
+ },
1489
+ prompt: {
1490
+ type: "string",
1491
+ description: "Optional prompt describing what information to extract from the page."
1492
+ }
1493
+ },
1494
+ required: ["url"],
1495
+ additionalProperties: false
1496
+ };
1497
+ abortController = null;
1498
+ validate(args) {
1499
+ if (typeof args !== "object" || args === null) {
1500
+ return { valid: false, errors: ["Arguments must be an object."] };
1501
+ }
1502
+ const { url, prompt } = args;
1503
+ const errors = [];
1504
+ if (typeof url !== "string" || url.trim().length === 0) {
1505
+ errors.push("url must be a non-empty string.");
1506
+ }
1507
+ if (typeof url === "string") {
1508
+ try {
1509
+ const parsed = new URL(url);
1510
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1511
+ errors.push("url must use http or https protocol.");
1512
+ }
1513
+ if (isBlockedHostname(parsed.hostname)) {
1514
+ errors.push("url targets a blocked or private network address.");
1515
+ }
1516
+ } catch {
1517
+ errors.push("url must be a valid URL.");
1518
+ }
1519
+ }
1520
+ if (prompt !== void 0 && typeof prompt !== "string") {
1521
+ errors.push("prompt must be a string.");
1522
+ }
1523
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
1524
+ }
1525
+ async execute(args, context) {
1526
+ const validation = this.validate(args);
1527
+ if (!validation.valid) {
1528
+ return {
1529
+ success: false,
1530
+ output: "",
1531
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
1532
+ };
1533
+ }
1534
+ const { url, prompt } = args;
1535
+ let fetchUrl = url.replace(/^http:\/\//, "https://");
1536
+ if (context.abortSignal.aborted) {
1537
+ return {
1538
+ success: false,
1539
+ output: "",
1540
+ error: "Request aborted before execution."
1541
+ };
1542
+ }
1543
+ try {
1544
+ const parsed = new URL(fetchUrl);
1545
+ const dnsCheck = await resolveAndCheckPrivate(parsed.hostname);
1546
+ if (dnsCheck.blocked) {
1547
+ return {
1548
+ success: false,
1549
+ output: "",
1550
+ error: `SSRF blocked: ${dnsCheck.reason}`,
1551
+ metadata: { url: fetchUrl, ssrfBlocked: true }
1552
+ };
1553
+ }
1554
+ } catch {
1555
+ return {
1556
+ success: false,
1557
+ output: "",
1558
+ error: "Failed to parse URL for SSRF check.",
1559
+ metadata: { url: fetchUrl }
1560
+ };
1561
+ }
1562
+ this.abortController = new AbortController();
1563
+ const onContextAbort = () => this.abortController?.abort();
1564
+ context.abortSignal.addEventListener("abort", onContextAbort, { once: true });
1565
+ const timeoutId = setTimeout(() => {
1566
+ this.abortController?.abort();
1567
+ }, DEFAULT_TIMEOUT_MS2);
1568
+ try {
1569
+ context.onProgress(`Fetching ${fetchUrl}...`);
1570
+ let response = null;
1571
+ let redirectCount = 0;
1572
+ while (redirectCount <= MAX_REDIRECTS) {
1573
+ response = await fetch(fetchUrl, {
1574
+ signal: this.abortController.signal,
1575
+ headers: {
1576
+ "User-Agent": "ch4p/0.1.0",
1577
+ Accept: "text/html, application/json, text/plain, */*"
1578
+ },
1579
+ redirect: "manual"
1580
+ // We follow redirects ourselves for SSRF safety.
1581
+ });
1582
+ const status = response.status;
1583
+ if (status >= 300 && status < 400) {
1584
+ const location = response.headers.get("location");
1585
+ if (!location) break;
1586
+ const redirectUrl = new URL(location, fetchUrl).toString();
1587
+ const redirectParsed = new URL(redirectUrl);
1588
+ if (isBlockedHostname(redirectParsed.hostname)) {
1589
+ return {
1590
+ success: false,
1591
+ output: "",
1592
+ error: `SSRF blocked: redirect to private/blocked address ${redirectParsed.hostname}`,
1593
+ metadata: { url: fetchUrl, redirectUrl, ssrfBlocked: true }
1594
+ };
1595
+ }
1596
+ const redirectDns = await resolveAndCheckPrivate(redirectParsed.hostname);
1597
+ if (redirectDns.blocked) {
1598
+ return {
1599
+ success: false,
1600
+ output: "",
1601
+ error: `SSRF blocked on redirect: ${redirectDns.reason}`,
1602
+ metadata: { url: fetchUrl, redirectUrl, ssrfBlocked: true }
1603
+ };
1604
+ }
1605
+ fetchUrl = redirectUrl;
1606
+ redirectCount++;
1607
+ continue;
1608
+ }
1609
+ break;
1610
+ }
1611
+ if (!response) {
1612
+ return {
1613
+ success: false,
1614
+ output: "",
1615
+ error: "Failed to obtain a response.",
1616
+ metadata: { url: fetchUrl }
1617
+ };
1618
+ }
1619
+ if (redirectCount > MAX_REDIRECTS) {
1620
+ return {
1621
+ success: false,
1622
+ output: "",
1623
+ error: `Too many redirects (${MAX_REDIRECTS} max).`,
1624
+ metadata: { url: fetchUrl, redirectCount }
1625
+ };
1626
+ }
1627
+ if (response.status === 402) {
1628
+ const payResult = await this.tryX402Payment(response, context);
1629
+ if (!payResult.headerValue) {
1630
+ return {
1631
+ success: false,
1632
+ output: "",
1633
+ error: payResult.error ?? "Payment required (x402). Configure x402.client.privateKey to enable auto-payment.",
1634
+ metadata: { url: fetchUrl, status: 402, x402Required: true }
1635
+ };
1636
+ }
1637
+ context.onProgress("Paying x402 fee and retrying...");
1638
+ const retryResponse = await fetch(fetchUrl, {
1639
+ signal: this.abortController.signal,
1640
+ redirect: "manual",
1641
+ headers: {
1642
+ "User-Agent": "ch4p/0.1.0",
1643
+ Accept: "text/html, application/json, text/plain, */*",
1644
+ "X-PAYMENT": payResult.headerValue
1645
+ }
1646
+ });
1647
+ if (retryResponse.status >= 300 && retryResponse.status < 400) {
1648
+ return {
1649
+ success: false,
1650
+ output: "",
1651
+ error: `Unexpected redirect (${retryResponse.status}) after x402 payment. Blocked for SSRF safety.`,
1652
+ metadata: { url: fetchUrl, status: retryResponse.status, x402Paid: true }
1653
+ };
1654
+ }
1655
+ if (!retryResponse.ok) {
1656
+ return {
1657
+ success: false,
1658
+ output: "",
1659
+ error: `HTTP ${retryResponse.status} after x402 payment: ${retryResponse.statusText}`,
1660
+ metadata: { url: fetchUrl, status: retryResponse.status, x402Paid: true }
1661
+ };
1662
+ }
1663
+ response = retryResponse;
1664
+ }
1665
+ if (!response.ok) {
1666
+ return {
1667
+ success: false,
1668
+ output: "",
1669
+ error: `HTTP ${response.status}: ${response.statusText}`,
1670
+ metadata: {
1671
+ url: fetchUrl,
1672
+ status: response.status,
1673
+ statusText: response.statusText
1674
+ }
1675
+ };
1676
+ }
1677
+ const contentLength = response.headers.get("content-length");
1678
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
1679
+ return {
1680
+ success: false,
1681
+ output: "",
1682
+ error: `Response too large: ${contentLength} bytes (limit: ${MAX_RESPONSE_SIZE}).`,
1683
+ metadata: { url: fetchUrl, contentLength: parseInt(contentLength, 10) }
1684
+ };
1685
+ }
1686
+ const contentType = response.headers.get("content-type") ?? "";
1687
+ const body = await response.text();
1688
+ if (body.length > MAX_RESPONSE_SIZE) {
1689
+ return {
1690
+ success: false,
1691
+ output: "",
1692
+ error: `Response body too large: ${body.length} bytes (limit: ${MAX_RESPONSE_SIZE}).`,
1693
+ metadata: { url: fetchUrl, size: body.length }
1694
+ };
1695
+ }
1696
+ let textContent;
1697
+ if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
1698
+ textContent = htmlToText(body);
1699
+ } else if (contentType.includes("application/json")) {
1700
+ try {
1701
+ const json = JSON.parse(body);
1702
+ textContent = JSON.stringify(json, null, 2);
1703
+ } catch {
1704
+ textContent = body;
1705
+ }
1706
+ } else {
1707
+ textContent = body;
1708
+ }
1709
+ if (textContent.length > MAX_OUTPUT_LENGTH) {
1710
+ textContent = textContent.slice(0, MAX_OUTPUT_LENGTH) + "\n\n... [content truncated] ...";
1711
+ }
1712
+ let output = textContent;
1713
+ if (prompt) {
1714
+ output = `[Prompt: ${prompt}]
1715
+
1716
+ ${textContent}`;
1717
+ }
1718
+ return {
1719
+ success: true,
1720
+ output,
1721
+ metadata: {
1722
+ url: fetchUrl,
1723
+ status: response.status,
1724
+ contentType,
1725
+ size: body.length,
1726
+ truncated: textContent.length > MAX_OUTPUT_LENGTH
1727
+ }
1728
+ };
1729
+ } catch (err) {
1730
+ if (err.name === "AbortError") {
1731
+ if (context.abortSignal.aborted) {
1732
+ return {
1733
+ success: false,
1734
+ output: "",
1735
+ error: "Request was aborted."
1736
+ };
1737
+ }
1738
+ return {
1739
+ success: false,
1740
+ output: "",
1741
+ error: `Request timed out after ${DEFAULT_TIMEOUT_MS2}ms.`,
1742
+ metadata: { url: fetchUrl, timedOut: true }
1743
+ };
1744
+ }
1745
+ return {
1746
+ success: false,
1747
+ output: "",
1748
+ error: `Fetch failed: ${err.message}`,
1749
+ metadata: { url: fetchUrl }
1750
+ };
1751
+ } finally {
1752
+ clearTimeout(timeoutId);
1753
+ context.abortSignal.removeEventListener("abort", onContextAbort);
1754
+ this.abortController = null;
1755
+ }
1756
+ }
1757
+ abort(_reason) {
1758
+ this.abortController?.abort();
1759
+ }
1760
+ /**
1761
+ * Attempt an x402 auto-payment for a 402 response.
1762
+ *
1763
+ * Parses the 402 body, builds an EIP-3009 authorization struct, signs it
1764
+ * using context.x402Signer, and returns the base64-encoded X-PAYMENT header
1765
+ * value. Returns an error string if payment is not possible.
1766
+ */
1767
+ async tryX402Payment(response, context) {
1768
+ if (!context.x402Signer || !context.agentWalletAddress) {
1769
+ return {
1770
+ error: "No x402 signer configured. Set x402.client.privateKey to enable auto-payment."
1771
+ };
1772
+ }
1773
+ let body;
1774
+ try {
1775
+ const text = await response.text();
1776
+ body = JSON.parse(text);
1777
+ } catch {
1778
+ return { error: "Could not parse x402 payment requirements from 402 response body." };
1779
+ }
1780
+ const req = body.accepts?.find((r) => r.scheme === "exact") ?? body.accepts?.[0];
1781
+ if (!req) {
1782
+ return { error: "No acceptable payment scheme found in 402 response." };
1783
+ }
1784
+ const nowSecs = Math.floor(Date.now() / 1e3);
1785
+ const authorization = {
1786
+ from: context.agentWalletAddress,
1787
+ to: req.payTo,
1788
+ value: req.maxAmountRequired,
1789
+ validAfter: "0",
1790
+ validBefore: String(nowSecs + req.maxTimeoutSeconds),
1791
+ nonce: "0x" + randomBytes(32).toString("hex")
1792
+ };
1793
+ let signature;
1794
+ try {
1795
+ signature = await context.x402Signer(authorization);
1796
+ } catch (err) {
1797
+ return { error: `x402 signing failed: ${err.message}` };
1798
+ }
1799
+ const payload = {
1800
+ x402Version: 1,
1801
+ scheme: req.scheme,
1802
+ network: req.network,
1803
+ payload: { signature, authorization }
1804
+ };
1805
+ return { headerValue: Buffer.from(JSON.stringify(payload)).toString("base64") };
1806
+ }
1807
+ };
1808
+ function htmlToText(html) {
1809
+ let text = html;
1810
+ text = text.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
1811
+ text = text.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
1812
+ text = text.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, "");
1813
+ text = text.replace(/<!--[\s\S]*?-->/g, "");
1814
+ text = text.replace(/<\/?(p|div|br|hr|h[1-6]|ul|ol|li|table|tr|td|th|blockquote|pre|section|article|header|footer|nav|main|aside|figure|figcaption)\b[^>]*\/?>/gi, "\n");
1815
+ text = text.replace(/<[^>]+>/g, "");
1816
+ text = decodeHtmlEntities(text);
1817
+ text = text.replace(/[^\S\n]+/g, " ");
1818
+ text = text.replace(/\n\s*\n/g, "\n\n");
1819
+ text = text.replace(/\n{3,}/g, "\n\n");
1820
+ return text.trim();
1821
+ }
1822
+ function decodeHtmlEntities(text) {
1823
+ const entities = {
1824
+ "&amp;": "&",
1825
+ "&lt;": "<",
1826
+ "&gt;": ">",
1827
+ "&quot;": '"',
1828
+ "&#39;": "'",
1829
+ "&apos;": "'",
1830
+ "&nbsp;": " ",
1831
+ "&mdash;": "\u2014",
1832
+ "&ndash;": "\u2013",
1833
+ "&laquo;": "\xAB",
1834
+ "&raquo;": "\xBB",
1835
+ "&bull;": "\u2022",
1836
+ "&hellip;": "\u2026",
1837
+ "&copy;": "\xA9",
1838
+ "&reg;": "\xAE",
1839
+ "&trade;": "\u2122"
1840
+ };
1841
+ let result = text;
1842
+ for (const [entity, char] of Object.entries(entities)) {
1843
+ result = result.split(entity).join(char);
1844
+ }
1845
+ result = result.replace(/&#(\d+);/g, (_, code) => {
1846
+ const num = parseInt(code, 10);
1847
+ return num > 0 && num < 1114112 ? String.fromCodePoint(num) : "";
1848
+ });
1849
+ result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => {
1850
+ const num = parseInt(code, 16);
1851
+ return num > 0 && num < 1114112 ? String.fromCodePoint(num) : "";
1852
+ });
1853
+ return result;
1854
+ }
1855
+ var BRAVE_API_URL = "https://api.search.brave.com/res/v1/web/search";
1856
+ var DEFAULT_TIMEOUT_MS3 = 15e3;
1857
+ var MAX_OUTPUT_LENGTH2 = 5e4;
1858
+ var VALID_FRESHNESS = /* @__PURE__ */ new Set(["pd", "pw", "pm", "py"]);
1859
+ var WebSearchTool = class {
1860
+ name = "web_search";
1861
+ description = "Search the web for current information. Returns titles, URLs, and descriptions from web search results. Use this to find facts, research topics, or look up current events.";
1862
+ // Lightweight: web_search is a single HTTP call to the Brave API. It does
1863
+ // not need worker-pool process isolation, and running inline ensures the
1864
+ // full ToolContext (searchApiKey, searchConfig) is available — the worker
1865
+ // context is stripped to sessionId+cwd and would lose the API key.
1866
+ weight = "lightweight";
1867
+ parameters = {
1868
+ type: "object",
1869
+ properties: {
1870
+ query: {
1871
+ type: "string",
1872
+ description: "The search query. Be specific for better results.",
1873
+ minLength: 1
1874
+ },
1875
+ count: {
1876
+ type: "integer",
1877
+ description: "Number of results to return (1-20). Default: 5.",
1878
+ minimum: 1,
1879
+ maximum: 20
1880
+ },
1881
+ offset: {
1882
+ type: "integer",
1883
+ description: "Pagination offset. Use to get additional pages of results.",
1884
+ minimum: 0
1885
+ },
1886
+ freshness: {
1887
+ type: "string",
1888
+ description: "Time filter for results. pd=past day, pw=past week, pm=past month, py=past year.",
1889
+ enum: ["pd", "pw", "pm", "py"]
1890
+ },
1891
+ country: {
1892
+ type: "string",
1893
+ description: "Country code for localized results (e.g., US, GB, DE)."
1894
+ }
1895
+ },
1896
+ required: ["query"],
1897
+ additionalProperties: false
1898
+ };
1899
+ abortController = null;
1900
+ validate(args) {
1901
+ if (typeof args !== "object" || args === null) {
1902
+ return { valid: false, errors: ["Arguments must be an object."] };
1903
+ }
1904
+ const { query, count, offset, freshness, country } = args;
1905
+ const errors = [];
1906
+ if (typeof query !== "string" || query.trim().length === 0) {
1907
+ errors.push("query must be a non-empty string.");
1908
+ } else if (query.length > 2e3) {
1909
+ errors.push("query must be 2000 characters or fewer.");
1910
+ }
1911
+ if (count !== void 0) {
1912
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > 20) {
1913
+ errors.push("count must be an integer between 1 and 20.");
1914
+ }
1915
+ }
1916
+ if (offset !== void 0) {
1917
+ if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) {
1918
+ errors.push("offset must be a non-negative integer.");
1919
+ }
1920
+ }
1921
+ if (freshness !== void 0) {
1922
+ if (typeof freshness !== "string" || !VALID_FRESHNESS.has(freshness)) {
1923
+ errors.push("freshness must be one of: pd, pw, pm, py.");
1924
+ }
1925
+ }
1926
+ if (country !== void 0) {
1927
+ if (typeof country !== "string" || country.trim().length === 0) {
1928
+ errors.push("country must be a non-empty string.");
1929
+ }
1930
+ }
1931
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
1932
+ }
1933
+ async execute(args, context) {
1934
+ const validation = this.validate(args);
1935
+ if (!validation.valid) {
1936
+ return {
1937
+ success: false,
1938
+ output: "",
1939
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
1940
+ };
1941
+ }
1942
+ const { query, count, offset, freshness, country } = args;
1943
+ const searchContext = context;
1944
+ const apiKey = searchContext.searchApiKey || process.env.BRAVE_SEARCH_API_KEY;
1945
+ if (!apiKey) {
1946
+ return {
1947
+ success: false,
1948
+ output: "",
1949
+ error: "Web search is not available: no Brave Search API key found. The BRAVE_SEARCH_API_KEY environment variable is not set, and search.apiKey is not configured in ~/.ch4p/config.json. Please tell the user to set the BRAVE_SEARCH_API_KEY environment variable in the shell where the ch4p gateway is running."
1950
+ };
1951
+ }
1952
+ if (context.abortSignal.aborted) {
1953
+ return {
1954
+ success: false,
1955
+ output: "",
1956
+ error: "Request aborted before execution."
1957
+ };
1958
+ }
1959
+ this.abortController = new AbortController();
1960
+ const onContextAbort = () => this.abortController?.abort();
1961
+ context.abortSignal.addEventListener("abort", onContextAbort, { once: true });
1962
+ const timeoutId = setTimeout(() => {
1963
+ this.abortController?.abort();
1964
+ }, DEFAULT_TIMEOUT_MS3);
1965
+ try {
1966
+ context.onProgress(`Searching for "${query}"...`);
1967
+ const searchConfig = searchContext.searchConfig ?? {};
1968
+ const effectiveCount = count ?? searchConfig.maxResults ?? 5;
1969
+ const url = new URL(BRAVE_API_URL);
1970
+ url.searchParams.set("q", query);
1971
+ url.searchParams.set("count", String(effectiveCount));
1972
+ if (offset) url.searchParams.set("offset", String(offset));
1973
+ if (freshness) url.searchParams.set("freshness", freshness);
1974
+ const effectiveCountry = country ?? searchConfig.country;
1975
+ if (effectiveCountry) url.searchParams.set("country", effectiveCountry);
1976
+ if (searchConfig.searchLang) {
1977
+ url.searchParams.set("search_lang", searchConfig.searchLang);
1978
+ }
1979
+ const response = await fetch(url.toString(), {
1980
+ signal: this.abortController.signal,
1981
+ headers: {
1982
+ "Accept": "application/json",
1983
+ "Accept-Encoding": "gzip",
1984
+ "X-Subscription-Token": apiKey
1985
+ }
1986
+ });
1987
+ if (!response.ok) {
1988
+ const status = response.status;
1989
+ if (status === 401 || status === 403) {
1990
+ return {
1991
+ success: false,
1992
+ output: "",
1993
+ error: "Invalid or expired Brave Search API key.",
1994
+ metadata: { query, status }
1995
+ };
1996
+ }
1997
+ if (status === 429) {
1998
+ return {
1999
+ success: false,
2000
+ output: "",
2001
+ error: "Brave Search rate limit exceeded. Try again later.",
2002
+ metadata: { query, status }
2003
+ };
2004
+ }
2005
+ return {
2006
+ success: false,
2007
+ output: "",
2008
+ error: `Search API returned HTTP ${status}: ${response.statusText}`,
2009
+ metadata: { query, status }
2010
+ };
2011
+ }
2012
+ const data = await response.json();
2013
+ const results = data.web?.results ?? [];
2014
+ let output = formatSearchResults(results, data.query);
2015
+ if (output.length > MAX_OUTPUT_LENGTH2) {
2016
+ output = output.slice(0, MAX_OUTPUT_LENGTH2) + "\n\n... [results truncated] ...";
2017
+ }
2018
+ return {
2019
+ success: true,
2020
+ output,
2021
+ metadata: {
2022
+ query,
2023
+ provider: "brave",
2024
+ resultCount: results.length,
2025
+ offset: offset ?? 0
2026
+ }
2027
+ };
2028
+ } catch (err) {
2029
+ if (err.name === "AbortError") {
2030
+ if (context.abortSignal.aborted) {
2031
+ return {
2032
+ success: false,
2033
+ output: "",
2034
+ error: "Request was aborted."
2035
+ };
2036
+ }
2037
+ return {
2038
+ success: false,
2039
+ output: "",
2040
+ error: `Search request timed out after ${DEFAULT_TIMEOUT_MS3}ms.`,
2041
+ metadata: { query, timedOut: true }
2042
+ };
2043
+ }
2044
+ return {
2045
+ success: false,
2046
+ output: "",
2047
+ error: `Search failed: ${err.message}`,
2048
+ metadata: { query }
2049
+ };
2050
+ } finally {
2051
+ clearTimeout(timeoutId);
2052
+ context.abortSignal.removeEventListener("abort", onContextAbort);
2053
+ this.abortController = null;
2054
+ }
2055
+ }
2056
+ abort(_reason) {
2057
+ this.abortController?.abort();
2058
+ }
2059
+ };
2060
+ function formatSearchResults(results, queryInfo) {
2061
+ if (results.length === 0) {
2062
+ return "No search results found.";
2063
+ }
2064
+ const lines = [];
2065
+ if (queryInfo?.altered && queryInfo.altered !== queryInfo.original) {
2066
+ lines.push(`Showing results for: "${queryInfo.altered}" (original: "${queryInfo.original}")
2067
+ `);
2068
+ }
2069
+ lines.push(`Found ${results.length} result${results.length === 1 ? "" : "s"}:
2070
+ `);
2071
+ for (let i = 0; i < results.length; i++) {
2072
+ const r = results[i];
2073
+ lines.push(`[${i + 1}] ${r.title}`);
2074
+ lines.push(` URL: ${r.url}`);
2075
+ if (r.description) {
2076
+ lines.push(` ${r.description}`);
2077
+ }
2078
+ if (r.age) {
2079
+ lines.push(` Age: ${r.age}`);
2080
+ }
2081
+ if (r.extra_snippets && r.extra_snippets.length > 0) {
2082
+ lines.push(` Snippets: ${r.extra_snippets.join(" | ")}`);
2083
+ }
2084
+ lines.push("");
2085
+ }
2086
+ return lines.join("\n");
2087
+ }
2088
+ var MemoryStoreTool = class {
2089
+ name = "memory_store";
2090
+ description = "Store content in persistent memory for later retrieval. Each entry has a unique key, content string, and optional metadata. Stored entries can be recalled using semantic or keyword search.";
2091
+ weight = "lightweight";
2092
+ parameters = {
2093
+ type: "object",
2094
+ properties: {
2095
+ key: {
2096
+ type: "string",
2097
+ description: 'Unique key for the memory entry. Use descriptive keys like "project/architecture" or "user/preferences".',
2098
+ minLength: 1,
2099
+ maxLength: 256
2100
+ },
2101
+ content: {
2102
+ type: "string",
2103
+ description: "The content to store.",
2104
+ minLength: 1
2105
+ },
2106
+ metadata: {
2107
+ type: "object",
2108
+ description: "Optional metadata to associate with the entry (e.g. tags, source, timestamp).",
2109
+ additionalProperties: true
2110
+ }
2111
+ },
2112
+ required: ["key", "content"],
2113
+ additionalProperties: false
2114
+ };
2115
+ validate(args) {
2116
+ if (typeof args !== "object" || args === null) {
2117
+ return { valid: false, errors: ["Arguments must be an object."] };
2118
+ }
2119
+ const { key, content, metadata } = args;
2120
+ const errors = [];
2121
+ if (typeof key !== "string" || key.trim().length === 0) {
2122
+ errors.push("key must be a non-empty string.");
2123
+ }
2124
+ if (typeof key === "string" && key.length > 256) {
2125
+ errors.push("key must not exceed 256 characters.");
2126
+ }
2127
+ if (typeof content !== "string" || content.length === 0) {
2128
+ errors.push("content must be a non-empty string.");
2129
+ }
2130
+ if (metadata !== void 0) {
2131
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
2132
+ errors.push("metadata must be a plain object.");
2133
+ }
2134
+ }
2135
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
2136
+ }
2137
+ async execute(args, context) {
2138
+ const validation = this.validate(args);
2139
+ if (!validation.valid) {
2140
+ return {
2141
+ success: false,
2142
+ output: "",
2143
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
2144
+ };
2145
+ }
2146
+ const memoryContext = context;
2147
+ if (!memoryContext.memoryBackend) {
2148
+ throw new ToolError(
2149
+ "Memory backend is not available. Configure a memory backend to use memory tools.",
2150
+ this.name
2151
+ );
2152
+ }
2153
+ const { key, content, metadata } = args;
2154
+ try {
2155
+ await memoryContext.memoryBackend.store(key, content, metadata);
2156
+ } catch (err) {
2157
+ return {
2158
+ success: false,
2159
+ output: "",
2160
+ error: `Failed to store memory entry: ${err.message}`
2161
+ };
2162
+ }
2163
+ return {
2164
+ success: true,
2165
+ output: `Stored memory entry with key "${key}" (${content.length} chars).`,
2166
+ metadata: {
2167
+ key,
2168
+ contentLength: content.length,
2169
+ hasMetadata: metadata !== void 0
2170
+ }
2171
+ };
2172
+ }
2173
+ };
2174
+ var DEFAULT_LIMIT = 10;
2175
+ var MAX_LIMIT = 50;
2176
+ var MemoryRecallTool = class {
2177
+ name = "memory_recall";
2178
+ description = "Query persistent memory using hybrid search (semantic + keyword). Returns ranked results matching the query. Use this to recall previously stored information, context, or decisions.";
2179
+ weight = "lightweight";
2180
+ parameters = {
2181
+ type: "object",
2182
+ properties: {
2183
+ query: {
2184
+ type: "string",
2185
+ description: "The search query. Can be natural language or keywords.",
2186
+ minLength: 1
2187
+ },
2188
+ limit: {
2189
+ type: "number",
2190
+ description: `Maximum number of results to return. Defaults to ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`,
2191
+ minimum: 1,
2192
+ maximum: MAX_LIMIT
2193
+ }
2194
+ },
2195
+ required: ["query"],
2196
+ additionalProperties: false
2197
+ };
2198
+ validate(args) {
2199
+ if (typeof args !== "object" || args === null) {
2200
+ return { valid: false, errors: ["Arguments must be an object."] };
2201
+ }
2202
+ const { query, limit } = args;
2203
+ const errors = [];
2204
+ if (typeof query !== "string" || query.trim().length === 0) {
2205
+ errors.push("query must be a non-empty string.");
2206
+ }
2207
+ if (limit !== void 0) {
2208
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1) {
2209
+ errors.push("limit must be a positive integer.");
2210
+ }
2211
+ if (typeof limit === "number" && limit > MAX_LIMIT) {
2212
+ errors.push(`limit cannot exceed ${MAX_LIMIT}.`);
2213
+ }
2214
+ }
2215
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
2216
+ }
2217
+ async execute(args, context) {
2218
+ const validation = this.validate(args);
2219
+ if (!validation.valid) {
2220
+ return {
2221
+ success: false,
2222
+ output: "",
2223
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
2224
+ };
2225
+ }
2226
+ const memoryContext = context;
2227
+ if (!memoryContext.memoryBackend) {
2228
+ throw new ToolError(
2229
+ "Memory backend is not available. Configure a memory backend to use memory tools.",
2230
+ this.name
2231
+ );
2232
+ }
2233
+ const { query, limit } = args;
2234
+ const resultLimit = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT);
2235
+ try {
2236
+ const results = await memoryContext.memoryBackend.recall(query, {
2237
+ limit: resultLimit
2238
+ });
2239
+ if (results.length === 0) {
2240
+ return {
2241
+ success: true,
2242
+ output: "No matching memory entries found.",
2243
+ metadata: { query, resultCount: 0 }
2244
+ };
2245
+ }
2246
+ const formatted = results.map((r, i) => {
2247
+ const header = `[${i + 1}] ${r.key} (score: ${r.score.toFixed(3)}, match: ${r.matchType})`;
2248
+ const metaStr = r.metadata ? `
2249
+ metadata: ${JSON.stringify(r.metadata)}` : "";
2250
+ return `${header}${metaStr}
2251
+ ${r.content}`;
2252
+ });
2253
+ return {
2254
+ success: true,
2255
+ output: formatted.join("\n\n---\n\n"),
2256
+ metadata: {
2257
+ query,
2258
+ resultCount: results.length,
2259
+ topScore: results[0]?.score
2260
+ }
2261
+ };
2262
+ } catch (err) {
2263
+ return {
2264
+ success: false,
2265
+ output: "",
2266
+ error: `Memory recall failed: ${err.message}`
2267
+ };
2268
+ }
2269
+ }
2270
+ };
2271
+ var DelegateTool = class {
2272
+ name = "delegate";
2273
+ description = "Delegate a task to a sub-agent, optionally on a different execution engine or model. Useful for parallelizing work or leveraging specialized models for specific subtasks.";
2274
+ weight = "heavyweight";
2275
+ parameters = {
2276
+ type: "object",
2277
+ properties: {
2278
+ task: {
2279
+ type: "string",
2280
+ description: "The task description for the sub-agent to execute.",
2281
+ minLength: 1
2282
+ },
2283
+ engine: {
2284
+ type: "string",
2285
+ description: "Engine ID to run the sub-agent on. Defaults to the current engine."
2286
+ },
2287
+ model: {
2288
+ type: "string",
2289
+ description: "Model ID to use for the sub-agent. Defaults to the current model."
2290
+ },
2291
+ context: {
2292
+ type: "string",
2293
+ description: "Optional parent context snippet to pass to the sub-agent (e.g. a conversation summary or relevant background). The sub-agent sees this as prior context before the task."
2294
+ }
2295
+ },
2296
+ required: ["task"],
2297
+ additionalProperties: false
2298
+ };
2299
+ cancelFn = null;
2300
+ validate(args) {
2301
+ if (typeof args !== "object" || args === null) {
2302
+ return { valid: false, errors: ["Arguments must be an object."] };
2303
+ }
2304
+ const { task, engine, model } = args;
2305
+ const errors = [];
2306
+ if (typeof task !== "string" || task.trim().length === 0) {
2307
+ errors.push("task must be a non-empty string.");
2308
+ }
2309
+ if (engine !== void 0 && typeof engine !== "string") {
2310
+ errors.push("engine must be a string.");
2311
+ }
2312
+ if (model !== void 0 && typeof model !== "string") {
2313
+ errors.push("model must be a string.");
2314
+ }
2315
+ const { context } = args;
2316
+ if (context !== void 0 && typeof context !== "string") {
2317
+ errors.push("context must be a string.");
2318
+ }
2319
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
2320
+ }
2321
+ async execute(args, context) {
2322
+ const validation = this.validate(args);
2323
+ if (!validation.valid) {
2324
+ return {
2325
+ success: false,
2326
+ output: "",
2327
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
2328
+ };
2329
+ }
2330
+ const delegateContext = context;
2331
+ if (!delegateContext.resolveEngine) {
2332
+ throw new ToolError(
2333
+ "Engine resolution is not available. The delegate tool requires access to the engine registry.",
2334
+ this.name
2335
+ );
2336
+ }
2337
+ const { task, engine: engineId, model, context: parentContext } = args;
2338
+ const targetEngine = delegateContext.resolveEngine(engineId);
2339
+ if (!targetEngine) {
2340
+ return {
2341
+ success: false,
2342
+ output: "",
2343
+ error: engineId ? `Engine "${engineId}" not found.` : "No default engine available."
2344
+ };
2345
+ }
2346
+ if (context.abortSignal.aborted) {
2347
+ return {
2348
+ success: false,
2349
+ output: "",
2350
+ error: "Delegation aborted before execution."
2351
+ };
2352
+ }
2353
+ context.onProgress(
2354
+ `Delegating to ${targetEngine.name}${model ? ` (model: ${model})` : ""}...`
2355
+ );
2356
+ try {
2357
+ const taskContent = parentContext ? `[Context]
2358
+ ${parentContext}
2359
+
2360
+ [Task]
2361
+ ${task}` : task;
2362
+ const subMessages = [
2363
+ { role: "user", content: taskContent }
2364
+ ];
2365
+ const handle = await targetEngine.startRun(
2366
+ {
2367
+ sessionId: `${context.sessionId}-delegate-${Date.now()}`,
2368
+ messages: subMessages,
2369
+ model: model ?? delegateContext.defaultModel,
2370
+ systemPrompt: "You are a sub-agent executing a delegated task. Complete the task thoroughly and return your findings."
2371
+ },
2372
+ {
2373
+ signal: context.abortSignal,
2374
+ onProgress: (event) => {
2375
+ if (event.type === "text_delta") {
2376
+ context.onProgress(event.delta);
2377
+ }
2378
+ }
2379
+ }
2380
+ );
2381
+ this.cancelFn = () => handle.cancel();
2382
+ let answer = "";
2383
+ let usage;
2384
+ for await (const event of handle.events) {
2385
+ if (context.abortSignal.aborted) {
2386
+ await handle.cancel();
2387
+ return {
2388
+ success: false,
2389
+ output: answer,
2390
+ error: "Delegation was aborted."
2391
+ };
2392
+ }
2393
+ switch (event.type) {
2394
+ case "text_delta":
2395
+ answer += event.delta;
2396
+ break;
2397
+ case "completed":
2398
+ answer = event.answer;
2399
+ usage = event.usage;
2400
+ break;
2401
+ case "error":
2402
+ return {
2403
+ success: false,
2404
+ output: answer,
2405
+ error: `Sub-agent error: ${event.error.message}`
2406
+ };
2407
+ }
2408
+ }
2409
+ this.cancelFn = null;
2410
+ return {
2411
+ success: true,
2412
+ output: answer,
2413
+ metadata: {
2414
+ engine: targetEngine.id,
2415
+ model: model ?? delegateContext.defaultModel,
2416
+ usage
2417
+ }
2418
+ };
2419
+ } catch (err) {
2420
+ this.cancelFn = null;
2421
+ if (err.name === "AbortError") {
2422
+ return {
2423
+ success: false,
2424
+ output: "",
2425
+ error: "Delegation was aborted."
2426
+ };
2427
+ }
2428
+ return {
2429
+ success: false,
2430
+ output: "",
2431
+ error: `Delegation failed: ${err.message}`
2432
+ };
2433
+ }
2434
+ }
2435
+ abort(_reason) {
2436
+ this.cancelFn?.();
2437
+ this.cancelFn = null;
2438
+ }
2439
+ };
2440
+ var Semaphore = class {
2441
+ count;
2442
+ queue = [];
2443
+ constructor(limit) {
2444
+ this.count = limit;
2445
+ }
2446
+ async acquire() {
2447
+ if (this.count > 0) {
2448
+ this.count--;
2449
+ return;
2450
+ }
2451
+ return new Promise((resolve7) => {
2452
+ this.queue.push(resolve7);
2453
+ });
2454
+ }
2455
+ release() {
2456
+ const next = this.queue.shift();
2457
+ if (next) {
2458
+ next();
2459
+ } else {
2460
+ this.count++;
2461
+ }
2462
+ }
2463
+ };
2464
+ var MeshTool = class {
2465
+ name = "mesh";
2466
+ description = "Execute multiple tasks in parallel across sub-agents. Each task can target a different engine or model. Results are collected and returned as a structured aggregate. Use this for parallelizable work like research, analysis, or multi-perspective problem solving.";
2467
+ weight = "heavyweight";
2468
+ parameters = {
2469
+ type: "object",
2470
+ properties: {
2471
+ tasks: {
2472
+ type: "array",
2473
+ items: {
2474
+ type: "object",
2475
+ properties: {
2476
+ task: {
2477
+ type: "string",
2478
+ description: "Task description for the sub-agent.",
2479
+ minLength: 1
2480
+ },
2481
+ engine: {
2482
+ type: "string",
2483
+ description: "Engine ID to run this task on (optional, defaults to current)."
2484
+ },
2485
+ model: {
2486
+ type: "string",
2487
+ description: "Model ID for this task (optional)."
2488
+ },
2489
+ context: {
2490
+ type: "string",
2491
+ description: "Optional parent context snippet to pass to this sub-agent task (e.g. a conversation summary or relevant background)."
2492
+ }
2493
+ },
2494
+ required: ["task"],
2495
+ additionalProperties: false
2496
+ },
2497
+ description: "Array of tasks to execute in parallel across sub-agents (at least 1)."
2498
+ },
2499
+ concurrency: {
2500
+ type: "number",
2501
+ description: "Max parallel sub-agents (default: from config or 3).",
2502
+ minimum: 1,
2503
+ maximum: 10
2504
+ }
2505
+ },
2506
+ required: ["tasks"],
2507
+ additionalProperties: false
2508
+ };
2509
+ cancelFns = [];
2510
+ validate(args) {
2511
+ if (typeof args !== "object" || args === null) {
2512
+ return { valid: false, errors: ["Arguments must be an object."] };
2513
+ }
2514
+ const { tasks, concurrency } = args;
2515
+ const errors = [];
2516
+ if (!Array.isArray(tasks) || tasks.length === 0) {
2517
+ errors.push("tasks must be a non-empty array.");
2518
+ } else {
2519
+ for (let i = 0; i < tasks.length; i++) {
2520
+ const t = tasks[i];
2521
+ if (!t || typeof t.task !== "string" || t.task.trim().length === 0) {
2522
+ errors.push(`tasks[${i}].task must be a non-empty string.`);
2523
+ }
2524
+ if (t?.engine !== void 0 && typeof t.engine !== "string") {
2525
+ errors.push(`tasks[${i}].engine must be a string.`);
2526
+ }
2527
+ if (t?.model !== void 0 && typeof t.model !== "string") {
2528
+ errors.push(`tasks[${i}].model must be a string.`);
2529
+ }
2530
+ }
2531
+ }
2532
+ if (concurrency !== void 0) {
2533
+ if (typeof concurrency !== "number" || concurrency < 1 || concurrency > 10) {
2534
+ errors.push("concurrency must be a number between 1 and 10.");
2535
+ }
2536
+ }
2537
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
2538
+ }
2539
+ async execute(args, context) {
2540
+ const validation = this.validate(args);
2541
+ if (!validation.valid) {
2542
+ return {
2543
+ success: false,
2544
+ output: "",
2545
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
2546
+ };
2547
+ }
2548
+ const meshContext = context;
2549
+ if (!meshContext.resolveEngine) {
2550
+ throw new ToolError(
2551
+ "Engine resolution is not available. The mesh tool requires access to the engine registry.",
2552
+ this.name
2553
+ );
2554
+ }
2555
+ const { tasks, concurrency: requestedConcurrency } = args;
2556
+ const maxConcurrency = requestedConcurrency ?? meshContext.meshConfig?.maxConcurrency ?? 3;
2557
+ const defaultTimeout = meshContext.meshConfig?.defaultTimeout ?? 12e4;
2558
+ if (context.abortSignal.aborted) {
2559
+ return {
2560
+ success: false,
2561
+ output: "",
2562
+ error: "Mesh execution aborted before start."
2563
+ };
2564
+ }
2565
+ context.onProgress(
2566
+ `Spawning ${tasks.length} sub-agent(s) with concurrency ${maxConcurrency}...`
2567
+ );
2568
+ const semaphore = new Semaphore(maxConcurrency);
2569
+ this.cancelFns = [];
2570
+ const outcomes = await Promise.allSettled(
2571
+ tasks.map(
2572
+ (task, index) => this.executeTask(task, index, meshContext, semaphore, defaultTimeout)
2573
+ )
2574
+ );
2575
+ const results = outcomes.map((outcome, index) => {
2576
+ if (outcome.status === "fulfilled") {
2577
+ return outcome.value;
2578
+ }
2579
+ return {
2580
+ task: tasks[index].task,
2581
+ engine: tasks[index].engine ?? "default",
2582
+ success: false,
2583
+ output: "",
2584
+ error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
2585
+ durationMs: 0
2586
+ };
2587
+ });
2588
+ this.cancelFns = [];
2589
+ const succeeded = results.filter((r) => r.success).length;
2590
+ const failed = results.length - succeeded;
2591
+ const outputLines = results.map((r, i) => {
2592
+ const status = r.success ? "\u2713" : "\u2717";
2593
+ const header = `[${status}] Task ${i + 1}: ${r.task.slice(0, 80)}`;
2594
+ const engineInfo = ` Engine: ${r.engine} (${r.durationMs}ms)`;
2595
+ const body = r.success ? r.output.split("\n").map((l) => ` ${l}`).join("\n") : ` Error: ${r.error}`;
2596
+ return `${header}
2597
+ ${engineInfo}
2598
+ ${body}`;
2599
+ });
2600
+ const summary = `${succeeded}/${results.length} tasks succeeded` + (failed > 0 ? ` (${failed} failed)` : "");
2601
+ return {
2602
+ success: failed === 0,
2603
+ output: `${summary}
2604
+
2605
+ ${outputLines.join("\n\n")}`,
2606
+ metadata: {
2607
+ total: results.length,
2608
+ succeeded,
2609
+ failed,
2610
+ results: results.map((r) => ({
2611
+ task: r.task,
2612
+ engine: r.engine,
2613
+ success: r.success,
2614
+ durationMs: r.durationMs,
2615
+ error: r.error
2616
+ }))
2617
+ }
2618
+ };
2619
+ }
2620
+ abort(_reason) {
2621
+ for (const cancel of this.cancelFns) {
2622
+ cancel().catch(() => {
2623
+ });
2624
+ }
2625
+ this.cancelFns = [];
2626
+ }
2627
+ // -------------------------------------------------------------------------
2628
+ // Private
2629
+ // -------------------------------------------------------------------------
2630
+ async executeTask(task, _index, context, semaphore, timeout) {
2631
+ await semaphore.acquire();
2632
+ const startTime = Date.now();
2633
+ try {
2634
+ if (context.abortSignal.aborted) {
2635
+ return {
2636
+ task: task.task,
2637
+ engine: task.engine ?? "default",
2638
+ success: false,
2639
+ output: "",
2640
+ error: "Aborted.",
2641
+ durationMs: 0
2642
+ };
2643
+ }
2644
+ const targetEngine = context.resolveEngine(task.engine);
2645
+ if (!targetEngine) {
2646
+ return {
2647
+ task: task.task,
2648
+ engine: task.engine ?? "default",
2649
+ success: false,
2650
+ output: "",
2651
+ error: task.engine ? `Engine "${task.engine}" not found.` : "No default engine available.",
2652
+ durationMs: Date.now() - startTime
2653
+ };
2654
+ }
2655
+ const taskAbort = new AbortController();
2656
+ const timeoutId = setTimeout(() => taskAbort.abort(), timeout);
2657
+ const parentAbortHandler = () => taskAbort.abort();
2658
+ context.abortSignal.addEventListener("abort", parentAbortHandler, { once: true });
2659
+ try {
2660
+ const taskContent = task.context ? `[Context]
2661
+ ${task.context}
2662
+
2663
+ [Task]
2664
+ ${task.task}` : task.task;
2665
+ const subMessages = [
2666
+ { role: "user", content: taskContent }
2667
+ ];
2668
+ const handle = await targetEngine.startRun(
2669
+ {
2670
+ sessionId: `${context.sessionId}-mesh-${Date.now()}-${_index}`,
2671
+ messages: subMessages,
2672
+ model: task.model ?? context.defaultModel,
2673
+ systemPrompt: "You are a sub-agent executing a delegated task as part of a parallel mesh. Complete the task thoroughly and return your findings concisely."
2674
+ },
2675
+ {
2676
+ signal: taskAbort.signal,
2677
+ onProgress: (event) => {
2678
+ if (event.type === "text_delta") {
2679
+ context.onProgress(`[Task ${_index + 1}] ${event.delta}`);
2680
+ }
2681
+ }
2682
+ }
2683
+ );
2684
+ this.cancelFns.push(() => handle.cancel());
2685
+ let answer = "";
2686
+ for await (const event of handle.events) {
2687
+ if (taskAbort.signal.aborted) {
2688
+ await handle.cancel();
2689
+ return {
2690
+ task: task.task,
2691
+ engine: targetEngine.id,
2692
+ success: false,
2693
+ output: answer,
2694
+ error: "Task timed out or was aborted.",
2695
+ durationMs: Date.now() - startTime
2696
+ };
2697
+ }
2698
+ switch (event.type) {
2699
+ case "text_delta":
2700
+ answer += event.delta;
2701
+ break;
2702
+ case "completed":
2703
+ answer = event.answer;
2704
+ break;
2705
+ case "error":
2706
+ return {
2707
+ task: task.task,
2708
+ engine: targetEngine.id,
2709
+ success: false,
2710
+ output: answer,
2711
+ error: `Sub-agent error: ${event.error.message}`,
2712
+ durationMs: Date.now() - startTime
2713
+ };
2714
+ }
2715
+ }
2716
+ return {
2717
+ task: task.task,
2718
+ engine: targetEngine.id,
2719
+ success: true,
2720
+ output: answer,
2721
+ durationMs: Date.now() - startTime
2722
+ };
2723
+ } finally {
2724
+ clearTimeout(timeoutId);
2725
+ context.abortSignal.removeEventListener("abort", parentAbortHandler);
2726
+ }
2727
+ } catch (err) {
2728
+ return {
2729
+ task: task.task,
2730
+ engine: task.engine ?? "default",
2731
+ success: false,
2732
+ output: "",
2733
+ error: err.name === "AbortError" ? "Task timed out or was aborted." : `Task failed: ${err.message}`,
2734
+ durationMs: Date.now() - startTime
2735
+ };
2736
+ } finally {
2737
+ semaphore.release();
2738
+ }
2739
+ }
2740
+ };
2741
+ var NAV_TIMEOUT_MS = 3e4;
2742
+ var MAX_SCREENSHOT_SIZE = 5 * 1024 * 1024;
2743
+ var MAX_EVAL_OUTPUT = 5e4;
2744
+ var MAX_WAIT_MS = 1e4;
2745
+ var MAX_TEXT_EXCERPT = 2e3;
2746
+ var VALID_ACTIONS = /* @__PURE__ */ new Set([
2747
+ "navigate",
2748
+ "click",
2749
+ "type",
2750
+ "screenshot",
2751
+ "evaluate",
2752
+ "scroll",
2753
+ "wait",
2754
+ "close"
2755
+ ]);
2756
+ var VALID_DIRECTIONS = /* @__PURE__ */ new Set(["up", "down", "left", "right"]);
2757
+ var BrowserTool = class {
2758
+ name = "browser";
2759
+ description = "Control a headless browser. Navigate to URLs, click elements, type text, take screenshots, evaluate JavaScript, and scroll pages. The browser persists across calls within a session.";
2760
+ weight = "heavyweight";
2761
+ parameters = {
2762
+ type: "object",
2763
+ properties: {
2764
+ action: {
2765
+ type: "string",
2766
+ description: "The browser action to perform: navigate, click, type, screenshot, evaluate, scroll, wait, or close.",
2767
+ enum: ["navigate", "click", "type", "screenshot", "evaluate", "scroll", "wait", "close"]
2768
+ },
2769
+ url: {
2770
+ type: "string",
2771
+ description: "URL to navigate to (required for navigate action)."
2772
+ },
2773
+ selector: {
2774
+ type: "string",
2775
+ description: "CSS selector of the target element (required for click and type actions)."
2776
+ },
2777
+ text: {
2778
+ type: "string",
2779
+ description: "Text to type into the selected element (required for type action)."
2780
+ },
2781
+ expression: {
2782
+ type: "string",
2783
+ description: "JavaScript expression to evaluate in the page context (required for evaluate action)."
2784
+ },
2785
+ direction: {
2786
+ type: "string",
2787
+ description: "Scroll direction: up, down, left, or right. Default: down.",
2788
+ enum: ["up", "down", "left", "right"]
2789
+ },
2790
+ distance: {
2791
+ type: "number",
2792
+ description: "Scroll distance in pixels. Default: 500.",
2793
+ minimum: 1,
2794
+ maximum: 1e4
2795
+ },
2796
+ timeout: {
2797
+ type: "number",
2798
+ description: "Wait time in milliseconds (for wait action). Max 10000.",
2799
+ minimum: 100,
2800
+ maximum: 1e4
2801
+ },
2802
+ fullPage: {
2803
+ type: "boolean",
2804
+ description: "Whether to capture a full-page screenshot. Default: false."
2805
+ }
2806
+ },
2807
+ required: ["action"],
2808
+ additionalProperties: false
2809
+ };
2810
+ // Persistent browser state.
2811
+ browser = null;
2812
+ page = null;
2813
+ launching = false;
2814
+ // ---------------------------------------------------------------------------
2815
+ // Validation
2816
+ // ---------------------------------------------------------------------------
2817
+ validate(args) {
2818
+ if (typeof args !== "object" || args === null) {
2819
+ return { valid: false, errors: ["Arguments must be an object."] };
2820
+ }
2821
+ const a = args;
2822
+ const errors = [];
2823
+ if (typeof a.action !== "string" || !VALID_ACTIONS.has(a.action)) {
2824
+ errors.push(`action must be one of: ${[...VALID_ACTIONS].join(", ")}.`);
2825
+ return { valid: false, errors };
2826
+ }
2827
+ const action = a.action;
2828
+ switch (action) {
2829
+ case "navigate":
2830
+ if (typeof a.url !== "string" || a.url.trim().length === 0) {
2831
+ errors.push("url is required for navigate action.");
2832
+ } else {
2833
+ try {
2834
+ const parsed = new URL(a.url);
2835
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2836
+ errors.push("url must use http or https protocol.");
2837
+ }
2838
+ if (isBlockedHostname(parsed.hostname)) {
2839
+ errors.push("url targets a blocked or private network address.");
2840
+ }
2841
+ } catch {
2842
+ errors.push("url must be a valid URL.");
2843
+ }
2844
+ }
2845
+ break;
2846
+ case "click":
2847
+ if (typeof a.selector !== "string" || a.selector.trim().length === 0) {
2848
+ errors.push("selector is required for click action.");
2849
+ }
2850
+ break;
2851
+ case "type":
2852
+ if (typeof a.selector !== "string" || a.selector.trim().length === 0) {
2853
+ errors.push("selector is required for type action.");
2854
+ }
2855
+ if (typeof a.text !== "string") {
2856
+ errors.push("text is required for type action.");
2857
+ }
2858
+ break;
2859
+ case "evaluate":
2860
+ if (typeof a.expression !== "string" || a.expression.trim().length === 0) {
2861
+ errors.push("expression is required for evaluate action.");
2862
+ }
2863
+ break;
2864
+ case "scroll":
2865
+ if (a.direction !== void 0 && (typeof a.direction !== "string" || !VALID_DIRECTIONS.has(a.direction))) {
2866
+ errors.push("direction must be one of: up, down, left, right.");
2867
+ }
2868
+ if (a.distance !== void 0) {
2869
+ if (typeof a.distance !== "number" || a.distance < 1 || a.distance > 1e4) {
2870
+ errors.push("distance must be a number between 1 and 10000.");
2871
+ }
2872
+ }
2873
+ break;
2874
+ case "wait":
2875
+ if (a.timeout !== void 0) {
2876
+ if (typeof a.timeout !== "number" || a.timeout < 100 || a.timeout > MAX_WAIT_MS) {
2877
+ errors.push(`timeout must be between 100 and ${MAX_WAIT_MS}ms.`);
2878
+ }
2879
+ }
2880
+ break;
2881
+ case "screenshot":
2882
+ if (a.fullPage !== void 0 && typeof a.fullPage !== "boolean") {
2883
+ errors.push("fullPage must be a boolean.");
2884
+ }
2885
+ break;
2886
+ case "close":
2887
+ break;
2888
+ }
2889
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
2890
+ }
2891
+ // ---------------------------------------------------------------------------
2892
+ // Execute
2893
+ // ---------------------------------------------------------------------------
2894
+ async execute(args, context) {
2895
+ const validation = this.validate(args);
2896
+ if (!validation.valid) {
2897
+ return {
2898
+ success: false,
2899
+ output: "",
2900
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
2901
+ };
2902
+ }
2903
+ const a = args;
2904
+ if (context.abortSignal.aborted) {
2905
+ return { success: false, output: "", error: "Request aborted before execution." };
2906
+ }
2907
+ if (a.action === "evaluate" && context.securityPolicy) {
2908
+ const policy = context.securityPolicy;
2909
+ const autonomy = policy.autonomyLevel;
2910
+ if (autonomy === "readonly") {
2911
+ return {
2912
+ success: false,
2913
+ output: "",
2914
+ error: "evaluate action is blocked in readonly autonomy mode."
2915
+ };
2916
+ }
2917
+ }
2918
+ if (a.action === "close") {
2919
+ return this.doClose();
2920
+ }
2921
+ const ensureResult = await this.ensureBrowser(context);
2922
+ if (ensureResult) return ensureResult;
2923
+ try {
2924
+ switch (a.action) {
2925
+ case "navigate":
2926
+ return await this.doNavigate(a, context);
2927
+ case "click":
2928
+ return await this.doClick(a);
2929
+ case "type":
2930
+ return await this.doType(a);
2931
+ case "screenshot":
2932
+ return await this.doScreenshot(a);
2933
+ case "evaluate":
2934
+ return await this.doEvaluate(a);
2935
+ case "scroll":
2936
+ return await this.doScroll(a);
2937
+ case "wait":
2938
+ return await this.doWait(a);
2939
+ default:
2940
+ return { success: false, output: "", error: `Unknown action: ${a.action}` };
2941
+ }
2942
+ } catch (err) {
2943
+ const message = err instanceof Error ? err.message : String(err);
2944
+ return { success: false, output: "", error: `Browser error: ${message}` };
2945
+ }
2946
+ }
2947
+ // ---------------------------------------------------------------------------
2948
+ // AWM state snapshot
2949
+ // ---------------------------------------------------------------------------
2950
+ async getStateSnapshot(_args, _context) {
2951
+ const state = {};
2952
+ if (this.page && !this.page.isClosed()) {
2953
+ try {
2954
+ state.url = this.page.url();
2955
+ state.title = await this.page.title();
2956
+ const excerpt = await this.page.evaluate(
2957
+ '(() => { const b = document.body; return b ? (b.innerText || "").slice(0, 2000) : ""; })()'
2958
+ ).catch(() => "");
2959
+ state.visibleText = (excerpt ?? "").slice(0, MAX_TEXT_EXCERPT);
2960
+ } catch {
2961
+ state.url = "unknown";
2962
+ state.title = "unknown";
2963
+ }
2964
+ } else {
2965
+ state.url = null;
2966
+ state.title = null;
2967
+ state.browserRunning = false;
2968
+ }
2969
+ return {
2970
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2971
+ state,
2972
+ description: state.url ? `Browser at ${state.url} \u2014 "${state.title}"` : "Browser not running"
2973
+ };
2974
+ }
2975
+ // ---------------------------------------------------------------------------
2976
+ // Abort
2977
+ // ---------------------------------------------------------------------------
2978
+ abort(_reason) {
2979
+ this.closeBrowser().catch(() => {
2980
+ });
2981
+ }
2982
+ // ---------------------------------------------------------------------------
2983
+ // Browser lifecycle
2984
+ // ---------------------------------------------------------------------------
2985
+ /**
2986
+ * Ensure the browser is launched. Lazy-starts on first call.
2987
+ * Returns a ToolResult error if playwright-core is not available.
2988
+ */
2989
+ async ensureBrowser(context) {
2990
+ if (this.page && !this.page.isClosed()) return null;
2991
+ if (this.browser && this.browser.isConnected()) {
2992
+ this.page = await this.browser.newPage();
2993
+ return null;
2994
+ }
2995
+ if (this.launching) {
2996
+ return { success: false, output: "", error: "Browser is already launching." };
2997
+ }
2998
+ this.launching = true;
2999
+ try {
3000
+ const pw = await import("playwright-core").catch(() => null);
3001
+ if (!pw) {
3002
+ return {
3003
+ success: false,
3004
+ output: "",
3005
+ error: "playwright-core is not installed. Run: npm install -D playwright-core && npx playwright install chromium"
3006
+ };
3007
+ }
3008
+ let executablePath;
3009
+ for (const browserType of [pw.chromium, pw.firefox, pw.webkit]) {
3010
+ try {
3011
+ executablePath = browserType.executablePath();
3012
+ if (executablePath) {
3013
+ this.browser = await browserType.launch({
3014
+ headless: true,
3015
+ args: ["--disable-dev-shm-usage", "--no-sandbox"]
3016
+ });
3017
+ break;
3018
+ }
3019
+ } catch {
3020
+ continue;
3021
+ }
3022
+ }
3023
+ if (!this.browser) {
3024
+ return {
3025
+ success: false,
3026
+ output: "",
3027
+ error: "No browser found. Install one with: npx playwright install chromium"
3028
+ };
3029
+ }
3030
+ this.page = await this.browser.newPage();
3031
+ context.abortSignal.addEventListener("abort", () => {
3032
+ this.closeBrowser().catch(() => {
3033
+ });
3034
+ }, { once: true });
3035
+ return null;
3036
+ } catch (err) {
3037
+ const message = err instanceof Error ? err.message : String(err);
3038
+ return {
3039
+ success: false,
3040
+ output: "",
3041
+ error: `Failed to launch browser: ${message}`
3042
+ };
3043
+ } finally {
3044
+ this.launching = false;
3045
+ }
3046
+ }
3047
+ async closeBrowser() {
3048
+ if (this.page && !this.page.isClosed()) {
3049
+ try {
3050
+ await this.page.close();
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ this.page = null;
3055
+ if (this.browser) {
3056
+ try {
3057
+ await this.browser.close();
3058
+ } catch {
3059
+ }
3060
+ }
3061
+ this.browser = null;
3062
+ }
3063
+ // ---------------------------------------------------------------------------
3064
+ // Action implementations
3065
+ // ---------------------------------------------------------------------------
3066
+ async doNavigate(a, context) {
3067
+ let url = a.url;
3068
+ url = url.replace(/^http:\/\//, "https://");
3069
+ try {
3070
+ const parsed = new URL(url);
3071
+ const dnsCheck = await resolveAndCheckPrivate(parsed.hostname);
3072
+ if (dnsCheck.blocked) {
3073
+ return {
3074
+ success: false,
3075
+ output: "",
3076
+ error: `SSRF blocked: ${dnsCheck.reason}`,
3077
+ metadata: { url, ssrfBlocked: true }
3078
+ };
3079
+ }
3080
+ } catch {
3081
+ return {
3082
+ success: false,
3083
+ output: "",
3084
+ error: "Failed to parse URL for SSRF check.",
3085
+ metadata: { url }
3086
+ };
3087
+ }
3088
+ context.onProgress(`Navigating to ${url}...`);
3089
+ await this.page.goto(url, {
3090
+ waitUntil: "domcontentloaded",
3091
+ timeout: NAV_TIMEOUT_MS
3092
+ });
3093
+ const title = await this.page.title();
3094
+ const currentUrl = this.page.url();
3095
+ return {
3096
+ success: true,
3097
+ output: `Navigated to ${currentUrl}
3098
+ Title: ${title}`,
3099
+ metadata: { url: currentUrl, title }
3100
+ };
3101
+ }
3102
+ async doClick(a) {
3103
+ const selector = a.selector;
3104
+ await this.page.click(selector, { timeout: 5e3 });
3105
+ await this.page.waitForTimeout(500);
3106
+ const url = this.page.url();
3107
+ const title = await this.page.title();
3108
+ return {
3109
+ success: true,
3110
+ output: `Clicked "${selector}"
3111
+ Current URL: ${url}
3112
+ Title: ${title}`,
3113
+ metadata: { selector, url, title }
3114
+ };
3115
+ }
3116
+ async doType(a) {
3117
+ const selector = a.selector;
3118
+ const text = a.text;
3119
+ await this.page.fill(selector, text, { timeout: 5e3 });
3120
+ return {
3121
+ success: true,
3122
+ output: `Typed ${text.length} characters into "${selector}"`,
3123
+ metadata: { selector, length: text.length }
3124
+ };
3125
+ }
3126
+ async doScreenshot(a) {
3127
+ const fullPage = a.fullPage ?? false;
3128
+ const buffer = await this.page.screenshot({
3129
+ type: "png",
3130
+ fullPage
3131
+ });
3132
+ if (buffer.byteLength > MAX_SCREENSHOT_SIZE) {
3133
+ return {
3134
+ success: false,
3135
+ output: "",
3136
+ error: `Screenshot too large: ${buffer.byteLength} bytes (limit: ${MAX_SCREENSHOT_SIZE}).`,
3137
+ metadata: { size: buffer.byteLength }
3138
+ };
3139
+ }
3140
+ const base64 = buffer.toString("base64");
3141
+ const url = this.page.url();
3142
+ const title = await this.page.title();
3143
+ return {
3144
+ success: true,
3145
+ output: `Screenshot captured (${buffer.byteLength} bytes, ${fullPage ? "full page" : "viewport"})
3146
+ URL: ${url}
3147
+ Title: ${title}`,
3148
+ metadata: {
3149
+ url,
3150
+ title,
3151
+ size: buffer.byteLength,
3152
+ fullPage,
3153
+ base64,
3154
+ mimeType: "image/png"
3155
+ }
3156
+ };
3157
+ }
3158
+ async doEvaluate(a) {
3159
+ const expression = a.expression;
3160
+ const result = await this.page.evaluate(expression);
3161
+ let output;
3162
+ if (result === void 0) {
3163
+ output = "undefined";
3164
+ } else if (result === null) {
3165
+ output = "null";
3166
+ } else if (typeof result === "string") {
3167
+ output = result;
3168
+ } else {
3169
+ try {
3170
+ output = JSON.stringify(result, null, 2);
3171
+ } catch {
3172
+ output = String(result);
3173
+ }
3174
+ }
3175
+ if (output.length > MAX_EVAL_OUTPUT) {
3176
+ output = output.slice(0, MAX_EVAL_OUTPUT) + "\n\n... [output truncated] ...";
3177
+ }
3178
+ return {
3179
+ success: true,
3180
+ output,
3181
+ metadata: { expression, outputLength: output.length }
3182
+ };
3183
+ }
3184
+ async doScroll(a) {
3185
+ const direction = a.direction ?? "down";
3186
+ const distance = a.distance ?? 500;
3187
+ let deltaX = 0;
3188
+ let deltaY = 0;
3189
+ switch (direction) {
3190
+ case "down":
3191
+ deltaY = distance;
3192
+ break;
3193
+ case "up":
3194
+ deltaY = -distance;
3195
+ break;
3196
+ case "right":
3197
+ deltaX = distance;
3198
+ break;
3199
+ case "left":
3200
+ deltaX = -distance;
3201
+ break;
3202
+ }
3203
+ await this.page.evaluate(
3204
+ `window.scrollBy(${deltaX}, ${deltaY})`
3205
+ );
3206
+ await this.page.waitForTimeout(300);
3207
+ const scrollPos = await this.page.evaluate(
3208
+ "({ x: window.scrollX, y: window.scrollY, height: document.documentElement.scrollHeight, viewportHeight: window.innerHeight })"
3209
+ );
3210
+ return {
3211
+ success: true,
3212
+ output: `Scrolled ${direction} ${distance}px
3213
+ Scroll position: ${scrollPos.y}px / ${scrollPos.height}px (viewport: ${scrollPos.viewportHeight}px)`,
3214
+ metadata: { direction, distance, ...scrollPos }
3215
+ };
3216
+ }
3217
+ async doWait(a) {
3218
+ const timeout = a.timeout ?? 1e3;
3219
+ const clamped = Math.min(timeout, MAX_WAIT_MS);
3220
+ await this.page.waitForTimeout(clamped);
3221
+ return {
3222
+ success: true,
3223
+ output: `Waited ${clamped}ms`,
3224
+ metadata: { waited: clamped }
3225
+ };
3226
+ }
3227
+ async doClose() {
3228
+ const wasRunning = this.browser !== null;
3229
+ await this.closeBrowser();
3230
+ return {
3231
+ success: true,
3232
+ output: wasRunning ? "Browser closed." : "Browser was not running.",
3233
+ metadata: { wasRunning }
3234
+ };
3235
+ }
3236
+ };
3237
+ var LoadSkillTool = class {
3238
+ name = "load_skill";
3239
+ description = "Load a skill by name to receive detailed instructions. Use this when you need the full instructions for a skill listed in your system prompt. The skill body will be returned as markdown instructions to follow.";
3240
+ weight = "lightweight";
3241
+ parameters = {
3242
+ type: "object",
3243
+ properties: {
3244
+ name: {
3245
+ type: "string",
3246
+ description: 'The skill name to load (kebab-case, e.g., "code-review", "test-runner").',
3247
+ minLength: 1,
3248
+ maxLength: 64
3249
+ }
3250
+ },
3251
+ required: ["name"],
3252
+ additionalProperties: false
3253
+ };
3254
+ provider;
3255
+ constructor(provider) {
3256
+ this.provider = provider;
3257
+ }
3258
+ validate(args) {
3259
+ if (typeof args !== "object" || args === null) {
3260
+ return { valid: false, errors: ["Arguments must be an object."] };
3261
+ }
3262
+ const { name } = args;
3263
+ const errors = [];
3264
+ if (typeof name !== "string" || name.trim().length === 0) {
3265
+ errors.push("`name` is required and must be a non-empty string.");
3266
+ }
3267
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
3268
+ }
3269
+ async execute(args, _context) {
3270
+ const { name } = args;
3271
+ const trimmedName = name.trim();
3272
+ if (!this.provider.has(trimmedName)) {
3273
+ const available = this.provider.names();
3274
+ const suggestion = available.length > 0 ? `Available skills: ${available.join(", ")}` : "No skills are currently loaded.";
3275
+ return {
3276
+ success: false,
3277
+ output: "",
3278
+ error: `Skill "${trimmedName}" not found. ${suggestion}`
3279
+ };
3280
+ }
3281
+ const body = this.provider.getSkillContext(trimmedName);
3282
+ if (!body) {
3283
+ return {
3284
+ success: false,
3285
+ output: "",
3286
+ error: `Skill "${trimmedName}" exists but has no content.`
3287
+ };
3288
+ }
3289
+ return {
3290
+ success: true,
3291
+ output: `# Skill: ${trimmedName}
3292
+
3293
+ ${body}`,
3294
+ metadata: {
3295
+ skillName: trimmedName,
3296
+ bodyLength: body.length
3297
+ }
3298
+ };
3299
+ }
3300
+ };
3301
+ var ToolRegistry = class _ToolRegistry {
3302
+ tools = /* @__PURE__ */ new Map();
3303
+ /**
3304
+ * Register a tool. Throws if a tool with the same name is already registered.
3305
+ */
3306
+ register(tool) {
3307
+ if (this.tools.has(tool.name)) {
3308
+ throw new ToolError(
3309
+ `Tool "${tool.name}" is already registered.`,
3310
+ tool.name
3311
+ );
3312
+ }
3313
+ this.tools.set(tool.name, tool);
3314
+ }
3315
+ /**
3316
+ * Get a tool by name. Returns undefined if not found.
3317
+ */
3318
+ get(name) {
3319
+ return this.tools.get(name);
3320
+ }
3321
+ /**
3322
+ * Check whether a tool is registered.
3323
+ */
3324
+ has(name) {
3325
+ return this.tools.has(name);
3326
+ }
3327
+ /**
3328
+ * List all registered tools.
3329
+ */
3330
+ list() {
3331
+ return Array.from(this.tools.values());
3332
+ }
3333
+ /**
3334
+ * List tool names.
3335
+ */
3336
+ names() {
3337
+ return Array.from(this.tools.keys());
3338
+ }
3339
+ /**
3340
+ * Get tools filtered by weight classification.
3341
+ */
3342
+ byWeight(weight) {
3343
+ return this.list().filter((t) => t.weight === weight);
3344
+ }
3345
+ /**
3346
+ * Unregister a tool by name. Returns true if removed, false if not found.
3347
+ */
3348
+ unregister(name) {
3349
+ return this.tools.delete(name);
3350
+ }
3351
+ /**
3352
+ * Convert all registered tools to ToolDefinition[] for LLM consumption.
3353
+ * This is the format expected by provider stream/complete calls.
3354
+ */
3355
+ getToolDefinitions(filterNames) {
3356
+ const tools = filterNames ? this.list().filter((t) => filterNames.includes(t.name)) : this.list();
3357
+ return tools.map((tool) => ({
3358
+ name: tool.name,
3359
+ description: tool.description,
3360
+ parameters: tool.parameters
3361
+ }));
3362
+ }
3363
+ /**
3364
+ * Get the count of registered tools.
3365
+ */
3366
+ get size() {
3367
+ return this.tools.size;
3368
+ }
3369
+ /**
3370
+ * Create a registry with the default set of built-in tools.
3371
+ *
3372
+ * Options:
3373
+ * - exclude: tool names to exclude from the default set
3374
+ * - include: if provided, only these tools are included (overrides exclude)
3375
+ */
3376
+ static createDefault(opts) {
3377
+ const registry = new _ToolRegistry();
3378
+ const allTools = [
3379
+ new BashTool(),
3380
+ new FileReadTool(),
3381
+ new FileWriteTool(),
3382
+ new FileEditTool(),
3383
+ new GrepTool(),
3384
+ new GlobTool(),
3385
+ new WebFetchTool(),
3386
+ new WebSearchTool(),
3387
+ new MemoryStoreTool(),
3388
+ new MemoryRecallTool(),
3389
+ new DelegateTool(),
3390
+ new MeshTool(),
3391
+ new BrowserTool()
3392
+ ];
3393
+ for (const tool of allTools) {
3394
+ if (opts?.include) {
3395
+ if (opts.include.includes(tool.name)) {
3396
+ registry.register(tool);
3397
+ }
3398
+ } else if (opts?.exclude) {
3399
+ if (!opts.exclude.includes(tool.name)) {
3400
+ registry.register(tool);
3401
+ }
3402
+ } else {
3403
+ registry.register(tool);
3404
+ }
3405
+ }
3406
+ return registry;
3407
+ }
3408
+ };
3409
+
3410
+ export {
3411
+ LoadSkillTool,
3412
+ ToolRegistry
3413
+ };