memory-journal-mcp 4.3.0 → 4.4.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.
Files changed (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -0,0 +1,335 @@
1
+ /**
2
+ * VectorSearchManager Tests
3
+ *
4
+ * Tests VectorSearchManager with mocked @xenova/transformers pipeline
5
+ * and vectra LocalIndex. No real model loading or file I/O needed.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
9
+
10
+ // ============================================================================
11
+ // Hoisted mock functions (must be declared before vi.mock)
12
+ // ============================================================================
13
+
14
+ const {
15
+ mockEmbedderFn,
16
+ mockIsIndexCreated,
17
+ mockCreateIndex,
18
+ mockInsertItem,
19
+ mockDeleteItem,
20
+ mockQueryItems,
21
+ mockListItems,
22
+ mockGetIndexStats,
23
+ } = vi.hoisted(() => ({
24
+ mockEmbedderFn: vi.fn(),
25
+ mockIsIndexCreated: vi.fn(),
26
+ mockCreateIndex: vi.fn(),
27
+ mockInsertItem: vi.fn(),
28
+ mockDeleteItem: vi.fn(),
29
+ mockQueryItems: vi.fn(),
30
+ mockListItems: vi.fn(),
31
+ mockGetIndexStats: vi.fn(),
32
+ }))
33
+
34
+ // ============================================================================
35
+ // Module mocks
36
+ // ============================================================================
37
+
38
+ vi.mock('vectra', () => ({
39
+ LocalIndex: function () {
40
+ return {
41
+ isIndexCreated: mockIsIndexCreated,
42
+ createIndex: mockCreateIndex,
43
+ insertItem: mockInsertItem,
44
+ deleteItem: mockDeleteItem,
45
+ queryItems: mockQueryItems,
46
+ listItems: mockListItems,
47
+ getIndexStats: mockGetIndexStats,
48
+ }
49
+ },
50
+ }))
51
+
52
+ vi.mock('@xenova/transformers', () => ({
53
+ pipeline: vi.fn().mockResolvedValue(mockEmbedderFn),
54
+ }))
55
+
56
+ vi.mock('node:fs', async (importOriginal) => {
57
+ const real = (await importOriginal()) as Record<string, unknown>
58
+ return {
59
+ ...real,
60
+ existsSync: vi.fn().mockReturnValue(true),
61
+ mkdirSync: vi.fn(),
62
+ rmSync: vi.fn(),
63
+ }
64
+ })
65
+
66
+ // Import AFTER mocks are set up
67
+ import { VectorSearchManager } from '../../src/vector/VectorSearchManager.js'
68
+
69
+ /**
70
+ * Helper to make the VectorSearchManager think it's initialized
71
+ */
72
+ async function initManager(vm: VectorSearchManager): Promise<void> {
73
+ mockIsIndexCreated.mockResolvedValue(true)
74
+ mockEmbedderFn.mockResolvedValue({ data: new Float32Array(384) })
75
+ await vm.initialize()
76
+ }
77
+
78
+ /** Generate a fake embedding vector of length 384 */
79
+ function fakeEmbedding(seed = 0): Float32Array {
80
+ const arr = new Float32Array(384)
81
+ for (let i = 0; i < 384; i++) arr[i] = Math.sin(i + seed) * 0.1
82
+ return arr
83
+ }
84
+
85
+ describe('VectorSearchManager', () => {
86
+ let vm: VectorSearchManager
87
+
88
+ beforeEach(() => {
89
+ vi.clearAllMocks()
90
+ vm = new VectorSearchManager('/tmp/test.db')
91
+ })
92
+
93
+ // ========================================================================
94
+ // Initialization
95
+ // ========================================================================
96
+
97
+ describe('isInitialized', () => {
98
+ it('should return false before initialize()', () => {
99
+ expect(vm.isInitialized()).toBe(false)
100
+ })
101
+
102
+ it('should return true after initialize()', async () => {
103
+ await initManager(vm)
104
+ expect(vm.isInitialized()).toBe(true)
105
+ })
106
+ })
107
+
108
+ describe('initialize', () => {
109
+ it('should load pipeline and create index if needed', async () => {
110
+ mockIsIndexCreated.mockResolvedValue(false)
111
+ mockEmbedderFn.mockResolvedValue({ data: new Float32Array(384) })
112
+
113
+ await vm.initialize()
114
+
115
+ expect(mockCreateIndex).toHaveBeenCalled()
116
+ expect(vm.isInitialized()).toBe(true)
117
+ })
118
+
119
+ it('should be idempotent', async () => {
120
+ await initManager(vm)
121
+ await vm.initialize()
122
+ expect(vm.isInitialized()).toBe(true)
123
+ })
124
+ })
125
+
126
+ // ========================================================================
127
+ // Generate Embedding
128
+ // ========================================================================
129
+
130
+ describe('generateEmbedding', () => {
131
+ it('should generate embedding array from text', async () => {
132
+ await initManager(vm)
133
+ mockEmbedderFn.mockResolvedValue({ data: fakeEmbedding(42) })
134
+
135
+ const embedding = await vm.generateEmbedding('test text')
136
+ expect(embedding).toHaveLength(384)
137
+ expect(typeof embedding[0]).toBe('number')
138
+ })
139
+
140
+ it('should throw if not initialized', async () => {
141
+ await expect(vm.generateEmbedding('test')).rejects.toThrow('not initialized')
142
+ })
143
+ })
144
+
145
+ // ========================================================================
146
+ // Add Entry
147
+ // ========================================================================
148
+
149
+ describe('addEntry', () => {
150
+ it('should upsert entry (delete+insert)', async () => {
151
+ await initManager(vm)
152
+ mockEmbedderFn.mockResolvedValue({ data: fakeEmbedding(1) })
153
+ mockDeleteItem.mockResolvedValue(undefined)
154
+ mockInsertItem.mockResolvedValue(undefined)
155
+
156
+ const result = await vm.addEntry(42, 'Some content')
157
+ expect(result).toBe(true)
158
+
159
+ expect(mockDeleteItem).toHaveBeenCalledWith('42')
160
+ expect(mockInsertItem).toHaveBeenCalledWith(
161
+ expect.objectContaining({
162
+ id: '42',
163
+ metadata: expect.objectContaining({ entryId: 42 }),
164
+ })
165
+ )
166
+ })
167
+
168
+ it('should return false on error', async () => {
169
+ await initManager(vm)
170
+ mockEmbedderFn.mockRejectedValue(new Error('Embedding failed'))
171
+
172
+ const result = await vm.addEntry(99, 'Content')
173
+ expect(result).toBe(false)
174
+ })
175
+ })
176
+
177
+ // ========================================================================
178
+ // Search
179
+ // ========================================================================
180
+
181
+ describe('search', () => {
182
+ it('should return filtered results above threshold', async () => {
183
+ await initManager(vm)
184
+ mockEmbedderFn.mockResolvedValue({ data: fakeEmbedding(0) })
185
+ mockQueryItems.mockResolvedValue([
186
+ { score: 0.9, item: { metadata: { entryId: 1 } } },
187
+ { score: 0.5, item: { metadata: { entryId: 2 } } },
188
+ { score: 0.1, item: { metadata: { entryId: 3 } } },
189
+ ])
190
+
191
+ const results = await vm.search('query text', 10, 0.3)
192
+ expect(results).toHaveLength(2)
193
+ expect(results[0]!.entryId).toBe(1)
194
+ expect(results[0]!.score).toBe(0.9)
195
+ })
196
+
197
+ it('should limit results to limit param', async () => {
198
+ await initManager(vm)
199
+ mockEmbedderFn.mockResolvedValue({ data: fakeEmbedding(0) })
200
+ mockQueryItems.mockResolvedValue([
201
+ { score: 0.9, item: { metadata: { entryId: 1 } } },
202
+ { score: 0.8, item: { metadata: { entryId: 2 } } },
203
+ { score: 0.7, item: { metadata: { entryId: 3 } } },
204
+ ])
205
+
206
+ const results = await vm.search('query', 2, 0.3)
207
+ expect(results).toHaveLength(2)
208
+ })
209
+
210
+ it('should return empty on error', async () => {
211
+ await initManager(vm)
212
+ mockEmbedderFn.mockRejectedValue(new Error('Embed fail'))
213
+
214
+ const results = await vm.search('query')
215
+ expect(results).toEqual([])
216
+ })
217
+ })
218
+
219
+ // ========================================================================
220
+ // Remove Entry
221
+ // ========================================================================
222
+
223
+ describe('removeEntry', () => {
224
+ it('should delete entry from index', async () => {
225
+ await initManager(vm)
226
+ mockDeleteItem.mockResolvedValue(undefined)
227
+
228
+ const result = await vm.removeEntry(42)
229
+ expect(result).toBe(true)
230
+ expect(mockDeleteItem).toHaveBeenCalledWith('42')
231
+ })
232
+
233
+ it('should return false if index not available', async () => {
234
+ const result = await vm.removeEntry(42)
235
+ expect(result).toBe(false)
236
+ })
237
+
238
+ it('should return false on delete error', async () => {
239
+ await initManager(vm)
240
+ mockDeleteItem.mockRejectedValue(new Error('Not found'))
241
+
242
+ const result = await vm.removeEntry(999)
243
+ expect(result).toBe(false)
244
+ })
245
+ })
246
+
247
+ // ========================================================================
248
+ // Get Stats
249
+ // ========================================================================
250
+
251
+ describe('getStats', () => {
252
+ it('should return stats from index', async () => {
253
+ await initManager(vm)
254
+ mockGetIndexStats.mockResolvedValue({ items: 100 })
255
+
256
+ const stats = await vm.getStats()
257
+ expect(stats.itemCount).toBe(100)
258
+ expect(stats.modelName).toBe('Xenova/all-MiniLM-L6-v2')
259
+ expect(stats.dimensions).toBe(384)
260
+ })
261
+
262
+ it('should return zero count when no index', async () => {
263
+ const stats = await vm.getStats()
264
+ expect(stats.itemCount).toBe(0)
265
+ })
266
+
267
+ it('should return zero count on error', async () => {
268
+ await initManager(vm)
269
+ mockGetIndexStats.mockRejectedValue(new Error('Corrupted'))
270
+
271
+ const stats = await vm.getStats()
272
+ expect(stats.itemCount).toBe(0)
273
+ })
274
+ })
275
+
276
+ // ========================================================================
277
+ // Rebuild Index
278
+ // ========================================================================
279
+
280
+ describe('rebuildIndex', () => {
281
+ it('should rebuild from database entries', async () => {
282
+ await initManager(vm)
283
+ mockEmbedderFn.mockResolvedValue({ data: fakeEmbedding(0) })
284
+ mockListItems.mockResolvedValue([])
285
+ mockInsertItem.mockResolvedValue(undefined)
286
+
287
+ const mockDb = {
288
+ getActiveEntryCount: vi.fn().mockReturnValue(2),
289
+ getEntriesPage: vi.fn().mockReturnValue([
290
+ { id: 1, content: 'Entry one' },
291
+ { id: 2, content: 'Entry two' },
292
+ ]),
293
+ }
294
+
295
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
296
+ const indexed = await vm.rebuildIndex(mockDb as any)
297
+ expect(indexed).toBe(2)
298
+ expect(mockInsertItem).toHaveBeenCalledTimes(2)
299
+ })
300
+
301
+ it('should handle orphan cleanup', async () => {
302
+ await initManager(vm)
303
+ mockEmbedderFn.mockResolvedValue({ data: fakeEmbedding(0) })
304
+
305
+ mockListItems
306
+ .mockResolvedValueOnce([{ id: '999' }])
307
+ .mockResolvedValueOnce([])
308
+ .mockResolvedValueOnce([])
309
+
310
+ mockDeleteItem.mockResolvedValue(undefined)
311
+ mockInsertItem.mockResolvedValue(undefined)
312
+
313
+ const mockDb = {
314
+ getActiveEntryCount: vi.fn().mockReturnValue(1),
315
+ getEntriesPage: vi.fn().mockReturnValue([{ id: 1, content: 'Active entry' }]),
316
+ }
317
+
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ const indexed = await vm.rebuildIndex(mockDb as any)
320
+ expect(indexed).toBe(1)
321
+ expect(mockDeleteItem).toHaveBeenCalledWith('999')
322
+ })
323
+
324
+ it('should return 0 when index not available', async () => {
325
+ const mockDb = {
326
+ getActiveEntryCount: vi.fn().mockReturnValue(0),
327
+ getEntriesPage: vi.fn().mockReturnValue([]),
328
+ }
329
+
330
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
331
+ const indexed = await vm.rebuildIndex(mockDb as any)
332
+ expect(indexed).toBe(0)
333
+ })
334
+ })
335
+ })
@@ -0,0 +1,53 @@
1
+ import { bench, beforeAll, afterAll } from 'vitest'
2
+ import { VectorSearchManager } from '../../src/vector/VectorSearchManager.js'
3
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
4
+ import * as fs from 'node:fs'
5
+ import * as path from 'node:path'
6
+
7
+ let vm: VectorSearchManager
8
+ let db: SqliteAdapter
9
+ const testDbPath = path.join(process.cwd(), 'benchmark-vector.db')
10
+ const testVectorPath = path.join(process.cwd(), 'benchmark-vector-index')
11
+
12
+ beforeAll(async () => {
13
+ db = new SqliteAdapter(testDbPath)
14
+ await db.initialize()
15
+
16
+ vm = new VectorSearchManager(testVectorPath)
17
+
18
+ // Mock the embedding generation to skip transformer inference delay
19
+ vm.generateEmbedding = async () => new Float32Array(384).fill(Math.random())
20
+
21
+ await vm.initialize()
22
+
23
+ // Setup some data
24
+ for (let i = 0; i < 100; i++) {
25
+ const entry = db.createEntry({
26
+ content: `Vector benchmark entry ${i} content`,
27
+ })
28
+ await vm.addEntry(entry.id, entry.content)
29
+ }
30
+ })
31
+
32
+ afterAll(() => {
33
+ db.close()
34
+ try {
35
+ if (fs.existsSync(testDbPath)) {
36
+ fs.unlinkSync(testDbPath)
37
+ }
38
+ if (fs.existsSync(testVectorPath)) {
39
+ fs.rmSync(testVectorPath, { recursive: true, force: true })
40
+ }
41
+ } catch {
42
+ // ignore
43
+ }
44
+ })
45
+
46
+ bench('addEntry', async () => {
47
+ const id = Math.floor(Math.random() * 1000000)
48
+ await vm.addEntry(id, `New content for adding id ${id}`)
49
+ })
50
+
51
+ bench('search', async () => {
52
+ await vm.search('Vector benchmark entry 42', 10, 0.1)
53
+ })
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ exclude: ['src/cli.ts', 'src/index.ts'],
13
+ },
14
+ },
15
+ })