maven-indexer-mcp 1.0.4 → 1.0.5
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 +17 -7
- package/build/config.js +13 -0
- package/build/index.js +60 -15
- package/build/indexer.js +125 -10
- package/build/reproduce_issue.js +20 -0
- package/build/source_parser.js +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Maven Indexer MCP Server
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and
|
|
3
|
+
A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and Gradle cache (`~/.gradle/caches/modules-2/files-2.1`) to provide AI agents with tools to search for Java classes, method signatures, and source code.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ A Model Context Protocol (MCP) server that indexes your local Maven repository (
|
|
|
8
8
|
* **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
|
|
9
9
|
* **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
|
|
10
10
|
* **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
|
|
11
|
-
* **Real-time Monitoring**: Watches the
|
|
11
|
+
* **Real-time Monitoring**: Watches the repositories for changes (e.g., new `mvn install`) and automatically updates the index.
|
|
12
12
|
* **Efficient Persistence**: Uses SQLite to store the index, handling large repositories with minimal memory footprint.
|
|
13
13
|
|
|
14
14
|
## Getting Started
|
|
@@ -26,13 +26,14 @@ Add the following config to your MCP client:
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
This will automatically download and run the latest version of the server. It will auto-detect your Maven repository location (usually `~/.m2/repository`).
|
|
29
|
+
This will automatically download and run the latest version of the server. It will auto-detect your Maven repository location (usually `~/.m2/repository`) and Gradle cache.
|
|
30
30
|
|
|
31
31
|
### Configuration (Optional)
|
|
32
32
|
|
|
33
33
|
If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to the configuration:
|
|
34
34
|
|
|
35
35
|
* **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if your repository is in a non-standard location.
|
|
36
|
+
* **`GRADLE_REPO_PATH`**: Absolute path to your Gradle cache (e.g., `/Users/yourname/.gradle/caches/modules-2/files-2.1`).
|
|
36
37
|
* **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
|
|
37
38
|
* **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server will attempt to use its bundled CFR version.
|
|
38
39
|
|
|
@@ -46,6 +47,7 @@ Example with optional configuration:
|
|
|
46
47
|
"args": ["-y", "maven-indexer-mcp@latest"],
|
|
47
48
|
"env": {
|
|
48
49
|
"MAVEN_REPO": "/Users/yourname/.m2/repository",
|
|
50
|
+
"GRADLE_REPO_PATH": "/Users/yourname/.gradle/caches/modules-2/files-2.1",
|
|
49
51
|
"INCLUDED_PACKAGES": "com.mycompany.*",
|
|
50
52
|
"MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
|
|
51
53
|
}
|
|
@@ -84,14 +86,22 @@ If you prefer to run from source:
|
|
|
84
86
|
|
|
85
87
|
## Available Tools
|
|
86
88
|
|
|
87
|
-
* **`search_classes`**: Search for Java classes.
|
|
89
|
+
* **`search_classes`**: Search for Java classes in the local Maven repository (dependencies).
|
|
90
|
+
* **WHEN TO USE**:
|
|
91
|
+
1. You cannot find a class definition in the current project source (it's likely a dependency).
|
|
92
|
+
2. You need to read the source code, method signatures, or Javadocs of an external library class.
|
|
93
|
+
3. You need to verify which version of a library class is being used.
|
|
94
|
+
* **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this class imported from?".
|
|
88
95
|
* Input: `className` (e.g., "StringUtils", "Json parser")
|
|
89
96
|
* Output: List of matching classes with their artifacts.
|
|
90
|
-
* **`get_class_details`**:
|
|
91
|
-
*
|
|
97
|
+
* **`get_class_details`**: Decompile and read the source code of external libraries/dependencies. **Use this instead of 'SearchCodebase' for classes that are imported but defined in JAR files.**
|
|
98
|
+
* **Key Value**: "Don't guess what the library does—read the code."
|
|
99
|
+
* **Tip**: When reviewing usages of an external class, use this to retrieve the class definition to understand the context fully.
|
|
100
|
+
* Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
|
|
92
101
|
* Output: Method signatures, Javadocs, or full source code.
|
|
102
|
+
* **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those with source code attached).
|
|
93
103
|
* **`search_artifacts`**: Search for artifacts by coordinate (groupId, artifactId).
|
|
94
|
-
* **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
|
|
104
|
+
* **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class. Useful for finding SPI implementations in external libraries.
|
|
95
105
|
* Input: `className` (e.g. "java.util.List")
|
|
96
106
|
* Output: List of implementation/subclass names and their artifacts.
|
|
97
107
|
* **`refresh_index`**: Trigger a re-scan of the Maven repository.
|
package/build/config.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
export class Config {
|
|
9
9
|
static instance;
|
|
10
10
|
localRepository = "";
|
|
11
|
+
gradleRepository = "";
|
|
11
12
|
javaBinary = "java";
|
|
12
13
|
includedPackages = ["*"];
|
|
13
14
|
cfrPath = null;
|
|
@@ -52,6 +53,18 @@ export class Config {
|
|
|
52
53
|
repoPath = path.join(os.homedir(), '.m2', 'repository');
|
|
53
54
|
}
|
|
54
55
|
this.localRepository = repoPath;
|
|
56
|
+
// Load Gradle Repository
|
|
57
|
+
let gradleRepoPath = null;
|
|
58
|
+
if (process.env.GRADLE_REPO_PATH) {
|
|
59
|
+
gradleRepoPath = process.env.GRADLE_REPO_PATH;
|
|
60
|
+
}
|
|
61
|
+
else if (process.env.GRADLE_USER_HOME) {
|
|
62
|
+
gradleRepoPath = path.join(process.env.GRADLE_USER_HOME, 'caches', 'modules-2', 'files-2.1');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
gradleRepoPath = path.join(os.homedir(), '.gradle', 'caches', 'modules-2', 'files-2.1');
|
|
66
|
+
}
|
|
67
|
+
this.gradleRepository = gradleRepoPath;
|
|
55
68
|
// Load Java Path
|
|
56
69
|
if (process.env.JAVA_HOME) {
|
|
57
70
|
this.javaBinary = path.join(process.env.JAVA_HOME, 'bin', 'java');
|
package/build/index.js
CHANGED
|
@@ -30,7 +30,7 @@ server.registerTool("search_artifacts", {
|
|
|
30
30
|
// Limit results to avoid overflow
|
|
31
31
|
const limitedMatches = matches.slice(0, 20);
|
|
32
32
|
const text = limitedMatches.length > 0
|
|
33
|
-
? limitedMatches.map(a =>
|
|
33
|
+
? limitedMatches.map(a => `${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
|
|
34
34
|
: "No artifacts found matching the query.";
|
|
35
35
|
return {
|
|
36
36
|
content: [
|
|
@@ -42,7 +42,7 @@ server.registerTool("search_artifacts", {
|
|
|
42
42
|
};
|
|
43
43
|
});
|
|
44
44
|
server.registerTool("search_classes", {
|
|
45
|
-
description: "Search for Java classes in the local Maven repository.
|
|
45
|
+
description: "Search for Java classes in the local Maven repository. WHEN TO USE: 1. You cannot find a class definition in the current project source (it's likely a dependency). 2. You need to read the source code, method signatures, or Javadocs of an external library class. 3. You need to verify which version of a library class is being used. Examples: 'Show me the source of StringUtils', 'What methods are available on DateTimeUtils?', 'Where is this class imported from?'.",
|
|
46
46
|
inputSchema: z.object({
|
|
47
47
|
className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
48
48
|
}),
|
|
@@ -51,7 +51,7 @@ server.registerTool("search_classes", {
|
|
|
51
51
|
const text = matches.length > 0
|
|
52
52
|
? matches.map(m => {
|
|
53
53
|
// Group by artifact ID to allow easy selection
|
|
54
|
-
const artifacts = m.artifacts.slice(0, 5).map(a =>
|
|
54
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
|
|
55
55
|
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
56
56
|
return `Class: ${m.className}\n ${artifacts}${more}`;
|
|
57
57
|
}).join("\n\n")
|
|
@@ -61,7 +61,7 @@ server.registerTool("search_classes", {
|
|
|
61
61
|
};
|
|
62
62
|
});
|
|
63
63
|
server.registerTool("search_implementations", {
|
|
64
|
-
description: "Search for classes that implement a specific interface or extend a specific class. This is useful for finding implementations of SPIs or base classes.",
|
|
64
|
+
description: "Search for classes that implement a specific interface or extend a specific class. This is useful for finding implementations of SPIs or base classes, especially in external libraries.",
|
|
65
65
|
inputSchema: z.object({
|
|
66
66
|
className: z.string().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
|
|
67
67
|
}),
|
|
@@ -69,7 +69,7 @@ server.registerTool("search_implementations", {
|
|
|
69
69
|
const matches = indexer.searchImplementations(className);
|
|
70
70
|
const text = matches.length > 0
|
|
71
71
|
? matches.map(m => {
|
|
72
|
-
const artifacts = m.artifacts.slice(0, 5).map(a =>
|
|
72
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}`).join("\n ");
|
|
73
73
|
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
74
74
|
return `Implementation: ${m.className}\n ${artifacts}${more}`;
|
|
75
75
|
}).join("\n\n")
|
|
@@ -79,17 +79,54 @@ server.registerTool("search_implementations", {
|
|
|
79
79
|
};
|
|
80
80
|
});
|
|
81
81
|
server.registerTool("get_class_details", {
|
|
82
|
-
description: "
|
|
82
|
+
description: "Decompile and read the source code of external libraries/dependencies. Use this instead of 'SearchCodebase' for classes that are imported but defined in JAR files. Returns method signatures, Javadocs, or full source. Essential for verifying implementation details during refactoring. Don't guess what the library does—read the code. When reviewing usages of an external class, use this to retrieve the class definition to understand the context fully.",
|
|
83
83
|
inputSchema: z.object({
|
|
84
84
|
className: z.string().describe("Fully qualified class name"),
|
|
85
|
-
|
|
85
|
+
coordinate: z.string().optional().describe("The Maven coordinate of the artifact (groupId:artifactId:version). Optional: if not provided, the tool will automatically find the best match (preferring artifacts with source code)."),
|
|
86
86
|
type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
|
|
87
87
|
}),
|
|
88
|
-
}, async ({ className,
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
88
|
+
}, async ({ className, coordinate, type }) => {
|
|
89
|
+
let targetArtifact;
|
|
90
|
+
if (coordinate) {
|
|
91
|
+
const parts = coordinate.split(':');
|
|
92
|
+
if (parts.length === 3) {
|
|
93
|
+
targetArtifact = indexer.getArtifactByCoordinate(parts[0], parts[1], parts[2]);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return { content: [{ type: "text", text: "Invalid coordinate format. Expected groupId:artifactId:version" }] };
|
|
97
|
+
}
|
|
98
|
+
if (!targetArtifact) {
|
|
99
|
+
return { content: [{ type: "text", text: `Artifact ${coordinate} not found in index.` }] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Auto-resolve artifact if coordinate is missing
|
|
104
|
+
const matches = indexer.searchClass(className);
|
|
105
|
+
// Find exact match for class name
|
|
106
|
+
const exactMatch = matches.find(m => m.className === className);
|
|
107
|
+
if (!exactMatch) {
|
|
108
|
+
// If no exact match, but we have some results, list them
|
|
109
|
+
if (matches.length > 0) {
|
|
110
|
+
const suggestions = matches.map(m => `- ${m.className}`).join("\n");
|
|
111
|
+
return { content: [{ type: "text", text: `Class '${className}' not found exactly. Did you mean:\n${suggestions}` }] };
|
|
112
|
+
}
|
|
113
|
+
return { content: [{ type: "text", text: `Class '${className}' not found in the index. Try 'search_classes' with a keyword if you are unsure of the full name.` }] };
|
|
114
|
+
}
|
|
115
|
+
// We have an exact match, choose the best artifact
|
|
116
|
+
// Strategy: 1. Prefer hasSource=true. 2. Prefer highest ID (likely newest).
|
|
117
|
+
const artifacts = exactMatch.artifacts.sort((a, b) => {
|
|
118
|
+
if (a.hasSource !== b.hasSource) {
|
|
119
|
+
return a.hasSource ? -1 : 1; // source comes first
|
|
120
|
+
}
|
|
121
|
+
return b.id - a.id; // higher ID comes first
|
|
122
|
+
});
|
|
123
|
+
if (artifacts.length === 0) {
|
|
124
|
+
return { content: [{ type: "text", text: `Class '${className}' found but no artifacts are associated with it (database inconsistency).` }] };
|
|
125
|
+
}
|
|
126
|
+
targetArtifact = artifacts[0];
|
|
127
|
+
// console.error(`Auto-resolved ${className} to artifact ${artifacts[0].groupId}:${artifacts[0].artifactId}:${artifacts[0].version} (ID: ${targetArtifact.id})`);
|
|
92
128
|
}
|
|
129
|
+
const artifact = targetArtifact;
|
|
93
130
|
let detail = null;
|
|
94
131
|
let usedDecompilation = false;
|
|
95
132
|
let lastError = "";
|
|
@@ -107,7 +144,10 @@ server.registerTool("get_class_details", {
|
|
|
107
144
|
}
|
|
108
145
|
// If not found in source jar (or no source jar), try main jar (decompilation)
|
|
109
146
|
if (!detail) {
|
|
110
|
-
|
|
147
|
+
let mainJarPath = artifact.abspath;
|
|
148
|
+
if (!mainJarPath.endsWith('.jar')) {
|
|
149
|
+
mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
150
|
+
}
|
|
111
151
|
try {
|
|
112
152
|
// SourceParser will try to decompile if source file not found in jar
|
|
113
153
|
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
@@ -123,7 +163,10 @@ server.registerTool("get_class_details", {
|
|
|
123
163
|
}
|
|
124
164
|
else {
|
|
125
165
|
// Signatures -> Use Main JAR
|
|
126
|
-
|
|
166
|
+
let mainJarPath = artifact.abspath;
|
|
167
|
+
if (!mainJarPath.endsWith('.jar')) {
|
|
168
|
+
mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
169
|
+
}
|
|
127
170
|
try {
|
|
128
171
|
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
129
172
|
}
|
|
@@ -137,12 +180,14 @@ server.registerTool("get_class_details", {
|
|
|
137
180
|
const errorMsg = lastError ? `\nLast error: ${lastError}` : "";
|
|
138
181
|
return { content: [{ type: "text", text: `Class ${className} not found in artifact ${artifact.artifactId}. \nDebug info: ${debugInfo}${errorMsg}` }] };
|
|
139
182
|
}
|
|
140
|
-
let resultText = `Class: ${detail.className}\n
|
|
183
|
+
let resultText = `Class: ${detail.className}\n`;
|
|
184
|
+
resultText += `Artifact: ${artifact.groupId}:${artifact.artifactId}:${artifact.version}\n\n`; // Inform user which artifact was used
|
|
141
185
|
if (usedDecompilation) {
|
|
142
186
|
resultText += "*Source code decompiled from binary class file.*\n\n";
|
|
143
187
|
}
|
|
144
188
|
if (type === 'source') {
|
|
145
|
-
|
|
189
|
+
const lang = detail.language || 'java';
|
|
190
|
+
resultText += "```" + lang + "\n" + detail.source + "\n```";
|
|
146
191
|
}
|
|
147
192
|
else {
|
|
148
193
|
if (detail.doc) {
|
package/build/indexer.js
CHANGED
|
@@ -28,16 +28,22 @@ export class Indexer {
|
|
|
28
28
|
*/
|
|
29
29
|
async startWatch() {
|
|
30
30
|
const config = await Config.getInstance();
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
|
|
31
|
+
const pathsToWatch = [];
|
|
32
|
+
if (config.localRepository && fsSync.existsSync(config.localRepository)) {
|
|
33
|
+
pathsToWatch.push(config.localRepository);
|
|
34
|
+
}
|
|
35
|
+
if (config.gradleRepository && fsSync.existsSync(config.gradleRepository)) {
|
|
36
|
+
pathsToWatch.push(config.gradleRepository);
|
|
37
|
+
}
|
|
38
|
+
if (pathsToWatch.length === 0) {
|
|
39
|
+
console.error("No repository paths found, skipping watch mode.");
|
|
34
40
|
return;
|
|
35
41
|
}
|
|
36
42
|
if (this.watcher) {
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
|
-
console.error(`Starting file watcher on ${
|
|
40
|
-
this.watcher = chokidar.watch(
|
|
45
|
+
console.error(`Starting file watcher on ${pathsToWatch.join(', ')}...`);
|
|
46
|
+
this.watcher = chokidar.watch(pathsToWatch, {
|
|
41
47
|
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
42
48
|
persistent: true,
|
|
43
49
|
ignoreInitial: true,
|
|
@@ -88,15 +94,28 @@ export class Indexer {
|
|
|
88
94
|
try {
|
|
89
95
|
const config = await Config.getInstance();
|
|
90
96
|
const repoPath = config.localRepository;
|
|
97
|
+
const gradleRepoPath = config.gradleRepository;
|
|
91
98
|
const db = DB.getInstance();
|
|
92
|
-
if (!repoPath) {
|
|
93
|
-
console.error("No
|
|
99
|
+
if (!repoPath && !gradleRepoPath) {
|
|
100
|
+
console.error("No repository path found.");
|
|
94
101
|
return;
|
|
95
102
|
}
|
|
96
103
|
// 1. Scan for artifacts
|
|
97
104
|
console.error("Scanning repository structure...");
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
let artifacts = [];
|
|
106
|
+
if (repoPath && fsSync.existsSync(repoPath)) {
|
|
107
|
+
console.error(`Scanning Maven repo: ${repoPath}`);
|
|
108
|
+
const mavenArtifacts = await this.scanRepository(repoPath);
|
|
109
|
+
console.error(`Found ${mavenArtifacts.length} Maven artifacts.`);
|
|
110
|
+
artifacts = artifacts.concat(mavenArtifacts);
|
|
111
|
+
}
|
|
112
|
+
if (gradleRepoPath && fsSync.existsSync(gradleRepoPath)) {
|
|
113
|
+
console.error(`Scanning Gradle repo: ${gradleRepoPath}`);
|
|
114
|
+
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath);
|
|
115
|
+
console.error(`Found ${gradleArtifacts.length} Gradle artifacts.`);
|
|
116
|
+
artifacts = artifacts.concat(gradleArtifacts);
|
|
117
|
+
}
|
|
118
|
+
console.error(`Found ${artifacts.length} total artifacts on disk.`);
|
|
100
119
|
// 2. Persist artifacts and determine what needs indexing
|
|
101
120
|
// We use is_indexed = 0 for new artifacts.
|
|
102
121
|
const insertArtifact = db.prepare(`
|
|
@@ -197,12 +216,86 @@ export class Indexer {
|
|
|
197
216
|
await scanDir(rootDir);
|
|
198
217
|
return results;
|
|
199
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Scans a Gradle cache directory for artifacts.
|
|
221
|
+
* Structure: group/artifact/version/hash/file
|
|
222
|
+
*/
|
|
223
|
+
async scanGradleRepository(rootDir) {
|
|
224
|
+
const results = [];
|
|
225
|
+
// Helper to read directory safely
|
|
226
|
+
const readDirSafe = async (p) => {
|
|
227
|
+
try {
|
|
228
|
+
return await fs.readdir(p, { withFileTypes: true });
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const groupDirs = await readDirSafe(rootDir);
|
|
235
|
+
for (const groupEntry of groupDirs) {
|
|
236
|
+
if (!groupEntry.isDirectory())
|
|
237
|
+
continue;
|
|
238
|
+
const groupId = groupEntry.name;
|
|
239
|
+
const groupPath = path.join(rootDir, groupId);
|
|
240
|
+
const artifactDirs = await readDirSafe(groupPath);
|
|
241
|
+
for (const artifactEntry of artifactDirs) {
|
|
242
|
+
if (!artifactEntry.isDirectory())
|
|
243
|
+
continue;
|
|
244
|
+
const artifactId = artifactEntry.name;
|
|
245
|
+
const artifactPath = path.join(groupPath, artifactId);
|
|
246
|
+
const versionDirs = await readDirSafe(artifactPath);
|
|
247
|
+
for (const versionEntry of versionDirs) {
|
|
248
|
+
if (!versionEntry.isDirectory())
|
|
249
|
+
continue;
|
|
250
|
+
const version = versionEntry.name;
|
|
251
|
+
const versionPath = path.join(artifactPath, version);
|
|
252
|
+
const hashDirs = await readDirSafe(versionPath);
|
|
253
|
+
let jarPath = null;
|
|
254
|
+
let hasSource = false;
|
|
255
|
+
// We need to iterate all hash dirs to find the jar and source jar
|
|
256
|
+
for (const hashEntry of hashDirs) {
|
|
257
|
+
if (!hashEntry.isDirectory())
|
|
258
|
+
continue;
|
|
259
|
+
const hashPath = path.join(versionPath, hashEntry.name);
|
|
260
|
+
const files = await readDirSafe(hashPath);
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
if (file.isFile()) {
|
|
263
|
+
if (file.name.endsWith('.jar')) {
|
|
264
|
+
if (file.name.endsWith('-sources.jar')) {
|
|
265
|
+
hasSource = true;
|
|
266
|
+
}
|
|
267
|
+
else if (!file.name.endsWith('-javadoc.jar')) {
|
|
268
|
+
// This should be the main jar
|
|
269
|
+
jarPath = path.join(hashPath, file.name);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (jarPath) {
|
|
276
|
+
results.push({
|
|
277
|
+
id: 0, // Placeholder
|
|
278
|
+
groupId,
|
|
279
|
+
artifactId,
|
|
280
|
+
version,
|
|
281
|
+
abspath: jarPath, // Full path to JAR
|
|
282
|
+
hasSource
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
200
290
|
/**
|
|
201
291
|
* Extracts classes from the artifact's JAR and indexes them.
|
|
202
292
|
* Updates the 'is_indexed' flag upon completion.
|
|
203
293
|
*/
|
|
204
294
|
async indexArtifactClasses(artifact) {
|
|
205
|
-
|
|
295
|
+
let jarPath = artifact.abspath;
|
|
296
|
+
if (!jarPath.endsWith('.jar')) {
|
|
297
|
+
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
298
|
+
}
|
|
206
299
|
const db = DB.getInstance();
|
|
207
300
|
const config = await Config.getInstance();
|
|
208
301
|
try {
|
|
@@ -485,4 +578,26 @@ export class Indexer {
|
|
|
485
578
|
}
|
|
486
579
|
return undefined;
|
|
487
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Retrieves an artifact by its Maven coordinate.
|
|
583
|
+
*/
|
|
584
|
+
getArtifactByCoordinate(groupId, artifactId, version) {
|
|
585
|
+
const db = DB.getInstance();
|
|
586
|
+
const row = db.prepare(`
|
|
587
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
588
|
+
FROM artifacts
|
|
589
|
+
WHERE group_id = ? AND artifact_id = ? AND version = ?
|
|
590
|
+
`).get(groupId, artifactId, version);
|
|
591
|
+
if (row) {
|
|
592
|
+
return {
|
|
593
|
+
id: row.id,
|
|
594
|
+
groupId: row.groupId,
|
|
595
|
+
artifactId: row.artifactId,
|
|
596
|
+
version: row.version,
|
|
597
|
+
abspath: row.abspath,
|
|
598
|
+
hasSource: Boolean(row.hasSource)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
488
603
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Indexer } from "./indexer.js";
|
|
2
|
+
import { DB } from "./db/index.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const db = DB.getInstance();
|
|
5
|
+
const indexer = Indexer.getInstance();
|
|
6
|
+
// We assume indexing is done since we just ran it.
|
|
7
|
+
console.log("Searching for 'Kotlin'...");
|
|
8
|
+
const kotlinResults = db.prepare("SELECT class_name FROM classes_fts WHERE class_name LIKE '%Kotlin%' LIMIT 10").all();
|
|
9
|
+
console.log("First 10 Kotlin classes:", kotlinResults);
|
|
10
|
+
console.log("Searching for 'AnAction'...");
|
|
11
|
+
const anActionResults = indexer.searchClass("AnAction");
|
|
12
|
+
console.log(`Found ${anActionResults.length} results for 'AnAction'.`);
|
|
13
|
+
if (anActionResults.length > 0) {
|
|
14
|
+
console.log(anActionResults[0]);
|
|
15
|
+
}
|
|
16
|
+
console.log("Searching for 'StringUtils'...");
|
|
17
|
+
const stringUtilsResults = indexer.searchClass("StringUtils");
|
|
18
|
+
console.log(`Found ${stringUtilsResults.length} results for 'StringUtils'.`);
|
|
19
|
+
}
|
|
20
|
+
main().catch(console.error);
|
package/build/source_parser.js
CHANGED
|
@@ -28,6 +28,7 @@ export class SourceParser {
|
|
|
28
28
|
zipfile.on('entry', (entry) => {
|
|
29
29
|
if (candidates.includes(entry.fileName)) {
|
|
30
30
|
found = true;
|
|
31
|
+
const language = entry.fileName.endsWith('.kt') ? 'kotlin' : 'java';
|
|
31
32
|
zipfile.openReadStream(entry, (err, readStream) => {
|
|
32
33
|
if (err || !readStream) {
|
|
33
34
|
resolve(null);
|
|
@@ -37,7 +38,7 @@ export class SourceParser {
|
|
|
37
38
|
readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
38
39
|
readStream.on('end', () => {
|
|
39
40
|
const source = Buffer.concat(chunks).toString('utf-8');
|
|
40
|
-
resolve(SourceParser.parse(className, source, type));
|
|
41
|
+
resolve(SourceParser.parse(className, source, type, language));
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
}
|
|
@@ -81,11 +82,12 @@ export class SourceParser {
|
|
|
81
82
|
throw new Error(`CFR stderr: ${stderr}`);
|
|
82
83
|
}
|
|
83
84
|
if (stdout) {
|
|
84
|
-
return this.parse(className, stdout, type);
|
|
85
|
+
return this.parse(className, stdout, type, 'java');
|
|
85
86
|
}
|
|
86
87
|
return {
|
|
87
88
|
className,
|
|
88
|
-
source: stdout // Return as source
|
|
89
|
+
source: stdout, // Return as source
|
|
90
|
+
language: 'java'
|
|
89
91
|
};
|
|
90
92
|
}
|
|
91
93
|
catch (e) {
|
|
@@ -108,7 +110,8 @@ export class SourceParser {
|
|
|
108
110
|
l !== '}');
|
|
109
111
|
return {
|
|
110
112
|
className,
|
|
111
|
-
signatures: lines
|
|
113
|
+
signatures: lines,
|
|
114
|
+
language: 'java'
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
117
|
catch (e) {
|
|
@@ -118,9 +121,9 @@ export class SourceParser {
|
|
|
118
121
|
return null;
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
|
-
static parse(className, source, type) {
|
|
124
|
+
static parse(className, source, type, language = 'java') {
|
|
122
125
|
if (type === 'source') {
|
|
123
|
-
return { className, source };
|
|
126
|
+
return { className, source, language };
|
|
124
127
|
}
|
|
125
128
|
// Very simple regex-based parsing to extract methods and javadocs
|
|
126
129
|
// This is heuristic and won't be perfect, but it's fast and dependency-free
|
|
@@ -169,7 +172,8 @@ export class SourceParser {
|
|
|
169
172
|
return {
|
|
170
173
|
className,
|
|
171
174
|
signatures,
|
|
172
|
-
doc: type === 'docs' ? allDocs.join('\n\n') : undefined
|
|
175
|
+
doc: type === 'docs' ? allDocs.join('\n\n') : undefined,
|
|
176
|
+
language
|
|
173
177
|
};
|
|
174
178
|
}
|
|
175
179
|
}
|