@softerist/heuristic-mcp 3.0.17 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,6 @@
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
1
 
8
- // Registry configurations with their API endpoints and response parsers
2
+
3
+
9
4
  const REGISTRIES = {
10
5
  npm: {
11
6
  name: 'npm',
@@ -20,7 +15,7 @@ const REGISTRIES = {
20
15
  pattern: /^(?:pip:|pypi:)(.+)$/,
21
16
  url: (pkg) => `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`,
22
17
  parse: (data) => data.info.version,
23
- detect: () => false, // Requires explicit prefix
18
+ detect: () => false,
24
19
  },
25
20
  crates: {
26
21
  name: 'crates.io',
@@ -59,7 +54,7 @@ const REGISTRIES = {
59
54
  parse: (data) => {
60
55
  const pkgName = Object.keys(data.packages)[0];
61
56
  const versions = data.packages[pkgName];
62
- // Filter out dev versions and get latest stable
57
+
63
58
  const stable = versions.find((v) => !v.version.includes('dev'));
64
59
  return stable ? stable.version : versions[0].version;
65
60
  },
@@ -86,7 +81,7 @@ const REGISTRIES = {
86
81
  name: 'Maven Central',
87
82
  pattern: /^(?:maven:|java:)(.+)$/,
88
83
  url: (pkg) => {
89
- // Maven packages are in format group:artifact or group/artifact
84
+
90
85
  const [group, artifact] = pkg.includes(':') ? pkg.split(':') : pkg.split('/');
91
86
  if (!artifact) return null;
92
87
  return `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(group)}+AND+a:${encodeURIComponent(artifact)}&rows=1&wt=json`;
@@ -114,11 +109,9 @@ const REGISTRIES = {
114
109
  },
115
110
  };
116
111
 
117
- /**
118
- * Detect the registry for a package based on its name or prefix
119
- */
112
+
120
113
  function detectRegistry(packageName) {
121
- // Check for explicit prefixes first
114
+
122
115
  for (const [key, registry] of Object.entries(REGISTRIES)) {
123
116
  if (registry.pattern.test(packageName) && key !== 'npm') {
124
117
  const match = packageName.match(registry.pattern);
@@ -128,7 +121,7 @@ function detectRegistry(packageName) {
128
121
  }
129
122
  }
130
123
 
131
- // Try to detect based on package name patterns
124
+
132
125
  for (const registry of Object.values(REGISTRIES)) {
133
126
  if (registry.detect(packageName)) {
134
127
  const match = packageName.match(registry.pattern);
@@ -136,14 +129,12 @@ function detectRegistry(packageName) {
136
129
  }
137
130
  }
138
131
 
139
- // Default to npm
132
+
140
133
  const npmMatch = packageName.match(REGISTRIES.npm.pattern);
141
134
  return { registry: REGISTRIES.npm, cleanName: npmMatch ? npmMatch[1] : packageName };
142
135
  }
143
136
 
144
- /**
145
- * Fetch the latest version of a package from its registry
146
- */
137
+
147
138
  async function fetchPackageVersion(packageName, timeoutMs = 10000) {
148
139
  const { registry, cleanName } = detectRegistry(packageName);
149
140
 
@@ -220,9 +211,7 @@ async function fetchPackageVersion(packageName, timeoutMs = 10000) {
220
211
  }
221
212
  }
222
213
 
223
- /**
224
- * Get supported registries list for help text
225
- */
214
+
226
215
  function getSupportedRegistries() {
227
216
  return Object.entries(REGISTRIES).map(([key, reg]) => ({
228
217
  key,
@@ -231,7 +220,7 @@ function getSupportedRegistries() {
231
220
  }));
232
221
  }
233
222
 
234
- // MCP Tool definition
223
+
235
224
  export function getToolDefinition() {
236
225
  return {
237
226
  name: 'e_check_package_version',
@@ -253,12 +242,12 @@ export function getToolDefinition() {
253
242
  readOnlyHint: true,
254
243
  destructiveHint: false,
255
244
  idempotentHint: true,
256
- openWorldHint: true, // Makes external network requests
245
+ openWorldHint: true,
257
246
  },
258
247
  };
259
248
  }
260
249
 
261
- // Tool handler
250
+
262
251
  export async function handleToolCall(request) {
263
252
  const args = request.params?.arguments || {};
264
253
  const packageName = args.package;
@@ -298,5 +287,5 @@ export async function handleToolCall(request) {
298
287
  }
299
288
  }
300
289
 
301
- // Export for testing
290
+
302
291
  export { fetchPackageVersion, detectRegistry, getSupportedRegistries, REGISTRIES };
@@ -3,7 +3,6 @@ import { writeFileSync } from 'fs';
3
3
 
4
4
  import path from 'path';
5
5
  import os from 'os';
6
- import { fileURLToPath } from 'url';
7
6
  import {
8
7
  parseJsonc,
9
8
  upsertMcpServerEntryInText,
@@ -17,9 +16,9 @@ function getUserHomeDir() {
17
16
  return os.homedir();
18
17
  }
19
18
 
20
- // Detect which IDE is running the install
19
+
21
20
  function detectCurrentIDE() {
22
- // Check environment variables to determine which IDE is running
21
+
23
22
  if (process.env.ANTIGRAVITY_AGENT) {
24
23
  return 'Antigravity';
25
24
  }
@@ -47,18 +46,18 @@ function detectCurrentIDE() {
47
46
  return 'Windsurf';
48
47
  }
49
48
 
50
- // Claude Desktop doesn't have a known env var.
49
+
51
50
  return null;
52
51
  }
53
52
 
54
- // Known config paths for different IDEs
53
+
55
54
  function getConfigPaths() {
56
55
  const platform = process.platform;
57
56
  const home = getUserHomeDir();
58
57
  const currentIDE = detectCurrentIDE();
59
58
  const allPaths = [];
60
59
 
61
- // Antigravity - dedicated mcp_config.json
60
+
62
61
  allPaths.push({
63
62
  name: 'Antigravity',
64
63
  path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
@@ -66,7 +65,7 @@ function getConfigPaths() {
66
65
  canCreate: true,
67
66
  });
68
67
 
69
- // Codex - dedicated config.toml
68
+
70
69
  allPaths.push({
71
70
  name: 'Codex',
72
71
  path: path.join(home, '.codex', 'config.toml'),
@@ -74,7 +73,7 @@ function getConfigPaths() {
74
73
  canCreate: true,
75
74
  });
76
75
 
77
- // Claude Desktop - dedicated config file
76
+
78
77
  if (platform === 'darwin') {
79
78
  allPaths.push({
80
79
  name: 'Claude Desktop',
@@ -97,7 +96,7 @@ function getConfigPaths() {
97
96
  });
98
97
  }
99
98
 
100
- // Cursor - settings.json with mcpServers key
99
+
101
100
  if (platform === 'darwin') {
102
101
  allPaths.push({
103
102
  name: 'Cursor',
@@ -139,7 +138,7 @@ function getConfigPaths() {
139
138
  preferredContainerKey: 'mcpServers',
140
139
  });
141
140
 
142
- // Warp MCP config
141
+
143
142
  allPaths.push({
144
143
  name: 'Warp',
145
144
  path: path.join(home, '.warp', 'mcp_settings.json'),
@@ -157,7 +156,7 @@ function getConfigPaths() {
157
156
  });
158
157
  }
159
158
 
160
- // VS Code MCP registry (mcp.json uses "servers" key)
159
+
161
160
  if (platform === 'darwin') {
162
161
  allPaths.push({
163
162
  name: 'VS Code',
@@ -212,15 +211,15 @@ function getConfigPaths() {
212
211
  });
213
212
  }
214
213
 
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.
214
+
215
+
217
216
  return allPaths.map((entry) => ({
218
217
  ...entry,
219
218
  canCreate: entry.canCreate || entry.name === currentIDE,
220
219
  }));
221
220
  }
222
221
 
223
- // Helper to force output to terminal, bypassing npm's silence
222
+
224
223
  function forceLog(message) {
225
224
  try {
226
225
  if (process.platform !== 'win32') {
@@ -240,7 +239,7 @@ function normalizeIdeName(name) {
240
239
  .replace(/[\s_-]+/g, '');
241
240
  }
242
241
 
243
- function ideMatchesFilter(name, filter) {
242
+ function ideMatchesFilter(name, filter) {
244
243
  if (!filter) return true;
245
244
  const normalizedName = normalizeIdeName(name);
246
245
  const normalizedFilter = normalizeIdeName(filter);
@@ -261,27 +260,41 @@ function ideMatchesFilter(name, filter) {
261
260
  return normalizedName === 'vscode' || normalizedName === 'vscodeinsiders';
262
261
  }
263
262
 
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;
263
+ return false;
264
+ }
265
+
266
+ function getServerConfigForIde(name) {
267
+ const normalizedName = normalizeIdeName(name);
268
+ const config = {
269
+ command: 'heuristic-mcp',
270
+ args: [],
271
+ };
272
+
273
+ if (normalizedName === 'antigravity') {
274
+ // Prefer explicit workspace forwarding in VS Code-compatible clients.
275
+ // If the variable is not expanded by the IDE, CLI parsing safely ignores it.
276
+ config.args = [
277
+ '--workspace',
278
+ '${workspaceFolder}',
279
+ '--workspace',
280
+ '${workspaceRoot}',
281
+ '--workspace',
282
+ '${workspace}',
283
+ ];
284
+ // Allow provider-specific workspace env discovery as a backup signal.
285
+ config.env = {
286
+ HEURISTIC_MCP_ENABLE_DYNAMIC_WORKSPACE_ENV: 'true',
287
+ };
288
+ }
289
+
290
+ return config;
291
+ }
292
+
293
+ export async function register(filter = null) {
294
+ const currentIDE = detectCurrentIDE();
295
+
296
+ const configPaths = getConfigPaths();
297
+ let registeredCount = 0;
285
298
 
286
299
  forceLog(`[Auto-Register] Detecting IDE configurations...`);
287
300
 
@@ -291,7 +304,7 @@ export async function register(filter = null) {
291
304
  }
292
305
 
293
306
  try {
294
- // Check if file exists - create if canCreate is true for this IDE
307
+
295
308
  let fileExists = true;
296
309
 
297
310
  try {
@@ -299,7 +312,7 @@ export async function register(filter = null) {
299
312
  } catch {
300
313
  fileExists = false;
301
314
 
302
- // Only create config if this IDE allows it (has dedicated MCP config file)
315
+
303
316
  if (canCreate) {
304
317
  try {
305
318
  await fs.mkdir(path.dirname(configPath), { recursive: true });
@@ -311,14 +324,14 @@ export async function register(filter = null) {
311
324
  continue;
312
325
  }
313
326
  } else {
314
- // Skip IDEs that use shared settings files if they don't exist
327
+
315
328
  continue;
316
329
  }
317
330
  }
318
331
 
319
- let content = '';
320
- if (fileExists) {
321
- content = await fs.readFile(configPath, 'utf-8');
332
+ let content = '';
333
+ if (fileExists) {
334
+ content = await fs.readFile(configPath, 'utf-8');
322
335
  if (format === 'json' && content.trim()) {
323
336
  const parsed = parseJsonc(content);
324
337
  if (!parsed) {
@@ -327,12 +340,13 @@ export async function register(filter = null) {
327
340
  );
328
341
  continue;
329
342
  }
330
- }
331
- }
332
-
333
- const updated =
334
- format === 'toml'
335
- ? upsertMcpServerEntryInToml(content, 'heuristic-mcp', serverConfig)
343
+ }
344
+ }
345
+
346
+ const serverConfig = getServerConfigForIde(name);
347
+ const updated =
348
+ format === 'toml'
349
+ ? upsertMcpServerEntryInToml(content, 'heuristic-mcp', serverConfig)
336
350
  : upsertMcpServerEntryInText(
337
351
  content,
338
352
  'heuristic-mcp',
@@ -346,7 +360,7 @@ export async function register(filter = null) {
346
360
  continue;
347
361
  }
348
362
 
349
- // Write back synchronously to avoid race conditions
363
+
350
364
  writeFileSync(configPath, updated);
351
365
 
352
366
  forceLog(`\x1b[32m[Auto-Register] ✅ Successfully registered with ${name}\x1b[0m`);
@@ -356,18 +370,22 @@ export async function register(filter = null) {
356
370
  }
357
371
  }
358
372
 
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)
373
+ if (registeredCount === 0) {
374
+ const manualServerConfig = {
375
+ command: 'heuristic-mcp',
376
+ args: [],
377
+ };
378
+ forceLog(`[Auto-Register] No compatible IDE configurations found to update.`);
379
+ forceLog(
380
+ `[Auto-Register] Manual Config:\n${JSON.stringify({ mcpServers: { 'heuristic-mcp': manualServerConfig } }, null, 2)}`
381
+ );
382
+ } else {
383
+
366
384
  forceLog('\n\x1b[36m' + '='.repeat(60));
367
385
  forceLog(' 🚀 Heuristic MCP Installed & Configured! ');
368
386
  forceLog('='.repeat(60) + '\x1b[0m');
369
387
 
370
- // Show important paths
388
+
371
389
  const home = getUserHomeDir();
372
390
  const cacheRoot =
373
391
  process.platform === 'win32'
@@ -1,58 +1,41 @@
1
- /**
2
- * MCP Resources Feature
3
- *
4
- * Exposes workspace files as MCP resources for discovery and reading.
5
- */
1
+
6
2
 
7
3
  import fs from 'fs/promises';
8
4
  import path from 'path';
9
5
  import { fdir } from 'fdir';
10
6
  import { getMimeType } from '../lib/constants.js';
11
7
 
12
- /**
13
- * Convert a file path to a file:// URI.
14
- * @param {string} filePath - Absolute file path
15
- * @returns {string}
16
- */
8
+
17
9
  function pathToUri(filePath) {
18
- // Normalize path separators and encode for URI
10
+
19
11
  const normalized = filePath.replace(/\\/g, '/');
20
- // On Windows, paths start with drive letter like C:/
12
+
21
13
  if (/^[a-zA-Z]:/.test(normalized)) {
22
14
  return `file:///${normalized}`;
23
15
  }
24
16
  return `file://${normalized}`;
25
17
  }
26
18
 
27
- /**
28
- * Convert a file:// URI back to a file path.
29
- * @param {string} uri
30
- * @returns {string}
31
- */
19
+
32
20
  function uriToPath(uri) {
33
21
  if (!uri.startsWith('file://')) {
34
22
  throw new Error(`Invalid file URI: ${uri}`);
35
23
  }
36
- let filePath = uri.slice(7); // Remove 'file://'
37
- // Handle Windows paths (file:///C:/...)
24
+ let filePath = uri.slice(7);
25
+
38
26
  if (/^\/[a-zA-Z]:/.test(filePath)) {
39
- filePath = filePath.slice(1); // Remove leading /
27
+ filePath = filePath.slice(1);
40
28
  }
41
- // Decode URI components
29
+
42
30
  filePath = decodeURIComponent(filePath);
43
- // Normalize to OS path separators
31
+
44
32
  if (process.platform === 'win32') {
45
33
  filePath = filePath.replace(/\//g, '\\');
46
34
  }
47
35
  return filePath;
48
36
  }
49
37
 
50
- /**
51
- * Check if a path is within the workspace directory.
52
- * @param {string} filePath - Absolute file path
53
- * @param {string} workspaceDir - Workspace directory
54
- * @returns {boolean}
55
- */
38
+
56
39
  function isWithinWorkspace(filePath, workspaceDir) {
57
40
  const resolvedPath = path.resolve(filePath);
58
41
  const resolvedWorkspace = path.resolve(workspaceDir);
@@ -60,24 +43,20 @@ function isWithinWorkspace(filePath, workspaceDir) {
60
43
  return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
61
44
  }
62
45
 
63
- /**
64
- * List resources handler for MCP.
65
- * @param {object} config - Server configuration
66
- * @returns {Promise<{resources: Array}>}
67
- */
46
+
68
47
  export async function handleListResources(config) {
69
48
  const workspaceDir = config.searchDirectory;
70
- const maxResults = 500; // Limit to avoid overwhelming clients
49
+ const maxResults = 500;
71
50
 
72
- // Build set of allowed extensions from config
51
+
73
52
  const allowedExtensions = new Set(
74
53
  (config.fileExtensions || []).map(ext => `.${ext.toLowerCase()}`)
75
54
  );
76
55
 
77
- // Extract directory names from exclude patterns (e.g., '**/node_modules/**' -> 'node_modules')
56
+
78
57
  const excludedDirs = new Set();
79
58
  for (const pattern of config.excludePatterns || []) {
80
- // Match patterns like '**/dirname/**' or 'dirname/**'
59
+
81
60
  const match = pattern.match(/(?:\*\*\/)?([^/*]+)(?:\/\*\*)?$/);
82
61
  if (match && match[1] && !match[1].includes('*')) {
83
62
  excludedDirs.add(match[1]);
@@ -85,7 +64,7 @@ export async function handleListResources(config) {
85
64
  }
86
65
 
87
66
  try {
88
- // Use fdir to scan workspace
67
+
89
68
  const crawler = new fdir()
90
69
  .withBasePath()
91
70
  .withMaxDepth(10)
@@ -117,27 +96,22 @@ export async function handleListResources(config) {
117
96
  }
118
97
  }
119
98
 
120
- /**
121
- * Read resource handler for MCP.
122
- * @param {string} uri - Resource URI
123
- * @param {object} config - Server configuration
124
- * @returns {Promise<{contents: Array}>}
125
- */
99
+
126
100
  export async function handleReadResource(uri, config) {
127
101
  const workspaceDir = config.searchDirectory;
128
102
 
129
103
  try {
130
104
  const filePath = uriToPath(uri);
131
105
 
132
- // Security check: ensure path is within workspace
106
+
133
107
  if (!isWithinWorkspace(filePath, workspaceDir)) {
134
108
  throw new Error(`Access denied: ${uri} is outside workspace`);
135
109
  }
136
110
 
137
- // Check file exists
111
+
138
112
  await fs.access(filePath);
139
113
 
140
- // Read file content
114
+
141
115
  const content = await fs.readFile(filePath, 'utf-8');
142
116
 
143
117
  return {