smart-context-mcp 1.7.3 → 1.7.6

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
@@ -83,7 +83,7 @@ The runner now covers:
83
83
 
84
84
  For Cursor projects, `smart-context-init` also generates `./.devctx/bin/cursor-devctx`, which routes through the same runner/policy stack.
85
85
 
86
- See [Task Runner Workflows](../../docs/task-runner.md) for the full behavior and command guidance.
86
+ See [Task Runner Workflows](https://github.com/Arrayo/smart-context-mcp/blob/main/docs/task-runner.md) for the full behavior and command guidance.
87
87
 
88
88
  ---
89
89
 
@@ -380,7 +380,7 @@ The MCP server provides **prompts** for automatic forcing:
380
380
  - Centrally managed
381
381
  - No typos
382
382
 
383
- See [MCP Prompts Documentation](../../docs/mcp-prompts.md).
383
+ See [MCP Prompts Documentation](https://github.com/Arrayo/smart-context-mcp/blob/main/docs/mcp-prompts.md).
384
384
 
385
385
  ## Core Tools
386
386
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
- "version": "1.7.3",
3
+ "version": "1.7.6",
4
4
  "description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
5
5
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
6
6
  "type": "module",
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile as execFileCallback } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { projectRoot } from './utils/paths.js';
6
+ import { loadIndex, buildIndex as buildIndexCore } from './index.js';
7
+
8
+ const execFile = promisify(execFileCallback);
9
+
10
+ const INDEX_FRESHNESS_MS = 24 * 60 * 60 * 1000;
11
+ const INDEX_BUILD_TIMEOUT_MS = 60000;
12
+
13
+ const resolveMetadataPath = (root = projectRoot) => {
14
+ return path.join(root, '.devctx', 'index-meta.json');
15
+ };
16
+
17
+ const loadIndexMetadata = (root = projectRoot) => {
18
+ try {
19
+ const metaPath = resolveMetadataPath(root);
20
+ const raw = fs.readFileSync(metaPath, 'utf8');
21
+ return JSON.parse(raw);
22
+ } catch {
23
+ return null;
24
+ }
25
+ };
26
+
27
+ const saveIndexMetadata = (meta, root = projectRoot) => {
28
+ try {
29
+ const metaPath = resolveMetadataPath(root);
30
+ const dir = path.dirname(metaPath);
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
35
+ } catch (error) {
36
+ console.warn('Failed to save index metadata:', error.message);
37
+ }
38
+ };
39
+
40
+ const getGitHead = (root = projectRoot) => {
41
+ try {
42
+ const gitHeadPath = path.join(root, '.git', 'HEAD');
43
+ if (!fs.existsSync(gitHeadPath)) return null;
44
+ return fs.readFileSync(gitHeadPath, 'utf8').trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ };
49
+
50
+ const isIndexFresh = (meta, root = projectRoot) => {
51
+ if (!meta || !meta.builtAt) return false;
52
+
53
+ const age = Date.now() - meta.builtAt;
54
+ if (age < INDEX_FRESHNESS_MS) return true;
55
+
56
+ const currentHead = getGitHead(root);
57
+ if (currentHead && currentHead === meta.gitHead) return true;
58
+
59
+ return false;
60
+ };
61
+
62
+ const timeout = (ms, message) => {
63
+ return new Promise((_, reject) => {
64
+ setTimeout(() => reject(new Error(message)), ms);
65
+ });
66
+ };
67
+
68
+ const isTestEnvironment = () => {
69
+ return process.env.NODE_ENV === 'test' ||
70
+ typeof process.env.NODE_TEST_CONTEXT !== 'undefined' ||
71
+ process.argv.some(arg => arg.includes('--test'));
72
+ };
73
+
74
+ export const ensureIndexReady = async (options = {}) => {
75
+ const { force = false, timeoutMs = INDEX_BUILD_TIMEOUT_MS, root = projectRoot, silent = isTestEnvironment() } = options;
76
+
77
+ if (!force) {
78
+ const existingIndex = loadIndex(root);
79
+ if (existingIndex) {
80
+ const meta = loadIndexMetadata(root);
81
+ if (isIndexFresh(meta, root)) {
82
+ return { status: 'ready', cached: true };
83
+ }
84
+ }
85
+ }
86
+
87
+ if (!silent) {
88
+ console.log('📦 Building search index (this may take 30-60s)...');
89
+ }
90
+
91
+ try {
92
+ const buildPromise = buildIndexCore({ root, incremental: true });
93
+ const result = await Promise.race([
94
+ buildPromise,
95
+ timeout(timeoutMs, 'Index build timeout')
96
+ ]);
97
+
98
+ saveIndexMetadata({
99
+ builtAt: Date.now(),
100
+ gitHead: getGitHead(root),
101
+ fileCount: result?.files?.length || 0,
102
+ version: result?.version
103
+ }, root);
104
+
105
+ if (!silent) {
106
+ console.log('✅ Index ready');
107
+ }
108
+ return { status: 'built', cached: false, fileCount: result?.files?.length || 0 };
109
+ } catch (error) {
110
+ if (!silent) {
111
+ console.warn('⚠️ Index build failed, search will use fallback mode');
112
+ }
113
+ return { status: 'fallback', error: error.message };
114
+ }
115
+ };
116
+
117
+ export const getIndexStatus = (root = projectRoot) => {
118
+ const index = loadIndex(root);
119
+ if (!index) {
120
+ return { available: false, fresh: false, reason: 'not_built' };
121
+ }
122
+
123
+ const meta = loadIndexMetadata(root);
124
+ const fresh = isIndexFresh(meta, root);
125
+
126
+ return {
127
+ available: true,
128
+ fresh,
129
+ builtAt: meta?.builtAt,
130
+ fileCount: meta?.fileCount,
131
+ age: meta?.builtAt ? Date.now() - meta.builtAt : null
132
+ };
133
+ };
@@ -6,6 +6,7 @@ import { smartSearch, VALID_INTENTS } from './smart-search.js';
6
6
  import { smartRead } from './smart-read.js';
7
7
  import { smartReadBatch } from './smart-read-batch.js';
8
8
  import { loadIndex, queryRelated, getGraphCoverage } from '../index.js';
9
+ import { ensureIndexReady } from '../index-manager.js';
9
10
  import { projectRoot } from '../utils/paths.js';
10
11
  import { resolveSafePath } from '../utils/fs.js';
11
12
  import { countTokens } from '../tokenCounter.js';
@@ -409,6 +410,8 @@ export const smartContext = async ({
409
410
  if (diff) {
410
411
  const changed = await getChangedFiles(diff, root);
411
412
 
413
+ await ensureIndexReady({ root });
414
+
412
415
  // Get detailed diff stats
413
416
  const detailedChanges = await getDetailedDiff(changed.ref, root);
414
417
  const index = loadIndex(root);
@@ -555,6 +558,8 @@ export const smartContext = async ({
555
558
  } catch { /* invalid path — skip */ }
556
559
  }
557
560
 
561
+ await ensureIndexReady({ root });
562
+
558
563
  const index = loadIndex(root);
559
564
 
560
565
  if (prefetch && !diff) {
@@ -14,6 +14,7 @@ import { recordDevctxOperation } from '../missed-opportunities.js';
14
14
  import { IGNORED_DIRS, IGNORED_FILE_NAMES } from '../config/ignored-paths.js';
15
15
  import { buildMetricsDisplay } from '../utils/metrics-display.js';
16
16
  import { createProgressReporter } from '../streaming.js';
17
+ import { ensureIndexReady } from '../index-manager.js';
17
18
 
18
19
  const execFile = promisify(execFileCallback);
19
20
  const supportedGlobs = [
@@ -354,6 +355,8 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
354
355
  progress.report({ phase: 'ranking', rawMatches: rawMatches.length });
355
356
  }
356
357
 
358
+ await ensureIndexReady({ root: indexRoot });
359
+
357
360
  try {
358
361
  loadedIndex = loadIndex(indexRoot);
359
362
  if (loadedIndex) {