@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.
Files changed (49) hide show
  1. package/README.md +104 -104
  2. package/config.jsonc +173 -173
  3. package/features/ann-config.js +131 -0
  4. package/features/clear-cache.js +84 -0
  5. package/features/find-similar-code.js +291 -0
  6. package/features/hybrid-search.js +544 -0
  7. package/features/index-codebase.js +3268 -0
  8. package/features/lifecycle.js +1189 -0
  9. package/features/package-version.js +302 -0
  10. package/features/register.js +408 -0
  11. package/features/resources.js +156 -0
  12. package/features/set-workspace.js +265 -0
  13. package/index.js +96 -96
  14. package/lib/cache-ops.js +22 -22
  15. package/lib/cache-utils.js +565 -565
  16. package/lib/cache.js +1870 -1870
  17. package/lib/call-graph.js +396 -396
  18. package/lib/cli.js +1 -1
  19. package/lib/config.js +517 -517
  20. package/lib/constants.js +39 -39
  21. package/lib/embed-query-process.js +7 -7
  22. package/lib/embedding-process.js +7 -7
  23. package/lib/embedding-worker.js +299 -299
  24. package/lib/ignore-patterns.js +316 -316
  25. package/lib/json-worker.js +14 -14
  26. package/lib/json-writer.js +337 -337
  27. package/lib/logging.js +164 -164
  28. package/lib/memory-logger.js +13 -13
  29. package/lib/onnx-backend.js +193 -193
  30. package/lib/project-detector.js +84 -84
  31. package/lib/server-lifecycle.js +165 -165
  32. package/lib/settings-editor.js +754 -754
  33. package/lib/tokenizer.js +256 -256
  34. package/lib/utils.js +428 -428
  35. package/lib/vector-store-binary.js +627 -627
  36. package/lib/vector-store-sqlite.js +95 -95
  37. package/lib/workspace-env.js +28 -28
  38. package/mcp_config.json +9 -9
  39. package/package.json +86 -75
  40. package/scripts/clear-cache.js +20 -0
  41. package/scripts/download-model.js +43 -0
  42. package/scripts/mcp-launcher.js +49 -0
  43. package/scripts/postinstall.js +12 -0
  44. package/search-configs.js +36 -36
  45. package/.prettierrc +0 -7
  46. package/debug-pids.js +0 -30
  47. package/eslint.config.js +0 -36
  48. package/specs/plan.md +0 -23
  49. 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
+ }