@townco/agent 0.1.82 → 0.1.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Supabase Storage-backed artifacts tool for agent backend
3
+ *
4
+ * Provides file storage capabilities using Supabase Storage with the following operations:
5
+ * - artifacts_cp: Copy files to/from Supabase Storage
6
+ * - artifacts_del: Delete files from Supabase Storage
7
+ * - artifacts_ls: List files in Supabase Storage
8
+ * - artifacts_url: Generate signed URLs
9
+ *
10
+ * Storage keys are scoped by: <deploying_user>/<agent_name>/<session_id>/<file_path>
11
+ */
12
+ import * as fs from "node:fs/promises";
13
+ import * as path from "node:path";
14
+ import { createSecretKeyClient } from "@townco/supabase/server";
15
+ import { tool } from "langchain";
16
+ import { z } from "zod";
17
+ // ============================================================================
18
+ // Supabase Client and Bucket Configuration
19
+ // ============================================================================
20
+ function getSupabaseClient() {
21
+ return createSecretKeyClient();
22
+ }
23
+ function getBucketName() {
24
+ return "town-builder-artifacts";
25
+ }
26
+ // ============================================================================
27
+ // Storage Key Prefix Logic
28
+ // ============================================================================
29
+ /**
30
+ * Generates the storage key prefix based on environment and configuration.
31
+ *
32
+ * Priority order:
33
+ * 1. Explicit ARTIFACTS_KEY_PREFIX env var (highest priority)
34
+ * 2. Parse username from AGENT_NAME (deployment mode: "username-agentname")
35
+ * 3. Local mode fallback: "local/agentname" or "local/unknown"
36
+ *
37
+ * @returns Prefix string without trailing slash (e.g., "alice/my-agent" or "local/my-agent")
38
+ */
39
+ function getArtifactsKeyPrefix() {
40
+ // 1. Explicit override (highest priority)
41
+ const explicitPrefix = process.env.ARTIFACTS_KEY_PREFIX;
42
+ if (explicitPrefix) {
43
+ return explicitPrefix.replace(/\/+$/, ""); // Remove trailing slashes
44
+ }
45
+ // 2. Check deployment vs local mode
46
+ const agentName = process.env.AGENT_NAME;
47
+ if (!agentName) {
48
+ return "local/unknown";
49
+ }
50
+ // 3. Parse username from AGENT_NAME (format: "username-agentname")
51
+ // In deployment, AGENT_NAME follows pattern "username-agentname"
52
+ const parts = agentName.split("-");
53
+ if (parts.length >= 2) {
54
+ const username = parts[0];
55
+ const agentNamePart = parts.slice(1).join("-");
56
+ return `${username}/${agentNamePart}`;
57
+ }
58
+ // Local mode fallback
59
+ return `local/${agentName}`;
60
+ }
61
+ /**
62
+ * Builds the full storage key for a file.
63
+ *
64
+ * @param sessionId Current session ID
65
+ * @param filePath Relative file path within the session
66
+ * @returns Full storage key (e.g., "alice/my-agent/sess-123/data/output.json")
67
+ */
68
+ function buildStorageKey(sessionId, filePath) {
69
+ // Validate session ID is provided
70
+ if (!sessionId || sessionId.trim() === "") {
71
+ throw new Error("FATAL: session_id is required but was not provided. " +
72
+ "This is a system error - session_id should be automatically injected. " +
73
+ "Please report this bug.");
74
+ }
75
+ const prefix = getArtifactsKeyPrefix();
76
+ // Normalize file path (remove leading slashes)
77
+ const normalizedPath = filePath.replace(/^\/+/, "");
78
+ return `${prefix}/${sessionId}/${normalizedPath}`;
79
+ }
80
+ // ============================================================================
81
+ // Path Validation
82
+ // ============================================================================
83
+ function validateLocalPath(path) {
84
+ if (!path.startsWith("/")) {
85
+ throw new Error(`Local paths must be absolute. Got: ${path}\n` +
86
+ "Hint: Use an absolute path starting with '/'");
87
+ }
88
+ }
89
+ function validateStoragePath(path) {
90
+ if (path.startsWith("/")) {
91
+ throw new Error(`Storage paths must be relative (no leading slash). Got: ${path}\n` +
92
+ "Hint: Use a relative path like 'data/file.json'");
93
+ }
94
+ if (path.includes("..")) {
95
+ throw new Error(`Storage paths cannot contain '..' for security reasons. Got: ${path}`);
96
+ }
97
+ // Check for empty path
98
+ if (!path || path.trim() === "") {
99
+ throw new Error("Storage path cannot be empty");
100
+ }
101
+ }
102
+ function validateExpiration(expiresIn) {
103
+ // Supabase supports up to 365 days (31536000 seconds)
104
+ if (expiresIn < 1 || expiresIn > 31536000) {
105
+ throw new Error(`URL expiration must be between 1 and 31536000 seconds (365 days). Got: ${expiresIn}`);
106
+ }
107
+ }
108
+ // ============================================================================
109
+ // Supabase Storage Operation Helpers
110
+ // ============================================================================
111
+ /**
112
+ * Upload a file to Supabase Storage
113
+ */
114
+ async function uploadToSupabase(storageKey, localPath) {
115
+ const supabase = getSupabaseClient();
116
+ const bucket = getBucketName();
117
+ // Read file content
118
+ const fileContent = await fs.readFile(localPath);
119
+ // Determine content type based on file extension
120
+ const ext = path.extname(localPath).toLowerCase();
121
+ const contentTypeMap = {
122
+ ".json": "application/json",
123
+ ".txt": "text/plain",
124
+ ".html": "text/html",
125
+ ".css": "text/css",
126
+ ".js": "application/javascript",
127
+ ".png": "image/png",
128
+ ".jpg": "image/jpeg",
129
+ ".jpeg": "image/jpeg",
130
+ ".gif": "image/gif",
131
+ ".svg": "image/svg+xml",
132
+ ".pdf": "application/pdf",
133
+ ".zip": "application/zip",
134
+ };
135
+ const contentType = contentTypeMap[ext] || "application/octet-stream";
136
+ const { error } = await supabase.storage
137
+ .from(bucket)
138
+ .upload(storageKey, fileContent, {
139
+ contentType,
140
+ upsert: true, // Allow overwriting
141
+ });
142
+ if (error) {
143
+ throw new Error(`Failed to upload to Supabase Storage: ${error.message}`);
144
+ }
145
+ }
146
+ /**
147
+ * Download a file from Supabase Storage
148
+ */
149
+ async function downloadFromSupabase(storageKey, localPath) {
150
+ const supabase = getSupabaseClient();
151
+ const bucket = getBucketName();
152
+ const { data, error } = await supabase.storage
153
+ .from(bucket)
154
+ .download(storageKey);
155
+ if (error) {
156
+ throw new Error(`Failed to download from Supabase Storage: ${error.message}`);
157
+ }
158
+ if (!data) {
159
+ throw new Error("Supabase Storage response body is empty");
160
+ }
161
+ // Ensure parent directory exists
162
+ const dir = path.dirname(localPath);
163
+ await fs.mkdir(dir, { recursive: true });
164
+ // Convert Blob to Buffer and write
165
+ const arrayBuffer = await data.arrayBuffer();
166
+ const buffer = Buffer.from(arrayBuffer);
167
+ await fs.writeFile(localPath, buffer);
168
+ }
169
+ /**
170
+ * Delete a file from Supabase Storage
171
+ */
172
+ async function deleteFromSupabase(storageKey) {
173
+ const supabase = getSupabaseClient();
174
+ const bucket = getBucketName();
175
+ const { error } = await supabase.storage.from(bucket).remove([storageKey]);
176
+ if (error) {
177
+ throw new Error(`Failed to delete from Supabase Storage: ${error.message}`);
178
+ }
179
+ }
180
+ /**
181
+ * List files in Supabase Storage with optional prefix and recursion
182
+ */
183
+ async function listFilesInSupabase(sessionId, relativePath, recursive = false) {
184
+ const supabase = getSupabaseClient();
185
+ const bucket = getBucketName();
186
+ // Build the prefix for listing
187
+ const basePath = relativePath
188
+ ? buildStorageKey(sessionId, relativePath)
189
+ : buildStorageKey(sessionId, "");
190
+ // For Supabase, we list at the base path
191
+ // If recursive, we'll need to handle it differently
192
+ const options = recursive
193
+ ? undefined
194
+ : {
195
+ limit: 100,
196
+ offset: 0,
197
+ sortBy: { column: "name", order: "asc" },
198
+ };
199
+ const { data, error } = await supabase.storage
200
+ .from(bucket)
201
+ .list(basePath, options);
202
+ if (error) {
203
+ throw new Error(`Failed to list files in Supabase Storage: ${error.message}`);
204
+ }
205
+ return data || [];
206
+ }
207
+ /**
208
+ * Generate a signed URL for a Supabase Storage object
209
+ */
210
+ async function generateSignedUrl(storageKey, expiresIn) {
211
+ const supabase = getSupabaseClient();
212
+ const bucket = getBucketName();
213
+ const { data, error } = await supabase.storage
214
+ .from(bucket)
215
+ .createSignedUrl(storageKey, expiresIn);
216
+ if (error) {
217
+ throw new Error(`Failed to generate signed URL: ${error.message}`);
218
+ }
219
+ if (!data?.signedUrl) {
220
+ throw new Error("Signed URL not returned from Supabase Storage");
221
+ }
222
+ return data.signedUrl;
223
+ }
224
+ // ============================================================================
225
+ // Error Handling Helpers
226
+ // ============================================================================
227
+ /**
228
+ * Maps Supabase Storage errors to user-friendly error messages
229
+ */
230
+ function handleStorageError(error, context) {
231
+ if (error instanceof Error) {
232
+ const message = error.message.toLowerCase();
233
+ if (message.includes("object not found") || message.includes("not found")) {
234
+ return `Error: File not found in storage. ${context}`;
235
+ }
236
+ if (message.includes("bucket not found")) {
237
+ return `Error: Artifacts bucket not configured. Please create the 'artifacts' bucket in Supabase. ${context}`;
238
+ }
239
+ if (message.includes("invalid jwt") || message.includes("jwt")) {
240
+ return `Error: Authentication error. Please check Supabase credentials. ${context}`;
241
+ }
242
+ if (message.includes("enoent")) {
243
+ return `Error: Local file not found. ${context}\n${error.message}`;
244
+ }
245
+ if (message.includes("eacces")) {
246
+ return `Error: Permission denied accessing local file. ${context}\n${error.message}`;
247
+ }
248
+ return `Error: ${error.message}. ${context}`;
249
+ }
250
+ return `Error: Unknown error occurred. ${context}`;
251
+ }
252
+ // ============================================================================
253
+ // Tool Implementations
254
+ // ============================================================================
255
+ /**
256
+ * Tool 1: artifacts_cp - Copy files to/from Supabase Storage
257
+ */
258
+ const artifactsCp = tool(async ({ session_id, source, destination, direction, }) => {
259
+ try {
260
+ if (direction === "upload") {
261
+ // Upload: local file -> Storage
262
+ validateLocalPath(source);
263
+ validateStoragePath(destination);
264
+ const storageKey = buildStorageKey(session_id, destination);
265
+ await uploadToSupabase(storageKey, source);
266
+ return `Successfully uploaded ${source} to storage at ${destination}`;
267
+ }
268
+ else {
269
+ // Download: Storage -> local file
270
+ validateStoragePath(source);
271
+ validateLocalPath(destination);
272
+ const storageKey = buildStorageKey(session_id, source);
273
+ await downloadFromSupabase(storageKey, destination);
274
+ return `Successfully downloaded ${source} from storage to ${destination}`;
275
+ }
276
+ }
277
+ catch (error) {
278
+ return handleStorageError(error, direction === "upload"
279
+ ? `Failed to upload ${source} to ${destination}`
280
+ : `Failed to download ${source} to ${destination}`);
281
+ }
282
+ }, {
283
+ name: "artifacts_cp",
284
+ description: "Copy files between local filesystem and Supabase Storage-backed artifact storage. " +
285
+ "Use direction='upload' to copy from local to storage, or direction='download' to copy from storage to local. " +
286
+ "Local paths must be absolute (e.g., '/tmp/file.txt'), storage paths must be relative (e.g., 'data/file.txt').",
287
+ schema: z.object({
288
+ session_id: z
289
+ .string()
290
+ .optional()
291
+ .describe("INTERNAL USE ONLY - Auto-injected by system"),
292
+ source: z
293
+ .string()
294
+ .describe("Source path: absolute path if local, relative path if storage"),
295
+ destination: z
296
+ .string()
297
+ .describe("Destination path: absolute path if local, relative path if storage"),
298
+ direction: z
299
+ .enum(["upload", "download"])
300
+ .describe("Direction: 'upload' (local to storage) or 'download' (storage to local)"),
301
+ }),
302
+ });
303
+ /**
304
+ * Tool 2: artifacts_del - Delete files from Supabase Storage
305
+ */
306
+ const artifactsDel = tool(async ({ session_id, path, }) => {
307
+ try {
308
+ validateStoragePath(path);
309
+ const storageKey = buildStorageKey(session_id, path);
310
+ await deleteFromSupabase(storageKey);
311
+ return `Successfully deleted ${path} from storage`;
312
+ }
313
+ catch (error) {
314
+ return handleStorageError(error, `Failed to delete ${path}`);
315
+ }
316
+ }, {
317
+ name: "artifacts_del",
318
+ description: "Delete a file from Supabase Storage-backed artifact storage. " +
319
+ "The path must be relative (e.g., 'data/file.txt').",
320
+ schema: z.object({
321
+ session_id: z
322
+ .string()
323
+ .optional()
324
+ .describe("INTERNAL USE ONLY - Auto-injected by system"),
325
+ path: z
326
+ .string()
327
+ .describe("Relative path to the file to delete in storage"),
328
+ }),
329
+ });
330
+ /**
331
+ * Tool 3: artifacts_ls - List files in Supabase Storage
332
+ */
333
+ const artifactsLs = tool(async ({ session_id, path, recursive = false, }) => {
334
+ try {
335
+ if (path) {
336
+ validateStoragePath(path);
337
+ }
338
+ const files = await listFilesInSupabase(session_id, path, recursive);
339
+ if (files.length === 0) {
340
+ return path
341
+ ? `No files found in ${path}`
342
+ : "No files found in this session";
343
+ }
344
+ // Format the file list
345
+ const fileList = files
346
+ .map((file) => {
347
+ const size = file.metadata?.size || 0;
348
+ const lastModified = file.updated_at || file.created_at || "unknown";
349
+ // Format size in human-readable format
350
+ let sizeStr;
351
+ if (size < 1024) {
352
+ sizeStr = `${size} B`;
353
+ }
354
+ else if (size < 1024 * 1024) {
355
+ sizeStr = `${(size / 1024).toFixed(2)} KB`;
356
+ }
357
+ else if (size < 1024 * 1024 * 1024) {
358
+ sizeStr = `${(size / (1024 * 1024)).toFixed(2)} MB`;
359
+ }
360
+ else {
361
+ sizeStr = `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
362
+ }
363
+ return `${file.name}\t${sizeStr}\t${lastModified}`;
364
+ })
365
+ .join("\n");
366
+ const header = path
367
+ ? `Files in ${path} (${files.length} total):\n`
368
+ : `Files in session (${files.length} total):\n`;
369
+ return `${header}Name\tSize\tLast Modified\n${fileList}`;
370
+ }
371
+ catch (error) {
372
+ return handleStorageError(error, `Failed to list files${path ? ` in ${path}` : ""}`);
373
+ }
374
+ }, {
375
+ name: "artifacts_ls",
376
+ description: "List files in Supabase Storage-backed artifact storage for the current session. " +
377
+ "Optionally specify a relative directory path to list. " +
378
+ "Use recursive=true to list files recursively in subdirectories.",
379
+ schema: z.object({
380
+ session_id: z
381
+ .string()
382
+ .optional()
383
+ .describe("INTERNAL USE ONLY - Auto-injected by system"),
384
+ path: z
385
+ .string()
386
+ .optional()
387
+ .describe("Optional relative directory path to list (e.g., 'data/'). If omitted, lists session root."),
388
+ recursive: z
389
+ .boolean()
390
+ .optional()
391
+ .describe("Whether to list files recursively in subdirectories (default: false)"),
392
+ }),
393
+ });
394
+ /**
395
+ * Tool 4: artifacts_url - Generate signed URL
396
+ */
397
+ const artifactsUrl = tool(async ({ session_id, path, expires_in = 3600, }) => {
398
+ try {
399
+ validateStoragePath(path);
400
+ validateExpiration(expires_in);
401
+ const storageKey = buildStorageKey(session_id, path);
402
+ const url = await generateSignedUrl(storageKey, expires_in);
403
+ const expiryHours = expires_in / 3600;
404
+ const expiryStr = expiryHours >= 1
405
+ ? `${expiryHours.toFixed(1)} hour(s)`
406
+ : `${expires_in} second(s)`;
407
+ return `Signed URL for ${path} (expires in ${expiryStr}):\n${url}`;
408
+ }
409
+ catch (error) {
410
+ return handleStorageError(error, `Failed to generate URL for ${path}`);
411
+ }
412
+ }, {
413
+ name: "artifacts_url",
414
+ description: "Generate a signed URL for accessing an artifact in Supabase Storage. " +
415
+ "The URL will be valid for the specified duration (default: 1 hour, max: 365 days). " +
416
+ "Anyone with this URL can access the file until it expires.",
417
+ schema: z.object({
418
+ session_id: z
419
+ .string()
420
+ .optional()
421
+ .describe("INTERNAL USE ONLY - Auto-injected by system"),
422
+ path: z.string().describe("Relative path to the artifact in storage"),
423
+ expires_in: z
424
+ .number()
425
+ .optional()
426
+ .describe("Expiration time in seconds (1-31536000). Default: 3600 (1 hour)"),
427
+ }),
428
+ });
429
+ // ============================================================================
430
+ // Tool Metadata
431
+ // ============================================================================
432
+ // Add metadata for UI display
433
+ artifactsCp.prettyName = "Copy Artifact";
434
+ artifactsCp.icon = "Upload";
435
+ artifactsCp.verbiage = {
436
+ active: "Copying artifact to {destination}",
437
+ past: "Copied artifact to {destination}",
438
+ paramKey: "destination",
439
+ };
440
+ artifactsDel.prettyName = "Delete Artifact";
441
+ artifactsDel.icon = "Trash";
442
+ artifactsDel.verbiage = {
443
+ active: "Deleting artifact {path}",
444
+ past: "Deleted artifact {path}",
445
+ paramKey: "path",
446
+ };
447
+ artifactsLs.prettyName = "List Artifacts";
448
+ artifactsLs.icon = "List";
449
+ artifactsLs.verbiage = {
450
+ active: "Listing artifacts",
451
+ past: "Listed artifacts",
452
+ };
453
+ artifactsUrl.prettyName = "Generate Artifact URL";
454
+ artifactsUrl.icon = "Link";
455
+ artifactsUrl.verbiage = {
456
+ active: "Generating URL for {path}",
457
+ past: "Generated URL for {path}",
458
+ paramKey: "path",
459
+ };
460
+ // ============================================================================
461
+ // Factory Function
462
+ // ============================================================================
463
+ /**
464
+ * Factory function to create the artifacts tools
465
+ * Returns an array of all four artifact management tools
466
+ */
467
+ export function makeArtifactsTools() {
468
+ return [artifactsCp, artifactsDel, artifactsLs, artifactsUrl];
469
+ }
@@ -59,9 +59,9 @@ export function makeBrowserTools() {
59
59
  try {
60
60
  const { liveViewUrl } = await ensureBrowserSession();
61
61
  const code = `
62
- await page.goto('${url.replace(/'/g, "\\'")}', {
62
+ await page.goto('${url.replace(/'/g, "\\'")}', {
63
63
  waitUntil: '${waitUntil}',
64
- timeout: 30000
64
+ timeout: 30000
65
65
  });
66
66
  return { url: page.url(), title: await page.title() };
67
67
  `;
@@ -103,7 +103,9 @@ export function makeBrowserTools() {
103
103
  .describe("When to consider navigation complete"),
104
104
  }),
105
105
  });
106
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
106
107
  browserNavigate.prettyName = "Browser Navigate";
108
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
107
109
  browserNavigate.icon = "Globe";
108
110
  // Browser Screenshot tool
109
111
  const browserScreenshot = tool(async ({ fullPage = false, selector, }) => {
@@ -180,7 +182,9 @@ export function makeBrowserTools() {
180
182
  .describe("CSS selector of element to screenshot (captures whole page if not provided)"),
181
183
  }),
182
184
  });
185
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
183
186
  browserScreenshot.prettyName = "Browser Screenshot";
187
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
184
188
  browserScreenshot.icon = "Camera";
185
189
  // Browser Extract Content tool
186
190
  const browserExtract = tool(async ({ selector, extractType = "text", }) => {
@@ -270,14 +274,16 @@ export function makeBrowserTools() {
270
274
  .describe("Type of content to extract"),
271
275
  }),
272
276
  });
277
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
273
278
  browserExtract.prettyName = "Browser Extract";
279
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
274
280
  browserExtract.icon = "FileText";
275
281
  // Browser Click tool
276
282
  const browserClick = tool(async ({ selector, button = "left" }) => {
277
283
  try {
278
284
  const { liveViewUrl } = await ensureBrowserSession();
279
285
  const code = `
280
- await page.click('${selector.replace(/'/g, "\\'")}', {
286
+ await page.click('${selector.replace(/'/g, "\\'")}', {
281
287
  button: '${button}',
282
288
  timeout: 10000,
283
289
  });
@@ -318,7 +324,9 @@ export function makeBrowserTools() {
318
324
  .describe("Which mouse button to use"),
319
325
  }),
320
326
  });
327
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
321
328
  browserClick.prettyName = "Browser Click";
329
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
322
330
  browserClick.icon = "MousePointer";
323
331
  // Browser Type tool
324
332
  const browserType = tool(async ({ selector, text, pressEnter = false, }) => {
@@ -364,7 +372,9 @@ export function makeBrowserTools() {
364
372
  .describe("Whether to press Enter after typing"),
365
373
  }),
366
374
  });
375
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
367
376
  browserType.prettyName = "Browser Type";
377
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
368
378
  browserType.icon = "Type";
369
379
  // Browser Close tool
370
380
  const browserClose = tool(async () => {
@@ -399,7 +409,9 @@ export function makeBrowserTools() {
399
409
  " - Calling this when no browser is active has no effect\n",
400
410
  schema: z.object({}),
401
411
  });
412
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
402
413
  browserClose.prettyName = "Browser Close";
414
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
403
415
  browserClose.icon = "X";
404
416
  return [
405
417
  browserNavigate,
@@ -1,12 +1,16 @@
1
1
  import { z } from "zod";
2
- export declare function makeFilesystemTools(workingDirectory: string): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
2
+ /**
3
+ * Create filesystem tools that are scoped to the current session.
4
+ * Tools use session context (via AsyncLocalStorage) to determine the session directory.
5
+ */
6
+ export declare function makeFilesystemTools(): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
3
7
  pattern: z.ZodString;
4
8
  path: z.ZodOptional<z.ZodString>;
5
9
  glob: z.ZodOptional<z.ZodString>;
6
10
  output_mode: z.ZodOptional<z.ZodEnum<{
7
11
  content: "content";
8
- count: "count";
9
12
  files_with_matches: "files_with_matches";
13
+ count: "count";
10
14
  }>>;
11
15
  "-B": z.ZodOptional<z.ZodNumber>;
12
16
  "-A": z.ZodOptional<z.ZodNumber>;
@@ -20,7 +24,7 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
20
24
  pattern: string;
21
25
  path?: string | undefined;
22
26
  glob?: string | undefined;
23
- output_mode?: "content" | "count" | "files_with_matches" | undefined;
27
+ output_mode?: "content" | "files_with_matches" | "count" | undefined;
24
28
  "-B"?: number | undefined;
25
29
  "-A"?: number | undefined;
26
30
  "-C"?: number | undefined;
@@ -33,7 +37,7 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
33
37
  pattern: string;
34
38
  path?: string | undefined;
35
39
  glob?: string | undefined;
36
- output_mode?: "content" | "count" | "files_with_matches" | undefined;
40
+ output_mode?: "content" | "files_with_matches" | "count" | undefined;
37
41
  "-B"?: number | undefined;
38
42
  "-A"?: number | undefined;
39
43
  "-C"?: number | undefined;