@esankhan3/code-search-mcp 0.0.2 → 0.2.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/README.md +343 -248
- package/dist/__tests__/config-resolver.test.js +153 -0
- package/dist/__tests__/daemon-rpc.test.js +100 -0
- package/dist/__tests__/observability.test.js +69 -0
- package/dist/__tests__/projects.test.js +141 -0
- package/dist/backends/daemon-client.js +87 -0
- package/dist/backends/in-process.js +104 -0
- package/dist/backends/index.js +29 -0
- package/dist/cli/index.js +231 -0
- package/dist/core/config.js +362 -49
- package/dist/core/env-config.js +45 -43
- package/dist/daemon/index.js +119 -0
- package/dist/daemon/rpc-server.js +99 -0
- package/dist/daemon/watcher.js +105 -0
- package/dist/index.js +7 -0
- package/dist/observability/logger.js +66 -0
- package/dist/observability/metrics.js +138 -0
- package/dist/projects/registry.js +146 -0
- package/dist/resources/resources.js +3 -3
- package/dist/server.js +6 -2
- package/dist/tools/graph.js +1 -1
- package/dist/tools/index-tools.js +1 -1
- package/dist/tools/profile.js +2 -2
- package/dist/tools/search.js +1 -1
- package/dist/transports/http-transport.js +38 -0
- package/package.json +31 -12
- package/scripts/smoke/run-smoke.sh +167 -0
- package/dist/core/ast-graph-builder.js +0 -800
- package/dist/core/chunker.js +0 -368
- package/dist/core/claude-runner.js +0 -173
- package/dist/core/cross-repo-detector.js +0 -887
- package/dist/core/embedder.js +0 -334
- package/dist/core/file-walker.js +0 -186
- package/dist/core/git-diff.js +0 -160
- package/dist/core/graph-metrics.js +0 -233
- package/dist/core/indexer.js +0 -552
- package/dist/core/project-graph-builder.js +0 -202
- package/dist/core/query-classifier.js +0 -144
- package/dist/core/query-router.js +0 -210
- package/dist/core/rag-evaluator.js +0 -213
- package/dist/core/repo-profiler.js +0 -383
- package/dist/core/reranker.js +0 -218
- package/dist/core/retriever.js +0 -242
- package/dist/core/semantic-edge-detector.js +0 -147
- package/dist/core/service-mesh-inferrer.js +0 -241
- package/dist/core/structural-hasher.js +0 -316
- package/dist/core/tree-sitter-parser.js +0 -539
- package/dist/core/vector-store.js +0 -259
- package/dist/core/workspace-detector.js +0 -481
- /package/dist/{core → backends}/types.js +0 -0
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { runClaude } from "./claude-runner.js";
|
|
2
|
-
const ANSWER_SYSTEM = `You are a code expert answering questions about a codebase.
|
|
3
|
-
Rules:
|
|
4
|
-
- Answer using ONLY the provided code context in your project prompt.
|
|
5
|
-
- If the context doesn't contain enough information, say so explicitly.
|
|
6
|
-
- Be specific \u2014 reference file paths, function names, and line numbers when relevant.
|
|
7
|
-
- Keep your answer concise (2-4 paragraphs max).
|
|
8
|
-
- Do NOT make up information not present in the context.
|
|
9
|
-
- Do NOT use any tools. Just answer from the context provided.`;
|
|
10
|
-
async function generateAnswer(query, chunks, graphContext, opts) {
|
|
11
|
-
const contextParts = [];
|
|
12
|
-
if (graphContext) {
|
|
13
|
-
contextParts.push(`## Structural Context
|
|
14
|
-
${graphContext}`);
|
|
15
|
-
}
|
|
16
|
-
for (const sc of chunks) {
|
|
17
|
-
contextParts.push(
|
|
18
|
-
`## ${sc.chunk.filePath} (${sc.chunk.entityType}: ${sc.chunk.entityName ?? "anonymous"})
|
|
19
|
-
\`\`\`${sc.chunk.language}
|
|
20
|
-
${sc.chunk.content}
|
|
21
|
-
\`\`\``
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
const contextText = contextParts.join("\n\n");
|
|
25
|
-
const contextTokens = Math.ceil(contextText.length / 4);
|
|
26
|
-
const projectPrompt = `${ANSWER_SYSTEM}
|
|
27
|
-
|
|
28
|
-
# Code Context
|
|
29
|
-
|
|
30
|
-
${contextText}`;
|
|
31
|
-
const prompt = `Answer this question using ONLY the code context in your project prompt:
|
|
32
|
-
|
|
33
|
-
${query}`;
|
|
34
|
-
const result = await runClaude(prompt, projectPrompt, { model: opts?.model });
|
|
35
|
-
return {
|
|
36
|
-
answer: result.result,
|
|
37
|
-
inputTokens: result.inputTokens,
|
|
38
|
-
outputTokens: result.outputTokens,
|
|
39
|
-
costUsd: result.costUsd,
|
|
40
|
-
durationMs: result.durationMs,
|
|
41
|
-
model: opts?.model ?? "sonnet",
|
|
42
|
-
contextTokens
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const MODE_LABELS = {
|
|
46
|
-
"vector": "Vector (semantic similarity)",
|
|
47
|
-
"bm25": "BM25 (keyword matching)",
|
|
48
|
-
"vector+bm25": "Vector + BM25 (hybrid)",
|
|
49
|
-
"vector+graph": "Vector + Graph (structural)",
|
|
50
|
-
"vector+bm25+graph": "Full Hybrid (vector + BM25 + graph)"
|
|
51
|
-
};
|
|
52
|
-
async function judgeAnswers(query, answers, referenceAnswer, opts) {
|
|
53
|
-
const answerSection = Object.entries(answers).map(([mode, answer]) => `### ${MODE_LABELS[mode] ?? mode}
|
|
54
|
-
${answer}`).join("\n\n");
|
|
55
|
-
const refSection = referenceAnswer ? `
|
|
56
|
-
## Reference Answer (ground truth)
|
|
57
|
-
${referenceAnswer}
|
|
58
|
-
` : "";
|
|
59
|
-
const similarityInstruction = referenceAnswer ? "\n- **similarity** (1-10): How close is this answer to the reference answer in meaning and coverage?" : "";
|
|
60
|
-
const similarityJson = referenceAnswer ? ', "similarity": <1-10>' : "";
|
|
61
|
-
const projectPrompt = `You are an expert evaluator of code search and RAG quality. Always respond with valid JSON only.
|
|
62
|
-
|
|
63
|
-
## Question
|
|
64
|
-
${query}
|
|
65
|
-
${refSection}
|
|
66
|
-
## Answers to Evaluate
|
|
67
|
-
|
|
68
|
-
${answerSection}`;
|
|
69
|
-
const prompt = `Score each answer in your project prompt. Dimensions (1-10 each):
|
|
70
|
-
- correctness: Is the answer factually accurate?
|
|
71
|
-
- completeness: Does it cover all aspects?
|
|
72
|
-
- groundedness: Is every claim supported by code context?${similarityInstruction}
|
|
73
|
-
|
|
74
|
-
Respond with ONLY valid JSON:
|
|
75
|
-
{
|
|
76
|
-
${Object.keys(answers).map((mode) => `"${mode}": { "correctness": <1-10>, "completeness": <1-10>, "groundedness": <1-10>${similarityJson}, "reasoning": "<1-2 sentences>" }`).join(",\n ")}
|
|
77
|
-
}`;
|
|
78
|
-
const result = await runClaude(prompt, projectPrompt, { model: opts?.model });
|
|
79
|
-
let scoresRaw = result.result.trim();
|
|
80
|
-
if (scoresRaw.startsWith("```")) {
|
|
81
|
-
scoresRaw = scoresRaw.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
82
|
-
}
|
|
83
|
-
let parsed;
|
|
84
|
-
try {
|
|
85
|
-
parsed = JSON.parse(scoresRaw);
|
|
86
|
-
} catch {
|
|
87
|
-
parsed = {};
|
|
88
|
-
for (const mode of Object.keys(answers)) {
|
|
89
|
-
parsed[mode] = { correctness: 5, completeness: 5, groundedness: 5, reasoning: "Judge output could not be parsed" };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const scores = {};
|
|
93
|
-
for (const [mode, raw] of Object.entries(parsed)) {
|
|
94
|
-
const dims = [raw.correctness, raw.completeness, raw.groundedness];
|
|
95
|
-
if (raw.similarity !== void 0) dims.push(raw.similarity);
|
|
96
|
-
const overall = dims.reduce((s, v) => s + v, 0) / dims.length;
|
|
97
|
-
scores[mode] = {
|
|
98
|
-
correctness: raw.correctness,
|
|
99
|
-
completeness: raw.completeness,
|
|
100
|
-
groundedness: raw.groundedness,
|
|
101
|
-
similarity: raw.similarity,
|
|
102
|
-
overall: Math.round(overall * 10) / 10,
|
|
103
|
-
reasoning: raw.reasoning
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
scores,
|
|
108
|
-
inputTokens: result.inputTokens,
|
|
109
|
-
outputTokens: result.outputTokens,
|
|
110
|
-
costUsd: result.costUsd,
|
|
111
|
-
durationMs: result.durationMs,
|
|
112
|
-
model: opts?.model ?? "sonnet"
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
const EXPERT_JUDGE_SYSTEM = `You are an expert code reviewer and evaluator. You will be given:
|
|
116
|
-
1. Actual source code from the codebase (this is the ground truth)
|
|
117
|
-
2. A question about the code
|
|
118
|
-
3. Five candidate answers generated by different retrieval strategies
|
|
119
|
-
|
|
120
|
-
Your job:
|
|
121
|
-
|
|
122
|
-
STEP 1: Read ALL the source code carefully. Write your own comprehensive, accurate answer to the question based on what you see in the code. This is your expert reference answer. Be thorough \u2014 cover every relevant function, file, and data flow you can find in the provided code.
|
|
123
|
-
|
|
124
|
-
STEP 2: Score each of the 5 candidate answers by comparing them against the source code AND your expert answer.
|
|
125
|
-
|
|
126
|
-
Scoring dimensions (1-10 each):
|
|
127
|
-
- **correctness**: Does the answer accurately describe what the code actually does? Deduct for any incorrect claim about function behavior, data flow, or logic.
|
|
128
|
-
- **completeness**: How much of your expert answer does this candidate cover? If your answer has 5 key points and the candidate covers 3, score ~6.
|
|
129
|
-
- **groundedness**: Does every specific claim (function names, file paths, behavior descriptions) match the actual source code provided? Deduct heavily for hallucinated names, invented file paths, or fabricated logic.
|
|
130
|
-
- **hallucination_count**: Count the number of specific factual claims that are NOT verifiable from the provided source code. A function name that doesn't exist = 1 hallucination. A file path not in the code = 1 hallucination. 0 = perfect.
|
|
131
|
-
|
|
132
|
-
Respond with ONLY valid JSON (no markdown fences, no explanation outside the JSON):`;
|
|
133
|
-
async function expertJudge(query, answers, expertChunks, opts) {
|
|
134
|
-
const codeContext = expertChunks.map((sc) => `## ${sc.chunk.filePath} (${sc.chunk.entityType}: ${sc.chunk.entityName ?? "module"})
|
|
135
|
-
\`\`\`${sc.chunk.language}
|
|
136
|
-
${sc.chunk.content}
|
|
137
|
-
\`\`\``).join("\n\n");
|
|
138
|
-
const answerSection = Object.entries(answers).map(([mode, answer]) => `### ${MODE_LABELS[mode] ?? mode}
|
|
139
|
-
${answer}`).join("\n\n");
|
|
140
|
-
const projectPrompt = `${EXPERT_JUDGE_SYSTEM}
|
|
141
|
-
|
|
142
|
-
# Source Code (Ground Truth)
|
|
143
|
-
|
|
144
|
-
${codeContext}
|
|
145
|
-
|
|
146
|
-
# Question
|
|
147
|
-
|
|
148
|
-
${query}
|
|
149
|
-
|
|
150
|
-
# Candidate Answers to Evaluate
|
|
151
|
-
|
|
152
|
-
${answerSection}`;
|
|
153
|
-
const prompt = `Read the source code in your project prompt, write your expert answer, then score each candidate.
|
|
154
|
-
|
|
155
|
-
Respond with ONLY this JSON structure:
|
|
156
|
-
{
|
|
157
|
-
"expert_answer": "Your comprehensive answer based on the source code...",
|
|
158
|
-
${Object.keys(answers).map((mode) => `"${mode}": { "correctness": <1-10>, "completeness": <1-10>, "groundedness": <1-10>, "hallucination_count": <0-N>, "reasoning": "<2-3 sentences comparing to source code>" }`).join(",\n ")}
|
|
159
|
-
}`;
|
|
160
|
-
const result = await runClaude(prompt, projectPrompt, { model: opts?.model });
|
|
161
|
-
let scoresRaw = result.result.trim();
|
|
162
|
-
if (scoresRaw.startsWith("```")) {
|
|
163
|
-
scoresRaw = scoresRaw.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
164
|
-
}
|
|
165
|
-
let parsed;
|
|
166
|
-
try {
|
|
167
|
-
parsed = JSON.parse(scoresRaw);
|
|
168
|
-
} catch {
|
|
169
|
-
const jsonMatch = scoresRaw.match(/\{[\s\S]*\}/);
|
|
170
|
-
if (jsonMatch) {
|
|
171
|
-
try {
|
|
172
|
-
parsed = JSON.parse(jsonMatch[0]);
|
|
173
|
-
} catch {
|
|
174
|
-
parsed = {};
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
parsed = {};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const expertAnswer = typeof parsed.expert_answer === "string" ? parsed.expert_answer : void 0;
|
|
181
|
-
const scores = {};
|
|
182
|
-
for (const mode of Object.keys(answers)) {
|
|
183
|
-
const raw = parsed[mode];
|
|
184
|
-
if (raw && typeof raw === "object") {
|
|
185
|
-
const dims = [raw.correctness ?? 5, raw.completeness ?? 5, raw.groundedness ?? 5];
|
|
186
|
-
const overall = dims.reduce((s, v) => s + v, 0) / dims.length;
|
|
187
|
-
scores[mode] = {
|
|
188
|
-
correctness: raw.correctness ?? 5,
|
|
189
|
-
completeness: raw.completeness ?? 5,
|
|
190
|
-
groundedness: raw.groundedness ?? 5,
|
|
191
|
-
hallucination_count: typeof raw.hallucination_count === "number" ? raw.hallucination_count : void 0,
|
|
192
|
-
overall: Math.round(overall * 10) / 10,
|
|
193
|
-
reasoning: raw.reasoning ?? "No reasoning provided"
|
|
194
|
-
};
|
|
195
|
-
} else {
|
|
196
|
-
scores[mode] = { correctness: 5, completeness: 5, groundedness: 5, overall: 5, reasoning: "Judge output missing for this mode" };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
scores,
|
|
201
|
-
expertAnswer,
|
|
202
|
-
inputTokens: result.inputTokens,
|
|
203
|
-
outputTokens: result.outputTokens,
|
|
204
|
-
costUsd: result.costUsd,
|
|
205
|
-
durationMs: result.durationMs,
|
|
206
|
-
model: opts?.model ?? "sonnet"
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
export {
|
|
210
|
-
expertJudge,
|
|
211
|
-
generateAnswer,
|
|
212
|
-
judgeAnswers
|
|
213
|
-
};
|
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
-
import { join, basename } from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { runClaude } from "./claude-runner.js";
|
|
5
|
-
import { getKnowledgeBasePath } from "./config.js";
|
|
6
|
-
const MAX_FINGERPRINT_CHARS = 6e3;
|
|
7
|
-
const MAX_CONCURRENT = 3;
|
|
8
|
-
const PROFILE_FILENAME = "profile.json";
|
|
9
|
-
const PROFILER_SYSTEM_PROMPT = `You are a senior software architect analyzing a repository to understand its role in a larger system.
|
|
10
|
-
|
|
11
|
-
Given the fingerprint files below, produce a structured JSON profile.
|
|
12
|
-
|
|
13
|
-
Rules:
|
|
14
|
-
- Infer the repo's role from its code, not just its name
|
|
15
|
-
- For "domain", group by business function (e.g., "email-delivery", "billing", "user-management", "infrastructure")
|
|
16
|
-
- For "exposes", list what this repo provides: HTTP endpoints, Kafka topics it produces, gRPC services, etc.
|
|
17
|
-
- For "consumes", list what this repo depends on: Kafka topics it subscribes to, HTTP services it calls, databases it reads
|
|
18
|
-
- Be specific with identifiers: use actual topic names, endpoint paths, table names from the code
|
|
19
|
-
- If you can't determine something, use "unknown" -- don't guess
|
|
20
|
-
|
|
21
|
-
Respond with ONLY valid JSON matching this schema (no markdown fences):
|
|
22
|
-
{
|
|
23
|
-
"name": "string",
|
|
24
|
-
"role": "service | library | cli | worker | gateway | ui | schema | config | monorepo | unknown",
|
|
25
|
-
"domain": "string",
|
|
26
|
-
"description": "string (1-2 sentences)",
|
|
27
|
-
"technologies": ["string"],
|
|
28
|
-
"exposes": [{ "type": "http | grpc | kafka-producer | kafka-consumer | database | redis | s3 | websocket | cron | other", "identifier": "string", "description": "string" }],
|
|
29
|
-
"consumes": [{ "type": "http | grpc | kafka-producer | kafka-consumer | database | redis | s3 | websocket | cron | other", "identifier": "string", "description": "string" }],
|
|
30
|
-
"entryPoints": ["string"]
|
|
31
|
-
}`;
|
|
32
|
-
function safeReadFile(path, maxChars) {
|
|
33
|
-
try {
|
|
34
|
-
if (!existsSync(path)) return null;
|
|
35
|
-
const content = readFileSync(path, "utf-8");
|
|
36
|
-
return maxChars ? content.slice(0, maxChars) : content;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function safeReadLines(path, maxLines) {
|
|
42
|
-
try {
|
|
43
|
-
if (!existsSync(path)) return null;
|
|
44
|
-
const content = readFileSync(path, "utf-8");
|
|
45
|
-
return content.split("\n").slice(0, maxLines).join("\n");
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function findFirst(repoPath, candidates) {
|
|
51
|
-
for (const c of candidates) {
|
|
52
|
-
const full = join(repoPath, c);
|
|
53
|
-
if (existsSync(full)) return c;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
function findFirstGlob(repoPath, dir, ext) {
|
|
58
|
-
const dirPath = join(repoPath, dir);
|
|
59
|
-
try {
|
|
60
|
-
if (!existsSync(dirPath)) return null;
|
|
61
|
-
const entries = readdirSync(dirPath);
|
|
62
|
-
const match = entries.find((e) => e.endsWith(ext));
|
|
63
|
-
return match ? join(dir, match) : null;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function extractPackageJsonEssentials(raw) {
|
|
69
|
-
try {
|
|
70
|
-
const pkg = JSON.parse(raw);
|
|
71
|
-
const essentials = {};
|
|
72
|
-
for (const key of ["name", "description", "scripts", "dependencies", "devDependencies", "peerDependencies"]) {
|
|
73
|
-
if (!(key in pkg)) continue;
|
|
74
|
-
if (key === "scripts") {
|
|
75
|
-
essentials[key] = pkg[key];
|
|
76
|
-
} else if (typeof pkg[key] === "object" && pkg[key] !== null && key !== "name" && key !== "description") {
|
|
77
|
-
essentials[key] = Object.keys(pkg[key]);
|
|
78
|
-
} else {
|
|
79
|
-
essentials[key] = pkg[key];
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return JSON.stringify(essentials, null, 2);
|
|
83
|
-
} catch {
|
|
84
|
-
return raw;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function extractGoModEssentials(raw) {
|
|
88
|
-
const lines = raw.split("\n");
|
|
89
|
-
const result = [];
|
|
90
|
-
let inRequire = false;
|
|
91
|
-
for (const line of lines) {
|
|
92
|
-
if (line.startsWith("module ")) {
|
|
93
|
-
result.push(line);
|
|
94
|
-
} else if (line.startsWith("require")) {
|
|
95
|
-
inRequire = true;
|
|
96
|
-
result.push(line);
|
|
97
|
-
} else if (inRequire) {
|
|
98
|
-
result.push(line);
|
|
99
|
-
if (line.trim() === ")") inRequire = false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return result.join("\n");
|
|
103
|
-
}
|
|
104
|
-
async function extractFingerprint(repoPath) {
|
|
105
|
-
const repoName = basename(repoPath);
|
|
106
|
-
const files = [];
|
|
107
|
-
let totalChars = 0;
|
|
108
|
-
function addFile(relativePath, content) {
|
|
109
|
-
if (totalChars + content.length > MAX_FINGERPRINT_CHARS) {
|
|
110
|
-
const remaining = MAX_FINGERPRINT_CHARS - totalChars;
|
|
111
|
-
if (remaining > 200) {
|
|
112
|
-
files.push({ relativePath, content: content.slice(0, remaining) });
|
|
113
|
-
totalChars += remaining;
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
files.push({ relativePath, content });
|
|
118
|
-
totalChars += content.length;
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
const readme = safeReadFile(join(repoPath, "README.md"), 2e3);
|
|
122
|
-
if (readme) addFile("README.md", readme);
|
|
123
|
-
const dockerFile = findFirst(repoPath, ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"]);
|
|
124
|
-
if (dockerFile) {
|
|
125
|
-
const content = safeReadFile(join(repoPath, dockerFile), 1500);
|
|
126
|
-
if (content) addFile(dockerFile, content);
|
|
127
|
-
}
|
|
128
|
-
const entryPointCandidates = [
|
|
129
|
-
"main.go",
|
|
130
|
-
"src/index.ts",
|
|
131
|
-
"src/main.ts",
|
|
132
|
-
"src/index.js",
|
|
133
|
-
"app.py",
|
|
134
|
-
"main.py",
|
|
135
|
-
"index.ts",
|
|
136
|
-
"index.js"
|
|
137
|
-
];
|
|
138
|
-
const cmdMainGo = findFirstGlob(repoPath, "cmd", "main.go");
|
|
139
|
-
if (cmdMainGo) entryPointCandidates.unshift(cmdMainGo);
|
|
140
|
-
const javaApp = findFirstGlob(repoPath, "src/main/java", "Application.java");
|
|
141
|
-
if (javaApp) entryPointCandidates.push(javaApp);
|
|
142
|
-
const entryPoint = findFirst(repoPath, entryPointCandidates);
|
|
143
|
-
if (entryPoint && totalChars < MAX_FINGERPRINT_CHARS) {
|
|
144
|
-
const content = safeReadLines(join(repoPath, entryPoint), 100);
|
|
145
|
-
if (content) addFile(entryPoint, content);
|
|
146
|
-
}
|
|
147
|
-
if (totalChars < MAX_FINGERPRINT_CHARS) {
|
|
148
|
-
const manifestFile = findFirst(repoPath, ["package.json", "go.mod", "pyproject.toml", "Cargo.toml", "pom.xml"]);
|
|
149
|
-
if (manifestFile) {
|
|
150
|
-
const raw = safeReadFile(join(repoPath, manifestFile));
|
|
151
|
-
if (raw) {
|
|
152
|
-
let content = raw;
|
|
153
|
-
if (manifestFile === "package.json") {
|
|
154
|
-
content = extractPackageJsonEssentials(raw);
|
|
155
|
-
} else if (manifestFile === "go.mod") {
|
|
156
|
-
content = extractGoModEssentials(raw);
|
|
157
|
-
}
|
|
158
|
-
addFile(manifestFile, content);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (totalChars < MAX_FINGERPRINT_CHARS) {
|
|
163
|
-
const ciFile = findFirstGlob(repoPath, ".github/workflows", ".yml") ?? findFirstGlob(repoPath, ".github/workflows", ".yaml");
|
|
164
|
-
if (ciFile) {
|
|
165
|
-
const content = safeReadFile(join(repoPath, ciFile), 1e3);
|
|
166
|
-
if (content) addFile(ciFile, content);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (totalChars < MAX_FINGERPRINT_CHARS) {
|
|
170
|
-
const apiSchema = findFirst(repoPath, ["openapi.yaml", "openapi.json", "asyncapi.yaml"]);
|
|
171
|
-
if (apiSchema) {
|
|
172
|
-
const content = safeReadFile(join(repoPath, apiSchema), 1500);
|
|
173
|
-
if (content) addFile(apiSchema, content);
|
|
174
|
-
}
|
|
175
|
-
if (!apiSchema) {
|
|
176
|
-
const protoFile = findFirstGlob(repoPath, "proto", ".proto") ?? findFirstGlob(repoPath, ".", ".proto");
|
|
177
|
-
if (protoFile) {
|
|
178
|
-
const content = safeReadFile(join(repoPath, protoFile), 1500);
|
|
179
|
-
if (content) addFile(protoFile, content);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (totalChars < MAX_FINGERPRINT_CHARS) {
|
|
184
|
-
const k8sFile = findFirstGlob(repoPath, "k8s", ".yaml") ?? findFirstGlob(repoPath, "k8s", ".yml") ?? findFirstGlob(repoPath, "deploy", ".yaml") ?? findFirstGlob(repoPath, "deploy", ".yml");
|
|
185
|
-
if (k8sFile) {
|
|
186
|
-
const content = safeReadFile(join(repoPath, k8sFile), 500);
|
|
187
|
-
if (content) addFile(k8sFile, content);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const hash = createHash("sha256");
|
|
191
|
-
for (const f of files) {
|
|
192
|
-
hash.update(f.relativePath);
|
|
193
|
-
hash.update(f.content);
|
|
194
|
-
}
|
|
195
|
-
const fingerprintHash = hash.digest("hex");
|
|
196
|
-
return {
|
|
197
|
-
repoPath,
|
|
198
|
-
repoName,
|
|
199
|
-
files,
|
|
200
|
-
totalChars,
|
|
201
|
-
fingerprintHash
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
function buildFingerprintText(fingerprint) {
|
|
205
|
-
const sections = fingerprint.files.map(
|
|
206
|
-
(f) => `### ${f.relativePath}
|
|
207
|
-
|
|
208
|
-
${f.content}`
|
|
209
|
-
);
|
|
210
|
-
return `Repository: ${fingerprint.repoName}
|
|
211
|
-
|
|
212
|
-
${sections.join("\n\n---\n\n")}`;
|
|
213
|
-
}
|
|
214
|
-
function parseLLMJson(raw) {
|
|
215
|
-
let text = raw.trim();
|
|
216
|
-
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
217
|
-
if (fenceMatch) {
|
|
218
|
-
text = fenceMatch[1].trim();
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
return JSON.parse(text);
|
|
222
|
-
} catch {
|
|
223
|
-
const jsonStart = text.indexOf("{");
|
|
224
|
-
const jsonEnd = text.lastIndexOf("}");
|
|
225
|
-
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
226
|
-
try {
|
|
227
|
-
return JSON.parse(text.slice(jsonStart, jsonEnd + 1));
|
|
228
|
-
} catch {
|
|
229
|
-
throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 200)}...`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
throw new Error(`No JSON object found in LLM response: ${text.slice(0, 200)}...`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function profileRepo(fingerprint, opts) {
|
|
236
|
-
const fingerprintText = buildFingerprintText(fingerprint);
|
|
237
|
-
const result = await runClaude(
|
|
238
|
-
fingerprintText,
|
|
239
|
-
PROFILER_SYSTEM_PROMPT,
|
|
240
|
-
{ model: opts?.model, timeoutMs: 6e5 }
|
|
241
|
-
);
|
|
242
|
-
const parsed = parseLLMJson(result.result);
|
|
243
|
-
const profile = {
|
|
244
|
-
name: parsed.name ?? fingerprint.repoName,
|
|
245
|
-
role: parsed.role ?? "unknown",
|
|
246
|
-
domain: parsed.domain ?? "unknown",
|
|
247
|
-
description: parsed.description ?? "",
|
|
248
|
-
technologies: parsed.technologies ?? [],
|
|
249
|
-
exposes: parsed.exposes ?? [],
|
|
250
|
-
consumes: parsed.consumes ?? [],
|
|
251
|
-
entryPoints: parsed.entryPoints ?? [],
|
|
252
|
-
profiledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
253
|
-
profiledBy: opts?.model ?? "default",
|
|
254
|
-
fingerprintHash: fingerprint.fingerprintHash
|
|
255
|
-
};
|
|
256
|
-
return profile;
|
|
257
|
-
}
|
|
258
|
-
async function profileProject(project, repos, opts) {
|
|
259
|
-
const progress = opts?.onProgress ?? (() => {
|
|
260
|
-
});
|
|
261
|
-
const kbPath = getKnowledgeBasePath(project);
|
|
262
|
-
const profiles = [];
|
|
263
|
-
const errors = [];
|
|
264
|
-
progress(`Profiling ${repos.length} repos...`);
|
|
265
|
-
const fingerprints = await Promise.all(
|
|
266
|
-
repos.map(async (repo) => {
|
|
267
|
-
try {
|
|
268
|
-
const fp = await extractFingerprint(repo.path);
|
|
269
|
-
return { repo, fingerprint: fp };
|
|
270
|
-
} catch (err) {
|
|
271
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
272
|
-
errors.push({ name: repo.name, error: msg });
|
|
273
|
-
progress(` [WARN] Failed to extract fingerprint for ${repo.name}: ${msg}`);
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
);
|
|
278
|
-
const validFingerprints = fingerprints.filter(
|
|
279
|
-
(f) => f !== null
|
|
280
|
-
);
|
|
281
|
-
const needsProfiling = [];
|
|
282
|
-
for (const item of validFingerprints) {
|
|
283
|
-
if (opts?.force) {
|
|
284
|
-
needsProfiling.push(item);
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const cached = loadCachedProfile(kbPath, item.repo.name);
|
|
288
|
-
if (cached && cached.fingerprintHash === item.fingerprint.fingerprintHash) {
|
|
289
|
-
progress(` [CACHED] ${item.repo.name} (fingerprint unchanged)`);
|
|
290
|
-
profiles.push(cached);
|
|
291
|
-
} else {
|
|
292
|
-
needsProfiling.push(item);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (needsProfiling.length === 0) {
|
|
296
|
-
progress("All repos cached, no profiling needed.");
|
|
297
|
-
return profiles;
|
|
298
|
-
}
|
|
299
|
-
progress(`Profiling ${needsProfiling.length} repos (${validFingerprints.length - needsProfiling.length} cached)...`);
|
|
300
|
-
for (let i = 0; i < needsProfiling.length; i += MAX_CONCURRENT) {
|
|
301
|
-
const batch = needsProfiling.slice(i, i + MAX_CONCURRENT);
|
|
302
|
-
const batchResults = await Promise.allSettled(
|
|
303
|
-
batch.map(async (item) => {
|
|
304
|
-
progress(` [PROFILING] ${item.repo.name} (${item.fingerprint.files.length} files, ${item.fingerprint.totalChars} chars)`);
|
|
305
|
-
const profile = await profileRepo(item.fingerprint, { model: opts?.model });
|
|
306
|
-
return { repo: item.repo, profile };
|
|
307
|
-
})
|
|
308
|
-
);
|
|
309
|
-
for (const result of batchResults) {
|
|
310
|
-
if (result.status === "fulfilled") {
|
|
311
|
-
const { repo, profile } = result.value;
|
|
312
|
-
profiles.push(profile);
|
|
313
|
-
saveProfileToDisk(kbPath, repo.name, profile);
|
|
314
|
-
progress(` [DONE] ${repo.name} -> ${profile.role} (${profile.domain})`);
|
|
315
|
-
} else {
|
|
316
|
-
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
317
|
-
const batchIndex = batchResults.indexOf(result);
|
|
318
|
-
const repoName = batch[batchIndex]?.repo.name ?? "unknown";
|
|
319
|
-
errors.push({ name: repoName, error: reason });
|
|
320
|
-
progress(` [WARN] Failed to profile ${repoName}: ${reason}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (errors.length > 0) {
|
|
325
|
-
progress(`Completed with ${errors.length} error(s): ${errors.map((e) => e.name).join(", ")}`);
|
|
326
|
-
}
|
|
327
|
-
return profiles;
|
|
328
|
-
}
|
|
329
|
-
function loadCachedProfile(kbPath, repoName) {
|
|
330
|
-
const profilePath = join(kbPath, repoName, PROFILE_FILENAME);
|
|
331
|
-
try {
|
|
332
|
-
if (!existsSync(profilePath)) return null;
|
|
333
|
-
const raw = readFileSync(profilePath, "utf-8");
|
|
334
|
-
return JSON.parse(raw);
|
|
335
|
-
} catch {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function saveProfileToDisk(kbPath, repoName, profile) {
|
|
340
|
-
const repoDir = join(kbPath, repoName);
|
|
341
|
-
mkdirSync(repoDir, { recursive: true });
|
|
342
|
-
const profilePath = join(repoDir, PROFILE_FILENAME);
|
|
343
|
-
writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf-8");
|
|
344
|
-
}
|
|
345
|
-
function loadProfile(project, repoName) {
|
|
346
|
-
const kbPath = getKnowledgeBasePath(project);
|
|
347
|
-
const profilePath = join(kbPath, repoName, PROFILE_FILENAME);
|
|
348
|
-
try {
|
|
349
|
-
if (!existsSync(profilePath)) return null;
|
|
350
|
-
const raw = readFileSync(profilePath, "utf-8");
|
|
351
|
-
return JSON.parse(raw);
|
|
352
|
-
} catch {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
function loadAllProfiles(project) {
|
|
357
|
-
const kbPath = getKnowledgeBasePath(project);
|
|
358
|
-
const profiles = [];
|
|
359
|
-
try {
|
|
360
|
-
if (!existsSync(kbPath)) return profiles;
|
|
361
|
-
const entries = readdirSync(kbPath, { withFileTypes: true });
|
|
362
|
-
for (const entry of entries) {
|
|
363
|
-
if (!entry.isDirectory()) continue;
|
|
364
|
-
const profilePath = join(kbPath, entry.name, PROFILE_FILENAME);
|
|
365
|
-
try {
|
|
366
|
-
if (!existsSync(profilePath)) continue;
|
|
367
|
-
const raw = readFileSync(profilePath, "utf-8");
|
|
368
|
-
const profile = JSON.parse(raw);
|
|
369
|
-
profiles.push(profile);
|
|
370
|
-
} catch {
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
} catch {
|
|
374
|
-
}
|
|
375
|
-
return profiles;
|
|
376
|
-
}
|
|
377
|
-
export {
|
|
378
|
-
extractFingerprint,
|
|
379
|
-
loadAllProfiles,
|
|
380
|
-
loadProfile,
|
|
381
|
-
profileProject,
|
|
382
|
-
profileRepo
|
|
383
|
-
};
|