bobs-workshop 0.3.3 → 3.1.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.
Files changed (200) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +199 -210
  3. package/bin/bobs-workshop.js +109 -0
  4. package/config/agents.json +27 -0
  5. package/dist/plugins/bobs-workshop.js +34 -0
  6. package/dist/tools/background-agent/cancel.d.ts +3 -0
  7. package/dist/tools/background-agent/cancel.d.ts.map +1 -0
  8. package/dist/tools/background-agent/cancel.js +52 -0
  9. package/dist/tools/background-agent/concurrency.d.ts +15 -0
  10. package/dist/tools/background-agent/concurrency.d.ts.map +1 -0
  11. package/dist/tools/background-agent/concurrency.js +61 -0
  12. package/dist/tools/background-agent/index.d.ts +8 -0
  13. package/dist/tools/background-agent/index.d.ts.map +1 -0
  14. package/dist/tools/background-agent/index.js +7 -0
  15. package/dist/tools/background-agent/launch.d.ts +6 -0
  16. package/dist/tools/background-agent/launch.d.ts.map +1 -0
  17. package/dist/tools/background-agent/launch.js +33 -0
  18. package/dist/tools/background-agent/list.d.ts +7 -0
  19. package/dist/tools/background-agent/list.d.ts.map +1 -0
  20. package/dist/tools/background-agent/list.js +40 -0
  21. package/dist/tools/background-agent/manager.d.ts +29 -0
  22. package/dist/tools/background-agent/manager.d.ts.map +1 -0
  23. package/dist/tools/background-agent/manager.js +377 -0
  24. package/dist/tools/background-agent/output.d.ts +3 -0
  25. package/dist/tools/background-agent/output.d.ts.map +1 -0
  26. package/dist/tools/background-agent/output.js +41 -0
  27. package/dist/tools/background-agent/types.d.ts +46 -0
  28. package/dist/tools/background-agent/types.d.ts.map +1 -0
  29. package/dist/tools/background-agent/types.js +1 -0
  30. package/dist/tools/index.d.ts +9 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/index.js +8 -0
  33. package/dist/tools/manual/index.d.ts +3 -0
  34. package/dist/tools/manual/index.d.ts.map +1 -0
  35. package/dist/tools/manual/index.js +2 -0
  36. package/dist/tools/manual/manual-update.d.ts +4 -0
  37. package/dist/tools/manual/manual-update.d.ts.map +1 -0
  38. package/dist/tools/manual/manual-update.js +190 -0
  39. package/dist/tools/manual/verify-manual.d.ts +4 -0
  40. package/dist/tools/manual/verify-manual.d.ts.map +1 -0
  41. package/dist/tools/manual/verify-manual.js +46 -0
  42. package/package.json +34 -66
  43. package/postinstall.js +190 -0
  44. package/src/agents/alice.md +466 -0
  45. package/src/agents/bob-rev.md +493 -0
  46. package/src/agents/bob-send.md +277 -0
  47. package/src/agents/bob.md +442 -0
  48. package/src/agents/trace.md +451 -0
  49. package/src/plugins/bobs-workshop.ts +45 -0
  50. package/src/skills/api-patterns/SKILL.md +376 -0
  51. package/src/skills/architecture/SKILL.md +271 -0
  52. package/src/skills/bobs-workshop/performance/icon.svg +3 -0
  53. package/src/skills/brainstorming/SKILL.md +210 -0
  54. package/src/skills/clean-code/SKILL.md +151 -0
  55. package/src/skills/code-review-checklist/SKILL.md +220 -0
  56. package/src/skills/database-design/SKILL.md +271 -0
  57. package/src/skills/exploration/SKILL.md +257 -0
  58. package/src/skills/frontend-ui-ux/SKILL.md +78 -0
  59. package/src/skills/git-master/SKILL.md +1105 -0
  60. package/src/skills/performance/SKILL.md +144 -0
  61. package/src/skills/performance/icon.svg +3 -0
  62. package/src/skills/plan-writing/SKILL.md +225 -0
  63. package/src/skills/security/SKILL.md +410 -0
  64. package/src/skills/simplification/SKILL.md +238 -0
  65. package/src/skills/systematic-debugging/SKILL.md +175 -0
  66. package/src/skills/testing-patterns/SKILL.md +305 -0
  67. package/src/skills/verification/SKILL.md +286 -0
  68. package/src/tools/background-agent/cancel.ts +67 -0
  69. package/src/tools/background-agent/concurrency.ts +71 -0
  70. package/src/tools/background-agent/index.ts +7 -0
  71. package/src/tools/background-agent/launch.ts +39 -0
  72. package/src/tools/background-agent/list.ts +50 -0
  73. package/src/tools/background-agent/manager.ts +455 -0
  74. package/src/tools/background-agent/output.ts +57 -0
  75. package/src/tools/background-agent/types.ts +55 -0
  76. package/src/tools/index.ts +8 -0
  77. package/src/tools/manual/index.ts +2 -0
  78. package/src/tools/manual/manual-update.ts +197 -0
  79. package/src/tools/manual/verify-manual.ts +55 -0
  80. package/uninstall.js +64 -0
  81. package/Claude.md +0 -162
  82. package/bin/bobs-mcp-server.js +0 -11
  83. package/bin/bobs-mcp.js +0 -130
  84. package/dist/api/taskLogger.js +0 -106
  85. package/dist/api/taskLogger.js.map +0 -1
  86. package/dist/cli/checker.js +0 -401
  87. package/dist/cli/checker.js.map +0 -1
  88. package/dist/cli/cleanup.js +0 -131
  89. package/dist/cli/cleanup.js.map +0 -1
  90. package/dist/cli/debug.js +0 -157
  91. package/dist/cli/debug.js.map +0 -1
  92. package/dist/cli/health.js +0 -97
  93. package/dist/cli/health.js.map +0 -1
  94. package/dist/cli/setup.js +0 -81
  95. package/dist/cli/setup.js.map +0 -1
  96. package/dist/cli/workshop.js +0 -42
  97. package/dist/cli/workshop.js.map +0 -1
  98. package/dist/dashboard/server.js +0 -1203
  99. package/dist/dashboard/server.js.map +0 -1
  100. package/dist/index.js +0 -960
  101. package/dist/index.js.map +0 -1
  102. package/dist/prompts/architect.js +0 -221
  103. package/dist/prompts/architect.js.map +0 -1
  104. package/dist/prompts/debugger.js +0 -257
  105. package/dist/prompts/debugger.js.map +0 -1
  106. package/dist/prompts/engineer.js +0 -249
  107. package/dist/prompts/engineer.js.map +0 -1
  108. package/dist/prompts/orchestrator.js +0 -304
  109. package/dist/prompts/orchestrator.js.map +0 -1
  110. package/dist/prompts/reviewer.js +0 -289
  111. package/dist/prompts/reviewer.js.map +0 -1
  112. package/dist/services/activitySummarizer.js +0 -388
  113. package/dist/services/activitySummarizer.js.map +0 -1
  114. package/dist/services/changeValidator.js +0 -396
  115. package/dist/services/changeValidator.js.map +0 -1
  116. package/dist/services/claudeOrchestrator.js +0 -343
  117. package/dist/services/claudeOrchestrator.js.map +0 -1
  118. package/dist/services/fileMonitor.js +0 -250
  119. package/dist/services/fileMonitor.js.map +0 -1
  120. package/dist/services/implementationSummarizer.js +0 -306
  121. package/dist/services/implementationSummarizer.js.map +0 -1
  122. package/dist/services/liveMonitor.js +0 -315
  123. package/dist/services/liveMonitor.js.map +0 -1
  124. package/dist/services/mcpAuditLogger.js +0 -104
  125. package/dist/services/mcpAuditLogger.js.map +0 -1
  126. package/dist/services/mcpLogger.js +0 -223
  127. package/dist/services/mcpLogger.js.map +0 -1
  128. package/dist/services/tmuxManager.js +0 -541
  129. package/dist/services/tmuxManager.js.map +0 -1
  130. package/dist/tools/approvalTools.js +0 -244
  131. package/dist/tools/approvalTools.js.map +0 -1
  132. package/dist/tools/autoDebugger.js +0 -147
  133. package/dist/tools/autoDebugger.js.map +0 -1
  134. package/dist/tools/cleanupService.js +0 -221
  135. package/dist/tools/cleanupService.js.map +0 -1
  136. package/dist/tools/dashboardTools.js +0 -342
  137. package/dist/tools/dashboardTools.js.map +0 -1
  138. package/dist/tools/developmentNudges.js +0 -336
  139. package/dist/tools/developmentNudges.js.map +0 -1
  140. package/dist/tools/gitTools.js +0 -741
  141. package/dist/tools/gitTools.js.map +0 -1
  142. package/dist/tools/orchestratorTools.js +0 -832
  143. package/dist/tools/orchestratorTools.js.map +0 -1
  144. package/dist/tools/searchCache.js +0 -64
  145. package/dist/tools/searchCache.js.map +0 -1
  146. package/dist/tools/searchTools.js +0 -1107
  147. package/dist/tools/searchTools.js.map +0 -1
  148. package/dist/tools/semgrep-patterns.js +0 -296
  149. package/dist/tools/semgrep-patterns.js.map +0 -1
  150. package/dist/tools/specTools.js +0 -332
  151. package/dist/tools/specTools.js.map +0 -1
  152. package/dist/tools/structural/__tests__/orchestrator.test.js +0 -61
  153. package/dist/tools/structural/__tests__/orchestrator.test.js.map +0 -1
  154. package/dist/tools/structural/cache.js +0 -226
  155. package/dist/tools/structural/cache.js.map +0 -1
  156. package/dist/tools/structural/engines/python/index.js +0 -118
  157. package/dist/tools/structural/engines/python/index.js.map +0 -1
  158. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js +0 -97
  159. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js.map +0 -1
  160. package/dist/tools/structural/engines/typescript/analyzer.js +0 -433
  161. package/dist/tools/structural/engines/typescript/analyzer.js.map +0 -1
  162. package/dist/tools/structural/engines/typescript/index.js +0 -381
  163. package/dist/tools/structural/engines/typescript/index.js.map +0 -1
  164. package/dist/tools/structural/engines/typescript/utils.js +0 -279
  165. package/dist/tools/structural/engines/typescript/utils.js.map +0 -1
  166. package/dist/tools/structural/index.js +0 -248
  167. package/dist/tools/structural/index.js.map +0 -1
  168. package/dist/tools/structural/types.js +0 -18
  169. package/dist/tools/structural/types.js.map +0 -1
  170. package/dist/tools/tmuxTools.js +0 -100
  171. package/dist/tools/tmuxTools.js.map +0 -1
  172. package/dist/tools/workRecorder.js +0 -215
  173. package/dist/tools/workRecorder.js.map +0 -1
  174. package/dist/tools/worktreeTools.js +0 -705
  175. package/dist/tools/worktreeTools.js.map +0 -1
  176. package/dist/utils/__tests__/integration.test.js +0 -57
  177. package/dist/utils/__tests__/integration.test.js.map +0 -1
  178. package/dist/utils/__tests__/serverDetection.test.js +0 -151
  179. package/dist/utils/__tests__/serverDetection.test.js.map +0 -1
  180. package/dist/utils/errorHandling.js +0 -336
  181. package/dist/utils/errorHandling.js.map +0 -1
  182. package/dist/utils/processManager.js +0 -172
  183. package/dist/utils/processManager.js.map +0 -1
  184. package/dist/utils/reliability.js +0 -263
  185. package/dist/utils/reliability.js.map +0 -1
  186. package/dist/utils/responseFormatter.js +0 -250
  187. package/dist/utils/responseFormatter.js.map +0 -1
  188. package/dist/utils/serverDetection.js +0 -133
  189. package/dist/utils/serverDetection.js.map +0 -1
  190. package/dist/utils/specMigration.js +0 -105
  191. package/dist/utils/specMigration.js.map +0 -1
  192. package/dist/validation/schemas.js +0 -299
  193. package/dist/validation/schemas.js.map +0 -1
  194. package/public/.well-known/mcp/manifest.json +0 -473
  195. package/public/index.html +0 -3157
  196. package/public/index.html.backup +0 -2805
  197. package/public/index.html.backup2 +0 -1292
  198. package/scripts/cleanup-system-logs.ts +0 -121
  199. package/scripts/init-workspace.js +0 -63
  200. package/scripts/install-search-tools.js +0 -116
@@ -1,1107 +0,0 @@
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
- import path from "path";
7
- import { fileURLToPath } from "url";
8
- import { searchCache } from "./searchCache.js";
9
- import { SemgrepPatternGenerator } from "./semgrep-patterns.js";
10
- import { StructuralSearchOrchestrator } from "./structural/index.js";
11
- const execAsync = promisify(exec);
12
- // ES module __dirname equivalent
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
- // Tool-specific exit code semantics
16
- const EXIT_CODE_SEMANTICS = {
17
- 'rg': { 0: 'success', 1: 'no_matches', 2: 'error' },
18
- 'semgrep': {
19
- 0: 'no_findings',
20
- 1: 'error',
21
- 2: 'findings',
22
- 7: 'config_not_available' // Config rulesets not found (non-fatal)
23
- }
24
- };
25
- // Production-ready spawn execution with timeout handling
26
- function spawnAsync(command, args, options = {}) {
27
- return new Promise((resolve, reject) => {
28
- const child = spawn(command, args, {
29
- ...options,
30
- stdio: ['ignore', 'pipe', 'pipe'] // Explicitly set stdio
31
- });
32
- let stdout = '';
33
- let stderr = '';
34
- let isResolved = false;
35
- // Production timeout handling (default 60 seconds - MCP standard)
36
- const timeout = options.timeout || 60000;
37
- const timeoutId = setTimeout(() => {
38
- if (!isResolved) {
39
- isResolved = true;
40
- child.kill('SIGTERM'); // Graceful termination
41
- setTimeout(() => {
42
- if (!child.killed) {
43
- child.kill('SIGKILL'); // Force kill if still running
44
- }
45
- }, 5000);
46
- reject(new Error(`Command timed out after ${timeout}ms`));
47
- }
48
- }, timeout);
49
- child.stdout?.on('data', (data) => {
50
- stdout += data.toString();
51
- });
52
- child.stderr?.on('data', (data) => {
53
- stderr += data.toString();
54
- });
55
- child.on('close', (code) => {
56
- if (!isResolved) {
57
- isResolved = true;
58
- clearTimeout(timeoutId);
59
- const toolName = options.toolName || 'unknown';
60
- const semantics = EXIT_CODE_SEMANTICS[toolName];
61
- const meaning = semantics?.[code ?? 0] || 'error';
62
- // Treat success, no_matches, no_findings, findings, and config_not_available as successful outcomes
63
- if (meaning === 'success' || meaning === 'no_matches' || meaning === 'no_findings' || meaning === 'findings' || meaning === 'config_not_available') {
64
- resolve({ stdout, stderr, exitCode: code ?? 0 });
65
- }
66
- else {
67
- const error = new Error(`${toolName} failed with code ${code}: ${meaning}`);
68
- error.code = code;
69
- error.exitCode = code;
70
- error.stdout = stdout;
71
- error.stderr = stderr;
72
- reject(error);
73
- }
74
- }
75
- });
76
- child.on('error', (err) => {
77
- if (!isResolved) {
78
- isResolved = true;
79
- clearTimeout(timeoutId);
80
- reject(err);
81
- }
82
- });
83
- });
84
- }
85
- // Cross-platform tool path finder with multiple fallback strategies
86
- async function findToolPath(toolName) {
87
- console.log(`[TOOL PATH] Finding path for: ${toolName}`);
88
- // Strategy 1: Try system PATH first (most portable)
89
- try {
90
- await execAsync(`${toolName} --version`, { timeout: 3000 });
91
- console.log(`[TOOL PATH] Found ${toolName} in system PATH`);
92
- return toolName;
93
- }
94
- catch (error) {
95
- console.log(`[TOOL PATH] ${toolName} not in system PATH, trying fallbacks`);
96
- }
97
- // Strategy 2: Try npm packages in current project
98
- try {
99
- const projectPath = path.resolve(process.cwd(), `node_modules/.bin/${toolName}`);
100
- if (await fs.pathExists(projectPath)) {
101
- await execAsync(`${projectPath} --version`, { timeout: 3000 });
102
- console.log(`[TOOL PATH] Found ${toolName} in project node_modules/.bin`);
103
- return projectPath;
104
- }
105
- }
106
- catch (error) {
107
- console.log(`[TOOL PATH] ${toolName} not in project node_modules/.bin`);
108
- }
109
- // Strategy 3: Try npm packages in bobs-workshop directory
110
- try {
111
- const bobsPath = path.resolve(__dirname, `../../node_modules/.bin/${toolName}`);
112
- if (await fs.pathExists(bobsPath)) {
113
- await execAsync(`${bobsPath} --version`, { timeout: 3000 });
114
- console.log(`[TOOL PATH] Found ${toolName} in bobs-workshop node_modules/.bin`);
115
- return bobsPath;
116
- }
117
- }
118
- catch (error) {
119
- console.log(`[TOOL PATH] ${toolName} not in bobs-workshop node_modules/.bin`);
120
- }
121
- // Strategy 4: Tool-specific fallback paths
122
- if (toolName === 'rg') {
123
- // Try @vscode/ripgrep package
124
- const rgPaths = [
125
- path.resolve(process.cwd(), 'node_modules/@vscode/ripgrep/bin/rg'),
126
- path.resolve(__dirname, '../../node_modules/@vscode/ripgrep/bin/rg'),
127
- // Claude Code location as last resort
128
- `/Users/${process.env.USER}/.claude/local/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/arm64-darwin/rg`
129
- ];
130
- for (const rgPath of rgPaths) {
131
- try {
132
- if (await fs.pathExists(rgPath)) {
133
- await execAsync(`${rgPath} --version`, { timeout: 3000 });
134
- console.log(`[TOOL PATH] Found ripgrep at: ${rgPath}`);
135
- return rgPath;
136
- }
137
- }
138
- catch (error) {
139
- console.log(`[TOOL PATH] ripgrep not found at: ${rgPath}`);
140
- }
141
- }
142
- }
143
- // Strategy 5: Final fallback with warning
144
- console.log(`[TOOL PATH] Warning: ${toolName} not found, using system fallback`);
145
- return toolName;
146
- }
147
- export const FileSearchInput = z.object({
148
- query: z.string(),
149
- max_results: z.number().optional().default(50),
150
- path_filters: z.array(z.string()).optional()
151
- });
152
- export const FileSearchOutput = z.object({
153
- results: z.array(z.object({
154
- file: z.string(),
155
- line: z.number(),
156
- snippet: z.string(),
157
- score: z.number()
158
- }))
159
- });
160
- export const SymbolSearchInput = z.object({
161
- symbol: z.string()
162
- });
163
- export const SymbolSearchOutput = z.object({
164
- definitions: z.array(z.object({
165
- file: z.string(),
166
- line: z.number()
167
- }))
168
- });
169
- export const ReferencesSearchInput = z.object({
170
- symbol: z.string()
171
- });
172
- export const ReferencesSearchOutput = z.object({
173
- references: z.array(z.object({
174
- file: z.string(),
175
- line: z.number()
176
- }))
177
- });
178
- export const SummarizeInput = z.object({
179
- file: z.string()
180
- });
181
- export const SummarizeOutput = z.object({
182
- summary: z.string()
183
- });
184
- export const AnalyzeInput = z.object({
185
- file: z.string()
186
- });
187
- export const AnalyzeOutput = z.object({
188
- imports: z.array(z.string()),
189
- classes: z.array(z.string()),
190
- functions: z.array(z.string())
191
- });
192
- // New semgrep-based search input/output schemas
193
- export const SemgrepSearchInput = z.object({
194
- pattern: z.string().describe("Semgrep pattern to search for (e.g., 'function verifyToken($TOKEN)' for structured AST search)"),
195
- language: z.enum(["typescript", "javascript", "python", "go", "java", "auto"]).optional().default("auto").describe("Language for pattern matching"),
196
- path_filters: z.array(z.string()).optional().describe("File path filters/globs to limit search scope"),
197
- max_results: z.number().optional().default(50).describe("Maximum number of results to return")
198
- });
199
- export const SemgrepSearchOutput = z.object({
200
- results: z.array(z.object({
201
- file: z.string(),
202
- line: z.number(),
203
- snippet: z.string(),
204
- rule_id: z.string().optional(),
205
- message: z.string().optional()
206
- }))
207
- });
208
- // Enhanced file search with more options
209
- export const EnhancedFileSearchInput = z.object({
210
- query: z.string().describe("Search query (regex pattern supported)"),
211
- search_type: z.enum(["text", "regex", "exact"]).optional().default("text").describe("Type of search to perform"),
212
- file_extensions: z.array(z.string()).optional().describe("File extensions to include (e.g., ['.ts', '.js'])"),
213
- exclude_patterns: z.array(z.string()).optional().describe("Patterns to exclude from search"),
214
- max_results: z.number().optional().default(50),
215
- include_context: z.boolean().optional().default(false).describe("Include surrounding lines for context")
216
- });
217
- // Hybrid Search schemas - unified entry point for lexical + semantic search
218
- export const HybridSearchInput = z.object({
219
- query: z.string().describe("Search query to find in codebase. Multi-word queries (3+ words) are automatically routed to semantic search in auto mode for better conceptual matching. Structural queries (e.g., 'find references to X', 'show dependencies') use AST-based analysis. For exact phrases, use fewer words or specify mode=lexical."),
220
- mode: z.enum(["lexical", "semantic", "structural", "auto"]).optional().default("auto").describe("Search mode: lexical (ripgrep), semantic (semgrep), structural (AST-based), or auto (intelligent routing based on query). Auto mode uses semantic for multi-word conceptual queries, structural for symbol/dependency queries, and falls back to lexical OR search if tools unavailable."),
221
- phase: z.enum(["architect", "engineer", "debugger", "reviewer"]).optional().describe("Caller phase for phase-aware Semgrep rule selection"),
222
- path: z.string().optional().describe("Root path or subdirectory to search (optional)"),
223
- includeHidden: z.boolean().optional().default(false).describe("Include hidden/ignored files (equivalent to rg -uuu)"),
224
- followGitIgnore: z.boolean().optional().default(true).describe("Respect .gitignore rules"),
225
- fileTypes: z.array(z.string()).optional().describe("File types to include (e.g., ['ts', 'tsx', 'py'])"),
226
- maxHits: z.number().optional().default(30).describe("Maximum total results to return (default: 30 for token efficiency)"),
227
- perFileLimit: z.number().optional().default(5).describe("Maximum matches per file (default: 5 to prevent single-file domination)"),
228
- contextLines: z.number().optional().default(2).describe("Context lines before/after matches"),
229
- timeoutMs: z.number().optional().default(4000).describe("Timeout in milliseconds"),
230
- verbose: z.boolean().optional().default(false).describe("Include full lexicalHits/semanticHits arrays for debugging (increases token usage)")
231
- });
232
- export const HybridSearchOutput = z.object({
233
- summary: z.string().describe("Human-readable summary of search results"),
234
- results: z.array(z.object({
235
- file: z.string(),
236
- line: z.number(),
237
- text: z.string(),
238
- score: z.number(),
239
- source: z.enum(['lexical', 'semantic'])
240
- })).describe("Top ranked search results (sorted by relevance score)"),
241
- truncated: z.boolean().describe("True if results were capped at maxHits"),
242
- stats: z.object({
243
- elapsedMs: z.number(),
244
- rgHits: z.number(),
245
- semgrepHits: z.number(),
246
- totalResults: z.number()
247
- }).describe("Search execution statistics"),
248
- errors: z.array(z.string()).optional().describe("Non-fatal errors during search"),
249
- _verbose: z.object({
250
- lexicalHits: z.array(z.object({
251
- file: z.string(),
252
- line: z.number(),
253
- text: z.string(),
254
- before: z.array(z.string()).optional(),
255
- after: z.array(z.string()).optional()
256
- })),
257
- semanticHits: z.array(z.object({
258
- file: z.string(),
259
- line: z.number(),
260
- ruleId: z.string(),
261
- message: z.string(),
262
- severity: z.string().optional(),
263
- confidence: z.number().optional()
264
- }))
265
- }).optional().describe("Full detailed results (only included when verbose=true)")
266
- });
267
- // Helper function to parse ripgrep JSON output into normalized format
268
- async function parseRipgrepJSON(stdout, contextLines = 2) {
269
- const results = [];
270
- const lines = stdout.split('\n').filter(Boolean);
271
- for (const line of lines) {
272
- try {
273
- const parsed = JSON.parse(line);
274
- if (parsed.type === 'match') {
275
- const result = {
276
- file: parsed.data.path.text,
277
- line: parsed.data.line_number,
278
- text: parsed.data.lines.text.trim()
279
- };
280
- // Add context lines if available
281
- if (contextLines > 0 && parsed.data.submatches) {
282
- const before = [];
283
- const after = [];
284
- // ripgrep context would be in separate events, for now just add the match
285
- result.before = before;
286
- result.after = after;
287
- }
288
- results.push(result);
289
- }
290
- }
291
- catch (e) {
292
- // Skip non-JSON lines or malformed JSON
293
- continue;
294
- }
295
- }
296
- return results;
297
- }
298
- // Helper function to normalize Semgrep results into our format
299
- function normalizeSemgrepResults(semgrepOutput) {
300
- if (!semgrepOutput.results)
301
- return [];
302
- return semgrepOutput.results.map((result) => ({
303
- file: result.path,
304
- line: result.start?.line || 0,
305
- ruleId: result.check_id || 'semgrep-rule',
306
- message: result.extra?.message || result.message || 'Semgrep match',
307
- severity: result.extra?.severity || 'info',
308
- confidence: result.extra?.metadata?.confidence || 0.8
309
- }));
310
- }
311
- export async function fileSearchHandler(input) {
312
- const validated = FileSearchInput.parse(input);
313
- try {
314
- const rgPath = await findToolPath('rg');
315
- let rgCmd = `${rgPath} --line-number --max-count ${validated.max_results} '${validated.query}'`;
316
- if (validated.path_filters && validated.path_filters.length > 0) {
317
- const globs = validated.path_filters.map(filter => `--glob '${filter}'`).join(' ');
318
- rgCmd += ` ${globs}`;
319
- }
320
- const { stdout } = await execAsync(rgCmd);
321
- const results = stdout.split("\n").filter(Boolean).map(line => {
322
- const parts = line.split(":", 3);
323
- if (parts.length >= 3) {
324
- const [file, lineNo, snippet] = parts;
325
- return { file, line: parseInt(lineNo, 10) || 0, snippet: snippet.trim(), score: 1.0 };
326
- }
327
- return null;
328
- }).filter(Boolean);
329
- return { results };
330
- }
331
- catch (error) {
332
- // If ripgrep fails (command not found or no matches), return empty results
333
- return { results: [] };
334
- }
335
- }
336
- export async function symbolSearchHandler(input) {
337
- const validated = SymbolSearchInput.parse(input);
338
- try {
339
- // Use ripgrep to find symbol definitions (functions, classes, etc.)
340
- const patterns = [
341
- `function\\s+${validated.symbol}`,
342
- `class\\s+${validated.symbol}`,
343
- `const\\s+${validated.symbol}`,
344
- `let\\s+${validated.symbol}`,
345
- `var\\s+${validated.symbol}`,
346
- `export.*${validated.symbol}`,
347
- `interface\\s+${validated.symbol}`,
348
- `type\\s+${validated.symbol}`
349
- ];
350
- const definitions = [];
351
- for (const pattern of patterns) {
352
- try {
353
- const rgPath = await findToolPath('rg');
354
- const { stdout } = await execAsync(`${rgPath} --line-number '${pattern}'`);
355
- const matches = stdout.split("\n").filter(Boolean).map(line => {
356
- const parts = line.split(":", 2);
357
- if (parts.length >= 2) {
358
- return { file: parts[0], line: parseInt(parts[1], 10) || 0 };
359
- }
360
- return null;
361
- }).filter(Boolean);
362
- definitions.push(...matches);
363
- }
364
- catch (error) {
365
- // Continue with next pattern if this one fails
366
- }
367
- }
368
- return { definitions };
369
- }
370
- catch (error) {
371
- return { definitions: [] };
372
- }
373
- }
374
- export async function referencesSearchHandler(input) {
375
- const validated = ReferencesSearchInput.parse(input);
376
- try {
377
- const rgPath = await findToolPath('rg');
378
- const { stdout } = await execAsync(`${rgPath} --line-number '\\b${validated.symbol}\\b'`);
379
- const references = stdout.split("\n").filter(Boolean).map(line => {
380
- const parts = line.split(":", 2);
381
- if (parts.length >= 2) {
382
- return { file: parts[0], line: parseInt(parts[1], 10) || 0 };
383
- }
384
- return null;
385
- }).filter(Boolean);
386
- return { references };
387
- }
388
- catch (error) {
389
- return { references: [] };
390
- }
391
- }
392
- export async function summarizeHandler(input) {
393
- const validated = SummarizeInput.parse(input);
394
- try {
395
- if (!await fs.pathExists(validated.file)) {
396
- throw new Error(`File ${validated.file} not found`);
397
- }
398
- const content = await fs.readFile(validated.file, 'utf8');
399
- const lines = content.split('\n');
400
- const totalLines = lines.length;
401
- // Basic analysis
402
- const imports = lines.filter(line => line.trim().startsWith('import') || line.trim().startsWith('from')).length;
403
- const functions = lines.filter(line => line.includes('function ') || line.includes('def ') || line.includes('=>')).length;
404
- const classes = lines.filter(line => line.includes('class ')).length;
405
- const comments = lines.filter(line => line.trim().startsWith('//') || line.trim().startsWith('#')).length;
406
- const summary = `File: ${validated.file}
407
- Lines: ${totalLines}
408
- Imports: ${imports}
409
- Functions: ${functions}
410
- Classes: ${classes}
411
- Comments: ${comments}
412
- Estimated complexity: ${functions + classes > 10 ? 'high' : functions + classes > 5 ? 'medium' : 'low'}`;
413
- return { summary };
414
- }
415
- catch (error) {
416
- throw new Error(`Failed to summarize file: ${error instanceof Error ? error.message : String(error)}`);
417
- }
418
- }
419
- export async function analyzeHandler(input) {
420
- const validated = AnalyzeInput.parse(input);
421
- try {
422
- if (!await fs.pathExists(validated.file)) {
423
- throw new Error(`File ${validated.file} not found`);
424
- }
425
- const content = await fs.readFile(validated.file, 'utf8');
426
- const lines = content.split('\n');
427
- // Extract imports
428
- const imports = lines
429
- .filter(line => line.trim().startsWith('import') || line.trim().startsWith('from'))
430
- .map(line => line.trim());
431
- // Extract function names
432
- const functions = [];
433
- const functionRegex = /(?:function\s+(\w+)|(\w+)\s*(?:=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*\{)|:\s*(?:async\s+)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*\{))|def\s+(\w+))/g;
434
- for (const line of lines) {
435
- let match;
436
- while ((match = functionRegex.exec(line)) !== null) {
437
- const funcName = match[1] || match[2] || match[3];
438
- if (funcName && !functions.includes(funcName)) {
439
- functions.push(funcName);
440
- }
441
- }
442
- }
443
- // Extract class names
444
- const classes = [];
445
- const classRegex = /class\s+(\w+)/g;
446
- for (const line of lines) {
447
- let match;
448
- while ((match = classRegex.exec(line)) !== null) {
449
- if (!classes.includes(match[1])) {
450
- classes.push(match[1]);
451
- }
452
- }
453
- }
454
- return { imports, classes, functions };
455
- }
456
- catch (error) {
457
- throw new Error(`Failed to analyze file: ${error instanceof Error ? error.message : String(error)}`);
458
- }
459
- }
460
- // New semgrep-based search handler with debug logging
461
- export async function semgrepSearchHandler(input) {
462
- const validated = SemgrepSearchInput.parse(input);
463
- console.log('[SEMGREP DEBUG] Semgrep search started with input:', JSON.stringify(validated, null, 2));
464
- try {
465
- const semgrepPath = await findToolPath('semgrep');
466
- console.log('[SEMGREP DEBUG] Found semgrep path:', semgrepPath);
467
- // Build semgrep command
468
- let semgrepCmd = `${semgrepPath} --json --no-git-ignore`;
469
- // Add language specification
470
- if (validated.language !== "auto") {
471
- semgrepCmd += ` --lang ${validated.language}`;
472
- }
473
- // Add pattern
474
- semgrepCmd += ` --pattern '${validated.pattern}'`;
475
- // Add path filters
476
- if (validated.path_filters && validated.path_filters.length > 0) {
477
- const includes = validated.path_filters.map(filter => `--include '${filter}'`).join(' ');
478
- semgrepCmd += ` ${includes}`;
479
- }
480
- console.log('[SEMGREP DEBUG] Executing command:', semgrepCmd);
481
- console.log('[SEMGREP DEBUG] Working directory:', process.cwd());
482
- // Execute semgrep
483
- const { stdout, stderr } = await execAsync(semgrepCmd);
484
- console.log('[SEMGREP DEBUG] Command executed, stdout length:', stdout.length);
485
- if (stderr)
486
- console.log('[SEMGREP DEBUG] stderr:', stderr);
487
- const semgrepOutput = JSON.parse(stdout || '{"results": []}');
488
- console.log('[SEMGREP DEBUG] Parsed semgrep output, results count:', semgrepOutput.results?.length || 0);
489
- // Convert semgrep results to our format
490
- const results = (semgrepOutput.results || [])
491
- .slice(0, validated.max_results)
492
- .map((result) => ({
493
- file: result.path,
494
- line: result.start?.line || 0,
495
- snippet: result.extra?.lines || '',
496
- rule_id: result.check_id,
497
- message: result.extra?.message || ''
498
- }));
499
- console.log('[SEMGREP DEBUG] Returning results:', results.length);
500
- return { results };
501
- }
502
- catch (error) {
503
- console.warn('[SEMGREP WARN] Semgrep failed, falling back to ripgrep:', error);
504
- try {
505
- const rgPath = await findToolPath('rg');
506
- console.log('[SEMGREP FALLBACK] Using ripgrep at:', rgPath);
507
- const { stdout } = await execAsync(`${rgPath} --line-number --max-count ${validated.max_results} '${validated.pattern}'`);
508
- const results = stdout.split("\n").filter(Boolean).map(line => {
509
- const parts = line.split(":", 3);
510
- if (parts.length >= 3) {
511
- const [file, lineNo, snippet] = parts;
512
- return {
513
- file,
514
- line: parseInt(lineNo, 10) || 0,
515
- snippet: snippet.trim(),
516
- rule_id: "ripgrep-fallback",
517
- message: "Fallback search using ripgrep"
518
- };
519
- }
520
- return null;
521
- }).filter(Boolean);
522
- console.log('[SEMGREP FALLBACK] Fallback results:', results.length);
523
- return { results };
524
- }
525
- catch (fallbackError) {
526
- console.error('[SEMGREP ERROR] Both semgrep and fallback failed:', fallbackError);
527
- throw new Error(`Semgrep search failed: ${error instanceof Error ? error.message : String(error)}`);
528
- }
529
- }
530
- }
531
- // Enhanced file search with better options and debug logging
532
- export async function enhancedFileSearchHandler(input) {
533
- const validated = EnhancedFileSearchInput.parse(input);
534
- console.log('[SEARCH DEBUG] Enhanced search started with input:', JSON.stringify(validated, null, 2));
535
- try {
536
- const rgPath = await findToolPath('rg');
537
- console.log('[SEARCH DEBUG] Found rg path:', rgPath);
538
- // Use spawn approach (based on successful MCP patterns)
539
- const args = ['--line-number', '--no-heading'];
540
- // Add search type options
541
- if (validated.search_type === "exact") {
542
- args.push('--fixed-strings');
543
- }
544
- else if (validated.search_type === "regex") {
545
- args.push('--regexp');
546
- }
547
- // Add context if requested
548
- if (validated.include_context) {
549
- args.push('--context', '2');
550
- }
551
- // Add file extension filters
552
- if (validated.file_extensions && validated.file_extensions.length > 0) {
553
- validated.file_extensions.forEach(ext => {
554
- const cleanExt = ext.startsWith('.') ? ext : '.' + ext;
555
- args.push('--glob', `*${cleanExt}`);
556
- });
557
- }
558
- // Add exclusion patterns
559
- if (validated.exclude_patterns && validated.exclude_patterns.length > 0) {
560
- validated.exclude_patterns.forEach(pattern => {
561
- args.push('--glob', `!${pattern}`);
562
- });
563
- }
564
- // Add max results and query
565
- args.push('--max-count', validated.max_results.toString());
566
- args.push(validated.query);
567
- console.log('[SEARCH DEBUG] Executing command:', rgPath, 'with args:', args);
568
- console.log('[SEARCH DEBUG] Working directory:', process.cwd());
569
- let stdout, stderr;
570
- try {
571
- const result = await spawnAsync(rgPath, args, {
572
- cwd: process.cwd(),
573
- timeout: 60000 // 60 second timeout (MCP TypeScript client standard)
574
- });
575
- stdout = result.stdout;
576
- stderr = result.stderr;
577
- console.log('[SEARCH DEBUG] Command executed successfully, stdout length:', stdout.length);
578
- if (stderr)
579
- console.log('[SEARCH DEBUG] stderr:', stderr);
580
- }
581
- catch (spawnError) {
582
- console.error('[SEARCH ERROR] Spawn failed:', spawnError);
583
- throw new Error(`Search execution failed: ${spawnError.message}`);
584
- }
585
- const results = stdout.split("\n").filter(Boolean).map(line => {
586
- const parts = line.split(":", 3);
587
- if (parts.length >= 3) {
588
- const [file, lineNo, snippet] = parts;
589
- return {
590
- file,
591
- line: parseInt(lineNo, 10) || 0,
592
- snippet: snippet.trim(),
593
- score: 1.0
594
- };
595
- }
596
- return null;
597
- }).filter(Boolean);
598
- console.log('[SEARCH DEBUG] Found results:', results.length);
599
- return { results };
600
- }
601
- catch (error) {
602
- console.error('[SEARCH ERROR] Enhanced search failed:', error);
603
- throw new Error(`Search failed: ${error instanceof Error ? error.message : String(error)}`);
604
- }
605
- }
606
- // Phase-aware Semgrep rule selection based on caller context
607
- function getPhaseAwareSemgrepRules(mode, phase) {
608
- const phaseRules = {
609
- 'architect': [
610
- '--config', 'p/security-audit',
611
- '--config', 'p/performance'
612
- ],
613
- 'engineer': [
614
- // Use basic pattern-based search for engineer
615
- ],
616
- 'debugger': [
617
- '--config', 'p/security-audit',
618
- '--config', 'p/correctness'
619
- ],
620
- 'reviewer': [
621
- '--config', 'p/security-audit',
622
- '--config', 'p/performance',
623
- '--config', 'p/correctness'
624
- ]
625
- };
626
- return phaseRules[phase || 'engineer'] || [];
627
- }
628
- // Enhanced Hybrid Search Handler - unified entry point for lexical + semantic search
629
- export async function hybridSearchHandler(input) {
630
- const validated = HybridSearchInput.parse(input);
631
- // Check cache first
632
- const cached = searchCache.get(validated);
633
- if (cached) {
634
- return { ...cached, _cached: true };
635
- }
636
- const startTime = Date.now();
637
- const errors = [];
638
- console.log('[HYBRID DEBUG] Hybrid search started with input:', JSON.stringify(validated, null, 2));
639
- // Phase 0: Structural Search Detection (NEW)
640
- const normalized = validated.query.toLowerCase();
641
- const isStructuralQuery = (normalized.match(/find references (to|for|of)\s+\w+/) ||
642
- normalized.match(/show (all )?usages? (of|for)\s+\w+/) ||
643
- normalized.match(/go to definition (of|for)\s+\w+/) ||
644
- normalized.match(/where is \w+ defined/) ||
645
- normalized.match(/show dependencies (for|of)\s+/) ||
646
- normalized.match(/what does .+ import/) ||
647
- normalized.match(/dependencies (of|for)\s+/) ||
648
- normalized.match(/find (circular|architectural|unused) (dependencies|issues|exports)/) ||
649
- normalized.match(/check architecture/) ||
650
- normalized.match(/analyze (architecture|structure)/) ||
651
- normalized.match(/what calls \w+/) ||
652
- normalized.match(/call graph/));
653
- if (validated.mode === 'structural' || (validated.mode === 'auto' && isStructuralQuery)) {
654
- try {
655
- console.log('[HYBRID DEBUG] Routing to structural search');
656
- const orchestrator = new StructuralSearchOrchestrator();
657
- const structuralResults = await orchestrator.search(validated.query, {
658
- scope: validated.path || process.cwd(),
659
- maxResults: validated.maxHits,
660
- phase: validated.phase,
661
- includeDefinition: true
662
- });
663
- // Format structural results to match HybridSearchOutput schema
664
- const results = structuralResults.map(result => ({
665
- file: result.file,
666
- line: result.line,
667
- text: result.context || `${result.symbol} (${result.kind})`,
668
- match: result.symbol,
669
- relevance: result.relevance || 1.0,
670
- source: 'structural'
671
- }));
672
- const output = {
673
- summary: `Found ${structuralResults.length} structural match${structuralResults.length !== 1 ? 'es' : ''} using AST analysis`,
674
- results,
675
- totalHits: structuralResults.length,
676
- rgHits: 0,
677
- semgrepHits: 0,
678
- structuralHits: structuralResults.length,
679
- durationMs: Date.now() - startTime,
680
- interpretation: `Structural search: AST-based symbol analysis for "${validated.query}"`
681
- };
682
- searchCache.set(validated, output);
683
- return output;
684
- }
685
- catch (error) {
686
- console.error('[HYBRID DEBUG] Structural search failed:', error);
687
- errors.push(`⚠️ Structural search failed: ${error instanceof Error ? error.message : String(error)}`);
688
- // Fall through to lexical/semantic search
689
- }
690
- }
691
- // Smart query analysis for auto mode
692
- const queryWords = validated.query.trim().split(/\s+/).filter(w => w.length > 0);
693
- const isMultiWordQuery = queryWords.length >= 3;
694
- const isSemanticPhase = ['architect', 'debugger', 'reviewer'].includes(validated.phase || '');
695
- // Auto mode routing: prefer semantic for multi-word conceptual queries
696
- let effectiveMode = validated.mode;
697
- if (validated.mode === 'auto' && isMultiWordQuery && isSemanticPhase) {
698
- effectiveMode = 'semantic';
699
- console.log('[HYBRID DEBUG] Auto mode: routing multi-word query to semantic search');
700
- }
701
- let lexicalHits = [];
702
- let semanticHits = [];
703
- let rgHits = 0;
704
- let semgrepHits = 0;
705
- let semgrepFailed = false;
706
- // Phase 1: Run ripgrep for lexical search (ALWAYS run in auto mode, unless explicit semantic-only)
707
- if (validated.mode !== "semantic") {
708
- try {
709
- console.log('[HYBRID DEBUG] Running lexical search with ripgrep');
710
- const rgPath = await findToolPath('rg');
711
- // Build ripgrep command with JSON output
712
- const args = ['--json', '--line-number'];
713
- // Handle gitignore and hidden files
714
- if (!validated.followGitIgnore) {
715
- args.push('--no-ignore');
716
- }
717
- if (validated.includeHidden) {
718
- args.push('--hidden', '--no-ignore');
719
- }
720
- // Add context lines
721
- if (validated.contextLines && validated.contextLines > 0) {
722
- args.push('--context', validated.contextLines.toString());
723
- }
724
- // Add file type filters
725
- if (validated.fileTypes && validated.fileTypes.length > 0) {
726
- validated.fileTypes.forEach(type => {
727
- args.push('--type', type);
728
- });
729
- }
730
- // Add per-file limit to prevent single-file domination
731
- args.push('--max-count', validated.perFileLimit.toString());
732
- // Smart Multi-Word Cascade for lexical search
733
- let interpretedPattern = validated.query;
734
- let interpretationNote = "";
735
- let triedMethodCall = false;
736
- if (queryWords.length >= 2 && effectiveMode === "lexical") {
737
- // Priority 1: Method/Property Call (2 words, both identifiers)
738
- if (queryWords.length === 2 &&
739
- queryWords[0].match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) &&
740
- queryWords[1].match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) {
741
- interpretedPattern = `${queryWords[0]}\\.(${queryWords[1]})`;
742
- interpretationNote = `Auto-interpreted: Method/property call: ${queryWords[0]}.${queryWords[1]}()`;
743
- triedMethodCall = true;
744
- console.log('[HYBRID DEBUG] Smart cascade: Priority 1 (method call):', interpretedPattern);
745
- }
746
- // Priority 2: Ordered Proximity (3+ words or not a method call pattern)
747
- else {
748
- interpretedPattern = queryWords.join('.*');
749
- interpretationNote = `Auto-interpreted: Ordered proximity: ${queryWords.join(' → ')}`;
750
- console.log('[HYBRID DEBUG] Smart cascade: Priority 2 (ordered proximity):', interpretedPattern);
751
- }
752
- if (interpretationNote) {
753
- errors.push(`ℹ️ ${interpretationNote}`);
754
- }
755
- }
756
- // Add query and path
757
- args.push(interpretedPattern);
758
- if (validated.path) {
759
- args.push(validated.path);
760
- }
761
- console.log('[HYBRID DEBUG] Ripgrep command:', rgPath, args.join(' '));
762
- const { stdout, stderr, exitCode } = await spawnAsync(rgPath, args, {
763
- cwd: process.cwd(),
764
- timeout: validated.timeoutMs,
765
- toolName: 'rg'
766
- });
767
- if (stderr) {
768
- console.warn('[HYBRID WARN] Ripgrep stderr:', stderr);
769
- errors.push(`ripgrep warning: ${stderr}`);
770
- }
771
- // exitCode 0 = success with results, exitCode 1 = no matches (both are successful outcomes)
772
- if (exitCode === 0 || exitCode === 1) {
773
- lexicalHits = await parseRipgrepJSON(stdout, validated.contextLines);
774
- rgHits = lexicalHits.length;
775
- console.log('[HYBRID DEBUG] Ripgrep found', rgHits, 'results');
776
- // Priority 1→2 Fallback: If method call returned 0, try ordered proximity
777
- if (rgHits === 0 && triedMethodCall && queryWords.length >= 2 && effectiveMode === "lexical") {
778
- console.log('[HYBRID DEBUG] Priority 1 (method call) returned 0 results, falling back to Priority 2 (ordered proximity)');
779
- const proximityPattern = queryWords.join('.*');
780
- const proximityArgs = args.slice(0, -1 - (validated.path ? 1 : 0));
781
- proximityArgs.push(proximityPattern);
782
- if (validated.path) {
783
- proximityArgs.push(validated.path);
784
- }
785
- console.log('[HYBRID DEBUG] Retrying with ordered proximity:', proximityPattern);
786
- const { stdout: proxStdout, exitCode: proxExitCode } = await spawnAsync(rgPath, proximityArgs, {
787
- cwd: process.cwd(),
788
- timeout: validated.timeoutMs,
789
- toolName: 'rg'
790
- });
791
- if (proxExitCode === 0 || proxExitCode === 1) {
792
- lexicalHits = await parseRipgrepJSON(proxStdout, validated.contextLines);
793
- rgHits = lexicalHits.length;
794
- if (rgHits > 0) {
795
- const lastNoteIndex = errors.findIndex(e => e.startsWith('ℹ️ Auto-interpreted:'));
796
- if (lastNoteIndex !== -1) {
797
- errors[lastNoteIndex] = `ℹ️ Auto-interpreted: Method call (0 results) → Ordered proximity: ${queryWords.join(' → ')} (${rgHits} results)`;
798
- }
799
- console.log('[HYBRID DEBUG] Ordered proximity found', rgHits, 'results');
800
- }
801
- }
802
- }
803
- // Priority 2→3 Fallback: If ordered proximity returned 0, try OR
804
- if (rgHits === 0 && queryWords.length >= 2 &&
805
- (interpretedPattern.includes('.*') || triedMethodCall) && effectiveMode === "lexical") {
806
- console.log('[HYBRID DEBUG] Priority 2 returned 0 results, trying Priority 3 (OR fallback)');
807
- // Rebuild args with OR pattern
808
- const orPattern = `\\b(${queryWords.join('|')})\\b`;
809
- const orArgs = args.slice(0, -1 - (validated.path ? 1 : 0)); // Remove pattern and path if exists
810
- orArgs.push(orPattern);
811
- if (validated.path) {
812
- orArgs.push(validated.path);
813
- }
814
- console.log('[HYBRID DEBUG] Retrying with OR pattern:', orPattern);
815
- const { stdout: orStdout, exitCode: orExitCode } = await spawnAsync(rgPath, orArgs, {
816
- cwd: process.cwd(),
817
- timeout: validated.timeoutMs,
818
- toolName: 'rg'
819
- });
820
- if (orExitCode === 0 || orExitCode === 1) {
821
- lexicalHits = await parseRipgrepJSON(orStdout, validated.contextLines);
822
- rgHits = lexicalHits.length;
823
- if (rgHits > 0) {
824
- // Update interpretation note
825
- const lastNoteIndex = errors.findIndex(e => e.startsWith('ℹ️ Auto-interpreted:'));
826
- if (lastNoteIndex !== -1) {
827
- errors[lastNoteIndex] = `ℹ️ Auto-interpreted: Ordered proximity (0 results) → Broadened to OR search: ${queryWords.join(' | ')}`;
828
- }
829
- console.log('[HYBRID DEBUG] OR fallback found', rgHits, 'results');
830
- }
831
- }
832
- }
833
- }
834
- }
835
- catch (error) {
836
- console.warn('[HYBRID WARN] Ripgrep failed:', error);
837
- const errorMsg = error instanceof Error ? error.message : String(error);
838
- const stderr = error?.stderr;
839
- errors.push(`ripgrep failed: ${errorMsg}${stderr ? ` - ${stderr.trim()}` : ''}`);
840
- }
841
- }
842
- // Phase 2: Run Semgrep for semantic search (ALWAYS run in auto mode, unless explicit lexical-only)
843
- if (validated.mode !== "lexical") {
844
- try {
845
- console.log('[HYBRID DEBUG] Running semantic search with multi-pattern semgrep');
846
- // NEW: Use multi-pattern generator instead of broken single pattern
847
- const patternGenerator = new SemgrepPatternGenerator();
848
- const projectPath = validated.path || process.cwd();
849
- const language = patternGenerator.detectLanguageFromProject(projectPath);
850
- console.log('[HYBRID DEBUG] Detected language:', language);
851
- // Generate 7 core patterns for the query
852
- const patterns = patternGenerator.generateCorePatterns(validated.query, language);
853
- console.log('[HYBRID DEBUG] Generated', patterns.length, 'patterns');
854
- // Run all patterns in parallel
855
- const semgrepResults = await patternGenerator.runPatterns(patterns, projectPath, validated.timeoutMs);
856
- // Normalize results
857
- semanticHits = semgrepResults.map(result => ({
858
- file: result.file,
859
- line: result.line,
860
- ruleId: result.ruleId,
861
- message: result.message,
862
- severity: 'info',
863
- confidence: 0.8
864
- }));
865
- semgrepHits = semanticHits.length;
866
- console.log('[HYBRID DEBUG] Multi-pattern semgrep found', semgrepHits, 'total results');
867
- }
868
- catch (error) {
869
- console.warn('[HYBRID WARN] Semgrep failed:', error);
870
- errors.push(`semgrep failed: ${error instanceof Error ? error.message : String(error)}`);
871
- semgrepFailed = true;
872
- }
873
- }
874
- // Phase 2.5: Fallback - if semantic search failed and we have a multi-word query, try lexical OR search
875
- console.log('[HYBRID DEBUG] Fallback check:', { semgrepFailed, isMultiWordQuery, effectiveMode, lexicalHitsLength: lexicalHits.length });
876
- if (semgrepFailed && isMultiWordQuery && effectiveMode === 'semantic' && lexicalHits.length === 0) {
877
- try {
878
- console.log('[HYBRID DEBUG] Semgrep unavailable, falling back to lexical OR search for multi-word query');
879
- const rgPath = await findToolPath('rg');
880
- // Build OR search with multiple -e flags
881
- const args = ['--json', '--line-number'];
882
- if (!validated.followGitIgnore) {
883
- args.push('--no-ignore');
884
- }
885
- if (validated.includeHidden) {
886
- args.push('--hidden', '--no-ignore');
887
- }
888
- if (validated.contextLines && validated.contextLines > 0) {
889
- args.push('--context', validated.contextLines.toString());
890
- }
891
- if (validated.fileTypes && validated.fileTypes.length > 0) {
892
- validated.fileTypes.forEach(type => {
893
- args.push('--type', type);
894
- });
895
- }
896
- args.push('--max-count', validated.maxHits.toString());
897
- // Add each word as a separate pattern for OR search
898
- queryWords.forEach(word => {
899
- args.push('-e', word);
900
- });
901
- if (validated.path) {
902
- args.push(validated.path);
903
- }
904
- console.log('[HYBRID DEBUG] Fallback ripgrep OR command:', rgPath, args.join(' '));
905
- const { stdout, stderr, exitCode } = await spawnAsync(rgPath, args, {
906
- cwd: process.cwd(),
907
- timeout: validated.timeoutMs,
908
- toolName: 'rg'
909
- });
910
- if (stderr) {
911
- console.warn('[HYBRID WARN] Fallback ripgrep stderr:', stderr);
912
- }
913
- if (exitCode === 0 || exitCode === 1) {
914
- lexicalHits = await parseRipgrepJSON(stdout, validated.contextLines);
915
- rgHits = lexicalHits.length;
916
- console.log('[HYBRID DEBUG] Fallback OR search found', rgHits, 'results');
917
- }
918
- }
919
- catch (error) {
920
- console.warn('[HYBRID WARN] Fallback lexical OR search failed:', error);
921
- const errorMsg = error instanceof Error ? error.message : String(error);
922
- const stderr = error?.stderr;
923
- errors.push(`fallback search failed: ${errorMsg}${stderr ? ` - ${stderr.trim()}` : ''}`);
924
- }
925
- }
926
- // Phase 3: Score, rank, and limit results
927
- const scoringStart = Date.now();
928
- const scoredResults = await scoreAndRankResults(lexicalHits, semanticHits);
929
- const scoringMs = Date.now() - scoringStart;
930
- const totalHits = scoredResults.length;
931
- const truncated = totalHits > validated.maxHits;
932
- // Slice to maxHits (top ranked results)
933
- const topResults = scoredResults.slice(0, validated.maxHits);
934
- const elapsedMs = Date.now() - startTime;
935
- // Generate structured summary for terminal display
936
- const modeBanner = generateModeBanner(validated.mode, validated.phase, validated.query, topResults.length, elapsedMs);
937
- // Build result object based on verbose flag
938
- const result = {
939
- summary: modeBanner,
940
- results: topResults,
941
- truncated,
942
- stats: {
943
- elapsedMs,
944
- rgHits,
945
- semgrepHits,
946
- totalResults: topResults.length
947
- }
948
- };
949
- // Add errors if any
950
- if (errors.length > 0) {
951
- result.errors = errors;
952
- }
953
- // Include full hits arrays only in verbose mode
954
- if (validated.verbose) {
955
- result._verbose = {
956
- lexicalHits,
957
- semanticHits
958
- };
959
- }
960
- console.log('[HYBRID DEBUG] Hybrid search completed in', elapsedMs, 'ms (scoring:', scoringMs, 'ms)');
961
- console.log('[HYBRID DEBUG] Final result:', JSON.stringify({
962
- totalHits,
963
- returnedResults: topResults.length,
964
- truncated,
965
- topScores: topResults.slice(0, 5).map(r => r.score),
966
- errors: errors.length
967
- }, null, 2));
968
- // Cache the result before returning
969
- searchCache.set(validated, result);
970
- return result;
971
- }
972
- // Helper function to generate mode banner for structured output
973
- function generateModeBanner(mode, phase, query, totalResults, elapsedMs) {
974
- const phaseLabel = phase ? ` | Phase: ${phase}` : '';
975
- const modeLabel = mode === 'auto' ? 'hybrid' : mode;
976
- const resultsLabel = totalResults === 1 ? 'result' : 'results';
977
- const timeLabel = elapsedMs < 1000 ? `${elapsedMs}ms` : `${(elapsedMs / 1000).toFixed(1)}s`;
978
- return `🔍 [${modeLabel.charAt(0).toUpperCase() + modeLabel.slice(1)} Search]${phaseLabel} | Query: '${query}' | ${totalResults} ${resultsLabel} (${timeLabel})`;
979
- }
980
- // Helper function to generate summary hits for terminal display
981
- function generateSummaryHits(lexicalHits, semanticHits, maxDisplay = 10) {
982
- const summaryHits = [];
983
- let displayCount = 0;
984
- // Combine and prioritize hits (lexical first, then semantic)
985
- const allHits = [
986
- ...lexicalHits.map(hit => ({
987
- type: 'lexical',
988
- file: hit.file,
989
- line: hit.line,
990
- preview: hit.text?.substring(0, 100) || hit.snippet?.substring(0, 100) || ''
991
- })),
992
- ...semanticHits.map(hit => ({
993
- type: 'semantic',
994
- file: hit.file,
995
- line: hit.line,
996
- preview: hit.message?.substring(0, 100) || hit.ruleId || ''
997
- }))
998
- ];
999
- // Generate concise file:line → preview format
1000
- for (const hit of allHits) {
1001
- if (displayCount >= maxDisplay)
1002
- break;
1003
- const preview = hit.preview.replace(/\s+/g, ' ').trim();
1004
- const truncatedPreview = preview.length > 80 ? preview.substring(0, 80) + '...' : preview;
1005
- const typeIcon = hit.type === 'semantic' ? '🎯' : '📄';
1006
- summaryHits.push(`${typeIcon} ${hit.file}:${hit.line} → ${truncatedPreview}`);
1007
- displayCount++;
1008
- }
1009
- if (allHits.length > maxDisplay) {
1010
- summaryHits.push(`... and ${allHits.length - maxDisplay} more results`);
1011
- }
1012
- return summaryHits;
1013
- }
1014
- // Cache for git recency checks (session-level)
1015
- const gitRecencyCache = new Map();
1016
- async function calculateRelevanceScore(file, source, matchCount = 1) {
1017
- let score = 0;
1018
- // 1. File priority based on path (0-100)
1019
- if (file.includes('/src/'))
1020
- score += 100;
1021
- else if (file.includes('/lib/'))
1022
- score += 80;
1023
- else if (file.includes('/test/'))
1024
- score += 60;
1025
- else if (file.includes('/docs/'))
1026
- score += 40;
1027
- else if (file.includes('node_modules'))
1028
- score = 0; // Skip node_modules
1029
- else
1030
- score += 50; // Default for other files
1031
- // 2. Semantic boost (+30 for semgrep matches)
1032
- if (source === 'semantic') {
1033
- score += 30;
1034
- }
1035
- // 3. Git recency boost (cached)
1036
- if (!gitRecencyCache.has(file)) {
1037
- try {
1038
- const { stdout } = await execAsync(`git log --format=%ct --max-count=1 "${file}"`, {
1039
- cwd: process.cwd(),
1040
- timeout: 2000
1041
- });
1042
- const timestamp = parseInt(stdout.trim(), 10);
1043
- if (!isNaN(timestamp)) {
1044
- const ageMs = Date.now() - (timestamp * 1000);
1045
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
1046
- let recencyBoost = 0;
1047
- if (ageDays <= 7)
1048
- recencyBoost = 20;
1049
- else if (ageDays <= 30)
1050
- recencyBoost = 10;
1051
- else if (ageDays <= 365)
1052
- recencyBoost = 5;
1053
- gitRecencyCache.set(file, recencyBoost);
1054
- score += recencyBoost;
1055
- }
1056
- }
1057
- catch (error) {
1058
- // File not in git or error - no recency boost
1059
- gitRecencyCache.set(file, 0);
1060
- }
1061
- }
1062
- else {
1063
- score += gitRecencyCache.get(file) || 0;
1064
- }
1065
- // 4. Match density penalty (fewer files with matches = higher relevance)
1066
- // If a file has many matches, slightly reduce its score to avoid one file dominating
1067
- if (matchCount > 10) {
1068
- score -= Math.min(20, matchCount - 10);
1069
- }
1070
- return Math.max(0, Math.min(150, score)); // Clamp to 0-150
1071
- }
1072
- // Score and rank search results
1073
- async function scoreAndRankResults(lexicalHits, semanticHits) {
1074
- const scoredResults = [];
1075
- // Count matches per file for density calculation
1076
- const fileMatchCounts = new Map();
1077
- [...lexicalHits, ...semanticHits].forEach(hit => {
1078
- fileMatchCounts.set(hit.file, (fileMatchCounts.get(hit.file) || 0) + 1);
1079
- });
1080
- // Score lexical hits
1081
- for (const hit of lexicalHits) {
1082
- const matchCount = fileMatchCounts.get(hit.file) || 1;
1083
- const score = await calculateRelevanceScore(hit.file, 'lexical', matchCount);
1084
- scoredResults.push({
1085
- file: hit.file,
1086
- line: hit.line,
1087
- text: hit.text || '',
1088
- score,
1089
- source: 'lexical'
1090
- });
1091
- }
1092
- // Score semantic hits
1093
- for (const hit of semanticHits) {
1094
- const matchCount = fileMatchCounts.get(hit.file) || 1;
1095
- const score = await calculateRelevanceScore(hit.file, 'semantic', matchCount);
1096
- scoredResults.push({
1097
- file: hit.file,
1098
- line: hit.line,
1099
- text: hit.message || hit.ruleId || '',
1100
- score,
1101
- source: 'semantic'
1102
- });
1103
- }
1104
- // Sort by score descending (highest relevance first)
1105
- return scoredResults.sort((a, b) => b.score - a.score);
1106
- }
1107
- //# sourceMappingURL=searchTools.js.map