(args: z.infer<typeof FindCallersArgs>, ctx: ToolContext)
| 180 | argsSchema = FindCallersArgs; |
| 181 | |
| 182 | async execute(args: z.infer<typeof FindCallersArgs>, ctx: ToolContext): Promise<ToolResult> { |
| 183 | const db = requireDB(); |
| 184 | if ('error' in db) return { content: db.error, isError: true }; |
| 185 | |
| 186 | // Build a set of definition locations to filter out from ripgrep results |
| 187 | const defs = db.findSymbolsByName(args.name); |
| 188 | const defKeys = new Set(defs.map(d => `${d.file_path}:${d.start_line}`)); |
| 189 | |
| 190 | const escaped = args.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 191 | const pattern = `\\b${escaped}\\s*\\(`; |
| 192 | const limit = args.limit ?? 100; |
| 193 | |
| 194 | const rgArgs = ['-n', '--no-heading', '--max-count', String(limit + defs.length), '-e', pattern]; |
| 195 | if (args.language) rgArgs.push('--type', args.language); |
| 196 | rgArgs.push(ctx.cwd); |
| 197 | |
| 198 | const search = await runLineSearch(ctx.cwd, { |
| 199 | rgArgs, |
| 200 | regex: new RegExp(pattern), |
| 201 | language: args.language, |
| 202 | maxCount: limit + defs.length, |
| 203 | signal: ctx.signal, |
| 204 | }); |
| 205 | if ('error' in search) return { content: search.error, isError: true }; |
| 206 | |
| 207 | const lines = search.stdout.split('\n').filter(Boolean); |
| 208 | // Format: file:line:content |
| 209 | const callers: Array<{ file: string; line: number; preview: string }> = []; |
| 210 | for (const raw of lines) { |
| 211 | const firstColon = raw.indexOf(':'); |
| 212 | const secondColon = raw.indexOf(':', firstColon + 1); |
| 213 | if (firstColon === -1 || secondColon === -1) continue; |
| 214 | const file = raw.slice(0, firstColon); |
| 215 | const lineNum = parseInt(raw.slice(firstColon + 1, secondColon), 10); |
| 216 | if (!Number.isFinite(lineNum)) continue; |
| 217 | const preview = raw.slice(secondColon + 1).trim(); |
| 218 | // Filter out definition lines |
| 219 | if (defKeys.has(`${file}:${lineNum}`)) continue; |
| 220 | callers.push({ file, line: lineNum, preview: preview.slice(0, 200) }); |
| 221 | if (callers.length >= limit) break; |
| 222 | } |
| 223 | if (callers.length === 0) { |
| 224 | return { content: `[NO_CALLERS] No call sites found for "${args.name}". Either it's only defined (never called from the indexed paths), or the project hasn't been indexed yet (try /index).` }; |
| 225 | } |
| 226 | // Group by file |
| 227 | const byFile = new Map<string, typeof callers>(); |
| 228 | for (const c of callers) { |
| 229 | const rel = path.relative(ctx.cwd, c.file); |
| 230 | if (!byFile.has(rel)) byFile.set(rel, []); |
| 231 | byFile.get(rel)!.push(c); |
| 232 | } |
| 233 | const out: string[] = [`Found ${callers.length} call site${callers.length > 1 ? 's' : ''} for "${args.name}":`]; |
| 234 | for (const [rel, items] of byFile) { |
| 235 | out.push(`\n ${rel}`); |
| 236 | for (const it of items) { |
| 237 | out.push(` :${it.line} ${it.preview}`); |
| 238 | } |
| 239 | } |
nothing calls this directly
no test coverage detected