bobs-workshop 0.1.4

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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/bin/bobs-mcp.js +130 -0
  4. package/dist/api/taskLogger.js +106 -0
  5. package/dist/api/taskLogger.js.map +1 -0
  6. package/dist/cli/checker.js +401 -0
  7. package/dist/cli/checker.js.map +1 -0
  8. package/dist/cli/cleanup.js +131 -0
  9. package/dist/cli/cleanup.js.map +1 -0
  10. package/dist/cli/debug.js +157 -0
  11. package/dist/cli/debug.js.map +1 -0
  12. package/dist/cli/health.js +97 -0
  13. package/dist/cli/health.js.map +1 -0
  14. package/dist/cli/setup.js +81 -0
  15. package/dist/cli/setup.js.map +1 -0
  16. package/dist/cli/workshop.js +42 -0
  17. package/dist/cli/workshop.js.map +1 -0
  18. package/dist/dashboard/server.js +1206 -0
  19. package/dist/dashboard/server.js.map +1 -0
  20. package/dist/index.js +757 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/prompts/architect.js +157 -0
  23. package/dist/prompts/architect.js.map +1 -0
  24. package/dist/prompts/debugger.js +201 -0
  25. package/dist/prompts/debugger.js.map +1 -0
  26. package/dist/prompts/engineer.js +171 -0
  27. package/dist/prompts/engineer.js.map +1 -0
  28. package/dist/prompts/orchestrator.js +225 -0
  29. package/dist/prompts/orchestrator.js.map +1 -0
  30. package/dist/prompts/reviewer.js +199 -0
  31. package/dist/prompts/reviewer.js.map +1 -0
  32. package/dist/services/activitySummarizer.js +353 -0
  33. package/dist/services/activitySummarizer.js.map +1 -0
  34. package/dist/services/changeValidator.js +396 -0
  35. package/dist/services/changeValidator.js.map +1 -0
  36. package/dist/services/claudeOrchestrator.js +343 -0
  37. package/dist/services/claudeOrchestrator.js.map +1 -0
  38. package/dist/services/fileMonitor.js +250 -0
  39. package/dist/services/fileMonitor.js.map +1 -0
  40. package/dist/services/implementationSummarizer.js +306 -0
  41. package/dist/services/implementationSummarizer.js.map +1 -0
  42. package/dist/services/liveMonitor.js +315 -0
  43. package/dist/services/liveMonitor.js.map +1 -0
  44. package/dist/services/mcpAuditLogger.js +104 -0
  45. package/dist/services/mcpAuditLogger.js.map +1 -0
  46. package/dist/services/mcpLogger.js +223 -0
  47. package/dist/services/mcpLogger.js.map +1 -0
  48. package/dist/services/tmuxManager.js +541 -0
  49. package/dist/services/tmuxManager.js.map +1 -0
  50. package/dist/tools/approvalTools.js +244 -0
  51. package/dist/tools/approvalTools.js.map +1 -0
  52. package/dist/tools/autoDebugger.js +147 -0
  53. package/dist/tools/autoDebugger.js.map +1 -0
  54. package/dist/tools/cleanupService.js +221 -0
  55. package/dist/tools/cleanupService.js.map +1 -0
  56. package/dist/tools/dashboardTools.js +359 -0
  57. package/dist/tools/dashboardTools.js.map +1 -0
  58. package/dist/tools/developmentNudges.js +336 -0
  59. package/dist/tools/developmentNudges.js.map +1 -0
  60. package/dist/tools/gitTools.js +741 -0
  61. package/dist/tools/gitTools.js.map +1 -0
  62. package/dist/tools/orchestratorTools.js +765 -0
  63. package/dist/tools/orchestratorTools.js.map +1 -0
  64. package/dist/tools/searchTools.js +788 -0
  65. package/dist/tools/searchTools.js.map +1 -0
  66. package/dist/tools/specTools.js +350 -0
  67. package/dist/tools/specTools.js.map +1 -0
  68. package/dist/tools/tmuxTools.js +100 -0
  69. package/dist/tools/tmuxTools.js.map +1 -0
  70. package/dist/tools/workRecorder.js +215 -0
  71. package/dist/tools/workRecorder.js.map +1 -0
  72. package/dist/tools/worktreeTools.js +705 -0
  73. package/dist/tools/worktreeTools.js.map +1 -0
  74. package/dist/utils/__tests__/integration.test.js +57 -0
  75. package/dist/utils/__tests__/integration.test.js.map +1 -0
  76. package/dist/utils/__tests__/serverDetection.test.js +151 -0
  77. package/dist/utils/__tests__/serverDetection.test.js.map +1 -0
  78. package/dist/utils/errorHandling.js +336 -0
  79. package/dist/utils/errorHandling.js.map +1 -0
  80. package/dist/utils/processManager.js +172 -0
  81. package/dist/utils/processManager.js.map +1 -0
  82. package/dist/utils/reliability.js +263 -0
  83. package/dist/utils/reliability.js.map +1 -0
  84. package/dist/utils/responseFormatter.js +250 -0
  85. package/dist/utils/responseFormatter.js.map +1 -0
  86. package/dist/utils/serverDetection.js +133 -0
  87. package/dist/utils/serverDetection.js.map +1 -0
  88. package/dist/utils/specMigration.js +105 -0
  89. package/dist/utils/specMigration.js.map +1 -0
  90. package/dist/validation/schemas.js +299 -0
  91. package/dist/validation/schemas.js.map +1 -0
  92. package/package.json +79 -0
  93. package/scripts/init-workspace.js +63 -0
  94. package/scripts/install-search-tools.js +116 -0
@@ -0,0 +1,788 @@
1
+ // src/tools/searchTools.ts
2
+ import { exec, spawn } from "child_process";
3
+ import { promisify } from "util";
4
+ import { z } from "zod";
5
+ import fs from "fs-extra";
6
+ const execAsync = promisify(exec);
7
+ // Production-ready spawn execution with timeout handling
8
+ function spawnAsync(command, args, options = {}) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(command, args, {
11
+ ...options,
12
+ stdio: ['ignore', 'pipe', 'pipe'] // Explicitly set stdio
13
+ });
14
+ let stdout = '';
15
+ let stderr = '';
16
+ let isResolved = false;
17
+ // Production timeout handling (default 60 seconds - MCP standard)
18
+ const timeout = options.timeout || 60000;
19
+ const timeoutId = setTimeout(() => {
20
+ if (!isResolved) {
21
+ isResolved = true;
22
+ child.kill('SIGTERM'); // Graceful termination
23
+ setTimeout(() => {
24
+ if (!child.killed) {
25
+ child.kill('SIGKILL'); // Force kill if still running
26
+ }
27
+ }, 5000);
28
+ reject(new Error(`Command timed out after ${timeout}ms`));
29
+ }
30
+ }, timeout);
31
+ child.stdout?.on('data', (data) => {
32
+ stdout += data.toString();
33
+ });
34
+ child.stderr?.on('data', (data) => {
35
+ stderr += data.toString();
36
+ });
37
+ child.on('close', (code) => {
38
+ if (!isResolved) {
39
+ isResolved = true;
40
+ clearTimeout(timeoutId);
41
+ if (code === 0) {
42
+ resolve({ stdout, stderr });
43
+ }
44
+ else {
45
+ const error = new Error(`Command failed with code ${code}`);
46
+ error.code = code;
47
+ error.stdout = stdout;
48
+ error.stderr = stderr;
49
+ reject(error);
50
+ }
51
+ }
52
+ });
53
+ child.on('error', (err) => {
54
+ if (!isResolved) {
55
+ isResolved = true;
56
+ clearTimeout(timeoutId);
57
+ reject(err);
58
+ }
59
+ });
60
+ });
61
+ }
62
+ // Simplified tool path finder based on successful MCP implementations
63
+ async function findToolPath(toolName) {
64
+ // Research shows successful MCP servers use simple PATH-based discovery
65
+ // Try system PATH first, then fallback to known locations
66
+ try {
67
+ // Test if tool is in PATH (like successful MCP implementations do)
68
+ await execAsync(`${toolName} --version`, { timeout: 3000 });
69
+ return toolName; // Tool found in PATH
70
+ }
71
+ catch (error) {
72
+ // Fallback to specific locations only if PATH fails
73
+ if (toolName === 'rg') {
74
+ // Claude Code provides rg, try that location
75
+ const claudeRg = `/Users/${process.env.USER}/.claude/local/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/arm64-darwin/rg`;
76
+ try {
77
+ await execAsync(`${claudeRg} --version`, { timeout: 3000 });
78
+ return claudeRg;
79
+ }
80
+ catch (e) {
81
+ // Final fallback
82
+ return 'rg';
83
+ }
84
+ }
85
+ // For semgrep and others, just return the tool name (PATH should work after installation)
86
+ return toolName;
87
+ }
88
+ }
89
+ export const FileSearchInput = z.object({
90
+ query: z.string(),
91
+ max_results: z.number().optional().default(50),
92
+ path_filters: z.array(z.string()).optional()
93
+ });
94
+ export const FileSearchOutput = z.object({
95
+ results: z.array(z.object({
96
+ file: z.string(),
97
+ line: z.number(),
98
+ snippet: z.string(),
99
+ score: z.number()
100
+ }))
101
+ });
102
+ export const SymbolSearchInput = z.object({
103
+ symbol: z.string()
104
+ });
105
+ export const SymbolSearchOutput = z.object({
106
+ definitions: z.array(z.object({
107
+ file: z.string(),
108
+ line: z.number()
109
+ }))
110
+ });
111
+ export const ReferencesSearchInput = z.object({
112
+ symbol: z.string()
113
+ });
114
+ export const ReferencesSearchOutput = z.object({
115
+ references: z.array(z.object({
116
+ file: z.string(),
117
+ line: z.number()
118
+ }))
119
+ });
120
+ export const SummarizeInput = z.object({
121
+ file: z.string()
122
+ });
123
+ export const SummarizeOutput = z.object({
124
+ summary: z.string()
125
+ });
126
+ export const AnalyzeInput = z.object({
127
+ file: z.string()
128
+ });
129
+ export const AnalyzeOutput = z.object({
130
+ imports: z.array(z.string()),
131
+ classes: z.array(z.string()),
132
+ functions: z.array(z.string())
133
+ });
134
+ // New semgrep-based search input/output schemas
135
+ export const SemgrepSearchInput = z.object({
136
+ pattern: z.string().describe("Semgrep pattern to search for (e.g., 'function verifyToken($TOKEN)' for structured AST search)"),
137
+ language: z.enum(["typescript", "javascript", "python", "go", "java", "auto"]).optional().default("auto").describe("Language for pattern matching"),
138
+ path_filters: z.array(z.string()).optional().describe("File path filters/globs to limit search scope"),
139
+ max_results: z.number().optional().default(50).describe("Maximum number of results to return")
140
+ });
141
+ export const SemgrepSearchOutput = z.object({
142
+ results: z.array(z.object({
143
+ file: z.string(),
144
+ line: z.number(),
145
+ snippet: z.string(),
146
+ rule_id: z.string().optional(),
147
+ message: z.string().optional()
148
+ }))
149
+ });
150
+ // Enhanced file search with more options
151
+ export const EnhancedFileSearchInput = z.object({
152
+ query: z.string().describe("Search query (regex pattern supported)"),
153
+ search_type: z.enum(["text", "regex", "exact"]).optional().default("text").describe("Type of search to perform"),
154
+ file_extensions: z.array(z.string()).optional().describe("File extensions to include (e.g., ['.ts', '.js'])"),
155
+ exclude_patterns: z.array(z.string()).optional().describe("Patterns to exclude from search"),
156
+ max_results: z.number().optional().default(50),
157
+ include_context: z.boolean().optional().default(false).describe("Include surrounding lines for context")
158
+ });
159
+ // Hybrid Search schemas - unified entry point for lexical + semantic search
160
+ export const HybridSearchInput = z.object({
161
+ query: z.string().describe("Search query to find in codebase"),
162
+ mode: z.enum(["lexical", "semantic", "auto"]).optional().default("auto").describe("Search mode: lexical (ripgrep), semantic (semgrep), or auto (both)"),
163
+ phase: z.enum(["architect", "engineer", "debugger", "reviewer"]).optional().describe("Caller phase for phase-aware Semgrep rule selection"),
164
+ path: z.string().optional().describe("Root path or subdirectory to search (optional)"),
165
+ includeHidden: z.boolean().optional().default(false).describe("Include hidden/ignored files (equivalent to rg -uuu)"),
166
+ followGitIgnore: z.boolean().optional().default(true).describe("Respect .gitignore rules"),
167
+ fileTypes: z.array(z.string()).optional().describe("File types to include (e.g., ['ts', 'tsx', 'py'])"),
168
+ maxHits: z.number().optional().default(200).describe("Maximum total results across lexical and semantic"),
169
+ contextLines: z.number().optional().default(2).describe("Context lines before/after matches"),
170
+ timeoutMs: z.number().optional().default(4000).describe("Timeout in milliseconds")
171
+ });
172
+ export const HybridSearchOutput = z.object({
173
+ lexicalHits: z.array(z.object({
174
+ file: z.string(),
175
+ line: z.number(),
176
+ text: z.string(),
177
+ before: z.array(z.string()).optional(),
178
+ after: z.array(z.string()).optional()
179
+ })).describe("Ripgrep lexical search results"),
180
+ semanticHits: z.array(z.object({
181
+ file: z.string(),
182
+ line: z.number(),
183
+ ruleId: z.string(),
184
+ message: z.string(),
185
+ severity: z.string().optional(),
186
+ confidence: z.number().optional()
187
+ })).describe("Semgrep semantic search results"),
188
+ truncated: z.boolean().describe("True if results were capped at maxHits"),
189
+ stats: z.object({
190
+ elapsedMs: z.number(),
191
+ rgHits: z.number(),
192
+ semgrepHits: z.number()
193
+ }).describe("Search execution statistics"),
194
+ errors: z.array(z.string()).optional().describe("Non-fatal errors during search")
195
+ });
196
+ // Helper function to parse ripgrep JSON output into normalized format
197
+ async function parseRipgrepJSON(stdout, contextLines = 2) {
198
+ const results = [];
199
+ const lines = stdout.split('\n').filter(Boolean);
200
+ for (const line of lines) {
201
+ try {
202
+ const parsed = JSON.parse(line);
203
+ if (parsed.type === 'match') {
204
+ const result = {
205
+ file: parsed.data.path.text,
206
+ line: parsed.data.line_number,
207
+ text: parsed.data.lines.text.trim()
208
+ };
209
+ // Add context lines if available
210
+ if (contextLines > 0 && parsed.data.submatches) {
211
+ const before = [];
212
+ const after = [];
213
+ // ripgrep context would be in separate events, for now just add the match
214
+ result.before = before;
215
+ result.after = after;
216
+ }
217
+ results.push(result);
218
+ }
219
+ }
220
+ catch (e) {
221
+ // Skip non-JSON lines or malformed JSON
222
+ continue;
223
+ }
224
+ }
225
+ return results;
226
+ }
227
+ // Helper function to normalize Semgrep results into our format
228
+ function normalizeSemgrepResults(semgrepOutput) {
229
+ if (!semgrepOutput.results)
230
+ return [];
231
+ return semgrepOutput.results.map((result) => ({
232
+ file: result.path,
233
+ line: result.start?.line || 0,
234
+ ruleId: result.check_id || 'semgrep-rule',
235
+ message: result.extra?.message || result.message || 'Semgrep match',
236
+ severity: result.extra?.severity || 'info',
237
+ confidence: result.extra?.metadata?.confidence || 0.8
238
+ }));
239
+ }
240
+ export async function fileSearchHandler(input) {
241
+ const validated = FileSearchInput.parse(input);
242
+ try {
243
+ const rgPath = await findToolPath('rg');
244
+ let rgCmd = `${rgPath} --line-number --max-count ${validated.max_results} '${validated.query}'`;
245
+ if (validated.path_filters && validated.path_filters.length > 0) {
246
+ const globs = validated.path_filters.map(filter => `--glob '${filter}'`).join(' ');
247
+ rgCmd += ` ${globs}`;
248
+ }
249
+ const { stdout } = await execAsync(rgCmd);
250
+ const results = stdout.split("\n").filter(Boolean).map(line => {
251
+ const parts = line.split(":", 3);
252
+ if (parts.length >= 3) {
253
+ const [file, lineNo, snippet] = parts;
254
+ return { file, line: parseInt(lineNo, 10) || 0, snippet: snippet.trim(), score: 1.0 };
255
+ }
256
+ return null;
257
+ }).filter(Boolean);
258
+ return { results };
259
+ }
260
+ catch (error) {
261
+ // If ripgrep fails (command not found or no matches), return empty results
262
+ return { results: [] };
263
+ }
264
+ }
265
+ export async function symbolSearchHandler(input) {
266
+ const validated = SymbolSearchInput.parse(input);
267
+ try {
268
+ // Use ripgrep to find symbol definitions (functions, classes, etc.)
269
+ const patterns = [
270
+ `function\\s+${validated.symbol}`,
271
+ `class\\s+${validated.symbol}`,
272
+ `const\\s+${validated.symbol}`,
273
+ `let\\s+${validated.symbol}`,
274
+ `var\\s+${validated.symbol}`,
275
+ `export.*${validated.symbol}`,
276
+ `interface\\s+${validated.symbol}`,
277
+ `type\\s+${validated.symbol}`
278
+ ];
279
+ const definitions = [];
280
+ for (const pattern of patterns) {
281
+ try {
282
+ const rgPath = await findToolPath('rg');
283
+ const { stdout } = await execAsync(`${rgPath} --line-number '${pattern}'`);
284
+ const matches = stdout.split("\n").filter(Boolean).map(line => {
285
+ const parts = line.split(":", 2);
286
+ if (parts.length >= 2) {
287
+ return { file: parts[0], line: parseInt(parts[1], 10) || 0 };
288
+ }
289
+ return null;
290
+ }).filter(Boolean);
291
+ definitions.push(...matches);
292
+ }
293
+ catch (error) {
294
+ // Continue with next pattern if this one fails
295
+ }
296
+ }
297
+ return { definitions };
298
+ }
299
+ catch (error) {
300
+ return { definitions: [] };
301
+ }
302
+ }
303
+ export async function referencesSearchHandler(input) {
304
+ const validated = ReferencesSearchInput.parse(input);
305
+ try {
306
+ const rgPath = await findToolPath('rg');
307
+ const { stdout } = await execAsync(`${rgPath} --line-number '\\b${validated.symbol}\\b'`);
308
+ const references = stdout.split("\n").filter(Boolean).map(line => {
309
+ const parts = line.split(":", 2);
310
+ if (parts.length >= 2) {
311
+ return { file: parts[0], line: parseInt(parts[1], 10) || 0 };
312
+ }
313
+ return null;
314
+ }).filter(Boolean);
315
+ return { references };
316
+ }
317
+ catch (error) {
318
+ return { references: [] };
319
+ }
320
+ }
321
+ export async function summarizeHandler(input) {
322
+ const validated = SummarizeInput.parse(input);
323
+ try {
324
+ if (!await fs.pathExists(validated.file)) {
325
+ throw new Error(`File ${validated.file} not found`);
326
+ }
327
+ const content = await fs.readFile(validated.file, 'utf8');
328
+ const lines = content.split('\n');
329
+ const totalLines = lines.length;
330
+ // Basic analysis
331
+ const imports = lines.filter(line => line.trim().startsWith('import') || line.trim().startsWith('from')).length;
332
+ const functions = lines.filter(line => line.includes('function ') || line.includes('def ') || line.includes('=>')).length;
333
+ const classes = lines.filter(line => line.includes('class ')).length;
334
+ const comments = lines.filter(line => line.trim().startsWith('//') || line.trim().startsWith('#')).length;
335
+ const summary = `File: ${validated.file}
336
+ Lines: ${totalLines}
337
+ Imports: ${imports}
338
+ Functions: ${functions}
339
+ Classes: ${classes}
340
+ Comments: ${comments}
341
+ Estimated complexity: ${functions + classes > 10 ? 'high' : functions + classes > 5 ? 'medium' : 'low'}`;
342
+ return { summary };
343
+ }
344
+ catch (error) {
345
+ throw new Error(`Failed to summarize file: ${error instanceof Error ? error.message : String(error)}`);
346
+ }
347
+ }
348
+ export async function analyzeHandler(input) {
349
+ const validated = AnalyzeInput.parse(input);
350
+ try {
351
+ if (!await fs.pathExists(validated.file)) {
352
+ throw new Error(`File ${validated.file} not found`);
353
+ }
354
+ const content = await fs.readFile(validated.file, 'utf8');
355
+ const lines = content.split('\n');
356
+ // Extract imports
357
+ const imports = lines
358
+ .filter(line => line.trim().startsWith('import') || line.trim().startsWith('from'))
359
+ .map(line => line.trim());
360
+ // Extract function names
361
+ const functions = [];
362
+ const functionRegex = /(?:function\s+(\w+)|(\w+)\s*(?:=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*\{)|:\s*(?:async\s+)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*\{))|def\s+(\w+))/g;
363
+ for (const line of lines) {
364
+ let match;
365
+ while ((match = functionRegex.exec(line)) !== null) {
366
+ const funcName = match[1] || match[2] || match[3];
367
+ if (funcName && !functions.includes(funcName)) {
368
+ functions.push(funcName);
369
+ }
370
+ }
371
+ }
372
+ // Extract class names
373
+ const classes = [];
374
+ const classRegex = /class\s+(\w+)/g;
375
+ for (const line of lines) {
376
+ let match;
377
+ while ((match = classRegex.exec(line)) !== null) {
378
+ if (!classes.includes(match[1])) {
379
+ classes.push(match[1]);
380
+ }
381
+ }
382
+ }
383
+ return { imports, classes, functions };
384
+ }
385
+ catch (error) {
386
+ throw new Error(`Failed to analyze file: ${error instanceof Error ? error.message : String(error)}`);
387
+ }
388
+ }
389
+ // New semgrep-based search handler with debug logging
390
+ export async function semgrepSearchHandler(input) {
391
+ const validated = SemgrepSearchInput.parse(input);
392
+ console.log('[SEMGREP DEBUG] Semgrep search started with input:', JSON.stringify(validated, null, 2));
393
+ try {
394
+ const semgrepPath = await findToolPath('semgrep');
395
+ console.log('[SEMGREP DEBUG] Found semgrep path:', semgrepPath);
396
+ // Build semgrep command
397
+ let semgrepCmd = `${semgrepPath} --json --no-git-ignore`;
398
+ // Add language specification
399
+ if (validated.language !== "auto") {
400
+ semgrepCmd += ` --lang ${validated.language}`;
401
+ }
402
+ // Add pattern
403
+ semgrepCmd += ` --pattern '${validated.pattern}'`;
404
+ // Add path filters
405
+ if (validated.path_filters && validated.path_filters.length > 0) {
406
+ const includes = validated.path_filters.map(filter => `--include '${filter}'`).join(' ');
407
+ semgrepCmd += ` ${includes}`;
408
+ }
409
+ console.log('[SEMGREP DEBUG] Executing command:', semgrepCmd);
410
+ console.log('[SEMGREP DEBUG] Working directory:', process.cwd());
411
+ // Execute semgrep
412
+ const { stdout, stderr } = await execAsync(semgrepCmd);
413
+ console.log('[SEMGREP DEBUG] Command executed, stdout length:', stdout.length);
414
+ if (stderr)
415
+ console.log('[SEMGREP DEBUG] stderr:', stderr);
416
+ const semgrepOutput = JSON.parse(stdout || '{"results": []}');
417
+ console.log('[SEMGREP DEBUG] Parsed semgrep output, results count:', semgrepOutput.results?.length || 0);
418
+ // Convert semgrep results to our format
419
+ const results = (semgrepOutput.results || [])
420
+ .slice(0, validated.max_results)
421
+ .map((result) => ({
422
+ file: result.path,
423
+ line: result.start?.line || 0,
424
+ snippet: result.extra?.lines || '',
425
+ rule_id: result.check_id,
426
+ message: result.extra?.message || ''
427
+ }));
428
+ console.log('[SEMGREP DEBUG] Returning results:', results.length);
429
+ return { results };
430
+ }
431
+ catch (error) {
432
+ console.warn('[SEMGREP WARN] Semgrep failed, falling back to ripgrep:', error);
433
+ try {
434
+ const rgPath = await findToolPath('rg');
435
+ console.log('[SEMGREP FALLBACK] Using ripgrep at:', rgPath);
436
+ const { stdout } = await execAsync(`${rgPath} --line-number --max-count ${validated.max_results} '${validated.pattern}'`);
437
+ const results = stdout.split("\n").filter(Boolean).map(line => {
438
+ const parts = line.split(":", 3);
439
+ if (parts.length >= 3) {
440
+ const [file, lineNo, snippet] = parts;
441
+ return {
442
+ file,
443
+ line: parseInt(lineNo, 10) || 0,
444
+ snippet: snippet.trim(),
445
+ rule_id: "ripgrep-fallback",
446
+ message: "Fallback search using ripgrep"
447
+ };
448
+ }
449
+ return null;
450
+ }).filter(Boolean);
451
+ console.log('[SEMGREP FALLBACK] Fallback results:', results.length);
452
+ return { results };
453
+ }
454
+ catch (fallbackError) {
455
+ console.error('[SEMGREP ERROR] Both semgrep and fallback failed:', fallbackError);
456
+ throw new Error(`Semgrep search failed: ${error instanceof Error ? error.message : String(error)}`);
457
+ }
458
+ }
459
+ }
460
+ // Enhanced file search with better options and debug logging
461
+ export async function enhancedFileSearchHandler(input) {
462
+ const validated = EnhancedFileSearchInput.parse(input);
463
+ console.log('[SEARCH DEBUG] Enhanced search started with input:', JSON.stringify(validated, null, 2));
464
+ try {
465
+ const rgPath = await findToolPath('rg');
466
+ console.log('[SEARCH DEBUG] Found rg path:', rgPath);
467
+ // Use spawn approach (based on successful MCP patterns)
468
+ const args = ['--line-number', '--no-heading'];
469
+ // Add search type options
470
+ if (validated.search_type === "exact") {
471
+ args.push('--fixed-strings');
472
+ }
473
+ else if (validated.search_type === "regex") {
474
+ args.push('--regexp');
475
+ }
476
+ // Add context if requested
477
+ if (validated.include_context) {
478
+ args.push('--context', '2');
479
+ }
480
+ // Add file extension filters
481
+ if (validated.file_extensions && validated.file_extensions.length > 0) {
482
+ validated.file_extensions.forEach(ext => {
483
+ const cleanExt = ext.startsWith('.') ? ext : '.' + ext;
484
+ args.push('--glob', `*${cleanExt}`);
485
+ });
486
+ }
487
+ // Add exclusion patterns
488
+ if (validated.exclude_patterns && validated.exclude_patterns.length > 0) {
489
+ validated.exclude_patterns.forEach(pattern => {
490
+ args.push('--glob', `!${pattern}`);
491
+ });
492
+ }
493
+ // Add max results and query
494
+ args.push('--max-count', validated.max_results.toString());
495
+ args.push(validated.query);
496
+ console.log('[SEARCH DEBUG] Executing command:', rgPath, 'with args:', args);
497
+ console.log('[SEARCH DEBUG] Working directory:', process.cwd());
498
+ let stdout, stderr;
499
+ try {
500
+ const result = await spawnAsync(rgPath, args, {
501
+ cwd: process.cwd(),
502
+ timeout: 60000 // 60 second timeout (MCP TypeScript client standard)
503
+ });
504
+ stdout = result.stdout;
505
+ stderr = result.stderr;
506
+ console.log('[SEARCH DEBUG] Command executed successfully, stdout length:', stdout.length);
507
+ if (stderr)
508
+ console.log('[SEARCH DEBUG] stderr:', stderr);
509
+ }
510
+ catch (spawnError) {
511
+ console.error('[SEARCH ERROR] Spawn failed:', spawnError);
512
+ throw new Error(`Search execution failed: ${spawnError.message}`);
513
+ }
514
+ const results = stdout.split("\n").filter(Boolean).map(line => {
515
+ const parts = line.split(":", 3);
516
+ if (parts.length >= 3) {
517
+ const [file, lineNo, snippet] = parts;
518
+ return {
519
+ file,
520
+ line: parseInt(lineNo, 10) || 0,
521
+ snippet: snippet.trim(),
522
+ score: 1.0
523
+ };
524
+ }
525
+ return null;
526
+ }).filter(Boolean);
527
+ console.log('[SEARCH DEBUG] Found results:', results.length);
528
+ return { results };
529
+ }
530
+ catch (error) {
531
+ console.error('[SEARCH ERROR] Enhanced search failed:', error);
532
+ throw new Error(`Search failed: ${error instanceof Error ? error.message : String(error)}`);
533
+ }
534
+ }
535
+ // Phase-aware Semgrep rule selection based on caller context
536
+ function getPhaseAwareSemgrepRules(mode, phase) {
537
+ const phaseRules = {
538
+ 'architect': [
539
+ '--config', 'p/security-audit',
540
+ '--config', 'p/performance'
541
+ ],
542
+ 'engineer': [
543
+ // Use basic pattern-based search for engineer
544
+ ],
545
+ 'debugger': [
546
+ '--config', 'p/security-audit',
547
+ '--config', 'p/correctness'
548
+ ],
549
+ 'reviewer': [
550
+ '--config', 'p/security-audit',
551
+ '--config', 'p/performance',
552
+ '--config', 'p/correctness'
553
+ ]
554
+ };
555
+ return phaseRules[phase || 'engineer'] || [];
556
+ }
557
+ // Enhanced Hybrid Search Handler - unified entry point for lexical + semantic search
558
+ export async function hybridSearchHandler(input) {
559
+ const validated = HybridSearchInput.parse(input);
560
+ const startTime = Date.now();
561
+ const errors = [];
562
+ console.log('[HYBRID DEBUG] Hybrid search started with input:', JSON.stringify(validated, null, 2));
563
+ let lexicalHits = [];
564
+ let semanticHits = [];
565
+ let rgHits = 0;
566
+ let semgrepHits = 0;
567
+ // Phase 1: Run ripgrep for lexical search (unless mode is purely semantic)
568
+ if (validated.mode === "lexical" || validated.mode === "auto") {
569
+ try {
570
+ console.log('[HYBRID DEBUG] Running lexical search with ripgrep');
571
+ const rgPath = await findToolPath('rg');
572
+ // Build ripgrep command with JSON output
573
+ const args = ['--json', '--line-number'];
574
+ // Handle gitignore and hidden files
575
+ if (!validated.followGitIgnore) {
576
+ args.push('--no-ignore');
577
+ }
578
+ if (validated.includeHidden) {
579
+ args.push('--hidden', '--no-ignore');
580
+ }
581
+ // Add context lines
582
+ if (validated.contextLines && validated.contextLines > 0) {
583
+ args.push('--context', validated.contextLines.toString());
584
+ }
585
+ // Add file type filters
586
+ if (validated.fileTypes && validated.fileTypes.length > 0) {
587
+ validated.fileTypes.forEach(type => {
588
+ args.push('--type', type);
589
+ });
590
+ }
591
+ // Add max results (split between lexical and semantic)
592
+ const maxLexical = validated.mode === "lexical" ? validated.maxHits : Math.floor(validated.maxHits / 2);
593
+ args.push('--max-count', maxLexical.toString());
594
+ // Add query and path
595
+ args.push(validated.query);
596
+ if (validated.path) {
597
+ args.push(validated.path);
598
+ }
599
+ console.log('[HYBRID DEBUG] Ripgrep command:', rgPath, args.join(' '));
600
+ const { stdout, stderr } = await spawnAsync(rgPath, args, {
601
+ cwd: process.cwd(),
602
+ timeout: validated.timeoutMs
603
+ });
604
+ if (stderr) {
605
+ console.warn('[HYBRID WARN] Ripgrep stderr:', stderr);
606
+ errors.push(`ripgrep warning: ${stderr}`);
607
+ }
608
+ // Parse ripgrep JSON output
609
+ lexicalHits = await parseRipgrepJSON(stdout, validated.contextLines);
610
+ rgHits = lexicalHits.length;
611
+ console.log('[HYBRID DEBUG] Ripgrep found', rgHits, 'results');
612
+ }
613
+ catch (error) {
614
+ console.warn('[HYBRID WARN] Ripgrep failed:', error);
615
+ errors.push(`ripgrep failed: ${error instanceof Error ? error.message : String(error)}`);
616
+ }
617
+ }
618
+ // Phase 2: Run Semgrep for semantic search (if mode allows and we have target files)
619
+ if (validated.mode === "semantic" || (validated.mode === "auto" && lexicalHits.length > 0)) {
620
+ try {
621
+ console.log('[HYBRID DEBUG] Running semantic search with semgrep');
622
+ const semgrepPath = await findToolPath('semgrep');
623
+ // Build semgrep command with phase-aware configuration
624
+ const semgrepArgs = ['--json', '--no-git-ignore', '--quiet', '--metrics=off'];
625
+ // Add phase-aware rule configuration
626
+ const phaseRules = getPhaseAwareSemgrepRules(validated.mode, input.phase);
627
+ // If no rules available (like engineer phase) or semantic mode, use pattern-based search
628
+ if (phaseRules.length === 0 || validated.mode === "semantic") {
629
+ // Use a pattern based on the query for semantic analysis
630
+ const semanticPattern = `$FUNC(..., ${validated.query}, ...)`;
631
+ semgrepArgs.push('--pattern', semanticPattern);
632
+ }
633
+ else {
634
+ // Use phase-aware rules
635
+ semgrepArgs.push(...phaseRules);
636
+ }
637
+ // Determine target files to scan
638
+ const targetFiles = [];
639
+ // Scope to files from lexical search if available
640
+ if (lexicalHits.length > 0 && validated.mode === "auto") {
641
+ const uniqueFiles = Array.from(new Set(lexicalHits.map(hit => hit.file)));
642
+ console.log('[HYBRID DEBUG] Scoping semgrep to', uniqueFiles.length, 'files from lexical search');
643
+ targetFiles.push(...uniqueFiles);
644
+ }
645
+ else {
646
+ // Default to current directory or specified path
647
+ targetFiles.push(validated.path || '.');
648
+ }
649
+ // Add target files to args
650
+ semgrepArgs.push(...targetFiles);
651
+ // Add language specification
652
+ if (validated.fileTypes && validated.fileTypes.length > 0) {
653
+ const langMap = {
654
+ 'ts': 'typescript',
655
+ 'tsx': 'typescript',
656
+ 'js': 'javascript',
657
+ 'jsx': 'javascript',
658
+ 'py': 'python',
659
+ 'go': 'go',
660
+ 'java': 'java'
661
+ };
662
+ const lang = validated.fileTypes.map(t => langMap[t] || t)[0];
663
+ if (lang) {
664
+ semgrepArgs.push('--lang', lang);
665
+ }
666
+ }
667
+ console.log('[HYBRID DEBUG] Semgrep command:', semgrepPath, semgrepArgs.join(' '));
668
+ const { stdout: semgrepStdout, stderr: semgrepStderr } = await spawnAsync(semgrepPath, semgrepArgs, {
669
+ cwd: process.cwd(),
670
+ timeout: validated.timeoutMs
671
+ });
672
+ if (semgrepStderr) {
673
+ console.warn('[HYBRID WARN] Semgrep stderr:', semgrepStderr);
674
+ errors.push(`semgrep warning: ${semgrepStderr}`);
675
+ }
676
+ // Parse semgrep output
677
+ const semgrepOutput = JSON.parse(semgrepStdout || '{"results": []}');
678
+ semanticHits = normalizeSemgrepResults(semgrepOutput);
679
+ semgrepHits = semanticHits.length;
680
+ console.log('[HYBRID DEBUG] Semgrep found', semgrepHits, 'results');
681
+ }
682
+ catch (error) {
683
+ // Handle Semgrep exit codes properly:
684
+ // 0 = success, no findings
685
+ // 1 = error (configuration, parsing, etc.)
686
+ // 2 = success, findings found
687
+ if (error.code === 2) {
688
+ // Exit code 2 means findings were found - this is success for Semgrep
689
+ try {
690
+ const semgrepOutput = JSON.parse(error.stdout || '{"results": []}');
691
+ semanticHits = normalizeSemgrepResults(semgrepOutput);
692
+ semgrepHits = semanticHits.length;
693
+ console.log('[HYBRID DEBUG] Semgrep found', semgrepHits, 'results via exit code 2');
694
+ }
695
+ catch (parseError) {
696
+ console.warn('[HYBRID WARN] Semgrep exit code 2 but failed to parse output:', parseError);
697
+ errors.push(`semgrep output parsing failed: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
698
+ }
699
+ }
700
+ else {
701
+ console.warn('[HYBRID WARN] Semgrep failed:', error);
702
+ errors.push(`semgrep failed: ${error instanceof Error ? error.message : String(error)}`);
703
+ }
704
+ }
705
+ }
706
+ // Phase 3: Merge, deduplicate, and cap results
707
+ const totalHits = rgHits + semgrepHits;
708
+ const truncated = totalHits > validated.maxHits;
709
+ if (truncated) {
710
+ const lexicalCap = Math.floor(validated.maxHits * 0.7); // Favor lexical results
711
+ const semanticCap = validated.maxHits - lexicalCap;
712
+ lexicalHits = lexicalHits.slice(0, lexicalCap);
713
+ semanticHits = semanticHits.slice(0, semanticCap);
714
+ }
715
+ const elapsedMs = Date.now() - startTime;
716
+ // Generate structured summary for terminal display (as per reliability manifest)
717
+ const totalResults = lexicalHits.length + semanticHits.length;
718
+ const modeBanner = generateModeBanner(validated.mode, validated.phase, validated.query, totalResults, elapsedMs);
719
+ // Create summary hits for terminal display
720
+ const summaryHits = generateSummaryHits(lexicalHits, semanticHits, validated.maxHits);
721
+ const result = {
722
+ // Summary output for terminal (concise, human-readable)
723
+ summary: modeBanner,
724
+ hits: summaryHits,
725
+ // Detailed output for manual/verbose logging
726
+ lexicalHits,
727
+ semanticHits,
728
+ truncated,
729
+ stats: {
730
+ elapsedMs,
731
+ rgHits,
732
+ semgrepHits,
733
+ totalResults
734
+ },
735
+ errors: errors.length > 0 ? errors : undefined
736
+ };
737
+ console.log('[HYBRID DEBUG] Hybrid search completed in', elapsedMs, 'ms');
738
+ console.log('[HYBRID DEBUG] Final result:', JSON.stringify({
739
+ lexicalCount: lexicalHits.length,
740
+ semanticCount: semanticHits.length,
741
+ truncated,
742
+ errors: errors.length
743
+ }, null, 2));
744
+ return result;
745
+ }
746
+ // Helper function to generate mode banner for structured output
747
+ function generateModeBanner(mode, phase, query, totalResults, elapsedMs) {
748
+ const phaseLabel = phase ? ` | Phase: ${phase}` : '';
749
+ const modeLabel = mode === 'auto' ? 'hybrid' : mode;
750
+ const resultsLabel = totalResults === 1 ? 'result' : 'results';
751
+ const timeLabel = elapsedMs < 1000 ? `${elapsedMs}ms` : `${(elapsedMs / 1000).toFixed(1)}s`;
752
+ return `🔍 [${modeLabel.charAt(0).toUpperCase() + modeLabel.slice(1)} Search]${phaseLabel} | Query: '${query}' | ${totalResults} ${resultsLabel} (${timeLabel})`;
753
+ }
754
+ // Helper function to generate summary hits for terminal display
755
+ function generateSummaryHits(lexicalHits, semanticHits, maxDisplay = 10) {
756
+ const summaryHits = [];
757
+ let displayCount = 0;
758
+ // Combine and prioritize hits (lexical first, then semantic)
759
+ const allHits = [
760
+ ...lexicalHits.map(hit => ({
761
+ type: 'lexical',
762
+ file: hit.file,
763
+ line: hit.line,
764
+ preview: hit.text?.substring(0, 100) || hit.snippet?.substring(0, 100) || ''
765
+ })),
766
+ ...semanticHits.map(hit => ({
767
+ type: 'semantic',
768
+ file: hit.file,
769
+ line: hit.line,
770
+ preview: hit.message?.substring(0, 100) || hit.ruleId || ''
771
+ }))
772
+ ];
773
+ // Generate concise file:line → preview format
774
+ for (const hit of allHits) {
775
+ if (displayCount >= maxDisplay)
776
+ break;
777
+ const preview = hit.preview.replace(/\s+/g, ' ').trim();
778
+ const truncatedPreview = preview.length > 80 ? preview.substring(0, 80) + '...' : preview;
779
+ const typeIcon = hit.type === 'semantic' ? '🎯' : '📄';
780
+ summaryHits.push(`${typeIcon} ${hit.file}:${hit.line} → ${truncatedPreview}`);
781
+ displayCount++;
782
+ }
783
+ if (allHits.length > maxDisplay) {
784
+ summaryHits.push(`... and ${allHits.length - maxDisplay} more results`);
785
+ }
786
+ return summaryHits;
787
+ }
788
+ //# sourceMappingURL=searchTools.js.map