@softerist/heuristic-mcp 3.0.15 → 3.0.16
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 +104 -104
- package/config.jsonc +173 -173
- package/features/ann-config.js +131 -0
- package/features/clear-cache.js +84 -0
- package/features/find-similar-code.js +291 -0
- package/features/hybrid-search.js +544 -0
- package/features/index-codebase.js +3268 -0
- package/features/lifecycle.js +1189 -0
- package/features/package-version.js +302 -0
- package/features/register.js +408 -0
- package/features/resources.js +156 -0
- package/features/set-workspace.js +265 -0
- package/index.js +96 -96
- package/lib/cache-ops.js +22 -22
- package/lib/cache-utils.js +565 -565
- package/lib/cache.js +1870 -1870
- package/lib/call-graph.js +396 -396
- package/lib/cli.js +1 -1
- package/lib/config.js +517 -517
- package/lib/constants.js +39 -39
- package/lib/embed-query-process.js +7 -7
- package/lib/embedding-process.js +7 -7
- package/lib/embedding-worker.js +299 -299
- package/lib/ignore-patterns.js +316 -316
- package/lib/json-worker.js +14 -14
- package/lib/json-writer.js +337 -337
- package/lib/logging.js +164 -164
- package/lib/memory-logger.js +13 -13
- package/lib/onnx-backend.js +193 -193
- package/lib/project-detector.js +84 -84
- package/lib/server-lifecycle.js +165 -165
- package/lib/settings-editor.js +754 -754
- package/lib/tokenizer.js +256 -256
- package/lib/utils.js +428 -428
- package/lib/vector-store-binary.js +627 -627
- package/lib/vector-store-sqlite.js +95 -95
- package/lib/workspace-env.js +28 -28
- package/mcp_config.json +9 -9
- package/package.json +86 -75
- package/scripts/clear-cache.js +20 -0
- package/scripts/download-model.js +43 -0
- package/scripts/mcp-launcher.js +49 -0
- package/scripts/postinstall.js +12 -0
- package/search-configs.js +36 -36
- package/.prettierrc +0 -7
- package/debug-pids.js +0 -30
- package/eslint.config.js +0 -36
- package/specs/plan.md +0 -23
- package/vitest.config.js +0 -39
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Version Lookup Tool
|
|
3
|
+
*
|
|
4
|
+
* Fetches the latest version of packages from various registries.
|
|
5
|
+
* Supports npm, PyPI, crates.io, Maven, Go, RubyGems, NuGet, Packagist, Hex, pub.dev, and more.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Registry configurations with their API endpoints and response parsers
|
|
9
|
+
const REGISTRIES = {
|
|
10
|
+
npm: {
|
|
11
|
+
name: 'npm',
|
|
12
|
+
pattern: /^(?:npm:)?(.+)$/,
|
|
13
|
+
url: (pkg) => `https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`,
|
|
14
|
+
parse: (data) => data.version,
|
|
15
|
+
detect: (pkg) =>
|
|
16
|
+
pkg.startsWith('@') || /^[a-z0-9][-a-z0-9._]*$/i.test(pkg.replace(/^npm:/, '')),
|
|
17
|
+
},
|
|
18
|
+
pypi: {
|
|
19
|
+
name: 'PyPI',
|
|
20
|
+
pattern: /^(?:pip:|pypi:)(.+)$/,
|
|
21
|
+
url: (pkg) => `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`,
|
|
22
|
+
parse: (data) => data.info.version,
|
|
23
|
+
detect: () => false, // Requires explicit prefix
|
|
24
|
+
},
|
|
25
|
+
crates: {
|
|
26
|
+
name: 'crates.io',
|
|
27
|
+
pattern: /^(?:cargo:|crates:|rust:)(.+)$/,
|
|
28
|
+
url: (pkg) => `https://crates.io/api/v1/crates/${encodeURIComponent(pkg)}`,
|
|
29
|
+
parse: (data) => data.crate.max_version,
|
|
30
|
+
headers: { 'User-Agent': 'heuristic-mcp/1.0' },
|
|
31
|
+
detect: () => false,
|
|
32
|
+
},
|
|
33
|
+
go: {
|
|
34
|
+
name: 'Go',
|
|
35
|
+
pattern: /^(?:go:)(.+)$/,
|
|
36
|
+
url: (pkg) => `https://proxy.golang.org/${encodeURIComponent(pkg)}/@latest`,
|
|
37
|
+
parse: (data) => data.Version,
|
|
38
|
+
detect: (pkg) => pkg.includes('/') && pkg.includes('.'),
|
|
39
|
+
},
|
|
40
|
+
rubygems: {
|
|
41
|
+
name: 'RubyGems',
|
|
42
|
+
pattern: /^(?:gem:|ruby:)(.+)$/,
|
|
43
|
+
url: (pkg) => `https://rubygems.org/api/v1/gems/${encodeURIComponent(pkg)}.json`,
|
|
44
|
+
parse: (data) => data.version,
|
|
45
|
+
detect: () => false,
|
|
46
|
+
},
|
|
47
|
+
nuget: {
|
|
48
|
+
name: 'NuGet',
|
|
49
|
+
pattern: /^(?:nuget:|dotnet:)(.+)$/,
|
|
50
|
+
url: (pkg) =>
|
|
51
|
+
`https://api.nuget.org/v3-flatcontainer/${encodeURIComponent(pkg.toLowerCase())}/index.json`,
|
|
52
|
+
parse: (data) => data.versions[data.versions.length - 1],
|
|
53
|
+
detect: () => false,
|
|
54
|
+
},
|
|
55
|
+
packagist: {
|
|
56
|
+
name: 'Packagist',
|
|
57
|
+
pattern: /^(?:composer:|php:)(.+)$/,
|
|
58
|
+
url: (pkg) => `https://repo.packagist.org/p2/${encodeURIComponent(pkg)}.json`,
|
|
59
|
+
parse: (data) => {
|
|
60
|
+
const pkgName = Object.keys(data.packages)[0];
|
|
61
|
+
const versions = data.packages[pkgName];
|
|
62
|
+
// Filter out dev versions and get latest stable
|
|
63
|
+
const stable = versions.find((v) => !v.version.includes('dev'));
|
|
64
|
+
return stable ? stable.version : versions[0].version;
|
|
65
|
+
},
|
|
66
|
+
detect: (pkg) => pkg.includes('/'),
|
|
67
|
+
},
|
|
68
|
+
hex: {
|
|
69
|
+
name: 'Hex',
|
|
70
|
+
pattern: /^(?:hex:|elixir:|mix:)(.+)$/,
|
|
71
|
+
url: (pkg) => `https://hex.pm/api/packages/${encodeURIComponent(pkg)}`,
|
|
72
|
+
parse: (data) => {
|
|
73
|
+
const releases = data.releases;
|
|
74
|
+
return releases.length > 0 ? releases[0].version : null;
|
|
75
|
+
},
|
|
76
|
+
detect: () => false,
|
|
77
|
+
},
|
|
78
|
+
pub: {
|
|
79
|
+
name: 'pub.dev',
|
|
80
|
+
pattern: /^(?:pub:|dart:|flutter:)(.+)$/,
|
|
81
|
+
url: (pkg) => `https://pub.dev/api/packages/${encodeURIComponent(pkg)}`,
|
|
82
|
+
parse: (data) => data.latest.version,
|
|
83
|
+
detect: () => false,
|
|
84
|
+
},
|
|
85
|
+
maven: {
|
|
86
|
+
name: 'Maven Central',
|
|
87
|
+
pattern: /^(?:maven:|java:)(.+)$/,
|
|
88
|
+
url: (pkg) => {
|
|
89
|
+
// Maven packages are in format group:artifact or group/artifact
|
|
90
|
+
const [group, artifact] = pkg.includes(':') ? pkg.split(':') : pkg.split('/');
|
|
91
|
+
if (!artifact) return null;
|
|
92
|
+
return `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(group)}+AND+a:${encodeURIComponent(artifact)}&rows=1&wt=json`;
|
|
93
|
+
},
|
|
94
|
+
parse: (data) => {
|
|
95
|
+
if (data.response.docs.length === 0) return null;
|
|
96
|
+
return data.response.docs[0].latestVersion;
|
|
97
|
+
},
|
|
98
|
+
detect: (pkg) => pkg.includes(':') || (pkg.includes('.') && pkg.includes('/')),
|
|
99
|
+
},
|
|
100
|
+
homebrew: {
|
|
101
|
+
name: 'Homebrew',
|
|
102
|
+
pattern: /^(?:brew:|homebrew:)(.+)$/,
|
|
103
|
+
url: (pkg) => `https://formulae.brew.sh/api/formula/${encodeURIComponent(pkg)}.json`,
|
|
104
|
+
parse: (data) => data.versions.stable,
|
|
105
|
+
detect: () => false,
|
|
106
|
+
},
|
|
107
|
+
conda: {
|
|
108
|
+
name: 'Conda',
|
|
109
|
+
pattern: /^(?:conda:)(.+)$/,
|
|
110
|
+
url: (pkg) =>
|
|
111
|
+
`https://api.anaconda.org/package/conda-forge/${encodeURIComponent(pkg)}`,
|
|
112
|
+
parse: (data) => data.latest_version,
|
|
113
|
+
detect: () => false,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Detect the registry for a package based on its name or prefix
|
|
119
|
+
*/
|
|
120
|
+
function detectRegistry(packageName) {
|
|
121
|
+
// Check for explicit prefixes first
|
|
122
|
+
for (const [key, registry] of Object.entries(REGISTRIES)) {
|
|
123
|
+
if (registry.pattern.test(packageName) && key !== 'npm') {
|
|
124
|
+
const match = packageName.match(registry.pattern);
|
|
125
|
+
if (match) {
|
|
126
|
+
return { registry, cleanName: match[1] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try to detect based on package name patterns
|
|
132
|
+
for (const registry of Object.values(REGISTRIES)) {
|
|
133
|
+
if (registry.detect(packageName)) {
|
|
134
|
+
const match = packageName.match(registry.pattern);
|
|
135
|
+
return { registry, cleanName: match ? match[1] : packageName };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Default to npm
|
|
140
|
+
const npmMatch = packageName.match(REGISTRIES.npm.pattern);
|
|
141
|
+
return { registry: REGISTRIES.npm, cleanName: npmMatch ? npmMatch[1] : packageName };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Fetch the latest version of a package from its registry
|
|
146
|
+
*/
|
|
147
|
+
async function fetchPackageVersion(packageName, timeoutMs = 10000) {
|
|
148
|
+
const { registry, cleanName } = detectRegistry(packageName);
|
|
149
|
+
|
|
150
|
+
const url = registry.url(cleanName);
|
|
151
|
+
if (!url) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: `Invalid package format for ${registry.name}: ${cleanName}`,
|
|
155
|
+
registry: registry.name,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const headers = {
|
|
164
|
+
Accept: 'application/json',
|
|
165
|
+
...(registry.headers || {}),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
headers,
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
if (response.status === 404) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: `Package "${cleanName}" not found on ${registry.name}`,
|
|
178
|
+
registry: registry.name,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `${registry.name} returned status ${response.status}`,
|
|
184
|
+
registry: registry.name,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const data = await response.json();
|
|
189
|
+
const version = registry.parse(data);
|
|
190
|
+
|
|
191
|
+
if (!version) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
error: `Could not parse version from ${registry.name} response`,
|
|
195
|
+
registry: registry.name,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
package: cleanName,
|
|
202
|
+
version,
|
|
203
|
+
registry: registry.name,
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error.name === 'AbortError') {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: `Request to ${registry.name} timed out`,
|
|
210
|
+
registry: registry.name,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
error: `Failed to fetch from ${registry.name}: ${error.message}`,
|
|
216
|
+
registry: registry.name,
|
|
217
|
+
};
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timeout);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get supported registries list for help text
|
|
225
|
+
*/
|
|
226
|
+
function getSupportedRegistries() {
|
|
227
|
+
return Object.entries(REGISTRIES).map(([key, reg]) => ({
|
|
228
|
+
key,
|
|
229
|
+
name: reg.name,
|
|
230
|
+
prefix: reg.pattern.source.match(/\?:([^)]+)\)/)?.[1] || key + ':',
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// MCP Tool definition
|
|
235
|
+
export function getToolDefinition() {
|
|
236
|
+
return {
|
|
237
|
+
name: 'e_check_package_version',
|
|
238
|
+
description:
|
|
239
|
+
'Fetches the latest version of a package from its official registry. Supports npm, PyPI, crates.io, Maven, Go, RubyGems, NuGet, Packagist, Hex, pub.dev, Homebrew, and Conda. Use prefix like "pip:requests" for non-npm packages.',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
package: {
|
|
244
|
+
type: 'string',
|
|
245
|
+
description:
|
|
246
|
+
'Package name, optionally prefixed with registry (e.g., "lodash", "pip:requests", "cargo:serde", "go:github.com/gin-gonic/gin")',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
required: ['package'],
|
|
250
|
+
},
|
|
251
|
+
annotations: {
|
|
252
|
+
title: 'Check Package Version',
|
|
253
|
+
readOnlyHint: true,
|
|
254
|
+
destructiveHint: false,
|
|
255
|
+
idempotentHint: true,
|
|
256
|
+
openWorldHint: true, // Makes external network requests
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Tool handler
|
|
262
|
+
export async function handleToolCall(request) {
|
|
263
|
+
const args = request.params?.arguments || {};
|
|
264
|
+
const packageName = args.package;
|
|
265
|
+
|
|
266
|
+
if (!packageName || typeof packageName !== 'string' || packageName.trim() === '') {
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: 'text',
|
|
271
|
+
text: 'Error: Please provide a package name.',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await fetchPackageVersion(packageName.trim());
|
|
279
|
+
|
|
280
|
+
if (result.success) {
|
|
281
|
+
return {
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: 'text',
|
|
285
|
+
text: `**${result.package}** (${result.registry})\n\nLatest version: \`${result.version}\``,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
} else {
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: 'text',
|
|
294
|
+
text: `Error: ${result.error}`,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Export for testing
|
|
302
|
+
export { fetchPackageVersion, detectRegistry, getSupportedRegistries, REGISTRIES };
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import {
|
|
8
|
+
parseJsonc,
|
|
9
|
+
upsertMcpServerEntryInText,
|
|
10
|
+
upsertMcpServerEntryInToml,
|
|
11
|
+
} from '../lib/settings-editor.js';
|
|
12
|
+
|
|
13
|
+
function getUserHomeDir() {
|
|
14
|
+
if (process.platform === 'win32' && process.env.USERPROFILE) {
|
|
15
|
+
return process.env.USERPROFILE;
|
|
16
|
+
}
|
|
17
|
+
return os.homedir();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Detect which IDE is running the install
|
|
21
|
+
function detectCurrentIDE() {
|
|
22
|
+
// Check environment variables to determine which IDE is running
|
|
23
|
+
if (process.env.ANTIGRAVITY_AGENT) {
|
|
24
|
+
return 'Antigravity';
|
|
25
|
+
}
|
|
26
|
+
if (process.env.CURSOR_AGENT) {
|
|
27
|
+
return 'Cursor';
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
process.env.CODEX_THREAD_ID ||
|
|
31
|
+
process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE ||
|
|
32
|
+
process.env.CODEX_WORKSPACE
|
|
33
|
+
) {
|
|
34
|
+
return 'Codex';
|
|
35
|
+
}
|
|
36
|
+
if (process.env.VSCODE_IPC_HOOK || process.env.TERM_PROGRAM === 'vscode') {
|
|
37
|
+
return 'VS Code';
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
process.env.WARP_SESSION_ID ||
|
|
41
|
+
process.env.TERM_PROGRAM === 'WarpTerminal' ||
|
|
42
|
+
process.env.TERM_PROGRAM === 'Warp'
|
|
43
|
+
) {
|
|
44
|
+
return 'Warp';
|
|
45
|
+
}
|
|
46
|
+
if (process.env.WINDSURF_AGENT || process.env.WINDSURF_WORKSPACE) {
|
|
47
|
+
return 'Windsurf';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Claude Desktop doesn't have a known env var.
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Known config paths for different IDEs
|
|
55
|
+
function getConfigPaths() {
|
|
56
|
+
const platform = process.platform;
|
|
57
|
+
const home = getUserHomeDir();
|
|
58
|
+
const currentIDE = detectCurrentIDE();
|
|
59
|
+
const allPaths = [];
|
|
60
|
+
|
|
61
|
+
// Antigravity - dedicated mcp_config.json
|
|
62
|
+
allPaths.push({
|
|
63
|
+
name: 'Antigravity',
|
|
64
|
+
path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
65
|
+
format: 'json',
|
|
66
|
+
canCreate: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Codex - dedicated config.toml
|
|
70
|
+
allPaths.push({
|
|
71
|
+
name: 'Codex',
|
|
72
|
+
path: path.join(home, '.codex', 'config.toml'),
|
|
73
|
+
format: 'toml',
|
|
74
|
+
canCreate: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Claude Desktop - dedicated config file
|
|
78
|
+
if (platform === 'darwin') {
|
|
79
|
+
allPaths.push({
|
|
80
|
+
name: 'Claude Desktop',
|
|
81
|
+
path: path.join(
|
|
82
|
+
home,
|
|
83
|
+
'Library',
|
|
84
|
+
'Application Support',
|
|
85
|
+
'Claude',
|
|
86
|
+
'claude_desktop_config.json'
|
|
87
|
+
),
|
|
88
|
+
format: 'json',
|
|
89
|
+
canCreate: false,
|
|
90
|
+
});
|
|
91
|
+
} else if (platform === 'win32') {
|
|
92
|
+
allPaths.push({
|
|
93
|
+
name: 'Claude Desktop',
|
|
94
|
+
path: path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json'),
|
|
95
|
+
format: 'json',
|
|
96
|
+
canCreate: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Cursor - settings.json with mcpServers key
|
|
101
|
+
if (platform === 'darwin') {
|
|
102
|
+
allPaths.push({
|
|
103
|
+
name: 'Cursor',
|
|
104
|
+
path: path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
|
|
105
|
+
format: 'json',
|
|
106
|
+
canCreate: false,
|
|
107
|
+
});
|
|
108
|
+
} else if (platform === 'win32') {
|
|
109
|
+
allPaths.push({
|
|
110
|
+
name: 'Cursor',
|
|
111
|
+
path: path.join(process.env.APPDATA || '', 'Cursor', 'User', 'settings.json'),
|
|
112
|
+
format: 'json',
|
|
113
|
+
canCreate: false,
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
allPaths.push({
|
|
117
|
+
name: 'Cursor',
|
|
118
|
+
path: path.join(home, '.config', 'Cursor', 'User', 'settings.json'),
|
|
119
|
+
format: 'json',
|
|
120
|
+
canCreate: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cursor global MCP config (discovered by VS Code MCP Discovery)
|
|
125
|
+
allPaths.push({
|
|
126
|
+
name: 'Cursor Global',
|
|
127
|
+
path: path.join(home, '.cursor', 'mcp.json'),
|
|
128
|
+
format: 'json',
|
|
129
|
+
canCreate: false,
|
|
130
|
+
preferredContainerKey: 'mcpServers',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Windsurf global MCP config (discovered by VS Code MCP Discovery)
|
|
134
|
+
allPaths.push({
|
|
135
|
+
name: 'Windsurf',
|
|
136
|
+
path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
137
|
+
format: 'json',
|
|
138
|
+
canCreate: false,
|
|
139
|
+
preferredContainerKey: 'mcpServers',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Warp MCP config
|
|
143
|
+
allPaths.push({
|
|
144
|
+
name: 'Warp',
|
|
145
|
+
path: path.join(home, '.warp', 'mcp_settings.json'),
|
|
146
|
+
format: 'json',
|
|
147
|
+
canCreate: true,
|
|
148
|
+
preferredContainerKey: 'mcpServers',
|
|
149
|
+
});
|
|
150
|
+
if (platform === 'win32') {
|
|
151
|
+
allPaths.push({
|
|
152
|
+
name: 'Warp AppData',
|
|
153
|
+
path: path.join(process.env.APPDATA || '', 'Warp', 'mcp_settings.json'),
|
|
154
|
+
format: 'json',
|
|
155
|
+
canCreate: false,
|
|
156
|
+
preferredContainerKey: 'mcpServers',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// VS Code MCP registry (mcp.json uses "servers" key)
|
|
161
|
+
if (platform === 'darwin') {
|
|
162
|
+
allPaths.push({
|
|
163
|
+
name: 'VS Code',
|
|
164
|
+
path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'),
|
|
165
|
+
format: 'json',
|
|
166
|
+
canCreate: false,
|
|
167
|
+
preferredContainerKey: 'servers',
|
|
168
|
+
});
|
|
169
|
+
allPaths.push({
|
|
170
|
+
name: 'VS Code Insiders',
|
|
171
|
+
path: path.join(
|
|
172
|
+
home,
|
|
173
|
+
'Library',
|
|
174
|
+
'Application Support',
|
|
175
|
+
'Code - Insiders',
|
|
176
|
+
'User',
|
|
177
|
+
'mcp.json'
|
|
178
|
+
),
|
|
179
|
+
format: 'json',
|
|
180
|
+
canCreate: false,
|
|
181
|
+
preferredContainerKey: 'servers',
|
|
182
|
+
});
|
|
183
|
+
} else if (platform === 'win32') {
|
|
184
|
+
allPaths.push({
|
|
185
|
+
name: 'VS Code',
|
|
186
|
+
path: path.join(process.env.APPDATA || '', 'Code', 'User', 'mcp.json'),
|
|
187
|
+
format: 'json',
|
|
188
|
+
canCreate: false,
|
|
189
|
+
preferredContainerKey: 'servers',
|
|
190
|
+
});
|
|
191
|
+
allPaths.push({
|
|
192
|
+
name: 'VS Code Insiders',
|
|
193
|
+
path: path.join(process.env.APPDATA || '', 'Code - Insiders', 'User', 'mcp.json'),
|
|
194
|
+
format: 'json',
|
|
195
|
+
canCreate: false,
|
|
196
|
+
preferredContainerKey: 'servers',
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
allPaths.push({
|
|
200
|
+
name: 'VS Code',
|
|
201
|
+
path: path.join(home, '.config', 'Code', 'User', 'mcp.json'),
|
|
202
|
+
format: 'json',
|
|
203
|
+
canCreate: false,
|
|
204
|
+
preferredContainerKey: 'servers',
|
|
205
|
+
});
|
|
206
|
+
allPaths.push({
|
|
207
|
+
name: 'VS Code Insiders',
|
|
208
|
+
path: path.join(home, '.config', 'Code - Insiders', 'User', 'mcp.json'),
|
|
209
|
+
format: 'json',
|
|
210
|
+
canCreate: false,
|
|
211
|
+
preferredContainerKey: 'servers',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Keep updates broad so a single install can refresh dedicated configs (Codex + Antigravity)
|
|
216
|
+
// while still avoiding creation of shared IDE settings files unless they already exist.
|
|
217
|
+
return allPaths.map((entry) => ({
|
|
218
|
+
...entry,
|
|
219
|
+
canCreate: entry.canCreate || entry.name === currentIDE,
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Helper to force output to terminal, bypassing npm's silence
|
|
224
|
+
function forceLog(message) {
|
|
225
|
+
try {
|
|
226
|
+
if (process.platform !== 'win32') {
|
|
227
|
+
writeFileSync('/dev/tty', message + '\n');
|
|
228
|
+
} else {
|
|
229
|
+
console.error(message);
|
|
230
|
+
}
|
|
231
|
+
} catch (_e) {
|
|
232
|
+
console.error(message);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeIdeName(name) {
|
|
237
|
+
return String(name || '')
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.replace(/["']/g, '')
|
|
240
|
+
.replace(/[\s_-]+/g, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function ideMatchesFilter(name, filter) {
|
|
244
|
+
if (!filter) return true;
|
|
245
|
+
const normalizedName = normalizeIdeName(name);
|
|
246
|
+
const normalizedFilter = normalizeIdeName(filter);
|
|
247
|
+
if (!normalizedFilter) return true;
|
|
248
|
+
|
|
249
|
+
if (normalizedName === normalizedFilter) return true;
|
|
250
|
+
if (normalizedFilter === 'claude') return normalizedName === 'claudedesktop';
|
|
251
|
+
if (normalizedFilter === 'cursor') {
|
|
252
|
+
return normalizedName === 'cursor' || normalizedName === 'cursorglobal';
|
|
253
|
+
}
|
|
254
|
+
if (normalizedFilter === 'windsurf') {
|
|
255
|
+
return normalizedName === 'windsurf';
|
|
256
|
+
}
|
|
257
|
+
if (normalizedFilter === 'warp') {
|
|
258
|
+
return normalizedName === 'warp' || normalizedName === 'warpappdata';
|
|
259
|
+
}
|
|
260
|
+
if (normalizedFilter === 'vscode') {
|
|
261
|
+
return normalizedName === 'vscode' || normalizedName === 'vscodeinsiders';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function register(filter = null) {
|
|
268
|
+
const binaryPath = process.execPath; // The node binary
|
|
269
|
+
const scriptPath = fileURLToPath(new URL('../index.js', import.meta.url)); // Absolute path to index.js
|
|
270
|
+
const currentIDE = detectCurrentIDE();
|
|
271
|
+
|
|
272
|
+
// Build args array - add --expose-gc if enableExplicitGc is likely needed
|
|
273
|
+
// For workspace selection, rely on runtime auto-detection (env vars + cwd) so one config
|
|
274
|
+
// can be reused across projects without editing the installed path each day.
|
|
275
|
+
const args = ['--expose-gc', scriptPath];
|
|
276
|
+
|
|
277
|
+
const serverConfig = {
|
|
278
|
+
command: binaryPath,
|
|
279
|
+
args,
|
|
280
|
+
disabled: false,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const configPaths = getConfigPaths();
|
|
284
|
+
let registeredCount = 0;
|
|
285
|
+
|
|
286
|
+
forceLog(`[Auto-Register] Detecting IDE configurations...`);
|
|
287
|
+
|
|
288
|
+
for (const { name, path: configPath, canCreate, format, preferredContainerKey } of configPaths) {
|
|
289
|
+
if (!ideMatchesFilter(name, filter)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// Check if file exists - create if canCreate is true for this IDE
|
|
295
|
+
let fileExists = true;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await fs.access(configPath);
|
|
299
|
+
} catch {
|
|
300
|
+
fileExists = false;
|
|
301
|
+
|
|
302
|
+
// Only create config if this IDE allows it (has dedicated MCP config file)
|
|
303
|
+
if (canCreate) {
|
|
304
|
+
try {
|
|
305
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
306
|
+
forceLog(`[Auto-Register] Creating ${name} config at ${configPath}`);
|
|
307
|
+
} catch (mkdirErr) {
|
|
308
|
+
forceLog(
|
|
309
|
+
`[Auto-Register] Skipped ${name}: Cannot create config directory: ${mkdirErr.message}`
|
|
310
|
+
);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
// Skip IDEs that use shared settings files if they don't exist
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let content = '';
|
|
320
|
+
if (fileExists) {
|
|
321
|
+
content = await fs.readFile(configPath, 'utf-8');
|
|
322
|
+
if (format === 'json' && content.trim()) {
|
|
323
|
+
const parsed = parseJsonc(content);
|
|
324
|
+
if (!parsed) {
|
|
325
|
+
forceLog(
|
|
326
|
+
`[Auto-Register] Warning: ${name} config is not valid JSON/JSONC; skipping to avoid data loss.`
|
|
327
|
+
);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const updated =
|
|
334
|
+
format === 'toml'
|
|
335
|
+
? upsertMcpServerEntryInToml(content, 'heuristic-mcp', serverConfig)
|
|
336
|
+
: upsertMcpServerEntryInText(
|
|
337
|
+
content,
|
|
338
|
+
'heuristic-mcp',
|
|
339
|
+
serverConfig,
|
|
340
|
+
preferredContainerKey || 'mcpServers'
|
|
341
|
+
);
|
|
342
|
+
if (!updated) {
|
|
343
|
+
forceLog(
|
|
344
|
+
`[Auto-Register] Warning: Failed to update ${name} config (could not locate root object).`
|
|
345
|
+
);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Write back synchronously to avoid race conditions
|
|
350
|
+
writeFileSync(configPath, updated);
|
|
351
|
+
|
|
352
|
+
forceLog(`\x1b[32m[Auto-Register] ✅ Successfully registered with ${name}\x1b[0m`);
|
|
353
|
+
registeredCount++;
|
|
354
|
+
} catch (err) {
|
|
355
|
+
forceLog(`[Auto-Register] Failed to register with ${name}: ${err.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (registeredCount === 0) {
|
|
360
|
+
forceLog(`[Auto-Register] No compatible IDE configurations found to update.`);
|
|
361
|
+
forceLog(
|
|
362
|
+
`[Auto-Register] Manual Config:\n${JSON.stringify({ mcpServers: { 'heuristic-mcp': serverConfig } }, null, 2)}`
|
|
363
|
+
);
|
|
364
|
+
} else {
|
|
365
|
+
// Friendly Banner (Using forceLog to bypass npm stdout suppression)
|
|
366
|
+
forceLog('\n\x1b[36m' + '='.repeat(60));
|
|
367
|
+
forceLog(' 🚀 Heuristic MCP Installed & Configured! ');
|
|
368
|
+
forceLog('='.repeat(60) + '\x1b[0m');
|
|
369
|
+
|
|
370
|
+
// Show important paths
|
|
371
|
+
const home = getUserHomeDir();
|
|
372
|
+
const cacheRoot =
|
|
373
|
+
process.platform === 'win32'
|
|
374
|
+
? path.join(
|
|
375
|
+
process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'),
|
|
376
|
+
'heuristic-mcp'
|
|
377
|
+
)
|
|
378
|
+
: process.platform === 'darwin'
|
|
379
|
+
? path.join(home, 'Library', 'Caches', 'heuristic-mcp')
|
|
380
|
+
: path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), 'heuristic-mcp');
|
|
381
|
+
|
|
382
|
+
forceLog(`
|
|
383
|
+
\x1b[33mACTION REQUIRED:\x1b[0m
|
|
384
|
+
1. \x1b[1mRestart your IDE\x1b[0m (or reload the window) to load the new config.
|
|
385
|
+
2. The server will start automatically in the background.
|
|
386
|
+
|
|
387
|
+
\x1b[32mSTATUS:\x1b[0m
|
|
388
|
+
- \x1b[1mConfig:\x1b[0m Updated ${registeredCount} config file(s).
|
|
389
|
+
- \x1b[1mIndexing:\x1b[0m Will begin immediately after restart.
|
|
390
|
+
- \x1b[1mUsage:\x1b[0m You can work while it indexes (it catches up!).
|
|
391
|
+
|
|
392
|
+
\x1b[90mPATHS:\x1b[0m
|
|
393
|
+
- \x1b[1mMCP Config:\x1b[0m ${configPaths.map((p) => p.path).join(', ')}
|
|
394
|
+
- \x1b[1mCache:\x1b[0m ${cacheRoot}
|
|
395
|
+
- \x1b[1mCheck status:\x1b[0m heuristic-mcp --status
|
|
396
|
+
- \x1b[1mView logs:\x1b[0m heuristic-mcp --logs
|
|
397
|
+
|
|
398
|
+
\x1b[36mHappy Coding! 🤖\x1b[0m
|
|
399
|
+
`);
|
|
400
|
+
forceLog(`\n\x1b[90m(Please wait while npm finalizes the installation...)\x1b[0m`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (currentIDE === 'Warp' && registeredCount === 0) {
|
|
404
|
+
forceLog(
|
|
405
|
+
'[Auto-Register] Warp detected but no local Warp MCP config was writable. Use Warp MCP settings/UI if needed.'
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|