ai-mind-map 1.6.2 → 1.7.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/dist/change-tracker/watcher.d.ts.map +1 -1
- package/dist/change-tracker/watcher.js +1 -0
- package/dist/change-tracker/watcher.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1396 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge-graph/graph.d.ts +5 -0
- package/dist/knowledge-graph/graph.d.ts.map +1 -1
- package/dist/knowledge-graph/graph.js +13 -1
- package/dist/knowledge-graph/graph.js.map +1 -1
- package/dist/knowledge-graph/indexer.d.ts +9 -0
- package/dist/knowledge-graph/indexer.d.ts.map +1 -1
- package/dist/knowledge-graph/indexer.js +339 -289
- package/dist/knowledge-graph/indexer.js.map +1 -1
- package/dist/memory/decision-log.d.ts.map +1 -1
- package/dist/memory/decision-log.js +11 -0
- package/dist/memory/decision-log.js.map +1 -1
- package/dist/memory/persistent-memory.d.ts.map +1 -1
- package/dist/memory/persistent-memory.js +7 -0
- package/dist/memory/persistent-memory.js.map +1 -1
- package/dist/memory/shared-sync.d.ts.map +1 -1
- package/dist/memory/shared-sync.js +6 -2
- package/dist/memory/shared-sync.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,1397 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AI Mind Map — MCP Server Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Creates the MCP server with stdio transport, registers all tools,
|
|
6
|
+
* initialises ALL real subsystems (knowledge graph, change tracker,
|
|
7
|
+
* persistent memory, context engine), and handles graceful shutdown.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ai-mind-map [--project-root <path>] [--db-path <path>] [--log-level debug|info|warn|error]
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import process from 'node:process';
|
|
15
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import Database from 'better-sqlite3';
|
|
18
|
+
import { loadConfig, parseCliArgs } from './config.js';
|
|
19
|
+
// ── Knowledge Graph ───────────────────────────────────────────
|
|
20
|
+
import { KnowledgeGraph } from './knowledge-graph/graph.js';
|
|
21
|
+
// Note: parser.ts exports functions (parseFile, parseFiles, etc.), not a class
|
|
22
|
+
import { Indexer } from './knowledge-graph/indexer.js';
|
|
23
|
+
import { PageRankEngine } from './knowledge-graph/pagerank.js';
|
|
24
|
+
// ── Change Tracker ────────────────────────────────────────────
|
|
25
|
+
import { FileWatcher } from './change-tracker/watcher.js';
|
|
26
|
+
import { DiffEngine } from './change-tracker/diff-engine.js';
|
|
27
|
+
import { ChangeLog } from './change-tracker/change-log.js';
|
|
28
|
+
// ── Memory ────────────────────────────────────────────────────
|
|
29
|
+
import { SessionMemory } from './memory/session-memory.js';
|
|
30
|
+
import { PersistentMemory } from './memory/persistent-memory.js';
|
|
31
|
+
import { DecisionLog } from './memory/decision-log.js';
|
|
32
|
+
import { syncSharedContext } from './memory/shared-sync.js';
|
|
33
|
+
// ── Context Engine ────────────────────────────────────────────
|
|
34
|
+
// compressor.ts exports functions: compress, detectContentType
|
|
35
|
+
import { compress as compressContent } from './context/compressor.js';
|
|
36
|
+
// progressive-disclosure.ts exports function: buildContextPackage
|
|
37
|
+
import { buildContextPackage } from './context/progressive-disclosure.js';
|
|
38
|
+
import { estimateTokens } from './utils/token-counter.js';
|
|
39
|
+
// ── Tools ─────────────────────────────────────────────────────
|
|
40
|
+
import { registerGraphTools } from './tools/graph-tools.js';
|
|
41
|
+
import { registerChangeTools } from './tools/change-tools.js';
|
|
42
|
+
import { registerMemoryTools } from './tools/memory-tools.js';
|
|
43
|
+
import { registerContextTools } from './tools/context-tools.js';
|
|
44
|
+
import { registerDebugTools } from './tools/debug-tools.js';
|
|
45
|
+
import { registerFlowTools } from './tools/flow-tools.js';
|
|
46
|
+
import { registerSnapshotTools } from './tools/snapshot-tools.js';
|
|
47
|
+
import { registerSmartTools } from './tools/smart-tools.js';
|
|
48
|
+
import { registerEvolvingTools } from './tools/evolving-tools.js';
|
|
49
|
+
import { registerAdvancedTools } from './tools/advanced-tools.js';
|
|
50
|
+
import { registerSemanticTools } from './tools/semantic-tools.js';
|
|
51
|
+
import { SemanticSearchEngine } from './knowledge-graph/semantic-search.js';
|
|
52
|
+
import { ChangelogEngine } from './knowledge-graph/changelog.js';
|
|
53
|
+
import { registerSessionTools } from './tools/session-tools.js';
|
|
54
|
+
import { registerDigestTools } from './tools/digest-tools.js';
|
|
55
|
+
// ============================================================
|
|
56
|
+
// Logger — writes to stderr so MCP stdio is uncontaminated
|
|
57
|
+
// ============================================================
|
|
58
|
+
const LOG_LEVELS = {
|
|
59
|
+
debug: 0,
|
|
60
|
+
info: 1,
|
|
61
|
+
warn: 2,
|
|
62
|
+
error: 3,
|
|
63
|
+
};
|
|
64
|
+
let currentLogLevel = LOG_LEVELS.info;
|
|
65
|
+
function setLogLevel(level) {
|
|
66
|
+
currentLogLevel = LOG_LEVELS[level];
|
|
67
|
+
}
|
|
68
|
+
function log(level, message, ...extra) {
|
|
69
|
+
if (LOG_LEVELS[level] < currentLogLevel)
|
|
70
|
+
return;
|
|
71
|
+
const ts = new Date().toISOString();
|
|
72
|
+
const prefix = `[${ts}] [${level.toUpperCase()}]`;
|
|
73
|
+
if (extra.length > 0) {
|
|
74
|
+
process.stderr.write(`${prefix} ${message} ${JSON.stringify(extra)}\n`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
process.stderr.write(`${prefix} ${message}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ============================================================
|
|
81
|
+
// Adapters — Bridge real implementations to tool interfaces
|
|
82
|
+
// ============================================================
|
|
83
|
+
/**
|
|
84
|
+
* Creates an adapter that satisfies IKnowledgeGraph from the real
|
|
85
|
+
* KnowledgeGraph and PageRankEngine classes.
|
|
86
|
+
*
|
|
87
|
+
* Key API mappings:
|
|
88
|
+
* - graph.search(query, limit) — FTS5 search, no type filter param
|
|
89
|
+
* - graph.getProjectOverview() — returns Map<string, GraphNode[]>, no args
|
|
90
|
+
* - graph.findCallers(nodeId) / graph.findCallees(nodeId) — single nodeId arg
|
|
91
|
+
* - graph.getNodesByName(name) — returns GraphNode[]
|
|
92
|
+
* - graph.getFileStructure(filePath) — returns GraphNode[]
|
|
93
|
+
*/
|
|
94
|
+
function createGraphAdapter(graph, pagerank) {
|
|
95
|
+
return {
|
|
96
|
+
search: (query, type, limit) => {
|
|
97
|
+
const maxResults = limit ?? 20;
|
|
98
|
+
let results = graph.search(query, maxResults);
|
|
99
|
+
// Expand with learned search aliases
|
|
100
|
+
try {
|
|
101
|
+
const aliases = graph.getLearnedSearchAliases();
|
|
102
|
+
const lowerQuery = query.toLowerCase();
|
|
103
|
+
for (const alias of aliases) {
|
|
104
|
+
if (alias.term.toLowerCase() === lowerQuery) {
|
|
105
|
+
for (const alt of alias.aliases) {
|
|
106
|
+
if (results.length >= maxResults)
|
|
107
|
+
break;
|
|
108
|
+
const aliasResults = graph.search(alt, Math.max(3, maxResults - results.length));
|
|
109
|
+
const existingIds = new Set(results.map(r => r.id));
|
|
110
|
+
for (const r of aliasResults) {
|
|
111
|
+
if (!existingIds.has(r.id) && results.length < maxResults) {
|
|
112
|
+
results.push(r);
|
|
113
|
+
existingIds.add(r.id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Touch the alias to track usage
|
|
118
|
+
try {
|
|
119
|
+
graph.touchLearnedRule(alias.id);
|
|
120
|
+
}
|
|
121
|
+
catch { }
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Learned rules table might not exist
|
|
128
|
+
}
|
|
129
|
+
if (type) {
|
|
130
|
+
return results.filter((n) => n.type === type);
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
},
|
|
134
|
+
getStructure: (depth) => {
|
|
135
|
+
// graph.getProjectOverview() returns Map<string, GraphNode[]>
|
|
136
|
+
// We convert it to the shape expected by IKnowledgeGraph.getStructure
|
|
137
|
+
const overview = graph.getProjectOverview();
|
|
138
|
+
const files = [];
|
|
139
|
+
for (const [filePath, nodes] of overview) {
|
|
140
|
+
files.push({
|
|
141
|
+
path: filePath,
|
|
142
|
+
symbols: nodes.map(n => ({
|
|
143
|
+
name: n.name,
|
|
144
|
+
type: n.type,
|
|
145
|
+
signature: n.signature,
|
|
146
|
+
})),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// depth is accepted but getProjectOverview doesn't take a depth arg;
|
|
150
|
+
// we can slice results if needed, but return all for now.
|
|
151
|
+
return { files };
|
|
152
|
+
},
|
|
153
|
+
traceDependencies: (symbolName, direction, depth) => {
|
|
154
|
+
const nodes = graph.getNodesByName(symbolName);
|
|
155
|
+
if (nodes.length === 0) {
|
|
156
|
+
return { root: symbolName, direction, depth, nodes: [], edges: [] };
|
|
157
|
+
}
|
|
158
|
+
const rootNode = nodes[0];
|
|
159
|
+
let traced = [];
|
|
160
|
+
// findCallers(nodeId) and findCallees(nodeId) each take a single string arg
|
|
161
|
+
if (direction === 'callers' || direction === 'both') {
|
|
162
|
+
traced = traced.concat(graph.findCallers(rootNode.id));
|
|
163
|
+
}
|
|
164
|
+
if (direction === 'callees' || direction === 'both') {
|
|
165
|
+
traced = traced.concat(graph.findCallees(rootNode.id));
|
|
166
|
+
}
|
|
167
|
+
// Deduplicate
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
const unique = traced.filter(n => {
|
|
170
|
+
if (seen.has(n.id))
|
|
171
|
+
return false;
|
|
172
|
+
seen.add(n.id);
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
// Collect relevant edges
|
|
176
|
+
const edgeSet = [];
|
|
177
|
+
for (const n of unique) {
|
|
178
|
+
const outEdges = graph.getOutEdges(n.id);
|
|
179
|
+
const inEdges = graph.getInEdges(n.id);
|
|
180
|
+
edgeSet.push(...outEdges, ...inEdges);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
root: symbolName,
|
|
184
|
+
direction,
|
|
185
|
+
depth,
|
|
186
|
+
nodes: unique,
|
|
187
|
+
edges: edgeSet,
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
getSignature: (symbolName, filePath) => {
|
|
191
|
+
const nodes = graph.getNodesByName(symbolName);
|
|
192
|
+
let match;
|
|
193
|
+
if (filePath) {
|
|
194
|
+
match = nodes.find((n) => n.filePath === filePath);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
match = nodes[0];
|
|
198
|
+
}
|
|
199
|
+
if (!match)
|
|
200
|
+
return null;
|
|
201
|
+
// Return the shape expected by IKnowledgeGraph.getSignature
|
|
202
|
+
return {
|
|
203
|
+
node: match,
|
|
204
|
+
parameters: match.parameters,
|
|
205
|
+
returnType: match.returnType,
|
|
206
|
+
docComment: match.docComment,
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
findReferences: (symbolName) => {
|
|
210
|
+
const nodes = graph.getNodesByName(symbolName);
|
|
211
|
+
if (nodes.length === 0) {
|
|
212
|
+
return { symbol: symbolName, references: [] };
|
|
213
|
+
}
|
|
214
|
+
// Find all callers of the first matching node as "references"
|
|
215
|
+
const callers = graph.findCallers(nodes[0].id);
|
|
216
|
+
return {
|
|
217
|
+
symbol: symbolName,
|
|
218
|
+
references: callers.map((n) => ({
|
|
219
|
+
filePath: n.filePath,
|
|
220
|
+
line: n.startLine,
|
|
221
|
+
context: n.signature,
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
getFileMap: (filePath) => {
|
|
226
|
+
const nodes = graph.getFileStructure(filePath);
|
|
227
|
+
if (!nodes || nodes.length === 0)
|
|
228
|
+
return null;
|
|
229
|
+
return {
|
|
230
|
+
filePath,
|
|
231
|
+
symbols: nodes
|
|
232
|
+
.filter(n => n.type !== 'file')
|
|
233
|
+
.map(n => ({
|
|
234
|
+
name: n.name,
|
|
235
|
+
type: n.type,
|
|
236
|
+
signature: n.signature,
|
|
237
|
+
startLine: n.startLine,
|
|
238
|
+
endLine: n.endLine,
|
|
239
|
+
visibility: n.visibility,
|
|
240
|
+
isExported: n.isExported,
|
|
241
|
+
})),
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Creates an adapter that satisfies IChangeTracker from the real
|
|
248
|
+
* DiffEngine and ChangeLog classes.
|
|
249
|
+
*
|
|
250
|
+
* Key API mappings:
|
|
251
|
+
* - changeLog.getLatestSession() — returns ChangeSession | null
|
|
252
|
+
* - changeLog.queryChanges(options) — options has `since` (timestamp), not `afterTimestamp`
|
|
253
|
+
* - changeLog.generateSessionSummary(sessionId) — returns string
|
|
254
|
+
* - changeLog.recordChange(change) — records a FileChange
|
|
255
|
+
*/
|
|
256
|
+
function createChangeAdapter(diffEngine, changeLog, graph) {
|
|
257
|
+
return {
|
|
258
|
+
getChanges: (since) => {
|
|
259
|
+
try {
|
|
260
|
+
const latestSession = changeLog.getLatestSession();
|
|
261
|
+
const sinceTimestamp = since === 'last_session'
|
|
262
|
+
? (latestSession?.endedAt ?? Date.now() - 86400000)
|
|
263
|
+
: since === 'today'
|
|
264
|
+
? new Date().setHours(0, 0, 0, 0)
|
|
265
|
+
: since === 'this_week'
|
|
266
|
+
? Date.now() - 7 * 86400000
|
|
267
|
+
: new Date(since).getTime();
|
|
268
|
+
// ChangeLog.queryChanges uses ChangeQueryOptions with `since` field (timestamp)
|
|
269
|
+
const changes = changeLog.queryChanges({
|
|
270
|
+
since: sinceTimestamp,
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
since,
|
|
274
|
+
resolvedTimestamp: sinceTimestamp,
|
|
275
|
+
changes,
|
|
276
|
+
totalFilesChanged: new Set(changes.map(c => c.filePath)).size,
|
|
277
|
+
totalLinesAdded: changes.reduce((sum, c) => sum + c.linesAdded, 0),
|
|
278
|
+
totalLinesRemoved: changes.reduce((sum, c) => sum + c.linesRemoved, 0),
|
|
279
|
+
summary: changes.length > 0
|
|
280
|
+
? changes.map(c => `- ${c.filePath}: ${c.summary}`).join('\n')
|
|
281
|
+
: 'No changes found since the specified time.',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return {
|
|
286
|
+
since,
|
|
287
|
+
resolvedTimestamp: Date.now(),
|
|
288
|
+
changes: [],
|
|
289
|
+
totalFilesChanged: 0,
|
|
290
|
+
totalLinesAdded: 0,
|
|
291
|
+
totalLinesRemoved: 0,
|
|
292
|
+
summary: 'Unable to retrieve changes.',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
getSessionDiff: () => {
|
|
297
|
+
try {
|
|
298
|
+
const latestSession = changeLog.getLatestSession();
|
|
299
|
+
if (!latestSession) {
|
|
300
|
+
return {
|
|
301
|
+
previousSession: null,
|
|
302
|
+
changes: [],
|
|
303
|
+
affectedSymbols: [],
|
|
304
|
+
summary: 'No previous session found.',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const changes = changeLog.queryChanges({
|
|
308
|
+
since: latestSession.endedAt ?? latestSession.startedAt,
|
|
309
|
+
});
|
|
310
|
+
const symbols = changes.flatMap(c => c.symbolsAffected);
|
|
311
|
+
return {
|
|
312
|
+
previousSession: latestSession,
|
|
313
|
+
changes,
|
|
314
|
+
affectedSymbols: [...new Set(symbols)],
|
|
315
|
+
summary: changeLog.generateSessionSummary(latestSession.sessionId),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return {
|
|
320
|
+
previousSession: null,
|
|
321
|
+
changes: [],
|
|
322
|
+
affectedSymbols: [],
|
|
323
|
+
summary: 'Unable to compute session diff.',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
analyseImpact: (params) => {
|
|
328
|
+
const target = params.filePath ?? params.symbolName ?? 'unknown';
|
|
329
|
+
// Try to find the node and compute blast radius
|
|
330
|
+
let directlyAffected = [];
|
|
331
|
+
let transitivelyAffected = [];
|
|
332
|
+
let riskLevel = 'low';
|
|
333
|
+
try {
|
|
334
|
+
if (params.symbolName) {
|
|
335
|
+
const nodes = graph.getNodesByName(params.symbolName);
|
|
336
|
+
if (nodes.length > 0) {
|
|
337
|
+
const rootNode = nodes[0];
|
|
338
|
+
const callers = graph.findCallers(rootNode.id);
|
|
339
|
+
directlyAffected = callers.map(n => ({
|
|
340
|
+
node: n,
|
|
341
|
+
relationship: 'calls',
|
|
342
|
+
}));
|
|
343
|
+
const blastNodes = graph.blastRadius(rootNode.id, 3);
|
|
344
|
+
transitivelyAffected = blastNodes.map((n, idx) => ({
|
|
345
|
+
node: n,
|
|
346
|
+
depth: Math.min(idx + 1, 3),
|
|
347
|
+
}));
|
|
348
|
+
const total = directlyAffected.length + transitivelyAffected.length;
|
|
349
|
+
riskLevel = total > 20 ? 'high' : total > 5 ? 'medium' : 'low';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (params.filePath) {
|
|
353
|
+
const fileNodes = graph.getFileStructure(params.filePath);
|
|
354
|
+
for (const node of fileNodes) {
|
|
355
|
+
if (node.type === 'file')
|
|
356
|
+
continue;
|
|
357
|
+
const callers = graph.findCallers(node.id);
|
|
358
|
+
for (const caller of callers) {
|
|
359
|
+
directlyAffected.push({ node: caller, relationship: 'calls' });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
riskLevel = directlyAffected.length > 10 ? 'high'
|
|
363
|
+
: directlyAffected.length > 3 ? 'medium' : 'low';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Fall through with empty arrays
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
target,
|
|
371
|
+
directlyAffected,
|
|
372
|
+
transitivelyAffected,
|
|
373
|
+
riskLevel,
|
|
374
|
+
summary: directlyAffected.length > 0
|
|
375
|
+
? `${target}: ${directlyAffected.length} directly affected, ${transitivelyAffected.length} transitively affected. Risk: ${riskLevel}.`
|
|
376
|
+
: `${target}: No dependents found. Use mindmap_trace_dependencies for full dependency chain.`,
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Creates an adapter that satisfies IMemoryStore from PersistentMemory,
|
|
383
|
+
* DecisionLog, and SessionMemory.
|
|
384
|
+
*
|
|
385
|
+
* Key API mappings:
|
|
386
|
+
* - persistentMemory.queryMemories(query) — query uses MemoryQuery shape
|
|
387
|
+
* - persistentMemory.createMemory(input) — input is CreateMemoryInput
|
|
388
|
+
* - persistentMemory.getStats() — returns MemoryStats
|
|
389
|
+
* - decisionLog.queryDecisions(query) — query uses DecisionQuery
|
|
390
|
+
* - decisionLog.createDecision(input) — returns { decision, conflicts }
|
|
391
|
+
* - decisionLog.getActiveDecisions() — returns Decision[]
|
|
392
|
+
* - sessionMemory.listRecentSessions(limit) — returns SessionListItem[]
|
|
393
|
+
*/
|
|
394
|
+
function createMemoryAdapter(persistentMemory, decisionLog, sessionMemory) {
|
|
395
|
+
return {
|
|
396
|
+
recall: (query, category, limit) => {
|
|
397
|
+
return persistentMemory.queryMemories({
|
|
398
|
+
text: query,
|
|
399
|
+
categories: category ? [category] : undefined,
|
|
400
|
+
limit: limit ?? 10,
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
remember: (params) => {
|
|
404
|
+
return persistentMemory.createMemory({
|
|
405
|
+
category: params.category,
|
|
406
|
+
content: params.content,
|
|
407
|
+
importance: params.importance,
|
|
408
|
+
tags: params.tags,
|
|
409
|
+
relatedFiles: params.relatedFiles,
|
|
410
|
+
sessionId: params.sessionId,
|
|
411
|
+
source: params.source,
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
getDecisions: (params) => {
|
|
415
|
+
if (params.query) {
|
|
416
|
+
// Use DecisionLog.queryDecisions with text search
|
|
417
|
+
const results = decisionLog.queryDecisions({
|
|
418
|
+
text: params.query,
|
|
419
|
+
status: params.status === 'all' ? undefined : (params.status ?? 'active'),
|
|
420
|
+
});
|
|
421
|
+
return results;
|
|
422
|
+
}
|
|
423
|
+
if (!params.status || params.status === 'active') {
|
|
424
|
+
return decisionLog.getActiveDecisions();
|
|
425
|
+
}
|
|
426
|
+
// 'all' — query with no filters
|
|
427
|
+
return decisionLog.queryDecisions({});
|
|
428
|
+
},
|
|
429
|
+
decide: (params) => {
|
|
430
|
+
// DecisionLog.createDecision returns { decision, conflicts }
|
|
431
|
+
const result = decisionLog.createDecision({
|
|
432
|
+
title: params.title,
|
|
433
|
+
description: params.description,
|
|
434
|
+
rationale: params.rationale,
|
|
435
|
+
alternatives: params.alternatives,
|
|
436
|
+
consequences: params.consequences,
|
|
437
|
+
relatedFiles: params.relatedFiles,
|
|
438
|
+
tags: params.tags,
|
|
439
|
+
decidedBy: params.decidedBy,
|
|
440
|
+
});
|
|
441
|
+
return result.decision;
|
|
442
|
+
},
|
|
443
|
+
getSessionSummaries: (count) => {
|
|
444
|
+
// sessionMemory.listRecentSessions returns SessionListItem[]
|
|
445
|
+
// We need to return SessionSummary[], so we adapt
|
|
446
|
+
const sessions = sessionMemory.listRecentSessions(count);
|
|
447
|
+
return sessions.map(s => {
|
|
448
|
+
// Try to get full session details if available
|
|
449
|
+
const full = sessionMemory.getSession(s.sessionId);
|
|
450
|
+
if (full) {
|
|
451
|
+
return {
|
|
452
|
+
sessionId: full.sessionId,
|
|
453
|
+
startedAt: full.startedAt,
|
|
454
|
+
endedAt: full.endedAt,
|
|
455
|
+
tasksCompleted: full.tasksCompleted,
|
|
456
|
+
filesModified: full.filesModified,
|
|
457
|
+
decisionseMade: full.decisionseMade,
|
|
458
|
+
memoriesCreated: full.memoriesCreated,
|
|
459
|
+
tokensSaved: full.tokensSaved,
|
|
460
|
+
summary: full.summary,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
// Fallback: return minimal SessionSummary from SessionListItem
|
|
464
|
+
return {
|
|
465
|
+
sessionId: s.sessionId,
|
|
466
|
+
startedAt: s.startedAt,
|
|
467
|
+
endedAt: s.endedAt ?? Date.now(),
|
|
468
|
+
tasksCompleted: [],
|
|
469
|
+
filesModified: [],
|
|
470
|
+
decisionseMade: [],
|
|
471
|
+
memoriesCreated: 0,
|
|
472
|
+
tokensSaved: 0,
|
|
473
|
+
summary: s.summary,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Creates an adapter that satisfies ISessionProvider.
|
|
481
|
+
* sessionMemory.getCurrentSessionId() returns string | null.
|
|
482
|
+
* The interface expects string, so we provide a fallback.
|
|
483
|
+
*/
|
|
484
|
+
function createSessionAdapter(sessionMemory) {
|
|
485
|
+
return {
|
|
486
|
+
currentSessionId: () => {
|
|
487
|
+
return sessionMemory.getCurrentSessionId() ?? 'no-session';
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Creates an adapter that satisfies IContextEngine.
|
|
493
|
+
*
|
|
494
|
+
* Key API mappings:
|
|
495
|
+
* - compress(text, level, contentType) — module-level function from compressor.ts
|
|
496
|
+
* - buildContextPackage(...) — module-level function from progressive-disclosure.ts
|
|
497
|
+
*/
|
|
498
|
+
function createContextAdapter(graph, persistentMemory, decisionLog, changeLog, config) {
|
|
499
|
+
return {
|
|
500
|
+
getContext: (params) => {
|
|
501
|
+
try {
|
|
502
|
+
// Build ProjectInfo for Tier 1
|
|
503
|
+
const overview = graph.getProjectOverview();
|
|
504
|
+
const stats = graph.getStats();
|
|
505
|
+
const directoryTree = Array.from(overview.keys())
|
|
506
|
+
.slice(0, 20)
|
|
507
|
+
.map(f => ` ${f}`)
|
|
508
|
+
.join('\n');
|
|
509
|
+
const projectInfo = {
|
|
510
|
+
name: path.basename(config.projectRoot),
|
|
511
|
+
description: params.taskDescription,
|
|
512
|
+
techStack: Object.keys(stats.languageBreakdown),
|
|
513
|
+
directoryTree,
|
|
514
|
+
conventions: [],
|
|
515
|
+
currentTask: params.taskDescription,
|
|
516
|
+
};
|
|
517
|
+
// Build Tier 2 data
|
|
518
|
+
const tier2Data = {};
|
|
519
|
+
if (params.includeMemories) {
|
|
520
|
+
tier2Data.memories = persistentMemory.queryMemories({
|
|
521
|
+
text: params.taskDescription,
|
|
522
|
+
limit: 5,
|
|
523
|
+
});
|
|
524
|
+
tier2Data.decisions = decisionLog.getActiveDecisions();
|
|
525
|
+
}
|
|
526
|
+
if (params.includeChanges) {
|
|
527
|
+
tier2Data.recentChanges = changeLog.queryChanges({
|
|
528
|
+
limit: 10,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
// Search graph for relevant nodes
|
|
532
|
+
const graphNodes = graph.search(params.taskDescription, 10);
|
|
533
|
+
if (graphNodes.length > 0) {
|
|
534
|
+
tier2Data.graphNodes = graphNodes;
|
|
535
|
+
}
|
|
536
|
+
// Build context package
|
|
537
|
+
const pkg = buildContextPackage(projectInfo, tier2Data, {}, // Tier 3 data — empty for initial load
|
|
538
|
+
config.tokenBudgets, params.taskDescription);
|
|
539
|
+
return pkg;
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return {
|
|
543
|
+
tier1: 'Project context loading failed.',
|
|
544
|
+
tier2: '',
|
|
545
|
+
tier3: '',
|
|
546
|
+
totalTokens: 10,
|
|
547
|
+
tokensSaved: 0,
|
|
548
|
+
breakdown: [],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
compress: (params) => {
|
|
553
|
+
// compress(text, level, contentType?) from compressor.ts
|
|
554
|
+
// Returns CompressionResult { compressed, originalTokens, compressedTokens, ratio, contentType, level }
|
|
555
|
+
const result = compressContent(params.content, params.level, params.contentType);
|
|
556
|
+
return {
|
|
557
|
+
original: params.content,
|
|
558
|
+
compressed: result.compressed,
|
|
559
|
+
originalTokens: result.originalTokens,
|
|
560
|
+
compressedTokens: result.compressedTokens,
|
|
561
|
+
ratio: result.ratio,
|
|
562
|
+
contentType: result.contentType,
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Creates an adapter that satisfies IIndexer.
|
|
569
|
+
*
|
|
570
|
+
* Key API mappings:
|
|
571
|
+
* - indexer.fullIndex(onProgress?) — returns Promise<IndexStats>
|
|
572
|
+
* IndexStats has: filesScanned, filesParsed, filesSkipped, filesDeleted,
|
|
573
|
+
* nodesCreated, edgesCreated, parseErrors, durationMs, languages
|
|
574
|
+
* - graph.getStats() — returns { totalNodes, totalEdges, totalFiles, nodesByType, edgesByType, languageBreakdown }
|
|
575
|
+
* - persistentMemory.getStats() — returns MemoryStats
|
|
576
|
+
* - changeLog.getStats(topN?) — returns ChangeLogStats
|
|
577
|
+
*/
|
|
578
|
+
function createIndexerAdapter(indexer, graph, persistentMemory, decisionLog, changeLog, config, watcher) {
|
|
579
|
+
return {
|
|
580
|
+
reindex: async () => {
|
|
581
|
+
const startTime = Date.now();
|
|
582
|
+
try {
|
|
583
|
+
const result = await indexer.fullIndex();
|
|
584
|
+
return {
|
|
585
|
+
filesScanned: result.filesScanned,
|
|
586
|
+
filesIndexed: result.filesParsed,
|
|
587
|
+
nodesCreated: result.nodesCreated,
|
|
588
|
+
edgesCreated: result.edgesCreated,
|
|
589
|
+
durationMs: result.durationMs,
|
|
590
|
+
errors: result.parseErrors > 0
|
|
591
|
+
? [`${result.parseErrors} parse errors encountered`]
|
|
592
|
+
: [],
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
return {
|
|
597
|
+
filesScanned: 0,
|
|
598
|
+
filesIndexed: 0,
|
|
599
|
+
nodesCreated: 0,
|
|
600
|
+
edgesCreated: 0,
|
|
601
|
+
durationMs: Date.now() - startTime,
|
|
602
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
reindexProject: async (projectPath) => {
|
|
607
|
+
const startTime = Date.now();
|
|
608
|
+
const resolvedPath = path.resolve(projectPath);
|
|
609
|
+
try {
|
|
610
|
+
// Re-target the indexer to the new project
|
|
611
|
+
indexer.setProjectRoot(resolvedPath);
|
|
612
|
+
log('info', `📠Re-targeted to project: ${resolvedPath}`);
|
|
613
|
+
// Also watch the new project directory for changes
|
|
614
|
+
if (watcher) {
|
|
615
|
+
watcher.addRoot(resolvedPath);
|
|
616
|
+
log('info', `ðŸ‘ï¸ File watcher now also watching: ${resolvedPath}`);
|
|
617
|
+
}
|
|
618
|
+
// Run full index on the new project (don't clear — multi-project)
|
|
619
|
+
const result = await indexer.fullIndex();
|
|
620
|
+
return {
|
|
621
|
+
filesScanned: result.filesScanned,
|
|
622
|
+
filesIndexed: result.filesParsed,
|
|
623
|
+
nodesCreated: result.nodesCreated,
|
|
624
|
+
edgesCreated: result.edgesCreated,
|
|
625
|
+
durationMs: result.durationMs,
|
|
626
|
+
errors: result.parseErrors > 0
|
|
627
|
+
? [`${result.parseErrors} parse errors encountered`]
|
|
628
|
+
: [],
|
|
629
|
+
projectRoot: resolvedPath,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
catch (err) {
|
|
633
|
+
return {
|
|
634
|
+
filesScanned: 0,
|
|
635
|
+
filesIndexed: 0,
|
|
636
|
+
nodesCreated: 0,
|
|
637
|
+
edgesCreated: 0,
|
|
638
|
+
durationMs: Date.now() - startTime,
|
|
639
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
640
|
+
projectRoot: resolvedPath,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
getStats: () => {
|
|
645
|
+
let dbSize = 0;
|
|
646
|
+
try {
|
|
647
|
+
if (existsSync(config.dbPath)) {
|
|
648
|
+
dbSize = statSync(config.dbPath).size;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// ignore
|
|
653
|
+
}
|
|
654
|
+
const graphStats = graph.getStats();
|
|
655
|
+
const memoryStats = persistentMemory.getStats();
|
|
656
|
+
const changeStats = changeLog.getStats();
|
|
657
|
+
// DecisionLog doesn't have a count() method; use queryDecisions
|
|
658
|
+
const allDecisions = decisionLog.queryDecisions({});
|
|
659
|
+
return {
|
|
660
|
+
projectRoot: config.projectRoot,
|
|
661
|
+
indexedFiles: graphStats.totalFiles,
|
|
662
|
+
totalNodes: graphStats.totalNodes,
|
|
663
|
+
totalEdges: graphStats.totalEdges,
|
|
664
|
+
totalMemories: memoryStats.totalMemories,
|
|
665
|
+
totalDecisions: allDecisions.length,
|
|
666
|
+
totalChangesTracked: changeStats.totalChanges,
|
|
667
|
+
lastIndexedAt: null, // Graph doesn't track this directly
|
|
668
|
+
lastChangeAt: null,
|
|
669
|
+
dbSizeBytes: dbSize,
|
|
670
|
+
languageBreakdown: graphStats.languageBreakdown,
|
|
671
|
+
tokensSavedEstimate: graphStats.totalNodes * 500,
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
// ============================================================
|
|
677
|
+
// Ensure database directory exists
|
|
678
|
+
// ============================================================
|
|
679
|
+
function ensureDbDirectory(dbPath) {
|
|
680
|
+
const dir = path.dirname(dbPath);
|
|
681
|
+
if (!existsSync(dir)) {
|
|
682
|
+
mkdirSync(dir, { recursive: true });
|
|
683
|
+
log('info', `Created database directory: ${dir}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// ============================================================
|
|
687
|
+
// Session Token Tracker
|
|
688
|
+
// ============================================================
|
|
689
|
+
/**
|
|
690
|
+
* Tracks cumulative token usage across all tool calls in a session.
|
|
691
|
+
* Every tool response is enriched with `_sessionTokens` metadata
|
|
692
|
+
* so AI agents always know their token footprint.
|
|
693
|
+
*/
|
|
694
|
+
class SessionTokenTracker {
|
|
695
|
+
totalToolCalls = 0;
|
|
696
|
+
totalOutputTokens = 0;
|
|
697
|
+
totalInputTokens = 0;
|
|
698
|
+
totalTokensSaved = 0;
|
|
699
|
+
startTime = Date.now();
|
|
700
|
+
/** Record a tool call with its input and output token counts. */
|
|
701
|
+
record(inputTokens, outputTokens, tokensSaved) {
|
|
702
|
+
this.totalToolCalls++;
|
|
703
|
+
this.totalInputTokens += inputTokens;
|
|
704
|
+
this.totalOutputTokens += outputTokens;
|
|
705
|
+
this.totalTokensSaved += tokensSaved;
|
|
706
|
+
}
|
|
707
|
+
/** Get a summary object to include in every tool response. */
|
|
708
|
+
getSummary() {
|
|
709
|
+
return {
|
|
710
|
+
totalToolCalls: this.totalToolCalls,
|
|
711
|
+
totalInputTokens: this.totalInputTokens,
|
|
712
|
+
totalOutputTokens: this.totalOutputTokens,
|
|
713
|
+
totalTokensSaved: this.totalTokensSaved,
|
|
714
|
+
sessionDurationMs: Date.now() - this.startTime,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Directories that should NEVER be indexed — IDE install dirs, tool dirs, etc.
|
|
720
|
+
*/
|
|
721
|
+
const BLOCKED_DIRECTORY_PATTERNS = [
|
|
722
|
+
'antigravity', '.gemini', '.cursor', '.vscode-server',
|
|
723
|
+
'program files', 'programdata', 'appdata',
|
|
724
|
+
'node_modules', '.npm', '.yarn',
|
|
725
|
+
'system32', 'windows',
|
|
726
|
+
];
|
|
727
|
+
/** Check if a path is a known IDE/tool directory that should never be indexed */
|
|
728
|
+
function isBlockedDirectory(dirPath) {
|
|
729
|
+
const lower = dirPath.toLowerCase().replace(/\\/g, '/');
|
|
730
|
+
return BLOCKED_DIRECTORY_PATTERNS.some(pattern => lower.includes(pattern));
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Intercept an MCP tool response to inject session token metadata.
|
|
734
|
+
*
|
|
735
|
+
* Parses the JSON text content, adds `_sessionTokens` and optionally
|
|
736
|
+
* `_hint` fields, then re-serialises. This works because all our tools
|
|
737
|
+
* return `{ content: [{ type: 'text', text: JSON.stringify(result) }] }`.
|
|
738
|
+
*/
|
|
739
|
+
function enrichToolResponse(response, tracker, estimator, graphNodeCount, getProjectInfo) {
|
|
740
|
+
if (!response?.content?.[0]?.text)
|
|
741
|
+
return response;
|
|
742
|
+
try {
|
|
743
|
+
const result = JSON.parse(response.content[0].text);
|
|
744
|
+
// Estimate tokens for this response
|
|
745
|
+
const outputTokens = estimator.estimate(response.content[0].text);
|
|
746
|
+
const tokensSaved = result.tokensSaved ?? 0;
|
|
747
|
+
// Record in tracker
|
|
748
|
+
tracker.record(0, outputTokens, tokensSaved);
|
|
749
|
+
// Always add project metadata
|
|
750
|
+
const projectInfo = getProjectInfo();
|
|
751
|
+
result._project = projectInfo;
|
|
752
|
+
// ALWAYS tell the agent to provide project path if no real project is indexed
|
|
753
|
+
const nodeCount = graphNodeCount();
|
|
754
|
+
const rootIsBlocked = isBlockedDirectory(projectInfo.root);
|
|
755
|
+
if (nodeCount === 0 || rootIsBlocked) {
|
|
756
|
+
result._indexRequired = true;
|
|
757
|
+
result._action = 'REQUIRED: Call mindmap_reindex({ projectPath: "<USER_WORKSPACE_PATH>" }). ' +
|
|
758
|
+
'The current root "' + projectInfo.root + '" is NOT a user project. ' +
|
|
759
|
+
'Use the workspace/project directory that the user has open in their editor.';
|
|
760
|
+
}
|
|
761
|
+
// Always add session token metadata
|
|
762
|
+
result._sessionTokens = tracker.getSummary();
|
|
763
|
+
response.content[0].text = JSON.stringify(result);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
// If parsing fails, return original response unchanged
|
|
767
|
+
}
|
|
768
|
+
return response;
|
|
769
|
+
}
|
|
770
|
+
// ============================================================
|
|
771
|
+
// Main entry point
|
|
772
|
+
// ============================================================
|
|
773
|
+
async function main() {
|
|
774
|
+
// ── 1. Parse CLI & load config ──────────────────────────────
|
|
775
|
+
let cliArgs;
|
|
776
|
+
try {
|
|
777
|
+
cliArgs = parseCliArgs();
|
|
778
|
+
}
|
|
779
|
+
catch (err) {
|
|
780
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
781
|
+
process.stderr.write(`Error parsing CLI arguments: ${msg}\n`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
setLogLevel(cliArgs.logLevel);
|
|
785
|
+
log('info', '🧠AI Mind Map MCP Server starting…');
|
|
786
|
+
let config;
|
|
787
|
+
try {
|
|
788
|
+
config = await loadConfig(cliArgs);
|
|
789
|
+
log('info', `Project root: ${config.projectRoot}`);
|
|
790
|
+
log('info', `Database path: ${config.dbPath}`);
|
|
791
|
+
log('debug', 'Loaded config', config);
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
795
|
+
log('error', `Failed to load configuration: ${msg}`);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
// ── 2. Initialise database directory ────────────────────────
|
|
799
|
+
try {
|
|
800
|
+
ensureDbDirectory(config.dbPath);
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
804
|
+
log('error', `Failed to create DB directory: ${msg}`);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
// ── 3. Initialise SQLite database ──────────────────────────
|
|
808
|
+
// KnowledgeGraph manages its own db connection, but ChangeLog,
|
|
809
|
+
// SessionMemory, PersistentMemory, and DecisionLog each need
|
|
810
|
+
// a shared Database instance for their tables.
|
|
811
|
+
log('info', 'Initialising database…');
|
|
812
|
+
let sharedDb;
|
|
813
|
+
try {
|
|
814
|
+
sharedDb = new Database(config.dbPath);
|
|
815
|
+
sharedDb.pragma('journal_mode = WAL');
|
|
816
|
+
sharedDb.pragma('foreign_keys = ON');
|
|
817
|
+
sharedDb.pragma('busy_timeout = 5000');
|
|
818
|
+
log('info', 'Database initialized with WAL mode');
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
822
|
+
log('error', `Failed to initialize database: ${msg}`);
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
// ── 4. Initialise real subsystems ──────────────────────────
|
|
826
|
+
log('info', 'Initialising subsystems…');
|
|
827
|
+
// Knowledge Graph — constructor takes dbPath string
|
|
828
|
+
const graph = new KnowledgeGraph(config.dbPath);
|
|
829
|
+
// Indexer — constructor is Indexer(graph, config)
|
|
830
|
+
const indexer = new Indexer(graph, config);
|
|
831
|
+
// PageRankEngine — constructor is PageRankEngine(graph, config?)
|
|
832
|
+
const pagerank = new PageRankEngine(graph);
|
|
833
|
+
// Changelog Engine — node-level change tracking (v1.4.0)
|
|
834
|
+
const changelogEngine = new ChangelogEngine(graph.getDb());
|
|
835
|
+
indexer.setChangelog(changelogEngine);
|
|
836
|
+
log('info', '✅ Knowledge Graph initialized (with changelog engine)');
|
|
837
|
+
// Change Tracker
|
|
838
|
+
// ChangeLog constructor takes ChangeLogConfig: { dbPath, retentionDays?, defaultSearchLimit? }
|
|
839
|
+
const changeLog = new ChangeLog({ dbPath: config.dbPath });
|
|
840
|
+
const diffEngine = new DiffEngine(config.projectRoot);
|
|
841
|
+
let watcher = null;
|
|
842
|
+
// SessionMemory — must be created before watcher so it's available in the handler
|
|
843
|
+
// SessionMemory constructor takes Database.Database instance
|
|
844
|
+
const sessionMemory = new SessionMemory(sharedDb);
|
|
845
|
+
const sessionId = sessionMemory.startSession();
|
|
846
|
+
log('info', `Session started: ${sessionId}`);
|
|
847
|
+
if (config.watchEnabled && !config.memoryOnly) {
|
|
848
|
+
// FileWatcher constructor: config with { projectRoot, watchDebounceMs?, maxFileSize?, ignore? }
|
|
849
|
+
watcher = new FileWatcher({
|
|
850
|
+
projectRoot: config.projectRoot,
|
|
851
|
+
ignore: config.ignore,
|
|
852
|
+
watchDebounceMs: config.watchDebounceMs,
|
|
853
|
+
maxFileSize: config.maxFileSize,
|
|
854
|
+
});
|
|
855
|
+
// Wire watcher to indexer so fullIndex() can pause/resume it
|
|
856
|
+
indexer.setWatcher(watcher);
|
|
857
|
+
// FileWatcher emits 'changes' with WatcherEvent[]
|
|
858
|
+
watcher.on('changes', async (events) => {
|
|
859
|
+
log('debug', `File watcher detected ${events.length} changes`);
|
|
860
|
+
for (const event of events) {
|
|
861
|
+
try {
|
|
862
|
+
if (event.changeType === 'deleted') {
|
|
863
|
+
indexer.removeFile(event.filePath);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
await indexer.indexFile(event.filePath);
|
|
867
|
+
}
|
|
868
|
+
const currentSessionId = sessionMemory.getCurrentSessionId() ?? 'no-session';
|
|
869
|
+
changeLog.recordChange({
|
|
870
|
+
filePath: event.filePath,
|
|
871
|
+
changeType: event.changeType,
|
|
872
|
+
summary: `File ${event.changeType}: ${path.basename(event.filePath)}`,
|
|
873
|
+
symbolsAffected: [],
|
|
874
|
+
linesAdded: 0,
|
|
875
|
+
linesRemoved: 0,
|
|
876
|
+
timestamp: event.timestamp,
|
|
877
|
+
sessionId: currentSessionId,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
catch (err) {
|
|
881
|
+
log('warn', `Failed to process file change: ${event.filePath}`, err);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Invalidate PageRank cache when graph changes
|
|
885
|
+
pagerank.invalidateCache();
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
log('info', `✅ Change Tracker initialized (watcher: ${config.watchEnabled ? 'enabled' : 'disabled'})`);
|
|
889
|
+
// Memory
|
|
890
|
+
// PersistentMemory constructor: (db, config?) where config is Pick<MindMapConfig['memory'], 'decayRate' | 'maxMemories' | 'importanceThreshold'>
|
|
891
|
+
const persistentMemory = new PersistentMemory(sharedDb, {
|
|
892
|
+
decayRate: config.memory.decayRate,
|
|
893
|
+
maxMemories: config.memory.maxMemories,
|
|
894
|
+
importanceThreshold: config.memory.importanceThreshold,
|
|
895
|
+
});
|
|
896
|
+
// DecisionLog constructor: (db, config?) where config is Pick<MindMapConfig['memory'], 'maxDecisions'>
|
|
897
|
+
const decisionLog = new DecisionLog(sharedDb, {
|
|
898
|
+
maxDecisions: config.memory.maxDecisions,
|
|
899
|
+
});
|
|
900
|
+
log('info', `✅ Memory initialized (session: ${sessionMemory.getCurrentSessionId()})`);
|
|
901
|
+
// Apply memory decay on startup
|
|
902
|
+
try {
|
|
903
|
+
const decayed = persistentMemory.applyDecay();
|
|
904
|
+
log('info', `Applied time-based memory decay at startup (${decayed} memories decayed)`);
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
log('warn', 'Failed to apply memory decay at startup', err);
|
|
908
|
+
}
|
|
909
|
+
// Context Engine — no class instances needed; uses module-level functions
|
|
910
|
+
log('info', '✅ Context Engine initialized');
|
|
911
|
+
// ── 5. Build adapters ──────────────────────────────────────
|
|
912
|
+
const graphAdapter = createGraphAdapter(graph, pagerank);
|
|
913
|
+
const changeAdapter = createChangeAdapter(diffEngine, changeLog, graph);
|
|
914
|
+
const memoryAdapter = createMemoryAdapter(persistentMemory, decisionLog, sessionMemory);
|
|
915
|
+
const sessionAdapter = createSessionAdapter(sessionMemory);
|
|
916
|
+
const contextAdapter = createContextAdapter(graph, persistentMemory, decisionLog, changeLog, config);
|
|
917
|
+
const indexerAdapter = createIndexerAdapter(indexer, graph, persistentMemory, decisionLog, changeLog, config, watcher);
|
|
918
|
+
// Token estimator using the exported estimateTokens function
|
|
919
|
+
const tokenEstimator = {
|
|
920
|
+
estimate: (text) => estimateTokens(text),
|
|
921
|
+
};
|
|
922
|
+
// ── 6. Create MCP server ──────────────────────────────────
|
|
923
|
+
const server = new McpServer({
|
|
924
|
+
name: 'ai-mind-map',
|
|
925
|
+
version: '1.7.0',
|
|
926
|
+
}, {
|
|
927
|
+
instructions: [
|
|
928
|
+
'# AI Mind Map — Code Memory Engine',
|
|
929
|
+
'',
|
|
930
|
+
'## ⚠ CRITICAL: FIRST STEP IN EVERY CONVERSATION',
|
|
931
|
+
'Before calling ANY other tool, you MUST ensure a project is indexed:',
|
|
932
|
+
'1. Check if `_indexRequired: true` appears in any response — if so, the index is empty or pointing at an IDE directory.',
|
|
933
|
+
'2. Call `mindmap_reindex({ projectPath: "<WORKSPACE>" })` where <WORKSPACE> is the user project directory from their open editor.',
|
|
934
|
+
'3. NEVER use paths containing: .gemini, antigravity, .cursor, .vscode, AppData, Program Files — these are IDE directories, NOT projects.',
|
|
935
|
+
'4. Example: `mindmap_reindex({ projectPath: "E:\\myproject" })`',
|
|
936
|
+
'',
|
|
937
|
+
'## After Indexing - Resume Session:',
|
|
938
|
+
'Call `mindmap_session_resume`. It returns:',
|
|
939
|
+
'- What the previous AI worked on',
|
|
940
|
+
'- What code changed since then (function-level diffs)',
|
|
941
|
+
'- Project structure + tech stack',
|
|
942
|
+
'- Hot files (most frequently changed)',
|
|
943
|
+
'',
|
|
944
|
+
'## Project Indexing:',
|
|
945
|
+
'Every response includes `_project.root` and `_project.totalNodes`.',
|
|
946
|
+
'If `_indexRequired: true` appears, you MUST call `mindmap_reindex` with the correct project path.',
|
|
947
|
+
'NEVER index directories containing: antigravity, .gemini, .cursor, .vscode, AppData, Program Files.',
|
|
948
|
+
'The graph supports multiple projects — call `mindmap_reindex` with different paths to switch.',
|
|
949
|
+
'',
|
|
950
|
+
'## Token Tracking:',
|
|
951
|
+
'Every response includes `_sessionTokens` with cumulative usage.',
|
|
952
|
+
'',
|
|
953
|
+
'## Tool Selection Guide:',
|
|
954
|
+
'',
|
|
955
|
+
'### When you need to UNDERSTAND the project:',
|
|
956
|
+
'- `mindmap_digest` → Full project summary in <2000 tokens',
|
|
957
|
+
'- `mindmap_architecture` → Layers, patterns, component overview',
|
|
958
|
+
'- `mindmap_file_digest` → Understand a file WITHOUT reading it',
|
|
959
|
+
'',
|
|
960
|
+
'### When you need to FIND code:',
|
|
961
|
+
'- `mindmap_smart_search` → Search by name/concept (best for most lookups)',
|
|
962
|
+
'- `mindmap_semantic_search` → Search by meaning ("authentication", "error handling")',
|
|
963
|
+
'- `mindmap_search_code` → Grep-like text search in code bodies',
|
|
964
|
+
'- `mindmap_trace_dependencies` → Who calls X? What does X call?',
|
|
965
|
+
'',
|
|
966
|
+
'### When you need to READ code:',
|
|
967
|
+
'- `mindmap_get_code_snippet` → Read actual source code for a function/class',
|
|
968
|
+
'- `mindmap_get_file_map` → All symbols in a file with signatures + line ranges',
|
|
969
|
+
'',
|
|
970
|
+
'### When you need to know WHAT CHANGED:',
|
|
971
|
+
'- `mindmap_changelog` → Symbol-level diffs (added/modified/deleted functions)',
|
|
972
|
+
'- `mindmap_git_changes` → Git-aware changes with symbol mapping',
|
|
973
|
+
'- `mindmap_verify` → Check if your cached knowledge is still valid',
|
|
974
|
+
'- `mindmap_hotspots` → Most frequently changed files + symbols',
|
|
975
|
+
'',
|
|
976
|
+
'### When you need to REMEMBER:',
|
|
977
|
+
'- `mindmap_remember` → Save a fact/convention for future sessions',
|
|
978
|
+
'- `mindmap_recall` → Retrieve relevant memories for current task',
|
|
979
|
+
'- `mindmap_decide` → Record architectural decisions with rationale',
|
|
980
|
+
'',
|
|
981
|
+
'### When you finish work:',
|
|
982
|
+
'- `mindmap_session_end` → Save summary so next AI can resume instantly',
|
|
983
|
+
'',
|
|
984
|
+
'### After editing code:',
|
|
985
|
+
'- `mindmap_verify_changes` → Verify your edits at the symbol level WITHOUT re-reading files',
|
|
986
|
+
'',
|
|
987
|
+
'## âš¡ Token-Saving Rules:',
|
|
988
|
+
'1. ALWAYS call `mindmap_session_resume` first — never start blind',
|
|
989
|
+
'2. Use `mindmap_file_digest` BEFORE reading a full file — you may not need the full file',
|
|
990
|
+
'3. Use `mindmap_verify_changes` after editing to verify changes — do NOT re-read whole files',
|
|
991
|
+
'4. Use `mindmap_changelog` instead of re-reading files to see what changed',
|
|
992
|
+
'5. Call `mindmap_session_end` when done — save context for next session',
|
|
993
|
+
'',
|
|
994
|
+
'## 🔄 Project Indexing:',
|
|
995
|
+
'Every response includes `_project.root` showing which directory is currently indexed.',
|
|
996
|
+
'If `_project.totalNodes` is 0, you MUST call `mindmap_reindex` with `projectPath` set to',
|
|
997
|
+
'the user\'s project directory (e.g. their workspace root). This creates the knowledge graph.',
|
|
998
|
+
'Example: `mindmap_reindex({ projectPath: "/home/user/my-project" })`',
|
|
999
|
+
'You can also reindex to switch between projects — the graph supports multiple projects.',
|
|
1000
|
+
'',
|
|
1001
|
+
'## 📊 Token Tracking:',
|
|
1002
|
+
'Every response includes `_sessionTokens` with cumulative usage.',
|
|
1003
|
+
].join('\n'),
|
|
1004
|
+
});
|
|
1005
|
+
// ── 7. Register all tools ─────────────────────────────────
|
|
1006
|
+
log('info', 'Registering MCP tools…');
|
|
1007
|
+
// ── 7.0 Token tracking middleware ──────────────────────────
|
|
1008
|
+
// Wrap every tool handler to inject session token metadata
|
|
1009
|
+
const tokenTracker = new SessionTokenTracker();
|
|
1010
|
+
const originalToolFn = server.tool.bind(server);
|
|
1011
|
+
// Override server.tool to wrap every handler with token tracking
|
|
1012
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1013
|
+
server.tool = function (...args) {
|
|
1014
|
+
// server.tool(name, description, schema, handler) or server.tool(name, description, handler)
|
|
1015
|
+
const lastArgIdx = args.length - 1;
|
|
1016
|
+
const originalHandler = args[lastArgIdx];
|
|
1017
|
+
if (typeof originalHandler === 'function') {
|
|
1018
|
+
args[lastArgIdx] = async (...handlerArgs) => {
|
|
1019
|
+
// Estimate input tokens from args
|
|
1020
|
+
const inputStr = JSON.stringify(handlerArgs);
|
|
1021
|
+
const inputTokens = tokenEstimator.estimate(inputStr);
|
|
1022
|
+
tokenTracker.totalInputTokens += inputTokens;
|
|
1023
|
+
// Call original handler
|
|
1024
|
+
const response = await originalHandler(...handlerArgs);
|
|
1025
|
+
// Enrich response with token tracking + project metadata
|
|
1026
|
+
return enrichToolResponse(response, tokenTracker, tokenEstimator, () => graph.getStats().totalNodes, () => {
|
|
1027
|
+
const s = graph.getStats();
|
|
1028
|
+
return { root: config.projectRoot, indexedFiles: s.totalFiles, totalNodes: s.totalNodes };
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
return originalToolFn.apply(server, args);
|
|
1033
|
+
};
|
|
1034
|
+
registerGraphTools(server, graphAdapter, tokenEstimator);
|
|
1035
|
+
log('debug', 'Registered graph tools (6)');
|
|
1036
|
+
registerChangeTools(server, changeAdapter, tokenEstimator);
|
|
1037
|
+
log('debug', 'Registered change tools (3)');
|
|
1038
|
+
registerMemoryTools(server, memoryAdapter, sessionAdapter, tokenEstimator);
|
|
1039
|
+
log('debug', 'Registered memory tools (5)');
|
|
1040
|
+
registerContextTools(server, contextAdapter, indexerAdapter, tokenEstimator);
|
|
1041
|
+
log('debug', 'Registered context tools (4)');
|
|
1042
|
+
registerDebugTools(server, graph, config, tokenEstimator);
|
|
1043
|
+
log('debug', 'Registered debug tools (3)');
|
|
1044
|
+
registerFlowTools(server, graph, config, tokenEstimator);
|
|
1045
|
+
log('debug', 'Registered flow tools (4)');
|
|
1046
|
+
registerSnapshotTools(server, graph, config, tokenEstimator);
|
|
1047
|
+
log('debug', 'Registered snapshot tools (3)');
|
|
1048
|
+
// Initialize semantic search engine
|
|
1049
|
+
const semanticEngine = new SemanticSearchEngine(graph.getDb());
|
|
1050
|
+
log('debug', 'Initialized semantic search engine');
|
|
1051
|
+
registerSmartTools(server, graph, config, tokenEstimator, semanticEngine);
|
|
1052
|
+
log('debug', 'Registered smart tools (3)');
|
|
1053
|
+
registerEvolvingTools(server, graph, config, tokenEstimator);
|
|
1054
|
+
log('debug', 'Registered evolving tools (3)');
|
|
1055
|
+
registerAdvancedTools(server, graph, config, tokenEstimator, semanticEngine);
|
|
1056
|
+
log('debug', 'Registered advanced tools (7)');
|
|
1057
|
+
registerSemanticTools(server, graph, semanticEngine, config, tokenEstimator);
|
|
1058
|
+
log('debug', 'Registered semantic search tools (3)');
|
|
1059
|
+
// Session, Changelog & Digest tools (v1.4.0)
|
|
1060
|
+
registerSessionTools(server, graph, changelogEngine, config, tokenEstimator);
|
|
1061
|
+
log('debug', 'Registered session tools (5)');
|
|
1062
|
+
registerDigestTools(server, graph, changelogEngine, config, tokenEstimator);
|
|
1063
|
+
log('debug', 'Registered digest tools (3)');
|
|
1064
|
+
// ── mindmap_sync_shared_context ─────────────────────────────
|
|
1065
|
+
server.tool('mindmap_sync_shared_context', 'Synchronise local memories, decisions, and learned rules with the team-shared `.mindmap-shared.json` file. ' +
|
|
1066
|
+
'Performs a bidirectional sync to import new conventions/decisions and export local updates.', {}, async () => {
|
|
1067
|
+
try {
|
|
1068
|
+
const syncStats = await syncSharedContext(config, graph, persistentMemory, decisionLog);
|
|
1069
|
+
return {
|
|
1070
|
+
content: [
|
|
1071
|
+
{
|
|
1072
|
+
type: 'text',
|
|
1073
|
+
text: JSON.stringify({
|
|
1074
|
+
success: true,
|
|
1075
|
+
message: 'Bidirectional synchronization completed successfully.',
|
|
1076
|
+
stats: syncStats
|
|
1077
|
+
})
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
catch (err) {
|
|
1083
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1084
|
+
return {
|
|
1085
|
+
content: [
|
|
1086
|
+
{
|
|
1087
|
+
type: 'text',
|
|
1088
|
+
text: JSON.stringify({
|
|
1089
|
+
success: false,
|
|
1090
|
+
message: `Synchronization failed: ${msg}`
|
|
1091
|
+
})
|
|
1092
|
+
}
|
|
1093
|
+
]
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
log('debug', 'Registered shared context sync tool');
|
|
1098
|
+
log('info', '🔧 All MCP tools registered:');
|
|
1099
|
+
log('info', ' Graph: mindmap_search, mindmap_get_structure, mindmap_trace_dependencies, mindmap_get_signature, mindmap_find_references, mindmap_get_file_map');
|
|
1100
|
+
log('info', ' Changes: mindmap_what_changed, mindmap_session_diff, mindmap_impact_analysis');
|
|
1101
|
+
log('info', ' Memory: mindmap_recall, mindmap_remember, mindmap_get_decisions, mindmap_decide, mindmap_session_summary, mindmap_sync_shared_context');
|
|
1102
|
+
log('info', ' Context: mindmap_get_context, mindmap_compress, mindmap_reindex, mindmap_status');
|
|
1103
|
+
log('info', ' Debug: mindmap_debug_changes, mindmap_file_before, mindmap_file_history');
|
|
1104
|
+
log('info', ' Flow: mindmap_trace_flow, mindmap_interaction_map, mindmap_classify_file, mindmap_layer_overview');
|
|
1105
|
+
log('info', ' Snapshot: mindmap_project_map, mindmap_change_delta, mindmap_session_kickoff â');
|
|
1106
|
+
log('info', ' Advanced: mindmap_query_graph, mindmap_dead_code, mindmap_architecture, mindmap_get_code_snippet, mindmap_search_code, mindmap_list_projects, mindmap_health');
|
|
1107
|
+
log('info', ' Smart: mindmap_explain â, mindmap_git_changes â, mindmap_smart_search â');
|
|
1108
|
+
log('info', ' Evolving: mindmap_teach â, mindmap_get_learned, mindmap_forget');
|
|
1109
|
+
log('info', ' Semantic: mindmap_semantic_search â, mindmap_semantic_stats, mindmap_synonyms');
|
|
1110
|
+
log('info', ' Session: mindmap_session_start 🆕, mindmap_session_resume 🔥🆕, mindmap_session_end, mindmap_changelog 🆕, mindmap_hotspots, mindmap_verify_changes 🆕');
|
|
1111
|
+
log('info', ' Digest: mindmap_digest â, mindmap_file_digest â, mindmap_verify');
|
|
1112
|
+
// ── 7.3 Register MCP Prompts ──────────────────────────────
|
|
1113
|
+
// These are interactive workflow templates that AI agents can request
|
|
1114
|
+
server.prompt('start_session', 'Recommended first prompt for any AI coding session. Calls mindmap_session_resume and returns a complete project briefing.', async () => {
|
|
1115
|
+
// Auto-start session
|
|
1116
|
+
const sessionId = changelogEngine.ensureSession('ai-agent');
|
|
1117
|
+
const lastSession = changelogEngine.getLastSession();
|
|
1118
|
+
const stats = graph.getStats();
|
|
1119
|
+
const since = lastSession?.endedAt || lastSession?.startedAt || (Date.now() - 24 * 3600_000);
|
|
1120
|
+
const changes = changelogEngine.getChangesSince(since);
|
|
1121
|
+
const lines = [
|
|
1122
|
+
'# Session Briefing',
|
|
1123
|
+
'',
|
|
1124
|
+
`**Project**: ${path.basename(config.projectRoot)}`,
|
|
1125
|
+
`**Files**: ${stats.totalFiles} | **Symbols**: ${stats.totalNodes} | **Relationships**: ${stats.totalEdges}`,
|
|
1126
|
+
`**Languages**: ${Object.entries(stats.languageBreakdown).map(([l, c]) => `${l}(${c})`).join(', ')}`,
|
|
1127
|
+
'',
|
|
1128
|
+
];
|
|
1129
|
+
if (lastSession) {
|
|
1130
|
+
lines.push('## Previous Session', `- **Agent**: ${lastSession.agentName}`, `- **Task**: ${lastSession.taskDescription || 'Not specified'}`, `- **Summary**: ${lastSession.summary || 'No summary'}`, `- **Files modified**: ${lastSession.filesModified.length}`, '');
|
|
1131
|
+
}
|
|
1132
|
+
if (changes.totalChanges > 0) {
|
|
1133
|
+
lines.push(`## Changes Since Last Session (${changes.sinceLabel})`, `${changes.filesChanged} files, ${changes.totalChanges} symbol changes:`);
|
|
1134
|
+
for (const f of changes.files.slice(0, 8)) {
|
|
1135
|
+
const rel = path.relative(config.projectRoot, f.filePath).replace(/\\/g, '/');
|
|
1136
|
+
const parts = [];
|
|
1137
|
+
if (f.added.length > 0)
|
|
1138
|
+
parts.push(`+${f.added.length}`);
|
|
1139
|
+
if (f.modified.length > 0)
|
|
1140
|
+
parts.push(`~${f.modified.length}`);
|
|
1141
|
+
if (f.deleted.length > 0)
|
|
1142
|
+
parts.push(`-${f.deleted.length}`);
|
|
1143
|
+
lines.push(` ${rel}: ${parts.join(', ')}`);
|
|
1144
|
+
}
|
|
1145
|
+
lines.push('');
|
|
1146
|
+
}
|
|
1147
|
+
lines.push('## What to do next', '- Use `mindmap_smart_search` to find specific code', '- Use `mindmap_explain` to understand a symbol', '- Use `mindmap_changelog` for detailed change diffs', '- Use `mindmap_session_end` when done');
|
|
1148
|
+
return {
|
|
1149
|
+
messages: [{
|
|
1150
|
+
role: 'user',
|
|
1151
|
+
content: { type: 'text', text: lines.join('\n') },
|
|
1152
|
+
}],
|
|
1153
|
+
};
|
|
1154
|
+
});
|
|
1155
|
+
server.prompt('tool_guide', 'Complete guide to all AI Mind Map tools — when to use each one, organized by task.', async () => ({
|
|
1156
|
+
messages: [{
|
|
1157
|
+
role: 'user',
|
|
1158
|
+
content: {
|
|
1159
|
+
type: 'text',
|
|
1160
|
+
text: [
|
|
1161
|
+
'# AI Mind Map — Complete Tool Guide',
|
|
1162
|
+
'',
|
|
1163
|
+
'## 🚀 Session Lifecycle (use these to avoid re-reading code)',
|
|
1164
|
+
'| Tool | When to Use |',
|
|
1165
|
+
'|------|------------|',
|
|
1166
|
+
'| `mindmap_session_resume` | **FIRST call every conversation** — returns project context + changes |',
|
|
1167
|
+
'| `mindmap_session_kickoff` | Full preamble: project map + change delta + memories in ONE call |',
|
|
1168
|
+
'| `mindmap_session_start` | Start tracking a new task (records agent name + task) |',
|
|
1169
|
+
'| `mindmap_session_end` | Save summary for next AI session |',
|
|
1170
|
+
'',
|
|
1171
|
+
'## 🔠Finding Code (instead of grep/reading files)',
|
|
1172
|
+
'| Tool | When to Use |',
|
|
1173
|
+
'|------|------------|',
|
|
1174
|
+
'| `mindmap_smart_search` | Search by function/class name — returns full context |',
|
|
1175
|
+
'| `mindmap_semantic_search` | Search by concept ("error handling", "auth") |',
|
|
1176
|
+
'| `mindmap_search_code` | Grep-like text search in code bodies |',
|
|
1177
|
+
'| `mindmap_find_references` | Find all usages of a symbol |',
|
|
1178
|
+
'| `mindmap_trace_dependencies` | Who calls X? What does X call? |',
|
|
1179
|
+
'',
|
|
1180
|
+
'## 📖 Reading Code (without reading full files)',
|
|
1181
|
+
'| Tool | When to Use |',
|
|
1182
|
+
'|------|------------|',
|
|
1183
|
+
'| `mindmap_explain` | Get EVERYTHING about a symbol in one call |',
|
|
1184
|
+
'| `mindmap_get_code_snippet` | Read actual source for a function |',
|
|
1185
|
+
'| `mindmap_file_digest` | Understand a file without reading it |',
|
|
1186
|
+
'| `mindmap_get_file_map` | All symbols in a file with signatures |',
|
|
1187
|
+
'| `mindmap_get_signature` | Just the signature (cheapest read) |',
|
|
1188
|
+
'',
|
|
1189
|
+
'## 📊 Understanding the Project',
|
|
1190
|
+
'| Tool | When to Use |',
|
|
1191
|
+
'|------|------------|',
|
|
1192
|
+
'| `mindmap_digest` | Full project summary in <2000 tokens |',
|
|
1193
|
+
'| `mindmap_architecture` | Architecture layers + patterns |',
|
|
1194
|
+
'| `mindmap_project_map` | Complete project map |',
|
|
1195
|
+
'',
|
|
1196
|
+
'## 🔄 Change Tracking',
|
|
1197
|
+
'| Tool | When to Use |',
|
|
1198
|
+
'|------|------------|',
|
|
1199
|
+
'| `mindmap_changelog` | Symbol-level diffs since a time |',
|
|
1200
|
+
'| `mindmap_git_changes` | Git-aware change detection |',
|
|
1201
|
+
'| `mindmap_verify` | Check if cached code is still valid |',
|
|
1202
|
+
'| `mindmap_hotspots` | Most frequently changed files |',
|
|
1203
|
+
'',
|
|
1204
|
+
'## 🧠Memory & Decisions',
|
|
1205
|
+
'| Tool | When to Use |',
|
|
1206
|
+
'|------|------------|',
|
|
1207
|
+
'| `mindmap_remember` | Save important facts for future |',
|
|
1208
|
+
'| `mindmap_recall` | Retrieve relevant memories |',
|
|
1209
|
+
'| `mindmap_decide` | Record architectural decisions |',
|
|
1210
|
+
'| `mindmap_teach` | Teach persistent rules |',
|
|
1211
|
+
].join('\n'),
|
|
1212
|
+
},
|
|
1213
|
+
}],
|
|
1214
|
+
}));
|
|
1215
|
+
log('debug', 'Registered 2 MCP prompts (start_session, tool_guide)');
|
|
1216
|
+
// ── 7.5 Auto-sync shared context on startup ────────────────
|
|
1217
|
+
if (config.autoSyncSharedContext) {
|
|
1218
|
+
log('info', '🔄 Auto-syncing shared context…');
|
|
1219
|
+
try {
|
|
1220
|
+
const syncStats = await syncSharedContext(config, graph, persistentMemory, decisionLog);
|
|
1221
|
+
log('info', `✅ Shared context sync complete: ` +
|
|
1222
|
+
`Imported: ${syncStats.memoriesImported} memories, ${syncStats.decisionsImported} decisions, ${syncStats.rulesImported} rules. ` +
|
|
1223
|
+
`Exported: ${syncStats.memoriesExported} memories, ${syncStats.decisionsExported} decisions, ${syncStats.rulesExported} rules.`);
|
|
1224
|
+
}
|
|
1225
|
+
catch (err) {
|
|
1226
|
+
log('warn', `âš ï¸ Auto-sync of shared context failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// ── 8. Smart auto-index (only if projectRoot looks like a real project) ──
|
|
1230
|
+
if (config.memoryOnly) {
|
|
1231
|
+
log('info', '🧠Running in memoryOnly mode. Bypassing codebase parsing and indexing.');
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
// Check if projectRoot is a real project (not an IDE install directory)
|
|
1235
|
+
const projectMarkers = [
|
|
1236
|
+
'.git', 'package.json', 'build.gradle', 'build.gradle.kts',
|
|
1237
|
+
'Cargo.toml', 'go.mod', 'pom.xml', 'CMakeLists.txt',
|
|
1238
|
+
'Makefile', '.project', 'setup.py', 'pyproject.toml',
|
|
1239
|
+
'pubspec.yaml', 'Gemfile', '*.sln', '*.csproj',
|
|
1240
|
+
];
|
|
1241
|
+
const isRealProject = projectMarkers.some(marker => existsSync(path.join(config.projectRoot, marker)));
|
|
1242
|
+
if (!isRealProject || isBlockedDirectory(config.projectRoot)) {
|
|
1243
|
+
log('info', `âš ï¸ Project root "${config.projectRoot}" does not look like a code project (no .git, package.json, etc.).`);
|
|
1244
|
+
log('info', ' Skipping auto-index. The AI agent will be prompted to call mindmap_reindex with the correct project path.');
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
const stats = graph.getStats();
|
|
1248
|
+
if (stats.totalNodes === 0) {
|
|
1249
|
+
log('info', `📋 Real project detected at: ${config.projectRoot}. Running initial indexing…`);
|
|
1250
|
+
try {
|
|
1251
|
+
const result = await indexer.fullIndex();
|
|
1252
|
+
log('info', `✅ Initial index complete: ${result.filesParsed} files, ${result.nodesCreated} nodes, ${result.edgesCreated} edges`);
|
|
1253
|
+
if (result.parseErrors > 0) {
|
|
1254
|
+
log('warn', `âš ï¸ ${result.parseErrors} parse errors (non-fatal)`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
catch (err) {
|
|
1258
|
+
log('warn', `âš ï¸ Initial indexing failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
log('info', `📋 Existing index found: ${stats.totalNodes} nodes. Running incremental update…`);
|
|
1263
|
+
try {
|
|
1264
|
+
const result = await indexer.incrementalIndex();
|
|
1265
|
+
log('info', `✅ Incremental update: ${result.filesParsed} files reindexed`);
|
|
1266
|
+
}
|
|
1267
|
+
catch (err) {
|
|
1268
|
+
log('warn', `âš ï¸ Incremental update failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// Build semantic search TF-IDF index from existing graph nodes (if any)
|
|
1273
|
+
try {
|
|
1274
|
+
const allNodes = graph.getAllNodes();
|
|
1275
|
+
const nonFileNodes = allNodes.filter(n => n.type !== 'file');
|
|
1276
|
+
if (nonFileNodes.length > 0) {
|
|
1277
|
+
semanticEngine.indexNodes(nonFileNodes.map(n => ({
|
|
1278
|
+
id: n.id,
|
|
1279
|
+
name: n.name,
|
|
1280
|
+
qualifiedName: n.qualifiedName,
|
|
1281
|
+
signature: n.signature,
|
|
1282
|
+
docComment: n.docComment,
|
|
1283
|
+
filePath: n.filePath,
|
|
1284
|
+
})));
|
|
1285
|
+
semanticEngine.rebuildIDF();
|
|
1286
|
+
log('info', `🧠Semantic index built: ${nonFileNodes.length} symbols indexed`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
log('warn', `âš ï¸ Semantic index build failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
// ── 9. Start file watcher ──────────────────────────────────
|
|
1294
|
+
if (watcher && !config.memoryOnly) {
|
|
1295
|
+
try {
|
|
1296
|
+
await watcher.start();
|
|
1297
|
+
log('info', 'ðŸ‘ï¸ File watcher started');
|
|
1298
|
+
}
|
|
1299
|
+
catch (err) {
|
|
1300
|
+
log('warn', `âš ï¸ File watcher failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
// ── 10. Graceful shutdown ──────────────────────────────────
|
|
1304
|
+
let shuttingDown = false;
|
|
1305
|
+
async function shutdown(signal) {
|
|
1306
|
+
if (shuttingDown)
|
|
1307
|
+
return;
|
|
1308
|
+
shuttingDown = true;
|
|
1309
|
+
log('info', `Received ${signal}, shutting down gracefully…`);
|
|
1310
|
+
try {
|
|
1311
|
+
// Stop file watcher
|
|
1312
|
+
if (watcher) {
|
|
1313
|
+
await watcher.stop();
|
|
1314
|
+
log('debug', 'File watcher stopped');
|
|
1315
|
+
}
|
|
1316
|
+
// End current session
|
|
1317
|
+
try {
|
|
1318
|
+
sessionMemory.endSession();
|
|
1319
|
+
log('debug', 'Session ended');
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
// Ignore session end errors
|
|
1323
|
+
}
|
|
1324
|
+
// Apply memory decay
|
|
1325
|
+
try {
|
|
1326
|
+
persistentMemory.applyDecay();
|
|
1327
|
+
log('debug', 'Memory decay applied');
|
|
1328
|
+
}
|
|
1329
|
+
catch {
|
|
1330
|
+
// Ignore decay errors
|
|
1331
|
+
}
|
|
1332
|
+
// Close change log database
|
|
1333
|
+
try {
|
|
1334
|
+
changeLog.close();
|
|
1335
|
+
log('debug', 'Change log closed');
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
// Ignore close errors
|
|
1339
|
+
}
|
|
1340
|
+
// Close graph database
|
|
1341
|
+
try {
|
|
1342
|
+
graph.close();
|
|
1343
|
+
log('debug', 'Knowledge graph closed');
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
// Ignore close errors
|
|
1347
|
+
}
|
|
1348
|
+
// Close shared database
|
|
1349
|
+
try {
|
|
1350
|
+
sharedDb.close();
|
|
1351
|
+
log('debug', 'Shared database closed');
|
|
1352
|
+
}
|
|
1353
|
+
catch {
|
|
1354
|
+
// Ignore close errors
|
|
1355
|
+
}
|
|
1356
|
+
log('info', '✅ Cleanup complete. Goodbye!');
|
|
1357
|
+
}
|
|
1358
|
+
catch (err) {
|
|
1359
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1360
|
+
log('error', `Error during shutdown: ${msg}`);
|
|
1361
|
+
}
|
|
1362
|
+
process.exit(0);
|
|
1363
|
+
}
|
|
1364
|
+
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
1365
|
+
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
1366
|
+
// Handle uncaught errors gracefully
|
|
1367
|
+
process.on('uncaughtException', (err) => {
|
|
1368
|
+
log('error', `Uncaught exception: ${err.message}`, err.stack);
|
|
1369
|
+
void shutdown('uncaughtException');
|
|
1370
|
+
});
|
|
1371
|
+
process.on('unhandledRejection', (reason) => {
|
|
1372
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
1373
|
+
log('error', `Unhandled rejection: ${msg}`);
|
|
1374
|
+
});
|
|
1375
|
+
// ── 11. Connect transport and start serving ────────────────
|
|
1376
|
+
log('info', 'Connecting stdio transport…');
|
|
1377
|
+
try {
|
|
1378
|
+
const transport = new StdioServerTransport();
|
|
1379
|
+
await server.connect(transport);
|
|
1380
|
+
log('info', '🧠AI Mind Map MCP Server is LIVE. Waiting for requests…');
|
|
1381
|
+
log('info', ` Project: ${config.projectRoot}`);
|
|
1382
|
+
log('info', ` Database: ${config.dbPath}`);
|
|
1383
|
+
log('info', ` Session: ${sessionMemory.getCurrentSessionId()}`);
|
|
1384
|
+
}
|
|
1385
|
+
catch (err) {
|
|
1386
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1387
|
+
log('error', `Failed to start MCP server: ${msg}`);
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// ── Kick off ────────────────────────────────────────────────
|
|
1392
|
+
main().catch((err) => {
|
|
1393
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1394
|
+
process.stderr.write(`Fatal: ${msg}\n`);
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
});
|
|
2
1397
|
//# sourceMappingURL=index.js.map
|