smart-context-mcp 1.9.0 → 1.10.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/README.md CHANGED
@@ -56,7 +56,7 @@ Restart your AI client. Done.
56
56
  # Check installed version
57
57
  npm list -g smart-context-mcp
58
58
 
59
- # Should show: smart-context-mcp@1.9.0 (or later)
59
+ # Should show: smart-context-mcp@1.10.0 (or later)
60
60
 
61
61
  # Update to latest version
62
62
  npm update -g smart-context-mcp
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
3
  "mcpName": "io.github.Arrayo/smart-context-mcp",
4
- "version": "1.9.0",
4
+ "version": "1.10.0",
5
5
  "description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
6
6
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
7
7
  "type": "module",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/Arrayo/smart-context-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.9.0",
9
+ "version": "1.10.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.9.0",
14
+ "version": "1.10.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -99,39 +99,35 @@ const parseRgLine = (line, root) => {
99
99
 
100
100
  const MAX_FILE_SIZE = '1M';
101
101
 
102
- const searchWithRipgrep = async (root, query) => {
102
+ const buildRgBaseArgs = () => {
103
103
  const args = [
104
104
  '--line-number',
105
105
  '--no-heading',
106
- '--color',
107
- 'never',
106
+ '--color', 'never',
108
107
  '--smart-case',
109
- '--fixed-strings',
110
108
  '--max-filesize', MAX_FILE_SIZE,
111
109
  ];
112
-
113
110
  for (const dir of ignoredDirs) {
114
111
  args.push('--glob', `!${dir}/**`);
115
112
  args.push('--glob', `!**/${dir}/**`);
116
113
  }
117
-
118
114
  for (const fileName of ignoredFileNames) {
119
115
  args.push('--glob', `!${fileName}`);
120
116
  }
121
-
122
117
  for (const extension of supportedGlobs) {
123
118
  args.push('--glob', extension);
124
119
  }
120
+ return args;
121
+ };
125
122
 
126
- args.push(query, '.');
127
-
123
+ const runRg = async (root, pattern, extraArgs = []) => {
124
+ const args = [...buildRgBaseArgs(), ...extraArgs, pattern, '.'];
128
125
  try {
129
126
  const { stdout } = await execFile(rgPath, args, {
130
127
  cwd: root,
131
128
  maxBuffer: 1024 * 1024 * 10,
132
129
  timeout: 15000,
133
130
  });
134
-
135
131
  return stdout
136
132
  .split('\n')
137
133
  .filter(Boolean)
@@ -140,11 +136,47 @@ const searchWithRipgrep = async (root, query) => {
140
136
  .filter((match) => !shouldIgnoreFile(match.file));
141
137
  } catch (error) {
142
138
  if (error.code === 1) return [];
143
- console.error('[smart-search] ripgrep failed:', error.message, { code: error.code, signal: error.signal });
139
+ process.stderr.write(`[smart-search] ripgrep failed: ${error.message}\n`);
144
140
  return null;
145
141
  }
146
142
  };
147
143
 
144
+ const extractTerms = (query) =>
145
+ query
146
+ .split(/[\s,;|/\\]+/)
147
+ .map((t) => t.trim())
148
+ .filter((t) => t.length >= 3);
149
+
150
+ const searchWithRipgrep = async (root, query) => {
151
+ // Pass 1: exact literal match
152
+ const exact = await runRg(root, query, ['--fixed-strings']);
153
+ if (exact === null) return null;
154
+ if (exact.length > 0) return { matches: exact, searchMode: 'exact' };
155
+
156
+ // Pass 2: regex (handles partial words, snake_case, camelCase fragments)
157
+ const escaped = query.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&');
158
+ const regex = await runRg(root, escaped);
159
+ if (regex === null) return null;
160
+ if (regex.length > 0) return { matches: regex, searchMode: 'regex' };
161
+
162
+ // Pass 3: term expansion — search each significant word independently and merge
163
+ const terms = extractTerms(query);
164
+ if (terms.length < 2) return { matches: [], searchMode: 'exact', zeroReason: 'no_matches' };
165
+
166
+ const seen = new Set();
167
+ const merged = [];
168
+ for (const term of terms) {
169
+ const hits = await runRg(root, term, ['--fixed-strings']);
170
+ if (!hits) continue;
171
+ for (const hit of hits) {
172
+ const key = `${hit.file}:${hit.lineNumber}`;
173
+ if (!seen.has(key)) { seen.add(key); merged.push({ ...hit, matchedTerm: term }); }
174
+ }
175
+ }
176
+
177
+ return { matches: merged, searchMode: 'terms', terms };
178
+ };
179
+
148
180
  const MAX_FALLBACK_FILE_BYTES = 1024 * 1024;
149
181
 
150
182
  export const isSmartCaseSensitive = (query) => query !== query.toLowerCase();
@@ -273,16 +305,43 @@ const groupMatches = (matches, query, intent, indexHits, graphHits) => {
273
305
  return { groups: sorted, breakdown };
274
306
  };
275
307
 
276
- const buildCompactResult = (groups, totalMatches, query, root) => {
308
+ const buildZeroResultsMessage = (query, searchMode, provenance) => {
309
+ const lines = [`No matches found for: "${query}"`];
310
+
311
+ if (searchMode === 'exact') {
312
+ lines.push('• Tried: exact literal match (--fixed-strings)');
313
+ lines.push('• Tried: regex match');
314
+ } else if (searchMode === 'terms') {
315
+ const terms = provenance?.expandedTerms ?? [];
316
+ lines.push(`• Tried: exact, regex, and term expansion (${terms.join(', ')})`);
317
+ }
318
+
319
+ lines.push('');
320
+ lines.push('Suggestions:');
321
+ lines.push(' – Use a shorter, more specific term (e.g. a function name, not a phrase)');
322
+ lines.push(' – Try Grep for raw text: the query may be in a file type not indexed by smart_search');
323
+ lines.push(' – Run build_index to enable symbol-level search if the codebase is new');
324
+
325
+ return lines.join('\n');
326
+ };
327
+
328
+ const buildCompactResult = (groups, totalMatches, query, root, searchMode, provenance) => {
329
+ if (totalMatches === 0) {
330
+ return buildZeroResultsMessage(query, searchMode, provenance);
331
+ }
332
+
333
+ const modeLabel = searchMode === 'exact' ? '' : searchMode === 'regex' ? ' [regex fallback]' : ` [term expansion: ${(provenance?.expandedTerms ?? []).join(', ')}]`;
334
+
277
335
  if (totalMatches <= 20) {
278
- return groups
336
+ const header = modeLabel ? `# Search mode:${modeLabel}\n\n` : '';
337
+ return header + groups
279
338
  .flatMap((group) => group.matches)
280
339
  .map(formatMatch)
281
340
  .join('\n');
282
341
  }
283
342
 
284
343
  const lines = [
285
- `query: ${query}`,
344
+ `query: ${query}${modeLabel}`,
286
345
  `root: ${root}`,
287
346
  `total matches: ${totalMatches}`,
288
347
  `matched files: ${groups.length}`,
@@ -314,12 +373,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
314
373
  }
315
374
 
316
375
  const root = resolveSafePath(cwd);
317
- const rgMatches = _testForceWalk ? null : await searchWithRipgrep(root, query);
318
- const usedFallback = rgMatches === null;
376
+ const rgResult = _testForceWalk ? null : await searchWithRipgrep(root, query);
377
+ const usedFallback = rgResult === null;
319
378
  const engine = usedFallback ? 'walk' : 'rg';
320
379
 
321
380
  let rawMatches;
322
381
  let provenance;
382
+ let searchMode = 'exact';
323
383
 
324
384
  if (usedFallback) {
325
385
  const fallback = searchWithFallback(root, query);
@@ -340,7 +400,9 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
340
400
  warnings,
341
401
  };
342
402
  } else {
343
- rawMatches = rgMatches;
403
+ rawMatches = rgResult.matches;
404
+ searchMode = rgResult.searchMode;
405
+ if (rgResult.terms) provenance = { expandedTerms: rgResult.terms };
344
406
  }
345
407
 
346
408
  rawMatches = rawMatches.filter((match) => !shouldIgnoreFile(match.file));
@@ -403,7 +465,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
403
465
  }
404
466
 
405
467
  const rawText = dedupedMatches.map(formatMatch).join('\n');
406
- const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root), 5000);
468
+ const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root, searchMode, provenance), 5000);
407
469
  const metrics = buildMetrics({
408
470
  tool: 'smart_search',
409
471
  target: `${root} :: ${query}`,
@@ -442,9 +504,11 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
442
504
  });
443
505
 
444
506
  let retrievalConfidence = 'high';
445
- if (provenance) {
446
- retrievalConfidence = provenance.skippedItemsTotal > 0 ? 'low' : 'medium';
447
- }
507
+ if (dedupedMatches.length === 0) retrievalConfidence = 'none';
508
+ else if (searchMode === 'terms') retrievalConfidence = 'low';
509
+ else if (searchMode === 'regex') retrievalConfidence = 'medium';
510
+ else if (usedFallback) retrievalConfidence = provenance?.skippedItemsTotal > 0 ? 'low' : 'medium';
511
+ else if (provenance?.skippedItemsTotal > 0) retrievalConfidence = 'low';
448
512
 
449
513
  const confidence = { level: retrievalConfidence, indexFreshness };
450
514