semantic-code-mcp 2.0.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 +22 -0
- package/README.md +259 -0
- package/config.json +85 -0
- package/features/check-last-version.js +504 -0
- package/features/clear-cache.js +75 -0
- package/features/get-status.js +210 -0
- package/features/hybrid-search.js +189 -0
- package/features/index-codebase.js +999 -0
- package/features/set-workspace.js +183 -0
- package/index.js +297 -0
- package/lib/ast-chunker.js +273 -0
- package/lib/cache-factory.js +13 -0
- package/lib/cache.js +157 -0
- package/lib/config.js +1296 -0
- package/lib/embedding-worker.js +155 -0
- package/lib/gemini-embedder.js +351 -0
- package/lib/ignore-patterns.js +896 -0
- package/lib/milvus-cache.js +478 -0
- package/lib/mrl-embedder.js +235 -0
- package/lib/project-detector.js +75 -0
- package/lib/resource-throttle.js +85 -0
- package/lib/sqlite-cache.js +468 -0
- package/lib/tokenizer.js +149 -0
- package/lib/utils.js +214 -0
- package/package.json +70 -0
- package/reindex.js +109 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// Using native global fetch (Node.js 18+)
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version checker for package registries across multiple ecosystems.
|
|
5
|
+
* Supports caching and automatic retry for transient failures.
|
|
6
|
+
*/
|
|
7
|
+
export class VersionChecker {
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new VersionChecker instance.
|
|
10
|
+
* @param {Object} config - Configuration object
|
|
11
|
+
* @param {number} [config.versionCheckTimeout=10000] - Timeout for API requests in ms
|
|
12
|
+
* @param {number} [config.versionCacheTTL=300000] - Cache TTL in ms (default: 5 minutes)
|
|
13
|
+
* @param {number} [config.retryAttempts=1] - Number of retry attempts for failed requests
|
|
14
|
+
* @param {number} [config.retryDelay=500] - Delay between retries in ms
|
|
15
|
+
*/
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.timeout = config.versionCheckTimeout || 10000; // 10 seconds for slow APIs
|
|
19
|
+
this.cacheTTL = config.versionCacheTTL || 300000; // 5 minutes
|
|
20
|
+
this.retryAttempts = config.retryAttempts ?? 1;
|
|
21
|
+
this.retryDelay = config.retryDelay || 500;
|
|
22
|
+
|
|
23
|
+
// Simple in-memory cache: Map<cacheKey, { data, timestamp }>
|
|
24
|
+
this.cache = new Map();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks the latest version of a package from its registry.
|
|
29
|
+
* @param {string} packageName - Package name (may include ecosystem prefix like "npm:")
|
|
30
|
+
* @param {string} [ecosystem] - Ecosystem identifier (auto-detected if not provided)
|
|
31
|
+
* @returns {Promise<Object>} Result object with package, ecosystem, version, found, source, or error
|
|
32
|
+
*/
|
|
33
|
+
async checkVersion(packageName, ecosystem) {
|
|
34
|
+
try {
|
|
35
|
+
// 1. Sanitization
|
|
36
|
+
if (!packageName || typeof packageName !== 'string') {
|
|
37
|
+
throw new Error("Invalid package name");
|
|
38
|
+
}
|
|
39
|
+
packageName = packageName.trim();
|
|
40
|
+
|
|
41
|
+
// 2. Auto-detect
|
|
42
|
+
if (!ecosystem) {
|
|
43
|
+
ecosystem = this._detectEcosystem(packageName);
|
|
44
|
+
} else {
|
|
45
|
+
ecosystem = ecosystem.toLowerCase().trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Check cache
|
|
49
|
+
const cacheKey = `${ecosystem || 'unknown'}:${packageName}`;
|
|
50
|
+
const cached = this._getFromCache(cacheKey);
|
|
51
|
+
if (cached) {
|
|
52
|
+
return { ...cached, fromCache: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 4. Fetch
|
|
56
|
+
const result = await this._fetchVersion(packageName, ecosystem);
|
|
57
|
+
const response = {
|
|
58
|
+
package: packageName,
|
|
59
|
+
ecosystem: ecosystem || "unknown",
|
|
60
|
+
...result
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 5. Cache successful results
|
|
64
|
+
if (result.found) {
|
|
65
|
+
this._setCache(cacheKey, response);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return response;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
package: packageName,
|
|
72
|
+
ecosystem: ecosystem || "unknown",
|
|
73
|
+
error: error.message,
|
|
74
|
+
found: false,
|
|
75
|
+
message: `Failed to check version: ${error.message}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets a value from cache if it exists and hasn't expired.
|
|
82
|
+
* @param {string} key - Cache key
|
|
83
|
+
* @returns {Object|null} Cached data or null
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
_getFromCache(key) {
|
|
87
|
+
const entry = this.cache.get(key);
|
|
88
|
+
if (!entry) return null;
|
|
89
|
+
|
|
90
|
+
if (Date.now() - entry.timestamp > this.cacheTTL) {
|
|
91
|
+
this.cache.delete(key);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return entry.data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sets a value in the cache.
|
|
99
|
+
* @param {string} key - Cache key
|
|
100
|
+
* @param {Object} data - Data to cache
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
_setCache(key, data) {
|
|
104
|
+
this.cache.set(key, { data, timestamp: Date.now() });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clears the version cache.
|
|
109
|
+
*/
|
|
110
|
+
clearCache() {
|
|
111
|
+
this.cache.clear();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Detects the ecosystem from package name prefixes.
|
|
116
|
+
* @param {string} packageName - Package name with optional prefix
|
|
117
|
+
* @returns {string|null} Detected ecosystem or null
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
_detectEcosystem(packageName) {
|
|
121
|
+
// Check specific prefixes first (order matters - more specific before generic)
|
|
122
|
+
if (packageName.startsWith("npm:")) return "npm";
|
|
123
|
+
if (packageName.startsWith("pip:")) return "pypi";
|
|
124
|
+
if (packageName.startsWith("go:")) return "go";
|
|
125
|
+
if (packageName.startsWith("cargo:")) return "crates";
|
|
126
|
+
if (packageName.startsWith("gem:")) return "rubygems";
|
|
127
|
+
if (packageName.startsWith("composer:")) return "packagist";
|
|
128
|
+
if (packageName.startsWith("nuget:")) return "nuget";
|
|
129
|
+
if (packageName.startsWith("pod:")) return "cocoapods";
|
|
130
|
+
if (packageName.startsWith("hex:")) return "hex";
|
|
131
|
+
|
|
132
|
+
// R ecosystem
|
|
133
|
+
if (packageName.startsWith("cran:") || packageName.startsWith("R:")) return "cran";
|
|
134
|
+
// Perl ecosystem
|
|
135
|
+
if (packageName.startsWith("cpan:") || packageName.startsWith("perl:")) return "cpan";
|
|
136
|
+
// Dart ecosystem
|
|
137
|
+
if (packageName.startsWith("dart:") || packageName.startsWith("pub:")) return "pub";
|
|
138
|
+
|
|
139
|
+
// Homebrew
|
|
140
|
+
if (packageName.startsWith("brew:")) return "homebrew";
|
|
141
|
+
// Conda
|
|
142
|
+
if (packageName.startsWith("conda:")) return "conda";
|
|
143
|
+
// Clojars (Clojure)
|
|
144
|
+
if (packageName.startsWith("clojars:") || packageName.startsWith("clj:")) return "clojars";
|
|
145
|
+
// Hackage (Haskell)
|
|
146
|
+
if (packageName.startsWith("hackage:") || packageName.startsWith("haskell:")) return "hackage";
|
|
147
|
+
// Julia
|
|
148
|
+
if (packageName.startsWith("julia:") || packageName.startsWith("jl:")) return "julia";
|
|
149
|
+
// Swift Package Manager
|
|
150
|
+
if (packageName.startsWith("swift:") || packageName.startsWith("spm:")) return "swift";
|
|
151
|
+
// Chocolatey (Windows)
|
|
152
|
+
if (packageName.startsWith("choco:")) return "chocolatey";
|
|
153
|
+
|
|
154
|
+
// Check for Maven patterns last (generic colon for group:artifact or explicit mvn:)
|
|
155
|
+
if (packageName.startsWith("mvn:") || packageName.includes(":")) return "maven";
|
|
156
|
+
|
|
157
|
+
// Default to npm for unprefixed package names (most common use case)
|
|
158
|
+
return "npm";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Fetches with automatic retry for transient failures.
|
|
163
|
+
* @param {string} url - URL to fetch
|
|
164
|
+
* @param {Object} options - Fetch options
|
|
165
|
+
* @param {number} [retriesLeft] - Number of retries remaining
|
|
166
|
+
* @returns {Promise<Response>} Fetch response
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
async _fetchWithRetry(url, options, retriesLeft = this.retryAttempts) {
|
|
170
|
+
try {
|
|
171
|
+
return await fetch(url, options);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (retriesLeft > 0) {
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
|
175
|
+
return this._fetchWithRetry(url, options, retriesLeft - 1);
|
|
176
|
+
}
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetches the latest version from the appropriate registry.
|
|
183
|
+
* @param {string} pkgInput - Package name (with or without prefix)
|
|
184
|
+
* @param {string} ecosystem - Target ecosystem
|
|
185
|
+
* @returns {Promise<Object>} Result with version info or error
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
async _fetchVersion(pkgInput, ecosystem) {
|
|
189
|
+
const signal = AbortSignal.timeout(this.timeout);
|
|
190
|
+
let url, version;
|
|
191
|
+
|
|
192
|
+
// Helper to handle simple JSON fetch with specific 404 message
|
|
193
|
+
const fetchJson = async (u, errorPrefix) => {
|
|
194
|
+
const res = await this._fetchWithRetry(u, { signal });
|
|
195
|
+
if (res.status === 404) throw new Error(`${errorPrefix} not found (404)`);
|
|
196
|
+
if (!res.ok) throw new Error(`${errorPrefix} registry error: ${res.status} ${res.statusText}`);
|
|
197
|
+
return res.json();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
switch (ecosystem) {
|
|
201
|
+
case "npm":
|
|
202
|
+
let npmPkg = pkgInput.replace("npm:", "");
|
|
203
|
+
if (npmPkg.startsWith("@") && npmPkg.includes("/")) {
|
|
204
|
+
npmPkg = npmPkg.replace("/", "%2f");
|
|
205
|
+
}
|
|
206
|
+
url = `https://registry.npmjs.org/${npmPkg}`;
|
|
207
|
+
const npmData = await fetchJson(url, "npm package");
|
|
208
|
+
version = npmData["dist-tags"]?.latest;
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
case "pypi":
|
|
212
|
+
url = `https://pypi.org/pypi/${pkgInput.replace("pip:", "")}/json`;
|
|
213
|
+
const pypiData = await fetchJson(url, "PyPI package");
|
|
214
|
+
version = pypiData.info?.version;
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case "packagist":
|
|
218
|
+
const packPkg = pkgInput.replace("composer:", "");
|
|
219
|
+
const parts = packPkg.split("/");
|
|
220
|
+
if (parts.length !== 2) throw new Error("Packagist package must be in 'vendor/package' format");
|
|
221
|
+
url = `https://repo.packagist.org/p2/${packPkg}.json`;
|
|
222
|
+
const packData = await fetchJson(url, "Packagist package");
|
|
223
|
+
const versions = packData.packages[packPkg];
|
|
224
|
+
if (!versions || versions.length === 0) throw new Error("No versions found in registry response");
|
|
225
|
+
version = versions[0].version;
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case "crates":
|
|
229
|
+
url = `https://crates.io/api/v1/crates/${pkgInput.replace("cargo:", "")}`;
|
|
230
|
+
const crateRes = await this._fetchWithRetry(url, { headers: { "User-Agent": "Smart-Coding-MCP" }, signal });
|
|
231
|
+
if (crateRes.status === 404) throw new Error("Crate not found (404)");
|
|
232
|
+
if (!crateRes.ok) throw new Error(`Crates.io error: ${crateRes.status}`);
|
|
233
|
+
const crateData = await crateRes.json();
|
|
234
|
+
version = crateData.crate?.max_stable_version || crateData.crate?.newest_version;
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case "maven":
|
|
238
|
+
const mvnPkg = pkgInput.replace("mvn:", "");
|
|
239
|
+
const [g, a] = mvnPkg.split(":");
|
|
240
|
+
if (!g || !a) throw new Error("Maven artifact must be in 'group:artifact' format");
|
|
241
|
+
url = `https://search.maven.org/solrsearch/select?q=g:"${g}"+AND+a:"${a}"&wt=json`;
|
|
242
|
+
const mvnRes = await this._fetchWithRetry(url, { signal });
|
|
243
|
+
if (!mvnRes.ok) throw new Error(`Maven Central error: ${mvnRes.status}`);
|
|
244
|
+
const mvnData = await mvnRes.json();
|
|
245
|
+
if (mvnData.response?.docs?.length > 0) {
|
|
246
|
+
version = mvnData.response.docs[0].latestVersion;
|
|
247
|
+
} else {
|
|
248
|
+
throw new Error("Maven artifact not found");
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case "go":
|
|
253
|
+
url = `https://proxy.golang.org/${pkgInput.replace("go:", "")}/@latest`;
|
|
254
|
+
const goData = await fetchJson(url, "Go module");
|
|
255
|
+
version = goData.Version;
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case "rubygems":
|
|
259
|
+
url = `https://rubygems.org/api/v1/versions/${pkgInput.replace("gem:", "")}/latest.json`;
|
|
260
|
+
const gemData = await fetchJson(url, "Gem");
|
|
261
|
+
version = gemData.version;
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case "nuget":
|
|
265
|
+
const nugetPkg = pkgInput.replace("nuget:", "").toLowerCase();
|
|
266
|
+
url = `https://api.nuget.org/v3-flatcontainer/${nugetPkg}/index.json`;
|
|
267
|
+
const nugetData = await fetchJson(url, "NuGet package");
|
|
268
|
+
if (nugetData.versions && nugetData.versions.length > 0) {
|
|
269
|
+
version = nugetData.versions[nugetData.versions.length - 1];
|
|
270
|
+
} else {
|
|
271
|
+
throw new Error("No versions found");
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case "cocoapods":
|
|
276
|
+
// CocoaPods trunk API requires special client authentication.
|
|
277
|
+
// The API returns pod metadata but version info requires parsing specs repo.
|
|
278
|
+
// We intentionally fall back to suggesting a web search for reliable results.
|
|
279
|
+
url = `https://trunk.cocoapods.org/api/v1/pods/${pkgInput.replace("pod:", "")}`;
|
|
280
|
+
const podRes = await this._fetchWithRetry(url, { signal });
|
|
281
|
+
if (podRes.ok) {
|
|
282
|
+
throw new Error("CocoaPods API requires client; please perform a web search.");
|
|
283
|
+
}
|
|
284
|
+
throw new Error("CocoaPods lookup not supported directly; please perform a web search.");
|
|
285
|
+
|
|
286
|
+
case "hex":
|
|
287
|
+
url = `https://hex.pm/api/packages/${pkgInput.replace("hex:", "")}`;
|
|
288
|
+
const hexData = await fetchJson(url, "Hex package");
|
|
289
|
+
if (hexData.releases && hexData.releases.length > 0) {
|
|
290
|
+
version = hexData.releases[0].version;
|
|
291
|
+
} else {
|
|
292
|
+
throw new Error("No releases found");
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case "cran":
|
|
297
|
+
// R packages from CRAN
|
|
298
|
+
const cranPkg = pkgInput.replace(/^R:|^cran:/i, "");
|
|
299
|
+
url = `https://crandb.r-pkg.org/${cranPkg}`;
|
|
300
|
+
const cranData = await fetchJson(url, "CRAN package");
|
|
301
|
+
version = cranData.Version;
|
|
302
|
+
break;
|
|
303
|
+
|
|
304
|
+
case "cpan":
|
|
305
|
+
// Perl modules from CPAN/MetaCPAN
|
|
306
|
+
const cpanPkg = pkgInput.replace(/^perl:|^cpan:/i, "");
|
|
307
|
+
url = `https://fastapi.metacpan.org/v1/module/${cpanPkg}`;
|
|
308
|
+
const cpanData = await fetchJson(url, "CPAN module");
|
|
309
|
+
version = cpanData.version;
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case "pub":
|
|
313
|
+
// Dart packages from pub.dev
|
|
314
|
+
const pubPkg = pkgInput.replace(/^dart:|^pub:/i, "");
|
|
315
|
+
url = `https://pub.dev/api/packages/${pubPkg}`;
|
|
316
|
+
const pubData = await fetchJson(url, "pub.dev package");
|
|
317
|
+
version = pubData.latest?.version;
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
case "homebrew":
|
|
321
|
+
// Homebrew formulae (macOS/Linux)
|
|
322
|
+
const brewPkg = pkgInput.replace(/^brew:/i, "");
|
|
323
|
+
url = `https://formulae.brew.sh/api/formula/${brewPkg}.json`;
|
|
324
|
+
const brewData = await fetchJson(url, "Homebrew formula");
|
|
325
|
+
version = brewData.versions?.stable;
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case "conda":
|
|
329
|
+
// Conda packages (defaults channel via anaconda.org)
|
|
330
|
+
const condaPkg = pkgInput.replace(/^conda:/i, "");
|
|
331
|
+
// Try conda-forge first, then defaults
|
|
332
|
+
url = `https://api.anaconda.org/package/conda-forge/${condaPkg}`;
|
|
333
|
+
try {
|
|
334
|
+
const condaData = await fetchJson(url, "Conda package");
|
|
335
|
+
version = condaData.latest_version;
|
|
336
|
+
} catch {
|
|
337
|
+
// Fallback to main channel
|
|
338
|
+
url = `https://api.anaconda.org/package/anaconda/${condaPkg}`;
|
|
339
|
+
const condaData2 = await fetchJson(url, "Conda package");
|
|
340
|
+
version = condaData2.latest_version;
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
|
|
344
|
+
case "clojars":
|
|
345
|
+
// Clojure packages from Clojars
|
|
346
|
+
const cljPkg = pkgInput.replace(/^clojars:|^clj:/i, "");
|
|
347
|
+
// Format: group/artifact or just artifact (implies same group)
|
|
348
|
+
const cljParts = cljPkg.includes("/") ? cljPkg.split("/") : [cljPkg, cljPkg];
|
|
349
|
+
url = `https://clojars.org/api/artifacts/${cljParts[0]}/${cljParts[1]}`;
|
|
350
|
+
const cljData = await fetchJson(url, "Clojars artifact");
|
|
351
|
+
version = cljData.latest_version;
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
case "hackage":
|
|
355
|
+
// Haskell packages from Hackage
|
|
356
|
+
const hackPkg = pkgInput.replace(/^hackage:|^haskell:/i, "");
|
|
357
|
+
url = `https://hackage.haskell.org/package/${hackPkg}/preferred.json`;
|
|
358
|
+
const hackData = await fetchJson(url, "Hackage package");
|
|
359
|
+
// preferred.json returns array of version ranges, get the latest normal version
|
|
360
|
+
if (hackData["normal-version"] && hackData["normal-version"].length > 0) {
|
|
361
|
+
version = hackData["normal-version"][0];
|
|
362
|
+
} else {
|
|
363
|
+
throw new Error("No versions found");
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case "julia":
|
|
368
|
+
// Julia packages from JuliaHub/General registry
|
|
369
|
+
const juliaPkg = pkgInput.replace(/^julia:|^jl:/i, "");
|
|
370
|
+
url = `https://juliahub.com/ui/Packages/General/${juliaPkg}`;
|
|
371
|
+
// JuliaHub doesn't have a simple JSON API, use package registry
|
|
372
|
+
const juliaUrl = `https://raw.githubusercontent.com/JuliaRegistries/General/master/${juliaPkg[0].toUpperCase()}/${juliaPkg}/Versions.toml`;
|
|
373
|
+
const juliaRes = await this._fetchWithRetry(juliaUrl, { signal });
|
|
374
|
+
if (!juliaRes.ok) throw new Error("Julia package not found");
|
|
375
|
+
const juliaText = await juliaRes.text();
|
|
376
|
+
// Parse TOML to find latest version (versions are in ["x.y.z"] sections)
|
|
377
|
+
const juliaVersions = juliaText.match(/\["([\d.]+)"\]/g);
|
|
378
|
+
if (juliaVersions && juliaVersions.length > 0) {
|
|
379
|
+
version = juliaVersions[juliaVersions.length - 1].replace(/[\["\]]/g, "");
|
|
380
|
+
url = juliaUrl;
|
|
381
|
+
} else {
|
|
382
|
+
throw new Error("No versions found in Julia registry");
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case "swift":
|
|
387
|
+
// Swift Package Index
|
|
388
|
+
const swiftPkg = pkgInput.replace(/^swift:|^spm:/i, "");
|
|
389
|
+
// Format: owner/repo
|
|
390
|
+
if (!swiftPkg.includes("/")) {
|
|
391
|
+
throw new Error("Swift package must be in 'owner/repo' format");
|
|
392
|
+
}
|
|
393
|
+
url = `https://swiftpackageindex.com/api/packages/${swiftPkg}`;
|
|
394
|
+
const swiftRes = await this._fetchWithRetry(url, {
|
|
395
|
+
headers: { "Accept": "application/json" },
|
|
396
|
+
signal
|
|
397
|
+
});
|
|
398
|
+
if (swiftRes.status === 404) throw new Error("Swift package not found (404)");
|
|
399
|
+
if (!swiftRes.ok) throw new Error(`Swift Package Index error: ${swiftRes.status}`);
|
|
400
|
+
const swiftData = await swiftRes.json();
|
|
401
|
+
// Get latest stable release
|
|
402
|
+
if (swiftData.releases && swiftData.releases.length > 0) {
|
|
403
|
+
const stable = swiftData.releases.find(r => !r.preRelease) || swiftData.releases[0];
|
|
404
|
+
version = stable.tagName?.replace(/^v/, "");
|
|
405
|
+
} else {
|
|
406
|
+
throw new Error("No releases found");
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case "chocolatey":
|
|
411
|
+
// Chocolatey (Windows) packages
|
|
412
|
+
const chocoPkg = pkgInput.replace(/^choco:/i, "").toLowerCase();
|
|
413
|
+
url = `https://community.chocolatey.org/api/v2/package-versions/${chocoPkg}`;
|
|
414
|
+
const chocoRes = await this._fetchWithRetry(url, { signal });
|
|
415
|
+
if (chocoRes.status === 404) throw new Error("Chocolatey package not found (404)");
|
|
416
|
+
if (!chocoRes.ok) throw new Error(`Chocolatey error: ${chocoRes.status}`);
|
|
417
|
+
const chocoVersions = await chocoRes.json();
|
|
418
|
+
if (chocoVersions && chocoVersions.length > 0) {
|
|
419
|
+
version = chocoVersions[chocoVersions.length - 1];
|
|
420
|
+
} else {
|
|
421
|
+
throw new Error("No versions found");
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
default:
|
|
426
|
+
return {
|
|
427
|
+
found: false,
|
|
428
|
+
message: `Ecosystem '${ecosystem || "unknown"}' not directly supported. Please perform a web search for '${pkgInput} latest version'.`
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (version) {
|
|
433
|
+
return {
|
|
434
|
+
found: true,
|
|
435
|
+
version: version,
|
|
436
|
+
source: url
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
throw new Error("Version not found in response (parsing failed)");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Returns the MCP tool definition for version checking.
|
|
446
|
+
* @returns {Object} Tool definition object
|
|
447
|
+
*/
|
|
448
|
+
export function getToolDefinition() {
|
|
449
|
+
return {
|
|
450
|
+
name: "d_check_last_version",
|
|
451
|
+
description: "Get the latest version of a library/package from its official registry. Supported ecosystems: npm (JS/TS), PyPI (Python), Packagist (PHP), Crates.io (Rust), Maven (Java/Kotlin), Go, RubyGems, NuGet (.NET), Hex (Elixir), CRAN (R), CPAN (Perl), pub.dev (Dart), Homebrew (macOS), Conda (Python/R), Clojars (Clojure), Hackage (Haskell), Julia, Swift PM, Chocolatey (Windows). Returns the version string to help you avoid using outdated dependencies.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: {
|
|
455
|
+
package: {
|
|
456
|
+
type: "string",
|
|
457
|
+
description: "Package name (e.g., 'express', 'requests', 'flutter', 'brew:wget', 'conda:numpy', 'swift:apple/swift-nio'). Use prefixes for explicit ecosystem detection."
|
|
458
|
+
},
|
|
459
|
+
ecosystem: {
|
|
460
|
+
type: "string",
|
|
461
|
+
enum: ["npm", "pypi", "packagist", "crates", "maven", "go", "rubygems", "nuget", "cocoapods", "hex", "cran", "cpan", "pub", "homebrew", "conda", "clojars", "hackage", "julia", "swift", "chocolatey"],
|
|
462
|
+
description: "Package ecosystem (optional - auto-detected from prefix)"
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
required: ["package"]
|
|
466
|
+
},
|
|
467
|
+
annotations: {
|
|
468
|
+
title: "Check Latest Package Version",
|
|
469
|
+
readOnlyHint: true,
|
|
470
|
+
idempotentHint: true
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Handles an MCP tool call for version checking.
|
|
477
|
+
* @param {Object} request - MCP request object
|
|
478
|
+
* @param {VersionChecker} checker - VersionChecker instance
|
|
479
|
+
* @returns {Promise<Object>} MCP response object
|
|
480
|
+
*/
|
|
481
|
+
export async function handleToolCall(request, checker) {
|
|
482
|
+
const pkg = request.params.arguments.package;
|
|
483
|
+
const ecosystem = request.params.arguments.ecosystem;
|
|
484
|
+
|
|
485
|
+
const result = await checker.checkVersion(pkg, ecosystem);
|
|
486
|
+
|
|
487
|
+
if (result.found) {
|
|
488
|
+
const cacheNote = result.fromCache ? " (cached)" : "";
|
|
489
|
+
return {
|
|
490
|
+
content: [{
|
|
491
|
+
type: "text",
|
|
492
|
+
text: `Latest version of \`${result.package}\` (${result.ecosystem}): **${result.version}**${cacheNote}\n\nSource: ${result.source}`
|
|
493
|
+
}]
|
|
494
|
+
};
|
|
495
|
+
} else {
|
|
496
|
+
return {
|
|
497
|
+
content: [{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: result.message || `Could not find version for \`${pkg}\`. Error: ${result.error}`
|
|
500
|
+
}],
|
|
501
|
+
isError: true
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class CacheClearer {
|
|
2
|
+
constructor(embedder, cache, config, indexer) {
|
|
3
|
+
this.cache = cache;
|
|
4
|
+
this.config = config;
|
|
5
|
+
this.indexer = indexer;
|
|
6
|
+
this.isClearing = false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async execute() {
|
|
10
|
+
// Check if indexing is in progress
|
|
11
|
+
if (this.indexer && this.indexer.isIndexing) {
|
|
12
|
+
throw new Error("Cannot clear cache while indexing is in progress. Please wait for indexing to complete.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check if cache is currently being saved (race condition prevention)
|
|
16
|
+
if (this.cache.isSaving) {
|
|
17
|
+
throw new Error("Cannot clear cache while cache is being saved. Please try again in a moment.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if a clear operation is already in progress (prevent concurrent clears)
|
|
21
|
+
if (this.isClearing) {
|
|
22
|
+
throw new Error("Cache clear operation already in progress. Please wait for it to complete.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.isClearing = true;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await this.cache.clear();
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
message: `Cache cleared successfully. Next indexing will be a full rebuild.`,
|
|
32
|
+
cacheDirectory: this.config.cacheDirectory
|
|
33
|
+
};
|
|
34
|
+
} finally {
|
|
35
|
+
this.isClearing = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getToolDefinition() {
|
|
41
|
+
return {
|
|
42
|
+
name: "c_clear_cache",
|
|
43
|
+
description: "Clears the embeddings cache, forcing a complete reindex on next search or manual index operation. Useful when encountering cache corruption or after major codebase changes.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {}
|
|
47
|
+
},
|
|
48
|
+
annotations: {
|
|
49
|
+
title: "Clear Embeddings Cache",
|
|
50
|
+
readOnlyHint: false,
|
|
51
|
+
destructiveHint: true,
|
|
52
|
+
idempotentHint: true,
|
|
53
|
+
openWorldHint: false
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function handleToolCall(request, cacheClearer) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await cacheClearer.execute();
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `${result.message}\n\nCache directory: ${result.cacheDirectory}`
|
|
65
|
+
}]
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: `Failed to clear cache: ${error.message}`
|
|
72
|
+
}]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|