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.
- package/LICENSE +2 -2
- package/README.md +199 -210
- package/bin/bobs-workshop.js +109 -0
- package/config/agents.json +27 -0
- package/dist/plugins/bobs-workshop.js +34 -0
- package/dist/tools/background-agent/cancel.d.ts +3 -0
- package/dist/tools/background-agent/cancel.d.ts.map +1 -0
- package/dist/tools/background-agent/cancel.js +52 -0
- package/dist/tools/background-agent/concurrency.d.ts +15 -0
- package/dist/tools/background-agent/concurrency.d.ts.map +1 -0
- package/dist/tools/background-agent/concurrency.js +61 -0
- package/dist/tools/background-agent/index.d.ts +8 -0
- package/dist/tools/background-agent/index.d.ts.map +1 -0
- package/dist/tools/background-agent/index.js +7 -0
- package/dist/tools/background-agent/launch.d.ts +6 -0
- package/dist/tools/background-agent/launch.d.ts.map +1 -0
- package/dist/tools/background-agent/launch.js +33 -0
- package/dist/tools/background-agent/list.d.ts +7 -0
- package/dist/tools/background-agent/list.d.ts.map +1 -0
- package/dist/tools/background-agent/list.js +40 -0
- package/dist/tools/background-agent/manager.d.ts +29 -0
- package/dist/tools/background-agent/manager.d.ts.map +1 -0
- package/dist/tools/background-agent/manager.js +377 -0
- package/dist/tools/background-agent/output.d.ts +3 -0
- package/dist/tools/background-agent/output.d.ts.map +1 -0
- package/dist/tools/background-agent/output.js +41 -0
- package/dist/tools/background-agent/types.d.ts +46 -0
- package/dist/tools/background-agent/types.d.ts.map +1 -0
- package/dist/tools/background-agent/types.js +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/manual/index.d.ts +3 -0
- package/dist/tools/manual/index.d.ts.map +1 -0
- package/dist/tools/manual/index.js +2 -0
- package/dist/tools/manual/manual-update.d.ts +4 -0
- package/dist/tools/manual/manual-update.d.ts.map +1 -0
- package/dist/tools/manual/manual-update.js +190 -0
- package/dist/tools/manual/verify-manual.d.ts +4 -0
- package/dist/tools/manual/verify-manual.d.ts.map +1 -0
- package/dist/tools/manual/verify-manual.js +46 -0
- package/package.json +34 -66
- package/postinstall.js +190 -0
- package/src/agents/alice.md +466 -0
- package/src/agents/bob-rev.md +493 -0
- package/src/agents/bob-send.md +277 -0
- package/src/agents/bob.md +442 -0
- package/src/agents/trace.md +451 -0
- package/src/plugins/bobs-workshop.ts +45 -0
- package/src/skills/api-patterns/SKILL.md +376 -0
- package/src/skills/architecture/SKILL.md +271 -0
- package/src/skills/bobs-workshop/performance/icon.svg +3 -0
- package/src/skills/brainstorming/SKILL.md +210 -0
- package/src/skills/clean-code/SKILL.md +151 -0
- package/src/skills/code-review-checklist/SKILL.md +220 -0
- package/src/skills/database-design/SKILL.md +271 -0
- package/src/skills/exploration/SKILL.md +257 -0
- package/src/skills/frontend-ui-ux/SKILL.md +78 -0
- package/src/skills/git-master/SKILL.md +1105 -0
- package/src/skills/performance/SKILL.md +144 -0
- package/src/skills/performance/icon.svg +3 -0
- package/src/skills/plan-writing/SKILL.md +225 -0
- package/src/skills/security/SKILL.md +410 -0
- package/src/skills/simplification/SKILL.md +238 -0
- package/src/skills/systematic-debugging/SKILL.md +175 -0
- package/src/skills/testing-patterns/SKILL.md +305 -0
- package/src/skills/verification/SKILL.md +286 -0
- package/src/tools/background-agent/cancel.ts +67 -0
- package/src/tools/background-agent/concurrency.ts +71 -0
- package/src/tools/background-agent/index.ts +7 -0
- package/src/tools/background-agent/launch.ts +39 -0
- package/src/tools/background-agent/list.ts +50 -0
- package/src/tools/background-agent/manager.ts +455 -0
- package/src/tools/background-agent/output.ts +57 -0
- package/src/tools/background-agent/types.ts +55 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/manual/index.ts +2 -0
- package/src/tools/manual/manual-update.ts +197 -0
- package/src/tools/manual/verify-manual.ts +55 -0
- package/uninstall.js +64 -0
- package/Claude.md +0 -162
- package/bin/bobs-mcp-server.js +0 -11
- package/bin/bobs-mcp.js +0 -130
- package/dist/api/taskLogger.js +0 -106
- package/dist/api/taskLogger.js.map +0 -1
- package/dist/cli/checker.js +0 -401
- package/dist/cli/checker.js.map +0 -1
- package/dist/cli/cleanup.js +0 -131
- package/dist/cli/cleanup.js.map +0 -1
- package/dist/cli/debug.js +0 -157
- package/dist/cli/debug.js.map +0 -1
- package/dist/cli/health.js +0 -97
- package/dist/cli/health.js.map +0 -1
- package/dist/cli/setup.js +0 -81
- package/dist/cli/setup.js.map +0 -1
- package/dist/cli/workshop.js +0 -42
- package/dist/cli/workshop.js.map +0 -1
- package/dist/dashboard/server.js +0 -1203
- package/dist/dashboard/server.js.map +0 -1
- package/dist/index.js +0 -960
- package/dist/index.js.map +0 -1
- package/dist/prompts/architect.js +0 -221
- package/dist/prompts/architect.js.map +0 -1
- package/dist/prompts/debugger.js +0 -257
- package/dist/prompts/debugger.js.map +0 -1
- package/dist/prompts/engineer.js +0 -249
- package/dist/prompts/engineer.js.map +0 -1
- package/dist/prompts/orchestrator.js +0 -304
- package/dist/prompts/orchestrator.js.map +0 -1
- package/dist/prompts/reviewer.js +0 -289
- package/dist/prompts/reviewer.js.map +0 -1
- package/dist/services/activitySummarizer.js +0 -388
- package/dist/services/activitySummarizer.js.map +0 -1
- package/dist/services/changeValidator.js +0 -396
- package/dist/services/changeValidator.js.map +0 -1
- package/dist/services/claudeOrchestrator.js +0 -343
- package/dist/services/claudeOrchestrator.js.map +0 -1
- package/dist/services/fileMonitor.js +0 -250
- package/dist/services/fileMonitor.js.map +0 -1
- package/dist/services/implementationSummarizer.js +0 -306
- package/dist/services/implementationSummarizer.js.map +0 -1
- package/dist/services/liveMonitor.js +0 -315
- package/dist/services/liveMonitor.js.map +0 -1
- package/dist/services/mcpAuditLogger.js +0 -104
- package/dist/services/mcpAuditLogger.js.map +0 -1
- package/dist/services/mcpLogger.js +0 -223
- package/dist/services/mcpLogger.js.map +0 -1
- package/dist/services/tmuxManager.js +0 -541
- package/dist/services/tmuxManager.js.map +0 -1
- package/dist/tools/approvalTools.js +0 -244
- package/dist/tools/approvalTools.js.map +0 -1
- package/dist/tools/autoDebugger.js +0 -147
- package/dist/tools/autoDebugger.js.map +0 -1
- package/dist/tools/cleanupService.js +0 -221
- package/dist/tools/cleanupService.js.map +0 -1
- package/dist/tools/dashboardTools.js +0 -342
- package/dist/tools/dashboardTools.js.map +0 -1
- package/dist/tools/developmentNudges.js +0 -336
- package/dist/tools/developmentNudges.js.map +0 -1
- package/dist/tools/gitTools.js +0 -741
- package/dist/tools/gitTools.js.map +0 -1
- package/dist/tools/orchestratorTools.js +0 -832
- package/dist/tools/orchestratorTools.js.map +0 -1
- package/dist/tools/searchCache.js +0 -64
- package/dist/tools/searchCache.js.map +0 -1
- package/dist/tools/searchTools.js +0 -1107
- package/dist/tools/searchTools.js.map +0 -1
- package/dist/tools/semgrep-patterns.js +0 -296
- package/dist/tools/semgrep-patterns.js.map +0 -1
- package/dist/tools/specTools.js +0 -332
- package/dist/tools/specTools.js.map +0 -1
- package/dist/tools/structural/__tests__/orchestrator.test.js +0 -61
- package/dist/tools/structural/__tests__/orchestrator.test.js.map +0 -1
- package/dist/tools/structural/cache.js +0 -226
- package/dist/tools/structural/cache.js.map +0 -1
- package/dist/tools/structural/engines/python/index.js +0 -118
- package/dist/tools/structural/engines/python/index.js.map +0 -1
- package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js +0 -97
- package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js.map +0 -1
- package/dist/tools/structural/engines/typescript/analyzer.js +0 -433
- package/dist/tools/structural/engines/typescript/analyzer.js.map +0 -1
- package/dist/tools/structural/engines/typescript/index.js +0 -381
- package/dist/tools/structural/engines/typescript/index.js.map +0 -1
- package/dist/tools/structural/engines/typescript/utils.js +0 -279
- package/dist/tools/structural/engines/typescript/utils.js.map +0 -1
- package/dist/tools/structural/index.js +0 -248
- package/dist/tools/structural/index.js.map +0 -1
- package/dist/tools/structural/types.js +0 -18
- package/dist/tools/structural/types.js.map +0 -1
- package/dist/tools/tmuxTools.js +0 -100
- package/dist/tools/tmuxTools.js.map +0 -1
- package/dist/tools/workRecorder.js +0 -215
- package/dist/tools/workRecorder.js.map +0 -1
- package/dist/tools/worktreeTools.js +0 -705
- package/dist/tools/worktreeTools.js.map +0 -1
- package/dist/utils/__tests__/integration.test.js +0 -57
- package/dist/utils/__tests__/integration.test.js.map +0 -1
- package/dist/utils/__tests__/serverDetection.test.js +0 -151
- package/dist/utils/__tests__/serverDetection.test.js.map +0 -1
- package/dist/utils/errorHandling.js +0 -336
- package/dist/utils/errorHandling.js.map +0 -1
- package/dist/utils/processManager.js +0 -172
- package/dist/utils/processManager.js.map +0 -1
- package/dist/utils/reliability.js +0 -263
- package/dist/utils/reliability.js.map +0 -1
- package/dist/utils/responseFormatter.js +0 -250
- package/dist/utils/responseFormatter.js.map +0 -1
- package/dist/utils/serverDetection.js +0 -133
- package/dist/utils/serverDetection.js.map +0 -1
- package/dist/utils/specMigration.js +0 -105
- package/dist/utils/specMigration.js.map +0 -1
- package/dist/validation/schemas.js +0 -299
- package/dist/validation/schemas.js.map +0 -1
- package/public/.well-known/mcp/manifest.json +0 -473
- package/public/index.html +0 -3157
- package/public/index.html.backup +0 -2805
- package/public/index.html.backup2 +0 -1292
- package/scripts/cleanup-system-logs.ts +0 -121
- package/scripts/init-workspace.js +0 -63
- 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
|