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,473 @@
1
+ /**
2
+ * GitHub Resource Handler Tests
3
+ *
4
+ * Tests GitHub-dependent resources using a mock GitHubIntegration object.
5
+ */
6
+
7
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
8
+ import { readResource } from '../../src/handlers/resources/index.js'
9
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
10
+ import type { GitHubIntegration } from '../../src/github/GitHubIntegration.js'
11
+
12
+ /**
13
+ * Creates a minimal mock GitHubIntegration with sensible defaults.
14
+ */
15
+ function createMockGitHub(overrides: Partial<Record<string, unknown>> = {}): GitHubIntegration {
16
+ const mock = {
17
+ isApiAvailable: vi.fn().mockReturnValue(true),
18
+ getRepoInfo: vi.fn().mockResolvedValue({
19
+ owner: 'testowner',
20
+ repo: 'testrepo',
21
+ branch: 'main',
22
+ remoteUrl: 'git@github.com:testowner/testrepo.git',
23
+ }),
24
+ getRepoContext: vi.fn().mockResolvedValue({
25
+ repoName: 'testrepo',
26
+ branch: 'main',
27
+ commit: 'abc1234',
28
+ remoteUrl: 'url',
29
+ projects: [],
30
+ issues: [],
31
+ pullRequests: [],
32
+ workflowRuns: [],
33
+ milestones: [],
34
+ }),
35
+ getIssues: vi
36
+ .fn()
37
+ .mockResolvedValue([
38
+ { number: 1, title: 'Bug fix needed', url: 'url1', state: 'OPEN', milestone: null },
39
+ ]),
40
+ getPullRequests: vi
41
+ .fn()
42
+ .mockResolvedValue([{ number: 10, title: 'Feature PR', url: 'url10', state: 'OPEN' }]),
43
+ getWorkflowRuns: vi.fn().mockResolvedValue([
44
+ {
45
+ id: 100,
46
+ name: 'CI',
47
+ status: 'completed',
48
+ conclusion: 'success',
49
+ url: 'url',
50
+ headBranch: 'main',
51
+ headSha: 'abc1234',
52
+ createdAt: '2025-01-01T00:00:00Z',
53
+ updatedAt: '2025-01-01T01:00:00Z',
54
+ },
55
+ ]),
56
+ getProjectKanban: vi.fn().mockResolvedValue(null),
57
+ getMilestones: vi.fn().mockResolvedValue([
58
+ {
59
+ number: 1,
60
+ title: 'v1.0',
61
+ description: 'First release',
62
+ state: 'open',
63
+ url: 'url',
64
+ dueOn: '2025-06-01T00:00:00Z',
65
+ openIssues: 5,
66
+ closedIssues: 10,
67
+ createdAt: '2025-01-01T00:00:00Z',
68
+ updatedAt: '2025-01-02T00:00:00Z',
69
+ creator: 'owner1',
70
+ },
71
+ ]),
72
+ getMilestone: vi.fn().mockResolvedValue({
73
+ number: 1,
74
+ title: 'v1.0',
75
+ description: 'First release',
76
+ state: 'open',
77
+ url: 'url',
78
+ dueOn: '2025-06-01T00:00:00Z',
79
+ openIssues: 5,
80
+ closedIssues: 10,
81
+ createdAt: '2025-01-01T00:00:00Z',
82
+ updatedAt: '2025-01-02T00:00:00Z',
83
+ creator: 'owner1',
84
+ }),
85
+ getCachedRepoInfo: vi.fn().mockReturnValue({
86
+ owner: 'testowner',
87
+ repo: 'testrepo',
88
+ branch: 'main',
89
+ remoteUrl: 'git@github.com:testowner/testrepo.git',
90
+ }),
91
+ getRepoStats: vi.fn().mockResolvedValue({
92
+ stars: 42,
93
+ forks: 7,
94
+ watchers: 3,
95
+ }),
96
+ getTrafficData: vi.fn().mockResolvedValue({
97
+ clones: { total: 120, uniqueCloners: 30 },
98
+ views: { total: 500, uniqueVisitors: 80 },
99
+ }),
100
+ ...overrides,
101
+ }
102
+ return mock as unknown as GitHubIntegration
103
+ }
104
+
105
+ describe('GitHub Resource Handlers', () => {
106
+ let db: SqliteAdapter
107
+ const testDbPath = './test-gh-resources.db'
108
+
109
+ beforeAll(async () => {
110
+ db = new SqliteAdapter(testDbPath)
111
+ await db.initialize()
112
+ })
113
+
114
+ afterAll(() => {
115
+ db.close()
116
+ try {
117
+ const fs = require('node:fs')
118
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
119
+ } catch {
120
+ // Ignore cleanup errors
121
+ }
122
+ })
123
+
124
+ // ========================================================================
125
+ // memory://github/status
126
+ // ========================================================================
127
+
128
+ describe('memory://github/status', () => {
129
+ it('should return status with repo info, CI, issues, PRs', async () => {
130
+ const github = createMockGitHub()
131
+ const result = await readResource(
132
+ 'memory://github/status',
133
+ db,
134
+ undefined,
135
+ undefined,
136
+ github
137
+ )
138
+
139
+ const data = result.data as {
140
+ repository: string
141
+ branch: string
142
+ ci: { status: string }
143
+ issues: { openCount: number }
144
+ pullRequests: { openCount: number }
145
+ }
146
+
147
+ expect(data.repository).toBe('testowner/testrepo')
148
+ expect(data.branch).toBe('main')
149
+ expect(data.ci.status).toBe('passing')
150
+ expect(data.issues.openCount).toBe(1)
151
+ expect(data.pullRequests.openCount).toBe(1)
152
+ })
153
+
154
+ it('should return error when no github integration', async () => {
155
+ const result = await readResource(
156
+ 'memory://github/status',
157
+ db,
158
+ undefined,
159
+ undefined,
160
+ null
161
+ )
162
+
163
+ const data = result.data as { error: string; hint: string }
164
+ expect(data.error).toContain('GitHub integration not available')
165
+ })
166
+
167
+ it('should handle missing owner/repo', async () => {
168
+ const github = createMockGitHub({
169
+ getRepoInfo: vi.fn().mockResolvedValue({
170
+ owner: null,
171
+ repo: null,
172
+ branch: 'main',
173
+ remoteUrl: null,
174
+ }),
175
+ })
176
+
177
+ const result = await readResource(
178
+ 'memory://github/status',
179
+ db,
180
+ undefined,
181
+ undefined,
182
+ github
183
+ )
184
+
185
+ const data = result.data as { error: string }
186
+ expect(data.error).toContain('Could not detect repository')
187
+ })
188
+ })
189
+
190
+ // ========================================================================
191
+ // memory://github/milestones
192
+ // ========================================================================
193
+
194
+ describe('memory://github/milestones', () => {
195
+ it('should return milestones with completion percentages', async () => {
196
+ const github = createMockGitHub()
197
+ const result = await readResource(
198
+ 'memory://github/milestones',
199
+ db,
200
+ undefined,
201
+ undefined,
202
+ github
203
+ )
204
+
205
+ const data = result.data as {
206
+ repository: string
207
+ milestones: { completionPercentage: number }[]
208
+ count: number
209
+ }
210
+
211
+ expect(data.repository).toBe('testowner/testrepo')
212
+ expect(data.count).toBe(1)
213
+ // 10 closed / (5 open + 10 closed) = 66.67% => 67%
214
+ expect(data.milestones[0]!.completionPercentage).toBe(67)
215
+ })
216
+
217
+ it('should return error when no github', async () => {
218
+ const result = await readResource(
219
+ 'memory://github/milestones',
220
+ db,
221
+ undefined,
222
+ undefined,
223
+ null
224
+ )
225
+
226
+ const data = result.data as { error: string }
227
+ expect(data.error).toContain('GitHub integration not available')
228
+ })
229
+ })
230
+
231
+ // ========================================================================
232
+ // memory://milestones/{number}
233
+ // ========================================================================
234
+
235
+ describe('memory://milestones/{number}', () => {
236
+ it('should return single milestone detail', async () => {
237
+ const github = createMockGitHub()
238
+ const result = await readResource(
239
+ 'memory://milestones/1',
240
+ db,
241
+ undefined,
242
+ undefined,
243
+ github
244
+ )
245
+
246
+ const data = result.data as {
247
+ repository: string
248
+ milestone: { number: number; completionPercentage: number }
249
+ }
250
+
251
+ expect(data.repository).toBe('testowner/testrepo')
252
+ expect(data.milestone.number).toBe(1)
253
+ expect(data.milestone.completionPercentage).toBe(67)
254
+ })
255
+
256
+ it('should return error for not found milestone', async () => {
257
+ const github = createMockGitHub({
258
+ getMilestone: vi.fn().mockResolvedValue(null),
259
+ })
260
+ const result = await readResource(
261
+ 'memory://milestones/999',
262
+ db,
263
+ undefined,
264
+ undefined,
265
+ github
266
+ )
267
+
268
+ const data = result.data as { error: string }
269
+ expect(data.error).toContain('not found')
270
+ })
271
+ })
272
+
273
+ // ========================================================================
274
+ // memory://kanban/{project_number}
275
+ // ========================================================================
276
+
277
+ describe('memory://kanban/{project_number}', () => {
278
+ it('should return kanban board when available', async () => {
279
+ const github = createMockGitHub({
280
+ getProjectKanban: vi.fn().mockResolvedValue({
281
+ projectId: 'PVT_1',
282
+ projectTitle: 'My Board',
283
+ columns: [
284
+ {
285
+ status: 'Todo',
286
+ items: [{ id: 'I1', type: 'ISSUE', title: 'Task', number: 1 }],
287
+ },
288
+ ],
289
+ statusOptions: [],
290
+ totalItems: 1,
291
+ }),
292
+ })
293
+
294
+ const result = await readResource('memory://kanban/1', db, undefined, undefined, github)
295
+
296
+ const data = result.data as { projectTitle: string; totalItems: number }
297
+ expect(data.projectTitle).toBe('My Board')
298
+ expect(data.totalItems).toBe(1)
299
+ })
300
+
301
+ it('should return error when project not found', async () => {
302
+ const github = createMockGitHub()
303
+ const result = await readResource(
304
+ 'memory://kanban/999',
305
+ db,
306
+ undefined,
307
+ undefined,
308
+ github
309
+ )
310
+
311
+ const data = result.data as { error: string }
312
+ expect(data.error).toContain('not found')
313
+ })
314
+
315
+ it('should return error when no github', async () => {
316
+ const result = await readResource('memory://kanban/1', db, undefined, undefined, null)
317
+
318
+ const data = result.data as { error: string }
319
+ expect(data.error).toContain('GitHub integration not available')
320
+ })
321
+ })
322
+
323
+ // ========================================================================
324
+ // memory://kanban/{project_number}/diagram
325
+ // ========================================================================
326
+
327
+ describe('memory://kanban/{project_number}/diagram', () => {
328
+ it('should return mermaid diagram', async () => {
329
+ const github = createMockGitHub({
330
+ getProjectKanban: vi.fn().mockResolvedValue({
331
+ projectId: 'PVT_1',
332
+ projectTitle: 'Board',
333
+ columns: [
334
+ {
335
+ status: 'Done',
336
+ items: [
337
+ {
338
+ id: 'PVTITEM_A1B2C3D4',
339
+ type: 'ISSUE',
340
+ title: 'Completed task',
341
+ number: 5,
342
+ },
343
+ ],
344
+ },
345
+ ],
346
+ statusOptions: [],
347
+ totalItems: 1,
348
+ }),
349
+ })
350
+
351
+ const result = await readResource(
352
+ 'memory://kanban/1/diagram',
353
+ db,
354
+ undefined,
355
+ undefined,
356
+ github
357
+ )
358
+
359
+ const data = result.data as {
360
+ format: string
361
+ diagram: string
362
+ projectNumber: number
363
+ totalItems: number
364
+ }
365
+
366
+ expect(data.format).toBe('mermaid')
367
+ expect(data.diagram).toContain('graph LR')
368
+ expect(data.diagram).toContain('Done')
369
+ expect(data.totalItems).toBe(1)
370
+ })
371
+
372
+ it('should show fallback when no github', async () => {
373
+ const result = await readResource(
374
+ 'memory://kanban/1/diagram',
375
+ db,
376
+ undefined,
377
+ undefined,
378
+ null
379
+ )
380
+
381
+ const data = result.data as { format: string; diagram: string }
382
+ expect(data.format).toBe('mermaid')
383
+ expect(data.diagram).toContain('NoGitHub')
384
+ })
385
+ })
386
+
387
+ // ========================================================================
388
+ // memory://briefing with GitHub insights
389
+ // ========================================================================
390
+
391
+ describe('memory://briefing with GitHub', () => {
392
+ it('should include insights with stars, forks, and traffic', async () => {
393
+ const github = createMockGitHub()
394
+ const result = await readResource('memory://briefing', db, undefined, undefined, github)
395
+
396
+ const data = result.data as {
397
+ github: {
398
+ repo: string
399
+ insights?: {
400
+ stars: number | null
401
+ forks: number | null
402
+ clones14d?: number
403
+ views14d?: number
404
+ }
405
+ }
406
+ userMessage: string
407
+ }
408
+
409
+ expect(data.github.repo).toBe('testowner/testrepo')
410
+ expect(data.github.insights).toBeDefined()
411
+ expect(data.github.insights!.stars).toBe(42)
412
+ expect(data.github.insights!.forks).toBe(7)
413
+ expect(data.github.insights!.clones14d).toBe(120)
414
+ expect(data.github.insights!.views14d).toBe(500)
415
+ expect(data.userMessage).toContain('stars')
416
+ expect(data.userMessage).toContain('forks')
417
+ expect(data.userMessage).toContain('clones')
418
+ })
419
+
420
+ it('should include insights without traffic when getTrafficData fails', async () => {
421
+ const github = createMockGitHub({
422
+ getTrafficData: vi.fn().mockRejectedValue(new Error('403 Forbidden')),
423
+ })
424
+ const result = await readResource('memory://briefing', db, undefined, undefined, github)
425
+
426
+ const data = result.data as {
427
+ github: {
428
+ insights?: {
429
+ stars: number | null
430
+ forks: number | null
431
+ clones14d?: number
432
+ views14d?: number
433
+ }
434
+ }
435
+ userMessage: string
436
+ }
437
+
438
+ // Stars and forks should still be present
439
+ expect(data.github.insights).toBeDefined()
440
+ expect(data.github.insights!.stars).toBe(42)
441
+ expect(data.github.insights!.forks).toBe(7)
442
+ // Traffic should be absent
443
+ expect(data.github.insights!.clones14d).toBeUndefined()
444
+ expect(data.github.insights!.views14d).toBeUndefined()
445
+ })
446
+
447
+ it('should omit insights when getRepoStats fails', async () => {
448
+ const github = createMockGitHub({
449
+ getRepoStats: vi.fn().mockRejectedValue(new Error('API error')),
450
+ })
451
+ const result = await readResource('memory://briefing', db, undefined, undefined, github)
452
+
453
+ const data = result.data as {
454
+ github: {
455
+ insights?: unknown
456
+ }
457
+ }
458
+
459
+ expect(data.github.insights).toBeUndefined()
460
+ })
461
+
462
+ it('should include repoInsights in more section', async () => {
463
+ const github = createMockGitHub()
464
+ const result = await readResource('memory://briefing', db, undefined, undefined, github)
465
+
466
+ const data = result.data as {
467
+ more: { repoInsights: string }
468
+ }
469
+
470
+ expect(data.more.repoInsights).toBe('memory://github/insights')
471
+ })
472
+ })
473
+ })