( params: Record<string, unknown>, context: ExecutionContext )
| 67 | } |
| 68 | |
| 69 | export async function executeVfsGrep( |
| 70 | params: Record<string, unknown>, |
| 71 | context: ExecutionContext |
| 72 | ): Promise<ToolCallResult> { |
| 73 | const pattern = params.pattern as string | undefined |
| 74 | if (!pattern) { |
| 75 | return { success: false, error: "Missing required parameter 'pattern'" } |
| 76 | } |
| 77 | const outputMode = (params.output_mode as string) ?? 'content' |
| 78 | |
| 79 | const workspaceId = context.workspaceId |
| 80 | if (!workspaceId) { |
| 81 | return { success: false, error: 'No workspace context available' } |
| 82 | } |
| 83 | |
| 84 | const rawPath = typeof params.path === 'string' ? params.path : undefined |
| 85 | |
| 86 | try { |
| 87 | const grepOptions = { |
| 88 | maxResults: (params.maxResults as number) ?? 50, |
| 89 | outputMode: outputMode as 'content' | 'files_with_matches' | 'count', |
| 90 | ignoreCase: (params.ignoreCase as boolean) ?? false, |
| 91 | lineNumbers: (params.lineNumbers as boolean) ?? true, |
| 92 | context: (params.context as number) ?? 0, |
| 93 | } |
| 94 | |
| 95 | // Routing mirrors read/glob: |
| 96 | // - uploads/<file> -> grep one chat upload's content (chat-scoped) |
| 97 | // - files/<file> -> grep one workspace file's content (one file only) |
| 98 | // - everything else -> grep the in-memory VFS map (workflow JSON, metadata) |
| 99 | // Chat uploads are opt-in like recently-deleted/: they are never in the VFS |
| 100 | // map, so an unscoped grep can't touch them — only an explicit uploads/<file> |
| 101 | // path does, and only one upload at a time. |
| 102 | let result: GrepMatch[] | string[] | GrepCountEntry[] |
| 103 | if (isChatUploadGrepPath(rawPath)) { |
| 104 | if (!context.chatId) { |
| 105 | return { success: false, error: 'No chat context available for uploads/' } |
| 106 | } |
| 107 | // The upload is the first segment after uploads/; any trailing segment |
| 108 | // (e.g. a /content suffix) is ignored, mirroring the uploads read path. |
| 109 | const filename = rawPath |
| 110 | .replace(/^\/+/, '') |
| 111 | .replace(/^uploads\/?/, '') |
| 112 | .split('/')[0] |
| 113 | if (!filename) { |
| 114 | return { |
| 115 | success: false, |
| 116 | error: |
| 117 | 'Grep over chat uploads must target a single upload (e.g. path: "uploads/report.json"). Use glob("uploads/*") to list uploads.', |
| 118 | } |
| 119 | } |
| 120 | result = await grepChatUpload(filename, context.chatId, pattern, grepOptions) |
| 121 | } else { |
| 122 | const vfs = await getOrMaterializeVFS(workspaceId, context.userId) |
| 123 | result = isWorkspaceFileGrepPath(rawPath) |
| 124 | ? await vfs.grepFile(rawPath, pattern, grepOptions) |
| 125 | : await vfs.grep(pattern, rawPath, grepOptions) |
| 126 | } |
no test coverage detected