memory-journal-mcp 4.4.2 → 5.0.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 (291) hide show
  1. package/.github/workflows/codeql.yml +1 -6
  2. package/.github/workflows/docker-publish.yml +15 -49
  3. package/.github/workflows/lint-and-test.yml +1 -1
  4. package/.github/workflows/secrets-scanning.yml +4 -3
  5. package/.github/workflows/security-update.yml +3 -3
  6. package/CHANGELOG.md +213 -0
  7. package/CONTRIBUTING.md +132 -97
  8. package/DOCKER_README.md +184 -235
  9. package/Dockerfile +27 -24
  10. package/README.md +218 -190
  11. package/SECURITY.md +27 -35
  12. package/dist/cli.js +16 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/constants/ServerInstructions.d.ts +5 -1
  15. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  16. package/dist/constants/ServerInstructions.js +133 -73
  17. package/dist/constants/ServerInstructions.js.map +1 -1
  18. package/dist/constants/icons.d.ts +2 -2
  19. package/dist/constants/icons.d.ts.map +1 -1
  20. package/dist/constants/icons.js +7 -6
  21. package/dist/constants/icons.js.map +1 -1
  22. package/dist/database/SqliteAdapter.d.ts +37 -24
  23. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  24. package/dist/database/SqliteAdapter.js +319 -157
  25. package/dist/database/SqliteAdapter.js.map +1 -1
  26. package/dist/database/schema.d.ts +45 -0
  27. package/dist/database/schema.d.ts.map +1 -0
  28. package/dist/database/schema.js +92 -0
  29. package/dist/database/schema.js.map +1 -0
  30. package/dist/filtering/ToolFilter.d.ts +1 -1
  31. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  32. package/dist/filtering/ToolFilter.js +13 -2
  33. package/dist/filtering/ToolFilter.js.map +1 -1
  34. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  35. package/dist/github/GitHubIntegration.js +1 -3
  36. package/dist/github/GitHubIntegration.js.map +1 -1
  37. package/dist/handlers/prompts/github.d.ts +12 -0
  38. package/dist/handlers/prompts/github.d.ts.map +1 -0
  39. package/dist/handlers/prompts/github.js +178 -0
  40. package/dist/handlers/prompts/github.js.map +1 -0
  41. package/dist/handlers/prompts/index.d.ts +23 -2
  42. package/dist/handlers/prompts/index.d.ts.map +1 -1
  43. package/dist/handlers/prompts/index.js +7 -432
  44. package/dist/handlers/prompts/index.js.map +1 -1
  45. package/dist/handlers/prompts/workflow.d.ts +12 -0
  46. package/dist/handlers/prompts/workflow.d.ts.map +1 -0
  47. package/dist/handlers/prompts/workflow.js +277 -0
  48. package/dist/handlers/prompts/workflow.js.map +1 -0
  49. package/dist/handlers/resources/core.d.ts +11 -0
  50. package/dist/handlers/resources/core.d.ts.map +1 -0
  51. package/dist/handlers/resources/core.js +433 -0
  52. package/dist/handlers/resources/core.js.map +1 -0
  53. package/dist/handlers/resources/github.d.ts +11 -0
  54. package/dist/handlers/resources/github.d.ts.map +1 -0
  55. package/dist/handlers/resources/github.js +314 -0
  56. package/dist/handlers/resources/github.js.map +1 -0
  57. package/dist/handlers/resources/graph.d.ts +11 -0
  58. package/dist/handlers/resources/graph.d.ts.map +1 -0
  59. package/dist/handlers/resources/graph.js +204 -0
  60. package/dist/handlers/resources/graph.js.map +1 -0
  61. package/dist/handlers/resources/index.d.ts +5 -20
  62. package/dist/handlers/resources/index.d.ts.map +1 -1
  63. package/dist/handlers/resources/index.js +16 -1278
  64. package/dist/handlers/resources/index.js.map +1 -1
  65. package/dist/handlers/resources/shared.d.ts +60 -0
  66. package/dist/handlers/resources/shared.d.ts.map +1 -0
  67. package/dist/handlers/resources/shared.js +49 -0
  68. package/dist/handlers/resources/shared.js.map +1 -0
  69. package/dist/handlers/resources/team.d.ts +13 -0
  70. package/dist/handlers/resources/team.d.ts.map +1 -0
  71. package/dist/handlers/resources/team.js +119 -0
  72. package/dist/handlers/resources/team.js.map +1 -0
  73. package/dist/handlers/resources/templates.d.ts +13 -0
  74. package/dist/handlers/resources/templates.d.ts.map +1 -0
  75. package/dist/handlers/resources/templates.js +310 -0
  76. package/dist/handlers/resources/templates.js.map +1 -0
  77. package/dist/handlers/tools/admin.d.ts +8 -0
  78. package/dist/handlers/tools/admin.d.ts.map +1 -0
  79. package/dist/handlers/tools/admin.js +270 -0
  80. package/dist/handlers/tools/admin.js.map +1 -0
  81. package/dist/handlers/tools/analytics.d.ts +8 -0
  82. package/dist/handlers/tools/analytics.d.ts.map +1 -0
  83. package/dist/handlers/tools/analytics.js +256 -0
  84. package/dist/handlers/tools/analytics.js.map +1 -0
  85. package/dist/handlers/tools/backup.d.ts +8 -0
  86. package/dist/handlers/tools/backup.d.ts.map +1 -0
  87. package/dist/handlers/tools/backup.js +224 -0
  88. package/dist/handlers/tools/backup.js.map +1 -0
  89. package/dist/handlers/tools/core.d.ts +9 -0
  90. package/dist/handlers/tools/core.d.ts.map +1 -0
  91. package/dist/handlers/tools/core.js +326 -0
  92. package/dist/handlers/tools/core.js.map +1 -0
  93. package/dist/handlers/tools/export.d.ts +8 -0
  94. package/dist/handlers/tools/export.d.ts.map +1 -0
  95. package/dist/handlers/tools/export.js +89 -0
  96. package/dist/handlers/tools/export.js.map +1 -0
  97. package/dist/handlers/tools/github/helpers.d.ts +34 -0
  98. package/dist/handlers/tools/github/helpers.d.ts.map +1 -0
  99. package/dist/handlers/tools/github/helpers.js +52 -0
  100. package/dist/handlers/tools/github/helpers.js.map +1 -0
  101. package/dist/handlers/tools/github/insights-tools.d.ts +8 -0
  102. package/dist/handlers/tools/github/insights-tools.d.ts.map +1 -0
  103. package/dist/handlers/tools/github/insights-tools.js +104 -0
  104. package/dist/handlers/tools/github/insights-tools.js.map +1 -0
  105. package/dist/handlers/tools/github/issue-tools.d.ts +8 -0
  106. package/dist/handlers/tools/github/issue-tools.d.ts.map +1 -0
  107. package/dist/handlers/tools/github/issue-tools.js +359 -0
  108. package/dist/handlers/tools/github/issue-tools.js.map +1 -0
  109. package/dist/handlers/tools/github/kanban-tools.d.ts +8 -0
  110. package/dist/handlers/tools/github/kanban-tools.d.ts.map +1 -0
  111. package/dist/handlers/tools/github/kanban-tools.js +108 -0
  112. package/dist/handlers/tools/github/kanban-tools.js.map +1 -0
  113. package/dist/handlers/tools/github/milestone-tools.d.ts +9 -0
  114. package/dist/handlers/tools/github/milestone-tools.d.ts.map +1 -0
  115. package/dist/handlers/tools/github/milestone-tools.js +302 -0
  116. package/dist/handlers/tools/github/milestone-tools.js.map +1 -0
  117. package/dist/handlers/tools/github/mutation-tools.d.ts +12 -0
  118. package/dist/handlers/tools/github/mutation-tools.d.ts.map +1 -0
  119. package/dist/handlers/tools/github/mutation-tools.js +15 -0
  120. package/dist/handlers/tools/github/mutation-tools.js.map +1 -0
  121. package/dist/handlers/tools/github/read-tools.d.ts +8 -0
  122. package/dist/handlers/tools/github/read-tools.d.ts.map +1 -0
  123. package/dist/handlers/tools/github/read-tools.js +260 -0
  124. package/dist/handlers/tools/github/read-tools.js.map +1 -0
  125. package/dist/handlers/tools/github/schemas.d.ts +467 -0
  126. package/dist/handlers/tools/github/schemas.d.ts.map +1 -0
  127. package/dist/handlers/tools/github/schemas.js +335 -0
  128. package/dist/handlers/tools/github/schemas.js.map +1 -0
  129. package/dist/handlers/tools/github.d.ts +14 -0
  130. package/dist/handlers/tools/github.d.ts.map +1 -0
  131. package/dist/handlers/tools/github.js +28 -0
  132. package/dist/handlers/tools/github.js.map +1 -0
  133. package/dist/handlers/tools/index.d.ts +15 -20
  134. package/dist/handlers/tools/index.d.ts.map +1 -1
  135. package/dist/handlers/tools/index.js +117 -2909
  136. package/dist/handlers/tools/index.js.map +1 -1
  137. package/dist/handlers/tools/relationships.d.ts +8 -0
  138. package/dist/handlers/tools/relationships.d.ts.map +1 -0
  139. package/dist/handlers/tools/relationships.js +308 -0
  140. package/dist/handlers/tools/relationships.js.map +1 -0
  141. package/dist/handlers/tools/schemas.d.ts +108 -0
  142. package/dist/handlers/tools/schemas.d.ts.map +1 -0
  143. package/dist/handlers/tools/schemas.js +122 -0
  144. package/dist/handlers/tools/schemas.js.map +1 -0
  145. package/dist/handlers/tools/search.d.ts +8 -0
  146. package/dist/handlers/tools/search.d.ts.map +1 -0
  147. package/dist/handlers/tools/search.js +282 -0
  148. package/dist/handlers/tools/search.js.map +1 -0
  149. package/dist/handlers/tools/team.d.ts +11 -0
  150. package/dist/handlers/tools/team.d.ts.map +1 -0
  151. package/dist/handlers/tools/team.js +239 -0
  152. package/dist/handlers/tools/team.js.map +1 -0
  153. package/dist/server/McpServer.d.ts +4 -0
  154. package/dist/server/McpServer.d.ts.map +1 -1
  155. package/dist/server/McpServer.js +48 -297
  156. package/dist/server/McpServer.js.map +1 -1
  157. package/dist/server/Scheduler.d.ts +91 -0
  158. package/dist/server/Scheduler.d.ts.map +1 -0
  159. package/dist/server/Scheduler.js +201 -0
  160. package/dist/server/Scheduler.js.map +1 -0
  161. package/dist/transports/http.d.ts +66 -0
  162. package/dist/transports/http.d.ts.map +1 -0
  163. package/dist/transports/http.js +519 -0
  164. package/dist/transports/http.js.map +1 -0
  165. package/dist/types/entities.d.ts +101 -0
  166. package/dist/types/entities.d.ts.map +1 -0
  167. package/dist/types/entities.js +5 -0
  168. package/dist/types/entities.js.map +1 -0
  169. package/dist/types/filtering.d.ts +34 -0
  170. package/dist/types/filtering.d.ts.map +1 -0
  171. package/dist/types/filtering.js +5 -0
  172. package/dist/types/filtering.js.map +1 -0
  173. package/dist/types/github.d.ts +166 -0
  174. package/dist/types/github.d.ts.map +1 -0
  175. package/dist/types/github.js +5 -0
  176. package/dist/types/github.js.map +1 -0
  177. package/dist/types/index.d.ts +35 -292
  178. package/dist/types/index.d.ts.map +1 -1
  179. package/dist/types/index.js +2 -2
  180. package/dist/types/index.js.map +1 -1
  181. package/dist/utils/error-helpers.d.ts +37 -0
  182. package/dist/utils/error-helpers.d.ts.map +1 -0
  183. package/dist/utils/error-helpers.js +47 -0
  184. package/dist/utils/error-helpers.js.map +1 -0
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +6 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/dist/utils/security-utils.d.ts +0 -21
  189. package/dist/utils/security-utils.d.ts.map +1 -1
  190. package/dist/utils/security-utils.js +0 -47
  191. package/dist/utils/security-utils.js.map +1 -1
  192. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  193. package/dist/vector/VectorSearchManager.js +9 -32
  194. package/dist/vector/VectorSearchManager.js.map +1 -1
  195. package/docker-compose.yml +11 -2
  196. package/hooks/README.md +107 -0
  197. package/hooks/cursor/hooks.json +10 -0
  198. package/hooks/cursor/memory-journal.mdc +22 -0
  199. package/hooks/cursor/session-end.sh +19 -0
  200. package/hooks/kilo-code/session-end-mode.json +11 -0
  201. package/hooks/kiro/session-end.md +13 -0
  202. package/mcp-config-example.json +1 -0
  203. package/package.json +11 -9
  204. package/playwright.config.ts +29 -0
  205. package/releases/v4.5.0.md +116 -0
  206. package/releases/v5.0.0.md +105 -0
  207. package/scripts/generate-server-instructions.ts +176 -0
  208. package/scripts/server-instructions-function-body.ts +77 -0
  209. package/server.json +3 -3
  210. package/src/cli.ts +45 -1
  211. package/src/constants/ServerInstructions.ts +133 -73
  212. package/src/constants/icons.ts +8 -7
  213. package/src/constants/server-instructions.md +268 -0
  214. package/src/database/SqliteAdapter.ts +358 -192
  215. package/src/database/schema.ts +125 -0
  216. package/src/filtering/ToolFilter.ts +13 -2
  217. package/src/github/GitHubIntegration.ts +1 -3
  218. package/src/handlers/prompts/github.ts +209 -0
  219. package/src/handlers/prompts/index.ts +10 -499
  220. package/src/handlers/prompts/workflow.ts +314 -0
  221. package/src/handlers/resources/core.ts +528 -0
  222. package/src/handlers/resources/github.ts +358 -0
  223. package/src/handlers/resources/graph.ts +254 -0
  224. package/src/handlers/resources/index.ts +23 -1570
  225. package/src/handlers/resources/shared.ts +103 -0
  226. package/src/handlers/resources/team.ts +133 -0
  227. package/src/handlers/resources/templates.ts +374 -0
  228. package/src/handlers/tools/admin.ts +285 -0
  229. package/src/handlers/tools/analytics.ts +301 -0
  230. package/src/handlers/tools/backup.ts +242 -0
  231. package/src/handlers/tools/core.ts +350 -0
  232. package/src/handlers/tools/export.ts +115 -0
  233. package/src/handlers/tools/github/helpers.ts +86 -0
  234. package/src/handlers/tools/github/insights-tools.ts +119 -0
  235. package/src/handlers/tools/github/issue-tools.ts +439 -0
  236. package/src/handlers/tools/github/kanban-tools.ts +134 -0
  237. package/src/handlers/tools/github/milestone-tools.ts +392 -0
  238. package/src/handlers/tools/github/mutation-tools.ts +17 -0
  239. package/src/handlers/tools/github/read-tools.ts +328 -0
  240. package/src/handlers/tools/github/schemas.ts +369 -0
  241. package/src/handlers/tools/github.ts +36 -0
  242. package/src/handlers/tools/index.ts +144 -3325
  243. package/src/handlers/tools/relationships.ts +358 -0
  244. package/src/handlers/tools/schemas.ts +132 -0
  245. package/src/handlers/tools/search.ts +343 -0
  246. package/src/handlers/tools/team.ts +273 -0
  247. package/src/server/McpServer.ts +63 -358
  248. package/src/server/Scheduler.ts +278 -0
  249. package/src/transports/http.ts +635 -0
  250. package/src/types/entities.ts +145 -0
  251. package/src/types/filtering.ts +54 -0
  252. package/src/types/github.ts +180 -0
  253. package/src/types/index.ts +67 -375
  254. package/src/utils/error-helpers.ts +52 -0
  255. package/src/utils/logger.ts +6 -3
  256. package/src/utils/security-utils.ts +0 -52
  257. package/src/vector/VectorSearchManager.ts +9 -33
  258. package/tests/constants/icons.test.ts +1 -2
  259. package/tests/constants/server-instructions.test.ts +30 -4
  260. package/tests/database/sqlite-adapter.test.ts +91 -7
  261. package/tests/e2e/auth.spec.ts +154 -0
  262. package/tests/e2e/health.spec.ts +63 -0
  263. package/tests/e2e/protocols.spec.ts +134 -0
  264. package/tests/e2e/resources.spec.ts +103 -0
  265. package/tests/e2e/scheduler.spec.ts +79 -0
  266. package/tests/e2e/security.spec.ts +91 -0
  267. package/tests/e2e/sessions.spec.ts +95 -0
  268. package/tests/e2e/stateless.spec.ts +121 -0
  269. package/tests/e2e/tools.spec.ts +111 -0
  270. package/tests/filtering/tool-filter.test.ts +46 -0
  271. package/tests/handlers/error-path-coverage.test.ts +324 -0
  272. package/tests/handlers/github-resource-handlers.test.ts +453 -0
  273. package/tests/handlers/github-tool-handlers.test.ts +899 -0
  274. package/tests/handlers/prompt-handler-coverage.test.ts +106 -0
  275. package/tests/handlers/prompt-handlers.test.ts +40 -0
  276. package/tests/handlers/resource-handler-coverage.test.ts +181 -0
  277. package/tests/handlers/resource-handlers.test.ts +33 -9
  278. package/tests/handlers/search-tool-handlers.test.ts +272 -0
  279. package/tests/handlers/targeted-gap-closure.test.ts +387 -0
  280. package/tests/handlers/team-resource-handlers.test.ts +156 -0
  281. package/tests/handlers/team-tool-handlers.test.ts +301 -0
  282. package/tests/handlers/tool-handler-coverage.test.ts +469 -0
  283. package/tests/handlers/tool-handlers.test.ts +2 -2
  284. package/tests/security/sql-injection.test.ts +3 -54
  285. package/tests/server/mcp-server.test.ts +503 -8
  286. package/tests/server/scheduler.test.ts +400 -0
  287. package/tests/transports/http-transport.test.ts +620 -0
  288. package/tests/vector/vector-search-manager.test.ts +60 -0
  289. package/vitest.config.ts +4 -1
  290. package/.memory-journal-team.db +0 -0
  291. package/.vscode/settings.json +0 -84
@@ -99,6 +99,32 @@ describe('generateInstructions', () => {
99
99
  )
100
100
  expect(result).not.toContain('## GitHub Integration')
101
101
  })
102
+
103
+ it('should include Session End section', () => {
104
+ const result = generateInstructions(
105
+ TEST_TOOLS,
106
+ TEST_RESOURCES,
107
+ TEST_PROMPTS,
108
+ undefined,
109
+ 'essential'
110
+ )
111
+ expect(result).toContain('Session End')
112
+ expect(result).toContain('session-summary')
113
+ expect(result).toContain('retrospective')
114
+ expect(result).toContain('opt-out')
115
+ })
116
+
117
+ it('should include Rule & Skill Suggestions section', () => {
118
+ const result = generateInstructions(
119
+ TEST_TOOLS,
120
+ TEST_RESOURCES,
121
+ TEST_PROMPTS,
122
+ undefined,
123
+ 'essential'
124
+ )
125
+ expect(result).toContain('Rule & Skill Suggestions')
126
+ expect(result).toContain('always ask the user first')
127
+ })
102
128
  })
103
129
 
104
130
  describe('standard level', () => {
@@ -466,7 +492,7 @@ describe('generateInstructions', () => {
466
492
  'memory://graph/recent',
467
493
  'memory://graph/actions',
468
494
  'memory://actions/recent',
469
- 'memory://team/recent',
495
+
470
496
  'memory://github/status',
471
497
  'memory://github/milestones',
472
498
  'memory://github/insights',
@@ -482,9 +508,9 @@ describe('generateInstructions', () => {
482
508
  })
483
509
 
484
510
  describe('tool count consistency', () => {
485
- it('should have 39 tools across all groups', () => {
511
+ it('should have 42 tools across all groups', () => {
486
512
  const allToolNames = getAllToolNames()
487
- expect(allToolNames.length).toBe(39)
513
+ expect(allToolNames.length).toBe(42)
488
514
  })
489
515
 
490
516
  it('should show correct active tool count for all tools', () => {
@@ -492,7 +518,7 @@ describe('generateInstructions', () => {
492
518
  expect(result).toContain(`Active Tools (${String(ALL_TOOLS.size)})`)
493
519
  })
494
520
 
495
- it('should list all 8 tool groups in active tools', () => {
521
+ it('should list all 9 tool groups in active tools', () => {
496
522
  const result = fullInstructions()
497
523
  const groups = Object.keys(TOOL_GROUPS)
498
524
  for (const group of groups) {
@@ -64,7 +64,7 @@ describe('SqliteAdapter', () => {
64
64
  it('should create an entry with all fields', () => {
65
65
  const entry = db.createEntry({
66
66
  content: 'Full entry',
67
- entryType: 'decision',
67
+ entryType: 'project_decision',
68
68
  tags: ['tag-a', 'tag-b'],
69
69
  isPersonal: false,
70
70
  significanceType: 'milestone',
@@ -73,7 +73,7 @@ describe('SqliteAdapter', () => {
73
73
  issueNumber: 7,
74
74
  })
75
75
 
76
- expect(entry.entryType).toBe('decision')
76
+ expect(entry.entryType).toBe('project_decision')
77
77
  expect(entry.isPersonal).toBe(false)
78
78
  expect(entry.tags).toContain('tag-a')
79
79
  expect(entry.tags).toContain('tag-b')
@@ -187,13 +187,13 @@ describe('SqliteAdapter', () => {
187
187
 
188
188
  it('should filter by isPersonal', () => {
189
189
  db.createEntry({ content: 'Personal entry', isPersonal: true })
190
- db.createEntry({ content: 'Team entry', isPersonal: false })
190
+ db.createEntry({ content: 'Non-personal entry', isPersonal: false })
191
191
 
192
192
  const personal = db.getRecentEntries(100, true)
193
- const team = db.getRecentEntries(100, false)
193
+ const nonPersonal = db.getRecentEntries(100, false)
194
194
 
195
195
  expect(personal.every((e) => e.isPersonal)).toBe(true)
196
- expect(team.every((e) => !e.isPersonal)).toBe(true)
196
+ expect(nonPersonal.every((e) => !e.isPersonal)).toBe(true)
197
197
  })
198
198
  })
199
199
 
@@ -471,6 +471,65 @@ describe('SqliteAdapter', () => {
471
471
  fs.unlinkSync(backup.path)
472
472
  }
473
473
  })
474
+
475
+ it('should delete old backups keeping only keepCount', () => {
476
+ const fs = require('node:fs')
477
+
478
+ // Clean up any pre-existing backups from other tests
479
+ const preExisting = db.listBackups()
480
+ for (const backup of preExisting) {
481
+ if (fs.existsSync(backup.path)) fs.unlinkSync(backup.path)
482
+ }
483
+
484
+ // Create 3 backups
485
+ const b1 = db.exportToFile('cleanup-1')
486
+ const b2 = db.exportToFile('cleanup-2')
487
+ const b3 = db.exportToFile('cleanup-3')
488
+
489
+ // Keep only 1 newest
490
+ db.deleteOldBackups(1)
491
+
492
+ const remaining = db.listBackups()
493
+ // Should have exactly 1 backup remaining (newest)
494
+ expect(remaining.length).toBe(1)
495
+
496
+ // Cleanup any remaining
497
+ for (const path of [b1.path, b2.path, b3.path]) {
498
+ if (fs.existsSync(path)) fs.unlinkSync(path)
499
+ }
500
+ })
501
+
502
+ it('should restore from a backup file', async () => {
503
+ const fs = require('node:fs')
504
+ // Create an entry and backup
505
+ db.createEntry({ content: 'Before restore test' })
506
+ const countBefore = db.getActiveEntryCount()
507
+ const backup = db.exportToFile('restore-test')
508
+
509
+ // Create more entries after backup
510
+ db.createEntry({ content: 'After backup 1' })
511
+ db.createEntry({ content: 'After backup 2' })
512
+ const countAfterAdding = db.getActiveEntryCount()
513
+ expect(countAfterAdding).toBeGreaterThan(countBefore)
514
+
515
+ // Restore should revert to backup state
516
+ const result = await db.restoreFromFile(backup.filename)
517
+ expect(result.previousEntryCount).toBe(countAfterAdding)
518
+ expect(result.newEntryCount).toBe(countBefore)
519
+
520
+ // Cleanup
521
+ const backups = db.listBackups()
522
+ for (const b of backups) {
523
+ const path = require('node:path').join('backups', b.filename)
524
+ if (fs.existsSync(path)) fs.unlinkSync(path)
525
+ }
526
+ })
527
+
528
+ it('should get raw database handle', () => {
529
+ const rawDb = db.getRawDb()
530
+ expect(rawDb).toBeDefined()
531
+ expect(typeof rawDb.exec).toBe('function')
532
+ })
474
533
  })
475
534
 
476
535
  // ========================================================================
@@ -536,10 +595,10 @@ describe('SqliteAdapter', () => {
536
595
  it('should filter by entry type', () => {
537
596
  const today = new Date().toISOString().split('T')[0]!
538
597
  const results = db.searchByDateRange(today, today, {
539
- entryType: 'decision',
598
+ entryType: 'project_decision',
540
599
  })
541
600
  for (const r of results) {
542
- expect(r.entryType).toBe('decision')
601
+ expect(r.entryType).toBe('project_decision')
543
602
  }
544
603
  })
545
604
 
@@ -552,4 +611,29 @@ describe('SqliteAdapter', () => {
552
611
  expect(results.length).toBeGreaterThan(0)
553
612
  })
554
613
  })
614
+
615
+ // ========================================================================
616
+ // Backup edge cases
617
+ // ========================================================================
618
+
619
+ describe('backup edge cases', () => {
620
+ it('should return empty array when backups directory does not exist', () => {
621
+ // Use a fresh adapter with no backups dir created
622
+ const tempDb = new SqliteAdapter('./test-no-backups.db')
623
+ tempDb.initialize()
624
+ const backups = tempDb.listBackups()
625
+ expect(backups).toEqual([])
626
+ tempDb.close()
627
+ })
628
+
629
+ it('should throw when deleteOldBackups keepCount is less than 1', () => {
630
+ expect(() => db.deleteOldBackups(0)).toThrow('keepCount must be at least 1')
631
+ })
632
+
633
+ it('should throw when restoring from non-existent backup file', async () => {
634
+ await expect(db.restoreFromFile('nonexistent-backup.db')).rejects.toThrow(
635
+ 'Backup file not found'
636
+ )
637
+ })
638
+ })
555
639
  })
@@ -0,0 +1,154 @@
1
+ /**
2
+ * E2E Tests: Bearer Token Authentication
3
+ *
4
+ * Tests the --auth-token middleware. Uses a test-local server
5
+ * on port 3101 to avoid conflicting with the main webServer.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+ import { type ChildProcess, spawn } from 'node:child_process'
10
+ import { setTimeout as delay } from 'node:timers/promises'
11
+
12
+ const AUTH_TOKEN = 'test-secret-token-e2e'
13
+ const AUTH_PORT = 3101
14
+ const AUTH_BASE = `http://localhost:${AUTH_PORT}`
15
+
16
+ let serverProcess: ChildProcess | null = null
17
+
18
+ /**
19
+ * Start a second MCP server with --auth-token on a different port.
20
+ * Waits for /health to become reachable before returning.
21
+ */
22
+ async function startAuthServer(): Promise<void> {
23
+ serverProcess = spawn(
24
+ 'node',
25
+ [
26
+ 'dist/cli.js',
27
+ '--transport',
28
+ 'http',
29
+ '--port',
30
+ String(AUTH_PORT),
31
+ '--db',
32
+ './test-e2e-auth.db',
33
+ '--auth-token',
34
+ AUTH_TOKEN,
35
+ ],
36
+ {
37
+ cwd: process.cwd(),
38
+ stdio: 'pipe',
39
+ }
40
+ )
41
+
42
+ // Wait for server to be ready (poll /health)
43
+ const maxAttempts = 30
44
+ for (let i = 0; i < maxAttempts; i++) {
45
+ try {
46
+ const res = await fetch(`${AUTH_BASE}/health`)
47
+ if (res.ok) return
48
+ } catch {
49
+ // Server not ready yet
50
+ }
51
+ await delay(500)
52
+ }
53
+ throw new Error('Auth server did not start within timeout')
54
+ }
55
+
56
+ function stopAuthServer(): void {
57
+ if (serverProcess) {
58
+ serverProcess.kill('SIGTERM')
59
+ serverProcess = null
60
+ }
61
+ }
62
+
63
+ test.describe('Bearer Token Authentication', () => {
64
+ test.beforeAll(async () => {
65
+ await startAuthServer()
66
+ })
67
+
68
+ test.afterAll(() => {
69
+ stopAuthServer()
70
+ })
71
+
72
+ test('/health should be accessible without token (exempt)', async () => {
73
+ const response = await fetch(`${AUTH_BASE}/health`)
74
+ expect(response.status).toBe(200)
75
+
76
+ const body = await response.json()
77
+ expect(body).toHaveProperty('status', 'healthy')
78
+ })
79
+
80
+ test('POST /mcp should return 401 without Authorization header', async () => {
81
+ const response = await fetch(`${AUTH_BASE}/mcp`, {
82
+ method: 'POST',
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ Accept: 'application/json, text/event-stream',
86
+ },
87
+ body: JSON.stringify({
88
+ jsonrpc: '2.0',
89
+ id: 1,
90
+ method: 'initialize',
91
+ params: {
92
+ protocolVersion: '2025-03-26',
93
+ capabilities: {},
94
+ clientInfo: { name: 'test', version: '1.0' },
95
+ },
96
+ }),
97
+ })
98
+
99
+ expect(response.status).toBe(401)
100
+ const body = await response.json()
101
+ expect(body).toHaveProperty('error', 'Unauthorized')
102
+ })
103
+
104
+ test('POST /mcp should return 401 with wrong token', async () => {
105
+ const response = await fetch(`${AUTH_BASE}/mcp`, {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ Accept: 'application/json, text/event-stream',
110
+ Authorization: 'Bearer wrong-token',
111
+ },
112
+ body: JSON.stringify({
113
+ jsonrpc: '2.0',
114
+ id: 1,
115
+ method: 'initialize',
116
+ params: {
117
+ protocolVersion: '2025-03-26',
118
+ capabilities: {},
119
+ clientInfo: { name: 'test', version: '1.0' },
120
+ },
121
+ }),
122
+ })
123
+
124
+ expect(response.status).toBe(401)
125
+ })
126
+
127
+ test('POST /mcp should succeed with correct Bearer token', async () => {
128
+ const response = await fetch(`${AUTH_BASE}/mcp`, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/json',
132
+ Accept: 'application/json, text/event-stream',
133
+ Authorization: `Bearer ${AUTH_TOKEN}`,
134
+ },
135
+ body: JSON.stringify({
136
+ jsonrpc: '2.0',
137
+ id: 1,
138
+ method: 'initialize',
139
+ params: {
140
+ protocolVersion: '2025-03-26',
141
+ capabilities: {},
142
+ clientInfo: { name: 'test', version: '1.0' },
143
+ },
144
+ }),
145
+ })
146
+
147
+ expect(response.status).toBe(200)
148
+ })
149
+
150
+ test('GET / should return 401 without token', async () => {
151
+ const response = await fetch(`${AUTH_BASE}/`)
152
+ expect(response.status).toBe(401)
153
+ })
154
+ })
@@ -0,0 +1,63 @@
1
+ /**
2
+ * E2E Tests: Health & Root Info
3
+ *
4
+ * Verifies the HTTP server's health check and root info endpoints
5
+ * return correct responses with expected structure.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+
10
+ test.describe('Health & Root Info', () => {
11
+ test('should return 200 OK from /health endpoint', async ({ request }) => {
12
+ const response = await request.get('/health')
13
+ expect(response.status()).toBe(200)
14
+
15
+ const body = await response.json()
16
+ expect(body).toHaveProperty('status', 'healthy')
17
+ expect(body).toHaveProperty('timestamp')
18
+ // Timestamp should be a valid ISO string
19
+ expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp)
20
+ })
21
+
22
+ test('should return server metadata on GET /', async ({ request }) => {
23
+ const response = await request.get('/')
24
+ expect(response.status()).toBe(200)
25
+
26
+ const body = await response.json()
27
+ expect(body).toHaveProperty('name', 'memory-journal-mcp')
28
+ expect(body).toHaveProperty('version')
29
+ expect(body).toHaveProperty('description')
30
+ expect(body).toHaveProperty('endpoints')
31
+ expect(body.endpoints).toHaveProperty('POST /mcp')
32
+ expect(body.endpoints).toHaveProperty('GET /sse')
33
+ expect(body.endpoints).toHaveProperty('GET /health')
34
+ })
35
+
36
+ test('should accept MCP initialization request on /mcp', async ({ request }) => {
37
+ const response = await request.post('/mcp', {
38
+ headers: {
39
+ Accept: 'application/json, text/event-stream',
40
+ },
41
+ data: {
42
+ jsonrpc: '2.0',
43
+ id: 1,
44
+ method: 'initialize',
45
+ params: {
46
+ protocolVersion: '2025-03-26',
47
+ capabilities: {},
48
+ clientInfo: {
49
+ name: 'playwright-test',
50
+ version: '1.0.0',
51
+ },
52
+ },
53
+ },
54
+ })
55
+
56
+ expect(response.status()).toBe(200)
57
+ // Session ID should be returned in response headers
58
+ const sessionId = response.headers()['mcp-session-id']
59
+ expect(sessionId).toBeDefined()
60
+ expect(typeof sessionId).toBe('string')
61
+ expect(sessionId!.length).toBeGreaterThan(0)
62
+ })
63
+ })
@@ -0,0 +1,134 @@
1
+ /**
2
+ * E2E Tests: HTTP Transport Protocols
3
+ *
4
+ * Tests both Streamable HTTP (MCP 2025-03-26) and Legacy SSE (MCP 2024-11-05)
5
+ * protocol error handling and routing.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+
10
+ test.describe('HTTP Transport Protocols', () => {
11
+ test.describe('Streamable HTTP (MCP 2025-03-26)', () => {
12
+ test('should reject non-init request without session ID', async ({ request }) => {
13
+ const response = await request.post('/mcp', {
14
+ headers: {
15
+ Accept: 'application/json, text/event-stream',
16
+ },
17
+ data: {
18
+ jsonrpc: '2.0',
19
+ id: 1,
20
+ method: 'ping',
21
+ },
22
+ })
23
+
24
+ expect(response.status()).toBe(400)
25
+ const body = await response.json()
26
+ expect(body.error).toHaveProperty(
27
+ 'message',
28
+ 'Bad Request: No valid session ID provided'
29
+ )
30
+ })
31
+
32
+ test('should reject malformed JSON body on /mcp', async ({ request }) => {
33
+ const response = await request.post('/mcp', {
34
+ headers: {
35
+ Accept: 'application/json, text/event-stream',
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ data: Buffer.from('{"broken": json}'),
39
+ })
40
+
41
+ // Express JSON parser rejects invalid JSON before reaching our handler
42
+ expect(response.status()).toBe(400)
43
+ })
44
+
45
+ test('should accept initialization and return session ID', async ({ request }) => {
46
+ const response = await request.post('/mcp', {
47
+ headers: {
48
+ Accept: 'application/json, text/event-stream',
49
+ },
50
+ data: {
51
+ jsonrpc: '2.0',
52
+ id: 1,
53
+ method: 'initialize',
54
+ params: {
55
+ protocolVersion: '2025-03-26',
56
+ capabilities: {},
57
+ clientInfo: {
58
+ name: 'playwright-protocol-test',
59
+ version: '1.0.0',
60
+ },
61
+ },
62
+ },
63
+ })
64
+
65
+ expect(response.status()).toBe(200)
66
+ const sessionId = response.headers()['mcp-session-id']
67
+ expect(sessionId).toBeDefined()
68
+ })
69
+ })
70
+
71
+ test.describe('Legacy SSE (MCP 2024-11-05)', () => {
72
+ test('should reject /messages POST without sessionId parameter', async ({ request }) => {
73
+ const response = await request.post('/messages', {
74
+ data: {
75
+ jsonrpc: '2.0',
76
+ id: 1,
77
+ method: 'initialize',
78
+ params: {
79
+ protocolVersion: '2024-11-05',
80
+ capabilities: {},
81
+ clientInfo: { name: 'test', version: '1.0' },
82
+ },
83
+ },
84
+ })
85
+
86
+ expect(response.status()).toBe(400)
87
+ const body = await response.json()
88
+ expect(body.error).toHaveProperty('message', 'Missing sessionId parameter')
89
+ })
90
+
91
+ test('should reject /messages POST with unknown sessionId', async ({ request }) => {
92
+ const response = await request.post('/messages?sessionId=invalid-session-uuid', {
93
+ data: {
94
+ jsonrpc: '2.0',
95
+ id: 1,
96
+ method: 'ping',
97
+ },
98
+ })
99
+
100
+ expect(response.status()).toBe(404)
101
+ const body = await response.json()
102
+ expect(body.error).toHaveProperty('message', 'Session not found')
103
+ })
104
+
105
+ test('should complete full SDK client round-trip via Legacy SSE', async () => {
106
+ // Regression test: server.connect() auto-calls start() on SSEServerTransport,
107
+ // so a redundant start() call would throw "already started!" and break SSE entirely.
108
+ const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
109
+ const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')
110
+
111
+ const transport = new SSEClientTransport(new URL('http://localhost:3100/sse'))
112
+ const client = new Client(
113
+ { name: 'playwright-sse-regression', version: '1.0.0' },
114
+ { capabilities: {} }
115
+ )
116
+
117
+ try {
118
+ await client.connect(transport)
119
+
120
+ const response = await client.callTool({
121
+ name: 'test_simple',
122
+ arguments: { message: 'SSE round-trip' },
123
+ })
124
+
125
+ expect(response.isError).toBeUndefined()
126
+ expect(Array.isArray(response.content)).toBe(true)
127
+ const text = (response.content[0] as { type: string; text: string }).text
128
+ expect(text).toContain('SSE round-trip')
129
+ } finally {
130
+ await client.close()
131
+ }
132
+ })
133
+ })
134
+ })
@@ -0,0 +1,103 @@
1
+ /**
2
+ * E2E Tests: MCP Resource Reads via SDK Client
3
+ *
4
+ * Uses the official @modelcontextprotocol/sdk client to connect
5
+ * via Streamable HTTP transport and read resources end-to-end.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
10
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
11
+
12
+ test.describe.configure({ mode: 'serial' })
13
+
14
+ test.describe('E2E Resource Reads (via MCP SDK Client)', () => {
15
+ let client: Client
16
+
17
+ test.beforeAll(async () => {
18
+ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3100/mcp'))
19
+ client = new Client(
20
+ { name: 'playwright-resource-test', version: '1.0.0' },
21
+ { capabilities: {} }
22
+ )
23
+ await client.connect(transport)
24
+ })
25
+
26
+ test.afterAll(async () => {
27
+ await client.close()
28
+ })
29
+
30
+ test('should list available resources', async () => {
31
+ const listResponse = await client.listResources()
32
+
33
+ expect(listResponse.resources).toBeDefined()
34
+ expect(Array.isArray(listResponse.resources)).toBe(true)
35
+ expect(listResponse.resources.length).toBeGreaterThan(0)
36
+
37
+ const uris = listResponse.resources.map((r) => r.uri)
38
+ expect(uris).toContain('memory://health')
39
+ expect(uris).toContain('memory://briefing')
40
+ expect(uris).toContain('memory://recent')
41
+ expect(uris).toContain('memory://statistics')
42
+ })
43
+
44
+ test('should read memory://health resource', async () => {
45
+ const response = await client.readResource({ uri: 'memory://health' })
46
+
47
+ expect(response.contents).toBeDefined()
48
+ expect(response.contents.length).toBeGreaterThan(0)
49
+
50
+ const text = response.contents[0]!.text as string
51
+ const parsed = JSON.parse(text)
52
+ expect(parsed).toHaveProperty('database')
53
+ expect(parsed).toHaveProperty('vectorIndex')
54
+ expect(parsed).toHaveProperty('scheduler')
55
+ })
56
+
57
+ test('should read memory://briefing resource', async () => {
58
+ const response = await client.readResource({ uri: 'memory://briefing' })
59
+
60
+ expect(response.contents).toBeDefined()
61
+ expect(response.contents.length).toBeGreaterThan(0)
62
+
63
+ const text = response.contents[0]!.text as string
64
+ const parsed = JSON.parse(text)
65
+ expect(parsed).toHaveProperty('userMessage')
66
+ expect(parsed).toHaveProperty('journal')
67
+ })
68
+
69
+ test('should read memory://recent resource', async () => {
70
+ const response = await client.readResource({ uri: 'memory://recent' })
71
+
72
+ expect(response.contents).toBeDefined()
73
+ expect(response.contents.length).toBeGreaterThan(0)
74
+
75
+ const text = response.contents[0]!.text as string
76
+ const parsed = JSON.parse(text)
77
+ // May be empty if no entries exist, but should be valid JSON
78
+ expect(parsed).toBeDefined()
79
+ })
80
+
81
+ test('should read memory://statistics resource', async () => {
82
+ const response = await client.readResource({ uri: 'memory://statistics' })
83
+
84
+ expect(response.contents).toBeDefined()
85
+ expect(response.contents.length).toBeGreaterThan(0)
86
+
87
+ const text = response.contents[0]!.text as string
88
+ const parsed = JSON.parse(text)
89
+ expect(parsed).toHaveProperty('totalEntries')
90
+ })
91
+
92
+ test('should list resource templates', async () => {
93
+ const response = await client.listResourceTemplates()
94
+
95
+ expect(response.resourceTemplates).toBeDefined()
96
+ expect(Array.isArray(response.resourceTemplates)).toBe(true)
97
+ expect(response.resourceTemplates.length).toBeGreaterThan(0)
98
+
99
+ const uriTemplates = response.resourceTemplates.map((t) => t.uriTemplate)
100
+ expect(uriTemplates).toContain('memory://issues/{issue_number}/entries')
101
+ expect(uriTemplates).toContain('memory://kanban/{project_number}')
102
+ })
103
+ })