@danielsimonjr/memory-mcp 0.7.2 → 0.47.1
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/__tests__/edge-cases/edge-cases.test.js +406 -0
- package/dist/__tests__/file-path.test.js +5 -5
- package/dist/__tests__/integration/workflows.test.js +449 -0
- package/dist/__tests__/knowledge-graph.test.js +8 -3
- package/dist/__tests__/performance/benchmarks.test.js +413 -0
- package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
- package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
- package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
- package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
- package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
- package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
- package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
- package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
- package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
- package/dist/core/EntityManager.js +554 -0
- package/dist/core/GraphStorage.js +172 -0
- package/dist/core/KnowledgeGraphManager.js +423 -0
- package/dist/core/ObservationManager.js +129 -0
- package/dist/core/RelationManager.js +186 -0
- package/dist/core/TransactionManager.js +389 -0
- package/dist/core/index.js +9 -0
- package/dist/features/AnalyticsManager.js +222 -0
- package/dist/features/ArchiveManager.js +74 -0
- package/dist/features/BackupManager.js +311 -0
- package/dist/features/CompressionManager.js +291 -0
- package/dist/features/ExportManager.js +305 -0
- package/dist/features/HierarchyManager.js +219 -0
- package/dist/features/ImportExportManager.js +50 -0
- package/dist/features/ImportManager.js +328 -0
- package/dist/features/TagManager.js +210 -0
- package/dist/features/index.js +12 -0
- package/dist/index.js +13 -996
- package/dist/memory.jsonl +18 -0
- package/dist/search/BasicSearch.js +131 -0
- package/dist/search/BooleanSearch.js +283 -0
- package/dist/search/FuzzySearch.js +96 -0
- package/dist/search/RankedSearch.js +190 -0
- package/dist/search/SavedSearchManager.js +145 -0
- package/dist/search/SearchFilterChain.js +187 -0
- package/dist/search/SearchManager.js +305 -0
- package/dist/search/SearchSuggestions.js +57 -0
- package/dist/search/TFIDFIndexManager.js +217 -0
- package/dist/search/index.js +14 -0
- package/dist/server/MCPServer.js +52 -0
- package/dist/server/toolDefinitions.js +732 -0
- package/dist/server/toolHandlers.js +117 -0
- package/dist/types/analytics.types.js +6 -0
- package/dist/types/entity.types.js +7 -0
- package/dist/types/import-export.types.js +7 -0
- package/dist/types/index.js +12 -0
- package/dist/types/search.types.js +7 -0
- package/dist/types/tag.types.js +6 -0
- package/dist/utils/constants.js +128 -0
- package/dist/utils/dateUtils.js +89 -0
- package/dist/utils/entityUtils.js +108 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/filterUtils.js +155 -0
- package/dist/utils/index.js +39 -0
- package/dist/utils/levenshtein.js +62 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/paginationUtils.js +81 -0
- package/dist/utils/pathUtils.js +115 -0
- package/dist/utils/responseFormatter.js +55 -0
- package/dist/utils/schemas.js +184 -0
- package/dist/utils/searchCache.js +209 -0
- package/dist/utils/tagUtils.js +107 -0
- package/dist/utils/tfidf.js +90 -0
- package/dist/utils/validationHelper.js +99 -0
- package/dist/utils/validationUtils.js +109 -0
- package/package.json +82 -48
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Manager
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates all search types (basic, ranked, boolean, fuzzy).
|
|
5
|
+
*
|
|
6
|
+
* @module search/SearchManager
|
|
7
|
+
*/
|
|
8
|
+
import { BasicSearch } from './BasicSearch.js';
|
|
9
|
+
import { RankedSearch } from './RankedSearch.js';
|
|
10
|
+
import { BooleanSearch } from './BooleanSearch.js';
|
|
11
|
+
import { FuzzySearch } from './FuzzySearch.js';
|
|
12
|
+
import { SearchSuggestions } from './SearchSuggestions.js';
|
|
13
|
+
import { SavedSearchManager } from './SavedSearchManager.js';
|
|
14
|
+
/**
|
|
15
|
+
* Unified search manager providing access to all search types.
|
|
16
|
+
*/
|
|
17
|
+
export class SearchManager {
|
|
18
|
+
basicSearch;
|
|
19
|
+
rankedSearch;
|
|
20
|
+
booleanSearcher;
|
|
21
|
+
fuzzySearcher;
|
|
22
|
+
searchSuggestions;
|
|
23
|
+
savedSearchManager;
|
|
24
|
+
constructor(storage, savedSearchesFilePath) {
|
|
25
|
+
this.basicSearch = new BasicSearch(storage);
|
|
26
|
+
this.rankedSearch = new RankedSearch(storage);
|
|
27
|
+
this.booleanSearcher = new BooleanSearch(storage);
|
|
28
|
+
this.fuzzySearcher = new FuzzySearch(storage);
|
|
29
|
+
this.searchSuggestions = new SearchSuggestions(storage);
|
|
30
|
+
this.savedSearchManager = new SavedSearchManager(savedSearchesFilePath, this.basicSearch);
|
|
31
|
+
}
|
|
32
|
+
// ==================== Basic Search ====================
|
|
33
|
+
/**
|
|
34
|
+
* Perform a simple text-based search across entity names and observations.
|
|
35
|
+
*
|
|
36
|
+
* This is the primary search method that searches through entity names,
|
|
37
|
+
* observations, and types using case-insensitive substring matching.
|
|
38
|
+
* Optionally filter by tags and importance range.
|
|
39
|
+
*
|
|
40
|
+
* @param query - Text to search for (case-insensitive, searches names/observations/types)
|
|
41
|
+
* @param tags - Optional array of tags to filter results (lowercase)
|
|
42
|
+
* @param minImportance - Optional minimum importance value (0-10)
|
|
43
|
+
* @param maxImportance - Optional maximum importance value (0-10)
|
|
44
|
+
* @returns KnowledgeGraph containing matching entities and their relations
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const manager = new SearchManager(storage, savedSearchesPath);
|
|
49
|
+
*
|
|
50
|
+
* // Simple text search
|
|
51
|
+
* const results = await manager.searchNodes('Alice');
|
|
52
|
+
*
|
|
53
|
+
* // Search with tag filter
|
|
54
|
+
* const engineeringResults = await manager.searchNodes('project', ['engineering']);
|
|
55
|
+
*
|
|
56
|
+
* // Search with importance range
|
|
57
|
+
* const importantResults = await manager.searchNodes('critical', undefined, 8, 10);
|
|
58
|
+
*
|
|
59
|
+
* // Combined filters
|
|
60
|
+
* const filtered = await manager.searchNodes('bug', ['backend'], 5, 10);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
async searchNodes(query, tags, minImportance, maxImportance) {
|
|
64
|
+
return this.basicSearch.searchNodes(query, tags, minImportance, maxImportance);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Open specific nodes by name.
|
|
68
|
+
*
|
|
69
|
+
* @param names - Array of entity names
|
|
70
|
+
* @returns Knowledge graph with specified entities
|
|
71
|
+
*/
|
|
72
|
+
async openNodes(names) {
|
|
73
|
+
return this.basicSearch.openNodes(names);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Search by date range.
|
|
77
|
+
*
|
|
78
|
+
* @param startDate - Optional start date (ISO 8601)
|
|
79
|
+
* @param endDate - Optional end date (ISO 8601)
|
|
80
|
+
* @param entityType - Optional entity type filter
|
|
81
|
+
* @param tags - Optional tags filter
|
|
82
|
+
* @returns Filtered knowledge graph
|
|
83
|
+
*/
|
|
84
|
+
async searchByDateRange(startDate, endDate, entityType, tags) {
|
|
85
|
+
return this.basicSearch.searchByDateRange(startDate, endDate, entityType, tags);
|
|
86
|
+
}
|
|
87
|
+
// ==================== Ranked Search ====================
|
|
88
|
+
/**
|
|
89
|
+
* Perform TF-IDF ranked search with relevance scoring.
|
|
90
|
+
*
|
|
91
|
+
* Uses Term Frequency-Inverse Document Frequency algorithm to rank results
|
|
92
|
+
* by relevance to the query. Results are sorted by score (highest first).
|
|
93
|
+
* This is ideal for finding the most relevant entities for a search query.
|
|
94
|
+
*
|
|
95
|
+
* @param query - Search query (analyzed for term frequency)
|
|
96
|
+
* @param tags - Optional array of tags to filter results (lowercase)
|
|
97
|
+
* @param minImportance - Optional minimum importance value (0-10)
|
|
98
|
+
* @param maxImportance - Optional maximum importance value (0-10)
|
|
99
|
+
* @param limit - Maximum number of results to return (default: 50, max: 200)
|
|
100
|
+
* @returns Array of SearchResult objects sorted by relevance score (descending)
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const manager = new SearchManager(storage, savedSearchesPath);
|
|
105
|
+
*
|
|
106
|
+
* // Basic ranked search
|
|
107
|
+
* const results = await manager.searchNodesRanked('machine learning algorithms');
|
|
108
|
+
* results.forEach(r => {
|
|
109
|
+
* console.log(`${r.entity.name} (score: ${r.score})`);
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* // Limit to top 10 most relevant results
|
|
113
|
+
* const top10 = await manager.searchNodesRanked('database optimization', undefined, undefined, undefined, 10);
|
|
114
|
+
*
|
|
115
|
+
* // Ranked search with filters
|
|
116
|
+
* const relevantImportant = await manager.searchNodesRanked(
|
|
117
|
+
* 'security vulnerability',
|
|
118
|
+
* ['security', 'critical'],
|
|
119
|
+
* 8,
|
|
120
|
+
* 10,
|
|
121
|
+
* 20
|
|
122
|
+
* );
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async searchNodesRanked(query, tags, minImportance, maxImportance, limit) {
|
|
126
|
+
return this.rankedSearch.searchNodesRanked(query, tags, minImportance, maxImportance, limit);
|
|
127
|
+
}
|
|
128
|
+
// ==================== Boolean Search ====================
|
|
129
|
+
/**
|
|
130
|
+
* Perform boolean search with AND, OR, NOT operators.
|
|
131
|
+
*
|
|
132
|
+
* Supports complex boolean logic for precise search queries.
|
|
133
|
+
* Use AND/OR/NOT operators (case-insensitive) to combine search terms.
|
|
134
|
+
* Parentheses are supported for grouping.
|
|
135
|
+
*
|
|
136
|
+
* @param query - Boolean query string (e.g., "alice AND bob", "frontend OR backend NOT legacy")
|
|
137
|
+
* @param tags - Optional array of tags to filter results (lowercase)
|
|
138
|
+
* @param minImportance - Optional minimum importance value (0-10)
|
|
139
|
+
* @param maxImportance - Optional maximum importance value (0-10)
|
|
140
|
+
* @returns KnowledgeGraph containing entities matching the boolean expression
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const manager = new SearchManager(storage, savedSearchesPath);
|
|
145
|
+
*
|
|
146
|
+
* // AND operator - entities matching all terms
|
|
147
|
+
* const both = await manager.booleanSearch('database AND performance');
|
|
148
|
+
*
|
|
149
|
+
* // OR operator - entities matching any term
|
|
150
|
+
* const either = await manager.booleanSearch('frontend OR backend');
|
|
151
|
+
*
|
|
152
|
+
* // NOT operator - exclude terms
|
|
153
|
+
* const excluding = await manager.booleanSearch('API NOT deprecated');
|
|
154
|
+
*
|
|
155
|
+
* // Complex queries with grouping
|
|
156
|
+
* const complex = await manager.booleanSearch('(react OR vue) AND (component OR hook) NOT legacy');
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
async booleanSearch(query, tags, minImportance, maxImportance) {
|
|
160
|
+
return this.booleanSearcher.booleanSearch(query, tags, minImportance, maxImportance);
|
|
161
|
+
}
|
|
162
|
+
// ==================== Fuzzy Search ====================
|
|
163
|
+
/**
|
|
164
|
+
* Perform fuzzy search with typo tolerance.
|
|
165
|
+
*
|
|
166
|
+
* Uses Levenshtein distance to find entities that approximately match the query,
|
|
167
|
+
* making it ideal for handling typos and variations in spelling.
|
|
168
|
+
* Higher threshold values require closer matches.
|
|
169
|
+
*
|
|
170
|
+
* @param query - Search query (will match approximate spellings)
|
|
171
|
+
* @param threshold - Similarity threshold from 0.0 (very lenient) to 1.0 (exact match). Default: 0.7
|
|
172
|
+
* @param tags - Optional array of tags to filter results (lowercase)
|
|
173
|
+
* @param minImportance - Optional minimum importance value (0-10)
|
|
174
|
+
* @param maxImportance - Optional maximum importance value (0-10)
|
|
175
|
+
* @returns KnowledgeGraph containing entities with similar names/observations
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* const manager = new SearchManager(storage, savedSearchesPath);
|
|
180
|
+
*
|
|
181
|
+
* // Find entities even with typos
|
|
182
|
+
* const results = await manager.fuzzySearch('databse'); // Will match "database"
|
|
183
|
+
*
|
|
184
|
+
* // Adjust threshold for strictness
|
|
185
|
+
* const strict = await manager.fuzzySearch('optmization', 0.9); // Requires very close match
|
|
186
|
+
* const lenient = await manager.fuzzySearch('optmization', 0.6); // More tolerant of differences
|
|
187
|
+
*
|
|
188
|
+
* // Fuzzy search with filters
|
|
189
|
+
* const filtered = await manager.fuzzySearch('secrity', 0.7, ['important'], 7, 10);
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
async fuzzySearch(query, threshold, tags, minImportance, maxImportance) {
|
|
193
|
+
return this.fuzzySearcher.fuzzySearch(query, threshold, tags, minImportance, maxImportance);
|
|
194
|
+
}
|
|
195
|
+
// ==================== Search Suggestions ====================
|
|
196
|
+
/**
|
|
197
|
+
* Get search suggestions for a query.
|
|
198
|
+
*
|
|
199
|
+
* @param query - Search query
|
|
200
|
+
* @param maxSuggestions - Maximum suggestions to return
|
|
201
|
+
* @returns Array of suggested terms
|
|
202
|
+
*/
|
|
203
|
+
async getSearchSuggestions(query, maxSuggestions) {
|
|
204
|
+
return this.searchSuggestions.getSearchSuggestions(query, maxSuggestions);
|
|
205
|
+
}
|
|
206
|
+
// ==================== Saved Searches ====================
|
|
207
|
+
/**
|
|
208
|
+
* Save a search query for later reuse.
|
|
209
|
+
*
|
|
210
|
+
* Saved searches store query parameters and can be re-executed later.
|
|
211
|
+
* The system tracks usage count and last used timestamp automatically.
|
|
212
|
+
*
|
|
213
|
+
* @param search - Search parameters (name, query, and optional filters)
|
|
214
|
+
* @returns Newly created SavedSearch object with metadata
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const manager = new SearchManager(storage, savedSearchesPath);
|
|
219
|
+
*
|
|
220
|
+
* // Save a simple search
|
|
221
|
+
* const saved = await manager.saveSearch({
|
|
222
|
+
* name: 'High Priority Bugs',
|
|
223
|
+
* query: 'bug',
|
|
224
|
+
* tags: ['critical'],
|
|
225
|
+
* minImportance: 8
|
|
226
|
+
* });
|
|
227
|
+
*
|
|
228
|
+
* // Save a complex search
|
|
229
|
+
* await manager.saveSearch({
|
|
230
|
+
* name: 'Recent Frontend Work',
|
|
231
|
+
* query: 'component OR hook',
|
|
232
|
+
* tags: ['frontend', 'react'],
|
|
233
|
+
* searchType: 'boolean'
|
|
234
|
+
* });
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
async saveSearch(search) {
|
|
238
|
+
return this.savedSearchManager.saveSearch(search);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* List all saved searches.
|
|
242
|
+
*
|
|
243
|
+
* @returns Array of saved searches
|
|
244
|
+
*/
|
|
245
|
+
async listSavedSearches() {
|
|
246
|
+
return this.savedSearchManager.listSavedSearches();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get a saved search by name.
|
|
250
|
+
*
|
|
251
|
+
* @param name - Search name
|
|
252
|
+
* @returns Saved search or null
|
|
253
|
+
*/
|
|
254
|
+
async getSavedSearch(name) {
|
|
255
|
+
return this.savedSearchManager.getSavedSearch(name);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Execute a saved search by name.
|
|
259
|
+
*
|
|
260
|
+
* Runs a previously saved search with its stored parameters.
|
|
261
|
+
* Automatically updates the search's useCount and lastUsed timestamp.
|
|
262
|
+
*
|
|
263
|
+
* @param name - The unique name of the saved search to execute
|
|
264
|
+
* @returns KnowledgeGraph containing the search results
|
|
265
|
+
* @throws Error if saved search not found
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* const manager = new SearchManager(storage, savedSearchesPath);
|
|
270
|
+
*
|
|
271
|
+
* // Execute a saved search
|
|
272
|
+
* const results = await manager.executeSavedSearch('High Priority Bugs');
|
|
273
|
+
* console.log(`Found ${results.entities.length} high priority bugs`);
|
|
274
|
+
*
|
|
275
|
+
* // Handle missing saved search
|
|
276
|
+
* try {
|
|
277
|
+
* await manager.executeSavedSearch('NonExistent');
|
|
278
|
+
* } catch (error) {
|
|
279
|
+
* console.error('Search not found');
|
|
280
|
+
* }
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
async executeSavedSearch(name) {
|
|
284
|
+
return this.savedSearchManager.executeSavedSearch(name);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Delete a saved search.
|
|
288
|
+
*
|
|
289
|
+
* @param name - Search name
|
|
290
|
+
* @returns True if deleted
|
|
291
|
+
*/
|
|
292
|
+
async deleteSavedSearch(name) {
|
|
293
|
+
return this.savedSearchManager.deleteSavedSearch(name);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Update a saved search.
|
|
297
|
+
*
|
|
298
|
+
* @param name - Search name
|
|
299
|
+
* @param updates - Fields to update
|
|
300
|
+
* @returns Updated saved search
|
|
301
|
+
*/
|
|
302
|
+
async updateSavedSearch(name, updates) {
|
|
303
|
+
return this.savedSearchManager.updateSavedSearch(name, updates);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Suggestions
|
|
3
|
+
*
|
|
4
|
+
* Provides "did you mean?" suggestions using Levenshtein distance.
|
|
5
|
+
*
|
|
6
|
+
* @module search/SearchSuggestions
|
|
7
|
+
*/
|
|
8
|
+
import { levenshteinDistance } from '../utils/levenshtein.js';
|
|
9
|
+
/**
|
|
10
|
+
* Generates search suggestions based on entity names and types.
|
|
11
|
+
*/
|
|
12
|
+
export class SearchSuggestions {
|
|
13
|
+
storage;
|
|
14
|
+
constructor(storage) {
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get "did you mean?" suggestions for a query.
|
|
19
|
+
*
|
|
20
|
+
* Returns similar entity names and types that might be what the user intended.
|
|
21
|
+
* Excludes exact matches (similarity < 1.0) and very dissimilar strings (similarity > 0.5).
|
|
22
|
+
*
|
|
23
|
+
* @param query - The search query
|
|
24
|
+
* @param maxSuggestions - Maximum number of suggestions to return (default 5)
|
|
25
|
+
* @returns Array of suggested entity/type names sorted by similarity
|
|
26
|
+
*/
|
|
27
|
+
async getSearchSuggestions(query, maxSuggestions = 5) {
|
|
28
|
+
const graph = await this.storage.loadGraph();
|
|
29
|
+
const queryLower = query.toLowerCase();
|
|
30
|
+
const suggestions = [];
|
|
31
|
+
// Check entity names
|
|
32
|
+
for (const entity of graph.entities) {
|
|
33
|
+
const distance = levenshteinDistance(queryLower, entity.name.toLowerCase());
|
|
34
|
+
const maxLength = Math.max(queryLower.length, entity.name.length);
|
|
35
|
+
const similarity = 1 - distance / maxLength;
|
|
36
|
+
if (similarity > 0.5 && similarity < 1.0) {
|
|
37
|
+
// Not exact match but similar
|
|
38
|
+
suggestions.push({ text: entity.name, similarity });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Check entity types
|
|
42
|
+
const uniqueTypes = [...new Set(graph.entities.map(e => e.entityType))];
|
|
43
|
+
for (const type of uniqueTypes) {
|
|
44
|
+
const distance = levenshteinDistance(queryLower, type.toLowerCase());
|
|
45
|
+
const maxLength = Math.max(queryLower.length, type.length);
|
|
46
|
+
const similarity = 1 - distance / maxLength;
|
|
47
|
+
if (similarity > 0.5 && similarity < 1.0) {
|
|
48
|
+
suggestions.push({ text: type, similarity });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Sort by similarity and return top suggestions
|
|
52
|
+
return suggestions
|
|
53
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
54
|
+
.slice(0, maxSuggestions)
|
|
55
|
+
.map(s => s.text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF-IDF Index Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages pre-calculated TF-IDF indexes for fast ranked search.
|
|
5
|
+
* Handles index building, incremental updates, and persistence.
|
|
6
|
+
*
|
|
7
|
+
* @module search/TFIDFIndexManager
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { calculateIDF, tokenize } from '../utils/tfidf.js';
|
|
12
|
+
const INDEX_VERSION = '1.0';
|
|
13
|
+
const INDEX_FILENAME = 'tfidf-index.json';
|
|
14
|
+
/**
|
|
15
|
+
* Manages TF-IDF index lifecycle: building, updating, and persistence.
|
|
16
|
+
*/
|
|
17
|
+
export class TFIDFIndexManager {
|
|
18
|
+
indexPath;
|
|
19
|
+
index = null;
|
|
20
|
+
constructor(storageDir) {
|
|
21
|
+
this.indexPath = path.join(storageDir, '.indexes', INDEX_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build a complete TF-IDF index from a knowledge graph.
|
|
25
|
+
*
|
|
26
|
+
* @param graph - Knowledge graph to index
|
|
27
|
+
* @returns Newly built TF-IDF index
|
|
28
|
+
*/
|
|
29
|
+
async buildIndex(graph) {
|
|
30
|
+
const documents = new Map();
|
|
31
|
+
const allDocumentTexts = [];
|
|
32
|
+
const allTokens = [];
|
|
33
|
+
// Build document vectors
|
|
34
|
+
for (const entity of graph.entities) {
|
|
35
|
+
const documentText = [
|
|
36
|
+
entity.name,
|
|
37
|
+
entity.entityType,
|
|
38
|
+
...entity.observations,
|
|
39
|
+
].join(' ');
|
|
40
|
+
allDocumentTexts.push(documentText);
|
|
41
|
+
const tokens = tokenize(documentText);
|
|
42
|
+
allTokens.push(tokens);
|
|
43
|
+
// Calculate term frequencies
|
|
44
|
+
const termFreq = {};
|
|
45
|
+
for (const term of tokens) {
|
|
46
|
+
termFreq[term] = (termFreq[term] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
documents.set(entity.name, {
|
|
49
|
+
entityName: entity.name,
|
|
50
|
+
terms: termFreq,
|
|
51
|
+
documentText,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Calculate IDF for all terms
|
|
55
|
+
const idf = new Map();
|
|
56
|
+
const allTerms = new Set(allTokens.flat());
|
|
57
|
+
for (const term of allTerms) {
|
|
58
|
+
const idfScore = calculateIDF(term, allDocumentTexts);
|
|
59
|
+
idf.set(term, idfScore);
|
|
60
|
+
}
|
|
61
|
+
this.index = {
|
|
62
|
+
version: INDEX_VERSION,
|
|
63
|
+
lastUpdated: new Date().toISOString(),
|
|
64
|
+
documents,
|
|
65
|
+
idf,
|
|
66
|
+
};
|
|
67
|
+
return this.index;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Update the index incrementally when entities change.
|
|
71
|
+
*
|
|
72
|
+
* More efficient than rebuilding the entire index.
|
|
73
|
+
*
|
|
74
|
+
* @param graph - Updated knowledge graph
|
|
75
|
+
* @param changedEntityNames - Names of entities that changed
|
|
76
|
+
*/
|
|
77
|
+
async updateIndex(graph, changedEntityNames) {
|
|
78
|
+
if (!this.index) {
|
|
79
|
+
// No existing index, build from scratch
|
|
80
|
+
return this.buildIndex(graph);
|
|
81
|
+
}
|
|
82
|
+
// Rebuild document vectors for changed entities
|
|
83
|
+
const allDocumentTexts = [];
|
|
84
|
+
const allTokens = [];
|
|
85
|
+
const updatedDocuments = new Map(this.index.documents);
|
|
86
|
+
// Remove deleted entities
|
|
87
|
+
for (const entityName of changedEntityNames) {
|
|
88
|
+
const entity = graph.entities.find(e => e.name === entityName);
|
|
89
|
+
if (!entity) {
|
|
90
|
+
updatedDocuments.delete(entityName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Update/add changed entities
|
|
94
|
+
for (const entity of graph.entities) {
|
|
95
|
+
const documentText = [
|
|
96
|
+
entity.name,
|
|
97
|
+
entity.entityType,
|
|
98
|
+
...entity.observations,
|
|
99
|
+
].join(' ');
|
|
100
|
+
allDocumentTexts.push(documentText);
|
|
101
|
+
const tokens = tokenize(documentText);
|
|
102
|
+
allTokens.push(tokens);
|
|
103
|
+
if (changedEntityNames.has(entity.name)) {
|
|
104
|
+
// Calculate term frequencies for changed entity
|
|
105
|
+
const termFreq = {};
|
|
106
|
+
for (const term of tokens) {
|
|
107
|
+
termFreq[term] = (termFreq[term] || 0) + 1;
|
|
108
|
+
}
|
|
109
|
+
updatedDocuments.set(entity.name, {
|
|
110
|
+
entityName: entity.name,
|
|
111
|
+
terms: termFreq,
|
|
112
|
+
documentText,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Recalculate IDF (need all documents for accurate IDF)
|
|
117
|
+
const idf = new Map();
|
|
118
|
+
const allTerms = new Set(allTokens.flat());
|
|
119
|
+
for (const term of allTerms) {
|
|
120
|
+
const idfScore = calculateIDF(term, allDocumentTexts);
|
|
121
|
+
idf.set(term, idfScore);
|
|
122
|
+
}
|
|
123
|
+
this.index = {
|
|
124
|
+
version: INDEX_VERSION,
|
|
125
|
+
lastUpdated: new Date().toISOString(),
|
|
126
|
+
documents: updatedDocuments,
|
|
127
|
+
idf,
|
|
128
|
+
};
|
|
129
|
+
return this.index;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Load index from disk.
|
|
133
|
+
*
|
|
134
|
+
* @returns Loaded index or null if not found
|
|
135
|
+
*/
|
|
136
|
+
async loadIndex() {
|
|
137
|
+
try {
|
|
138
|
+
const data = await fs.readFile(this.indexPath, 'utf-8');
|
|
139
|
+
const serialized = JSON.parse(data);
|
|
140
|
+
this.index = {
|
|
141
|
+
version: serialized.version,
|
|
142
|
+
lastUpdated: serialized.lastUpdated,
|
|
143
|
+
documents: new Map(serialized.documents),
|
|
144
|
+
idf: new Map(serialized.idf),
|
|
145
|
+
};
|
|
146
|
+
return this.index;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// Index doesn't exist or is invalid
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Save index to disk.
|
|
155
|
+
*
|
|
156
|
+
* @param index - Index to save (uses cached index if not provided)
|
|
157
|
+
*/
|
|
158
|
+
async saveIndex(index) {
|
|
159
|
+
const indexToSave = index || this.index;
|
|
160
|
+
if (!indexToSave) {
|
|
161
|
+
throw new Error('No index to save');
|
|
162
|
+
}
|
|
163
|
+
// Ensure index directory exists
|
|
164
|
+
const indexDir = path.dirname(this.indexPath);
|
|
165
|
+
await fs.mkdir(indexDir, { recursive: true });
|
|
166
|
+
// Serialize Map objects to arrays for JSON
|
|
167
|
+
const serialized = {
|
|
168
|
+
version: indexToSave.version,
|
|
169
|
+
lastUpdated: indexToSave.lastUpdated,
|
|
170
|
+
documents: Array.from(indexToSave.documents.entries()),
|
|
171
|
+
idf: Array.from(indexToSave.idf.entries()),
|
|
172
|
+
};
|
|
173
|
+
await fs.writeFile(this.indexPath, JSON.stringify(serialized, null, 2), 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get the current cached index.
|
|
177
|
+
*
|
|
178
|
+
* @returns Cached index or null if not loaded
|
|
179
|
+
*/
|
|
180
|
+
getIndex() {
|
|
181
|
+
return this.index;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Clear the cached index and delete from disk.
|
|
185
|
+
*/
|
|
186
|
+
async clearIndex() {
|
|
187
|
+
this.index = null;
|
|
188
|
+
try {
|
|
189
|
+
await fs.unlink(this.indexPath);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Index file doesn't exist, nothing to delete
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if the index needs rebuilding based on graph state.
|
|
197
|
+
*
|
|
198
|
+
* @param graph - Current knowledge graph
|
|
199
|
+
* @returns True if index should be rebuilt
|
|
200
|
+
*/
|
|
201
|
+
needsRebuild(graph) {
|
|
202
|
+
if (!this.index) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
// Check if entity count matches
|
|
206
|
+
if (this.index.documents.size !== graph.entities.length) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
// Check if all entities are in index
|
|
210
|
+
for (const entity of graph.entities) {
|
|
211
|
+
if (!this.index.documents.has(entity.name)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Module Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Sprint 2: Added SearchFilterChain for centralized filter logic
|
|
5
|
+
*/
|
|
6
|
+
export { BasicSearch } from './BasicSearch.js';
|
|
7
|
+
export { RankedSearch } from './RankedSearch.js';
|
|
8
|
+
export { BooleanSearch } from './BooleanSearch.js';
|
|
9
|
+
export { FuzzySearch } from './FuzzySearch.js';
|
|
10
|
+
export { SearchSuggestions } from './SearchSuggestions.js';
|
|
11
|
+
export { SavedSearchManager } from './SavedSearchManager.js';
|
|
12
|
+
export { SearchManager } from './SearchManager.js';
|
|
13
|
+
// Sprint 2: Search Filter Chain utilities
|
|
14
|
+
export { SearchFilterChain } from './SearchFilterChain.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Handles Model Context Protocol server initialization and tool registration.
|
|
5
|
+
* Tool definitions and handlers are extracted to separate modules for maintainability.
|
|
6
|
+
*
|
|
7
|
+
* @module server/MCPServer
|
|
8
|
+
*/
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
import { toolDefinitions } from './toolDefinitions.js';
|
|
14
|
+
import { handleToolCall } from './toolHandlers.js';
|
|
15
|
+
/**
|
|
16
|
+
* MCP Server for Knowledge Graph operations.
|
|
17
|
+
* Exposes tools for entity/relation management, search, and analysis.
|
|
18
|
+
*/
|
|
19
|
+
export class MCPServer {
|
|
20
|
+
server;
|
|
21
|
+
manager;
|
|
22
|
+
constructor(manager) {
|
|
23
|
+
this.manager = manager;
|
|
24
|
+
this.server = new Server({
|
|
25
|
+
name: "memory-server",
|
|
26
|
+
version: "0.8.0",
|
|
27
|
+
}, {
|
|
28
|
+
capabilities: {
|
|
29
|
+
tools: {},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
this.registerToolHandlers();
|
|
33
|
+
}
|
|
34
|
+
registerToolHandlers() {
|
|
35
|
+
// Register list tools handler
|
|
36
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
37
|
+
return {
|
|
38
|
+
tools: toolDefinitions,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
// Register call tool handler
|
|
42
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
43
|
+
const { name, arguments: args } = request.params;
|
|
44
|
+
return handleToolCall(name, args || {}, this.manager);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async start() {
|
|
48
|
+
const transport = new StdioServerTransport();
|
|
49
|
+
await this.server.connect(transport);
|
|
50
|
+
logger.info('Knowledge Graph MCP Server running on stdio');
|
|
51
|
+
}
|
|
52
|
+
}
|