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.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/bin/bobs-mcp.js +130 -0
- package/dist/api/taskLogger.js +106 -0
- package/dist/api/taskLogger.js.map +1 -0
- package/dist/cli/checker.js +401 -0
- package/dist/cli/checker.js.map +1 -0
- package/dist/cli/cleanup.js +131 -0
- package/dist/cli/cleanup.js.map +1 -0
- package/dist/cli/debug.js +157 -0
- package/dist/cli/debug.js.map +1 -0
- package/dist/cli/health.js +97 -0
- package/dist/cli/health.js.map +1 -0
- package/dist/cli/setup.js +81 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/workshop.js +42 -0
- package/dist/cli/workshop.js.map +1 -0
- package/dist/dashboard/server.js +1206 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.js +757 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/architect.js +157 -0
- package/dist/prompts/architect.js.map +1 -0
- package/dist/prompts/debugger.js +201 -0
- package/dist/prompts/debugger.js.map +1 -0
- package/dist/prompts/engineer.js +171 -0
- package/dist/prompts/engineer.js.map +1 -0
- package/dist/prompts/orchestrator.js +225 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/prompts/reviewer.js +199 -0
- package/dist/prompts/reviewer.js.map +1 -0
- package/dist/services/activitySummarizer.js +353 -0
- package/dist/services/activitySummarizer.js.map +1 -0
- package/dist/services/changeValidator.js +396 -0
- package/dist/services/changeValidator.js.map +1 -0
- package/dist/services/claudeOrchestrator.js +343 -0
- package/dist/services/claudeOrchestrator.js.map +1 -0
- package/dist/services/fileMonitor.js +250 -0
- package/dist/services/fileMonitor.js.map +1 -0
- package/dist/services/implementationSummarizer.js +306 -0
- package/dist/services/implementationSummarizer.js.map +1 -0
- package/dist/services/liveMonitor.js +315 -0
- package/dist/services/liveMonitor.js.map +1 -0
- package/dist/services/mcpAuditLogger.js +104 -0
- package/dist/services/mcpAuditLogger.js.map +1 -0
- package/dist/services/mcpLogger.js +223 -0
- package/dist/services/mcpLogger.js.map +1 -0
- package/dist/services/tmuxManager.js +541 -0
- package/dist/services/tmuxManager.js.map +1 -0
- package/dist/tools/approvalTools.js +244 -0
- package/dist/tools/approvalTools.js.map +1 -0
- package/dist/tools/autoDebugger.js +147 -0
- package/dist/tools/autoDebugger.js.map +1 -0
- package/dist/tools/cleanupService.js +221 -0
- package/dist/tools/cleanupService.js.map +1 -0
- package/dist/tools/dashboardTools.js +359 -0
- package/dist/tools/dashboardTools.js.map +1 -0
- package/dist/tools/developmentNudges.js +336 -0
- package/dist/tools/developmentNudges.js.map +1 -0
- package/dist/tools/gitTools.js +741 -0
- package/dist/tools/gitTools.js.map +1 -0
- package/dist/tools/orchestratorTools.js +765 -0
- package/dist/tools/orchestratorTools.js.map +1 -0
- package/dist/tools/searchTools.js +788 -0
- package/dist/tools/searchTools.js.map +1 -0
- package/dist/tools/specTools.js +350 -0
- package/dist/tools/specTools.js.map +1 -0
- package/dist/tools/tmuxTools.js +100 -0
- package/dist/tools/tmuxTools.js.map +1 -0
- package/dist/tools/workRecorder.js +215 -0
- package/dist/tools/workRecorder.js.map +1 -0
- package/dist/tools/worktreeTools.js +705 -0
- package/dist/tools/worktreeTools.js.map +1 -0
- package/dist/utils/__tests__/integration.test.js +57 -0
- package/dist/utils/__tests__/integration.test.js.map +1 -0
- package/dist/utils/__tests__/serverDetection.test.js +151 -0
- package/dist/utils/__tests__/serverDetection.test.js.map +1 -0
- package/dist/utils/errorHandling.js +336 -0
- package/dist/utils/errorHandling.js.map +1 -0
- package/dist/utils/processManager.js +172 -0
- package/dist/utils/processManager.js.map +1 -0
- package/dist/utils/reliability.js +263 -0
- package/dist/utils/reliability.js.map +1 -0
- package/dist/utils/responseFormatter.js +250 -0
- package/dist/utils/responseFormatter.js.map +1 -0
- package/dist/utils/serverDetection.js +133 -0
- package/dist/utils/serverDetection.js.map +1 -0
- package/dist/utils/specMigration.js +105 -0
- package/dist/utils/specMigration.js.map +1 -0
- package/dist/validation/schemas.js +299 -0
- package/dist/validation/schemas.js.map +1 -0
- package/package.json +79 -0
- package/scripts/init-workspace.js +63 -0
- 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
|