smart-context-mcp 1.7.4 → 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/package.json +1 -1
- package/src/index-manager.js +133 -0
- package/src/tools/smart-context.js +5 -0
- package/src/tools/smart-search.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
|
-
"version": "1.7.
|
|
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) {
|