@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.
Files changed (70) hide show
  1. package/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
  2. package/dist/__tests__/file-path.test.js +5 -5
  3. package/dist/__tests__/integration/workflows.test.js +449 -0
  4. package/dist/__tests__/knowledge-graph.test.js +8 -3
  5. package/dist/__tests__/performance/benchmarks.test.js +413 -0
  6. package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
  7. package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
  8. package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
  9. package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
  10. package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
  11. package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
  12. package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
  13. package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
  14. package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
  15. package/dist/core/EntityManager.js +554 -0
  16. package/dist/core/GraphStorage.js +172 -0
  17. package/dist/core/KnowledgeGraphManager.js +423 -0
  18. package/dist/core/ObservationManager.js +129 -0
  19. package/dist/core/RelationManager.js +186 -0
  20. package/dist/core/TransactionManager.js +389 -0
  21. package/dist/core/index.js +9 -0
  22. package/dist/features/AnalyticsManager.js +222 -0
  23. package/dist/features/ArchiveManager.js +74 -0
  24. package/dist/features/BackupManager.js +311 -0
  25. package/dist/features/CompressionManager.js +291 -0
  26. package/dist/features/ExportManager.js +305 -0
  27. package/dist/features/HierarchyManager.js +219 -0
  28. package/dist/features/ImportExportManager.js +50 -0
  29. package/dist/features/ImportManager.js +328 -0
  30. package/dist/features/TagManager.js +210 -0
  31. package/dist/features/index.js +12 -0
  32. package/dist/index.js +13 -996
  33. package/dist/memory.jsonl +18 -0
  34. package/dist/search/BasicSearch.js +131 -0
  35. package/dist/search/BooleanSearch.js +283 -0
  36. package/dist/search/FuzzySearch.js +96 -0
  37. package/dist/search/RankedSearch.js +190 -0
  38. package/dist/search/SavedSearchManager.js +145 -0
  39. package/dist/search/SearchFilterChain.js +187 -0
  40. package/dist/search/SearchManager.js +305 -0
  41. package/dist/search/SearchSuggestions.js +57 -0
  42. package/dist/search/TFIDFIndexManager.js +217 -0
  43. package/dist/search/index.js +14 -0
  44. package/dist/server/MCPServer.js +52 -0
  45. package/dist/server/toolDefinitions.js +732 -0
  46. package/dist/server/toolHandlers.js +117 -0
  47. package/dist/types/analytics.types.js +6 -0
  48. package/dist/types/entity.types.js +7 -0
  49. package/dist/types/import-export.types.js +7 -0
  50. package/dist/types/index.js +12 -0
  51. package/dist/types/search.types.js +7 -0
  52. package/dist/types/tag.types.js +6 -0
  53. package/dist/utils/constants.js +128 -0
  54. package/dist/utils/dateUtils.js +89 -0
  55. package/dist/utils/entityUtils.js +108 -0
  56. package/dist/utils/errors.js +121 -0
  57. package/dist/utils/filterUtils.js +155 -0
  58. package/dist/utils/index.js +39 -0
  59. package/dist/utils/levenshtein.js +62 -0
  60. package/dist/utils/logger.js +33 -0
  61. package/dist/utils/paginationUtils.js +81 -0
  62. package/dist/utils/pathUtils.js +115 -0
  63. package/dist/utils/responseFormatter.js +55 -0
  64. package/dist/utils/schemas.js +184 -0
  65. package/dist/utils/searchCache.js +209 -0
  66. package/dist/utils/tagUtils.js +107 -0
  67. package/dist/utils/tfidf.js +90 -0
  68. package/dist/utils/validationHelper.js +99 -0
  69. package/dist/utils/validationUtils.js +109 -0
  70. package/package.json +82 -48
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Ranked Search
3
+ *
4
+ * TF-IDF relevance-based search with scoring and pre-calculated indexes.
5
+ *
6
+ * @module search/RankedSearch
7
+ */
8
+ import { calculateTFIDF, tokenize } from '../utils/tfidf.js';
9
+ import { SEARCH_LIMITS } from '../utils/constants.js';
10
+ import { TFIDFIndexManager } from './TFIDFIndexManager.js';
11
+ import { SearchFilterChain } from './SearchFilterChain.js';
12
+ /**
13
+ * Performs TF-IDF ranked search with optional pre-calculated indexes.
14
+ */
15
+ export class RankedSearch {
16
+ storage;
17
+ indexManager = null;
18
+ constructor(storage, storageDir) {
19
+ this.storage = storage;
20
+ // Initialize index manager if storage directory is provided
21
+ if (storageDir) {
22
+ this.indexManager = new TFIDFIndexManager(storageDir);
23
+ }
24
+ }
25
+ /**
26
+ * Initialize and build the TF-IDF index for fast searches.
27
+ *
28
+ * Should be called after graph changes to keep index up-to-date.
29
+ */
30
+ async buildIndex() {
31
+ if (!this.indexManager) {
32
+ throw new Error('Index manager not initialized. Provide storageDir to constructor.');
33
+ }
34
+ const graph = await this.storage.loadGraph();
35
+ await this.indexManager.buildIndex(graph);
36
+ await this.indexManager.saveIndex();
37
+ }
38
+ /**
39
+ * Update the index incrementally after entity changes.
40
+ *
41
+ * @param changedEntityNames - Names of entities that were created, updated, or deleted
42
+ */
43
+ async updateIndex(changedEntityNames) {
44
+ if (!this.indexManager) {
45
+ return; // No index manager, skip
46
+ }
47
+ const graph = await this.storage.loadGraph();
48
+ await this.indexManager.updateIndex(graph, changedEntityNames);
49
+ await this.indexManager.saveIndex();
50
+ }
51
+ /**
52
+ * Load the TF-IDF index from disk if available.
53
+ */
54
+ async ensureIndexLoaded() {
55
+ if (!this.indexManager) {
56
+ return null;
57
+ }
58
+ // Return cached index if already loaded
59
+ const cached = this.indexManager.getIndex();
60
+ if (cached) {
61
+ return cached;
62
+ }
63
+ // Try to load from disk
64
+ return await this.indexManager.loadIndex();
65
+ }
66
+ /**
67
+ * Search with TF-IDF relevance ranking.
68
+ *
69
+ * Uses pre-calculated index if available, falls back to on-the-fly calculation.
70
+ *
71
+ * @param query - Search query
72
+ * @param tags - Optional tags filter
73
+ * @param minImportance - Optional minimum importance
74
+ * @param maxImportance - Optional maximum importance
75
+ * @param limit - Maximum results to return (default 50, max 200)
76
+ * @returns Array of search results sorted by relevance
77
+ */
78
+ async searchNodesRanked(query, tags, minImportance, maxImportance, limit = SEARCH_LIMITS.DEFAULT) {
79
+ // Enforce maximum search limit
80
+ const effectiveLimit = Math.min(limit, SEARCH_LIMITS.MAX);
81
+ const graph = await this.storage.loadGraph();
82
+ // Apply tag and importance filters using SearchFilterChain
83
+ const filters = { tags, minImportance, maxImportance };
84
+ const filteredEntities = SearchFilterChain.applyFilters(graph.entities, filters);
85
+ // Try to use pre-calculated index
86
+ const index = await this.ensureIndexLoaded();
87
+ const queryTerms = tokenize(query);
88
+ if (index) {
89
+ // Use pre-calculated index for fast search
90
+ return this.searchWithIndex(filteredEntities, queryTerms, index, effectiveLimit);
91
+ }
92
+ else {
93
+ // Fall back to on-the-fly calculation
94
+ return this.searchWithoutIndex(filteredEntities, queryTerms, effectiveLimit);
95
+ }
96
+ }
97
+ /**
98
+ * Search using pre-calculated TF-IDF index (fast path).
99
+ */
100
+ searchWithIndex(entities, queryTerms, index, limit) {
101
+ const results = [];
102
+ for (const entity of entities) {
103
+ const docVector = index.documents.get(entity.name);
104
+ if (!docVector) {
105
+ continue; // Entity not in index
106
+ }
107
+ // Calculate total terms in document (sum of all term frequencies)
108
+ const totalTerms = Object.values(docVector.terms).reduce((sum, count) => sum + count, 0);
109
+ if (totalTerms === 0)
110
+ continue;
111
+ // Calculate score using pre-calculated term frequencies and IDF
112
+ let totalScore = 0;
113
+ const matchedFields = {};
114
+ for (const term of queryTerms) {
115
+ const termCount = docVector.terms[term] || 0;
116
+ const idf = index.idf.get(term) || 0;
117
+ // Calculate TF-IDF: (termCount / totalTerms) * IDF
118
+ const tf = termCount / totalTerms;
119
+ const tfidf = tf * idf;
120
+ totalScore += tfidf;
121
+ // Track which fields matched
122
+ if (termCount > 0) {
123
+ if (entity.name.toLowerCase().includes(term)) {
124
+ matchedFields.name = true;
125
+ }
126
+ if (entity.entityType.toLowerCase().includes(term)) {
127
+ matchedFields.entityType = true;
128
+ }
129
+ const matchedObs = entity.observations.filter(o => o.toLowerCase().includes(term));
130
+ if (matchedObs.length > 0) {
131
+ matchedFields.observations = matchedObs;
132
+ }
133
+ }
134
+ }
135
+ // Only include entities with non-zero scores
136
+ if (totalScore > 0) {
137
+ results.push({
138
+ entity,
139
+ score: totalScore,
140
+ matchedFields,
141
+ });
142
+ }
143
+ }
144
+ // Sort by score descending and apply limit
145
+ return results
146
+ .sort((a, b) => b.score - a.score)
147
+ .slice(0, limit);
148
+ }
149
+ /**
150
+ * Search without index (on-the-fly calculation, slow path).
151
+ */
152
+ searchWithoutIndex(entities, queryTerms, limit) {
153
+ const results = [];
154
+ const documents = entities.map(e => [e.name, e.entityType, ...e.observations].join(' '));
155
+ for (let i = 0; i < entities.length; i++) {
156
+ const entity = entities[i];
157
+ const document = documents[i];
158
+ // Calculate score for each query term
159
+ let totalScore = 0;
160
+ const matchedFields = {};
161
+ for (const term of queryTerms) {
162
+ const score = calculateTFIDF(term, document, documents);
163
+ totalScore += score;
164
+ // Track which fields matched
165
+ if (entity.name.toLowerCase().includes(term)) {
166
+ matchedFields.name = true;
167
+ }
168
+ if (entity.entityType.toLowerCase().includes(term)) {
169
+ matchedFields.entityType = true;
170
+ }
171
+ const matchedObs = entity.observations.filter(o => o.toLowerCase().includes(term));
172
+ if (matchedObs.length > 0) {
173
+ matchedFields.observations = matchedObs;
174
+ }
175
+ }
176
+ // Only include entities with non-zero scores
177
+ if (totalScore > 0) {
178
+ results.push({
179
+ entity,
180
+ score: totalScore,
181
+ matchedFields,
182
+ });
183
+ }
184
+ }
185
+ // Sort by score descending and apply limit
186
+ return results
187
+ .sort((a, b) => b.score - a.score)
188
+ .slice(0, limit);
189
+ }
190
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Saved Search Manager
3
+ *
4
+ * Manages persistent saved searches with JSONL storage.
5
+ *
6
+ * @module search/SavedSearchManager
7
+ */
8
+ import * as fs from 'fs/promises';
9
+ /**
10
+ * Manages saved search queries with usage tracking.
11
+ */
12
+ export class SavedSearchManager {
13
+ savedSearchesFilePath;
14
+ basicSearch;
15
+ constructor(savedSearchesFilePath, basicSearch) {
16
+ this.savedSearchesFilePath = savedSearchesFilePath;
17
+ this.basicSearch = basicSearch;
18
+ }
19
+ /**
20
+ * Load all saved searches from JSONL file.
21
+ *
22
+ * @returns Array of saved searches
23
+ */
24
+ async loadSavedSearches() {
25
+ try {
26
+ const data = await fs.readFile(this.savedSearchesFilePath, 'utf-8');
27
+ const lines = data.split('\n').filter((line) => line.trim() !== '');
28
+ return lines.map((line) => JSON.parse(line));
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
32
+ return [];
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ /**
38
+ * Save searches to JSONL file.
39
+ *
40
+ * @param searches - Array of saved searches
41
+ */
42
+ async saveSavedSearches(searches) {
43
+ const lines = searches.map(s => JSON.stringify(s));
44
+ await fs.writeFile(this.savedSearchesFilePath, lines.join('\n'));
45
+ }
46
+ /**
47
+ * Save a search query for later reuse.
48
+ *
49
+ * @param search - Search parameters (without createdAt, useCount, lastUsed)
50
+ * @returns The newly created saved search
51
+ * @throws Error if search name already exists
52
+ */
53
+ async saveSearch(search) {
54
+ const searches = await this.loadSavedSearches();
55
+ // Check if name already exists
56
+ if (searches.some(s => s.name === search.name)) {
57
+ throw new Error(`Saved search with name "${search.name}" already exists`);
58
+ }
59
+ const newSearch = {
60
+ ...search,
61
+ createdAt: new Date().toISOString(),
62
+ useCount: 0,
63
+ };
64
+ searches.push(newSearch);
65
+ await this.saveSavedSearches(searches);
66
+ return newSearch;
67
+ }
68
+ /**
69
+ * List all saved searches.
70
+ *
71
+ * @returns Array of all saved searches
72
+ */
73
+ async listSavedSearches() {
74
+ return await this.loadSavedSearches();
75
+ }
76
+ /**
77
+ * Get a specific saved search by name.
78
+ *
79
+ * @param name - Search name
80
+ * @returns Saved search or null if not found
81
+ */
82
+ async getSavedSearch(name) {
83
+ const searches = await this.loadSavedSearches();
84
+ return searches.find(s => s.name === name) || null;
85
+ }
86
+ /**
87
+ * Execute a saved search by name.
88
+ *
89
+ * Updates usage statistics (lastUsed, useCount) before executing.
90
+ *
91
+ * @param name - Search name
92
+ * @returns Search results as knowledge graph
93
+ * @throws Error if search not found
94
+ */
95
+ async executeSavedSearch(name) {
96
+ const searches = await this.loadSavedSearches();
97
+ const search = searches.find(s => s.name === name);
98
+ if (!search) {
99
+ throw new Error(`Saved search "${name}" not found`);
100
+ }
101
+ // Update usage statistics
102
+ search.lastUsed = new Date().toISOString();
103
+ search.useCount++;
104
+ await this.saveSavedSearches(searches);
105
+ // Execute the search using BasicSearch
106
+ return await this.basicSearch.searchNodes(search.query, search.tags, search.minImportance, search.maxImportance);
107
+ }
108
+ /**
109
+ * Delete a saved search.
110
+ *
111
+ * @param name - Search name
112
+ * @returns True if deleted, false if not found
113
+ */
114
+ async deleteSavedSearch(name) {
115
+ const searches = await this.loadSavedSearches();
116
+ const initialLength = searches.length;
117
+ const filtered = searches.filter(s => s.name !== name);
118
+ if (filtered.length === initialLength) {
119
+ return false; // Search not found
120
+ }
121
+ await this.saveSavedSearches(filtered);
122
+ return true;
123
+ }
124
+ /**
125
+ * Update a saved search.
126
+ *
127
+ * Cannot update name, createdAt, useCount, or lastUsed fields.
128
+ *
129
+ * @param name - Search name
130
+ * @param updates - Partial search with fields to update
131
+ * @returns Updated saved search
132
+ * @throws Error if search not found
133
+ */
134
+ async updateSavedSearch(name, updates) {
135
+ const searches = await this.loadSavedSearches();
136
+ const search = searches.find(s => s.name === name);
137
+ if (!search) {
138
+ throw new Error(`Saved search "${name}" not found`);
139
+ }
140
+ // Apply updates
141
+ Object.assign(search, updates);
142
+ await this.saveSavedSearches(searches);
143
+ return search;
144
+ }
145
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Search Filter Chain
3
+ *
4
+ * Centralizes filter logic for all search implementations to eliminate
5
+ * duplicate filtering code across BasicSearch, BooleanSearch, FuzzySearch,
6
+ * and RankedSearch.
7
+ *
8
+ * @module search/SearchFilterChain
9
+ */
10
+ import { normalizeTags, hasMatchingTag } from '../utils/tagUtils.js';
11
+ import { isWithinImportanceRange } from '../utils/filterUtils.js';
12
+ import { validatePagination, applyPagination } from '../utils/paginationUtils.js';
13
+ /**
14
+ * Centralized filter chain for all search implementations.
15
+ * Ensures consistent filtering behavior across search types.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const filters: SearchFilters = { tags: ['important'], minImportance: 5 };
20
+ * const filtered = SearchFilterChain.applyFilters(entities, filters);
21
+ * const pagination = SearchFilterChain.validatePagination(0, 50);
22
+ * const result = SearchFilterChain.paginate(filtered, pagination);
23
+ * ```
24
+ */
25
+ export class SearchFilterChain {
26
+ /**
27
+ * Applies all filters to an array of entities.
28
+ * Entities must pass ALL specified filters to be included.
29
+ *
30
+ * @param entities - Entities to filter
31
+ * @param filters - Filter criteria to apply
32
+ * @returns Filtered entities array
33
+ */
34
+ static applyFilters(entities, filters) {
35
+ // Early return if no filters are active
36
+ if (!this.hasActiveFilters(filters)) {
37
+ return entities;
38
+ }
39
+ // Pre-normalize tags once for efficiency
40
+ const normalizedSearchTags = filters.tags?.length
41
+ ? normalizeTags(filters.tags)
42
+ : undefined;
43
+ return entities.filter(entity => this.entityPassesFilters(entity, filters, normalizedSearchTags));
44
+ }
45
+ /**
46
+ * Checks if an entity passes all active filters.
47
+ * Short-circuits on first failing filter for performance.
48
+ *
49
+ * @param entity - Entity to check
50
+ * @param filters - Filter criteria
51
+ * @param normalizedSearchTags - Pre-normalized search tags (for efficiency)
52
+ * @returns true if entity passes all filters
53
+ */
54
+ static entityPassesFilters(entity, filters, normalizedSearchTags) {
55
+ // Tag filter - check if entity has any matching tag
56
+ if (normalizedSearchTags && normalizedSearchTags.length > 0) {
57
+ if (!entity.tags || entity.tags.length === 0) {
58
+ return false;
59
+ }
60
+ const entityTags = normalizeTags(entity.tags);
61
+ const hasMatch = normalizedSearchTags.some(tag => entityTags.includes(tag));
62
+ if (!hasMatch) {
63
+ return false;
64
+ }
65
+ }
66
+ // Importance filter
67
+ if (!isWithinImportanceRange(entity.importance, filters.minImportance, filters.maxImportance)) {
68
+ return false;
69
+ }
70
+ // Entity type filter
71
+ if (filters.entityType && entity.entityType !== filters.entityType) {
72
+ return false;
73
+ }
74
+ // Created date filter
75
+ if (filters.createdAfter || filters.createdBefore) {
76
+ if (!entity.createdAt) {
77
+ return false;
78
+ }
79
+ const createdAt = new Date(entity.createdAt);
80
+ if (filters.createdAfter && createdAt < new Date(filters.createdAfter)) {
81
+ return false;
82
+ }
83
+ if (filters.createdBefore && createdAt > new Date(filters.createdBefore)) {
84
+ return false;
85
+ }
86
+ }
87
+ // Modified date filter
88
+ if (filters.modifiedAfter || filters.modifiedBefore) {
89
+ if (!entity.lastModified) {
90
+ return false;
91
+ }
92
+ const modifiedAt = new Date(entity.lastModified);
93
+ if (filters.modifiedAfter && modifiedAt < new Date(filters.modifiedAfter)) {
94
+ return false;
95
+ }
96
+ if (filters.modifiedBefore && modifiedAt > new Date(filters.modifiedBefore)) {
97
+ return false;
98
+ }
99
+ }
100
+ return true;
101
+ }
102
+ /**
103
+ * Checks if any filters are actually specified.
104
+ * Used for early return optimization.
105
+ *
106
+ * @param filters - Filter criteria to check
107
+ * @returns true if at least one filter is active
108
+ */
109
+ static hasActiveFilters(filters) {
110
+ return !!((filters.tags && filters.tags.length > 0) ||
111
+ filters.minImportance !== undefined ||
112
+ filters.maxImportance !== undefined ||
113
+ filters.entityType ||
114
+ filters.createdAfter ||
115
+ filters.createdBefore ||
116
+ filters.modifiedAfter ||
117
+ filters.modifiedBefore);
118
+ }
119
+ /**
120
+ * Validates and returns pagination parameters.
121
+ * Delegates to paginationUtils.validatePagination.
122
+ *
123
+ * @param offset - Starting position
124
+ * @param limit - Maximum results
125
+ * @returns Validated pagination object
126
+ */
127
+ static validatePagination(offset = 0, limit) {
128
+ return validatePagination(offset, limit);
129
+ }
130
+ /**
131
+ * Applies pagination to a filtered result set.
132
+ *
133
+ * @param entities - Entities to paginate
134
+ * @param pagination - Validated pagination parameters
135
+ * @returns Paginated slice of entities
136
+ */
137
+ static paginate(entities, pagination) {
138
+ return applyPagination(entities, pagination);
139
+ }
140
+ /**
141
+ * Convenience method to apply both filters and pagination in one call.
142
+ *
143
+ * @param entities - Entities to process
144
+ * @param filters - Filter criteria
145
+ * @param offset - Pagination offset
146
+ * @param limit - Pagination limit
147
+ * @returns Filtered and paginated entities
148
+ */
149
+ static filterAndPaginate(entities, filters, offset = 0, limit) {
150
+ const filtered = this.applyFilters(entities, filters);
151
+ const pagination = this.validatePagination(offset, limit);
152
+ return this.paginate(filtered, pagination);
153
+ }
154
+ /**
155
+ * Applies tag filter only. Useful when other filters are handled separately.
156
+ *
157
+ * @param entities - Entities to filter
158
+ * @param tags - Tags to filter by
159
+ * @returns Filtered entities
160
+ */
161
+ static filterByTags(entities, tags) {
162
+ if (!tags || tags.length === 0) {
163
+ return entities;
164
+ }
165
+ const normalizedTags = normalizeTags(tags);
166
+ return entities.filter(entity => {
167
+ if (!entity.tags || entity.tags.length === 0) {
168
+ return false;
169
+ }
170
+ return hasMatchingTag(entity.tags, normalizedTags);
171
+ });
172
+ }
173
+ /**
174
+ * Applies importance filter only. Useful when other filters are handled separately.
175
+ *
176
+ * @param entities - Entities to filter
177
+ * @param minImportance - Minimum importance
178
+ * @param maxImportance - Maximum importance
179
+ * @returns Filtered entities
180
+ */
181
+ static filterByImportance(entities, minImportance, maxImportance) {
182
+ if (minImportance === undefined && maxImportance === undefined) {
183
+ return entities;
184
+ }
185
+ return entities.filter(entity => isWithinImportanceRange(entity.importance, minImportance, maxImportance));
186
+ }
187
+ }