@esankhan3/code-search-mcp 0.0.2 → 0.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.
@@ -1,202 +0,0 @@
1
- let _Graph;
2
- let _louvain;
3
- async function ensureGraphology() {
4
- if (_Graph) return;
5
- const graphMod = await import("graphology");
6
- _Graph = graphMod.default ?? graphMod;
7
- const louvainMod = await import("graphology-communities-louvain");
8
- _louvain = louvainMod.default ?? louvainMod;
9
- }
10
- function getGraphClass() {
11
- if (!_Graph) throw new Error("Call ensureGraphology() before using graph operations");
12
- return _Graph;
13
- }
14
- class ProjectGraphBuilder {
15
- graph;
16
- constructor() {
17
- this.graph = null;
18
- }
19
- /** Must be called before using the builder */
20
- async init() {
21
- await ensureGraphology();
22
- const G = getGraphClass();
23
- this.graph = new G({ type: "directed", multi: true });
24
- }
25
- /** Expose the underlying graph for query functions */
26
- getGraph() {
27
- return this.graph;
28
- }
29
- /** Load a per-repo graph.json from Graphify and merge into the project graph.
30
- * Namespace all nodes as "repoName::originalId" to avoid collisions. */
31
- addRepoGraph(repoName, graphJson) {
32
- for (const node of graphJson.nodes) {
33
- const nsId = `${repoName}::${node.id}`;
34
- if (!this.graph.hasNode(nsId)) {
35
- this.graph.addNode(nsId, {
36
- repo: repoName,
37
- label: node.label ?? node.id,
38
- community: node.community,
39
- type: node.type,
40
- file: node.file
41
- });
42
- }
43
- }
44
- for (const edge of graphJson.links) {
45
- const src = `${repoName}::${edge.source}`;
46
- const tgt = `${repoName}::${edge.target}`;
47
- if (this.graph.hasNode(src) && this.graph.hasNode(tgt)) {
48
- this.graph.addEdge(src, tgt, {
49
- type: edge.type ?? "depends",
50
- confidence: edge.confidence ?? 0.8
51
- });
52
- }
53
- }
54
- }
55
- /** Add cross-repo edges detected by the cross-repo detector */
56
- addCrossRepoEdges(edges) {
57
- for (const edge of edges) {
58
- const srcId = `${edge.sourceRepo}::${edge.sourceNode}`;
59
- const tgtId = `${edge.targetRepo}::${edge.targetNode}`;
60
- if (!this.graph.hasNode(srcId)) {
61
- this.graph.addNode(srcId, { repo: edge.sourceRepo, label: edge.sourceNode, type: "external" });
62
- }
63
- if (!this.graph.hasNode(tgtId)) {
64
- this.graph.addNode(tgtId, { repo: edge.targetRepo, label: edge.targetNode, type: "external" });
65
- }
66
- this.graph.addEdge(srcId, tgtId, {
67
- type: edge.edgeType,
68
- evidence: edge.evidence,
69
- confidence: edge.confidence,
70
- crossRepo: true
71
- });
72
- }
73
- }
74
- /** Run Louvain community detection on the merged graph */
75
- detectCommunities() {
76
- if (this.graph.order === 0) return /* @__PURE__ */ new Map();
77
- const G = getGraphClass();
78
- const undirected = new G({ type: "undirected" });
79
- this.graph.forEachNode((node, attrs) => {
80
- undirected.mergeNode(node, attrs);
81
- });
82
- this.graph.forEachEdge((_edge, attrs, src, tgt) => {
83
- undirected.mergeEdge(src, tgt, attrs);
84
- });
85
- const communities = _louvain(undirected);
86
- for (const [node, community] of Object.entries(communities)) {
87
- if (this.graph.hasNode(node)) {
88
- this.graph.setNodeAttribute(node, "systemCommunity", community);
89
- }
90
- }
91
- return new Map(Object.entries(communities).map(([k, v]) => [k, v]));
92
- }
93
- /** Confidence-weighted BFS impact analysis from entry nodes.
94
- * Only follows edges with confidence >= minConfidence.
95
- * Sorts neighbors by confidence so high-quality paths are explored first. */
96
- impactAnalysis(entryNodes, maxDepth = 3, opts) {
97
- const minConf = opts?.minConfidence ?? 0;
98
- const visited = /* @__PURE__ */ new Set();
99
- let frontier = entryNodes.filter((n) => this.graph.hasNode(n));
100
- for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
101
- const nextFrontier = [];
102
- for (const node of frontier) {
103
- if (visited.has(node)) continue;
104
- visited.add(node);
105
- this.graph.forEachOutEdge(node, (_edge, attrs, _src, tgt) => {
106
- const conf = attrs.confidence ?? 0.8;
107
- if (conf >= minConf && !visited.has(tgt)) {
108
- nextFrontier.push({ node: tgt, confidence: conf });
109
- }
110
- });
111
- }
112
- nextFrontier.sort((a, b) => b.confidence - a.confidence);
113
- frontier = nextFrontier.map((f) => f.node);
114
- }
115
- return [...visited];
116
- }
117
- /** Get node type for a given node ID */
118
- getNodeType(nodeId) {
119
- if (!this.graph.hasNode(nodeId)) return null;
120
- return this.graph.getNodeAttribute(nodeId, "type") ?? null;
121
- }
122
- /** Get highest-degree nodes (architectural hotspots) */
123
- getHotspots(topN = 20) {
124
- const nodes = [];
125
- this.graph.forEachNode((node, attrs) => {
126
- nodes.push({ node, degree: this.graph.degree(node), repo: attrs.repo ?? "" });
127
- });
128
- nodes.sort((a, b) => b.degree - a.degree);
129
- return nodes.slice(0, topN);
130
- }
131
- /** Get cross-repo edges */
132
- getCrossRepoEdges() {
133
- const edges = [];
134
- this.graph.forEachEdge((_edge, attrs, src, tgt) => {
135
- if (attrs.crossRepo) {
136
- edges.push({ source: src, target: tgt, type: attrs.type, evidence: attrs.evidence ?? "" });
137
- }
138
- });
139
- return edges;
140
- }
141
- /** Export full graph as JSON */
142
- exportJson() {
143
- return this.graph.export();
144
- }
145
- /** Import from previously saved JSON */
146
- async importJson(data) {
147
- if (!this.graph) await this.init();
148
- this.graph.import(data);
149
- }
150
- /** Get node count */
151
- get nodeCount() {
152
- return this.graph.order;
153
- }
154
- /** Get edge count */
155
- get edgeCount() {
156
- return this.graph.size;
157
- }
158
- /** Export compact summary for agent prompt injection */
159
- exportForPrompt(maxChars = 5e3) {
160
- const sections = [];
161
- sections.push("## Project Architecture Graph\n");
162
- const repoNodes = /* @__PURE__ */ new Map();
163
- this.graph.forEachNode((_, attrs) => {
164
- const repo = attrs.repo ?? "unknown";
165
- repoNodes.set(repo, (repoNodes.get(repo) ?? 0) + 1);
166
- });
167
- sections.push("### Repositories");
168
- for (const [repo, count] of repoNodes) {
169
- sections.push(`- **${repo}**: ${count} nodes`);
170
- }
171
- const packageNodes = [];
172
- this.graph.forEachNode((node, attrs) => {
173
- if (attrs.type === "package") packageNodes.push({ node, repo: attrs.repo });
174
- });
175
- if (packageNodes.length > 0) {
176
- sections.push("\n### Workspace Packages");
177
- for (const p of packageNodes) {
178
- sections.push(`- **${p.node}** (${p.repo})`);
179
- }
180
- }
181
- const crossEdges = this.getCrossRepoEdges();
182
- if (crossEdges.length > 0) {
183
- sections.push("\n### Cross-Repo Connections");
184
- for (const edge of crossEdges.slice(0, 20)) {
185
- sections.push(`- ${edge.source} \u2192 ${edge.target} (${edge.type}: ${edge.evidence})`);
186
- }
187
- }
188
- const hotspots = this.getHotspots(10);
189
- if (hotspots.length > 0) {
190
- sections.push("\n### Architectural Hotspots");
191
- for (const h of hotspots) {
192
- sections.push(`- **${h.node}** (${h.repo}, degree: ${h.degree})`);
193
- }
194
- }
195
- let result = sections.join("\n");
196
- if (result.length > maxChars) result = result.slice(0, maxChars) + "\n[...]";
197
- return result;
198
- }
199
- }
200
- export {
201
- ProjectGraphBuilder
202
- };
@@ -1,144 +0,0 @@
1
- const CAMEL_CASE_RE = /[a-z][A-Z]/;
2
- const SNAKE_CASE_RE = /[a-z]_[a-z]/;
3
- const PASCAL_CASE_RE = /^[A-Z][a-z]+[A-Z]/;
4
- const DOT_PATH_RE = /[a-zA-Z]\.[a-zA-Z]/;
5
- const FILE_PATH_RE = /[/\\].*\.\w{1,5}$/;
6
- const FILE_EXT_RE = /\.\w{1,5}$/;
7
- const PATH_SEPARATOR_RE = /[/\\]/;
8
- const HEX_CODE_RE = /0x[A-Fa-f0-9]+/;
9
- const ERROR_PREFIX_RE = /\bERR_\w+/;
10
- const NUMERIC_ERROR_RE = /\bE\d{4,}/;
11
- const HTTP_STATUS_RE = /\b[1-5]\d{2}\b/;
12
- const QUESTION_WORDS = /* @__PURE__ */ new Set([
13
- "how",
14
- "what",
15
- "where",
16
- "why",
17
- "when",
18
- "does",
19
- "can",
20
- "is",
21
- "should",
22
- "which",
23
- "who",
24
- "explain",
25
- "describe",
26
- "show"
27
- ]);
28
- const WEIGHTS = {
29
- identifier: { vector: 0.2, bm25: 0.6, graph: 0.2 },
30
- path: { vector: 0.1, bm25: 0.7, graph: 0.2 },
31
- "error-code": { vector: 0.1, bm25: 0.7, graph: 0.2 },
32
- "natural-language": { vector: 0.6, bm25: 0.2, graph: 0.2 },
33
- mixed: { vector: 0.4, bm25: 0.35, graph: 0.25 }
34
- };
35
- function classifyQuery(query) {
36
- const trimmed = query.trim();
37
- if (trimmed.length === 0) {
38
- return {
39
- type: "natural-language",
40
- weights: { ...WEIGHTS["natural-language"] },
41
- shouldUseTrigram: false,
42
- explanation: "Empty query treated as natural language"
43
- };
44
- }
45
- const words = trimmed.split(/\s+/);
46
- const wordCount = words.length;
47
- const firstWordLower = words[0].toLowerCase();
48
- if (PATH_SEPARATOR_RE.test(trimmed) && FILE_EXT_RE.test(trimmed)) {
49
- return {
50
- type: "path",
51
- weights: { ...WEIGHTS.path },
52
- shouldUseTrigram: true,
53
- explanation: `File path detected: contains path separator and file extension`
54
- };
55
- }
56
- const hasHttpStatus = wordCount <= 3 && HTTP_STATUS_RE.test(trimmed);
57
- if (HEX_CODE_RE.test(trimmed) || ERROR_PREFIX_RE.test(trimmed) || NUMERIC_ERROR_RE.test(trimmed) || hasHttpStatus) {
58
- return {
59
- type: "error-code",
60
- weights: { ...WEIGHTS["error-code"] },
61
- shouldUseTrigram: true,
62
- explanation: `Error code pattern detected in query`
63
- };
64
- }
65
- const hasCodeIdentifier = CAMEL_CASE_RE.test(trimmed) || SNAKE_CASE_RE.test(trimmed) || PASCAL_CASE_RE.test(trimmed) || DOT_PATH_RE.test(trimmed);
66
- const hasQuestionWord = QUESTION_WORDS.has(firstWordLower);
67
- const isLongPhrase = wordCount > 4;
68
- const hasNaturalLanguageSignals = hasQuestionWord || isLongPhrase;
69
- if (hasCodeIdentifier && hasNaturalLanguageSignals) {
70
- return {
71
- type: "mixed",
72
- weights: { ...WEIGHTS.mixed },
73
- shouldUseTrigram: true,
74
- explanation: `Mixed query: contains code identifiers and natural language`
75
- };
76
- }
77
- if (hasCodeIdentifier || wordCount <= 2 && !hasNaturalLanguageSignals) {
78
- return {
79
- type: "identifier",
80
- weights: { ...WEIGHTS.identifier },
81
- shouldUseTrigram: true,
82
- explanation: `Identifier query: ${hasCodeIdentifier ? "code naming convention detected" : "short query without natural language signals"}`
83
- };
84
- }
85
- return {
86
- type: "natural-language",
87
- weights: { ...WEIGHTS["natural-language"] },
88
- shouldUseTrigram: false,
89
- explanation: `Natural language query: ${hasQuestionWord ? "starts with question word" : `${wordCount} words without code patterns`}`
90
- };
91
- }
92
- function routeQueryToRepos(query, profiles, opts) {
93
- if (profiles.length === 0) return [];
94
- const queryTokens = tokenize(query);
95
- if (queryTokens.length === 0) return profiles.map((p) => p.name);
96
- const scored = profiles.map((profile) => {
97
- const profileTokens = /* @__PURE__ */ new Set([
98
- ...tokenize(profile.name),
99
- ...tokenize(profile.domain),
100
- ...tokenize(profile.description),
101
- ...profile.technologies.flatMap((t) => tokenize(t))
102
- ]);
103
- let score = 0;
104
- for (const token of queryTokens) {
105
- if (profileTokens.has(token)) {
106
- score += 1;
107
- }
108
- for (const pt of Array.from(profileTokens)) {
109
- if (pt !== token && pt.includes(token) && token.length >= 3) {
110
- score += 0.5;
111
- }
112
- }
113
- }
114
- return { name: profile.name, score: score / queryTokens.length };
115
- });
116
- scored.sort((a, b) => b.score - a.score);
117
- const maxRepos = opts?.maxRepos;
118
- const topScore = scored[0].score;
119
- if (topScore < 0.1) {
120
- return profiles.map((p) => p.name);
121
- }
122
- const threshold = topScore * 0.3;
123
- let relevant = scored.filter((s) => s.score >= threshold);
124
- if (maxRepos && relevant.length > maxRepos) {
125
- relevant = relevant.slice(0, maxRepos);
126
- }
127
- if (!maxRepos && profiles.length > 10) {
128
- const cap = Math.ceil(profiles.length * 0.6);
129
- if (relevant.length > cap) {
130
- relevant = relevant.slice(0, cap);
131
- }
132
- }
133
- if (relevant.length === 0) {
134
- return profiles.map((p) => p.name);
135
- }
136
- return relevant.map((r) => r.name);
137
- }
138
- function tokenize(text) {
139
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
140
- }
141
- export {
142
- classifyQuery,
143
- routeQueryToRepos
144
- };
@@ -1,210 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
2
- import { join, dirname } from "node:path";
3
- import { getKnowledgeBasePath } from "./config.js";
4
- function profileToSearchText(profile) {
5
- const parts = [
6
- `Repository: ${profile.name}`,
7
- `Role: ${profile.role}`,
8
- `Domain: ${profile.domain}`,
9
- `Description: ${profile.description}`,
10
- `Technologies: ${profile.technologies.join(", ")}`
11
- ];
12
- if (profile.exposes.length > 0) {
13
- parts.push(
14
- `Exposes: ${profile.exposes.map((e) => `${e.type}:${e.identifier}`).join(", ")}`
15
- );
16
- }
17
- if (profile.consumes.length > 0) {
18
- parts.push(
19
- `Consumes: ${profile.consumes.map((c) => `${c.type}:${c.identifier}`).join(", ")}`
20
- );
21
- }
22
- return parts.join("\n");
23
- }
24
- function cosineSimilarity(a, b) {
25
- let dot = 0;
26
- let normA = 0;
27
- let normB = 0;
28
- for (let i = 0; i < a.length; i++) {
29
- dot += a[i] * b[i];
30
- normA += a[i] * a[i];
31
- normB += b[i] * b[i];
32
- }
33
- return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-10);
34
- }
35
- async function embedProfiles(profiles, embedder) {
36
- if (profiles.length === 0) return [];
37
- const texts = profiles.map(profileToSearchText);
38
- const embeddings = await embedder.embed(texts);
39
- return profiles.map((profile, i) => ({
40
- repoName: profile.name,
41
- embedding: embeddings[i],
42
- textUsed: texts[i]
43
- }));
44
- }
45
- async function routeQuery(query, profileEmbeddings, embedder, opts) {
46
- const minRepos = opts?.minRepos ?? 3;
47
- const maxFraction = opts?.maxFraction ?? 0.6;
48
- const total = profileEmbeddings.length;
49
- if (total <= 10) {
50
- const scores2 = /* @__PURE__ */ new Map();
51
- for (const pe of profileEmbeddings) {
52
- scores2.set(pe.repoName, 1);
53
- }
54
- return {
55
- repos: profileEmbeddings.map((pe) => pe.repoName),
56
- scores: scores2,
57
- strategy: "all"
58
- };
59
- }
60
- const queryEmbedding = await embedder.embedSingle(query);
61
- const scored = profileEmbeddings.map(
62
- (pe) => ({
63
- repoName: pe.repoName,
64
- score: cosineSimilarity(queryEmbedding, pe.embedding)
65
- })
66
- );
67
- scored.sort((a, b) => b.score - a.score);
68
- const scores = scored.map((s) => s.score);
69
- const mean = scores.reduce((sum, v) => sum + v, 0) / scores.length;
70
- const variance = scores.reduce((sum, v) => sum + (v - mean) ** 2, 0) / scores.length;
71
- const stddev = Math.sqrt(variance);
72
- const threshold = mean + 0.5 * stddev;
73
- let filtered = scored.filter((s) => s.score >= threshold);
74
- if (filtered.length < minRepos) {
75
- filtered = scored.slice(0, Math.min(minRepos, total));
76
- }
77
- const maxCount = Math.ceil(total * maxFraction);
78
- if (filtered.length > maxCount) {
79
- filtered = filtered.slice(0, maxCount);
80
- }
81
- if (filtered.length === 0) {
82
- filtered = scored.slice(0, Math.min(5, total));
83
- }
84
- const resultScores = /* @__PURE__ */ new Map();
85
- for (const s of filtered) {
86
- resultScores.set(s.repoName, s.score);
87
- }
88
- return {
89
- repos: filtered.map((s) => s.repoName),
90
- scores: resultScores,
91
- strategy: "filtered"
92
- };
93
- }
94
- class QueryRouter {
95
- profileEmbeddings = [];
96
- embedder;
97
- initialized = false;
98
- constructor(embedder) {
99
- this.embedder = embedder;
100
- }
101
- /**
102
- * Initialize with profiles. Embeds all profiles (one-time cost).
103
- */
104
- async init(profiles) {
105
- this.profileEmbeddings = await embedProfiles(profiles, this.embedder);
106
- this.initialized = true;
107
- }
108
- /**
109
- * Load cached embeddings from disk (skip re-embedding).
110
- * Returns true if cache was loaded successfully.
111
- */
112
- loadCached(cachePath) {
113
- const filePath = join(cachePath, "profile_embeddings.json");
114
- if (!existsSync(filePath)) return false;
115
- try {
116
- const raw = readFileSync(filePath, "utf-8");
117
- const data = JSON.parse(raw);
118
- if (!Array.isArray(data) || data.length === 0) return false;
119
- const first = data[0];
120
- if (typeof first.repoName !== "string" || !Array.isArray(first.embedding) || typeof first.textUsed !== "string") {
121
- return false;
122
- }
123
- this.profileEmbeddings = data;
124
- this.initialized = true;
125
- return true;
126
- } catch {
127
- return false;
128
- }
129
- }
130
- /**
131
- * Save embeddings to disk for future use.
132
- */
133
- saveCached(cachePath) {
134
- const filePath = join(cachePath, "profile_embeddings.json");
135
- const dir = dirname(filePath);
136
- if (!existsSync(dir)) {
137
- mkdirSync(dir, { recursive: true });
138
- }
139
- writeFileSync(filePath, JSON.stringify(this.profileEmbeddings, null, 2), "utf-8");
140
- }
141
- /**
142
- * Route a query to relevant repos.
143
- */
144
- async route(query, opts) {
145
- if (!this.initialized) {
146
- return { repos: [], scores: /* @__PURE__ */ new Map(), strategy: "all" };
147
- }
148
- return routeQuery(query, this.profileEmbeddings, this.embedder, opts);
149
- }
150
- /**
151
- * Get all repo names (for when routing is not needed).
152
- */
153
- getAllRepos() {
154
- return this.profileEmbeddings.map((pe) => pe.repoName);
155
- }
156
- /**
157
- * Check whether the router has been initialized.
158
- */
159
- isReady() {
160
- return this.initialized;
161
- }
162
- /**
163
- * Number of repos loaded.
164
- */
165
- get repoCount() {
166
- return this.profileEmbeddings.length;
167
- }
168
- }
169
- async function createQueryRouter(project, embedder) {
170
- const kbPath = getKnowledgeBasePath(project);
171
- if (!existsSync(kbPath)) return null;
172
- const profiles = [];
173
- let entries;
174
- try {
175
- entries = readdirSync(kbPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
176
- } catch {
177
- return null;
178
- }
179
- for (const repoDir of entries) {
180
- const profilePath = join(kbPath, repoDir, "profile.json");
181
- if (!existsSync(profilePath)) continue;
182
- try {
183
- const raw = readFileSync(profilePath, "utf-8");
184
- const profile = JSON.parse(raw);
185
- if (profile.name && profile.role) {
186
- profiles.push(profile);
187
- }
188
- } catch {
189
- }
190
- }
191
- if (profiles.length === 0) return null;
192
- const router = new QueryRouter(embedder);
193
- if (router.loadCached(kbPath)) {
194
- const cachedRepos = new Set(router.getAllRepos());
195
- const currentRepos = new Set(profiles.map((p) => p.name));
196
- const cacheValid = cachedRepos.size === currentRepos.size && Array.from(currentRepos).every((r) => cachedRepos.has(r));
197
- if (cacheValid) {
198
- return router;
199
- }
200
- }
201
- await router.init(profiles);
202
- router.saveCached(kbPath);
203
- return router;
204
- }
205
- export {
206
- QueryRouter,
207
- createQueryRouter,
208
- embedProfiles,
209
- routeQuery
210
- };