@kb-labs/agent-tools 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3983 @@
1
+ import * as fs2 from 'fs';
2
+ import * as path4 from 'path';
3
+ import path4__default from 'path';
4
+ import * as crypto from 'crypto';
5
+ import { glob } from 'glob';
6
+ import { execSync } from 'child_process';
7
+ import { useLLM } from '@kb-labs/sdk';
8
+ import * as readline from 'readline';
9
+ import { useLLM as useLLM$1 } from '@kb-labs/sdk/hooks';
10
+
11
+ // src/registry.ts
12
+ var ToolRegistry = class {
13
+ tools = /* @__PURE__ */ new Map();
14
+ context;
15
+ constructor(context) {
16
+ this.context = context;
17
+ }
18
+ /**
19
+ * Register a tool
20
+ */
21
+ register(tool) {
22
+ this.tools.set(tool.definition.function.name, tool);
23
+ }
24
+ /**
25
+ * Get tool by name
26
+ */
27
+ get(name) {
28
+ return this.tools.get(name);
29
+ }
30
+ /**
31
+ * Get all tool definitions for LLM
32
+ */
33
+ getDefinitions() {
34
+ return Array.from(this.tools.values()).map((t) => t.definition);
35
+ }
36
+ /**
37
+ * Execute a tool with automatic required-argument validation.
38
+ * Returns a structured error if required args are missing/null/undefined,
39
+ * so the agent understands what went wrong and can correct its call.
40
+ */
41
+ async execute(name, input) {
42
+ const tool = this.tools.get(name);
43
+ if (!tool) {
44
+ return {
45
+ success: false,
46
+ output: `UNKNOWN_TOOL: "${name}" is not a registered tool. Available tools: ${this.getToolNames().join(", ")}`
47
+ };
48
+ }
49
+ const required = tool.definition.function.parameters?.required ?? [];
50
+ const missing = required.filter(
51
+ (key) => input[key] === void 0 || input[key] === null
52
+ );
53
+ if (missing.length > 0) {
54
+ const params = tool.definition.function.parameters?.properties ?? {};
55
+ const details = missing.map((k) => {
56
+ const desc = params[k]?.description ?? "";
57
+ return ` - ${k}${desc ? ": " + desc : ""}`;
58
+ }).join("\n");
59
+ return {
60
+ success: false,
61
+ output: `MISSING_REQUIRED_ARGUMENTS for tool "${name}":
62
+ ${details}
63
+
64
+ Hint: if a string argument is missing, your response may have been cut off by the output token limit (stop_reason=max_tokens). For large content, write in multiple smaller calls instead of one large call.`
65
+ };
66
+ }
67
+ return tool.executor(input);
68
+ }
69
+ /**
70
+ * Get sorted list of registered tool names
71
+ */
72
+ getToolNames() {
73
+ return Array.from(this.tools.keys()).sort();
74
+ }
75
+ /**
76
+ * Get context
77
+ */
78
+ getContext() {
79
+ return this.context;
80
+ }
81
+ };
82
+
83
+ // src/tools/shared/tool-error.ts
84
+ function toolError(input) {
85
+ const parts = [`${input.code}: ${input.message}`];
86
+ if (input.hint) {
87
+ parts.push(`
88
+ Next step: ${input.hint}`);
89
+ }
90
+ if (input.retryable === false) {
91
+ parts.push("(This error cannot be resolved by retrying the same call.)");
92
+ }
93
+ const error = parts.join("\n");
94
+ return {
95
+ success: false,
96
+ error,
97
+ errorDetails: {
98
+ code: input.code,
99
+ message: input.message,
100
+ retryable: input.retryable,
101
+ hint: input.hint,
102
+ details: input.details
103
+ },
104
+ metadata: {
105
+ errorCode: input.code,
106
+ retryable: input.retryable ?? false,
107
+ ...input.details || {}
108
+ }
109
+ };
110
+ }
111
+
112
+ // src/config.ts
113
+ var FILESYSTEM_CONFIG = {
114
+ /** Hard cap on file size to read (500KB) — prevents context overflow */
115
+ maxFileSize: 5e5,
116
+ /** Hard cap on lines returned per read — prevents context overflow */
117
+ maxLinesPerRead: 2e3,
118
+ /** Default lines to return when not specified — read whole file up to maxLinesPerRead */
119
+ defaultLines: 2e3,
120
+ /** Hard cap on content size for write operations (1MB) */
121
+ maxWriteSize: 1e6,
122
+ /** Default limit for directory listing */
123
+ defaultListLimit: 50,
124
+ /** Maximum limit for directory listing */
125
+ maxListLimit: 200,
126
+ /** Max output characters before trimming (prevents context explosion) */
127
+ maxOutputChars: 12e3
128
+ };
129
+ var SEARCH_CONFIG = {
130
+ /** Timeout for search commands (15s) — fail fast, don't burn iterations */
131
+ timeoutMs: 15e3,
132
+ /** Max stdout buffer for search commands (8MB) */
133
+ maxBuffer: 8 * 1024 * 1024,
134
+ /** Default result limit — enough for actionable results */
135
+ defaultResultLimit: 50,
136
+ /** Maximum result limit */
137
+ maxResultLimit: 200,
138
+ /** Max output characters before trimming */
139
+ maxOutputChars: 8e3,
140
+ /**
141
+ * Directories excluded from search by default.
142
+ * These are never useful for code understanding and often huge.
143
+ * Pass exclude=[] to override entirely, or exclude=[...custom] to replace the list.
144
+ */
145
+ defaultExcludes: [
146
+ "node_modules",
147
+ ".git",
148
+ "dist",
149
+ "build",
150
+ ".next",
151
+ ".kb",
152
+ ".pnpm",
153
+ "coverage",
154
+ "__pycache__",
155
+ ".venv",
156
+ ".cache"
157
+ ]
158
+ };
159
+ var SHELL_CONFIG = {
160
+ /** Max stdout buffer — aligned with SEARCH_CONFIG (16MB) */
161
+ maxBuffer: 16 * 1024 * 1024
162
+ };
163
+ var TODO_CONFIG = {
164
+ /** Cache key prefix for todo lists */
165
+ cachePrefix: "agent:todo:",
166
+ /** TTL for cached todo lists (7 days) */
167
+ cacheTtlMs: 7 * 24 * 60 * 60 * 1e3
168
+ };
169
+ var DELEGATION_CONFIG = {
170
+ /** Default max iterations for spawned sub-agents (safety net — token budget is primary control) */
171
+ defaultMaxIterations: 100,
172
+ /** Default budget fraction (50% of parent remaining — sub-agents need real budget) */
173
+ defaultBudgetFraction: 0.5
174
+ };
175
+ var SUB_AGENT_PRESETS = {
176
+ /** Read-only exploration and evidence gathering. */
177
+ research: {
178
+ tools: /* @__PURE__ */ new Set([
179
+ "fs_read",
180
+ "fs_list",
181
+ "glob_search",
182
+ "grep_search",
183
+ "find_definition",
184
+ "code_stats",
185
+ "memory_get",
186
+ "memory_finding",
187
+ "archive_recall",
188
+ "todo_get",
189
+ "report"
190
+ ]),
191
+ maxIterations: 50
192
+ },
193
+ /** Full read/write capabilities for implementation tasks. */
194
+ execute: {
195
+ tools: /* @__PURE__ */ new Set([
196
+ "fs_read",
197
+ "fs_write",
198
+ "fs_patch",
199
+ "fs_replace",
200
+ "fs_list",
201
+ "mass_replace",
202
+ "glob_search",
203
+ "grep_search",
204
+ "find_definition",
205
+ "code_stats",
206
+ "shell_exec",
207
+ "memory_get",
208
+ "memory_finding",
209
+ "memory_blocker",
210
+ "memory_correction",
211
+ "todo_create",
212
+ "todo_update",
213
+ "todo_get",
214
+ "plan_write",
215
+ "ask_user",
216
+ "report"
217
+ ]),
218
+ maxIterations: 100
219
+ },
220
+ /** Code review and verification: read + shell (linters, tests). */
221
+ review: {
222
+ tools: /* @__PURE__ */ new Set([
223
+ "fs_read",
224
+ "fs_list",
225
+ "glob_search",
226
+ "grep_search",
227
+ "find_definition",
228
+ "code_stats",
229
+ "shell_exec",
230
+ "memory_get",
231
+ "memory_finding",
232
+ "archive_recall",
233
+ "todo_get",
234
+ "report"
235
+ ]),
236
+ maxIterations: 50
237
+ },
238
+ /**
239
+ * Adversarial verification: independent agent that checks another agent's work.
240
+ * Has read + shell access to run tests, check imports, verify builds.
241
+ * Reports PASS/FAIL verdict with evidence.
242
+ */
243
+ verification: {
244
+ tools: /* @__PURE__ */ new Set([
245
+ "fs_read",
246
+ "fs_list",
247
+ "glob_search",
248
+ "grep_search",
249
+ "find_definition",
250
+ "code_stats",
251
+ "shell_exec",
252
+ "report"
253
+ ]),
254
+ maxIterations: 25
255
+ }
256
+ };
257
+ var MASS_REPLACE_CONFIG = {
258
+ /** Hard cap on file size to process (1MB) — same as maxWriteSize */
259
+ maxFileSize: 1e6,
260
+ /** Maximum number of files to process in one operation */
261
+ maxFiles: 100
262
+ };
263
+ var PLAN_READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
264
+ "fs_read",
265
+ "fs_list",
266
+ "glob_search",
267
+ "grep_search",
268
+ "find_definition",
269
+ "code_stats",
270
+ "memory_get",
271
+ "memory_finding",
272
+ "memory_blocker",
273
+ "archive_recall",
274
+ "todo_create",
275
+ "todo_update",
276
+ "todo_get",
277
+ "ask_user",
278
+ "report",
279
+ "task_submit",
280
+ // plan-writer can delegate research to async sub-agents
281
+ "task_status",
282
+ // check progress of delegated tasks
283
+ "task_collect",
284
+ // wait for and collect sub-agent results
285
+ "plan_validate",
286
+ // LLM-based plan quality gate (agent calls this to self-assess before report)
287
+ "plan_write"
288
+ // Write/update plan file on disk (iterative plan building)
289
+ ]);
290
+ var SOURCE_FILE_EXTENSIONS = {
291
+ typescript: ["ts", "tsx"],
292
+ javascript: ["js", "jsx"],
293
+ python: ["py"],
294
+ csharp: ["cs"],
295
+ java: ["java"],
296
+ go: ["go"],
297
+ rust: ["rs"],
298
+ ruby: ["rb"],
299
+ php: ["php"],
300
+ swift: ["swift"],
301
+ kotlin: ["kt"],
302
+ scala: ["scala"],
303
+ cpp: ["cpp", "c", "h", "cc", "cxx"]
304
+ };
305
+ var ALL_SOURCE_EXTENSIONS = Object.values(SOURCE_FILE_EXTENSIONS).flat();
306
+ function toRgIncludes(exts) {
307
+ return exts.map((e) => `--include="*.${e}"`).join(" ");
308
+ }
309
+ function toFindNames(exts) {
310
+ return exts.map((e) => `-name "*.${e}"`).join(" -o ");
311
+ }
312
+ function normalizeOffsetLimit(input, config) {
313
+ const rawOffset = Number(input.offset);
314
+ const rawLimit = Number(input.limit);
315
+ const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? Math.floor(rawOffset) : 0;
316
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(config.maxLimit, Math.floor(rawLimit)) : config.defaultLimit;
317
+ return { offset, limit };
318
+ }
319
+ function validatePath(workingDir, filePath) {
320
+ let resolved = path4.resolve(workingDir, filePath);
321
+ try {
322
+ if (fs2.existsSync(resolved)) {
323
+ resolved = fs2.realpathSync(resolved);
324
+ }
325
+ } catch {
326
+ }
327
+ const relative3 = path4.relative(workingDir, resolved);
328
+ if (relative3.startsWith("..")) {
329
+ return {
330
+ valid: false,
331
+ resolved,
332
+ error: `PATH_TRAVERSAL_ERROR: Cannot access "${filePath}" - path is outside working directory.
333
+
334
+ HOW TO FIX: Use paths relative to the working directory. Do not use ".." to navigate above it.
335
+ WORKING_DIR: ${workingDir}
336
+ ATTEMPTED_PATH: ${resolved}`
337
+ };
338
+ }
339
+ return { valid: true, resolved };
340
+ }
341
+ function suggestDirectory(workingDir, requestedDir) {
342
+ const clean = requestedDir.replace(/^\.\//, "").replace(/\/$/, "");
343
+ if (!clean || clean === ".") {
344
+ return null;
345
+ }
346
+ const parts = clean.split("/");
347
+ if (parts.length < 2) {
348
+ return null;
349
+ }
350
+ const candidate = `${parts[0]}-${parts[1]}`;
351
+ const rest = parts.slice(2).join("/");
352
+ const fullCandidate = rest ? path4.join(candidate, rest) : candidate;
353
+ const resolved = path4.resolve(workingDir, fullCandidate);
354
+ if (fs2.existsSync(resolved) && fs2.statSync(resolved).isDirectory()) {
355
+ return fullCandidate;
356
+ }
357
+ return null;
358
+ }
359
+
360
+ // src/tools/filesystem/filesystem.ts
361
+ var MAX_FILE_SIZE = FILESYSTEM_CONFIG.maxFileSize;
362
+ var MAX_LINES_PER_READ = FILESYSTEM_CONFIG.maxLinesPerRead;
363
+ var DEFAULT_LINES = FILESYSTEM_CONFIG.defaultLines;
364
+ var MAX_WRITE_SIZE = FILESYSTEM_CONFIG.maxWriteSize;
365
+ var DEFAULT_LIST_LIMIT = FILESYSTEM_CONFIG.defaultListLimit;
366
+ var MAX_LIST_LIMIT = FILESYSTEM_CONFIG.maxListLimit;
367
+ var MAX_OUTPUT_CHARS = FILESYSTEM_CONFIG.maxOutputChars;
368
+ function trimOutput(output, maxChars, continuationHint) {
369
+ if (output.length <= maxChars) {
370
+ return output;
371
+ }
372
+ const trimmed = output.slice(0, maxChars);
373
+ return `${trimmed}
374
+
375
+ \u26A0\uFE0F OUTPUT TRIMMED (${output.length.toLocaleString()} chars \u2192 ${maxChars.toLocaleString()} shown)
376
+ ${continuationHint}`;
377
+ }
378
+ function formatSize(bytes) {
379
+ if (bytes < 1024) {
380
+ return `${bytes} bytes`;
381
+ }
382
+ if (bytes < 1024 * 1024) {
383
+ return `${(bytes / 1024).toFixed(1)} KB`;
384
+ }
385
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
386
+ }
387
+ function computeHash(content) {
388
+ return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
389
+ }
390
+ function normalizeListWindow(input) {
391
+ return normalizeOffsetLimit(input, { defaultLimit: DEFAULT_LIST_LIMIT, maxLimit: MAX_LIST_LIMIT });
392
+ }
393
+ function createFsReadTool(context) {
394
+ return {
395
+ definition: {
396
+ type: "function",
397
+ function: {
398
+ name: "fs_read",
399
+ description: `Read file contents. By default reads the entire file (up to ${MAX_LINES_PER_READ} lines). For very large files, use offset+limit to read specific sections. Prefer reading the whole file when possible \u2014 partial reads waste iterations.`,
400
+ parameters: {
401
+ type: "object",
402
+ properties: {
403
+ path: {
404
+ type: "string",
405
+ description: "File path relative to working directory"
406
+ },
407
+ offset: {
408
+ type: "number",
409
+ description: "Line number to start from (1-indexed, default: 1)"
410
+ },
411
+ limit: {
412
+ type: "number",
413
+ description: `Number of lines to read (default: all, max: ${MAX_LINES_PER_READ}). Only use for very large files \u2014 prefer reading the whole file.`
414
+ }
415
+ },
416
+ required: ["path"]
417
+ }
418
+ }
419
+ },
420
+ executor: async (input) => {
421
+ const filePath = input.path;
422
+ const offset = Math.max(1, input.offset || 1);
423
+ const requestedLimit = input.limit || DEFAULT_LINES;
424
+ const limit = Math.min(requestedLimit, MAX_LINES_PER_READ);
425
+ const pathValidation = validatePath(context.workingDir, filePath);
426
+ if (!pathValidation.valid) {
427
+ return toolError({
428
+ code: "PATH_VALIDATION_FAILED",
429
+ message: pathValidation.error || "Invalid path",
430
+ retryable: false,
431
+ hint: "Use a path inside working directory.",
432
+ details: { filePath, workingDir: context.workingDir }
433
+ });
434
+ }
435
+ const fullPath = pathValidation.resolved;
436
+ if (!fs2.existsSync(fullPath)) {
437
+ return toolError({
438
+ code: "FILE_NOT_FOUND",
439
+ message: `"${filePath}" does not exist.`,
440
+ retryable: true,
441
+ hint: `Use fs_list(path="${path4.dirname(filePath) || "."}") to inspect available files.`,
442
+ details: { filePath, workingDir: context.workingDir }
443
+ });
444
+ }
445
+ const stats = fs2.statSync(fullPath);
446
+ if (stats.isDirectory()) {
447
+ return toolError({
448
+ code: "NOT_A_FILE",
449
+ message: `"${filePath}" is a directory, not a file.`,
450
+ retryable: false,
451
+ hint: "Use fs_list for directories or provide a file path.",
452
+ details: { filePath }
453
+ });
454
+ }
455
+ if (stats.size > MAX_FILE_SIZE) {
456
+ return toolError({
457
+ code: "FILE_TOO_LARGE",
458
+ message: `"${filePath}" is ${formatSize(stats.size)}, exceeds limit ${formatSize(MAX_FILE_SIZE)}.`,
459
+ retryable: true,
460
+ hint: `Use fs_read(path="${filePath}", offset=<line>, limit=<small number>).`,
461
+ details: { filePath, size: stats.size, maxSize: MAX_FILE_SIZE }
462
+ });
463
+ }
464
+ const content = fs2.readFileSync(fullPath, "utf-8");
465
+ const allLines = content.split("\n");
466
+ const totalLines = allLines.length;
467
+ if (offset > totalLines) {
468
+ return toolError({
469
+ code: "OFFSET_OUT_OF_RANGE",
470
+ message: `Requested offset ${offset} but file has ${totalLines} lines.`,
471
+ retryable: true,
472
+ hint: `Use offset between 1 and ${totalLines}.`,
473
+ details: { filePath, offset, totalLines }
474
+ });
475
+ }
476
+ const startIndex = offset - 1;
477
+ const endIndex = Math.min(startIndex + limit, totalLines);
478
+ const selectedLines = allLines.slice(startIndex, endIndex);
479
+ const numberedLines = selectedLines.map((line, i) => {
480
+ const lineNum = (startIndex + i + 1).toString().padStart(5, " ");
481
+ return `${lineNum}\u2192${line}`;
482
+ });
483
+ const hasMore = endIndex < totalLines;
484
+ const linesRemaining = totalLines - endIndex;
485
+ const warnings = [];
486
+ if (requestedLimit > MAX_LINES_PER_READ) {
487
+ warnings.push(`\u26A0\uFE0F Limit capped at ${MAX_LINES_PER_READ} (you requested ${requestedLimit})`);
488
+ }
489
+ if (hasMore) {
490
+ warnings.push(`\u26A0\uFE0F PARTIAL READ: ${linesRemaining} lines unread. Next chunk: fs_read(path="${filePath}", offset=${endIndex + 1})`);
491
+ }
492
+ const header = [
493
+ `File: ${filePath}`,
494
+ `Lines: ${offset}-${endIndex} of ${totalLines}`,
495
+ hasMore ? `(${Math.round((endIndex - offset + 1) / totalLines * 100)}% read so far \u2014 ${linesRemaining} lines remain)` : "(end of file \u2713)",
496
+ ...warnings,
497
+ "\u2500".repeat(60)
498
+ ].filter(Boolean).join("\n");
499
+ const rawOutput = `${header}
500
+ ${numberedLines.join("\n")}`;
501
+ const continuationHint = hasMore ? `Next chunk: fs_read(path="${filePath}", offset=${endIndex + 1})` : "";
502
+ const hash = computeHash(content);
503
+ context.filesRead?.add(filePath);
504
+ context.filesReadHash?.set(filePath, hash);
505
+ return {
506
+ success: true,
507
+ output: trimOutput(rawOutput, MAX_OUTPUT_CHARS, continuationHint),
508
+ metadata: {
509
+ filePath,
510
+ totalLines,
511
+ readFrom: offset,
512
+ readTo: endIndex,
513
+ hasMore,
514
+ fileSize: stats.size,
515
+ contentHash: hash
516
+ }
517
+ };
518
+ }
519
+ };
520
+ }
521
+ function createFsWriteTool(context) {
522
+ return {
523
+ definition: {
524
+ type: "function",
525
+ function: {
526
+ name: "fs_write",
527
+ description: `Write a new file or completely rewrite an existing one. Creates parent directories if needed. IMPORTANT: Prefer fs_replace for modifying existing files \u2014 it only sends the diff. Only use fs_write to create NEW files or for complete rewrites where the entire content changes. If you're modifying an existing file, use fs_replace instead.`,
528
+ parameters: {
529
+ type: "object",
530
+ properties: {
531
+ path: {
532
+ type: "string",
533
+ description: "File path relative to working directory"
534
+ },
535
+ content: {
536
+ type: "string",
537
+ description: "Content to write to the file"
538
+ }
539
+ },
540
+ required: ["path", "content"]
541
+ }
542
+ }
543
+ },
544
+ executor: async (input) => {
545
+ const filePath = input.path;
546
+ const content = input.content;
547
+ const pathValidation = validatePath(context.workingDir, filePath);
548
+ if (!pathValidation.valid) {
549
+ return toolError({
550
+ code: "PATH_VALIDATION_FAILED",
551
+ message: pathValidation.error || "Invalid path",
552
+ retryable: false,
553
+ hint: "Use a path inside working directory.",
554
+ details: { filePath, workingDir: context.workingDir }
555
+ });
556
+ }
557
+ const fullPath = pathValidation.resolved;
558
+ const contentSize = Buffer.byteLength(content, "utf-8");
559
+ if (contentSize > MAX_WRITE_SIZE) {
560
+ return {
561
+ success: false,
562
+ error: `CONTENT_TOO_LARGE: Content is ${formatSize(contentSize)}, exceeds limit of ${formatSize(MAX_WRITE_SIZE)}.
563
+
564
+ HOW TO FIX: Split the content into smaller files, or write in chunks.`
565
+ };
566
+ }
567
+ const isOverwrite = fs2.existsSync(fullPath);
568
+ const beforeContent = isOverwrite ? fs2.readFileSync(fullPath, "utf-8") : void 0;
569
+ const dir = path4.dirname(fullPath);
570
+ if (!fs2.existsSync(dir)) {
571
+ fs2.mkdirSync(dir, { recursive: true });
572
+ }
573
+ fs2.writeFileSync(fullPath, content, "utf-8");
574
+ const writeHash = computeHash(content);
575
+ context.filesRead?.add(filePath);
576
+ context.filesReadHash?.set(filePath, writeHash);
577
+ const lineCount = content.split("\n").length;
578
+ return {
579
+ success: true,
580
+ output: `${isOverwrite ? "Overwrote" : "Created"} file: ${filePath}
581
+ Size: ${formatSize(contentSize)}
582
+ Lines: ${lineCount}`,
583
+ metadata: {
584
+ filePath,
585
+ fileContent: content,
586
+ size: contentSize,
587
+ lines: lineCount,
588
+ isOverwrite,
589
+ uiHint: "code",
590
+ // Consumed by ChangeTrackingMiddleware
591
+ changeSnapshot: {
592
+ operation: "write",
593
+ beforeContent,
594
+ afterContent: content,
595
+ isOverwrite
596
+ }
597
+ }
598
+ };
599
+ }
600
+ };
601
+ }
602
+ function createFsPatchTool(context) {
603
+ return {
604
+ definition: {
605
+ type: "function",
606
+ function: {
607
+ name: "fs_patch",
608
+ description: `Replace a range of lines by line numbers. Use fs_replace instead when possible \u2014 it's more reliable. Only use fs_patch for large block replacements where fs_replace match text would be too long. You must fs_read the file first. Line numbers are 1-indexed and inclusive.`,
609
+ parameters: {
610
+ type: "object",
611
+ properties: {
612
+ path: {
613
+ type: "string",
614
+ description: "File path relative to working directory"
615
+ },
616
+ startLine: {
617
+ type: "number",
618
+ description: "First line to replace (1-indexed, inclusive)"
619
+ },
620
+ endLine: {
621
+ type: "number",
622
+ description: "Last line to replace (1-indexed, inclusive)"
623
+ },
624
+ newContent: {
625
+ type: "string",
626
+ description: "New content to replace the line range with"
627
+ }
628
+ },
629
+ required: ["path", "startLine", "endLine", "newContent"]
630
+ }
631
+ }
632
+ },
633
+ executor: async (input) => {
634
+ const filePath = input.path;
635
+ const startLine = input.startLine;
636
+ const endLine = input.endLine;
637
+ const newContent = input.newContent;
638
+ const pathValidation = validatePath(context.workingDir, filePath);
639
+ if (!pathValidation.valid) {
640
+ return { success: false, error: pathValidation.error };
641
+ }
642
+ const fullPath = pathValidation.resolved;
643
+ if (context.filesRead && !context.filesRead.has(filePath)) {
644
+ return toolError({
645
+ code: "CANNOT_EDIT_UNREAD_FILE",
646
+ message: `File has not been read yet. You must read a file before editing it.`,
647
+ retryable: true,
648
+ hint: `Call fs_read(path="${filePath}") to read the file first, then retry this edit.`,
649
+ details: { filePath, startLine, endLine }
650
+ });
651
+ }
652
+ if (!fs2.existsSync(fullPath)) {
653
+ return toolError({
654
+ code: "FILE_NOT_FOUND",
655
+ message: `"${filePath}" does not exist.`,
656
+ retryable: true,
657
+ hint: "Use fs_list to find files or fs_write to create a new file.",
658
+ details: { filePath }
659
+ });
660
+ }
661
+ const currentContent = fs2.readFileSync(fullPath, "utf-8");
662
+ if (context.filesReadHash) {
663
+ const savedHash = context.filesReadHash.get(filePath);
664
+ const currentHash = computeHash(currentContent);
665
+ if (savedHash && savedHash !== currentHash) {
666
+ return toolError({
667
+ code: "FILE_CHANGED_SINCE_READ",
668
+ message: `File was modified since you last read it (possibly by a linter, formatter, or another tool).`,
669
+ retryable: true,
670
+ hint: `Call fs_read(path="${filePath}") to get the current content, then retry the edit with updated line numbers.`,
671
+ details: { filePath, startLine, endLine }
672
+ });
673
+ }
674
+ }
675
+ const allLines = currentContent.split("\n");
676
+ const totalLines = allLines.length;
677
+ if (startLine < 1 || startLine > totalLines) {
678
+ return toolError({
679
+ code: "INVALID_START_LINE",
680
+ message: `startLine ${startLine} is out of range (file has ${totalLines} lines).`,
681
+ retryable: true,
682
+ hint: `Use startLine between 1 and ${totalLines}.`,
683
+ details: { filePath, startLine, totalLines }
684
+ });
685
+ }
686
+ if (endLine < startLine || endLine > totalLines) {
687
+ return toolError({
688
+ code: "INVALID_END_LINE",
689
+ message: `endLine ${endLine} is invalid (startLine=${startLine}, file has ${totalLines} lines).`,
690
+ retryable: true,
691
+ hint: `Use endLine between ${startLine} and ${totalLines}.`,
692
+ details: { filePath, startLine, endLine, totalLines }
693
+ });
694
+ }
695
+ const beforeLines = allLines.slice(0, startLine - 1);
696
+ const afterLines = allLines.slice(endLine);
697
+ const newLines = newContent ? newContent.split("\n") : [];
698
+ const patchedContent = [...beforeLines, ...newLines, ...afterLines].join("\n");
699
+ const removedCount = endLine - startLine + 1;
700
+ const addedCount = newLines.length;
701
+ const netChange = addedCount - removedCount;
702
+ fs2.writeFileSync(fullPath, patchedContent, "utf-8");
703
+ const patchedHash = computeHash(patchedContent);
704
+ context.filesRead?.add(filePath);
705
+ context.filesReadHash?.set(filePath, patchedHash);
706
+ let action;
707
+ if (addedCount === 0) {
708
+ action = `Deleted ${removedCount} line(s)`;
709
+ } else if (removedCount === addedCount) {
710
+ action = `Replaced ${removedCount} line(s)`;
711
+ } else if (netChange > 0) {
712
+ action = `Replaced ${removedCount} line(s) with ${addedCount} line(s) (+${netChange})`;
713
+ } else {
714
+ action = `Replaced ${removedCount} line(s) with ${addedCount} line(s) (${netChange})`;
715
+ }
716
+ return {
717
+ success: true,
718
+ output: `${action} in ${filePath}
719
+ Lines ${startLine}-${endLine} \u2192 ${addedCount} new line(s)
720
+ File now has ${beforeLines.length + newLines.length + afterLines.length} lines (was ${totalLines})`,
721
+ metadata: {
722
+ filePath,
723
+ startLine,
724
+ endLine,
725
+ linesRemoved: removedCount,
726
+ linesAdded: addedCount,
727
+ netChange,
728
+ totalLinesBefore: totalLines,
729
+ totalLinesAfter: beforeLines.length + newLines.length + afterLines.length,
730
+ // Consumed by ChangeTrackingMiddleware
731
+ changeSnapshot: {
732
+ operation: "patch",
733
+ beforeContent: currentContent,
734
+ afterContent: patchedContent,
735
+ startLine,
736
+ endLine,
737
+ linesAdded: addedCount,
738
+ linesRemoved: removedCount
739
+ }
740
+ }
741
+ };
742
+ }
743
+ };
744
+ }
745
+ function createFsReplaceTool(context) {
746
+ return {
747
+ definition: {
748
+ type: "function",
749
+ function: {
750
+ name: "fs_replace",
751
+ description: `PREFERRED tool for editing existing files. Finds exact text and replaces it \u2014 only sends the diff, not the whole file. You must fs_read the file first. Use the smallest match text that uniquely identifies the target \u2014 usually 2-4 lines is enough. The match must appear exactly once (unless replace_all=true). If multiple matches, add more surrounding context to make it unique.`,
752
+ parameters: {
753
+ type: "object",
754
+ properties: {
755
+ path: {
756
+ type: "string",
757
+ description: "File path relative to working directory"
758
+ },
759
+ match: {
760
+ type: "string",
761
+ description: "Exact text to find in the file (copy from fs_read output)"
762
+ },
763
+ replacement: {
764
+ type: "string",
765
+ description: "Text to replace the match with"
766
+ },
767
+ replace_all: {
768
+ type: "boolean",
769
+ description: "Replace all occurrences (default: false \u2014 requires unique match)"
770
+ }
771
+ },
772
+ required: ["path", "match", "replacement"]
773
+ }
774
+ }
775
+ },
776
+ executor: async (input) => {
777
+ const filePath = input.path;
778
+ const match = input.match;
779
+ const replacement = input.replacement;
780
+ const replaceAll = input.replace_all || false;
781
+ const pathValidation = validatePath(context.workingDir, filePath);
782
+ if (!pathValidation.valid) {
783
+ return toolError({
784
+ code: "PATH_VALIDATION_FAILED",
785
+ message: pathValidation.error || "Invalid path",
786
+ retryable: false,
787
+ hint: "Use a path inside working directory.",
788
+ details: { filePath, workingDir: context.workingDir }
789
+ });
790
+ }
791
+ const fullPath = pathValidation.resolved;
792
+ if (context.filesRead && !context.filesRead.has(filePath)) {
793
+ return toolError({
794
+ code: "CANNOT_EDIT_UNREAD_FILE",
795
+ message: "File has not been read yet. You must read a file before editing it.",
796
+ retryable: true,
797
+ hint: `Call fs_read(path="${filePath}") to read the file first, then retry this edit.`,
798
+ details: { filePath }
799
+ });
800
+ }
801
+ if (!fs2.existsSync(fullPath)) {
802
+ return toolError({
803
+ code: "FILE_NOT_FOUND",
804
+ message: `"${filePath}" does not exist.`,
805
+ retryable: true,
806
+ hint: "Use fs_list to find files or fs_write to create a new file.",
807
+ details: { filePath }
808
+ });
809
+ }
810
+ const currentContent = fs2.readFileSync(fullPath, "utf-8");
811
+ if (context.filesReadHash) {
812
+ const savedHash = context.filesReadHash.get(filePath);
813
+ const currentHash = computeHash(currentContent);
814
+ if (savedHash && savedHash !== currentHash) {
815
+ return toolError({
816
+ code: "FILE_CHANGED_SINCE_READ",
817
+ message: "File was modified since you last read it (possibly by a linter, formatter, or another tool).",
818
+ retryable: true,
819
+ hint: `Call fs_read(path="${filePath}") to get the current content, then retry the edit.`,
820
+ details: { filePath }
821
+ });
822
+ }
823
+ }
824
+ if (match === replacement) {
825
+ return toolError({
826
+ code: "NO_CHANGE",
827
+ message: "The match and replacement text are identical. No changes needed.",
828
+ retryable: false,
829
+ hint: "Provide different replacement text, or skip this edit if no change is needed.",
830
+ details: { filePath }
831
+ });
832
+ }
833
+ if (!currentContent.includes(match)) {
834
+ return toolError({
835
+ code: "MATCH_NOT_FOUND",
836
+ message: "The match text was not found in the file.",
837
+ retryable: true,
838
+ hint: `Call fs_read(path="${filePath}") to see the current file content, then copy the exact text you want to replace.`,
839
+ details: { filePath, matchLength: match.length }
840
+ });
841
+ }
842
+ const occurrences = currentContent.split(match).length - 1;
843
+ if (occurrences > 1 && !replaceAll) {
844
+ return toolError({
845
+ code: "AMBIGUOUS_MATCH",
846
+ message: `Found ${occurrences} occurrences of the match text, but replace_all is false.`,
847
+ retryable: true,
848
+ hint: `Include more surrounding context in the match text to make it unique (only 1 occurrence), or set replace_all=true to replace all ${occurrences} occurrences.`,
849
+ details: { filePath, occurrences }
850
+ });
851
+ }
852
+ const updatedContent = replaceAll ? currentContent.replaceAll(match, replacement) : currentContent.replace(match, replacement);
853
+ fs2.writeFileSync(fullPath, updatedContent, "utf-8");
854
+ const newHash = computeHash(updatedContent);
855
+ context.filesRead?.add(filePath);
856
+ context.filesReadHash?.set(filePath, newHash);
857
+ const matchLines = match.split("\n").length;
858
+ const replacementLines = replacement.split("\n").length;
859
+ const netChange = replacementLines - matchLines;
860
+ const action = replaceAll ? `Replaced ${occurrences} occurrence(s)` : "Replaced 1 occurrence";
861
+ return {
862
+ success: true,
863
+ output: `${action} in ${filePath}
864
+ Match: ${matchLines} line(s) \u2192 Replacement: ${replacementLines} line(s) (${netChange >= 0 ? "+" : ""}${netChange})
865
+ File now has ${updatedContent.split("\n").length} lines`,
866
+ metadata: {
867
+ filePath,
868
+ occurrences: replaceAll ? occurrences : 1,
869
+ matchLines,
870
+ replacementLines,
871
+ netChange,
872
+ totalLinesAfter: updatedContent.split("\n").length,
873
+ changeSnapshot: {
874
+ operation: "replace",
875
+ beforeContent: currentContent,
876
+ afterContent: updatedContent,
877
+ match,
878
+ replacement,
879
+ replaceAll
880
+ }
881
+ }
882
+ };
883
+ }
884
+ };
885
+ }
886
+ function createFsListTool(context) {
887
+ return {
888
+ definition: {
889
+ type: "function",
890
+ function: {
891
+ name: "fs_list",
892
+ description: `List files and directories at a path. Non-recursive by default. IMPORTANT: node_modules/, dist/, .git/, build/, .next/ are hidden by default \u2014 they contain no useful source code. Use include_ignored=true only if you specifically need to inspect build artifacts.`,
893
+ parameters: {
894
+ type: "object",
895
+ properties: {
896
+ path: {
897
+ type: "string",
898
+ description: 'Directory path relative to working directory (default: ".")'
899
+ },
900
+ recursive: {
901
+ type: "boolean",
902
+ description: "Include subdirectories recursively (default: false, max depth: 3)"
903
+ },
904
+ include_ignored: {
905
+ type: "boolean",
906
+ description: "Include normally-hidden dirs: node_modules, dist, build, .next, .git (default: false). Only use when you explicitly need to inspect build artifacts or dependencies."
907
+ },
908
+ offset: {
909
+ type: "number",
910
+ description: "Result offset for pagination (default: 0)"
911
+ },
912
+ limit: {
913
+ type: "number",
914
+ description: `Result limit per page (default: ${DEFAULT_LIST_LIMIT}, max: ${MAX_LIST_LIMIT})`
915
+ }
916
+ }
917
+ }
918
+ }
919
+ },
920
+ executor: async (input) => {
921
+ const dirPath = input.path || ".";
922
+ const recursive = input.recursive || false;
923
+ const includeIgnored = input.include_ignored || false;
924
+ const { offset, limit } = normalizeListWindow(input);
925
+ const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next", ".git", ".pnpm", ".cache", "coverage", "__pycache__"]);
926
+ const pathValidation = validatePath(context.workingDir, dirPath);
927
+ if (!pathValidation.valid) {
928
+ return toolError({
929
+ code: "PATH_VALIDATION_FAILED",
930
+ message: pathValidation.error || "Invalid path",
931
+ retryable: false,
932
+ hint: "Use a path inside working directory.",
933
+ details: { path: dirPath, workingDir: context.workingDir }
934
+ });
935
+ }
936
+ const fullPath = pathValidation.resolved;
937
+ if (!fs2.existsSync(fullPath)) {
938
+ const suggestion = suggestDirectory(context.workingDir, dirPath);
939
+ return toolError({
940
+ code: "DIRECTORY_NOT_FOUND",
941
+ message: `"${dirPath}" does not exist.`,
942
+ retryable: true,
943
+ hint: suggestion ? `Did you mean "${suggestion}"?` : "Use fs_list on parent directory to discover valid paths.",
944
+ details: { path: dirPath, workingDir: context.workingDir }
945
+ });
946
+ }
947
+ const stats = fs2.statSync(fullPath);
948
+ if (!stats.isDirectory()) {
949
+ return toolError({
950
+ code: "NOT_A_DIRECTORY",
951
+ message: `"${dirPath}" is a file, not a directory.`,
952
+ retryable: false,
953
+ hint: "Use fs_read for files or provide a directory path.",
954
+ details: { path: dirPath }
955
+ });
956
+ }
957
+ const entries = fs2.readdirSync(fullPath, { withFileTypes: true });
958
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name).filter((name) => !name.startsWith(".")).sort();
959
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => {
960
+ if (name.startsWith(".")) {
961
+ return false;
962
+ }
963
+ if (!includeIgnored && IGNORED_DIRS.has(name)) {
964
+ return false;
965
+ }
966
+ return true;
967
+ }).sort();
968
+ const hiddenDirCount = includeIgnored ? 0 : entries.filter((e) => e.isDirectory() && IGNORED_DIRS.has(e.name)).length;
969
+ const combinedEntries = [
970
+ ...dirs.map((name) => ({ type: "dir", name })),
971
+ ...files.map((name) => ({ type: "file", name }))
972
+ ];
973
+ const pageEntries = combinedEntries.slice(offset, offset + limit);
974
+ const hasMore = offset + limit < combinedEntries.length;
975
+ const nextOffset = hasMore ? offset + limit : null;
976
+ const pageDirs = pageEntries.filter((entry) => entry.type === "dir");
977
+ const pageFiles = pageEntries.filter((entry) => entry.type === "file");
978
+ const ignoredNote = hiddenDirCount > 0 ? `[${hiddenDirCount} ignored dir(s) hidden: node_modules/dist/build/etc \u2014 use include_ignored=true to show]` : "";
979
+ const outputParts = [
980
+ `\u{1F4C1} ${dirPath === "." ? "Working Directory" : dirPath} (showing ${pageEntries.length}/${combinedEntries.length}, offset=${offset}, limit=${limit})`,
981
+ ignoredNote,
982
+ ""
983
+ ].filter(Boolean);
984
+ if (pageDirs.length > 0) {
985
+ outputParts.push(`Directories (${dirs.length}):`);
986
+ for (const d of pageDirs) {
987
+ outputParts.push(` \u{1F4C1} ${d.name}/`);
988
+ }
989
+ outputParts.push("");
990
+ }
991
+ if (pageFiles.length > 0) {
992
+ outputParts.push(`Files (${files.length}):`);
993
+ for (const f of pageFiles) {
994
+ const filePath = path4.join(fullPath, f.name);
995
+ const fileStats = fs2.statSync(filePath);
996
+ outputParts.push(` \u{1F4C4} ${f.name} (${formatSize(fileStats.size)})`);
997
+ }
998
+ }
999
+ if (combinedEntries.length === 0) {
1000
+ outputParts.push("(empty directory)");
1001
+ }
1002
+ if (recursive && dirs.length > 0) {
1003
+ outputParts.push("");
1004
+ outputParts.push("\u2500".repeat(40));
1005
+ outputParts.push("Recursive structure (depth 2):");
1006
+ for (const d of dirs.slice(0, 10)) {
1007
+ outputParts.push(`
1008
+ \u{1F4C1} ${d}/`);
1009
+ const subPath = path4.join(fullPath, d);
1010
+ try {
1011
+ const subEntries = fs2.readdirSync(subPath, { withFileTypes: true });
1012
+ const subDirs = subEntries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules");
1013
+ const subFiles = subEntries.filter((e) => e.isFile() && !e.name.startsWith("."));
1014
+ for (const sd of subDirs.slice(0, 5)) {
1015
+ outputParts.push(` \u{1F4C1} ${sd.name}/`);
1016
+ }
1017
+ if (subDirs.length > 5) {
1018
+ outputParts.push(` ... and ${subDirs.length - 5} more directories`);
1019
+ }
1020
+ for (const sf of subFiles.slice(0, 5)) {
1021
+ outputParts.push(` \u{1F4C4} ${sf.name}`);
1022
+ }
1023
+ if (subFiles.length > 5) {
1024
+ outputParts.push(` ... and ${subFiles.length - 5} more files`);
1025
+ }
1026
+ } catch {
1027
+ outputParts.push(" (unable to read)");
1028
+ }
1029
+ }
1030
+ if (dirs.length > 10) {
1031
+ outputParts.push(`
1032
+ ... and ${dirs.length - 10} more directories`);
1033
+ }
1034
+ }
1035
+ const rawOutput = outputParts.join("\n");
1036
+ const continuationHint = hasMore ? `Next page: fs_list(path="${dirPath}", offset=${nextOffset}, limit=${limit})` : "";
1037
+ return {
1038
+ success: true,
1039
+ output: trimOutput(rawOutput, MAX_OUTPUT_CHARS, continuationHint),
1040
+ metadata: {
1041
+ path: dirPath,
1042
+ directoryCount: dirs.length,
1043
+ fileCount: files.length,
1044
+ directories: dirs,
1045
+ files,
1046
+ totalEntries: combinedEntries.length,
1047
+ offset,
1048
+ limit,
1049
+ hasMore,
1050
+ nextOffset
1051
+ }
1052
+ };
1053
+ }
1054
+ };
1055
+ }
1056
+ var MAX_FILE_SIZE2 = MASS_REPLACE_CONFIG.maxFileSize;
1057
+ var MAX_FILES = MASS_REPLACE_CONFIG.maxFiles;
1058
+ function createMassReplaceTool(context) {
1059
+ return {
1060
+ definition: {
1061
+ type: "function",
1062
+ function: {
1063
+ name: "mass_replace",
1064
+ description: `Batch find-and-replace across multiple files. Supports literal and regex modes. Use dryRun=true to preview. Changes are tracked for rollback.`,
1065
+ parameters: {
1066
+ type: "object",
1067
+ properties: {
1068
+ pattern: {
1069
+ type: "string",
1070
+ description: "Text or regex pattern to search for. For regex mode, use JavaScript regex syntax (without / delimiters)."
1071
+ },
1072
+ replacement: {
1073
+ type: "string",
1074
+ description: "Replacement text. In regex mode, can use $1, $2 for capture groups."
1075
+ },
1076
+ scope: {
1077
+ type: "string",
1078
+ description: 'REQUIRED: Directory to limit search scope (e.g., "src", "packages/agent-core", "."). Prevents accidental project-wide changes. Use "." for current directory only.'
1079
+ },
1080
+ files: {
1081
+ type: "string",
1082
+ description: 'Glob pattern for files within scope (e.g., "**/*.ts", "*.js"). Combined with scope to form full path.'
1083
+ },
1084
+ mode: {
1085
+ type: "string",
1086
+ enum: ["literal", "regex"],
1087
+ description: 'Replacement mode: "literal" for exact string match (default), "regex" for regex patterns.'
1088
+ },
1089
+ dryRun: {
1090
+ type: "boolean",
1091
+ description: "Preview changes without applying them. Shows what would be changed."
1092
+ },
1093
+ caseInsensitive: {
1094
+ type: "boolean",
1095
+ description: 'For regex mode: make pattern case-insensitive (adds "i" flag).'
1096
+ }
1097
+ },
1098
+ required: ["pattern", "replacement", "scope", "files"]
1099
+ }
1100
+ }
1101
+ },
1102
+ executor: async (input) => {
1103
+ const pattern = String(input.pattern || "");
1104
+ const replacement = String(input.replacement || "");
1105
+ const scope = String(input.scope || "");
1106
+ const filesPattern = String(input.files || "");
1107
+ const mode = String(input.mode || "literal");
1108
+ const dryRun = Boolean(input.dryRun);
1109
+ const caseInsensitive = Boolean(input.caseInsensitive);
1110
+ if (!pattern) {
1111
+ return {
1112
+ success: false,
1113
+ error: 'VALIDATION_ERROR: "pattern" is required.'
1114
+ };
1115
+ }
1116
+ if (!scope) {
1117
+ return {
1118
+ success: false,
1119
+ error: 'VALIDATION_ERROR: "scope" is required. Specify directory to limit search (e.g., "src", "packages/agent-core", or "." for current dir).'
1120
+ };
1121
+ }
1122
+ if (!filesPattern) {
1123
+ return {
1124
+ success: false,
1125
+ error: 'VALIDATION_ERROR: "files" glob pattern is required.'
1126
+ };
1127
+ }
1128
+ if (mode !== "literal" && mode !== "regex") {
1129
+ return {
1130
+ success: false,
1131
+ error: `VALIDATION_ERROR: Invalid mode "${mode}". Must be "literal" or "regex".`
1132
+ };
1133
+ }
1134
+ try {
1135
+ const scopeValidation = validatePath(context.workingDir, scope);
1136
+ if (!scopeValidation.valid) {
1137
+ return {
1138
+ success: false,
1139
+ error: scopeValidation.error
1140
+ };
1141
+ }
1142
+ const scopePath = scopeValidation.resolved;
1143
+ if (!fs2.existsSync(scopePath)) {
1144
+ return {
1145
+ success: false,
1146
+ error: `SCOPE_ERROR: Directory "${scope}" does not exist.`
1147
+ };
1148
+ }
1149
+ if (!fs2.statSync(scopePath).isDirectory()) {
1150
+ return {
1151
+ success: false,
1152
+ error: `SCOPE_ERROR: "${scope}" is not a directory.`
1153
+ };
1154
+ }
1155
+ const matches = await glob(filesPattern, {
1156
+ cwd: scopePath,
1157
+ nodir: true,
1158
+ absolute: false,
1159
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
1160
+ });
1161
+ if (matches.length === 0) {
1162
+ return {
1163
+ success: true,
1164
+ output: `No files matched pattern: "${filesPattern}" in scope: "${scope}"`,
1165
+ metadata: {
1166
+ filesMatched: 0,
1167
+ filesChanged: 0,
1168
+ totalReplacements: 0,
1169
+ scope
1170
+ }
1171
+ };
1172
+ }
1173
+ if (matches.length > MAX_FILES) {
1174
+ return {
1175
+ success: false,
1176
+ error: `TOO_MANY_FILES: Pattern matched ${matches.length} files, but limit is ${MAX_FILES}. Use more specific glob pattern.`
1177
+ };
1178
+ }
1179
+ let searchPattern;
1180
+ if (mode === "regex") {
1181
+ const flags = caseInsensitive ? "gi" : "g";
1182
+ try {
1183
+ searchPattern = new RegExp(pattern, flags);
1184
+ } catch (err) {
1185
+ return {
1186
+ success: false,
1187
+ error: `REGEX_ERROR: Invalid regex pattern "${pattern}": ${err.message}`
1188
+ };
1189
+ }
1190
+ } else {
1191
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1192
+ const flags = caseInsensitive ? "gi" : "g";
1193
+ searchPattern = new RegExp(escapedPattern, flags);
1194
+ }
1195
+ const results = [];
1196
+ let totalReplacements = 0;
1197
+ let filesChanged = 0;
1198
+ for (const relPath of matches) {
1199
+ const scopedPath = path4.join(scope, relPath);
1200
+ const validation = validatePath(context.workingDir, scopedPath);
1201
+ if (!validation.valid) {
1202
+ return {
1203
+ success: false,
1204
+ error: validation.error
1205
+ };
1206
+ }
1207
+ const fullPath = validation.resolved;
1208
+ if (!fs2.existsSync(fullPath)) {
1209
+ continue;
1210
+ }
1211
+ const stats = fs2.statSync(fullPath);
1212
+ if (stats.size > MAX_FILE_SIZE2) {
1213
+ results.push({
1214
+ file: scopedPath,
1215
+ replacements: 0,
1216
+ preview: `[SKIPPED: File too large (${(stats.size / 1024).toFixed(1)} KB > ${MAX_FILE_SIZE2 / 1024} KB)]`
1217
+ });
1218
+ continue;
1219
+ }
1220
+ const beforeContent = fs2.readFileSync(fullPath, "utf-8");
1221
+ const afterContent = beforeContent.replace(searchPattern, replacement);
1222
+ if (beforeContent === afterContent) {
1223
+ continue;
1224
+ }
1225
+ const matchCount = (beforeContent.match(searchPattern) || []).length;
1226
+ totalReplacements += matchCount;
1227
+ filesChanged++;
1228
+ if (dryRun) {
1229
+ const lines = beforeContent.split("\n");
1230
+ const changedLines = [];
1231
+ let previewCount = 0;
1232
+ for (let i = 0; i < lines.length && previewCount < 3; i++) {
1233
+ const line = lines[i];
1234
+ if (searchPattern.test(line)) {
1235
+ const newLine = line.replace(searchPattern, replacement);
1236
+ changedLines.push(` Line ${i + 1}:`);
1237
+ changedLines.push(` - ${line.slice(0, 80)}`);
1238
+ changedLines.push(` + ${newLine.slice(0, 80)}`);
1239
+ previewCount++;
1240
+ }
1241
+ }
1242
+ results.push({
1243
+ file: scopedPath,
1244
+ replacements: matchCount,
1245
+ preview: changedLines.join("\n")
1246
+ });
1247
+ } else {
1248
+ fs2.writeFileSync(fullPath, afterContent, "utf-8");
1249
+ const hash = crypto.createHash("sha256").update(afterContent, "utf-8").digest("hex");
1250
+ context.filesRead?.add(scopedPath);
1251
+ context.filesReadHash?.set(scopedPath, hash);
1252
+ results.push({
1253
+ file: scopedPath,
1254
+ replacements: matchCount
1255
+ });
1256
+ }
1257
+ }
1258
+ const summary = dryRun ? `[DRY RUN] Would replace in ${filesChanged} file(s):` : `\u2705 Replaced in ${filesChanged} file(s):`;
1259
+ const details = results.map((r) => {
1260
+ if (r.preview) {
1261
+ return `${r.file}: ${r.replacements} replacement(s)
1262
+ ${r.preview}`;
1263
+ }
1264
+ return `${r.file}: ${r.replacements} replacement(s)`;
1265
+ }).join("\n\n");
1266
+ const output = `${summary}
1267
+
1268
+ ${details}
1269
+
1270
+ Scope: ${scope}
1271
+ Total replacements: ${totalReplacements}
1272
+ Files matched: ${matches.length}
1273
+ Files changed: ${filesChanged}` + (dryRun ? "\n\n\u26A0\uFE0F DRY RUN: No files were modified. Remove dryRun=true to apply changes." : "");
1274
+ return {
1275
+ success: true,
1276
+ output,
1277
+ metadata: {
1278
+ scope,
1279
+ filesMatched: matches.length,
1280
+ filesChanged,
1281
+ totalReplacements,
1282
+ dryRun,
1283
+ pattern,
1284
+ replacement,
1285
+ mode
1286
+ }
1287
+ };
1288
+ } catch (error) {
1289
+ return {
1290
+ success: false,
1291
+ error: `EXECUTION_ERROR: ${error.message}`
1292
+ };
1293
+ }
1294
+ }
1295
+ };
1296
+ }
1297
+ function trimOutput2(output, maxChars, continuationHint) {
1298
+ if (output.length <= maxChars) {
1299
+ return output;
1300
+ }
1301
+ const trimmed = output.slice(0, maxChars);
1302
+ return `${trimmed}
1303
+
1304
+ \u26A0\uFE0F OUTPUT TRIMMED (${output.length.toLocaleString()} chars \u2192 ${maxChars.toLocaleString()} shown)
1305
+ ${continuationHint}`;
1306
+ }
1307
+ var DEFAULT_EXCLUDES = SEARCH_CONFIG.defaultExcludes;
1308
+ var MAX_OUTPUT_CHARS2 = SEARCH_CONFIG.maxOutputChars;
1309
+ function buildFindExcludes(excludes) {
1310
+ return excludes.map((d) => `! -path "*/${d}/*"`).join(" ");
1311
+ }
1312
+ function buildGrepExcludes(excludes) {
1313
+ return excludes.map((d) => `--exclude-dir=${d}`).join(" ");
1314
+ }
1315
+ function toSingleQuoted(value) {
1316
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1317
+ }
1318
+ var SEARCH_TIMEOUT_MS = SEARCH_CONFIG.timeoutMs;
1319
+ var SEARCH_MAX_BUFFER = SEARCH_CONFIG.maxBuffer;
1320
+ var DEFAULT_RESULT_LIMIT = SEARCH_CONFIG.defaultResultLimit;
1321
+ var MAX_RESULT_LIMIT = SEARCH_CONFIG.maxResultLimit;
1322
+ function validateDirectory(workingDir, directory) {
1323
+ const fullPath = path4.resolve(workingDir, directory);
1324
+ if (fs2.existsSync(fullPath) && fs2.statSync(fullPath).isDirectory()) {
1325
+ return null;
1326
+ }
1327
+ let hint = "";
1328
+ try {
1329
+ const entries = fs2.readdirSync(workingDir, { withFileTypes: true });
1330
+ const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort();
1331
+ if (dirs.length > 0) {
1332
+ hint = `
1333
+ Available directories: ${dirs.slice(0, 15).join(", ")}${dirs.length > 15 ? ` ... (${dirs.length} total)` : ""}`;
1334
+ }
1335
+ } catch {
1336
+ }
1337
+ const suggestion = suggestDirectory(workingDir, directory);
1338
+ const didYouMean = suggestion ? `
1339
+ Did you mean: "${suggestion}"?` : "";
1340
+ return `Directory "${directory}" not found (resolved to ${fullPath}). Use "." to search from project root.${hint}${didYouMean}`;
1341
+ }
1342
+ function normalizeOffsetLimit2(input) {
1343
+ return normalizeOffsetLimit(input, { defaultLimit: DEFAULT_RESULT_LIMIT, maxLimit: MAX_RESULT_LIMIT });
1344
+ }
1345
+ function paginate(items, offset, limit) {
1346
+ const page = items.slice(offset, offset + limit);
1347
+ const hasMore = offset + limit < items.length;
1348
+ return {
1349
+ page,
1350
+ hasMore,
1351
+ nextOffset: hasMore ? offset + limit : null
1352
+ };
1353
+ }
1354
+ function createGlobSearchTool(context) {
1355
+ return {
1356
+ definition: {
1357
+ type: "function",
1358
+ function: {
1359
+ name: "glob_search",
1360
+ description: `Find files by name pattern (glob). Pattern matches filename only \u2014 use "*.ts" or "user.ts", not bare words. IMPORTANT: node_modules/, dist/, build/, .git/ are excluded by default \u2014 they contain no source code. Omit directory to search the entire working directory. Returns up to ${DEFAULT_RESULT_LIMIT} results.`,
1361
+ parameters: {
1362
+ type: "object",
1363
+ properties: {
1364
+ pattern: {
1365
+ type: "string",
1366
+ description: 'Glob pattern (e.g., "*.ts", "user.ts", "*user*.ts")'
1367
+ },
1368
+ directory: {
1369
+ type: "string",
1370
+ description: "Directory to search in (default: working directory root). Narrow to a subfolder when you already know the scope."
1371
+ },
1372
+ exclude: {
1373
+ type: "array",
1374
+ items: { type: "string" },
1375
+ description: "Extra directories to exclude on top of defaults (node_modules, dist, .git, build, .next, .kb, .pnpm, coverage are always excluded). Rarely needed."
1376
+ },
1377
+ offset: {
1378
+ type: "number",
1379
+ description: "Result offset for pagination (default: 0)"
1380
+ },
1381
+ limit: {
1382
+ type: "number",
1383
+ description: `Max results per page (default: ${DEFAULT_RESULT_LIMIT}, max: ${MAX_RESULT_LIMIT})`
1384
+ }
1385
+ },
1386
+ required: ["pattern"]
1387
+ }
1388
+ }
1389
+ },
1390
+ executor: async (input) => {
1391
+ const pattern = input.pattern;
1392
+ const directory = input.directory || ".";
1393
+ const extra = input.exclude;
1394
+ const excludes = extra && extra.length > 0 ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDES, ...extra])] : DEFAULT_EXCLUDES;
1395
+ const { offset, limit } = normalizeOffsetLimit2(input);
1396
+ try {
1397
+ const fullPath = path4.resolve(context.workingDir, directory);
1398
+ const dirError = validateDirectory(context.workingDir, directory);
1399
+ if (dirError) {
1400
+ return { success: true, output: dirError };
1401
+ }
1402
+ const windowSize = Math.min(2e3, Math.max(500, offset + limit));
1403
+ const cmd = `find "${fullPath}" -type f -iname "${pattern}" ${buildFindExcludes(excludes)} | head -${windowSize}`;
1404
+ const output = execSync(cmd, {
1405
+ cwd: context.workingDir,
1406
+ encoding: "utf-8",
1407
+ maxBuffer: SEARCH_MAX_BUFFER,
1408
+ timeout: SEARCH_TIMEOUT_MS
1409
+ });
1410
+ const files = output.trim().split("\n").filter(Boolean).map((f) => path4.relative(context.workingDir, f));
1411
+ if (files.length === 0) {
1412
+ return {
1413
+ success: true,
1414
+ output: `No files found matching pattern: ${pattern} in ${directory === "." ? "project root" : directory}. Note: glob_search matches filenames only. To search file contents, use grep_search. To find class/function definitions, use find_definition.`
1415
+ };
1416
+ }
1417
+ const page = paginate(files, offset, limit);
1418
+ const excludeNote = `[Excluded: ${DEFAULT_EXCLUDES.join(", ")}${extra && extra.length > 0 ? ` + ${extra.join(", ")}` : ""}]`;
1419
+ const result = [
1420
+ `Found ${files.length} file(s) matching "${pattern}" in "${directory}" (showing ${page.page.length}, offset=${offset}, limit=${limit})`,
1421
+ excludeNote,
1422
+ "",
1423
+ ...page.page.map((f) => ` ${f}`),
1424
+ page.hasMore ? "" : ""
1425
+ ].filter((s) => s !== void 0).join("\n");
1426
+ const continuationHint = page.hasMore ? `Next page: glob_search(pattern="${pattern}", directory="${directory}", offset=${page.nextOffset}, limit=${limit})` : "";
1427
+ return {
1428
+ success: true,
1429
+ output: trimOutput2(result, MAX_OUTPUT_CHARS2, continuationHint),
1430
+ metadata: {
1431
+ totalMatches: files.length,
1432
+ offset,
1433
+ limit,
1434
+ hasMore: page.hasMore,
1435
+ nextOffset: page.nextOffset
1436
+ }
1437
+ };
1438
+ } catch (error) {
1439
+ if (error instanceof Error && "killed" in error && error.killed) {
1440
+ return toolError({
1441
+ code: "SEARCH_TIMEOUT",
1442
+ message: `Glob search timed out after ${SEARCH_TIMEOUT_MS / 1e3}s.`,
1443
+ retryable: true,
1444
+ hint: "Narrow directory/pattern or lower limit.",
1445
+ details: { directory, pattern, timeoutMs: SEARCH_TIMEOUT_MS }
1446
+ });
1447
+ }
1448
+ if (error.code === "ENOBUFS") {
1449
+ return toolError({
1450
+ code: "SEARCH_BUFFER_OVERFLOW",
1451
+ message: "Search output exceeded buffer.",
1452
+ retryable: true,
1453
+ hint: "Narrow directory/pattern or reduce limit.",
1454
+ details: { directory, pattern, offset, limit }
1455
+ });
1456
+ }
1457
+ return toolError({
1458
+ code: "SEARCH_FAILED",
1459
+ message: `Glob search failed: ${error instanceof Error ? error.message : String(error)}`,
1460
+ retryable: true,
1461
+ hint: "Check pattern syntax and directory.",
1462
+ details: { directory, pattern }
1463
+ });
1464
+ }
1465
+ }
1466
+ };
1467
+ }
1468
+ function createGrepSearchTool(context) {
1469
+ return {
1470
+ definition: {
1471
+ type: "function",
1472
+ function: {
1473
+ name: "grep_search",
1474
+ description: `Search for text/regex in files. Returns file:line matches. IMPORTANT: node_modules/, dist/, build/ excluded by default. Omit directory to search the entire working directory. Add filePattern (e.g. "*.ts") to limit to specific file types. Returns up to ${DEFAULT_RESULT_LIMIT} matches.`,
1475
+ parameters: {
1476
+ type: "object",
1477
+ properties: {
1478
+ pattern: {
1479
+ type: "string",
1480
+ description: "Text pattern to search for (can be regex)"
1481
+ },
1482
+ directory: {
1483
+ type: "string",
1484
+ description: "Directory to search in (default: working directory root). Narrow to a subfolder when you already know the scope."
1485
+ },
1486
+ filePattern: {
1487
+ type: "string",
1488
+ description: 'Filter by file extension (e.g., "*.ts", "*.md"). STRONGLY RECOMMENDED: always set this to avoid scanning binary/generated files.'
1489
+ },
1490
+ exclude: {
1491
+ type: "array",
1492
+ items: { type: "string" },
1493
+ description: "Extra directories to exclude on top of defaults (node_modules, dist, .git, build, .next, .kb, .pnpm, coverage are always excluded). Rarely needed."
1494
+ },
1495
+ mode: {
1496
+ type: "string",
1497
+ enum: ["auto", "regex", "literal"],
1498
+ description: "Search mode: auto (default), regex, or literal"
1499
+ },
1500
+ offset: {
1501
+ type: "number",
1502
+ description: "Result offset for pagination (default: 0)"
1503
+ },
1504
+ limit: {
1505
+ type: "number",
1506
+ description: `Max matches per page (default: ${DEFAULT_RESULT_LIMIT}, max: ${MAX_RESULT_LIMIT})`
1507
+ }
1508
+ },
1509
+ required: ["pattern"]
1510
+ }
1511
+ }
1512
+ },
1513
+ executor: async (input) => {
1514
+ const pattern = input.pattern;
1515
+ const directory = input.directory || ".";
1516
+ const filePattern = input.filePattern;
1517
+ const extra = input.exclude;
1518
+ const excludes = extra && extra.length > 0 ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDES, ...extra])] : DEFAULT_EXCLUDES;
1519
+ const mode = (input.mode || "auto").toLowerCase();
1520
+ const { offset, limit } = normalizeOffsetLimit2(input);
1521
+ try {
1522
+ const fullPath = path4.resolve(context.workingDir, directory);
1523
+ const dirError = validateDirectory(context.workingDir, directory);
1524
+ if (dirError) {
1525
+ return { success: true, output: dirError };
1526
+ }
1527
+ const windowSize = Math.min(2e3, Math.max(500, offset + limit));
1528
+ const regexCmd = `grep -rIn ${toSingleQuoted(pattern)} "${fullPath}" ${buildGrepExcludes(excludes)}${filePattern ? ` --include="${filePattern}"` : ""}`;
1529
+ const literalCmd = `grep -rIFn ${toSingleQuoted(pattern)} "${fullPath}" ${buildGrepExcludes(excludes)}${filePattern ? ` --include="${filePattern}"` : ""}`;
1530
+ let cmd = mode === "literal" ? literalCmd : regexCmd;
1531
+ cmd += ` | head -${windowSize}`;
1532
+ let output = "";
1533
+ let usedLiteralFallback = false;
1534
+ try {
1535
+ output = execSync(cmd, {
1536
+ cwd: context.workingDir,
1537
+ encoding: "utf-8",
1538
+ maxBuffer: SEARCH_MAX_BUFFER,
1539
+ timeout: SEARCH_TIMEOUT_MS
1540
+ });
1541
+ } catch (error) {
1542
+ const status = error.status;
1543
+ const stderrRaw = error.stderr;
1544
+ const stderr = typeof stderrRaw === "string" ? stderrRaw : stderrRaw instanceof Buffer ? stderrRaw.toString("utf-8") : "";
1545
+ const looksLikeInvalidRegex = status === 2 && /(unbalanced|parentheses|invalid regular expression|regular expression)/i.test(stderr);
1546
+ if (!looksLikeInvalidRegex) {
1547
+ throw error;
1548
+ }
1549
+ usedLiteralFallback = true;
1550
+ if (mode === "regex") {
1551
+ throw error;
1552
+ }
1553
+ output = execSync(`${literalCmd} | head -${windowSize}`, {
1554
+ cwd: context.workingDir,
1555
+ encoding: "utf-8",
1556
+ maxBuffer: SEARCH_MAX_BUFFER,
1557
+ timeout: SEARCH_TIMEOUT_MS
1558
+ });
1559
+ }
1560
+ const lines = output.trim().split("\n").filter(Boolean);
1561
+ if (lines.length === 0) {
1562
+ return {
1563
+ success: true,
1564
+ output: `No matches found for "${pattern}" in ${directory === "." ? "project root" : directory}.${filePattern ? "" : ' Try adding filePattern (e.g. "*.ts") to narrow the search.'}`
1565
+ };
1566
+ }
1567
+ const page = paginate(lines, offset, limit);
1568
+ const excludeNote = `[Excluded: ${DEFAULT_EXCLUDES.join(", ")}${extra && extra.length > 0 ? ` + ${extra.join(", ")}` : ""}]`;
1569
+ const result = [
1570
+ `Found ${lines.length} match(es) for "${pattern}" in "${directory}"${filePattern ? ` (${filePattern})` : ""}${usedLiteralFallback ? " (literal fallback)" : ""} (showing ${page.page.length}, offset=${offset}, limit=${limit})`,
1571
+ excludeNote,
1572
+ "",
1573
+ ...page.page.map((line) => {
1574
+ const match = line.match(/^(.+?):(\d+):(.+)$/);
1575
+ if (match) {
1576
+ const [, filePath, lineNum, content] = match;
1577
+ const relPath = path4.relative(context.workingDir, filePath);
1578
+ return ` ${relPath}:${lineNum}
1579
+ ${content.trim()}`;
1580
+ }
1581
+ return ` ${line}`;
1582
+ }),
1583
+ page.hasMore ? "" : ""
1584
+ ].filter((s) => s !== void 0).join("\n");
1585
+ const continuationHint = page.hasMore ? `Next page: grep_search(pattern="${pattern}", directory="${directory}", offset=${page.nextOffset}, limit=${limit})` : "";
1586
+ return {
1587
+ success: true,
1588
+ output: trimOutput2(result, MAX_OUTPUT_CHARS2, continuationHint),
1589
+ metadata: {
1590
+ totalMatches: lines.length,
1591
+ offset,
1592
+ limit,
1593
+ hasMore: page.hasMore,
1594
+ nextOffset: page.nextOffset,
1595
+ modeUsed: usedLiteralFallback ? "literal" : mode === "auto" ? "regex" : mode
1596
+ }
1597
+ };
1598
+ } catch (error) {
1599
+ if (error instanceof Error && "status" in error && error.status === 1) {
1600
+ return {
1601
+ success: true,
1602
+ output: `No matches found for "${pattern}" in ${directory === "." ? "project root" : directory}.${filePattern ? "" : ' Try adding filePattern (e.g. "*.ts") to narrow the search.'}`
1603
+ };
1604
+ }
1605
+ if (error instanceof Error && "killed" in error && error.killed) {
1606
+ return toolError({
1607
+ code: "SEARCH_TIMEOUT",
1608
+ message: `Grep search timed out after ${SEARCH_TIMEOUT_MS / 1e3}s.`,
1609
+ retryable: true,
1610
+ hint: "Narrow directory, add filePattern, or lower limit.",
1611
+ details: { directory, pattern, timeoutMs: SEARCH_TIMEOUT_MS }
1612
+ });
1613
+ }
1614
+ if (error.code === "ENOBUFS") {
1615
+ return toolError({
1616
+ code: "SEARCH_BUFFER_OVERFLOW",
1617
+ message: "Search output exceeded buffer.",
1618
+ retryable: true,
1619
+ hint: "Narrow directory, add filePattern, or reduce limit.",
1620
+ details: { directory, pattern, filePattern, offset, limit, mode }
1621
+ });
1622
+ }
1623
+ return toolError({
1624
+ code: "SEARCH_FAILED",
1625
+ message: `Grep search failed: ${error instanceof Error ? error.message : String(error)}`,
1626
+ retryable: true,
1627
+ hint: 'Try mode="literal" for special characters, or narrow directory.',
1628
+ details: { directory, pattern, filePattern, mode }
1629
+ });
1630
+ }
1631
+ }
1632
+ };
1633
+ }
1634
+ function createListFilesTool(context) {
1635
+ return {
1636
+ definition: {
1637
+ type: "function",
1638
+ function: {
1639
+ name: "list_files",
1640
+ description: "List files and directories at a path. Good for exploring what exists.",
1641
+ parameters: {
1642
+ type: "object",
1643
+ properties: {
1644
+ directory: {
1645
+ type: "string",
1646
+ description: 'Directory to list (default: "." for current directory)'
1647
+ },
1648
+ recursive: {
1649
+ type: "boolean",
1650
+ description: "Include subdirectories (default: false)"
1651
+ },
1652
+ offset: {
1653
+ type: "number",
1654
+ description: "Result offset for pagination (default: 0)"
1655
+ },
1656
+ limit: {
1657
+ type: "number",
1658
+ description: `Max results per page (default: ${DEFAULT_RESULT_LIMIT}, max: ${MAX_RESULT_LIMIT})`
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ },
1664
+ executor: async (input) => {
1665
+ const directory = input.directory || ".";
1666
+ const recursive = input.recursive || false;
1667
+ const { offset, limit } = normalizeOffsetLimit2(input);
1668
+ try {
1669
+ const fullPath = path4.resolve(context.workingDir, directory);
1670
+ let cmd;
1671
+ if (recursive) {
1672
+ cmd = `find "${fullPath}" -type f ! -path "*/node_modules/*" ! -path "*/.git/*" ! -path "*/dist/*" ! -path "*/.kb/*" | head -100`;
1673
+ } else {
1674
+ cmd = `ls -la "${fullPath}" 2>/dev/null || echo "Directory not found"`;
1675
+ }
1676
+ const output = execSync(cmd, {
1677
+ cwd: context.workingDir,
1678
+ encoding: "utf-8",
1679
+ maxBuffer: 1024 * 1024
1680
+ });
1681
+ if (recursive) {
1682
+ const files = output.trim().split("\n").filter(Boolean).map((f) => path4.relative(context.workingDir, f));
1683
+ const page = paginate(files, offset, limit);
1684
+ return {
1685
+ success: true,
1686
+ output: files.length > 0 ? `Files in ${directory} (recursive, showing ${page.page.length}/${files.length}, offset=${offset}, limit=${limit}):
1687
+
1688
+ ${page.page.map((f) => ` ${f}`).join("\n")}${page.hasMore ? `
1689
+
1690
+ Next page: list_files(directory="${directory}", recursive=true, offset=${page.nextOffset}, limit=${limit})` : ""}` : `No files found in ${directory}`,
1691
+ metadata: {
1692
+ totalMatches: files.length,
1693
+ offset,
1694
+ limit,
1695
+ hasMore: page.hasMore,
1696
+ nextOffset: page.nextOffset
1697
+ }
1698
+ };
1699
+ }
1700
+ return {
1701
+ success: true,
1702
+ output: `Contents of ${directory}:
1703
+
1704
+ ${output}`
1705
+ };
1706
+ } catch (error) {
1707
+ return {
1708
+ success: false,
1709
+ error: `List files failed: ${error instanceof Error ? error.message : String(error)}`
1710
+ };
1711
+ }
1712
+ }
1713
+ };
1714
+ }
1715
+ function createFindDefinitionTool(context) {
1716
+ return {
1717
+ definition: {
1718
+ type: "function",
1719
+ function: {
1720
+ name: "find_definition",
1721
+ description: "Find where a symbol (class, function, interface, type, etc.) is defined. Works with any language. Uses project root as default directory, which works well for monorepos with nested packages.",
1722
+ parameters: {
1723
+ type: "object",
1724
+ properties: {
1725
+ name: {
1726
+ type: "string",
1727
+ description: "Name of the symbol to find (class, function, interface, type, struct, etc.)"
1728
+ },
1729
+ directory: {
1730
+ type: "string",
1731
+ description: 'Directory to search in (default: ".")'
1732
+ },
1733
+ filePattern: {
1734
+ type: "string",
1735
+ description: 'File pattern to search (e.g., "*.cs" for C#, "*.py" for Python). If not specified, searches all source files.'
1736
+ }
1737
+ },
1738
+ required: ["name"]
1739
+ }
1740
+ }
1741
+ },
1742
+ executor: async (input) => {
1743
+ const name = input.name;
1744
+ const directory = input.directory || ".";
1745
+ const filePattern = input.filePattern;
1746
+ try {
1747
+ const fullPath = path4.resolve(context.workingDir, directory);
1748
+ const dirError = validateDirectory(context.workingDir, directory);
1749
+ if (dirError) {
1750
+ return { success: true, output: dirError };
1751
+ }
1752
+ const patterns = [
1753
+ // Common across languages
1754
+ `class ${name}`,
1755
+ `interface ${name}`,
1756
+ `struct ${name}`,
1757
+ `enum ${name}`,
1758
+ // TypeScript/JavaScript
1759
+ `function ${name}`,
1760
+ `const ${name}`,
1761
+ `let ${name}`,
1762
+ `var ${name}`,
1763
+ `type ${name}`,
1764
+ `export.*${name}`,
1765
+ // Python
1766
+ `def ${name}`,
1767
+ // C#/Java
1768
+ `public.*${name}`,
1769
+ `private.*${name}`,
1770
+ `protected.*${name}`,
1771
+ `static.*${name}`,
1772
+ // Go
1773
+ `func ${name}`,
1774
+ `func \\(.*\\) ${name}`,
1775
+ // Rust
1776
+ `fn ${name}`,
1777
+ `impl ${name}`,
1778
+ `trait ${name}`,
1779
+ `mod ${name}`
1780
+ ];
1781
+ let includeFlags = "";
1782
+ if (filePattern) {
1783
+ includeFlags = `--include="${filePattern}"`;
1784
+ } else {
1785
+ includeFlags = toRgIncludes(ALL_SOURCE_EXTENSIONS);
1786
+ }
1787
+ const cmd = `grep -rn -E "(${patterns.join("|")})" "${fullPath}" ${includeFlags} ${buildGrepExcludes(DEFAULT_EXCLUDES)} --exclude-dir=bin --exclude-dir=obj --exclude-dir=target | head -30`;
1788
+ const output = execSync(cmd, {
1789
+ cwd: context.workingDir,
1790
+ encoding: "utf-8",
1791
+ maxBuffer: 1024 * 1024,
1792
+ timeout: SEARCH_TIMEOUT_MS
1793
+ });
1794
+ const lines = output.trim().split("\n").filter(Boolean);
1795
+ if (lines.length === 0) {
1796
+ return {
1797
+ success: true,
1798
+ output: `No definition found for "${name}" in ${directory === "." ? "project root" : directory}. Try: grep_search for text matching, or glob_search with "*${name.toLowerCase()}*" for filename matching.`
1799
+ };
1800
+ }
1801
+ const result = lines.map((line) => {
1802
+ const match = line.match(/^(.+?):(\d+):(.+)$/);
1803
+ if (match) {
1804
+ const [, filePath, lineNum, content] = match;
1805
+ const relPath = path4.relative(context.workingDir, filePath);
1806
+ return `${relPath}:${lineNum}
1807
+ ${content.trim()}`;
1808
+ }
1809
+ return line;
1810
+ });
1811
+ return {
1812
+ success: true,
1813
+ output: `Found definition(s) for "${name}":
1814
+
1815
+ ${result.join("\n\n")}`
1816
+ };
1817
+ } catch (error) {
1818
+ if (error instanceof Error && "status" in error && error.status === 1) {
1819
+ return {
1820
+ success: true,
1821
+ output: `No definition found for "${name}" in ${directory === "." ? "project root" : directory}. Try: grep_search for text matching, or glob_search with "*${name.toLowerCase()}*" for filename matching.`
1822
+ };
1823
+ }
1824
+ if (error instanceof Error && "killed" in error && error.killed) {
1825
+ return {
1826
+ success: false,
1827
+ error: `Find definition timed out after ${SEARCH_TIMEOUT_MS / 1e3}s. Try specifying a narrower directory.`
1828
+ };
1829
+ }
1830
+ return {
1831
+ success: false,
1832
+ error: `Find definition failed: ${error instanceof Error ? error.message : String(error)}`
1833
+ };
1834
+ }
1835
+ }
1836
+ };
1837
+ }
1838
+ function createCodeStatsTool(context) {
1839
+ return {
1840
+ definition: {
1841
+ type: "function",
1842
+ function: {
1843
+ name: "code_stats",
1844
+ description: "Get line counts and file counts by extension for a directory scope (not single-file line counts).",
1845
+ parameters: {
1846
+ type: "object",
1847
+ properties: {
1848
+ directory: {
1849
+ type: "string",
1850
+ description: 'Directory to analyze (default: "."). If a file path is provided by mistake, use its parent directory.'
1851
+ },
1852
+ extensions: {
1853
+ type: "string",
1854
+ description: 'Comma-separated list of extensions to count (e.g., "ts,tsx,js" or "cs,csproj" or "py"). If not specified, counts all source files.'
1855
+ },
1856
+ offset: {
1857
+ type: "number",
1858
+ description: "Offset for extension summary rows (default: 0)"
1859
+ },
1860
+ limit: {
1861
+ type: "number",
1862
+ description: `Rows per page for extension summary (default: ${DEFAULT_RESULT_LIMIT}, max: ${MAX_RESULT_LIMIT})`
1863
+ }
1864
+ }
1865
+ }
1866
+ }
1867
+ },
1868
+ executor: async (input) => {
1869
+ const directory = input.directory || ".";
1870
+ const extensionsInput = input.extensions;
1871
+ const { offset, limit } = normalizeOffsetLimit2(input);
1872
+ try {
1873
+ const fullPath = path4.resolve(context.workingDir, directory);
1874
+ const dirError = validateDirectory(context.workingDir, directory);
1875
+ if (dirError) {
1876
+ return { success: true, output: dirError };
1877
+ }
1878
+ let extFilter;
1879
+ if (extensionsInput) {
1880
+ const exts = extensionsInput.split(",").map((e) => e.trim());
1881
+ extFilter = toFindNames(exts);
1882
+ } else {
1883
+ extFilter = toFindNames(ALL_SOURCE_EXTENSIONS);
1884
+ }
1885
+ const totalCmd = `find "${fullPath}" -type f \\( ${extFilter} \\) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/.git/*" ! -path "*/bin/*" ! -path "*/obj/*" ! -path "*/target/*" ! -path "*/__pycache__/*" -exec wc -l {} + 2>/dev/null | tail -1 || echo "0 total"`;
1886
+ const totalOutput = execSync(totalCmd, {
1887
+ cwd: context.workingDir,
1888
+ encoding: "utf-8"
1889
+ }).trim();
1890
+ const countByExtCmd = `find "${fullPath}" -type f \\( ${extFilter} \\) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/.git/*" ! -path "*/bin/*" ! -path "*/obj/*" ! -path "*/target/*" ! -path "*/__pycache__/*" | sed 's/.*\\.//' | sort | uniq -c | sort -rn | head -200`;
1891
+ const countByExt = execSync(countByExtCmd, {
1892
+ cwd: context.workingDir,
1893
+ encoding: "utf-8"
1894
+ }).trim().split("\n").filter(Boolean);
1895
+ const fileCountCmd = `find "${fullPath}" -type f \\( ${extFilter} \\) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/.git/*" ! -path "*/bin/*" ! -path "*/obj/*" ! -path "*/target/*" ! -path "*/__pycache__/*" | wc -l`;
1896
+ const fileCount = execSync(fileCountCmd, {
1897
+ cwd: context.workingDir,
1898
+ encoding: "utf-8"
1899
+ }).trim();
1900
+ const page = paginate(countByExt, offset, limit);
1901
+ return {
1902
+ success: true,
1903
+ output: `Code statistics for directory ${directory}:
1904
+
1905
+ Total lines: ${totalOutput}
1906
+ Total files: ${fileCount}
1907
+
1908
+ Files by extension (showing ${page.page.length}/${countByExt.length}, offset=${offset}, limit=${limit}):
1909
+ ${page.page.join("\n")}${page.hasMore ? `
1910
+
1911
+ Next page: code_stats(directory="${directory}", offset=${page.nextOffset}, limit=${limit})` : ""}
1912
+
1913
+ Note: This is directory-level aggregate. For a single file line count, use fs_read on that file and rely on metadata.totalLines.`,
1914
+ metadata: {
1915
+ totalExtensionRows: countByExt.length,
1916
+ offset,
1917
+ limit,
1918
+ hasMore: page.hasMore,
1919
+ nextOffset: page.nextOffset
1920
+ }
1921
+ };
1922
+ } catch (error) {
1923
+ return toolError({
1924
+ code: "CODE_STATS_FAILED",
1925
+ message: `Code stats failed: ${error instanceof Error ? error.message : String(error)}`,
1926
+ retryable: true,
1927
+ hint: "Narrow directory or provide extensions.",
1928
+ details: { directory, extensions: extensionsInput }
1929
+ });
1930
+ }
1931
+ }
1932
+ };
1933
+ }
1934
+ function getMemoryPath(context) {
1935
+ const memoryDir = path4.join(context.workingDir, ".kb", "memory", "shared");
1936
+ if (!fs2.existsSync(memoryDir)) {
1937
+ fs2.mkdirSync(memoryDir, { recursive: true });
1938
+ }
1939
+ return path4.join(memoryDir, "memory.json");
1940
+ }
1941
+ function loadSharedMemory(context) {
1942
+ const memoryPath = getMemoryPath(context);
1943
+ const defaultMemory = {
1944
+ preferences: [],
1945
+ constraints: [],
1946
+ sessions: [],
1947
+ projectContext: {
1948
+ name: "",
1949
+ description: "",
1950
+ technologies: [],
1951
+ structure: ""
1952
+ }
1953
+ };
1954
+ if (!fs2.existsSync(memoryPath)) {
1955
+ return defaultMemory;
1956
+ }
1957
+ try {
1958
+ const content = fs2.readFileSync(memoryPath, "utf-8");
1959
+ const parsed = JSON.parse(content);
1960
+ if (parsed.facts && Array.isArray(parsed.facts)) {
1961
+ const preferences = parsed.facts.filter((f) => f.type === "user_preference");
1962
+ const constraints = parsed.facts.filter((f) => f.type === "constraint");
1963
+ return {
1964
+ preferences,
1965
+ constraints,
1966
+ sessions: parsed.sessions || [],
1967
+ projectContext: parsed.projectContext || defaultMemory.projectContext
1968
+ };
1969
+ }
1970
+ return {
1971
+ ...defaultMemory,
1972
+ ...parsed
1973
+ };
1974
+ } catch {
1975
+ return defaultMemory;
1976
+ }
1977
+ }
1978
+ function saveSharedMemory(context, memory) {
1979
+ const memoryPath = getMemoryPath(context);
1980
+ fs2.writeFileSync(memoryPath, JSON.stringify(memory, null, 2), "utf-8");
1981
+ }
1982
+ async function loadCanonicalSessionMemory(context) {
1983
+ if (!context.sessionMemory) {
1984
+ return { constraints: [], corrections: [] };
1985
+ }
1986
+ const kernel = await context.sessionMemory.loadKernelState();
1987
+ if (!kernel) {
1988
+ return { constraints: [], corrections: [] };
1989
+ }
1990
+ return {
1991
+ constraints: kernel.constraints,
1992
+ corrections: kernel.memory.corrections.map((item) => item.content)
1993
+ };
1994
+ }
1995
+ function createMemoryGetTool(context) {
1996
+ return {
1997
+ definition: {
1998
+ type: "function",
1999
+ function: {
2000
+ name: "memory_get",
2001
+ description: "Get stored user preferences, project constraints, and session history.",
2002
+ parameters: {
2003
+ type: "object",
2004
+ properties: {
2005
+ sessionId: {
2006
+ type: "string",
2007
+ description: "Optional session ID to get specific session context"
2008
+ }
2009
+ }
2010
+ }
2011
+ }
2012
+ },
2013
+ executor: async (input) => {
2014
+ const sessionId = input.sessionId;
2015
+ const memory = loadSharedMemory(context);
2016
+ const canonicalSession = await loadCanonicalSessionMemory(context);
2017
+ const preferencesText = memory.preferences.length > 0 ? memory.preferences.map((p) => `- ${p.content}`).join("\n") : "No preferences set";
2018
+ const constraintsText = memory.constraints.length > 0 ? memory.constraints.map((c) => `- ${c.content}`).join("\n") : "No constraints set";
2019
+ const sessionConstraintsText = canonicalSession.constraints.length > 0 ? canonicalSession.constraints.map((item) => `- ${item}`).join("\n") : "No session constraints set";
2020
+ const sessionCorrectionsText = canonicalSession.corrections.length > 0 ? canonicalSession.corrections.map((item) => `- ${item}`).join("\n") : "No session corrections recorded";
2021
+ const sessions = sessionId ? memory.sessions.filter((s) => s.sessionId === sessionId) : memory.sessions.slice(-5);
2022
+ const sessionInfo = sessions.map((s) => {
2023
+ const tasks = s.tasks.join(", ");
2024
+ return `Session ${s.sessionId} (${s.timestamp}):
2025
+ Tasks: ${tasks}
2026
+ Learnings: ${s.learnings}`;
2027
+ }).join("\n\n");
2028
+ const output = [
2029
+ "=== Shared Memory ===",
2030
+ "",
2031
+ "## \u{1F464} User Preferences (persistent)",
2032
+ preferencesText,
2033
+ "",
2034
+ "## \u{1F6AB} Constraints (persistent)",
2035
+ constraintsText,
2036
+ "",
2037
+ "## Session Constraints (canonical)",
2038
+ sessionConstraintsText,
2039
+ "",
2040
+ "## Session Corrections (canonical)",
2041
+ sessionCorrectionsText,
2042
+ "",
2043
+ "## Project Context",
2044
+ `Name: ${memory.projectContext.name || "Unknown"}`,
2045
+ `Description: ${memory.projectContext.description || "N/A"}`,
2046
+ `Technologies: ${memory.projectContext.technologies.join(", ") || "N/A"}`,
2047
+ "",
2048
+ "## Session History",
2049
+ sessionInfo || "No sessions yet",
2050
+ "",
2051
+ "---",
2052
+ "Note: Corrections, findings, and blockers are stored in session memory."
2053
+ ].join("\n");
2054
+ return {
2055
+ success: true,
2056
+ output
2057
+ };
2058
+ }
2059
+ };
2060
+ }
2061
+ function createMemoryPreferenceTool(context) {
2062
+ return {
2063
+ definition: {
2064
+ type: "function",
2065
+ function: {
2066
+ name: "memory_preference",
2067
+ description: "Store a persistent user preference (coding style, tool choices, etc.).",
2068
+ parameters: {
2069
+ type: "object",
2070
+ properties: {
2071
+ content: {
2072
+ type: "string",
2073
+ description: 'The preference to store (e.g., "Always use TypeScript strict mode")'
2074
+ },
2075
+ tags: {
2076
+ type: "array",
2077
+ items: { type: "string" },
2078
+ description: 'Optional tags for categorization (e.g., ["typescript", "style"])'
2079
+ }
2080
+ },
2081
+ required: ["content"]
2082
+ }
2083
+ }
2084
+ },
2085
+ executor: async (input) => {
2086
+ const content = input.content;
2087
+ const tags = input.tags;
2088
+ const memory = loadSharedMemory(context);
2089
+ const preference = {
2090
+ content,
2091
+ category: "user_input",
2092
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2093
+ type: "user_preference",
2094
+ metadata: {
2095
+ importance: 0.9,
2096
+ source: "user",
2097
+ scope: "project",
2098
+ tags
2099
+ }
2100
+ };
2101
+ memory.preferences.push(preference);
2102
+ if (memory.preferences.length > 50) {
2103
+ memory.preferences = memory.preferences.slice(-50);
2104
+ }
2105
+ saveSharedMemory(context, memory);
2106
+ return {
2107
+ success: true,
2108
+ output: `\u{1F464} Preference stored: ${content}`
2109
+ };
2110
+ }
2111
+ };
2112
+ }
2113
+ function createMemoryConstraintTool(context) {
2114
+ return {
2115
+ definition: {
2116
+ type: "function",
2117
+ function: {
2118
+ name: "memory_constraint",
2119
+ description: "Store a persistent project constraint or rule. Use this for durable project-wide rules, not session-only instructions.",
2120
+ parameters: {
2121
+ type: "object",
2122
+ properties: {
2123
+ content: {
2124
+ type: "string",
2125
+ description: 'The constraint (e.g., "Do not modify files in /legacy/ directory")'
2126
+ }
2127
+ },
2128
+ required: ["content"]
2129
+ }
2130
+ }
2131
+ },
2132
+ executor: async (input) => {
2133
+ const content = input.content;
2134
+ if (context.sessionMemory) {
2135
+ await context.sessionMemory.recordConstraint(content);
2136
+ }
2137
+ const memory = loadSharedMemory(context);
2138
+ const constraint = {
2139
+ content,
2140
+ category: "project_rules",
2141
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2142
+ type: "constraint",
2143
+ metadata: {
2144
+ importance: 1,
2145
+ source: "user",
2146
+ scope: "project"
2147
+ }
2148
+ };
2149
+ memory.constraints.push(constraint);
2150
+ if (memory.constraints.length > 50) {
2151
+ memory.constraints = memory.constraints.slice(-50);
2152
+ }
2153
+ saveSharedMemory(context, memory);
2154
+ return {
2155
+ success: true,
2156
+ output: `\u{1F6AB} Constraint stored: ${content}`
2157
+ };
2158
+ }
2159
+ };
2160
+ }
2161
+ function getSessionMemoryPath(context, sessionId) {
2162
+ const id = sessionId || `session-${Date.now()}`;
2163
+ const sessionDir = path4.join(context.workingDir, ".kb", "memory", id);
2164
+ if (!fs2.existsSync(sessionDir)) {
2165
+ fs2.mkdirSync(sessionDir, { recursive: true });
2166
+ }
2167
+ return sessionDir;
2168
+ }
2169
+ function saveToSession(context, entry, sessionId) {
2170
+ const sessionDir = getSessionMemoryPath(context, sessionId);
2171
+ const entryId = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
2172
+ const filePath = path4.join(sessionDir, `${entryId}.json`);
2173
+ fs2.writeFileSync(filePath, JSON.stringify({ ...entry, id: entryId }, null, 2), "utf-8");
2174
+ }
2175
+ function createMemoryCorrectionTool(context) {
2176
+ return {
2177
+ definition: {
2178
+ type: "function",
2179
+ function: {
2180
+ name: "memory_correction",
2181
+ description: "Store a user correction for the current session. Use this immediately after the user corrects you or changes active instructions. Set constraint=true if the correction should remain an active session constraint.",
2182
+ parameters: {
2183
+ type: "object",
2184
+ properties: {
2185
+ content: {
2186
+ type: "string",
2187
+ description: 'The correction (e.g., "AuthService is in packages/auth/, not src/services/")'
2188
+ },
2189
+ supersedes: {
2190
+ type: "string",
2191
+ description: "Optional: what this correction replaces"
2192
+ },
2193
+ invalidates: {
2194
+ type: "array",
2195
+ items: { type: "string" },
2196
+ description: "Optional: assumption IDs invalidated by this correction"
2197
+ },
2198
+ constraint: {
2199
+ type: "boolean",
2200
+ description: "Set true if this correction should also become an active session constraint"
2201
+ },
2202
+ sessionId: {
2203
+ type: "string",
2204
+ description: "Optional: session ID (defaults to current timestamp-based session)"
2205
+ }
2206
+ },
2207
+ required: ["content"]
2208
+ }
2209
+ }
2210
+ },
2211
+ executor: async (input) => {
2212
+ const content = input.content;
2213
+ const supersedes = input.supersedes;
2214
+ const invalidates = Array.isArray(input.invalidates) ? input.invalidates.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
2215
+ const asConstraint = input.constraint === true;
2216
+ const sessionId = input.sessionId;
2217
+ if (context.sessionMemory) {
2218
+ await context.sessionMemory.recordCorrection({
2219
+ content,
2220
+ invalidates,
2221
+ asConstraint
2222
+ });
2223
+ }
2224
+ const correction = {
2225
+ content,
2226
+ category: "user_input",
2227
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2228
+ type: "user_correction",
2229
+ metadata: {
2230
+ importance: 1,
2231
+ source: "user",
2232
+ supersedes,
2233
+ scope: "session"
2234
+ }
2235
+ };
2236
+ saveToSession(context, correction, sessionId);
2237
+ const supersedesInfo = supersedes ? ` (replaces: ${supersedes})` : "";
2238
+ const constraintInfo = asConstraint ? " [active session constraint]" : "";
2239
+ return {
2240
+ success: true,
2241
+ output: `\u26A0\uFE0F Correction stored in session${supersedesInfo}${constraintInfo}: ${content}`
2242
+ };
2243
+ }
2244
+ };
2245
+ }
2246
+ function createMemoryFindingTool(context) {
2247
+ return {
2248
+ definition: {
2249
+ type: "function",
2250
+ function: {
2251
+ name: "memory_finding",
2252
+ description: "Store a codebase finding with confidence level (session-scoped).",
2253
+ parameters: {
2254
+ type: "object",
2255
+ properties: {
2256
+ content: {
2257
+ type: "string",
2258
+ description: 'The finding (e.g., "All services use Repository pattern")'
2259
+ },
2260
+ confidence: {
2261
+ type: "number",
2262
+ description: "Confidence level 0-1 (e.g., 0.9 for high confidence)"
2263
+ },
2264
+ sources: {
2265
+ type: "array",
2266
+ items: { type: "string" },
2267
+ description: "Source files that support this finding"
2268
+ },
2269
+ sessionId: {
2270
+ type: "string",
2271
+ description: "Optional: session ID"
2272
+ }
2273
+ },
2274
+ required: ["content", "confidence"]
2275
+ }
2276
+ }
2277
+ },
2278
+ executor: async (input) => {
2279
+ const content = input.content;
2280
+ const confidence = input.confidence;
2281
+ const sources = input.sources;
2282
+ const sessionId = input.sessionId;
2283
+ const finding = {
2284
+ content,
2285
+ category: "learning",
2286
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2287
+ type: "finding",
2288
+ metadata: {
2289
+ importance: confidence,
2290
+ confidence,
2291
+ source: "agent",
2292
+ tags: sources,
2293
+ scope: "session"
2294
+ }
2295
+ };
2296
+ saveToSession(context, finding, sessionId);
2297
+ return {
2298
+ success: true,
2299
+ output: `\u{1F50D} Finding stored (confidence: ${(confidence * 100).toFixed(0)}%): ${content}`
2300
+ };
2301
+ }
2302
+ };
2303
+ }
2304
+ function createMemoryBlockerTool(context) {
2305
+ return {
2306
+ definition: {
2307
+ type: "function",
2308
+ function: {
2309
+ name: "memory_blocker",
2310
+ description: "Store a blocker that prevents progress (session-scoped).",
2311
+ parameters: {
2312
+ type: "object",
2313
+ properties: {
2314
+ content: {
2315
+ type: "string",
2316
+ description: 'The blocker (e.g., "Cannot find database config. Need DATABASE_URL")'
2317
+ },
2318
+ taskId: {
2319
+ type: "string",
2320
+ description: "Optional: related task ID"
2321
+ },
2322
+ sessionId: {
2323
+ type: "string",
2324
+ description: "Optional: session ID"
2325
+ }
2326
+ },
2327
+ required: ["content"]
2328
+ }
2329
+ }
2330
+ },
2331
+ executor: async (input) => {
2332
+ const content = input.content;
2333
+ const taskId = input.taskId;
2334
+ const sessionId = input.sessionId;
2335
+ const blocker = {
2336
+ content,
2337
+ category: "agent_state",
2338
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2339
+ type: "blocker",
2340
+ metadata: {
2341
+ importance: 1,
2342
+ source: "agent",
2343
+ taskId,
2344
+ scope: "session"
2345
+ }
2346
+ };
2347
+ saveToSession(context, blocker, sessionId);
2348
+ return {
2349
+ success: true,
2350
+ output: `\u{1F6D1} Blocker stored: ${content}`
2351
+ };
2352
+ }
2353
+ };
2354
+ }
2355
+ function createSessionSaveTool(context) {
2356
+ return {
2357
+ definition: {
2358
+ type: "function",
2359
+ function: {
2360
+ name: "session_save",
2361
+ description: "Save session summary and learnings to persistent memory.",
2362
+ parameters: {
2363
+ type: "object",
2364
+ properties: {
2365
+ sessionId: {
2366
+ type: "string",
2367
+ description: "Session identifier"
2368
+ },
2369
+ tasks: {
2370
+ type: "array",
2371
+ items: { type: "string" },
2372
+ description: "List of tasks completed in this session"
2373
+ },
2374
+ learnings: {
2375
+ type: "string",
2376
+ description: "Summary of what was learned or discovered"
2377
+ }
2378
+ },
2379
+ required: ["sessionId", "tasks", "learnings"]
2380
+ }
2381
+ }
2382
+ },
2383
+ executor: async (input) => {
2384
+ const sessionId = input.sessionId;
2385
+ const tasks = input.tasks;
2386
+ const learnings = input.learnings;
2387
+ const memory = loadSharedMemory(context);
2388
+ const session = {
2389
+ sessionId,
2390
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2391
+ tasks,
2392
+ learnings
2393
+ };
2394
+ memory.sessions.push(session);
2395
+ if (memory.sessions.length > 50) {
2396
+ memory.sessions = memory.sessions.slice(-50);
2397
+ }
2398
+ saveSharedMemory(context, memory);
2399
+ return {
2400
+ success: true,
2401
+ output: `Session ${sessionId} saved with ${tasks.length} task(s)`
2402
+ };
2403
+ }
2404
+ };
2405
+ }
2406
+
2407
+ // src/tools/memory/archive-recall.ts
2408
+ function createArchiveRecallTool(context) {
2409
+ return {
2410
+ definition: {
2411
+ type: "function",
2412
+ function: {
2413
+ name: "archive_recall",
2414
+ description: "Retrieve full content from previously-read files or tool outputs WITHOUT re-reading them. Use this to recall file contents, grep results, or any tool output from earlier iterations. Specify exactly one query mode: file_path, keyword, list_files, tool_name, or iteration.",
2415
+ parameters: {
2416
+ type: "object",
2417
+ properties: {
2418
+ file_path: {
2419
+ type: "string",
2420
+ description: "Recall the last read of this file (full content)"
2421
+ },
2422
+ keyword: {
2423
+ type: "string",
2424
+ description: "Search across all archived outputs for this keyword"
2425
+ },
2426
+ list_files: {
2427
+ type: "boolean",
2428
+ description: "List all file paths that have been archived"
2429
+ },
2430
+ tool_name: {
2431
+ type: "string",
2432
+ description: "Recall recent outputs of a specific tool (e.g., grep_search)"
2433
+ },
2434
+ iteration: {
2435
+ type: "number",
2436
+ description: "Recall all tool outputs from a specific iteration number"
2437
+ },
2438
+ limit: {
2439
+ type: "number",
2440
+ description: "Maximum number of results to return (default: 5)"
2441
+ }
2442
+ }
2443
+ }
2444
+ }
2445
+ },
2446
+ executor: async (input) => {
2447
+ const archive = context.archiveMemory;
2448
+ if (!archive) {
2449
+ return {
2450
+ success: false,
2451
+ error: "Archive memory not available. This tool requires the two-tier memory system."
2452
+ };
2453
+ }
2454
+ const limit = input.limit || 5;
2455
+ if (input.file_path) {
2456
+ const filePath = input.file_path;
2457
+ const entry = archive.recallByFilePath(filePath);
2458
+ if (!entry) {
2459
+ return {
2460
+ success: false,
2461
+ output: `File not found in archive: ${filePath}
2462
+
2463
+ Archived files:
2464
+ ${archive.getArchivedFilePaths().join("\n") || "(none)"}`
2465
+ };
2466
+ }
2467
+ return {
2468
+ success: true,
2469
+ output: `[Archive] File: ${filePath} (read in iteration ${entry.iteration} via ${entry.toolName})
2470
+
2471
+ ${entry.fullOutput}`
2472
+ };
2473
+ }
2474
+ if (input.keyword) {
2475
+ const keyword = input.keyword;
2476
+ const results = archive.search(keyword, limit);
2477
+ if (results.length === 0) {
2478
+ return {
2479
+ success: true,
2480
+ output: `No archived outputs contain "${keyword}"`
2481
+ };
2482
+ }
2483
+ const formatted = results.map((r, i) => {
2484
+ const fileInfo = r.filePath ? ` (file: ${r.filePath})` : "";
2485
+ const preview = r.fullOutput.length > 500 ? r.fullOutput.substring(0, 500) + `
2486
+ ... (${r.fullOutput.length} chars total)` : r.fullOutput;
2487
+ return `--- Result ${i + 1}: ${r.toolName} @ iter ${r.iteration}${fileInfo} ---
2488
+ ${preview}`;
2489
+ }).join("\n\n");
2490
+ return {
2491
+ success: true,
2492
+ output: `[Archive] Found ${results.length} result(s) for "${keyword}":
2493
+
2494
+ ${formatted}`
2495
+ };
2496
+ }
2497
+ if (input.list_files) {
2498
+ const files = archive.getArchivedFilePaths();
2499
+ if (files.length === 0) {
2500
+ return { success: true, output: "No files archived yet." };
2501
+ }
2502
+ return {
2503
+ success: true,
2504
+ output: `[Archive] ${files.length} file(s) archived:
2505
+ ${files.join("\n")}`
2506
+ };
2507
+ }
2508
+ if (input.tool_name) {
2509
+ const toolName = input.tool_name;
2510
+ const results = archive.recallByToolName(toolName, limit);
2511
+ if (results.length === 0) {
2512
+ return {
2513
+ success: true,
2514
+ output: `No archived outputs for tool "${toolName}"`
2515
+ };
2516
+ }
2517
+ const formatted = results.map((r, i) => {
2518
+ const fileInfo = r.filePath ? ` (file: ${r.filePath})` : "";
2519
+ const preview = r.fullOutput.length > 500 ? r.fullOutput.substring(0, 500) + `
2520
+ ... (${r.fullOutput.length} chars total)` : r.fullOutput;
2521
+ return `--- ${toolName} #${i + 1} @ iter ${r.iteration}${fileInfo} ---
2522
+ ${preview}`;
2523
+ }).join("\n\n");
2524
+ return {
2525
+ success: true,
2526
+ output: `[Archive] ${results.length} output(s) from "${toolName}":
2527
+
2528
+ ${formatted}`
2529
+ };
2530
+ }
2531
+ if (input.iteration !== void 0) {
2532
+ const iteration = input.iteration;
2533
+ const results = archive.recallByIteration(iteration);
2534
+ if (results.length === 0) {
2535
+ return {
2536
+ success: true,
2537
+ output: `No archived outputs from iteration ${iteration}`
2538
+ };
2539
+ }
2540
+ const formatted = results.map((r, i) => {
2541
+ const fileInfo = r.filePath ? ` (file: ${r.filePath})` : "";
2542
+ const preview = r.fullOutput.length > 500 ? r.fullOutput.substring(0, 500) + `
2543
+ ... (${r.fullOutput.length} chars total)` : r.fullOutput;
2544
+ return `--- ${r.toolName} #${i + 1}${fileInfo} ---
2545
+ ${preview}`;
2546
+ }).join("\n\n");
2547
+ return {
2548
+ success: true,
2549
+ output: `[Archive] ${results.length} output(s) from iteration ${iteration}:
2550
+
2551
+ ${formatted}`
2552
+ };
2553
+ }
2554
+ return {
2555
+ success: false,
2556
+ error: "Specify at least one query parameter: file_path, keyword, list_files, tool_name, or iteration"
2557
+ };
2558
+ }
2559
+ };
2560
+ }
2561
+
2562
+ // src/tools/planning/todo.ts
2563
+ var todoLists = /* @__PURE__ */ new Map();
2564
+ var TODO_CACHE_PREFIX = TODO_CONFIG.cachePrefix;
2565
+ var TODO_CACHE_TTL_MS = TODO_CONFIG.cacheTtlMs;
2566
+ function getTodoCacheKey(sessionId) {
2567
+ return `${TODO_CACHE_PREFIX}${sessionId}`;
2568
+ }
2569
+ async function loadTodoList(context, sessionId) {
2570
+ if (context.cache) {
2571
+ const cached = await context.cache.get(getTodoCacheKey(sessionId));
2572
+ if (cached) {
2573
+ todoLists.set(sessionId, cached);
2574
+ return cached;
2575
+ }
2576
+ }
2577
+ return todoLists.get(sessionId) ?? null;
2578
+ }
2579
+ async function persistTodoList(context, todoList) {
2580
+ todoLists.set(todoList.sessionId, todoList);
2581
+ if (context.cache) {
2582
+ await context.cache.set(getTodoCacheKey(todoList.sessionId), todoList, TODO_CACHE_TTL_MS);
2583
+ }
2584
+ }
2585
+ function resolveTodoItem(todoList, sessionId, itemId) {
2586
+ const direct = todoList.items.find((i) => i.id === itemId);
2587
+ if (direct) {
2588
+ return direct;
2589
+ }
2590
+ const asIndex = Number(itemId);
2591
+ if (Number.isFinite(asIndex) && asIndex >= 1) {
2592
+ const indexed = todoList.items.find((i) => i.id === `${sessionId}-${Math.floor(asIndex)}`);
2593
+ if (indexed) {
2594
+ return indexed;
2595
+ }
2596
+ }
2597
+ const normalized = itemId.trim().toLowerCase();
2598
+ if (!normalized) {
2599
+ return null;
2600
+ }
2601
+ const descriptionMatches = todoList.items.filter(
2602
+ (i) => i.description.toLowerCase().includes(normalized)
2603
+ );
2604
+ if (descriptionMatches.length === 1) {
2605
+ return descriptionMatches[0];
2606
+ }
2607
+ return null;
2608
+ }
2609
+ function createTodoCreateTool(context) {
2610
+ return {
2611
+ definition: {
2612
+ type: "function",
2613
+ function: {
2614
+ name: "todo_create",
2615
+ description: "Create a TODO list to track task progress. Optional - use if helpful for planning complex tasks.",
2616
+ parameters: {
2617
+ type: "object",
2618
+ properties: {
2619
+ sessionId: {
2620
+ type: "string",
2621
+ description: "Session identifier to associate TODO list"
2622
+ },
2623
+ items: {
2624
+ type: "array",
2625
+ items: {
2626
+ type: "object",
2627
+ properties: {
2628
+ description: {
2629
+ type: "string",
2630
+ description: "Task description"
2631
+ },
2632
+ priority: {
2633
+ type: "string",
2634
+ enum: ["low", "medium", "high"],
2635
+ description: "Task priority"
2636
+ }
2637
+ },
2638
+ required: ["description"]
2639
+ },
2640
+ description: "List of tasks"
2641
+ }
2642
+ },
2643
+ required: ["sessionId", "items"]
2644
+ }
2645
+ }
2646
+ },
2647
+ executor: async (input) => {
2648
+ const sessionId = input.sessionId;
2649
+ const items = input.items;
2650
+ const todoItems = items.map((item, index) => ({
2651
+ id: `${sessionId}-${index + 1}`,
2652
+ description: item.description,
2653
+ status: "pending",
2654
+ priority: item.priority || "medium",
2655
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2656
+ }));
2657
+ const todoList = {
2658
+ sessionId,
2659
+ items: todoItems,
2660
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2661
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2662
+ };
2663
+ await persistTodoList(context, todoList);
2664
+ const output = [
2665
+ `TODO list created for session: ${sessionId}`,
2666
+ "",
2667
+ ...todoItems.map(
2668
+ (item, i) => `${i + 1}. [ ] ${item.description} (${item.priority})`
2669
+ )
2670
+ ].join("\n");
2671
+ return {
2672
+ success: true,
2673
+ output,
2674
+ metadata: {
2675
+ uiHint: "todo",
2676
+ structured: { todoList }
2677
+ }
2678
+ };
2679
+ }
2680
+ };
2681
+ }
2682
+ function createTodoUpdateTool(context) {
2683
+ return {
2684
+ definition: {
2685
+ type: "function",
2686
+ function: {
2687
+ name: "todo_update",
2688
+ description: "Update status of a TODO item. Use to mark tasks as in-progress or completed.",
2689
+ parameters: {
2690
+ type: "object",
2691
+ properties: {
2692
+ sessionId: {
2693
+ type: "string",
2694
+ description: "Session identifier"
2695
+ },
2696
+ itemId: {
2697
+ type: "string",
2698
+ description: "TODO item ID to update"
2699
+ },
2700
+ status: {
2701
+ type: "string",
2702
+ enum: ["pending", "in-progress", "completed", "blocked"],
2703
+ description: "New status"
2704
+ },
2705
+ notes: {
2706
+ type: "string",
2707
+ description: "Optional notes about the update"
2708
+ }
2709
+ },
2710
+ required: ["sessionId", "itemId", "status"]
2711
+ }
2712
+ }
2713
+ },
2714
+ executor: async (input) => {
2715
+ const sessionId = input.sessionId;
2716
+ const itemId = input.itemId;
2717
+ const status = input.status;
2718
+ const notes = input.notes;
2719
+ const todoList = await loadTodoList(context, sessionId);
2720
+ if (!todoList) {
2721
+ return toolError({
2722
+ code: "TODO_LIST_NOT_FOUND",
2723
+ message: `No TODO list found for session: ${sessionId}`,
2724
+ retryable: true,
2725
+ hint: "Call todo_create first, then todo_update.",
2726
+ details: { sessionId }
2727
+ });
2728
+ }
2729
+ const item = resolveTodoItem(todoList, sessionId, itemId);
2730
+ if (!item) {
2731
+ return toolError({
2732
+ code: "TODO_ITEM_NOT_FOUND",
2733
+ message: `TODO item not found: ${itemId}`,
2734
+ retryable: true,
2735
+ hint: "Use todo_get to list valid item IDs, then retry with exact itemId.",
2736
+ details: {
2737
+ sessionId,
2738
+ itemId,
2739
+ knownIds: todoList.items.map((i) => i.id)
2740
+ }
2741
+ });
2742
+ }
2743
+ item.status = status;
2744
+ item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2745
+ if (notes) {
2746
+ item.notes = notes;
2747
+ }
2748
+ todoList.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2749
+ await persistTodoList(context, todoList);
2750
+ const statusIcon = status === "completed" ? "\u2713" : status === "in-progress" ? "\u23F3" : status === "blocked" ? "\u{1F6AB}" : " ";
2751
+ const updatedList = await loadTodoList(context, sessionId);
2752
+ return {
2753
+ success: true,
2754
+ output: `[${statusIcon}] ${item.description} \u2192 ${status}${notes ? `
2755
+ Note: ${notes}` : ""}`,
2756
+ metadata: {
2757
+ uiHint: "todo",
2758
+ structured: { todoList: updatedList }
2759
+ }
2760
+ };
2761
+ }
2762
+ };
2763
+ }
2764
+ function createTodoGetTool(context) {
2765
+ return {
2766
+ definition: {
2767
+ type: "function",
2768
+ function: {
2769
+ name: "todo_get",
2770
+ description: "Get current TODO list with task statuses. Use to check progress.",
2771
+ parameters: {
2772
+ type: "object",
2773
+ properties: {
2774
+ sessionId: {
2775
+ type: "string",
2776
+ description: "Session identifier"
2777
+ }
2778
+ },
2779
+ required: ["sessionId"]
2780
+ }
2781
+ }
2782
+ },
2783
+ executor: async (input) => {
2784
+ const sessionId = input.sessionId;
2785
+ const todoList = await loadTodoList(context, sessionId);
2786
+ if (!todoList) {
2787
+ return toolError({
2788
+ code: "TODO_LIST_NOT_FOUND",
2789
+ message: `No TODO list found for session: ${sessionId}`,
2790
+ retryable: true,
2791
+ hint: "Create a TODO list with todo_create before calling todo_get.",
2792
+ details: { sessionId }
2793
+ });
2794
+ }
2795
+ const completed = todoList.items.filter((i) => i.status === "completed").length;
2796
+ const total = todoList.items.length;
2797
+ const output = [
2798
+ `TODO List for ${sessionId} (${completed}/${total} completed)`,
2799
+ "",
2800
+ ...todoList.items.map((item) => {
2801
+ const icon = item.status === "completed" ? "\u2713" : item.status === "in-progress" ? "\u23F3" : item.status === "blocked" ? "\u{1F6AB}" : " ";
2802
+ return `[${icon}] ${item.description} (${item.priority})${item.notes ? `
2803
+ ${item.notes}` : ""}`;
2804
+ })
2805
+ ].join("\n");
2806
+ return {
2807
+ success: true,
2808
+ output,
2809
+ metadata: {
2810
+ uiHint: "todo",
2811
+ structured: { todoList }
2812
+ }
2813
+ };
2814
+ }
2815
+ };
2816
+ }
2817
+ var EVAL_SYSTEM_PROMPT = `You are a plan quality evaluator. Evaluate implementation plans written by AI agents.
2818
+
2819
+ You will receive the original user task and the plan markdown.
2820
+
2821
+ Score the plan on four dimensions (0.0\u20131.0 each):
2822
+
2823
+ **specificity** (weight 0.30)
2824
+ - Does the plan reference real file paths from the codebase?
2825
+ - Are function names, class names, or line numbers mentioned where relevant?
2826
+ - For simple tasks (1-2 files), mentioning those files is sufficient for a high score.
2827
+ - For complex tasks (many files), more paths are expected.
2828
+
2829
+ **actionability** (weight 0.35)
2830
+ - Can each step be executed without guessing what to do?
2831
+ - Steps should contain: what file to edit, what to change, or what command to run.
2832
+ - "Edit X to add Y" is actionable. "Update the configuration" is not.
2833
+
2834
+ **completeness** (weight 0.15)
2835
+ - Does the plan have: task description, steps, verification?
2836
+ - Are sections substantive (not just headings)?
2837
+
2838
+ **verification** (weight 0.20)
2839
+ - Does the plan include concrete commands to verify correctness?
2840
+ - At least one build or test command expected.
2841
+
2842
+ Respond in EXACTLY this format (no other text before or after):
2843
+ SCORE: 0.XX
2844
+ SPECIFICITY: 0.XX
2845
+ ACTIONABILITY: 0.XX
2846
+ COMPLETENESS: 0.XX
2847
+ VERIFICATION: 0.XX
2848
+ FEEDBACK:
2849
+ <One paragraph per dimension scoring below 0.7. Quote the weak part, explain the problem, give a fix example. If all dimensions \u2265 0.7, write "Plan meets quality threshold.">
2850
+
2851
+ IMPORTANT: Only output the format above. Do NOT output tool calls, code blocks, or any other format.`;
2852
+ function buildEvalPrompt(task, planMarkdown) {
2853
+ return `## Original user task
2854
+
2855
+ ${task}
2856
+
2857
+ ## Plan to evaluate
2858
+
2859
+ ${planMarkdown}`;
2860
+ }
2861
+ var PASS_THRESHOLD = 0.55;
2862
+ function formatResult(raw, tier) {
2863
+ const lines = raw.split("\n");
2864
+ const get = (key) => {
2865
+ const line = lines.find((l) => l.toUpperCase().startsWith(key.toUpperCase() + ":"));
2866
+ return line?.slice(line.indexOf(":") + 1).trim() ?? "";
2867
+ };
2868
+ const specificity = parseFloat(get("SPECIFICITY") || "0");
2869
+ const actionability = parseFloat(get("ACTIONABILITY") || "0");
2870
+ const completeness = parseFloat(get("COMPLETENESS") || "0");
2871
+ const verification = parseFloat(get("VERIFICATION") || "0");
2872
+ const composite = specificity * 0.3 + actionability * 0.35 + completeness * 0.15 + verification * 0.2;
2873
+ const passed = composite >= PASS_THRESHOLD;
2874
+ if (specificity === 0 && actionability === 0 && completeness === 0 && verification === 0) {
2875
+ return [
2876
+ `\u26A0\uFE0F EVAL_ERROR \u2014 LLM returned unparseable response (${tier} tier)`,
2877
+ "",
2878
+ "Raw response (first 300 chars):",
2879
+ raw.slice(0, 300),
2880
+ "",
2881
+ "ACTION: Try again with a different tier, or call report() if you believe the plan is ready."
2882
+ ].join("\n");
2883
+ }
2884
+ const feedbackIdx = raw.toUpperCase().indexOf("FEEDBACK:");
2885
+ const feedback = feedbackIdx >= 0 ? raw.slice(feedbackIdx + "FEEDBACK:".length).trim() : "";
2886
+ const status = passed ? "\u2705 PASSED" : "\u274C FAILED";
2887
+ const scoreBar = Math.round(composite * 10);
2888
+ const bar = "\u2588".repeat(scoreBar) + "\u2591".repeat(10 - scoreBar);
2889
+ return [
2890
+ `${status} \u2014 score: ${composite.toFixed(2)} [${bar}] (threshold ${PASS_THRESHOLD}, ${tier} tier)`,
2891
+ "",
2892
+ ` specificity: ${specificity.toFixed(2)} (weight 0.30)`,
2893
+ ` actionability: ${actionability.toFixed(2)} (weight 0.35)`,
2894
+ ` completeness: ${completeness.toFixed(2)} (weight 0.15)`,
2895
+ ` verification: ${verification.toFixed(2)} (weight 0.20)`,
2896
+ "",
2897
+ ...feedback ? ["\u2500\u2500\u2500 Feedback \u2500\u2500\u2500", feedback, ""] : [],
2898
+ passed ? "ACTION: Call report() with the plan \u2014 it is ready for user approval." : "ACTION: Revise the plan addressing the feedback above, then call plan_validate again.\n After 3 failed validations, call ask_user() with the plan and this feedback."
2899
+ ].join("\n");
2900
+ }
2901
+ function createPlanValidateTool(context) {
2902
+ return {
2903
+ definition: {
2904
+ type: "function",
2905
+ function: {
2906
+ name: "plan_validate",
2907
+ description: `Evaluate a plan draft against quality criteria using an LLM judge.
2908
+ Returns pass/fail, per-dimension scores, and concrete actionable feedback on what to fix.
2909
+ Call this after writing a plan draft, before calling report().
2910
+ If it fails, revise the plan based on the feedback and call plan_validate again.
2911
+ After 3 consecutive failures, call ask_user() with the plan and the feedback to escalate to the human.`,
2912
+ parameters: {
2913
+ type: "object",
2914
+ properties: {
2915
+ task: {
2916
+ type: "string",
2917
+ description: "The original user task/request that the plan is supposed to address. Copied verbatim \u2014 the evaluator needs this to judge relevance."
2918
+ },
2919
+ plan_markdown: {
2920
+ type: "string",
2921
+ description: "The full plan markdown to evaluate."
2922
+ },
2923
+ tier: {
2924
+ type: "string",
2925
+ enum: ["small", "medium", "large"],
2926
+ description: "LLM tier for evaluation. Default: small (fast, cheap). Use medium/large for complex plans or if small gives inconsistent results."
2927
+ }
2928
+ },
2929
+ required: ["task", "plan_markdown"]
2930
+ }
2931
+ }
2932
+ },
2933
+ executor: async (input) => {
2934
+ const task = input.task;
2935
+ const planMarkdown = input.plan_markdown;
2936
+ const tier = input.tier ?? "small";
2937
+ if (!task?.trim()) {
2938
+ return { success: false, output: "ERROR: task is required \u2014 provide the original user request so the evaluator can judge plan relevance." };
2939
+ }
2940
+ if (!planMarkdown?.trim()) {
2941
+ return { success: false, output: "ERROR: plan_markdown is required \u2014 provide the full plan markdown to evaluate." };
2942
+ }
2943
+ const llm = useLLM({ tier });
2944
+ if (!llm) {
2945
+ return { success: false, output: `ERROR: LLM tier "${tier}" is not available. Try a different tier or check platform configuration.` };
2946
+ }
2947
+ try {
2948
+ const response = await llm.complete(buildEvalPrompt(task, planMarkdown), {
2949
+ systemPrompt: EVAL_SYSTEM_PROMPT,
2950
+ maxTokens: 1500
2951
+ });
2952
+ const formatted = formatResult(response.content, tier);
2953
+ if (formatted.startsWith("\u2705 PASSED") || formatted.startsWith("\u26A0\uFE0F EVAL_ERROR")) {
2954
+ context.planValidationPassed = true;
2955
+ }
2956
+ return { success: true, output: formatted };
2957
+ } catch (err) {
2958
+ const message = err instanceof Error ? err.message : String(err);
2959
+ return { success: false, output: `ERROR: LLM evaluation failed: ${message}` };
2960
+ }
2961
+ }
2962
+ };
2963
+ }
2964
+ function createPlanWriteTool(context) {
2965
+ return {
2966
+ definition: {
2967
+ type: "function",
2968
+ function: {
2969
+ name: "plan_write",
2970
+ description: `Write or update the plan file for this session. The plan is saved to disk and survives context compaction. Use this to iteratively build your plan during exploration \u2014 write what you've learned so far, then continue exploring. The user can view the plan file at any time.`,
2971
+ parameters: {
2972
+ type: "object",
2973
+ properties: {
2974
+ content: {
2975
+ type: "string",
2976
+ description: "Full markdown content for the plan file (replaces previous content)"
2977
+ },
2978
+ append: {
2979
+ type: "string",
2980
+ description: "Text to append to the existing plan (mutually exclusive with content)"
2981
+ }
2982
+ }
2983
+ }
2984
+ }
2985
+ },
2986
+ executor: async (input) => {
2987
+ const content = input.content;
2988
+ const append = input.append;
2989
+ if (!content && !append) {
2990
+ return toolError({
2991
+ code: "MISSING_INPUT",
2992
+ message: "Either content or append must be provided.",
2993
+ retryable: false,
2994
+ hint: "Provide content= for full replacement, or append= to add to existing plan."
2995
+ });
2996
+ }
2997
+ if (content && append) {
2998
+ return toolError({
2999
+ code: "AMBIGUOUS_INPUT",
3000
+ message: "Cannot use both content and append at the same time.",
3001
+ retryable: false,
3002
+ hint: "Use content= to replace the entire plan, or append= to add a section."
3003
+ });
3004
+ }
3005
+ const sessionId = context.sessionId;
3006
+ if (!sessionId) {
3007
+ return toolError({
3008
+ code: "NO_SESSION",
3009
+ message: "No session ID available. Plan write requires an active session.",
3010
+ retryable: false
3011
+ });
3012
+ }
3013
+ const planDir = path4.join(context.workingDir, ".kb", "agents", "sessions", sessionId);
3014
+ const planPath = path4.join(planDir, "plan.md");
3015
+ if (!fs2.existsSync(planDir)) {
3016
+ fs2.mkdirSync(planDir, { recursive: true });
3017
+ }
3018
+ let finalContent;
3019
+ if (content) {
3020
+ finalContent = content;
3021
+ } else {
3022
+ let existing = "";
3023
+ try {
3024
+ existing = fs2.readFileSync(planPath, "utf-8");
3025
+ } catch {
3026
+ }
3027
+ finalContent = existing ? `${existing.trimEnd()}
3028
+
3029
+ ${append}` : append;
3030
+ }
3031
+ fs2.writeFileSync(planPath, finalContent, "utf-8");
3032
+ const lineCount = finalContent.split("\n").length;
3033
+ const mode = content ? "replaced" : "appended to";
3034
+ return {
3035
+ success: true,
3036
+ output: `Plan ${mode}: ${planPath}
3037
+ Lines: ${lineCount}`,
3038
+ metadata: {
3039
+ planPath,
3040
+ lineCount,
3041
+ mode: content ? "replace" : "append"
3042
+ }
3043
+ };
3044
+ }
3045
+ };
3046
+ }
3047
+ var MAX_OUTPUT_CHARS3 = 8e3;
3048
+ function trimOutput3(output, label) {
3049
+ if (output.length <= MAX_OUTPUT_CHARS3) {
3050
+ return output;
3051
+ }
3052
+ const head = output.slice(0, MAX_OUTPUT_CHARS3 * 0.3);
3053
+ const tail = output.slice(-Math.floor(MAX_OUTPUT_CHARS3 * 0.7));
3054
+ return `${head}
3055
+
3056
+ \u26A0\uFE0F ${label.toUpperCase()} TRIMMED (${output.length.toLocaleString()} chars, showing head+tail)
3057
+ \u{1F4A1} Pipe through grep/head/tail in your command to get focused output.
3058
+
3059
+ ...${tail}`;
3060
+ }
3061
+ function createShellExecTool(context) {
3062
+ return {
3063
+ definition: {
3064
+ type: "function",
3065
+ function: {
3066
+ name: "shell_exec",
3067
+ description: "Execute a shell command and return output. Use for running builds, tests, git commands, etc. Commands run in the working directory.",
3068
+ parameters: {
3069
+ type: "object",
3070
+ properties: {
3071
+ command: {
3072
+ type: "string",
3073
+ description: "Shell command to execute"
3074
+ },
3075
+ cwd: {
3076
+ type: "string",
3077
+ description: "Optional working directory relative to agent workingDir. Use to run command in a specific package/subdirectory."
3078
+ }
3079
+ },
3080
+ required: ["command"]
3081
+ }
3082
+ }
3083
+ },
3084
+ executor: async (input) => {
3085
+ const command = input.command;
3086
+ const requestedCwd = typeof input.cwd === "string" ? input.cwd.trim() : "";
3087
+ const resolvedCwd = requestedCwd ? path4__default.resolve(context.workingDir, requestedCwd) : context.workingDir;
3088
+ const relative3 = path4__default.relative(context.workingDir, resolvedCwd);
3089
+ if (relative3.startsWith("..") || path4__default.isAbsolute(relative3)) {
3090
+ return toolError({
3091
+ code: "INVALID_CWD",
3092
+ message: `Invalid cwd "${requestedCwd}" - outside working directory.`,
3093
+ retryable: false,
3094
+ hint: "Use cwd relative to the agent working directory.",
3095
+ details: { requestedCwd, resolvedCwd, workingDir: context.workingDir }
3096
+ });
3097
+ }
3098
+ try {
3099
+ const output = execSync(command, {
3100
+ cwd: resolvedCwd,
3101
+ encoding: "utf-8",
3102
+ maxBuffer: SHELL_CONFIG.maxBuffer,
3103
+ stdio: ["pipe", "pipe", "pipe"]
3104
+ });
3105
+ const trimmed = trimOutput3(output || "", "stdout");
3106
+ return {
3107
+ success: true,
3108
+ output: `[cwd: ${resolvedCwd}]
3109
+ ${trimmed || "(command completed with no output)"}`,
3110
+ metadata: {
3111
+ cwd: resolvedCwd,
3112
+ outputLength: output?.length ?? 0,
3113
+ trimmed: (output?.length ?? 0) > MAX_OUTPUT_CHARS3
3114
+ }
3115
+ };
3116
+ } catch (error) {
3117
+ const stderr = error.stderr?.toString() || "";
3118
+ const stdout = error.stdout?.toString() || "";
3119
+ const exitCode = error.status ?? -1;
3120
+ const commandText = typeof command === "string" ? command.trim() : "";
3121
+ if (error.code === "ETIMEDOUT" || error.signal === "SIGTERM") {
3122
+ return toolError({
3123
+ code: "SHELL_TIMEOUT",
3124
+ message: `Command timed out in ${resolvedCwd}`,
3125
+ retryable: true,
3126
+ hint: "Use a narrower command or run package-scoped command via cwd.",
3127
+ details: { cwd: resolvedCwd, command: commandText, exitCode }
3128
+ });
3129
+ }
3130
+ if (/command not found/i.test(stderr) || exitCode === 127) {
3131
+ return toolError({
3132
+ code: "COMMAND_NOT_FOUND",
3133
+ message: `Command not found: ${commandText}`,
3134
+ retryable: false,
3135
+ hint: "Check binary name or ensure dependency is installed in this workspace.",
3136
+ details: { cwd: resolvedCwd, command: commandText, exitCode }
3137
+ });
3138
+ }
3139
+ if (/permission denied/i.test(stderr) || exitCode === 126) {
3140
+ return toolError({
3141
+ code: "PERMISSION_DENIED",
3142
+ message: `Permission denied while executing command in ${resolvedCwd}`,
3143
+ retryable: false,
3144
+ hint: "Check file permissions and executable bits.",
3145
+ details: { cwd: resolvedCwd, command: commandText, exitCode }
3146
+ });
3147
+ }
3148
+ const stdoutDisplay = trimOutput3(stdout, "stdout");
3149
+ const stderrDisplay = trimOutput3(stderr, "stderr");
3150
+ return toolError({
3151
+ code: "NON_ZERO_EXIT",
3152
+ message: `Command failed with exit code ${exitCode}`,
3153
+ retryable: true,
3154
+ hint: "Inspect stderr/stdout below. Pipe output through grep/head/tail to narrow it down.",
3155
+ details: {
3156
+ cwd: resolvedCwd,
3157
+ exitCode,
3158
+ stdout: stdoutDisplay || void 0,
3159
+ stderr: stderrDisplay || void 0
3160
+ }
3161
+ });
3162
+ }
3163
+ }
3164
+ };
3165
+ }
3166
+
3167
+ // src/tools/execution/async-tasks.ts
3168
+ function createTaskSubmitTool(context) {
3169
+ return {
3170
+ definition: {
3171
+ type: "function",
3172
+ function: {
3173
+ name: "task_submit",
3174
+ description: "Start a sub-agent task in the background. Returns immediately with a task ID.\nThe sub-agent runs concurrently \u2014 you can continue working while it executes.\nUse task_status to check progress or task_collect to wait for results.",
3175
+ parameters: {
3176
+ type: "object",
3177
+ properties: {
3178
+ description: {
3179
+ type: "string",
3180
+ description: "Short description of the task (for tracking)."
3181
+ },
3182
+ task: {
3183
+ type: "string",
3184
+ description: "Detailed task description for the sub-agent."
3185
+ },
3186
+ preset: {
3187
+ type: "string",
3188
+ enum: ["research", "execute", "review"],
3189
+ description: 'Sub-agent preset (default: "research").'
3190
+ },
3191
+ budgetPercent: {
3192
+ type: "number",
3193
+ description: "Percent of remaining budget to allocate (1-100, default: 20)."
3194
+ }
3195
+ },
3196
+ required: ["description", "task"]
3197
+ }
3198
+ }
3199
+ },
3200
+ executor: async (input) => {
3201
+ if (!context.taskManager) {
3202
+ return {
3203
+ success: false,
3204
+ error: "Async task system not available. Task delegation is not enabled for this agent."
3205
+ };
3206
+ }
3207
+ const description = input.description;
3208
+ const task = input.task;
3209
+ const presetId = input.preset ?? "research";
3210
+ const preset = SUB_AGENT_PRESETS[presetId];
3211
+ if (!preset) {
3212
+ return { success: false, error: `Unknown preset: "${presetId}".` };
3213
+ }
3214
+ const budgetPercent = input.budgetPercent ?? 50;
3215
+ const budgetFraction = Math.min(1, Math.max(0, budgetPercent / 100));
3216
+ try {
3217
+ const asyncTask = await context.taskManager.submit(description, {
3218
+ task,
3219
+ preset: presetId,
3220
+ allowedTools: Array.from(preset.tools),
3221
+ maxIterations: preset.maxIterations,
3222
+ budgetFraction
3223
+ });
3224
+ return {
3225
+ success: true,
3226
+ output: `Task submitted: ${asyncTask.id}
3227
+ Description: ${description}
3228
+ Preset: ${presetId}
3229
+ Status: ${asyncTask.status}
3230
+
3231
+ Use task_status("${asyncTask.id}") to check progress or task_collect("${asyncTask.id}") to wait for results.`
3232
+ };
3233
+ } catch (error) {
3234
+ return {
3235
+ success: false,
3236
+ error: `Failed to submit task: ${error instanceof Error ? error.message : String(error)}`
3237
+ };
3238
+ }
3239
+ }
3240
+ };
3241
+ }
3242
+ function createTaskStatusTool(context) {
3243
+ return {
3244
+ definition: {
3245
+ type: "function",
3246
+ function: {
3247
+ name: "task_status",
3248
+ description: "Check status of one or all async tasks.\nWithout taskId: returns all tasks.\nWith taskId: returns details for that specific task.",
3249
+ parameters: {
3250
+ type: "object",
3251
+ properties: {
3252
+ taskId: {
3253
+ type: "string",
3254
+ description: "Specific task ID to check (omit for all tasks)."
3255
+ }
3256
+ },
3257
+ required: []
3258
+ }
3259
+ }
3260
+ },
3261
+ executor: async (input) => {
3262
+ if (!context.taskManager) {
3263
+ return { success: false, error: "Async task system not available." };
3264
+ }
3265
+ const taskId = input.taskId;
3266
+ const result = context.taskManager.getStatus(taskId);
3267
+ if (result === null) {
3268
+ return { success: false, error: `Task not found: "${taskId}"` };
3269
+ }
3270
+ const tasks = Array.isArray(result) ? result : [result];
3271
+ if (tasks.length === 0) {
3272
+ return { success: true, output: "No async tasks." };
3273
+ }
3274
+ const lines = tasks.map((t) => {
3275
+ const status = t.status === "completed" ? "\u2705" : t.status === "failed" ? "\u274C" : t.status === "running" ? "\u23F3" : "\u23F8\uFE0F";
3276
+ let line = `${status} ${t.id}: ${t.description} [${t.status}]`;
3277
+ if (t.result) {
3278
+ line += `
3279
+ Result: ${t.result.slice(0, 200)}`;
3280
+ }
3281
+ if (t.error) {
3282
+ line += `
3283
+ Error: ${t.error}`;
3284
+ }
3285
+ return line;
3286
+ });
3287
+ return { success: true, output: lines.join("\n") };
3288
+ }
3289
+ };
3290
+ }
3291
+ function createTaskCollectTool(context) {
3292
+ return {
3293
+ definition: {
3294
+ type: "function",
3295
+ function: {
3296
+ name: "task_collect",
3297
+ description: "Wait for a specific async task to complete and return its full result.\nBlocks until the task is done. Use task_status to check without waiting.",
3298
+ parameters: {
3299
+ type: "object",
3300
+ properties: {
3301
+ taskId: {
3302
+ type: "string",
3303
+ description: "The task ID to wait for."
3304
+ }
3305
+ },
3306
+ required: ["taskId"]
3307
+ }
3308
+ }
3309
+ },
3310
+ executor: async (input) => {
3311
+ if (!context.taskManager) {
3312
+ return { success: false, error: "Async task system not available." };
3313
+ }
3314
+ const taskId = input.taskId;
3315
+ try {
3316
+ const result = await context.taskManager.collect(taskId);
3317
+ const status = result.success ? "completed successfully" : "failed";
3318
+ const lines = [
3319
+ `Task ${taskId} ${status} [preset=${result.preset}, ${result.iterations} iters, ${result.tokensUsed.toLocaleString()} tokens, ${(result.durationMs / 1e3).toFixed(1)}s]`
3320
+ ];
3321
+ if (result.filesRead.length > 0) {
3322
+ lines.push(`Files read: ${result.filesRead.length}`);
3323
+ }
3324
+ if (result.filesModified.length > 0) {
3325
+ lines.push(`Files modified: ${result.filesModified.join(", ")}`);
3326
+ }
3327
+ if (result.filesCreated.length > 0) {
3328
+ lines.push(`Files created: ${result.filesCreated.join(", ")}`);
3329
+ }
3330
+ lines.push("", result.summary);
3331
+ if (result.error) {
3332
+ lines.push("", `Error: ${result.error}`);
3333
+ }
3334
+ return { success: result.success, output: lines.join("\n") };
3335
+ } catch (error) {
3336
+ return {
3337
+ success: false,
3338
+ error: `Failed to collect task: ${error instanceof Error ? error.message : String(error)}`
3339
+ };
3340
+ }
3341
+ }
3342
+ };
3343
+ }
3344
+ function createAskUserTool(_context) {
3345
+ return {
3346
+ definition: {
3347
+ type: "function",
3348
+ function: {
3349
+ name: "ask_user",
3350
+ description: "Ask the user a question and wait for their response. Use when you need clarification or additional information. In non-interactive mode, returns a default answer.",
3351
+ parameters: {
3352
+ type: "object",
3353
+ properties: {
3354
+ question: {
3355
+ type: "string",
3356
+ description: "Question to ask the user"
3357
+ }
3358
+ },
3359
+ required: ["question"]
3360
+ }
3361
+ }
3362
+ },
3363
+ executor: async (input) => {
3364
+ const question = input.question;
3365
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
3366
+ if (!isInteractive) {
3367
+ return {
3368
+ success: true,
3369
+ output: `[Auto-answer] Question was: "${question}". Proceeding with default behavior.`
3370
+ };
3371
+ }
3372
+ return new Promise((resolve3) => {
3373
+ const rl = readline.createInterface({
3374
+ input: process.stdin,
3375
+ output: process.stdout
3376
+ });
3377
+ rl.question(`
3378
+ \u2753 ${question}
3379
+ > `, (answer) => {
3380
+ rl.close();
3381
+ if (!answer || answer.trim() === "") {
3382
+ resolve3({
3383
+ success: false,
3384
+ error: "No answer provided"
3385
+ });
3386
+ } else {
3387
+ resolve3({
3388
+ success: true,
3389
+ output: answer.trim()
3390
+ });
3391
+ }
3392
+ });
3393
+ });
3394
+ }
3395
+ };
3396
+ }
3397
+ async function hasPendingCorrectionCommit(context) {
3398
+ const kernel = await context.sessionMemory?.loadKernelState();
3399
+ if (!kernel) {
3400
+ return false;
3401
+ }
3402
+ return kernel.memory.pendingActions.some(
3403
+ (action) => action.status !== "done" && action.content.startsWith("Persist user correction via memory_correction:")
3404
+ );
3405
+ }
3406
+ var VERIFY_REPORT_CLAIMS_PROMPT = [
3407
+ "Evaluate whether the proposed final agent answer is sufficiently supported by the provided session evidence.",
3408
+ "Prefer blocking only when the answer makes unsupported code, file, architecture, or behavioral claims.",
3409
+ "Allow simple session recall when kernel evidence is enough.",
3410
+ "If evidence is partial, prefer warn instead of block unless the answer states unsupported details as facts.",
3411
+ "Return strict JSON only."
3412
+ ].join(" ");
3413
+ async function verifyReportClaims(context, answer) {
3414
+ const kernel = await context.sessionMemory?.loadKernelState();
3415
+ const requirements = await resolveResponseRequirements(context, answer, kernel);
3416
+ const evidence = kernel?.memory.evidence ?? [];
3417
+ const recallFastPath = verifyEvidenceBackedRecall(answer, kernel, requirements);
3418
+ if (recallFastPath) {
3419
+ return recallFastPath;
3420
+ }
3421
+ const fallback = deterministicVerification(answer, kernel, requirements);
3422
+ const llm = useLLM$1({ tier: "small" });
3423
+ if (!llm?.complete) {
3424
+ return fallback;
3425
+ }
3426
+ try {
3427
+ const response = await llm.complete(buildClaimVerificationPrompt(answer, kernel, context), {
3428
+ systemPrompt: VERIFY_REPORT_CLAIMS_PROMPT,
3429
+ temperature: 0,
3430
+ maxTokens: 350
3431
+ });
3432
+ return normalizeClaimVerification(response.content, fallback, evidence);
3433
+ } catch {
3434
+ return fallback;
3435
+ }
3436
+ }
3437
+ async function resolveResponseRequirements(context, answer, kernel) {
3438
+ if (!context.responseRequirementsResolver) {
3439
+ return null;
3440
+ }
3441
+ try {
3442
+ return await context.responseRequirementsResolver({
3443
+ task: context.currentTask,
3444
+ answer,
3445
+ kernel: kernel ?? null
3446
+ });
3447
+ } catch {
3448
+ return null;
3449
+ }
3450
+ }
3451
+ function buildClaimVerificationPrompt(answer, kernel, context) {
3452
+ const recentEvidence = (kernel?.memory.evidence ?? []).slice(-6).map((item) => ({
3453
+ toolName: item.toolName ?? item.source,
3454
+ toolInputSummary: item.toolInputSummary,
3455
+ summary: item.summary
3456
+ }));
3457
+ const archiveEvidence = (kernel?.memory.evidence ?? []).slice(-4).flatMap((item) => {
3458
+ if (!item.toolInputSummary || !context.archiveMemory?.hasFile(item.toolInputSummary)) {
3459
+ return [];
3460
+ }
3461
+ const archived = context.archiveMemory.recallByFilePath(item.toolInputSummary);
3462
+ if (!archived) {
3463
+ return [];
3464
+ }
3465
+ return [{
3466
+ filePath: item.toolInputSummary,
3467
+ toolName: archived.toolName,
3468
+ excerpt: truncateText(archived.fullOutput, 1200)
3469
+ }];
3470
+ });
3471
+ return JSON.stringify({
3472
+ answer,
3473
+ objective: kernel?.objective ?? kernel?.currentTask ?? "",
3474
+ constraints: kernel?.constraints ?? [],
3475
+ recentCorrections: kernel?.memory.corrections.slice(-3).map((item) => item.content) ?? [],
3476
+ recentEvidence,
3477
+ archiveEvidence,
3478
+ schema: {
3479
+ verdict: "allow | warn | block",
3480
+ rationale: "short explanation",
3481
+ requirements: {
3482
+ allowsMemoryOnlyRecall: "boolean",
3483
+ needsDirectToolEvidence: "boolean",
3484
+ needsFileBackedClaims: "boolean",
3485
+ allowsInference: "boolean",
3486
+ maxUnsupportedClaims: "number"
3487
+ },
3488
+ supportedClaims: ["string"],
3489
+ unsupportedClaims: ["string"]
3490
+ }
3491
+ }, null, 2);
3492
+ }
3493
+ function normalizeClaimVerification(content, fallback, evidence) {
3494
+ const parsed = extractJsonObject(content);
3495
+ if (!parsed) {
3496
+ return fallback;
3497
+ }
3498
+ const requirements = normalizeEvidenceRequirements(parsed.requirements, fallback.requirements);
3499
+ const verdict = parsed.verdict === "allow" || parsed.verdict === "warn" || parsed.verdict === "block" ? parsed.verdict : fallback.verdict;
3500
+ const supportedClaims = normalizeStringArray(parsed.supportedClaims);
3501
+ const unsupportedClaims = normalizeStringArray(parsed.unsupportedClaims);
3502
+ const rationale = typeof parsed.rationale === "string" && parsed.rationale.trim() ? parsed.rationale.trim() : fallback.rationale;
3503
+ if (verdict === "block" && unsupportedClaims.length === 0 && evidence.length > 0) {
3504
+ return fallback;
3505
+ }
3506
+ return {
3507
+ verdict,
3508
+ rationale,
3509
+ requirements,
3510
+ supportedClaims,
3511
+ unsupportedClaims
3512
+ };
3513
+ }
3514
+ function deterministicVerification(answer, kernel, responseRequirements) {
3515
+ const evidence = kernel?.memory.evidence ?? [];
3516
+ const shellRecall = /shell command|previous shell command|used in this session|latest shell command/i.test(answer);
3517
+ const fileLikeClaims = /RuntimeEngine|projectKernelPrompt|compactKernelState|plugins\/|packages\/|\.ts\b|line \d+/i.test(answer);
3518
+ const hasFileEvidence = evidence.some((item) => item.toolName === "fs_read");
3519
+ const hasShellEvidence = evidence.some((item) => item.toolName === "shell_exec");
3520
+ const requirements = responseRequirements?.requirements;
3521
+ if (shellRecall && hasShellEvidence) {
3522
+ return {
3523
+ verdict: "allow",
3524
+ rationale: "Shell recall answer is grounded in stored shell_exec evidence.",
3525
+ requirements: {
3526
+ allowsMemoryOnlyRecall: true,
3527
+ needsDirectToolEvidence: false,
3528
+ needsFileBackedClaims: false,
3529
+ allowsInference: false,
3530
+ maxUnsupportedClaims: 0
3531
+ },
3532
+ supportedClaims: ["Session shell recall supported by shell_exec evidence."],
3533
+ unsupportedClaims: []
3534
+ };
3535
+ }
3536
+ if (fileLikeClaims && !hasFileEvidence) {
3537
+ return {
3538
+ verdict: "block",
3539
+ rationale: "Code/file claims require fs_read-backed evidence before report is allowed.",
3540
+ requirements: {
3541
+ allowsMemoryOnlyRecall: false,
3542
+ needsDirectToolEvidence: true,
3543
+ needsFileBackedClaims: true,
3544
+ allowsInference: false,
3545
+ maxUnsupportedClaims: 0
3546
+ },
3547
+ supportedClaims: [],
3548
+ unsupportedClaims: ["Answer makes file/code claims without fs_read evidence."]
3549
+ };
3550
+ }
3551
+ return {
3552
+ verdict: "allow",
3553
+ rationale: "No unsupported claims were detected by deterministic fallback.",
3554
+ requirements: requirements ?? {
3555
+ allowsMemoryOnlyRecall: true,
3556
+ needsDirectToolEvidence: false,
3557
+ needsFileBackedClaims: false,
3558
+ allowsInference: true,
3559
+ maxUnsupportedClaims: 1
3560
+ },
3561
+ supportedClaims: [],
3562
+ unsupportedClaims: []
3563
+ };
3564
+ }
3565
+ function verifyEvidenceBackedRecall(answer, kernel, responseRequirements) {
3566
+ if (!responseRequirements?.requirements.allowsMemoryOnlyRecall) {
3567
+ return null;
3568
+ }
3569
+ const evidence = kernel?.memory.evidence ?? [];
3570
+ const fileEvidence = evidence.filter((item) => item.toolName === "fs_read" && item.toolInputSummary).map((item) => item.toolInputSummary);
3571
+ const mentionedFiles = extractMentionedFilePaths(answer);
3572
+ if (mentionedFiles.length > 0 && mentionedFiles.every((filePath) => fileEvidence.includes(filePath))) {
3573
+ return {
3574
+ verdict: "allow",
3575
+ rationale: "Answer is a direct recall of file evidence already recorded in the session.",
3576
+ requirements: responseRequirements.requirements,
3577
+ supportedClaims: mentionedFiles.map((filePath) => `Read evidence exists for ${filePath}.`),
3578
+ unsupportedClaims: []
3579
+ };
3580
+ }
3581
+ return null;
3582
+ }
3583
+ function extractMentionedFilePaths(answer) {
3584
+ const matches = answer.match(/plugins\/[^\s`*]+|packages\/[^\s`*]+/g) ?? [];
3585
+ return [...new Set(matches)];
3586
+ }
3587
+ function normalizeEvidenceRequirements(value, fallback) {
3588
+ if (!value || typeof value !== "object") {
3589
+ return fallback;
3590
+ }
3591
+ const input = value;
3592
+ return {
3593
+ allowsMemoryOnlyRecall: typeof input.allowsMemoryOnlyRecall === "boolean" ? input.allowsMemoryOnlyRecall : fallback.allowsMemoryOnlyRecall,
3594
+ needsDirectToolEvidence: typeof input.needsDirectToolEvidence === "boolean" ? input.needsDirectToolEvidence : fallback.needsDirectToolEvidence,
3595
+ needsFileBackedClaims: typeof input.needsFileBackedClaims === "boolean" ? input.needsFileBackedClaims : fallback.needsFileBackedClaims,
3596
+ allowsInference: typeof input.allowsInference === "boolean" ? input.allowsInference : fallback.allowsInference,
3597
+ maxUnsupportedClaims: typeof input.maxUnsupportedClaims === "number" ? input.maxUnsupportedClaims : fallback.maxUnsupportedClaims
3598
+ };
3599
+ }
3600
+ function normalizeStringArray(value) {
3601
+ if (!Array.isArray(value)) {
3602
+ return [];
3603
+ }
3604
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
3605
+ }
3606
+ function extractJsonObject(content) {
3607
+ const start = content.indexOf("{");
3608
+ const end = content.lastIndexOf("}");
3609
+ if (start === -1 || end === -1 || end <= start) {
3610
+ return null;
3611
+ }
3612
+ try {
3613
+ return JSON.parse(content.slice(start, end + 1));
3614
+ } catch {
3615
+ return null;
3616
+ }
3617
+ }
3618
+ function truncateText(value, maxChars) {
3619
+ const normalized = value.trim();
3620
+ if (normalized.length <= maxChars) {
3621
+ return normalized;
3622
+ }
3623
+ return `${normalized.slice(0, maxChars - 1).trimEnd()}\u2026`;
3624
+ }
3625
+ function createAskParentTool(_context) {
3626
+ return {
3627
+ definition: {
3628
+ type: "function",
3629
+ function: {
3630
+ name: "ask_parent",
3631
+ description: `Ask the parent agent for help when stuck or need clarification.`,
3632
+ parameters: {
3633
+ type: "object",
3634
+ properties: {
3635
+ question: {
3636
+ type: "string",
3637
+ description: "Your question for the parent agent. Be specific about what you need help with."
3638
+ },
3639
+ reason: {
3640
+ type: "string",
3641
+ enum: ["stuck", "uncertain", "blocker", "clarification"],
3642
+ description: "Why are you asking? stuck=repeating tools, uncertain=unclear approach, blocker=cannot proceed, clarification=need more info"
3643
+ },
3644
+ context: {
3645
+ type: "object",
3646
+ description: "Relevant context: tools tried, iteration number, what you attempted"
3647
+ }
3648
+ },
3649
+ required: ["question", "reason"]
3650
+ }
3651
+ }
3652
+ },
3653
+ executor: async (input) => {
3654
+ const question = input.question;
3655
+ const reason = input.reason;
3656
+ const context = input.context;
3657
+ return {
3658
+ success: true,
3659
+ output: "",
3660
+ // Will be populated by parent agent
3661
+ metadata: {
3662
+ question,
3663
+ reason,
3664
+ context,
3665
+ needsParentHelp: true
3666
+ }
3667
+ };
3668
+ }
3669
+ };
3670
+ }
3671
+ function createReportTool(context) {
3672
+ return {
3673
+ definition: {
3674
+ type: "function",
3675
+ function: {
3676
+ name: "report",
3677
+ description: `Report your answer and exit. Include specific findings with file paths and code snippets as evidence.`,
3678
+ parameters: {
3679
+ type: "object",
3680
+ properties: {
3681
+ answer: {
3682
+ type: "string",
3683
+ description: "Synthesized answer with specific details, file references, and code snippets. Be concise but complete."
3684
+ },
3685
+ confidence: {
3686
+ type: "number",
3687
+ description: "How confident you are in this answer (0.0-1.0). Use 0.8+ if you found concrete evidence, 0.5-0.7 if partial, <0.5 if uncertain."
3688
+ }
3689
+ },
3690
+ required: ["answer", "confidence"]
3691
+ }
3692
+ }
3693
+ },
3694
+ executor: async (input) => {
3695
+ const answer = input.answer;
3696
+ const confidence = input.confidence;
3697
+ const inPlanMode = context.allowedTools?.has("plan_validate") ?? false;
3698
+ if (inPlanMode && !context.planValidationPassed) {
3699
+ const planPath = context.sessionId ? path4.join(context.workingDir, ".kb", "agents", "sessions", context.sessionId, "plan.md") : null;
3700
+ const hasPlanFile = planPath && fs2.existsSync(planPath);
3701
+ if (!hasPlanFile) {
3702
+ return {
3703
+ success: false,
3704
+ output: "BLOCKED: You must either (1) call plan_validate first, or (2) save your plan via plan_write and then call report. Call plan_write with your plan content now."
3705
+ };
3706
+ }
3707
+ }
3708
+ if (await hasPendingCorrectionCommit(context)) {
3709
+ return {
3710
+ success: false,
3711
+ output: "BLOCKED: You must call memory_correction before report because this session has an unpersisted user correction or active instruction. Persist it first, then report.",
3712
+ metadata: {
3713
+ code: "PENDING_MEMORY_COMMIT"
3714
+ }
3715
+ };
3716
+ }
3717
+ const verification = await verifyReportClaims(context, answer);
3718
+ if (verification.verdict === "block") {
3719
+ return {
3720
+ success: false,
3721
+ output: `BLOCKED: Report claims are not sufficiently supported by session evidence. ${verification.rationale}`,
3722
+ metadata: {
3723
+ code: "INSUFFICIENT_EVIDENCE",
3724
+ verification
3725
+ }
3726
+ };
3727
+ }
3728
+ return {
3729
+ success: true,
3730
+ output: "",
3731
+ // Intentionally empty - agent should exit after calling this
3732
+ metadata: {
3733
+ answer,
3734
+ confidence,
3735
+ earlyExit: true,
3736
+ verification
3737
+ }
3738
+ };
3739
+ }
3740
+ };
3741
+ }
3742
+
3743
+ // src/tools/index.ts
3744
+ function createToolRegistry(context) {
3745
+ const allowed = context.allowedTools;
3746
+ const allow = (name) => !allowed || allowed.has(name);
3747
+ const registry = new ToolRegistry(context);
3748
+ if (allow("fs_write")) {
3749
+ registry.register(createFsWriteTool(context));
3750
+ }
3751
+ if (allow("fs_read")) {
3752
+ registry.register(createFsReadTool(context));
3753
+ }
3754
+ if (allow("fs_patch")) {
3755
+ registry.register(createFsPatchTool(context));
3756
+ }
3757
+ if (allow("fs_replace")) {
3758
+ registry.register(createFsReplaceTool(context));
3759
+ }
3760
+ if (allow("fs_list")) {
3761
+ registry.register(createFsListTool(context));
3762
+ }
3763
+ if (allow("mass_replace")) {
3764
+ registry.register(createMassReplaceTool(context));
3765
+ }
3766
+ if (allow("glob_search")) {
3767
+ registry.register(createGlobSearchTool(context));
3768
+ }
3769
+ if (allow("grep_search")) {
3770
+ registry.register(createGrepSearchTool(context));
3771
+ }
3772
+ if (allow("find_definition")) {
3773
+ registry.register(createFindDefinitionTool(context));
3774
+ }
3775
+ if (allow("code_stats")) {
3776
+ registry.register(createCodeStatsTool(context));
3777
+ }
3778
+ if (allow("shell_exec")) {
3779
+ registry.register(createShellExecTool(context));
3780
+ }
3781
+ if (context.taskManager) {
3782
+ if (allow("task_submit")) {
3783
+ registry.register(createTaskSubmitTool(context));
3784
+ }
3785
+ if (allow("task_status")) {
3786
+ registry.register(createTaskStatusTool(context));
3787
+ }
3788
+ if (allow("task_collect")) {
3789
+ registry.register(createTaskCollectTool(context));
3790
+ }
3791
+ }
3792
+ if (allow("memory_get")) {
3793
+ registry.register(createMemoryGetTool(context));
3794
+ }
3795
+ if (allow("memory_preference")) {
3796
+ registry.register(createMemoryPreferenceTool(context));
3797
+ }
3798
+ if (allow("memory_constraint")) {
3799
+ registry.register(createMemoryConstraintTool(context));
3800
+ }
3801
+ if (allow("session_save")) {
3802
+ registry.register(createSessionSaveTool(context));
3803
+ }
3804
+ if (allow("memory_correction")) {
3805
+ registry.register(createMemoryCorrectionTool(context));
3806
+ }
3807
+ if (allow("memory_finding")) {
3808
+ registry.register(createMemoryFindingTool(context));
3809
+ }
3810
+ if (allow("memory_blocker")) {
3811
+ registry.register(createMemoryBlockerTool(context));
3812
+ }
3813
+ if (context.archiveMemory && allow("archive_recall")) {
3814
+ registry.register(createArchiveRecallTool(context));
3815
+ }
3816
+ if (allow("todo_create")) {
3817
+ registry.register(createTodoCreateTool(context));
3818
+ }
3819
+ if (allow("todo_update")) {
3820
+ registry.register(createTodoUpdateTool(context));
3821
+ }
3822
+ if (allow("todo_get")) {
3823
+ registry.register(createTodoGetTool(context));
3824
+ }
3825
+ if (allow("plan_validate")) {
3826
+ registry.register(createPlanValidateTool(context));
3827
+ }
3828
+ if (allow("plan_write")) {
3829
+ registry.register(createPlanWriteTool(context));
3830
+ }
3831
+ if (allow("ask_user")) {
3832
+ registry.register(createAskUserTool());
3833
+ }
3834
+ if (allow("ask_parent")) {
3835
+ registry.register(createAskParentTool());
3836
+ }
3837
+ if (allow("report")) {
3838
+ registry.register(createReportTool(context));
3839
+ }
3840
+ return registry;
3841
+ }
3842
+
3843
+ // src/gateway.ts
3844
+ var DEFAULT_TOOL_CAPABILITIES = {
3845
+ fs_write: ["filesystem"],
3846
+ fs_read: ["filesystem"],
3847
+ fs_patch: ["filesystem"],
3848
+ fs_list: ["filesystem"],
3849
+ mass_replace: ["filesystem"],
3850
+ glob_search: ["search"],
3851
+ grep_search: ["search"],
3852
+ list_files: ["search"],
3853
+ find_definition: ["code-navigation"],
3854
+ code_stats: ["search"],
3855
+ shell_exec: ["shell"],
3856
+ memory_get: ["memory"],
3857
+ memory_preference: ["memory"],
3858
+ memory_constraint: ["memory"],
3859
+ session_save: ["memory"],
3860
+ memory_correction: ["memory"],
3861
+ memory_finding: ["memory"],
3862
+ memory_blocker: ["memory"],
3863
+ archive_recall: ["memory"],
3864
+ todo_create: ["todo"],
3865
+ todo_update: ["todo"],
3866
+ todo_get: ["todo"],
3867
+ ask_user: ["interaction"],
3868
+ ask_parent: ["reporting"],
3869
+ report: ["reporting"],
3870
+ task_submit: ["delegation"],
3871
+ task_status: ["delegation"],
3872
+ task_collect: ["delegation"],
3873
+ plan_validate: ["planning"]
3874
+ };
3875
+ function toArtifact(toolName, result) {
3876
+ const summary = result.output || result.error || `${toolName} completed`;
3877
+ return {
3878
+ status: result.success ? "success" : "error",
3879
+ summary,
3880
+ artifact: result.metadata,
3881
+ evidence: summary ? [{
3882
+ id: `ev-${Date.now()}-${toolName}`,
3883
+ summary,
3884
+ source: toolName,
3885
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3886
+ artifact: result.metadata
3887
+ }] : [],
3888
+ mutations: typeof result.metadata?.filePath === "string" ? { filesRead: [result.metadata.filePath] } : void 0,
3889
+ followUpHints: typeof result.metadata?.summary === "string" ? [String(result.metadata.summary)] : void 0
3890
+ };
3891
+ }
3892
+ var ToolGateway = class {
3893
+ constructor(context, policies = []) {
3894
+ this.context = context;
3895
+ this.policies = policies;
3896
+ this.registry = createToolRegistry(context);
3897
+ }
3898
+ registry;
3899
+ getDefinitions() {
3900
+ return this.registry.getDefinitions().filter(
3901
+ (definition) => this.isAllowed(definition.function.name)
3902
+ );
3903
+ }
3904
+ getToolNames() {
3905
+ return this.getDefinitions().map((definition) => definition.function.name);
3906
+ }
3907
+ async execute(name, input) {
3908
+ const allowed = this.isAllowed(name);
3909
+ if (!allowed) {
3910
+ const artifact = {
3911
+ status: "error",
3912
+ summary: `Tool "${name}" blocked by policy`,
3913
+ evidence: []
3914
+ };
3915
+ return {
3916
+ result: { success: false, error: artifact.summary },
3917
+ artifact
3918
+ };
3919
+ }
3920
+ const result = await this.registry.execute(name, input);
3921
+ return {
3922
+ result,
3923
+ artifact: toArtifact(name, result)
3924
+ };
3925
+ }
3926
+ isAllowed(toolName) {
3927
+ const capabilities = this.getToolCapabilities(toolName);
3928
+ return this.policies.every(
3929
+ (policy) => policy.allows(toolName, this.context) && capabilities.every((capability) => policy.allowsCapability?.(capability, this.context, toolName) ?? true)
3930
+ );
3931
+ }
3932
+ getToolCapabilities(toolName) {
3933
+ const mapped = this.context.toolCapabilitiesByName?.get(toolName);
3934
+ return mapped ?? DEFAULT_TOOL_CAPABILITIES[toolName] ?? [];
3935
+ }
3936
+ };
3937
+
3938
+ // src/remote-workspace-provider.ts
3939
+ var RemoteWorkspaceProvider = class {
3940
+ constructor(opts) {
3941
+ this.opts = opts;
3942
+ }
3943
+ async readFile(path9, offset, limit) {
3944
+ const result = await this.opts.dispatch("filesystem", "readFile", [path9]);
3945
+ const content = result;
3946
+ const lines = content.split("\n");
3947
+ const start = (offset ?? 1) - 1;
3948
+ const count = limit ?? lines.length;
3949
+ const sliced = lines.slice(start, start + count);
3950
+ return {
3951
+ content: sliced.join("\n"),
3952
+ totalLines: lines.length,
3953
+ truncated: sliced.length < lines.length
3954
+ };
3955
+ }
3956
+ async writeFile(path9, content) {
3957
+ await this.opts.dispatch("filesystem", "writeFile", [path9, content]);
3958
+ }
3959
+ async listDir(path9, limit) {
3960
+ const result = await this.opts.dispatch("filesystem", "listDir", [path9]);
3961
+ const entries = result;
3962
+ return limit ? entries.slice(0, limit) : entries;
3963
+ }
3964
+ async stat(path9) {
3965
+ return await this.opts.dispatch("filesystem", "stat", [path9]);
3966
+ }
3967
+ async exists(path9) {
3968
+ return await this.opts.dispatch("filesystem", "exists", [path9]);
3969
+ }
3970
+ async grep(pattern, directory, options) {
3971
+ return await this.opts.dispatch("search", "grep", [pattern, directory, options]);
3972
+ }
3973
+ async glob(pattern, directory, options) {
3974
+ return await this.opts.dispatch("search", "glob", [pattern, directory, options]);
3975
+ }
3976
+ async shellExec(command, options) {
3977
+ return await this.opts.dispatch("shell", "exec", [command, options]);
3978
+ }
3979
+ };
3980
+
3981
+ export { ALL_SOURCE_EXTENSIONS, DELEGATION_CONFIG, FILESYSTEM_CONFIG, MASS_REPLACE_CONFIG, PLAN_READ_ONLY_TOOL_NAMES, RemoteWorkspaceProvider, SEARCH_CONFIG, SHELL_CONFIG, SOURCE_FILE_EXTENSIONS, SUB_AGENT_PRESETS, TODO_CONFIG, ToolGateway, ToolRegistry, createArchiveRecallTool, createAskParentTool, createAskUserTool, createCodeStatsTool, createFindDefinitionTool, createFsListTool, createFsPatchTool, createFsReadTool, createFsReplaceTool, createFsWriteTool, createGlobSearchTool, createGrepSearchTool, createListFilesTool, createMassReplaceTool, createMemoryBlockerTool, createMemoryConstraintTool, createMemoryCorrectionTool, createMemoryFindingTool, createMemoryGetTool, createMemoryPreferenceTool, createPlanValidateTool, createPlanWriteTool, createReportTool, createSessionSaveTool, createShellExecTool, createTaskCollectTool, createTaskStatusTool, createTaskSubmitTool, createTodoCreateTool, createTodoGetTool, createTodoUpdateTool, createToolRegistry, normalizeOffsetLimit, suggestDirectory, toFindNames, toRgIncludes, validatePath };
3982
+ //# sourceMappingURL=index.js.map
3983
+ //# sourceMappingURL=index.js.map