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
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Admin Tool Group - 5 tools
3
+ *
4
+ * Tools: update_entry, delete_entry, merge_tags, rebuild_vector_index, add_to_vector_index
5
+ */
6
+
7
+ import { z } from 'zod'
8
+ import type { ToolDefinition, ToolContext } from '../../types/index.js'
9
+ import { formatHandlerError } from '../../utils/error-helpers.js'
10
+ import { ENTRY_TYPES, EntryOutputSchema } from './schemas.js'
11
+
12
+ // ============================================================================
13
+ // Input Schemas
14
+ // ============================================================================
15
+
16
+ /** Strict schema — used inside handler for structured Zod errors */
17
+ const UpdateEntrySchema = z.object({
18
+ entry_id: z.number(),
19
+ content: z.string().optional(),
20
+ entry_type: z.enum(ENTRY_TYPES).optional(),
21
+ is_personal: z.boolean().optional(),
22
+ tags: z.array(z.string()).optional(),
23
+ })
24
+
25
+ /** Relaxed schema — passed to SDK inputSchema so Zod enum errors reach the handler */
26
+ const UpdateEntrySchemaMcp = z.object({
27
+ entry_id: z.number(),
28
+ content: z.string().optional(),
29
+ entry_type: z.string().optional(),
30
+ is_personal: z.boolean().optional(),
31
+ tags: z.array(z.string()).optional(),
32
+ })
33
+
34
+ const DeleteEntrySchema = z.object({
35
+ entry_id: z.number(),
36
+ permanent: z.boolean().optional().default(false),
37
+ })
38
+
39
+ // ============================================================================
40
+ // Output Schemas
41
+ // ============================================================================
42
+
43
+ const UpdateEntryOutputSchema = z.object({
44
+ success: z.boolean().optional(),
45
+ entry: EntryOutputSchema.optional(),
46
+ error: z.string().optional(),
47
+ })
48
+
49
+ const DeleteEntryOutputSchema = z.object({
50
+ success: z.boolean().optional(),
51
+ entryId: z.number().optional(),
52
+ permanent: z.boolean().optional(),
53
+ error: z.string().optional(),
54
+ })
55
+
56
+ const MergeTagsOutputSchema = z.object({
57
+ success: z.boolean().optional(),
58
+ sourceTag: z.string().optional(),
59
+ targetTag: z.string().optional(),
60
+ entriesUpdated: z.number().optional(),
61
+ sourceDeleted: z.boolean().optional(),
62
+ message: z.string().optional(),
63
+ error: z.string().optional(),
64
+ })
65
+
66
+ const RebuildVectorIndexOutputSchema = z.object({
67
+ success: z.boolean().optional(),
68
+ entriesIndexed: z.number().optional(),
69
+ error: z.string().optional(),
70
+ })
71
+
72
+ const AddToVectorIndexOutputSchema = z.object({
73
+ success: z.boolean().optional(),
74
+ entryId: z.number().optional(),
75
+ error: z.string().optional(),
76
+ })
77
+
78
+ // ============================================================================
79
+ // Tool Definitions
80
+ // ============================================================================
81
+
82
+ export function getAdminTools(context: ToolContext): ToolDefinition[] {
83
+ const { db, vectorManager, progress } = context
84
+ return [
85
+ {
86
+ name: 'update_entry',
87
+ title: 'Update Entry',
88
+ description: 'Update an existing journal entry',
89
+ group: 'admin',
90
+ inputSchema: UpdateEntrySchemaMcp,
91
+ outputSchema: UpdateEntryOutputSchema,
92
+ annotations: { readOnlyHint: false, idempotentHint: false },
93
+ handler: (params: unknown) => {
94
+ try {
95
+ const input = UpdateEntrySchema.parse(params)
96
+ const entry = db.updateEntry(input.entry_id, {
97
+ content: input.content,
98
+ entryType: input.entry_type,
99
+ isPersonal: input.is_personal,
100
+ tags: input.tags,
101
+ })
102
+ if (!entry) {
103
+ return {
104
+ success: false,
105
+ error: `Entry ${String(input.entry_id)} not found`,
106
+ }
107
+ }
108
+
109
+ // Re-index if content changed
110
+ if (input.content && vectorManager) {
111
+ vectorManager.addEntry(entry.id, entry.content).catch(() => {
112
+ // Non-critical failure, entry already updated in DB
113
+ })
114
+ }
115
+
116
+ return { success: true, entry }
117
+ } catch (err) {
118
+ return formatHandlerError(err)
119
+ }
120
+ },
121
+ },
122
+ {
123
+ name: 'delete_entry',
124
+ title: 'Delete Entry',
125
+ description: 'Delete a journal entry (soft delete with timestamp)',
126
+ group: 'admin',
127
+ inputSchema: DeleteEntrySchema,
128
+ outputSchema: DeleteEntryOutputSchema,
129
+ annotations: { readOnlyHint: false, destructiveHint: true },
130
+ handler: (params: unknown) => {
131
+ try {
132
+ const { entry_id, permanent } = DeleteEntrySchema.parse(params)
133
+ const success = db.deleteEntry(entry_id, permanent)
134
+
135
+ if (!success) {
136
+ return {
137
+ success: false,
138
+ entryId: entry_id,
139
+ permanent,
140
+ error: `Entry ${String(entry_id)} not found`,
141
+ }
142
+ }
143
+
144
+ // Remove from vector index (non-critical if fails)
145
+ if (vectorManager) {
146
+ vectorManager.removeEntry(entry_id).catch(() => {
147
+ // Non-critical failure, entry already deleted from DB
148
+ })
149
+ }
150
+
151
+ return { success, entryId: entry_id, permanent }
152
+ } catch (err) {
153
+ return formatHandlerError(err)
154
+ }
155
+ },
156
+ },
157
+ {
158
+ name: 'merge_tags',
159
+ title: 'Merge Tags',
160
+ description:
161
+ 'Merge one tag into another to consolidate similar tags (e.g., merge "phase-2" into "phase2"). The source tag is deleted after merge.',
162
+ group: 'admin',
163
+ inputSchema: z.object({
164
+ source_tag: z.string().min(1).describe('Tag to merge from (will be deleted)'),
165
+ target_tag: z
166
+ .string()
167
+ .min(1)
168
+ .describe('Tag to merge into (will be created if not exists)'),
169
+ }),
170
+ outputSchema: MergeTagsOutputSchema,
171
+ annotations: { readOnlyHint: false, idempotentHint: false },
172
+ handler: (params: unknown) => {
173
+ try {
174
+ const { source_tag, target_tag } = z
175
+ .object({
176
+ source_tag: z.string().min(1),
177
+ target_tag: z.string().min(1),
178
+ })
179
+ .parse(params)
180
+
181
+ if (source_tag === target_tag) {
182
+ return {
183
+ success: false,
184
+ sourceTag: source_tag,
185
+ targetTag: target_tag,
186
+ entriesUpdated: 0,
187
+ sourceDeleted: false,
188
+ message: 'Source and target tags cannot be the same',
189
+ error: 'Source and target tags must be different',
190
+ }
191
+ }
192
+
193
+ const result = db.mergeTags(source_tag, target_tag)
194
+ return {
195
+ success: true,
196
+ sourceTag: source_tag,
197
+ targetTag: target_tag,
198
+ entriesUpdated: result.entriesUpdated,
199
+ sourceDeleted: result.sourceDeleted,
200
+ message: `Merged "${source_tag}" into "${target_tag}". Updated ${String(result.entriesUpdated)} entries.`,
201
+ }
202
+ } catch (error) {
203
+ // Zod or domain error
204
+ if (error instanceof z.ZodError) {
205
+ return formatHandlerError(error)
206
+ }
207
+ // Domain error from db.mergeTags — try to preserve schema shape
208
+ try {
209
+ const parsed = z
210
+ .object({ source_tag: z.string(), target_tag: z.string() })
211
+ .parse(params)
212
+ return {
213
+ success: false,
214
+ sourceTag: parsed.source_tag,
215
+ targetTag: parsed.target_tag,
216
+ entriesUpdated: 0,
217
+ sourceDeleted: false,
218
+ message: 'Tag merge failed',
219
+ error: error instanceof Error ? error.message : 'Unknown error',
220
+ }
221
+ } catch {
222
+ return formatHandlerError(error)
223
+ }
224
+ }
225
+ },
226
+ },
227
+ {
228
+ name: 'rebuild_vector_index',
229
+ title: 'Rebuild Vector Index',
230
+ description: 'Rebuild the semantic search vector index from all existing entries',
231
+ group: 'admin',
232
+ inputSchema: z.object({}),
233
+ outputSchema: RebuildVectorIndexOutputSchema,
234
+ annotations: { readOnlyHint: false, idempotentHint: false },
235
+ handler: async (_params: unknown) => {
236
+ try {
237
+ if (!vectorManager) {
238
+ return {
239
+ success: false,
240
+ entriesIndexed: 0,
241
+ error: 'Vector search not available',
242
+ }
243
+ }
244
+ const indexed = await vectorManager.rebuildIndex(db, progress)
245
+ return { success: true, entriesIndexed: indexed }
246
+ } catch (err) {
247
+ return formatHandlerError(err)
248
+ }
249
+ },
250
+ },
251
+ {
252
+ name: 'add_to_vector_index',
253
+ title: 'Add Entry to Vector Index',
254
+ description: 'Add a specific entry to the semantic search vector index',
255
+ group: 'admin',
256
+ inputSchema: z.object({ entry_id: z.number() }),
257
+ outputSchema: AddToVectorIndexOutputSchema,
258
+ annotations: { readOnlyHint: false, idempotentHint: true },
259
+ handler: async (params: unknown) => {
260
+ try {
261
+ const { entry_id } = z.object({ entry_id: z.number() }).parse(params)
262
+ if (!vectorManager) {
263
+ return {
264
+ success: false,
265
+ entryId: entry_id,
266
+ error: 'Vector search not available',
267
+ }
268
+ }
269
+ const entry = db.getEntryById(entry_id)
270
+ if (!entry) {
271
+ return {
272
+ success: false,
273
+ entryId: entry_id,
274
+ error: `Entry ${String(entry_id)} not found`,
275
+ }
276
+ }
277
+ const success = await vectorManager.addEntry(entry_id, entry.content)
278
+ return { success, entryId: entry_id }
279
+ } catch (err) {
280
+ return formatHandlerError(err)
281
+ }
282
+ },
283
+ },
284
+ ]
285
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Analytics Tool Group - 2 tools
3
+ *
4
+ * Tools: get_statistics, get_cross_project_insights
5
+ */
6
+
7
+ import { z } from 'zod'
8
+ import type { ToolDefinition, ToolContext } from '../../types/index.js'
9
+ import { formatHandlerError } from '../../utils/error-helpers.js'
10
+ import { DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE, TagOutputSchema } from './schemas.js'
11
+
12
+ // ============================================================================
13
+ // Output Schemas
14
+ // ============================================================================
15
+
16
+ const StatisticsOutputSchema = z.object({
17
+ groupBy: z.string().optional(),
18
+ totalEntries: z.number().optional(),
19
+ entriesByType: z.record(z.string(), z.number()).optional(),
20
+ entriesByPeriod: z
21
+ .array(
22
+ z.object({
23
+ period: z.string(),
24
+ count: z.number(),
25
+ })
26
+ )
27
+ .optional(),
28
+ decisionDensity: z
29
+ .array(
30
+ z.object({
31
+ period: z.string(),
32
+ significantCount: z.number(),
33
+ })
34
+ )
35
+ .optional(),
36
+ relationshipComplexity: z
37
+ .object({
38
+ totalRelationships: z.number(),
39
+ avgPerEntry: z.number(),
40
+ })
41
+ .optional(),
42
+ activityTrend: z
43
+ .object({
44
+ currentPeriod: z.string(),
45
+ previousPeriod: z.string(),
46
+ growthPercent: z.number().nullable(),
47
+ })
48
+ .optional(),
49
+ causalMetrics: z
50
+ .object({
51
+ blocked_by: z.number(),
52
+ resolved: z.number(),
53
+ caused: z.number(),
54
+ })
55
+ .optional(),
56
+ success: z.boolean().optional(),
57
+ error: z.string().optional(),
58
+ })
59
+
60
+ const ProjectSummaryOutputSchema = z.object({
61
+ project_number: z.number(),
62
+ entry_count: z.number(),
63
+ first_entry: z.string(),
64
+ last_entry: z.string(),
65
+ active_days: z.number(),
66
+ top_tags: z.array(TagOutputSchema),
67
+ })
68
+
69
+ const CrossProjectInsightsOutputSchema = z.object({
70
+ project_count: z.number().optional(),
71
+ total_entries: z.number().optional(),
72
+ projects: z.array(ProjectSummaryOutputSchema).optional(),
73
+ inactive_projects: z
74
+ .array(
75
+ z.object({
76
+ project_number: z.number(),
77
+ last_entry_date: z.string(),
78
+ })
79
+ )
80
+ .optional(),
81
+ inactiveThresholdDays: z.number().optional(),
82
+ time_distribution: z
83
+ .array(
84
+ z.object({
85
+ project_number: z.number(),
86
+ percentage: z.string(),
87
+ })
88
+ )
89
+ .optional(),
90
+ message: z.string().optional(),
91
+ success: z.boolean().optional(),
92
+ error: z.string().optional(),
93
+ })
94
+
95
+ // ============================================================================
96
+ // Input Schemas
97
+ // ============================================================================
98
+
99
+ /** Strict schema — used inside handler for structured Zod errors */
100
+ const GetStatisticsSchema = z.object({
101
+ group_by: z.enum(['day', 'week', 'month']).optional().default('week'),
102
+ start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
103
+ end_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
104
+ project_breakdown: z.boolean().optional().default(false),
105
+ })
106
+
107
+ /** Relaxed schema — passed to SDK inputSchema so Zod errors reach the handler */
108
+ const GetStatisticsSchemaMcp = z.object({
109
+ group_by: z.string().optional().default('week'),
110
+ start_date: z.string().optional(),
111
+ end_date: z.string().optional(),
112
+ project_breakdown: z.boolean().optional().default(false),
113
+ })
114
+
115
+ /** Strict schema — used inside handler for structured Zod errors */
116
+ const CrossProjectInsightsInputSchema = z.object({
117
+ start_date: z
118
+ .string()
119
+ .regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE)
120
+ .optional()
121
+ .describe('Start date (YYYY-MM-DD)'),
122
+ end_date: z
123
+ .string()
124
+ .regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE)
125
+ .optional()
126
+ .describe('End date (YYYY-MM-DD)'),
127
+ min_entries: z.number().optional().default(3).describe('Minimum entries to include project'),
128
+ })
129
+
130
+ /** Relaxed schema — passed to SDK inputSchema so Zod errors reach the handler */
131
+ const CrossProjectInsightsInputSchemaMcp = z.object({
132
+ start_date: z.string().optional().describe('Start date (YYYY-MM-DD)'),
133
+ end_date: z.string().optional().describe('End date (YYYY-MM-DD)'),
134
+ min_entries: z.number().optional().default(3).describe('Minimum entries to include project'),
135
+ })
136
+
137
+ // ============================================================================
138
+ // Tool Definitions
139
+ // ============================================================================
140
+
141
+ export function getAnalyticsTools(context: ToolContext): ToolDefinition[] {
142
+ const { db } = context
143
+ return [
144
+ {
145
+ name: 'get_statistics',
146
+ title: 'Get Statistics',
147
+ description:
148
+ 'Get journal statistics and analytics (Phase 2: includes project breakdown)',
149
+ group: 'analytics',
150
+ inputSchema: GetStatisticsSchemaMcp,
151
+ outputSchema: StatisticsOutputSchema,
152
+ annotations: { readOnlyHint: true, idempotentHint: true },
153
+ handler: (params: unknown) => {
154
+ try {
155
+ const { group_by } = GetStatisticsSchema.parse(params)
156
+ const stats = db.getStatistics(group_by)
157
+ return { ...stats, groupBy: group_by }
158
+ } catch (err) {
159
+ return formatHandlerError(err)
160
+ }
161
+ },
162
+ },
163
+ {
164
+ name: 'get_cross_project_insights',
165
+ title: 'Get Cross-Project Insights',
166
+ description: 'Analyze patterns across all GitHub Projects tracked in journal entries',
167
+ group: 'analytics',
168
+ inputSchema: CrossProjectInsightsInputSchemaMcp,
169
+ outputSchema: CrossProjectInsightsOutputSchema,
170
+ annotations: { readOnlyHint: true, idempotentHint: true },
171
+ handler: (params: unknown) => {
172
+ try {
173
+ const input = CrossProjectInsightsInputSchema.parse(params)
174
+
175
+ const rawDb = db.getRawDb()
176
+
177
+ // Build WHERE clause
178
+ let where = 'WHERE deleted_at IS NULL AND project_number IS NOT NULL'
179
+ const sqlParams: unknown[] = []
180
+
181
+ if (input.start_date) {
182
+ where += ' AND DATE(timestamp) >= DATE(?)'
183
+ sqlParams.push(input.start_date)
184
+ }
185
+ if (input.end_date) {
186
+ where += ' AND DATE(timestamp) <= DATE(?)'
187
+ sqlParams.push(input.end_date)
188
+ }
189
+
190
+ // Get active projects with stats
191
+ const projectsResult = rawDb.exec(
192
+ `
193
+ SELECT project_number, COUNT(*) as entry_count,
194
+ MIN(DATE(timestamp)) as first_entry,
195
+ MAX(DATE(timestamp)) as last_entry,
196
+ COUNT(DISTINCT DATE(timestamp)) as active_days
197
+ FROM memory_journal ${where}
198
+ GROUP BY project_number
199
+ HAVING entry_count >= ?
200
+ ORDER BY entry_count DESC
201
+ `,
202
+ [...sqlParams, input.min_entries]
203
+ )
204
+
205
+ if (!projectsResult[0] || projectsResult[0].values.length === 0) {
206
+ return {
207
+ project_count: 0,
208
+ total_entries: 0,
209
+ projects: [],
210
+ inactive_projects: [],
211
+ inactiveThresholdDays: 7,
212
+ time_distribution: [],
213
+ message: `No projects found with at least ${String(input.min_entries)} entries`,
214
+ }
215
+ }
216
+
217
+ const columns = projectsResult[0].columns
218
+ const projects = projectsResult[0].values.map((row) => {
219
+ const obj: Record<string, unknown> = {}
220
+ columns.forEach((col, i) => {
221
+ obj[col] = row[i]
222
+ })
223
+ return obj
224
+ })
225
+
226
+ // Get top tags per project
227
+ const projectTags: Record<number, { name: string; count: number }[]> = {}
228
+ for (const proj of projects) {
229
+ const projNum = proj['project_number'] as number
230
+ const tagsResult = rawDb.exec(
231
+ `
232
+ SELECT t.name, COUNT(*) as count
233
+ FROM tags t
234
+ JOIN entry_tags et ON t.id = et.tag_id
235
+ JOIN memory_journal m ON et.entry_id = m.id
236
+ WHERE m.project_number = ? AND m.deleted_at IS NULL
237
+ GROUP BY t.name
238
+ ORDER BY count DESC
239
+ LIMIT 5
240
+ `,
241
+ [projNum]
242
+ )
243
+ if (tagsResult[0]) {
244
+ projectTags[projNum] = tagsResult[0].values.map((row) => ({
245
+ name: row[0] as string,
246
+ count: row[1] as number,
247
+ }))
248
+ }
249
+ }
250
+
251
+ // Find inactive projects (last entry > 7 days ago)
252
+ const cutoffDate = new Date(Date.now() - 7 * 86400000)
253
+ .toISOString()
254
+ .split('T')[0]
255
+ const inactiveResult = rawDb.exec(
256
+ `
257
+ SELECT project_number, MAX(DATE(timestamp)) as last_entry_date
258
+ FROM memory_journal
259
+ WHERE deleted_at IS NULL AND project_number IS NOT NULL
260
+ GROUP BY project_number
261
+ HAVING last_entry_date < ?
262
+ `,
263
+ [cutoffDate]
264
+ )
265
+
266
+ const inactiveProjects =
267
+ inactiveResult[0]?.values.map((row) => ({
268
+ project_number: row[0] as number,
269
+ last_entry_date: row[1] as string,
270
+ })) ?? []
271
+
272
+ // Calculate time distribution
273
+ const totalEntries = projects.reduce(
274
+ (sum, p) => sum + (p['entry_count'] as number),
275
+ 0
276
+ )
277
+ const distribution = projects.slice(0, 5).map((p) => ({
278
+ project_number: p['project_number'],
279
+ percentage: (((p['entry_count'] as number) / totalEntries) * 100).toFixed(
280
+ 1
281
+ ),
282
+ }))
283
+
284
+ return {
285
+ project_count: projects.length,
286
+ total_entries: totalEntries,
287
+ projects: projects.map((p) => ({
288
+ ...p,
289
+ top_tags: projectTags[p['project_number'] as number] ?? [],
290
+ })),
291
+ inactive_projects: inactiveProjects,
292
+ inactiveThresholdDays: 7,
293
+ time_distribution: distribution,
294
+ }
295
+ } catch (err) {
296
+ return formatHandlerError(err)
297
+ }
298
+ },
299
+ },
300
+ ]
301
+ }