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
@@ -1,2934 +1,142 @@
1
1
  /**
2
- * Memory Journal MCP Server - Tool Handlers
2
+ * Tool Handler Barrel
3
3
  *
4
- * Exports all MCP tools with annotations following MCP 2025-11-25 spec.
5
- */
6
- import { z } from 'zod';
7
- import { sendProgress } from '../../utils/progress-utils.js';
8
- import { getToolIcon } from '../../constants/icons.js';
9
- // ============================================================================
10
- // Zod Schemas for Input Validation
11
- // ============================================================================
12
- const CreateEntrySchema = z.object({
13
- content: z.string().min(1).max(50000),
14
- entry_type: z.string().optional().default('personal_reflection'),
15
- tags: z.array(z.string()).optional().default([]),
16
- is_personal: z.boolean().optional().default(true),
17
- significance_type: z.string().optional(),
18
- auto_context: z.boolean().optional().default(true),
19
- project_number: z.number().optional(),
20
- project_owner: z.string().optional(),
21
- issue_number: z.number().optional(),
22
- issue_url: z.string().optional(),
23
- pr_number: z.number().optional(),
24
- pr_url: z.string().optional(),
25
- pr_status: z.enum(['draft', 'open', 'merged', 'closed']).optional(),
26
- workflow_run_id: z.number().optional(),
27
- workflow_name: z.string().optional(),
28
- workflow_status: z.enum(['queued', 'in_progress', 'completed']).optional(),
29
- share_with_team: z.boolean().optional().default(false),
30
- });
31
- const GetEntryByIdSchema = z.object({
32
- entry_id: z.number(),
33
- include_relationships: z.boolean().optional().default(true),
34
- });
35
- const GetRecentEntriesSchema = z.object({
36
- limit: z.number().optional().default(5),
37
- is_personal: z.boolean().optional(),
38
- });
39
- const CreateEntryMinimalSchema = z.object({
40
- content: z.string().min(1).max(50000),
41
- });
42
- const TestSimpleSchema = z.object({
43
- message: z.string().optional().default('Hello'),
44
- });
45
- const SearchEntriesSchema = z.object({
46
- query: z.string().optional(),
47
- limit: z.number().optional().default(10),
48
- is_personal: z.boolean().optional(),
49
- project_number: z.number().optional(),
50
- issue_number: z.number().optional(),
51
- pr_number: z.number().optional(),
52
- pr_status: z.enum(['draft', 'open', 'merged', 'closed']).optional(),
53
- workflow_run_id: z.number().optional(),
54
- });
55
- const SearchByDateRangeSchema = z.object({
56
- start_date: z.string(),
57
- end_date: z.string(),
58
- entry_type: z.string().optional(),
59
- tags: z.array(z.string()).optional(),
60
- is_personal: z.boolean().optional(),
61
- project_number: z.number().optional(),
62
- issue_number: z.number().optional(),
63
- pr_number: z.number().optional(),
64
- workflow_run_id: z.number().optional(),
65
- });
66
- const SemanticSearchSchema = z.object({
67
- query: z.string(),
68
- limit: z.number().optional().default(10),
69
- similarity_threshold: z.number().optional().default(0.25),
70
- is_personal: z.boolean().optional(),
71
- hint_on_empty: z
72
- .boolean()
73
- .optional()
74
- .default(true)
75
- .describe('Include hint when no results found (default: true)'),
76
- });
77
- const GetStatisticsSchema = z.object({
78
- group_by: z.enum(['day', 'week', 'month']).optional().default('week'),
79
- start_date: z.string().optional(),
80
- end_date: z.string().optional(),
81
- project_breakdown: z.boolean().optional().default(false),
82
- });
83
- const LinkEntriesSchema = z.object({
84
- from_entry_id: z.number(),
85
- to_entry_id: z.number(),
86
- relationship_type: z
87
- .enum([
88
- 'evolves_from',
89
- 'references',
90
- 'implements',
91
- 'clarifies',
92
- 'response_to',
93
- 'blocked_by',
94
- 'resolved',
95
- 'caused',
96
- ])
97
- .optional()
98
- .default('references'),
99
- description: z.string().optional(),
100
- });
101
- const ExportEntriesSchema = z.object({
102
- format: z.enum(['json', 'markdown']).optional().default('json'),
103
- start_date: z.string().optional(),
104
- end_date: z.string().optional(),
105
- entry_types: z.array(z.string()).optional(),
106
- tags: z.array(z.string()).optional(),
107
- limit: z.number().optional().default(100).describe('Maximum entries to export (default: 100)'),
108
- });
109
- const UpdateEntrySchema = z.object({
110
- entry_id: z.number(),
111
- content: z.string().optional(),
112
- entry_type: z.string().optional(),
113
- is_personal: z.boolean().optional(),
114
- tags: z.array(z.string()).optional(),
115
- });
116
- const DeleteEntrySchema = z.object({
117
- entry_id: z.number(),
118
- permanent: z.boolean().optional().default(false),
119
- });
120
- // ============================================================================
121
- // Zod Schemas for Output Validation (MCP 2025-11-25 outputSchema)
122
- // ============================================================================
123
- /**
124
- * Schema for a journal entry in output responses.
125
- * Uses camelCase to match actual database output format.
126
- */
127
- const EntryOutputSchema = z.object({
128
- id: z.number(),
129
- content: z.string(),
130
- entryType: z.string(),
131
- isPersonal: z.boolean(),
132
- timestamp: z.string(),
133
- tags: z.array(z.string()).optional(),
134
- significanceType: z.string().nullable().optional(),
135
- autoContext: z.string().nullable().optional(),
136
- deletedAt: z.string().nullable().optional(),
137
- projectNumber: z.number().nullable().optional(),
138
- projectOwner: z.string().nullable().optional(),
139
- issueNumber: z.number().nullable().optional(),
140
- issueUrl: z.string().nullable().optional(),
141
- prNumber: z.number().nullable().optional(),
142
- prUrl: z.string().nullable().optional(),
143
- prStatus: z.string().nullable().optional(),
144
- workflowRunId: z.number().nullable().optional(),
145
- workflowName: z.string().nullable().optional(),
146
- workflowStatus: z.string().nullable().optional(),
147
- });
148
- /**
149
- * Schema for list of entries with count.
150
- * Used by get_recent_entries, search_entries, search_by_date_range.
151
- */
152
- const EntriesListOutputSchema = z.object({
153
- entries: z.array(EntryOutputSchema),
154
- count: z.number(),
155
- });
156
- /**
157
- * Schema for a relationship between entries.
158
- */
159
- const RelationshipOutputSchema = z.object({
160
- id: z.number(),
161
- fromEntryId: z.number(),
162
- toEntryId: z.number(),
163
- relationshipType: z.string(),
164
- description: z.string().nullable().optional(),
165
- createdAt: z.string(),
166
- });
167
- /**
168
- * Schema for get_entry_by_id output (entry with optional relationships).
169
- * Handles both success (entry found) and error (entry not found) cases.
170
- */
171
- const ImportanceBreakdownSchema = z.object({
172
- significance: z.number(),
173
- relationships: z.number(),
174
- causal: z.number(),
175
- recency: z.number(),
176
- });
177
- const EntryByIdOutputSchema = z.object({
178
- entry: EntryOutputSchema.optional(),
179
- relationships: z.array(RelationshipOutputSchema).optional(),
180
- importance: z.number().nullable().optional(), // Computed importance score (0.0-1.0)
181
- importanceBreakdown: ImportanceBreakdownSchema.optional(), // Weighted component contributions
182
- error: z.string().optional(),
183
- });
184
- /**
185
- * Schema for get_statistics output.
186
- * Matches SqliteAdapter.getStatistics() return type.
187
- */
188
- const StatisticsOutputSchema = z.object({
189
- groupBy: z.string(),
190
- totalEntries: z.number(),
191
- entriesByType: z.record(z.string(), z.number()),
192
- entriesByPeriod: z.array(z.object({
193
- period: z.string(),
194
- count: z.number(),
195
- })),
196
- // Enhanced analytics (v4.3.0)
197
- decisionDensity: z.array(z.object({
198
- period: z.string(),
199
- significantCount: z.number(),
200
- })),
201
- relationshipComplexity: z.object({
202
- totalRelationships: z.number(),
203
- avgPerEntry: z.number(),
204
- }),
205
- activityTrend: z.object({
206
- currentPeriod: z.string(),
207
- previousPeriod: z.string(),
208
- growthPercent: z.number().nullable(),
209
- }),
210
- causalMetrics: z.object({
211
- blocked_by: z.number(),
212
- resolved: z.number(),
213
- caused: z.number(),
214
- }),
215
- });
216
- // ============================================================================
217
- // Phase 1: Core Read Tool Output Schemas
218
- // ============================================================================
219
- /**
220
- * Entry with similarity score for semantic search results.
221
- */
222
- const SemanticEntryOutputSchema = EntryOutputSchema.extend({
223
- similarity: z.number(),
224
- });
225
- /**
226
- * Schema for semantic_search output.
227
- */
228
- const SemanticSearchOutputSchema = z.object({
229
- query: z.string(),
230
- entries: z.array(SemanticEntryOutputSchema),
231
- count: z.number(),
232
- hint: z.string().optional(),
233
- error: z.string().optional(),
234
- });
235
- /**
236
- * Tag with usage count.
237
- */
238
- const TagOutputSchema = z.object({
239
- name: z.string(),
240
- count: z.number(),
241
- });
242
- /**
243
- * Schema for list_tags output.
244
- */
245
- const TagsListOutputSchema = z.object({
246
- tags: z.array(TagOutputSchema),
247
- count: z.number(),
248
- });
249
- /**
250
- * Schema for merge_tags output.
251
- */
252
- const MergeTagsOutputSchema = z.object({
253
- success: z.boolean(),
254
- sourceTag: z.string(),
255
- targetTag: z.string(),
256
- entriesUpdated: z.number(),
257
- sourceDeleted: z.boolean(),
258
- message: z.string(),
259
- error: z.string().optional(),
260
- });
261
- /**
262
- * Schema for get_vector_index_stats output.
263
- */
264
- const VectorStatsOutputSchema = z.object({
265
- available: z.boolean(),
266
- error: z.string().optional(),
267
- entryCount: z.number().optional(),
268
- indexSize: z.number().optional(),
269
- });
270
- /**
271
- * Schema for visualize_relationships output.
272
- */
273
- const VisualizationOutputSchema = z.object({
274
- entry_count: z.number(),
275
- relationship_count: z.number(),
276
- root_entry: z.number().nullable(),
277
- depth: z.number(),
278
- mermaid: z.string().nullable(),
279
- message: z.string().optional(),
280
- legend: z
281
- .object({
282
- blue: z.string(),
283
- orange: z.string(),
284
- arrows: z.record(z.string(), z.string()),
285
- })
286
- .optional(),
287
- });
288
- /**
289
- * Project summary for cross-project insights.
290
- */
291
- const ProjectSummaryOutputSchema = z.object({
292
- project_number: z.number(),
293
- entry_count: z.number(),
294
- first_entry: z.string(),
295
- last_entry: z.string(),
296
- active_days: z.number(),
297
- top_tags: z.array(TagOutputSchema),
298
- });
299
- /**
300
- * Schema for get_cross_project_insights output.
301
- */
302
- const CrossProjectInsightsOutputSchema = z.object({
303
- project_count: z.number(),
304
- total_entries: z.number(),
305
- projects: z.array(ProjectSummaryOutputSchema),
306
- inactive_projects: z.array(z.object({
307
- project_number: z.number(),
308
- last_entry_date: z.string(),
309
- })),
310
- inactiveThresholdDays: z.number(), // Cutoff for inactive classification
311
- time_distribution: z.array(z.object({
312
- project_number: z.number(),
313
- percentage: z.string(),
314
- })),
315
- message: z.string().optional(),
316
- });
317
- // ============================================================================
318
- // Phase 2: Mutation Tool Output Schemas
319
- // ============================================================================
320
- /**
321
- * Schema for create_entry and create_entry_minimal output.
322
- */
323
- const CreateEntryOutputSchema = z.object({
324
- success: z.boolean(),
325
- entry: EntryOutputSchema,
326
- });
327
- /**
328
- * Schema for update_entry output (success or error).
329
- */
330
- const UpdateEntryOutputSchema = z.object({
331
- success: z.boolean().optional(),
332
- entry: EntryOutputSchema.optional(),
333
- error: z.string().optional(),
334
- });
335
- /**
336
- * Schema for delete_entry output.
337
- */
338
- const DeleteEntryOutputSchema = z.object({
339
- success: z.boolean(),
340
- entryId: z.number(),
341
- permanent: z.boolean(),
342
- error: z.string().optional(),
343
- });
344
- /**
345
- * Schema for link_entries output.
346
- */
347
- const LinkEntriesOutputSchema = z.object({
348
- success: z.boolean(),
349
- relationship: RelationshipOutputSchema,
350
- duplicate: z.boolean().optional().describe('True if relationship already existed'),
351
- message: z.string().optional().describe('Additional context about the operation'),
352
- });
353
- // ============================================================================
354
- // Phase 3: GitHub Tool Output Schemas
355
- // ============================================================================
356
- /**
357
- * GitHub issue schema (mirrors GitHub API shape).
358
- */
359
- const GitHubIssueOutputSchema = z.object({
360
- number: z.number(),
361
- title: z.string(),
362
- url: z.string(),
363
- state: z.enum(['OPEN', 'CLOSED']),
364
- milestone: z
365
- .object({
366
- number: z.number(),
367
- title: z.string(),
368
- })
369
- .nullable()
370
- .optional(),
371
- });
372
- /**
373
- * GitHub issue details schema (extended).
374
- */
375
- const GitHubIssueDetailsOutputSchema = GitHubIssueOutputSchema.extend({
376
- body: z.string().nullable(),
377
- labels: z.array(z.string()),
378
- assignees: z.array(z.string()),
379
- createdAt: z.string(),
380
- updatedAt: z.string(),
381
- closedAt: z.string().nullable(),
382
- commentsCount: z.number(),
383
- });
384
- /**
385
- * Schema for get_github_issues output.
386
- */
387
- const GitHubIssuesListOutputSchema = z.object({
388
- owner: z.string(),
389
- repo: z.string(),
390
- detectedOwner: z.string().nullable().optional(),
391
- detectedRepo: z.string().nullable().optional(),
392
- issues: z.array(GitHubIssueOutputSchema),
393
- count: z.number(),
394
- error: z.string().optional(),
395
- requiresUserInput: z.boolean().optional(),
396
- instruction: z.string().optional(),
397
- });
398
- /**
399
- * Schema for get_github_issue output.
400
- */
401
- const GitHubIssueResultOutputSchema = z.object({
402
- issue: GitHubIssueDetailsOutputSchema.optional(),
403
- owner: z.string().optional(),
404
- repo: z.string().optional(),
405
- detectedOwner: z.string().nullable().optional(),
406
- detectedRepo: z.string().nullable().optional(),
407
- error: z.string().optional(),
408
- requiresUserInput: z.boolean().optional(),
409
- instruction: z.string().optional(),
410
- });
411
- /**
412
- * GitHub pull request schema (mirrors GitHub API shape).
413
- */
414
- const GitHubPullRequestOutputSchema = z.object({
415
- number: z.number(),
416
- title: z.string(),
417
- url: z.string(),
418
- state: z.enum(['OPEN', 'CLOSED', 'MERGED']),
419
- });
420
- /**
421
- * GitHub PR details schema (extended).
422
- */
423
- const GitHubPRDetailsOutputSchema = GitHubPullRequestOutputSchema.extend({
424
- body: z.string().nullable(),
425
- draft: z.boolean(),
426
- headBranch: z.string(),
427
- baseBranch: z.string(),
428
- author: z.string(),
429
- createdAt: z.string(),
430
- updatedAt: z.string(),
431
- mergedAt: z.string().nullable(),
432
- closedAt: z.string().nullable(),
433
- additions: z.number(),
434
- deletions: z.number(),
435
- changedFiles: z.number(),
436
- });
437
- /**
438
- * Schema for get_github_prs output.
439
- */
440
- const GitHubPRsListOutputSchema = z.object({
441
- owner: z.string(),
442
- repo: z.string(),
443
- detectedOwner: z.string().nullable().optional(),
444
- detectedRepo: z.string().nullable().optional(),
445
- pullRequests: z.array(GitHubPullRequestOutputSchema),
446
- count: z.number(),
447
- error: z.string().optional(),
448
- requiresUserInput: z.boolean().optional(),
449
- instruction: z.string().optional(),
450
- });
451
- /**
452
- * Schema for get_github_pr output.
453
- */
454
- const GitHubPRResultOutputSchema = z.object({
455
- pullRequest: GitHubPRDetailsOutputSchema.optional(),
456
- owner: z.string().optional(),
457
- repo: z.string().optional(),
458
- detectedOwner: z.string().nullable().optional(),
459
- detectedRepo: z.string().nullable().optional(),
460
- error: z.string().optional(),
461
- requiresUserInput: z.boolean().optional(),
462
- instruction: z.string().optional(),
463
- });
464
- /**
465
- * Schema for get_github_context output.
466
- */
467
- const GitHubContextOutputSchema = z.object({
468
- repoName: z.string().nullable(),
469
- branch: z.string().nullable(),
470
- commit: z.string().nullable(),
471
- remoteUrl: z.string().nullable(),
472
- issues: z.array(GitHubIssueOutputSchema),
473
- pullRequests: z.array(GitHubPullRequestOutputSchema),
474
- issueCount: z.number(),
475
- prCount: z.number(),
476
- error: z.string().optional(),
477
- });
478
- /**
479
- * Kanban item schema.
480
- */
481
- const KanbanItemOutputSchema = z.object({
482
- id: z.string(),
483
- title: z.string(),
484
- url: z.string(),
485
- type: z.enum(['ISSUE', 'PULL_REQUEST', 'DRAFT_ISSUE']),
486
- status: z.string().nullable(),
487
- number: z.number().optional(),
488
- labels: z.array(z.string()).optional(),
489
- assignees: z.array(z.string()).optional(),
490
- createdAt: z.string(),
491
- updatedAt: z.string(),
492
- });
493
- /**
494
- * Status option schema.
495
- */
496
- const StatusOptionOutputSchema = z.object({
497
- id: z.string(),
498
- name: z.string(),
499
- color: z.string().optional(),
500
- });
501
- /**
502
- * Kanban column schema.
503
- */
504
- const KanbanColumnOutputSchema = z.object({
505
- status: z.string(),
506
- statusOptionId: z.string(),
507
- items: z.array(KanbanItemOutputSchema),
508
- });
509
- /**
510
- * Schema for get_kanban_board output.
511
- */
512
- const KanbanBoardOutputSchema = z.object({
513
- projectId: z.string(),
514
- projectNumber: z.number(),
515
- projectTitle: z.string(),
516
- statusFieldId: z.string(),
517
- statusOptions: z.array(StatusOptionOutputSchema),
518
- columns: z.array(KanbanColumnOutputSchema),
519
- totalItems: z.number(),
520
- owner: z.string().optional(),
521
- detectedOwner: z.string().nullable().optional(),
522
- detectedRepo: z.string().nullable().optional(),
523
- error: z.string().optional(),
524
- requiresUserInput: z.boolean().optional(),
525
- hint: z.string().optional(),
526
- instruction: z.string().optional(),
527
- });
528
- // ============================================================================
529
- // Phase 3b: Repository Insights Output Schema
530
- // ============================================================================
531
- /**
532
- * Schema for get_repo_insights output.
533
- * Uses optional sections to minimize token usage.
534
- */
535
- const RepoInsightsOutputSchema = z.object({
536
- owner: z.string().optional(),
537
- repo: z.string().optional(),
538
- section: z.string().optional(),
539
- stars: z.number().optional(),
540
- forks: z.number().optional(),
541
- watchers: z.number().optional(),
542
- openIssues: z.number().optional(),
543
- size: z.number().optional(),
544
- defaultBranch: z.string().optional(),
545
- traffic: z
546
- .object({
547
- clones: z.object({
548
- total: z.number(),
549
- unique: z.number(),
550
- dailyAvg: z.number(),
551
- }),
552
- views: z.object({
553
- total: z.number(),
554
- unique: z.number(),
555
- dailyAvg: z.number(),
556
- }),
557
- period: z.string(),
558
- })
559
- .optional(),
560
- referrers: z
561
- .array(z.object({
562
- referrer: z.string(),
563
- count: z.number(),
564
- uniques: z.number(),
565
- }))
566
- .optional(),
567
- paths: z
568
- .array(z.object({
569
- path: z.string(),
570
- title: z.string(),
571
- count: z.number(),
572
- uniques: z.number(),
573
- }))
574
- .optional(),
575
- error: z.string().optional(),
576
- requiresUserInput: z.boolean().optional(),
577
- instruction: z.string().optional(),
578
- });
579
- // ============================================================================
580
- // Phase 4: Backup Tool Output Schemas
581
- // ============================================================================
582
- /**
583
- * Schema for backup_journal output.
584
- */
585
- const BackupResultOutputSchema = z.object({
586
- success: z.boolean(),
587
- message: z.string(),
588
- filename: z.string(),
589
- path: z.string(),
590
- sizeBytes: z.number(),
591
- });
592
- /**
593
- * Backup info schema.
594
- */
595
- const BackupInfoOutputSchema = z.object({
596
- filename: z.string(),
597
- path: z.string(),
598
- sizeBytes: z.number(),
599
- createdAt: z.string(),
600
- });
601
- /**
602
- * Schema for list_backups output.
603
- */
604
- const BackupsListOutputSchema = z.object({
605
- backups: z.array(BackupInfoOutputSchema),
606
- total: z.number(),
607
- backupsDirectory: z.string(),
608
- hint: z.string().optional(),
609
- });
610
- /**
611
- * Schema for restore_backup output.
612
- */
613
- const RestoreResultOutputSchema = z.object({
614
- success: z.boolean(),
615
- message: z.string(),
616
- restoredFrom: z.string(),
617
- previousEntryCount: z.number(),
618
- newEntryCount: z.number(),
619
- warning: z.string().optional(),
620
- revertedChanges: z
621
- .object({
622
- tagMerges: z.string().optional(),
623
- entries: z.string().optional(),
624
- relationships: z.string().optional(),
625
- })
626
- .optional(),
627
- });
628
- // ============================================================================
629
- // Phase 5: Remaining Tool Output Schemas
630
- // ============================================================================
631
- /**
632
- * Schema for test_simple output.
633
- */
634
- const TestSimpleOutputSchema = z.object({
635
- message: z.string(),
636
- });
637
- /**
638
- * Schema for export_entries output.
639
- */
640
- const ExportEntriesOutputSchema = z.object({
641
- format: z.enum(['json', 'markdown']),
642
- entries: z.array(EntryOutputSchema).optional(), // For JSON format
643
- content: z.string().optional(), // For markdown format
644
- });
645
- /**
646
- * Schema for rebuild_vector_index output.
647
- */
648
- const RebuildVectorIndexOutputSchema = z.object({
649
- success: z.boolean(),
650
- entriesIndexed: z.number(),
651
- error: z.string().optional(),
652
- });
653
- /**
654
- * Schema for add_to_vector_index output.
655
- */
656
- const AddToVectorIndexOutputSchema = z.object({
657
- success: z.boolean(),
658
- entryId: z.number(),
659
- error: z.string().optional(),
660
- });
661
- /**
662
- * Schema for move_kanban_item output.
663
- */
664
- const MoveKanbanItemOutputSchema = z.object({
665
- success: z.boolean().optional(),
666
- itemId: z.string().optional(),
667
- newStatus: z.string().optional(),
668
- projectNumber: z.number().optional(),
669
- message: z.string().optional(),
670
- error: z.string().optional(),
671
- requiresUserInput: z.boolean().optional(),
672
- hint: z.string().optional(),
673
- });
674
- /**
675
- * Schema for create_github_issue_with_entry output.
676
- */
677
- const CreateGitHubIssueWithEntryOutputSchema = z.object({
678
- success: z.boolean().optional(),
679
- issue: z
680
- .object({
681
- number: z.number(),
682
- title: z.string(),
683
- url: z.string(),
684
- })
685
- .optional(),
686
- project: z
687
- .object({
688
- projectNumber: z.number(),
689
- added: z.boolean(),
690
- message: z.string(),
691
- initialStatus: z
692
- .object({
693
- status: z.string(),
694
- set: z.boolean(),
695
- })
696
- .optional(),
697
- })
698
- .optional(),
699
- journalEntry: z
700
- .object({
701
- id: z.number(),
702
- linkedToIssue: z.number(),
703
- })
704
- .optional(),
705
- message: z.string().optional(),
706
- error: z.string().optional(),
707
- requiresUserInput: z.boolean().optional(),
708
- instruction: z.string().optional(),
709
- });
710
- /**
711
- * Schema for close_github_issue_with_entry output.
712
- */
713
- const CloseGitHubIssueWithEntryOutputSchema = z.object({
714
- success: z.boolean().optional(),
715
- issue: z
716
- .object({
717
- number: z.number(),
718
- title: z.string(),
719
- url: z.string(),
720
- previousState: z.string(),
721
- newState: z.string(),
722
- })
723
- .optional(),
724
- journalEntry: z
725
- .object({
726
- id: z.number(),
727
- linkedToIssue: z.number(),
728
- significanceType: z.string(),
729
- })
730
- .optional(),
731
- kanban: z
732
- .object({
733
- moved: z.boolean(),
734
- projectNumber: z.number(),
735
- message: z.string().optional(),
736
- })
737
- .optional(),
738
- message: z.string().optional(),
739
- error: z.string().optional(),
740
- requiresUserInput: z.boolean().optional(),
741
- instruction: z.string().optional(),
742
- });
743
- // ============================================================================
744
- // GitHub Milestone Output Schemas
745
- // ============================================================================
746
- /**
747
- * GitHub milestone schema.
748
- */
749
- const GitHubMilestoneOutputSchema = z.object({
750
- number: z.number(),
751
- title: z.string(),
752
- description: z.string().nullable(),
753
- state: z.enum(['open', 'closed']),
754
- url: z.string(),
755
- dueOn: z.string().nullable(),
756
- openIssues: z.number(),
757
- closedIssues: z.number(),
758
- completionPercentage: z.number().optional(),
759
- createdAt: z.string(),
760
- updatedAt: z.string(),
761
- creator: z.string().nullable(),
762
- });
763
- /**
764
- * Schema for get_github_milestones output.
765
- */
766
- const GitHubMilestonesListOutputSchema = z.object({
767
- owner: z.string().optional(),
768
- repo: z.string().optional(),
769
- detectedOwner: z.string().nullable().optional(),
770
- detectedRepo: z.string().nullable().optional(),
771
- milestones: z.array(GitHubMilestoneOutputSchema).optional(),
772
- count: z.number().optional(),
773
- error: z.string().optional(),
774
- requiresUserInput: z.boolean().optional(),
775
- instruction: z.string().optional(),
776
- });
777
- /**
778
- * Schema for get_github_milestone output.
779
- */
780
- const GitHubMilestoneResultOutputSchema = z.object({
781
- milestone: GitHubMilestoneOutputSchema.optional(),
782
- owner: z.string().optional(),
783
- repo: z.string().optional(),
784
- detectedOwner: z.string().nullable().optional(),
785
- detectedRepo: z.string().nullable().optional(),
786
- error: z.string().optional(),
787
- requiresUserInput: z.boolean().optional(),
788
- instruction: z.string().optional(),
789
- });
790
- /**
791
- * Schema for create_github_milestone output.
792
- */
793
- const CreateMilestoneOutputSchema = z.object({
794
- success: z.boolean().optional(),
795
- milestone: GitHubMilestoneOutputSchema.optional(),
796
- message: z.string().optional(),
797
- error: z.string().optional(),
798
- requiresUserInput: z.boolean().optional(),
799
- instruction: z.string().optional(),
800
- });
801
- /**
802
- * Schema for update_github_milestone output.
803
- */
804
- const UpdateMilestoneOutputSchema = z.object({
805
- success: z.boolean().optional(),
806
- milestone: GitHubMilestoneOutputSchema.optional(),
807
- message: z.string().optional(),
808
- error: z.string().optional(),
809
- requiresUserInput: z.boolean().optional(),
810
- instruction: z.string().optional(),
811
- });
812
- /**
813
- * Schema for delete_github_milestone output.
814
- */
815
- const DeleteMilestoneOutputSchema = z.object({
816
- success: z.boolean().optional(),
817
- milestoneNumber: z.number().optional(),
818
- message: z.string().optional(),
819
- error: z.string().optional(),
820
- requiresUserInput: z.boolean().optional(),
821
- instruction: z.string().optional(),
822
- });
823
- /**
824
- * Schema for cleanup_backups output.
825
- */
826
- const CleanupBackupsOutputSchema = z.object({
827
- success: z.boolean(),
828
- deleted: z.array(z.string()),
829
- deletedCount: z.number(),
830
- keptCount: z.number(),
831
- message: z.string(),
832
- });
4
+ * Composes all tool group modules and exposes the public API:
5
+ * - getTools(): returns filtered tool definitions for the MCP server
6
+ * - callTool(): dispatches a tool call by name
7
+ *
8
+ * Each group module owns its:
9
+ * - Input/output Zod schemas
10
+ * - Tool definitions (name, description, group, annotations)
11
+ * - Handler implementations (with try/catch + formatHandlerError)
12
+ */
13
+ import { getCoreTools } from './core.js';
14
+ import { getSearchTools } from './search.js';
15
+ import { getAnalyticsTools } from './analytics.js';
16
+ import { getRelationshipTools } from './relationships.js';
17
+ import { getExportTools } from './export.js';
18
+ import { getAdminTools } from './admin.js';
19
+ import { getGitHubTools } from './github.js';
20
+ import { getBackupTools } from './backup.js';
21
+ import { getTeamTools } from './team.js';
22
+ // ============================================================================
23
+ // Icon Mapping
24
+ // ============================================================================
25
+ function getToolIcon(group) {
26
+ const iconMap = {
27
+ core: {
28
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/notebook.svg',
29
+ title: 'Journal Core',
30
+ description: 'Core journal operations',
31
+ },
32
+ search: {
33
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/magnify.svg',
34
+ title: 'Search',
35
+ description: 'Entry search operations',
36
+ },
37
+ analytics: {
38
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/chart-bar.svg',
39
+ title: 'Analytics',
40
+ description: 'Journal analytics',
41
+ },
42
+ relationships: {
43
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/graph-outline.svg',
44
+ title: 'Relationships',
45
+ description: 'Entry relationship management',
46
+ },
47
+ export: {
48
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/export.svg',
49
+ title: 'Export',
50
+ description: 'Data export operations',
51
+ },
52
+ admin: {
53
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/cog.svg',
54
+ title: 'Admin',
55
+ description: 'Administrative operations',
56
+ },
57
+ github: {
58
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/github.svg',
59
+ title: 'GitHub',
60
+ description: 'GitHub integration',
61
+ },
62
+ backup: {
63
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/backup-restore.svg',
64
+ title: 'Backup',
65
+ description: 'Backup and restore',
66
+ },
67
+ team: {
68
+ iconUrl: 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.4.47/svg/account-group.svg',
69
+ title: 'Team',
70
+ description: 'Team collaboration',
71
+ },
72
+ };
73
+ return iconMap[group];
74
+ }
833
75
  // ============================================================================
834
- // Tool Definitions with MCP 2025-11-25 Annotations
76
+ // Public API
835
77
  // ============================================================================
836
78
  /**
837
- * Get all tool definitions
79
+ * Get all tool definitions, optionally filtered by config
838
80
  */
839
- export function getTools(db, filterConfig, vectorManager, github, config) {
840
- const context = { db, vectorManager, github, config };
81
+ export function getTools(db, filterConfig, vectorManager, github, config, teamDb) {
82
+ const context = { db, teamDb, vectorManager, github, config };
841
83
  const allTools = getAllToolDefinitions(context);
842
- // Filter if config provided
843
- if (filterConfig) {
844
- return allTools
845
- .filter((t) => filterConfig.enabledTools.has(t.name))
846
- .map((t) => ({
847
- name: t.name,
848
- description: t.description,
849
- inputSchema: t.inputSchema,
850
- outputSchema: t.outputSchema, // MCP 2025-11-25
851
- annotations: t.annotations,
852
- icons: getToolIcon(t.group), // MCP 2025-11-25 icons
853
- }));
854
- }
855
- return allTools.map((t) => ({
84
+ const mapTool = (t) => ({
856
85
  name: t.name,
857
86
  description: t.description,
858
87
  inputSchema: t.inputSchema,
859
- outputSchema: t.outputSchema, // MCP 2025-11-25
88
+ outputSchema: t.outputSchema,
860
89
  annotations: t.annotations,
861
- icons: getToolIcon(t.group), // MCP 2025-11-25 icons
862
- }));
90
+ icons: getToolIcon(t.group),
91
+ });
92
+ if (filterConfig) {
93
+ return allTools.filter((t) => filterConfig.enabledTools.has(t.name)).map(mapTool);
94
+ }
95
+ return allTools.map(mapTool);
863
96
  }
97
+ /**
98
+ * Cached tool map for O(1) lookup in callTool.
99
+ * Built lazily on first callTool invocation. Invalidates when any
100
+ * context parameter changes (happens in tests with different mocks;
101
+ * in production, all instances are stable).
102
+ */
103
+ let toolMapCache = null;
104
+ let cachedContextRefs = null;
864
105
  /**
865
106
  * Call a tool by name
866
107
  */
867
- export async function callTool(name, args, db, vectorManager, github, config, progress) {
868
- const context = { db, vectorManager, github, config, progress };
869
- const tools = getAllToolDefinitions(context);
870
- const tool = tools.find((t) => t.name === name);
108
+ export function callTool(name, args, db, vectorManager, github, config, progress, teamDb) {
109
+ const context = { db, teamDb, vectorManager, github, config, progress };
110
+ // Build tool map cache on first invocation or when context changes
111
+ if (!toolMapCache ||
112
+ cachedContextRefs?.db !== db ||
113
+ cachedContextRefs.github !== github ||
114
+ cachedContextRefs.vectorManager !== vectorManager ||
115
+ cachedContextRefs.config !== config ||
116
+ cachedContextRefs.teamDb !== teamDb) {
117
+ toolMapCache = new Map(getAllToolDefinitions(context).map((t) => [t.name, t]));
118
+ cachedContextRefs = { db, github, vectorManager, config, teamDb };
119
+ }
120
+ const tool = toolMapCache.get(name);
871
121
  if (!tool) {
872
- throw new Error(`Unknown tool: ${name}`);
122
+ return Promise.reject(new Error(`Unknown tool: ${name}`));
873
123
  }
874
- return tool.handler(args);
124
+ return Promise.resolve(tool.handler(args));
875
125
  }
876
126
  /**
877
- * Get all tool definitions
127
+ * Compose all tool definitions from group modules
878
128
  */
879
129
  function getAllToolDefinitions(context) {
880
- const { db, vectorManager, github, progress } = context;
881
130
  return [
882
- // Core tools
883
- {
884
- name: 'create_entry',
885
- title: 'Create Journal Entry',
886
- description: 'Create a new journal entry with context and tags (v2.1.0: GitHub Actions support)',
887
- group: 'core',
888
- inputSchema: CreateEntrySchema,
889
- outputSchema: CreateEntryOutputSchema,
890
- annotations: { readOnlyHint: false, idempotentHint: false },
891
- handler: (params) => {
892
- const input = CreateEntrySchema.parse(params);
893
- // Auto-populate issueUrl if issue_number provided without issueUrl
894
- let resolvedIssueUrl = input.issue_url;
895
- if (input.issue_number !== undefined && !input.issue_url && github) {
896
- const cachedRepo = github.getCachedRepoInfo();
897
- if (cachedRepo?.owner && cachedRepo?.repo) {
898
- resolvedIssueUrl = `https://github.com/${cachedRepo.owner}/${cachedRepo.repo}/issues/${String(input.issue_number)}`;
899
- }
900
- }
901
- const entry = db.createEntry({
902
- content: input.content,
903
- entryType: input.entry_type,
904
- tags: input.tags,
905
- isPersonal: input.is_personal,
906
- significanceType: input.significance_type ?? null,
907
- projectNumber: input.project_number,
908
- projectOwner: input.project_owner,
909
- issueNumber: input.issue_number,
910
- issueUrl: resolvedIssueUrl,
911
- prNumber: input.pr_number,
912
- prUrl: input.pr_url,
913
- prStatus: input.pr_status,
914
- workflowRunId: input.workflow_run_id,
915
- workflowName: input.workflow_name,
916
- workflowStatus: input.workflow_status,
917
- });
918
- // Auto-index to vector store for semantic search (fire-and-forget)
919
- if (vectorManager) {
920
- vectorManager.addEntry(entry.id, entry.content).catch(() => {
921
- // Non-critical failure, entry already saved to DB
922
- });
923
- }
924
- return Promise.resolve({ success: true, entry });
925
- },
926
- },
927
- {
928
- name: 'get_entry_by_id',
929
- title: 'Get Entry by ID',
930
- description: 'Get a specific journal entry by ID with full details',
931
- group: 'core',
932
- inputSchema: GetEntryByIdSchema,
933
- outputSchema: EntryByIdOutputSchema, // MCP 2025-11-25: structured output
934
- annotations: { readOnlyHint: true, idempotentHint: true },
935
- handler: (params) => {
936
- const { entry_id, include_relationships } = GetEntryByIdSchema.parse(params);
937
- const entry = db.getEntryById(entry_id);
938
- if (!entry) {
939
- return Promise.resolve({ error: `Entry ${entry_id} not found` });
940
- }
941
- const { score: importance, breakdown: importanceBreakdown } = db.calculateImportance(entry_id);
942
- const result = {
943
- entry,
944
- importance,
945
- importanceBreakdown,
946
- };
947
- if (include_relationships) {
948
- result['relationships'] = db.getRelationships(entry_id);
949
- }
950
- return Promise.resolve(result);
951
- },
952
- },
953
- {
954
- name: 'get_recent_entries',
955
- title: 'Get Recent Entries',
956
- description: 'Get recent journal entries',
957
- group: 'core',
958
- inputSchema: GetRecentEntriesSchema,
959
- outputSchema: EntriesListOutputSchema, // MCP 2025-11-25: structured output
960
- annotations: { readOnlyHint: true, idempotentHint: true },
961
- handler: (params) => {
962
- const { limit, is_personal } = GetRecentEntriesSchema.parse(params);
963
- const entries = db.getRecentEntries(limit, is_personal);
964
- return Promise.resolve({ entries, count: entries.length });
965
- },
966
- },
967
- {
968
- name: 'create_entry_minimal',
969
- title: 'Create Entry (Minimal)',
970
- description: 'Minimal entry creation without context or tags',
971
- group: 'core',
972
- inputSchema: CreateEntryMinimalSchema,
973
- outputSchema: CreateEntryOutputSchema,
974
- annotations: { readOnlyHint: false, idempotentHint: false },
975
- handler: (params) => {
976
- const { content } = CreateEntryMinimalSchema.parse(params);
977
- const entry = db.createEntry({ content });
978
- // Auto-index to vector store for semantic search (fire-and-forget)
979
- if (vectorManager) {
980
- vectorManager.addEntry(entry.id, entry.content).catch(() => {
981
- // Non-critical failure, entry already saved to DB
982
- });
983
- }
984
- return Promise.resolve({ success: true, entry });
985
- },
986
- },
987
- {
988
- name: 'test_simple',
989
- title: 'Test Simple',
990
- description: 'Simple test tool that just returns a message',
991
- group: 'core',
992
- inputSchema: TestSimpleSchema,
993
- outputSchema: TestSimpleOutputSchema,
994
- annotations: { readOnlyHint: true, idempotentHint: true },
995
- handler: (params) => {
996
- const { message } = TestSimpleSchema.parse(params);
997
- return Promise.resolve({ message: `Test response: ${message}` });
998
- },
999
- },
1000
- // Search tools
1001
- {
1002
- name: 'search_entries',
1003
- title: 'Search Entries',
1004
- description: 'Search journal entries with optional filters for GitHub Projects, Issues, PRs, and Actions',
1005
- group: 'search',
1006
- inputSchema: SearchEntriesSchema,
1007
- outputSchema: EntriesListOutputSchema, // MCP 2025-11-25: structured output
1008
- annotations: { readOnlyHint: true, idempotentHint: true },
1009
- handler: (params) => {
1010
- const input = SearchEntriesSchema.parse(params);
1011
- // If no query and no filters, validation error usage of getRecentEntries
1012
- // But we want to allow filtering without text query
1013
- const hasFilters = input.project_number !== undefined ||
1014
- input.issue_number !== undefined ||
1015
- input.pr_number !== undefined ||
1016
- input.is_personal !== undefined;
1017
- if (!input.query && !hasFilters) {
1018
- const entries = db.getRecentEntries(input.limit, input.is_personal);
1019
- return Promise.resolve({ entries, count: entries.length });
1020
- }
1021
- const entries = db.searchEntries(input.query || '', {
1022
- limit: input.limit,
1023
- isPersonal: input.is_personal,
1024
- projectNumber: input.project_number,
1025
- issueNumber: input.issue_number,
1026
- prNumber: input.pr_number,
1027
- });
1028
- return Promise.resolve({ entries, count: entries.length });
1029
- },
1030
- },
1031
- {
1032
- name: 'search_by_date_range',
1033
- title: 'Search by Date Range',
1034
- description: 'Search journal entries within a date range with optional filters',
1035
- group: 'search',
1036
- inputSchema: SearchByDateRangeSchema,
1037
- outputSchema: EntriesListOutputSchema, // MCP 2025-11-25: structured output
1038
- annotations: { readOnlyHint: true, idempotentHint: true },
1039
- handler: (params) => {
1040
- const input = SearchByDateRangeSchema.parse(params);
1041
- const entries = db.searchByDateRange(input.start_date, input.end_date, {
1042
- entryType: input.entry_type,
1043
- tags: input.tags,
1044
- isPersonal: input.is_personal,
1045
- projectNumber: input.project_number,
1046
- });
1047
- return Promise.resolve({ entries, count: entries.length });
1048
- },
1049
- },
1050
- {
1051
- name: 'semantic_search',
1052
- title: 'Semantic Search',
1053
- description: 'Perform semantic/vector search on journal entries using AI embeddings',
1054
- group: 'search',
1055
- inputSchema: SemanticSearchSchema,
1056
- outputSchema: SemanticSearchOutputSchema,
1057
- annotations: { readOnlyHint: true, idempotentHint: true },
1058
- handler: async (params) => {
1059
- const input = SemanticSearchSchema.parse(params);
1060
- // Check if vector search is available
1061
- if (!vectorManager) {
1062
- return {
1063
- error: 'Semantic search not initialized. Vector search manager is not available.',
1064
- query: input.query,
1065
- entries: [],
1066
- count: 0,
1067
- };
1068
- }
1069
- // Perform semantic search
1070
- const results = await vectorManager.search(input.query, input.limit ?? 10, input.similarity_threshold ?? 0.25);
1071
- // Fetch full entries for matching IDs
1072
- const entries = results
1073
- .map((r) => {
1074
- const entry = db.getEntryById(r.entryId);
1075
- if (!entry)
1076
- return null;
1077
- return {
1078
- ...entry,
1079
- similarity: Math.round(r.score * 100) / 100,
1080
- };
1081
- })
1082
- .filter((e) => e !== null);
1083
- // Check index stats to provide accurate hint
1084
- const stats = await vectorManager.getStats();
1085
- const isIndexEmpty = stats.itemCount === 0;
1086
- // hint_on_empty controls whether to show helpful hints (default: true)
1087
- const includeHint = input.hint_on_empty ?? true;
1088
- return {
1089
- query: input.query,
1090
- entries,
1091
- count: entries.length,
1092
- ...(includeHint && isIndexEmpty
1093
- ? {
1094
- hint: 'No entries in vector index. Use rebuild_vector_index to index existing entries.',
1095
- }
1096
- : includeHint && entries.length === 0
1097
- ? {
1098
- hint: `No entries matched your query above the similarity threshold (${String(input.similarity_threshold ?? 0.25)}). Try lowering similarity_threshold (e.g., 0.15) for broader matches.`,
1099
- }
1100
- : {}),
1101
- };
1102
- },
1103
- },
1104
- // Analytics tools
1105
- {
1106
- name: 'get_statistics',
1107
- title: 'Get Statistics',
1108
- description: 'Get journal statistics and analytics (Phase 2: includes project breakdown)',
1109
- group: 'analytics',
1110
- inputSchema: GetStatisticsSchema,
1111
- outputSchema: StatisticsOutputSchema, // MCP 2025-11-25: structured output
1112
- annotations: { readOnlyHint: true, idempotentHint: true },
1113
- handler: (params) => {
1114
- const { group_by } = GetStatisticsSchema.parse(params);
1115
- const stats = db.getStatistics(group_by);
1116
- return Promise.resolve({ ...stats, groupBy: group_by });
1117
- },
1118
- },
1119
- {
1120
- name: 'get_cross_project_insights',
1121
- title: 'Get Cross-Project Insights',
1122
- description: 'Analyze patterns across all GitHub Projects tracked in journal entries',
1123
- group: 'analytics',
1124
- inputSchema: z.object({
1125
- start_date: z.string().optional().describe('Start date (YYYY-MM-DD)'),
1126
- end_date: z.string().optional().describe('End date (YYYY-MM-DD)'),
1127
- min_entries: z
1128
- .number()
1129
- .optional()
1130
- .default(3)
1131
- .describe('Minimum entries to include project'),
1132
- }),
1133
- outputSchema: CrossProjectInsightsOutputSchema,
1134
- annotations: { readOnlyHint: true, idempotentHint: true },
1135
- handler: (params) => {
1136
- const input = z
1137
- .object({
1138
- start_date: z.string().optional(),
1139
- end_date: z.string().optional(),
1140
- min_entries: z.number().optional().default(3),
1141
- })
1142
- .parse(params);
1143
- const rawDb = db.getRawDb();
1144
- // Build WHERE clause
1145
- let where = 'WHERE deleted_at IS NULL AND project_number IS NOT NULL';
1146
- const sqlParams = [];
1147
- if (input.start_date) {
1148
- where += ' AND DATE(timestamp) >= DATE(?)';
1149
- sqlParams.push(input.start_date);
1150
- }
1151
- if (input.end_date) {
1152
- where += ' AND DATE(timestamp) <= DATE(?)';
1153
- sqlParams.push(input.end_date);
1154
- }
1155
- // Get active projects with stats
1156
- const projectsResult = rawDb.exec(`
1157
- SELECT project_number, COUNT(*) as entry_count,
1158
- MIN(DATE(timestamp)) as first_entry,
1159
- MAX(DATE(timestamp)) as last_entry,
1160
- COUNT(DISTINCT DATE(timestamp)) as active_days
1161
- FROM memory_journal ${where}
1162
- GROUP BY project_number
1163
- HAVING entry_count >= ?
1164
- ORDER BY entry_count DESC
1165
- `, [...sqlParams, input.min_entries]);
1166
- if (!projectsResult[0] || projectsResult[0].values.length === 0) {
1167
- return Promise.resolve({
1168
- project_count: 0,
1169
- total_entries: 0,
1170
- projects: [],
1171
- inactive_projects: [],
1172
- inactiveThresholdDays: 7,
1173
- time_distribution: [],
1174
- message: `No projects found with at least ${String(input.min_entries)} entries`,
1175
- });
1176
- }
1177
- const columns = projectsResult[0].columns;
1178
- const projects = projectsResult[0].values.map((row) => {
1179
- const obj = {};
1180
- columns.forEach((col, i) => {
1181
- obj[col] = row[i];
1182
- });
1183
- return obj;
1184
- });
1185
- // Get top tags per project
1186
- const projectTags = {};
1187
- for (const proj of projects) {
1188
- const projNum = proj['project_number'];
1189
- const tagsResult = rawDb.exec(`
1190
- SELECT t.name, COUNT(*) as count
1191
- FROM tags t
1192
- JOIN entry_tags et ON t.id = et.tag_id
1193
- JOIN memory_journal m ON et.entry_id = m.id
1194
- WHERE m.project_number = ? AND m.deleted_at IS NULL
1195
- GROUP BY t.name
1196
- ORDER BY count DESC
1197
- LIMIT 5
1198
- `, [projNum]);
1199
- if (tagsResult[0]) {
1200
- projectTags[projNum] = tagsResult[0].values.map((row) => ({
1201
- name: row[0],
1202
- count: row[1],
1203
- }));
1204
- }
1205
- }
1206
- // Find inactive projects (last entry > 7 days ago)
1207
- const cutoffDate = new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0];
1208
- const inactiveResult = rawDb.exec(`
1209
- SELECT project_number, MAX(DATE(timestamp)) as last_entry_date
1210
- FROM memory_journal
1211
- WHERE deleted_at IS NULL AND project_number IS NOT NULL
1212
- GROUP BY project_number
1213
- HAVING last_entry_date < ?
1214
- `, [cutoffDate]);
1215
- const inactiveProjects = inactiveResult[0]?.values.map((row) => ({
1216
- project_number: row[0],
1217
- last_entry_date: row[1],
1218
- })) ?? [];
1219
- // Calculate time distribution
1220
- const totalEntries = projects.reduce((sum, p) => sum + p['entry_count'], 0);
1221
- const distribution = projects.slice(0, 5).map((p) => ({
1222
- project_number: p['project_number'],
1223
- percentage: ((p['entry_count'] / totalEntries) * 100).toFixed(1),
1224
- }));
1225
- return Promise.resolve({
1226
- project_count: projects.length,
1227
- total_entries: totalEntries,
1228
- projects: projects.map((p) => ({
1229
- ...p,
1230
- top_tags: projectTags[p['project_number']] ?? [],
1231
- })),
1232
- inactive_projects: inactiveProjects,
1233
- inactiveThresholdDays: 7,
1234
- time_distribution: distribution,
1235
- });
1236
- },
1237
- },
1238
- // Relationship tools
1239
- {
1240
- name: 'link_entries',
1241
- title: 'Link Entries',
1242
- description: 'Create a relationship between two journal entries',
1243
- group: 'relationships',
1244
- inputSchema: LinkEntriesSchema,
1245
- outputSchema: LinkEntriesOutputSchema,
1246
- annotations: { readOnlyHint: false, idempotentHint: false },
1247
- handler: (params) => {
1248
- const input = LinkEntriesSchema.parse(params);
1249
- // Check for existing duplicate relationship
1250
- const existingRelationships = db.getRelationships(input.from_entry_id);
1251
- const existing = existingRelationships.find((r) => r.toEntryId === input.to_entry_id &&
1252
- r.relationshipType === input.relationship_type);
1253
- if (existing) {
1254
- return Promise.resolve({
1255
- success: true,
1256
- relationship: existing,
1257
- duplicate: true,
1258
- message: 'Relationship already exists',
1259
- });
1260
- }
1261
- // P154: linkEntries throws for nonexistent entries
1262
- try {
1263
- const relationship = db.linkEntries(input.from_entry_id, input.to_entry_id, input.relationship_type, input.description);
1264
- return Promise.resolve({ success: true, relationship });
1265
- }
1266
- catch (error) {
1267
- return Promise.resolve({
1268
- success: false,
1269
- relationship: {
1270
- id: 0,
1271
- fromEntryId: input.from_entry_id,
1272
- toEntryId: input.to_entry_id,
1273
- relationshipType: input.relationship_type,
1274
- description: input.description ?? null,
1275
- createdAt: '',
1276
- },
1277
- message: error instanceof Error ? error.message : 'Unknown error',
1278
- });
1279
- }
1280
- },
1281
- },
1282
- {
1283
- name: 'visualize_relationships',
1284
- title: 'Visualize Relationships',
1285
- description: 'Generate a Mermaid diagram visualization of entry relationships',
1286
- group: 'relationships',
1287
- inputSchema: z.object({
1288
- entry_id: z
1289
- .number()
1290
- .optional()
1291
- .describe('Specific entry ID to visualize (shows connected entries)'),
1292
- tags: z.array(z.string()).optional().describe('Filter entries by tags'),
1293
- depth: z
1294
- .number()
1295
- .min(1)
1296
- .max(3)
1297
- .optional()
1298
- .default(2)
1299
- .describe('Relationship traversal depth'),
1300
- limit: z.number().optional().default(20).describe('Maximum entries to include'),
1301
- }),
1302
- outputSchema: VisualizationOutputSchema,
1303
- annotations: { readOnlyHint: true, idempotentHint: true },
1304
- handler: (params) => {
1305
- const input = z
1306
- .object({
1307
- entry_id: z.number().optional(),
1308
- tags: z.array(z.string()).optional(),
1309
- depth: z.number().optional().default(2),
1310
- limit: z.number().optional().default(20),
1311
- })
1312
- .parse(params);
1313
- const rawDb = db.getRawDb();
1314
- let entriesResult;
1315
- if (input.entry_id !== undefined) {
1316
- // P154: Pre-check entry existence to disambiguate responses
1317
- const entry = db.getEntryById(input.entry_id);
1318
- if (!entry) {
1319
- return Promise.resolve({
1320
- entry_count: 0,
1321
- relationship_count: 0,
1322
- root_entry: input.entry_id,
1323
- depth: input.depth,
1324
- mermaid: null,
1325
- message: `Entry ${String(input.entry_id)} not found`,
1326
- });
1327
- }
1328
- // Use recursive CTE to get connected entries up to depth
1329
- entriesResult = rawDb.exec(`
1330
- WITH RECURSIVE connected_entries(id, distance) AS (
1331
- SELECT id, 0 FROM memory_journal WHERE id = ? AND deleted_at IS NULL
1332
- UNION
1333
- SELECT DISTINCT
1334
- CASE
1335
- WHEN r.from_entry_id = ce.id THEN r.to_entry_id
1336
- ELSE r.from_entry_id
1337
- END,
1338
- ce.distance + 1
1339
- FROM connected_entries ce
1340
- JOIN relationships r ON r.from_entry_id = ce.id OR r.to_entry_id = ce.id
1341
- WHERE ce.distance < ?
1342
- )
1343
- SELECT DISTINCT mj.id, mj.entry_type, mj.content, mj.is_personal
1344
- FROM memory_journal mj
1345
- JOIN connected_entries ce ON mj.id = ce.id
1346
- WHERE mj.deleted_at IS NULL
1347
- LIMIT ?
1348
- `, [input.entry_id, input.depth, input.limit]);
1349
- }
1350
- else if (input.tags && input.tags.length > 0) {
1351
- // Filter by tags
1352
- const placeholders = input.tags.map(() => '?').join(',');
1353
- entriesResult = rawDb.exec(`
1354
- SELECT DISTINCT mj.id, mj.entry_type, mj.content, mj.is_personal
1355
- FROM memory_journal mj
1356
- WHERE mj.deleted_at IS NULL
1357
- AND mj.id IN (
1358
- SELECT et.entry_id FROM entry_tags et
1359
- JOIN tags t ON et.tag_id = t.id
1360
- WHERE t.name IN (${placeholders})
1361
- )
1362
- LIMIT ?
1363
- `, [...input.tags, input.limit]);
1364
- }
1365
- else {
1366
- // Get recent entries with relationships
1367
- entriesResult = rawDb.exec(`
1368
- SELECT DISTINCT mj.id, mj.entry_type, mj.content, mj.is_personal
1369
- FROM memory_journal mj
1370
- WHERE mj.deleted_at IS NULL
1371
- AND mj.id IN (
1372
- SELECT DISTINCT from_entry_id FROM relationships
1373
- UNION
1374
- SELECT DISTINCT to_entry_id FROM relationships
1375
- )
1376
- ORDER BY mj.id DESC
1377
- LIMIT ?
1378
- `, [input.limit]);
1379
- }
1380
- if (!entriesResult[0] || entriesResult[0].values.length === 0) {
1381
- return Promise.resolve({
1382
- entry_count: 0,
1383
- relationship_count: 0,
1384
- root_entry: input.entry_id ?? null,
1385
- depth: input.depth,
1386
- mermaid: null,
1387
- message: 'No entries found with relationships matching your criteria',
1388
- });
1389
- }
1390
- // Build entries map
1391
- const entries = {};
1392
- const cols = entriesResult[0].columns;
1393
- for (const row of entriesResult[0].values) {
1394
- const id = row[cols.indexOf('id')];
1395
- entries[id] = {
1396
- id,
1397
- entry_type: row[cols.indexOf('entry_type')],
1398
- content: row[cols.indexOf('content')],
1399
- is_personal: Boolean(row[cols.indexOf('is_personal')]),
1400
- };
1401
- }
1402
- const entryIds = Object.keys(entries).map(Number);
1403
- const placeholders = entryIds.map(() => '?').join(',');
1404
- // Get relationships between these entries
1405
- const relsResult = rawDb.exec(`
1406
- SELECT from_entry_id, to_entry_id, relationship_type
1407
- FROM relationships
1408
- WHERE from_entry_id IN (${placeholders})
1409
- AND to_entry_id IN (${placeholders})
1410
- `, [...entryIds, ...entryIds]);
1411
- const relationships = relsResult[0]?.values ?? [];
1412
- // Generate Mermaid diagram
1413
- let mermaid = '```mermaid\\ngraph TD\\n';
1414
- // Add nodes
1415
- for (const [idStr, entry] of Object.entries(entries)) {
1416
- let contentPreview = entry.content.slice(0, 40).replace(/\\n/g, ' ');
1417
- if (entry.content.length > 40)
1418
- contentPreview += '...';
1419
- // Escape for Mermaid
1420
- contentPreview = contentPreview
1421
- .replace(/"/g, "'")
1422
- .replace(/\\[/g, '(').replace(/\\]/g, ')');
1423
- const entryTypeShort = entry.entry_type.slice(0, 20);
1424
- mermaid += ` E${idStr}["#${idStr}: ${contentPreview}<br/>${entryTypeShort}"]\\n`;
1425
- }
1426
- mermaid += '\\n';
1427
- // Add relationships with arrows
1428
- const relSymbols = {
1429
- references: '-->',
1430
- implements: '==>',
1431
- clarifies: '-.->',
1432
- evolves_from: '-->',
1433
- response_to: '<-->',
1434
- // Causal relationship types
1435
- blocked_by: '--x',
1436
- resolved: '==>',
1437
- caused: '-.->',
1438
- };
1439
- for (const rel of relationships) {
1440
- const fromId = rel[0];
1441
- const toId = rel[1];
1442
- const relType = rel[2];
1443
- const arrow = relSymbols[relType] ?? '-->';
1444
- mermaid += ` E${String(fromId)} ${arrow}|${relType}| E${String(toId)}\\n`;
1445
- }
1446
- // Add styling
1447
- mermaid += '\\n';
1448
- for (const [idStr, entry] of Object.entries(entries)) {
1449
- if (entry.is_personal) {
1450
- mermaid += ` style E${idStr} fill:#E3F2FD\\n`;
1451
- }
1452
- else {
1453
- mermaid += ` style E${idStr} fill:#FFF3E0\\n`;
1454
- }
1455
- }
1456
- mermaid += '```';
1457
- return Promise.resolve({
1458
- entry_count: Object.keys(entries).length,
1459
- relationship_count: relationships.length,
1460
- root_entry: input.entry_id ?? null,
1461
- depth: input.depth,
1462
- mermaid,
1463
- legend: {
1464
- blue: 'Personal entries',
1465
- orange: 'Project entries',
1466
- arrows: {
1467
- '-->': 'references / evolves_from',
1468
- '==>': 'implements / resolved',
1469
- '-.->': 'clarifies / caused',
1470
- '<-->': 'response_to',
1471
- '--x': 'blocked_by',
1472
- },
1473
- },
1474
- });
1475
- },
1476
- },
1477
- // Export tools
1478
- {
1479
- name: 'export_entries',
1480
- title: 'Export Entries',
1481
- description: 'Export journal entries to JSON or Markdown format',
1482
- group: 'export',
1483
- inputSchema: ExportEntriesSchema,
1484
- outputSchema: ExportEntriesOutputSchema,
1485
- annotations: { readOnlyHint: true, idempotentHint: true },
1486
- handler: async (params) => {
1487
- const input = ExportEntriesSchema.parse(params);
1488
- const limit = input.limit ?? 100;
1489
- // Send initial progress
1490
- await sendProgress(progress, 0, 2, 'Fetching entries...');
1491
- const entries = db.getRecentEntries(limit);
1492
- // Send processing progress
1493
- await sendProgress(progress, 1, 2, `Processing ${String(entries.length)} entries...`);
1494
- if (input.format === 'markdown') {
1495
- const md = entries
1496
- .map((e) => `## ${e.timestamp}\n\n**Type:** ${e.entryType}\n\n${e.content}\n\n---`)
1497
- .join('\n\n');
1498
- await sendProgress(progress, 2, 2, 'Export complete');
1499
- return { format: 'markdown', content: md };
1500
- }
1501
- await sendProgress(progress, 2, 2, 'Export complete');
1502
- return { format: 'json', entries };
1503
- },
1504
- },
1505
- // Admin tools
1506
- {
1507
- name: 'update_entry',
1508
- title: 'Update Entry',
1509
- description: 'Update an existing journal entry',
1510
- group: 'admin',
1511
- inputSchema: UpdateEntrySchema,
1512
- outputSchema: UpdateEntryOutputSchema,
1513
- annotations: { readOnlyHint: false, idempotentHint: false },
1514
- handler: (params) => {
1515
- const input = UpdateEntrySchema.parse(params);
1516
- const entry = db.updateEntry(input.entry_id, {
1517
- content: input.content,
1518
- entryType: input.entry_type,
1519
- isPersonal: input.is_personal,
1520
- tags: input.tags,
1521
- });
1522
- if (!entry) {
1523
- return Promise.resolve({ error: `Entry ${input.entry_id} not found` });
1524
- }
1525
- // Re-index if content changed
1526
- if (input.content && vectorManager) {
1527
- vectorManager.addEntry(entry.id, entry.content).catch(() => {
1528
- // Non-critical failure, entry already updated in DB
1529
- });
1530
- }
1531
- return Promise.resolve({ success: true, entry });
1532
- },
1533
- },
1534
- {
1535
- name: 'delete_entry',
1536
- title: 'Delete Entry',
1537
- description: 'Delete a journal entry (soft delete with timestamp)',
1538
- group: 'admin',
1539
- inputSchema: DeleteEntrySchema,
1540
- outputSchema: DeleteEntryOutputSchema,
1541
- annotations: { readOnlyHint: false, destructiveHint: true },
1542
- handler: (params) => {
1543
- const { entry_id, permanent } = DeleteEntrySchema.parse(params);
1544
- const success = db.deleteEntry(entry_id, permanent);
1545
- // P154: Surface structured error for nonexistent entries
1546
- if (!success) {
1547
- return Promise.resolve({
1548
- success: false,
1549
- entryId: entry_id,
1550
- permanent,
1551
- error: `Entry ${String(entry_id)} not found`,
1552
- });
1553
- }
1554
- // Remove from vector index (non-critical if fails)
1555
- if (vectorManager) {
1556
- vectorManager.removeEntry(entry_id).catch(() => {
1557
- // Non-critical failure, entry already deleted from DB
1558
- });
1559
- }
1560
- return Promise.resolve({ success, entryId: entry_id, permanent });
1561
- },
1562
- },
1563
- // Utility tools
1564
- {
1565
- name: 'list_tags',
1566
- title: 'List Tags',
1567
- description: 'List all available tags',
1568
- group: 'core',
1569
- inputSchema: z.object({}),
1570
- outputSchema: TagsListOutputSchema,
1571
- annotations: { readOnlyHint: true, idempotentHint: true },
1572
- handler: (_params) => {
1573
- const rawTags = db.listTags();
1574
- const tags = rawTags.map((t) => ({ name: t.name, count: t.usageCount }));
1575
- return Promise.resolve({ tags, count: tags.length });
1576
- },
1577
- },
1578
- {
1579
- name: 'merge_tags',
1580
- title: 'Merge Tags',
1581
- description: 'Merge one tag into another to consolidate similar tags (e.g., merge "phase-2" into "phase2"). The source tag is deleted after merge.',
1582
- group: 'admin',
1583
- inputSchema: z.object({
1584
- source_tag: z.string().min(1).describe('Tag to merge from (will be deleted)'),
1585
- target_tag: z
1586
- .string()
1587
- .min(1)
1588
- .describe('Tag to merge into (will be created if not exists)'),
1589
- }),
1590
- outputSchema: MergeTagsOutputSchema,
1591
- annotations: { readOnlyHint: false, idempotentHint: false },
1592
- handler: (params) => {
1593
- const { source_tag, target_tag } = z
1594
- .object({
1595
- source_tag: z.string().min(1),
1596
- target_tag: z.string().min(1),
1597
- })
1598
- .parse(params);
1599
- if (source_tag === target_tag) {
1600
- return Promise.resolve({
1601
- success: false,
1602
- sourceTag: source_tag,
1603
- targetTag: target_tag,
1604
- entriesUpdated: 0,
1605
- sourceDeleted: false,
1606
- message: 'Source and target tags cannot be the same',
1607
- error: 'Source and target tags must be different',
1608
- });
1609
- }
1610
- try {
1611
- const result = db.mergeTags(source_tag, target_tag);
1612
- return Promise.resolve({
1613
- success: true,
1614
- sourceTag: source_tag,
1615
- targetTag: target_tag,
1616
- entriesUpdated: result.entriesUpdated,
1617
- sourceDeleted: result.sourceDeleted,
1618
- message: `Merged "${source_tag}" into "${target_tag}". Updated ${String(result.entriesUpdated)} entries.`,
1619
- });
1620
- }
1621
- catch (error) {
1622
- return Promise.resolve({
1623
- success: false,
1624
- sourceTag: source_tag,
1625
- targetTag: target_tag,
1626
- entriesUpdated: 0,
1627
- sourceDeleted: false,
1628
- message: 'Tag merge failed',
1629
- error: error instanceof Error ? error.message : 'Unknown error',
1630
- });
1631
- }
1632
- },
1633
- },
1634
- // Vector index management tools
1635
- {
1636
- name: 'rebuild_vector_index',
1637
- title: 'Rebuild Vector Index',
1638
- description: 'Rebuild the semantic search vector index from all existing entries',
1639
- group: 'admin',
1640
- inputSchema: z.object({}),
1641
- outputSchema: RebuildVectorIndexOutputSchema,
1642
- annotations: { readOnlyHint: false, idempotentHint: false },
1643
- handler: async (_params) => {
1644
- if (!vectorManager) {
1645
- return { error: 'Vector search not available' };
1646
- }
1647
- const indexed = await vectorManager.rebuildIndex(db, progress);
1648
- return { success: true, entriesIndexed: indexed };
1649
- },
1650
- },
1651
- {
1652
- name: 'add_to_vector_index',
1653
- title: 'Add Entry to Vector Index',
1654
- description: 'Add a specific entry to the semantic search vector index',
1655
- group: 'admin',
1656
- inputSchema: z.object({ entry_id: z.number() }),
1657
- outputSchema: AddToVectorIndexOutputSchema,
1658
- annotations: { readOnlyHint: false, idempotentHint: true },
1659
- handler: async (params) => {
1660
- const { entry_id } = z.object({ entry_id: z.number() }).parse(params);
1661
- if (!vectorManager) {
1662
- return { error: 'Vector search not available' };
1663
- }
1664
- const entry = db.getEntryById(entry_id);
1665
- if (!entry) {
1666
- return { error: `Entry ${String(entry_id)} not found` };
1667
- }
1668
- const success = await vectorManager.addEntry(entry_id, entry.content);
1669
- return { success, entryId: entry_id };
1670
- },
1671
- },
1672
- {
1673
- name: 'get_vector_index_stats',
1674
- title: 'Get Vector Index Stats',
1675
- description: 'Get statistics about the semantic search vector index',
1676
- group: 'search',
1677
- inputSchema: z.object({}),
1678
- outputSchema: VectorStatsOutputSchema,
1679
- annotations: { readOnlyHint: true, idempotentHint: true },
1680
- handler: async (_params) => {
1681
- if (!vectorManager) {
1682
- return { available: false, error: 'Vector search not available' };
1683
- }
1684
- const stats = await vectorManager.getStats();
1685
- return { available: true, ...stats };
1686
- },
1687
- },
1688
- // GitHub integration tools
1689
- {
1690
- name: 'get_github_issues',
1691
- title: 'Get GitHub Issues',
1692
- description: 'List issues from a GitHub repository. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
1693
- group: 'github',
1694
- inputSchema: z.object({
1695
- owner: z
1696
- .string()
1697
- .optional()
1698
- .describe('Repository owner - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
1699
- repo: z
1700
- .string()
1701
- .optional()
1702
- .describe('Repository name - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
1703
- state: z.enum(['open', 'closed', 'all']).optional().default('open'),
1704
- limit: z.number().optional().default(20),
1705
- }),
1706
- outputSchema: GitHubIssuesListOutputSchema,
1707
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1708
- handler: async (params) => {
1709
- const input = z
1710
- .object({
1711
- owner: z.string().optional(),
1712
- repo: z.string().optional(),
1713
- state: z.enum(['open', 'closed', 'all']).optional().default('open'),
1714
- limit: z.number().optional().default(20),
1715
- })
1716
- .parse(params);
1717
- if (!github) {
1718
- return { error: 'GitHub integration not available' };
1719
- }
1720
- // Get owner/repo from input or from current repo
1721
- const repoInfo = await github.getRepoInfo();
1722
- const detectedOwner = repoInfo.owner;
1723
- const detectedRepo = repoInfo.repo;
1724
- const owner = input.owner ?? detectedOwner ?? undefined;
1725
- const repo = input.repo ?? detectedRepo ?? undefined;
1726
- if (!owner || !repo) {
1727
- return {
1728
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
1729
- requiresUserInput: true,
1730
- detectedOwner,
1731
- detectedRepo,
1732
- instruction: 'Ask the user: "What GitHub repository would you like to query? Please provide the owner and repo name (e.g., owner/repo)."',
1733
- };
1734
- }
1735
- const issues = await github.getIssues(owner, repo, input.state, input.limit);
1736
- return { owner, repo, detectedOwner, detectedRepo, issues, count: issues.length };
1737
- },
1738
- },
1739
- {
1740
- name: 'get_github_prs',
1741
- title: 'Get GitHub Pull Requests',
1742
- description: 'List pull requests from a GitHub repository. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
1743
- group: 'github',
1744
- inputSchema: z.object({
1745
- owner: z
1746
- .string()
1747
- .optional()
1748
- .describe('Repository owner - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
1749
- repo: z
1750
- .string()
1751
- .optional()
1752
- .describe('Repository name - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
1753
- state: z.enum(['open', 'closed', 'all']).optional().default('open'),
1754
- limit: z.number().optional().default(20),
1755
- }),
1756
- outputSchema: GitHubPRsListOutputSchema,
1757
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1758
- handler: async (params) => {
1759
- const input = z
1760
- .object({
1761
- owner: z.string().optional(),
1762
- repo: z.string().optional(),
1763
- state: z.enum(['open', 'closed', 'all']).optional().default('open'),
1764
- limit: z.number().optional().default(20),
1765
- })
1766
- .parse(params);
1767
- if (!github) {
1768
- return { error: 'GitHub integration not available' };
1769
- }
1770
- const repoInfo = await github.getRepoInfo();
1771
- const detectedOwner = repoInfo.owner;
1772
- const detectedRepo = repoInfo.repo;
1773
- const owner = input.owner ?? detectedOwner ?? undefined;
1774
- const repo = input.repo ?? detectedRepo ?? undefined;
1775
- if (!owner || !repo) {
1776
- return {
1777
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
1778
- requiresUserInput: true,
1779
- detectedOwner,
1780
- detectedRepo,
1781
- instruction: 'Ask the user: "What GitHub repository would you like to query? Please provide the owner and repo name (e.g., owner/repo)."',
1782
- };
1783
- }
1784
- const pullRequests = await github.getPullRequests(owner, repo, input.state, input.limit);
1785
- return {
1786
- owner,
1787
- repo,
1788
- detectedOwner,
1789
- detectedRepo,
1790
- pullRequests,
1791
- count: pullRequests.length,
1792
- };
1793
- },
1794
- },
1795
- {
1796
- name: 'get_github_issue',
1797
- title: 'Get GitHub Issue Details',
1798
- description: 'Get detailed information about a specific GitHub issue. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
1799
- group: 'github',
1800
- inputSchema: z.object({
1801
- issue_number: z.number(),
1802
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
1803
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
1804
- }),
1805
- outputSchema: GitHubIssueResultOutputSchema,
1806
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1807
- handler: async (params) => {
1808
- const input = z
1809
- .object({
1810
- issue_number: z.number(),
1811
- owner: z.string().optional(),
1812
- repo: z.string().optional(),
1813
- })
1814
- .parse(params);
1815
- if (!github) {
1816
- return { error: 'GitHub integration not available' };
1817
- }
1818
- const repoInfo = await github.getRepoInfo();
1819
- const detectedOwner = repoInfo.owner;
1820
- const detectedRepo = repoInfo.repo;
1821
- const owner = input.owner ?? detectedOwner ?? undefined;
1822
- const repo = input.repo ?? detectedRepo ?? undefined;
1823
- if (!owner || !repo) {
1824
- return {
1825
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
1826
- requiresUserInput: true,
1827
- detectedOwner,
1828
- detectedRepo,
1829
- instruction: 'Ask the user: "What GitHub repository is this issue from? Please provide the owner and repo name (e.g., owner/repo)."',
1830
- };
1831
- }
1832
- const issue = await github.getIssue(owner, repo, input.issue_number);
1833
- if (!issue) {
1834
- return {
1835
- error: `Issue #${String(input.issue_number)} not found`,
1836
- owner,
1837
- repo,
1838
- detectedOwner,
1839
- detectedRepo,
1840
- };
1841
- }
1842
- return { issue, owner, repo, detectedOwner, detectedRepo };
1843
- },
1844
- },
1845
- {
1846
- name: 'get_github_pr',
1847
- title: 'Get GitHub PR Details',
1848
- description: 'Get detailed information about a specific GitHub pull request. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
1849
- group: 'github',
1850
- inputSchema: z.object({
1851
- pr_number: z.number(),
1852
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
1853
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
1854
- }),
1855
- outputSchema: GitHubPRResultOutputSchema,
1856
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1857
- handler: async (params) => {
1858
- const input = z
1859
- .object({
1860
- pr_number: z.number(),
1861
- owner: z.string().optional(),
1862
- repo: z.string().optional(),
1863
- })
1864
- .parse(params);
1865
- if (!github) {
1866
- return { error: 'GitHub integration not available' };
1867
- }
1868
- const repoInfo = await github.getRepoInfo();
1869
- const detectedOwner = repoInfo.owner;
1870
- const detectedRepo = repoInfo.repo;
1871
- const owner = input.owner ?? detectedOwner ?? undefined;
1872
- const repo = input.repo ?? detectedRepo ?? undefined;
1873
- if (!owner || !repo) {
1874
- return {
1875
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
1876
- requiresUserInput: true,
1877
- detectedOwner,
1878
- detectedRepo,
1879
- instruction: 'Ask the user: "What GitHub repository is this PR from? Please provide the owner and repo name (e.g., owner/repo)."',
1880
- };
1881
- }
1882
- const pullRequest = await github.getPullRequest(owner, repo, input.pr_number);
1883
- if (!pullRequest) {
1884
- return {
1885
- error: `PR #${String(input.pr_number)} not found`,
1886
- owner,
1887
- repo,
1888
- detectedOwner,
1889
- detectedRepo,
1890
- };
1891
- }
1892
- return { pullRequest, owner, repo, detectedOwner, detectedRepo };
1893
- },
1894
- },
1895
- {
1896
- name: 'get_github_context',
1897
- title: 'Get GitHub Repository Context',
1898
- description: 'Get current repository context including branch, open issues, and open PRs. Only counts OPEN items (closed items excluded).',
1899
- group: 'github',
1900
- inputSchema: z.object({}),
1901
- outputSchema: GitHubContextOutputSchema,
1902
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1903
- handler: async (_params) => {
1904
- if (!github) {
1905
- return { error: 'GitHub integration not available' };
1906
- }
1907
- const context = await github.getRepoContext();
1908
- return {
1909
- repoName: context.repoName,
1910
- branch: context.branch,
1911
- commit: context.commit,
1912
- remoteUrl: context.remoteUrl,
1913
- issues: context.issues,
1914
- pullRequests: context.pullRequests,
1915
- issueCount: context.issues.length,
1916
- prCount: context.pullRequests.length,
1917
- };
1918
- },
1919
- },
1920
- // Kanban tools (GitHub Projects v2)
1921
- {
1922
- name: 'get_kanban_board',
1923
- title: 'Get Kanban Board',
1924
- description: 'View a GitHub Project v2 as a Kanban board with items grouped by Status column. Returns all columns with their items.',
1925
- group: 'github',
1926
- inputSchema: z.object({
1927
- project_number: z.number().describe('GitHub Project number (from the project URL)'),
1928
- owner: z
1929
- .string()
1930
- .optional()
1931
- .describe('Repository owner - LEAVE EMPTY to auto-detect from git'),
1932
- }),
1933
- outputSchema: KanbanBoardOutputSchema,
1934
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1935
- handler: async (params) => {
1936
- const input = z
1937
- .object({
1938
- project_number: z.number(),
1939
- owner: z.string().optional(),
1940
- })
1941
- .parse(params);
1942
- if (!github) {
1943
- return { error: 'GitHub integration not available' };
1944
- }
1945
- // Get owner from input or from current repo
1946
- const repoInfo = await github.getRepoInfo();
1947
- const detectedOwner = repoInfo.owner;
1948
- const owner = input.owner ?? detectedOwner ?? undefined;
1949
- if (!owner) {
1950
- return {
1951
- error: 'STOP: Could not auto-detect repository owner. DO NOT GUESS. You MUST ask the user to provide the GitHub owner.',
1952
- requiresUserInput: true,
1953
- detectedOwner,
1954
- instruction: 'Ask the user: "What GitHub username or organization owns this project?"',
1955
- };
1956
- }
1957
- const repo = repoInfo.repo ?? undefined;
1958
- const board = await github.getProjectKanban(owner, input.project_number, repo);
1959
- if (!board) {
1960
- return {
1961
- error: `Project #${String(input.project_number)} not found or Status field not configured`,
1962
- owner,
1963
- repo,
1964
- hint: 'Ensure the project exists and has a "Status" single-select field. Projects can be at user, repository, or organization level.',
1965
- };
1966
- }
1967
- return {
1968
- ...board,
1969
- owner,
1970
- detectedOwner,
1971
- detectedRepo: repo,
1972
- };
1973
- },
1974
- },
1975
- {
1976
- name: 'move_kanban_item',
1977
- title: 'Move Kanban Item',
1978
- description: 'Move a project item to a different Status column. Use get_kanban_board first to get the item_id and exact status names. Status matching is case-insensitive.',
1979
- group: 'github',
1980
- inputSchema: z.object({
1981
- project_number: z.number().describe('GitHub Project number'),
1982
- item_id: z.string().describe('Project item node ID (from get_kanban_board)'),
1983
- target_status: z
1984
- .string()
1985
- .describe('Target status name (e.g., "Done", "In Progress")'),
1986
- owner: z
1987
- .string()
1988
- .optional()
1989
- .describe('Repository owner - LEAVE EMPTY to auto-detect'),
1990
- }),
1991
- outputSchema: MoveKanbanItemOutputSchema,
1992
- annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
1993
- handler: async (params) => {
1994
- const input = z
1995
- .object({
1996
- project_number: z.number(),
1997
- item_id: z.string(),
1998
- target_status: z.string(),
1999
- owner: z.string().optional(),
2000
- })
2001
- .parse(params);
2002
- if (!github) {
2003
- return { error: 'GitHub integration not available' };
2004
- }
2005
- // Get owner from input or from current repo
2006
- const repoInfo = await github.getRepoInfo();
2007
- const detectedOwner = repoInfo.owner;
2008
- const owner = input.owner ?? detectedOwner ?? undefined;
2009
- if (!owner) {
2010
- return {
2011
- error: 'STOP: Could not auto-detect repository owner. DO NOT GUESS.',
2012
- requiresUserInput: true,
2013
- };
2014
- }
2015
- // First, get the board to find projectId, statusFieldId, and target statusOptionId
2016
- const repo = repoInfo.repo ?? undefined;
2017
- const board = await github.getProjectKanban(owner, input.project_number, repo);
2018
- if (!board) {
2019
- return {
2020
- error: `Project #${String(input.project_number)} not found`,
2021
- };
2022
- }
2023
- // Find the target status option
2024
- const targetOption = board.statusOptions.find((opt) => opt.name.toLowerCase() === input.target_status.toLowerCase());
2025
- if (!targetOption) {
2026
- return {
2027
- error: `Status "${input.target_status}" not found in project`,
2028
- availableStatuses: board.statusOptions.map((opt) => opt.name),
2029
- hint: 'Use one of the available status names listed above.',
2030
- };
2031
- }
2032
- // Move the item
2033
- const result = await github.moveProjectItem(board.projectId, input.item_id, board.statusFieldId, targetOption.id);
2034
- if (!result.success) {
2035
- return {
2036
- success: false,
2037
- error: result.error,
2038
- targetStatus: input.target_status,
2039
- };
2040
- }
2041
- return {
2042
- success: true,
2043
- itemId: input.item_id,
2044
- newStatus: input.target_status,
2045
- projectNumber: input.project_number,
2046
- message: `Item moved to "${input.target_status}"`,
2047
- };
2048
- },
2049
- },
2050
- {
2051
- name: 'create_github_issue_with_entry',
2052
- title: 'Create GitHub Issue with Journal Entry',
2053
- description: 'Create a GitHub issue AND automatically create a linked journal entry documenting the issue creation.',
2054
- group: 'github',
2055
- inputSchema: z.object({
2056
- title: z.string().min(1).describe('Issue title'),
2057
- body: z.string().optional().describe('Issue body/description'),
2058
- labels: z.array(z.string()).optional().describe('Labels to apply'),
2059
- assignees: z.array(z.string()).optional().describe('Users to assign'),
2060
- milestone_number: z
2061
- .number()
2062
- .optional()
2063
- .describe('Milestone number to assign this issue to'),
2064
- project_number: z
2065
- .number()
2066
- .optional()
2067
- .describe('GitHub Project number to add this issue to'),
2068
- initial_status: z
2069
- .string()
2070
- .optional()
2071
- .describe('Initial status column (e.g., "Backlog", "Ready"). Defaults to "Backlog" when adding to a project.'),
2072
- owner: z
2073
- .string()
2074
- .optional()
2075
- .describe('Repository owner - LEAVE EMPTY to auto-detect'),
2076
- repo: z
2077
- .string()
2078
- .optional()
2079
- .describe('Repository name - LEAVE EMPTY to auto-detect'),
2080
- entry_content: z
2081
- .string()
2082
- .optional()
2083
- .describe('Custom journal content (defaults to auto-generated summary)'),
2084
- tags: z.array(z.string()).optional().describe('Journal entry tags'),
2085
- }),
2086
- outputSchema: CreateGitHubIssueWithEntryOutputSchema,
2087
- annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2088
- handler: async (params) => {
2089
- const input = z
2090
- .object({
2091
- title: z.string().min(1),
2092
- body: z.string().optional(),
2093
- labels: z.array(z.string()).optional(),
2094
- assignees: z.array(z.string()).optional(),
2095
- milestone_number: z.number().optional(),
2096
- project_number: z.number().optional(),
2097
- initial_status: z.string().optional(),
2098
- owner: z.string().optional(),
2099
- repo: z.string().optional(),
2100
- entry_content: z.string().optional(),
2101
- tags: z.array(z.string()).optional(),
2102
- })
2103
- .parse(params);
2104
- if (!github) {
2105
- return { error: 'GitHub integration not available' };
2106
- }
2107
- // Get owner/repo from input or from current repo
2108
- const repoInfo = await github.getRepoInfo();
2109
- const owner = input.owner ?? repoInfo.owner ?? undefined;
2110
- const repo = input.repo ?? repoInfo.repo ?? undefined;
2111
- if (!owner || !repo) {
2112
- return {
2113
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2114
- requiresUserInput: true,
2115
- detected: { owner, repo },
2116
- };
2117
- }
2118
- // Create the GitHub issue
2119
- const issue = await github.createIssue(owner, repo, input.title, input.body, input.labels, input.assignees, input.milestone_number);
2120
- if (!issue) {
2121
- return {
2122
- error: 'Failed to create GitHub issue. Check GITHUB_TOKEN permissions.',
2123
- };
2124
- }
2125
- const projectNumber = input.project_number ?? context.config?.defaultProjectNumber;
2126
- // Add to project if requested or default configured
2127
- let projectResult = undefined;
2128
- if (projectNumber !== undefined && issue.nodeId) {
2129
- try {
2130
- // Get project ID (needed for mutation)
2131
- const board = await github.getProjectKanban(owner, projectNumber, repo);
2132
- if (board) {
2133
- const added = await github.addProjectItem(board.projectId, issue.nodeId);
2134
- if (added.success) {
2135
- // Set initial status if provided
2136
- let statusResult = undefined;
2137
- // Default to "Backlog" when adding to project without explicit status
2138
- const initialStatus = input.initial_status ?? 'Backlog';
2139
- if (initialStatus && added.itemId) {
2140
- // Find the status option (case-insensitive)
2141
- const statusOption = board.statusOptions.find((opt) => opt.name.toLowerCase() === initialStatus.toLowerCase());
2142
- if (statusOption) {
2143
- const moveResult = await github.moveProjectItem(board.projectId, added.itemId, board.statusFieldId, statusOption.id);
2144
- if (moveResult.success) {
2145
- statusResult = { status: statusOption.name, set: true };
2146
- }
2147
- else {
2148
- statusResult = {
2149
- status: initialStatus,
2150
- set: false,
2151
- error: moveResult.error,
2152
- };
2153
- }
2154
- }
2155
- else {
2156
- statusResult = {
2157
- status: initialStatus,
2158
- set: false,
2159
- error: `Status "${initialStatus}" not found. Available: ${board.statusOptions.map((o) => o.name).join(', ')}`,
2160
- };
2161
- }
2162
- }
2163
- projectResult = {
2164
- projectNumber: projectNumber,
2165
- added: true,
2166
- message: `Added to project #${projectNumber}` +
2167
- (statusResult?.set ? ` (${statusResult.status})` : ''),
2168
- initialStatus: statusResult,
2169
- };
2170
- }
2171
- else {
2172
- projectResult = {
2173
- projectNumber: projectNumber,
2174
- added: false,
2175
- error: added.error,
2176
- };
2177
- }
2178
- }
2179
- else {
2180
- projectResult = {
2181
- projectNumber: projectNumber,
2182
- added: false,
2183
- error: `Project #${projectNumber} not found`,
2184
- };
2185
- }
2186
- }
2187
- catch (error) {
2188
- projectResult = {
2189
- projectNumber: projectNumber,
2190
- added: false,
2191
- error: error instanceof Error ? error.message : String(error),
2192
- };
2193
- }
2194
- }
2195
- // Create linked journal entry
2196
- const entryContent = input.entry_content ??
2197
- `Created GitHub issue #${String(issue.number)}: ${issue.title}\n\n` +
2198
- `URL: ${issue.url}\n` +
2199
- (projectNumber !== undefined ? `Project: #${projectNumber}\n` : '') +
2200
- (input.body
2201
- ? `\nDescription: ${input.body.slice(0, 200)}${input.body.length > 200 ? '...' : ''}`
2202
- : '');
2203
- const entry = db.createEntry({
2204
- content: entryContent,
2205
- entryType: 'planning',
2206
- tags: input.tags ?? ['github', 'issue-created'],
2207
- isPersonal: false,
2208
- significanceType: null,
2209
- issueNumber: issue.number,
2210
- issueUrl: issue.url,
2211
- projectNumber: projectNumber,
2212
- });
2213
- return {
2214
- success: true,
2215
- issue: {
2216
- number: issue.number,
2217
- title: issue.title,
2218
- url: issue.url,
2219
- },
2220
- project: projectResult,
2221
- journalEntry: {
2222
- id: entry.id,
2223
- linkedToIssue: issue.number,
2224
- },
2225
- message: `Created issue #${String(issue.number)}` +
2226
- (projectResult?.added ? ` (added to Project #${projectNumber})` : '') +
2227
- ` and journal entry #${String(entry.id)}`,
2228
- };
2229
- },
2230
- },
2231
- {
2232
- name: 'close_github_issue_with_entry',
2233
- title: 'Close GitHub Issue with Resolution Entry',
2234
- description: 'Close a GitHub issue AND create a journal entry documenting the resolution.',
2235
- group: 'github',
2236
- inputSchema: z.object({
2237
- issue_number: z.number().describe('Issue number to close'),
2238
- resolution_notes: z
2239
- .string()
2240
- .optional()
2241
- .describe('Notes about how the issue was resolved'),
2242
- comment: z
2243
- .string()
2244
- .optional()
2245
- .describe('Comment to add to the issue before closing'),
2246
- move_to_done: z
2247
- .boolean()
2248
- .optional()
2249
- .default(false)
2250
- .describe('Move the associated Kanban item to "Done" column'),
2251
- project_number: z
2252
- .number()
2253
- .optional()
2254
- .describe('GitHub Project number (required if move_to_done is true, or uses DEFAULT_PROJECT_NUMBER)'),
2255
- owner: z
2256
- .string()
2257
- .optional()
2258
- .describe('Repository owner - LEAVE EMPTY to auto-detect'),
2259
- repo: z
2260
- .string()
2261
- .optional()
2262
- .describe('Repository name - LEAVE EMPTY to auto-detect'),
2263
- tags: z.array(z.string()).optional().describe('Journal entry tags'),
2264
- }),
2265
- outputSchema: CloseGitHubIssueWithEntryOutputSchema,
2266
- annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2267
- handler: async (params) => {
2268
- const input = z
2269
- .object({
2270
- issue_number: z.number(),
2271
- resolution_notes: z.string().optional(),
2272
- comment: z.string().optional(),
2273
- move_to_done: z.boolean().optional().default(false),
2274
- project_number: z.number().optional(),
2275
- owner: z.string().optional(),
2276
- repo: z.string().optional(),
2277
- tags: z.array(z.string()).optional(),
2278
- })
2279
- .parse(params);
2280
- if (!github) {
2281
- return { error: 'GitHub integration not available' };
2282
- }
2283
- // Get owner/repo from input or from current repo
2284
- const repoInfo = await github.getRepoInfo();
2285
- const owner = input.owner ?? repoInfo.owner ?? undefined;
2286
- const repo = input.repo ?? repoInfo.repo ?? undefined;
2287
- if (!owner || !repo) {
2288
- return {
2289
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2290
- requiresUserInput: true,
2291
- detected: { owner, repo },
2292
- };
2293
- }
2294
- // Get issue details before closing
2295
- const issueDetails = await github.getIssue(owner, repo, input.issue_number);
2296
- if (!issueDetails) {
2297
- return { error: `Issue #${String(input.issue_number)} not found` };
2298
- }
2299
- if (issueDetails.state === 'CLOSED') {
2300
- return { error: `Issue #${String(input.issue_number)} is already closed` };
2301
- }
2302
- // Close the issue
2303
- const result = await github.closeIssue(owner, repo, input.issue_number, input.comment);
2304
- if (!result) {
2305
- return {
2306
- error: 'Failed to close GitHub issue. Check GITHUB_TOKEN permissions.',
2307
- };
2308
- }
2309
- // Move Kanban item to "Done" if requested
2310
- let kanbanResult;
2311
- if (input.move_to_done) {
2312
- const projectNum = input.project_number ?? context.config?.defaultProjectNumber;
2313
- if (projectNum === undefined) {
2314
- kanbanResult = {
2315
- moved: false,
2316
- error: 'project_number required when move_to_done is true',
2317
- };
2318
- }
2319
- else {
2320
- try {
2321
- const board = await github.getProjectKanban(owner, projectNum, repo);
2322
- if (!board) {
2323
- kanbanResult = {
2324
- moved: false,
2325
- error: `Project #${projectNum} not found`,
2326
- projectNumber: projectNum,
2327
- };
2328
- }
2329
- else {
2330
- // Find the item by issue number
2331
- const item = board.columns
2332
- .flatMap((c) => c.items)
2333
- .find((i) => i.type === 'ISSUE' && i.number === input.issue_number);
2334
- if (!item) {
2335
- kanbanResult = {
2336
- moved: false,
2337
- error: 'Issue not found on project board',
2338
- projectNumber: projectNum,
2339
- };
2340
- }
2341
- else {
2342
- const doneOption = board.statusOptions.find((opt) => opt.name.toLowerCase() === 'done');
2343
- if (!doneOption) {
2344
- kanbanResult = {
2345
- moved: false,
2346
- error: '"Done" status column not found on board',
2347
- projectNumber: projectNum,
2348
- };
2349
- }
2350
- else {
2351
- const moveResult = await github.moveProjectItem(board.projectId, item.id, board.statusFieldId, doneOption.id);
2352
- kanbanResult = {
2353
- moved: moveResult.success,
2354
- error: moveResult.error,
2355
- projectNumber: projectNum,
2356
- };
2357
- }
2358
- }
2359
- }
2360
- }
2361
- catch (err) {
2362
- kanbanResult = {
2363
- moved: false,
2364
- error: err instanceof Error ? err.message : String(err),
2365
- projectNumber: input.project_number ?? context.config?.defaultProjectNumber,
2366
- };
2367
- }
2368
- }
2369
- }
2370
- // Create resolution journal entry
2371
- const entryContent = `Closed GitHub issue #${String(input.issue_number)}: ${issueDetails.title}\n\n` +
2372
- `URL: ${issueDetails.url}\n` +
2373
- (input.resolution_notes ? `\nResolution: ${input.resolution_notes}` : '');
2374
- const entry = db.createEntry({
2375
- content: entryContent,
2376
- entryType: 'bug_fix',
2377
- tags: input.tags ?? ['github', 'issue-closed', 'resolution'],
2378
- isPersonal: false,
2379
- significanceType: 'blocker_resolved',
2380
- issueNumber: input.issue_number,
2381
- issueUrl: issueDetails.url,
2382
- });
2383
- return {
2384
- success: true,
2385
- issue: {
2386
- number: input.issue_number,
2387
- title: issueDetails.title,
2388
- url: result.url,
2389
- previousState: 'OPEN',
2390
- newState: 'CLOSED',
2391
- },
2392
- journalEntry: {
2393
- id: entry.id,
2394
- linkedToIssue: input.issue_number,
2395
- significanceType: 'blocker_resolved',
2396
- },
2397
- kanban: kanbanResult,
2398
- message: `Closed issue #${String(input.issue_number)} and created resolution entry #${String(entry.id)}` +
2399
- (kanbanResult?.moved ? ' and moved to Done' : ''),
2400
- };
2401
- },
2402
- },
2403
- // Milestone tools
2404
- {
2405
- name: 'get_github_milestones',
2406
- title: 'List GitHub Milestones',
2407
- description: 'List GitHub milestones for the repository with completion percentages and due dates.',
2408
- group: 'github',
2409
- inputSchema: z.object({
2410
- state: z
2411
- .enum(['open', 'closed', 'all'])
2412
- .optional()
2413
- .default('open')
2414
- .describe('Filter by state (default: open)'),
2415
- limit: z
2416
- .number()
2417
- .optional()
2418
- .default(20)
2419
- .describe('Max milestones to return (default: 20)'),
2420
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2421
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2422
- }),
2423
- outputSchema: GitHubMilestonesListOutputSchema,
2424
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2425
- handler: async (params) => {
2426
- const input = z
2427
- .object({
2428
- state: z.enum(['open', 'closed', 'all']).optional().default('open'),
2429
- limit: z.number().optional().default(20),
2430
- owner: z.string().optional(),
2431
- repo: z.string().optional(),
2432
- })
2433
- .parse(params);
2434
- if (!github) {
2435
- return { error: 'GitHub integration not available' };
2436
- }
2437
- const repoInfo = await github.getRepoInfo();
2438
- const detectedOwner = repoInfo.owner;
2439
- const detectedRepo = repoInfo.repo;
2440
- const owner = input.owner ?? detectedOwner ?? undefined;
2441
- const repo = input.repo ?? detectedRepo ?? undefined;
2442
- if (!owner || !repo) {
2443
- return {
2444
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2445
- requiresUserInput: true,
2446
- detectedOwner,
2447
- detectedRepo,
2448
- instruction: 'Ask the user: "What GitHub repository should I list milestones for? Please provide the owner and repo name (e.g., owner/repo)."',
2449
- };
2450
- }
2451
- const milestones = await github.getMilestones(owner, repo, input.state, input.limit);
2452
- const milestonesWithPercentage = milestones.map((ms) => {
2453
- const total = ms.openIssues + ms.closedIssues;
2454
- const completionPercentage = total > 0 ? Math.round((ms.closedIssues / total) * 100) : 0;
2455
- return { ...ms, completionPercentage };
2456
- });
2457
- return {
2458
- milestones: milestonesWithPercentage,
2459
- count: milestonesWithPercentage.length,
2460
- owner,
2461
- repo,
2462
- detectedOwner,
2463
- detectedRepo,
2464
- };
2465
- },
2466
- },
2467
- {
2468
- name: 'get_github_milestone',
2469
- title: 'Get GitHub Milestone Details',
2470
- description: 'Get detailed information about a specific GitHub milestone including progress and linked issue counts.',
2471
- group: 'github',
2472
- inputSchema: z.object({
2473
- milestone_number: z.number().describe('Milestone number'),
2474
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2475
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2476
- }),
2477
- outputSchema: GitHubMilestoneResultOutputSchema,
2478
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2479
- handler: async (params) => {
2480
- const input = z
2481
- .object({
2482
- milestone_number: z.number(),
2483
- owner: z.string().optional(),
2484
- repo: z.string().optional(),
2485
- })
2486
- .parse(params);
2487
- if (!github) {
2488
- return { error: 'GitHub integration not available' };
2489
- }
2490
- const repoInfo = await github.getRepoInfo();
2491
- const detectedOwner = repoInfo.owner;
2492
- const detectedRepo = repoInfo.repo;
2493
- const owner = input.owner ?? detectedOwner ?? undefined;
2494
- const repo = input.repo ?? detectedRepo ?? undefined;
2495
- if (!owner || !repo) {
2496
- return {
2497
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2498
- requiresUserInput: true,
2499
- detectedOwner,
2500
- detectedRepo,
2501
- instruction: 'Ask the user: "What GitHub repository is this milestone from? Please provide the owner and repo name (e.g., owner/repo)."',
2502
- };
2503
- }
2504
- const milestone = await github.getMilestone(owner, repo, input.milestone_number);
2505
- if (!milestone) {
2506
- return {
2507
- error: `Milestone #${String(input.milestone_number)} not found`,
2508
- owner,
2509
- repo,
2510
- detectedOwner,
2511
- detectedRepo,
2512
- };
2513
- }
2514
- const total = milestone.openIssues + milestone.closedIssues;
2515
- const completionPercentage = total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0;
2516
- return {
2517
- milestone: { ...milestone, completionPercentage },
2518
- owner,
2519
- repo,
2520
- detectedOwner,
2521
- detectedRepo,
2522
- };
2523
- },
2524
- },
2525
- {
2526
- name: 'create_github_milestone',
2527
- title: 'Create GitHub Milestone',
2528
- description: 'Create a new GitHub milestone for tracking progress toward a project goal.',
2529
- group: 'github',
2530
- inputSchema: z.object({
2531
- title: z.string().min(1).describe('Milestone title'),
2532
- description: z.string().optional().describe('Milestone description'),
2533
- due_on: z.string().optional().describe('Due date in YYYY-MM-DD format (optional)'),
2534
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2535
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2536
- }),
2537
- outputSchema: CreateMilestoneOutputSchema,
2538
- annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2539
- handler: async (params) => {
2540
- const input = z
2541
- .object({
2542
- title: z.string().min(1),
2543
- description: z.string().optional(),
2544
- due_on: z.string().optional(),
2545
- owner: z.string().optional(),
2546
- repo: z.string().optional(),
2547
- })
2548
- .parse(params);
2549
- if (!github) {
2550
- return { error: 'GitHub integration not available' };
2551
- }
2552
- const repoInfo = await github.getRepoInfo();
2553
- const owner = input.owner ?? repoInfo.owner ?? undefined;
2554
- const repo = input.repo ?? repoInfo.repo ?? undefined;
2555
- if (!owner || !repo) {
2556
- return {
2557
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2558
- requiresUserInput: true,
2559
- instruction: 'Ask the user: "What GitHub repository should I create the milestone in?"',
2560
- };
2561
- }
2562
- // Format due_on to ISO 8601 if provided (GitHub expects YYYY-MM-DDTHH:MM:SSZ)
2563
- const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : undefined;
2564
- const milestone = await github.createMilestone(owner, repo, input.title, input.description, dueOn);
2565
- if (!milestone) {
2566
- return {
2567
- error: 'Failed to create milestone. Check GITHUB_TOKEN permissions.',
2568
- };
2569
- }
2570
- return {
2571
- success: true,
2572
- milestone: { ...milestone, completionPercentage: 0 },
2573
- message: `Created milestone #${String(milestone.number)}: ${milestone.title}`,
2574
- };
2575
- },
2576
- },
2577
- {
2578
- name: 'update_github_milestone',
2579
- title: 'Update GitHub Milestone',
2580
- description: 'Update a GitHub milestone (title, description, due date, or state). Use state "closed" to close a completed milestone.',
2581
- group: 'github',
2582
- inputSchema: z.object({
2583
- milestone_number: z.number().describe('Milestone number to update'),
2584
- title: z.string().optional().describe('New title'),
2585
- description: z.string().optional().describe('New description'),
2586
- due_on: z.string().optional().describe('New due date in YYYY-MM-DD format'),
2587
- state: z
2588
- .enum(['open', 'closed'])
2589
- .optional()
2590
- .describe('Set to "closed" to close the milestone'),
2591
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2592
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2593
- }),
2594
- outputSchema: UpdateMilestoneOutputSchema,
2595
- annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2596
- handler: async (params) => {
2597
- const input = z
2598
- .object({
2599
- milestone_number: z.number(),
2600
- title: z.string().optional(),
2601
- description: z.string().optional(),
2602
- due_on: z.string().optional(),
2603
- state: z.enum(['open', 'closed']).optional(),
2604
- owner: z.string().optional(),
2605
- repo: z.string().optional(),
2606
- })
2607
- .parse(params);
2608
- if (!github) {
2609
- return { error: 'GitHub integration not available' };
2610
- }
2611
- const repoInfo = await github.getRepoInfo();
2612
- const owner = input.owner ?? repoInfo.owner ?? undefined;
2613
- const repo = input.repo ?? repoInfo.repo ?? undefined;
2614
- if (!owner || !repo) {
2615
- return {
2616
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2617
- requiresUserInput: true,
2618
- instruction: 'Ask the user: "What GitHub repository is this milestone in?"',
2619
- };
2620
- }
2621
- const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : undefined;
2622
- const milestone = await github.updateMilestone(owner, repo, input.milestone_number, {
2623
- title: input.title,
2624
- description: input.description,
2625
- dueOn,
2626
- state: input.state,
2627
- });
2628
- if (!milestone) {
2629
- return {
2630
- error: `Failed to update milestone #${String(input.milestone_number)}. Check that it exists and GITHUB_TOKEN has permissions.`,
2631
- };
2632
- }
2633
- const total = milestone.openIssues + milestone.closedIssues;
2634
- const completionPercentage = total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0;
2635
- return {
2636
- success: true,
2637
- milestone: { ...milestone, completionPercentage },
2638
- message: `Updated milestone #${String(milestone.number)}: ${milestone.title}`,
2639
- };
2640
- },
2641
- },
2642
- {
2643
- name: 'delete_github_milestone',
2644
- title: 'Delete GitHub Milestone',
2645
- description: 'Permanently delete a GitHub milestone. Issues assigned to the milestone will be un-assigned but not deleted.',
2646
- group: 'github',
2647
- inputSchema: z.object({
2648
- milestone_number: z.number().describe('Milestone number to delete'),
2649
- confirm: z.literal(true).describe('Must be set to true to confirm deletion'),
2650
- owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2651
- repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2652
- }),
2653
- outputSchema: DeleteMilestoneOutputSchema,
2654
- annotations: {
2655
- readOnlyHint: false,
2656
- idempotentHint: false,
2657
- destructiveHint: true,
2658
- openWorldHint: true,
2659
- },
2660
- handler: async (params) => {
2661
- const input = z
2662
- .object({
2663
- milestone_number: z.number(),
2664
- confirm: z.literal(true),
2665
- owner: z.string().optional(),
2666
- repo: z.string().optional(),
2667
- })
2668
- .parse(params);
2669
- if (!github) {
2670
- return { error: 'GitHub integration not available' };
2671
- }
2672
- const repoInfo = await github.getRepoInfo();
2673
- const owner = input.owner ?? repoInfo.owner ?? undefined;
2674
- const repo = input.repo ?? repoInfo.repo ?? undefined;
2675
- if (!owner || !repo) {
2676
- return {
2677
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2678
- requiresUserInput: true,
2679
- instruction: 'Ask the user: "What GitHub repository is this milestone in?"',
2680
- };
2681
- }
2682
- const result = await github.deleteMilestone(owner, repo, input.milestone_number);
2683
- if (!result.success) {
2684
- return {
2685
- success: false,
2686
- milestoneNumber: input.milestone_number,
2687
- message: `Failed to delete milestone #${String(input.milestone_number)}`,
2688
- error: result.error ?? undefined,
2689
- };
2690
- }
2691
- return {
2692
- success: true,
2693
- milestoneNumber: input.milestone_number,
2694
- message: `Deleted milestone #${String(input.milestone_number)}`,
2695
- };
2696
- },
2697
- },
2698
- // Repository insights tool
2699
- {
2700
- name: 'get_repo_insights',
2701
- title: 'Repository Insights',
2702
- description: 'Get repository insights: stars, forks, traffic (clones/views), referrers, and popular paths. Use "sections" to control token usage: stars (~50 tokens), traffic (~100), referrers (~100), paths (~100), or all (~350).',
2703
- group: 'github',
2704
- inputSchema: z.object({
2705
- sections: z
2706
- .enum(['stars', 'traffic', 'referrers', 'paths', 'all'])
2707
- .optional()
2708
- .default('stars')
2709
- .describe('Data section to return (default: stars). Use "all" for full payload.'),
2710
- owner: z
2711
- .string()
2712
- .optional()
2713
- .describe('Repository owner - LEAVE EMPTY to auto-detect'),
2714
- repo: z
2715
- .string()
2716
- .optional()
2717
- .describe('Repository name - LEAVE EMPTY to auto-detect'),
2718
- }),
2719
- outputSchema: RepoInsightsOutputSchema,
2720
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2721
- handler: async (params) => {
2722
- const input = z
2723
- .object({
2724
- sections: z
2725
- .enum(['stars', 'traffic', 'referrers', 'paths', 'all'])
2726
- .optional()
2727
- .default('stars'),
2728
- owner: z.string().optional(),
2729
- repo: z.string().optional(),
2730
- })
2731
- .parse(params);
2732
- if (!github) {
2733
- return { error: 'GitHub integration not available' };
2734
- }
2735
- const repoInfo = await github.getRepoInfo();
2736
- const owner = input.owner ?? repoInfo.owner ?? undefined;
2737
- const repo = input.repo ?? repoInfo.repo ?? undefined;
2738
- if (!owner || !repo) {
2739
- return {
2740
- error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2741
- requiresUserInput: true,
2742
- instruction: 'Ask the user: "What GitHub repository should I get insights for? Please provide the owner and repo name (e.g., owner/repo)."',
2743
- };
2744
- }
2745
- const section = input.sections;
2746
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- building response dynamically
2747
- const result = {
2748
- owner,
2749
- repo,
2750
- section,
2751
- };
2752
- // Stars section (default)
2753
- if (section === 'stars' || section === 'all') {
2754
- const stats = await github.getRepoStats(owner, repo);
2755
- if (stats) {
2756
- result['stars'] = stats.stars;
2757
- result['forks'] = stats.forks;
2758
- result['watchers'] = stats.watchers;
2759
- result['openIssues'] = stats.openIssues;
2760
- if (section === 'all') {
2761
- result['size'] = stats.size;
2762
- result['defaultBranch'] = stats.defaultBranch;
2763
- }
2764
- }
2765
- }
2766
- // Traffic section
2767
- if (section === 'traffic' || section === 'all') {
2768
- const traffic = await github.getTrafficData(owner, repo);
2769
- if (traffic) {
2770
- result['traffic'] = traffic;
2771
- }
2772
- }
2773
- // Referrers section
2774
- if (section === 'referrers' || section === 'all') {
2775
- const referrers = await github.getTopReferrers(owner, repo, 5);
2776
- result['referrers'] = referrers;
2777
- }
2778
- // Paths section
2779
- if (section === 'paths' || section === 'all') {
2780
- const paths = await github.getPopularPaths(owner, repo, 5);
2781
- result['paths'] = paths;
2782
- }
2783
- return result;
2784
- },
2785
- },
2786
- // Backup tools
2787
- {
2788
- name: 'backup_journal',
2789
- title: 'Backup Journal Database',
2790
- description: 'Create a timestamped backup of the journal database. Backups are stored in the backups/ directory.',
2791
- group: 'backup',
2792
- inputSchema: z.object({
2793
- name: z
2794
- .string()
2795
- .optional()
2796
- .describe('Custom backup name (optional, defaults to timestamp)'),
2797
- }),
2798
- outputSchema: BackupResultOutputSchema,
2799
- annotations: { readOnlyHint: false, idempotentHint: true },
2800
- handler: (params) => {
2801
- const input = z
2802
- .object({
2803
- name: z.string().optional(),
2804
- })
2805
- .parse(params);
2806
- const result = db.exportToFile(input.name);
2807
- return Promise.resolve({
2808
- success: true,
2809
- message: `Backup created successfully`,
2810
- filename: result.filename,
2811
- path: result.path,
2812
- sizeBytes: result.sizeBytes,
2813
- });
2814
- },
2815
- },
2816
- {
2817
- name: 'list_backups',
2818
- title: 'List Journal Backups',
2819
- description: 'List all available backup files with their sizes and creation dates',
2820
- group: 'backup',
2821
- inputSchema: z.object({}),
2822
- outputSchema: BackupsListOutputSchema,
2823
- annotations: { readOnlyHint: true, idempotentHint: true },
2824
- handler: (_params) => {
2825
- const backups = db.listBackups();
2826
- return Promise.resolve({
2827
- backups,
2828
- total: backups.length,
2829
- backupsDirectory: db.getBackupsDir(),
2830
- hint: backups.length === 0
2831
- ? 'No backups found. Use backup_journal to create one.'
2832
- : undefined,
2833
- });
2834
- },
2835
- },
2836
- {
2837
- name: 'restore_backup',
2838
- title: 'Restore Journal from Backup',
2839
- description: 'Restore the journal database from a backup file. WARNING: This replaces all current data. An automatic backup is created before restore.',
2840
- group: 'backup',
2841
- inputSchema: z.object({
2842
- filename: z
2843
- .string()
2844
- .describe('Backup filename to restore from (e.g., backup_2025-01-01.db)'),
2845
- confirm: z
2846
- .literal(true)
2847
- .describe('Must be set to true to confirm the restore operation'),
2848
- }),
2849
- outputSchema: RestoreResultOutputSchema,
2850
- annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true },
2851
- handler: async (params) => {
2852
- const input = z
2853
- .object({
2854
- filename: z.string(),
2855
- confirm: z.literal(true),
2856
- })
2857
- .parse(params);
2858
- // Capture progress context values BEFORE any async operations
2859
- // This prevents any possible reference corruption during db reinitialization
2860
- const progressServer = progress?.server;
2861
- const progressTokenValue = progress?.progressToken;
2862
- // Phase 1: Notify that we're starting
2863
- await sendProgress(progress, 1, 3, 'Preparing restore...');
2864
- // Phase 2: Restoring database (restoreFromFile creates backup internally)
2865
- await sendProgress(progress, 2, 3, 'Restoring database from backup...');
2866
- const result = await db.restoreFromFile(input.filename);
2867
- // Phase 3: Complete - send directly using captured primitives
2868
- // The db.restoreFromFile() reinitializes the database which can corrupt
2869
- // the progress context, so we use our captured values
2870
- if (progressServer !== undefined && progressTokenValue !== undefined) {
2871
- try {
2872
- await progressServer.notification({
2873
- method: 'notifications/progress',
2874
- params: {
2875
- progressToken: progressTokenValue,
2876
- progress: 3,
2877
- total: 3,
2878
- message: 'Restore complete',
2879
- },
2880
- });
2881
- }
2882
- catch {
2883
- // Best-effort notification
2884
- }
2885
- }
2886
- return {
2887
- success: true,
2888
- message: `Database restored from ${input.filename}`,
2889
- restoredFrom: result.restoredFrom,
2890
- previousEntryCount: result.previousEntryCount,
2891
- newEntryCount: result.newEntryCount,
2892
- warning: 'A pre-restore backup was automatically created. Any changes made since this backup (including tag merges, new entries, and relationships) have been reverted.',
2893
- revertedChanges: {
2894
- tagMerges: 'Any merge_tags operations since this backup are reverted. Previously merged tags will reappear as separate tags.',
2895
- entries: result.previousEntryCount !== result.newEntryCount
2896
- ? `Entry count changed from ${String(result.previousEntryCount)} to ${String(result.newEntryCount)}`
2897
- : undefined,
2898
- },
2899
- };
2900
- },
2901
- },
2902
- {
2903
- name: 'cleanup_backups',
2904
- title: 'Cleanup Old Backups',
2905
- description: 'Delete old backup files, keeping only the most recent N backups. Use list_backups to preview before cleanup.',
2906
- group: 'backup',
2907
- inputSchema: z.object({
2908
- keep_count: z
2909
- .number()
2910
- .min(1)
2911
- .default(5)
2912
- .describe('Number of most recent backups to keep (default: 5)'),
2913
- }),
2914
- outputSchema: CleanupBackupsOutputSchema,
2915
- annotations: { readOnlyHint: false, idempotentHint: false },
2916
- handler: (params) => {
2917
- const { keep_count } = z
2918
- .object({ keep_count: z.number().min(1).default(5) })
2919
- .parse(params);
2920
- const result = db.deleteOldBackups(keep_count);
2921
- return Promise.resolve({
2922
- success: true,
2923
- deleted: result.deleted,
2924
- deletedCount: result.deleted.length,
2925
- keptCount: result.kept,
2926
- message: result.deleted.length > 0
2927
- ? `Deleted ${String(result.deleted.length)} old backup(s), kept ${String(result.kept)}`
2928
- : `No backups to delete. Currently have ${String(result.kept)} backup(s).`,
2929
- });
2930
- },
2931
- },
131
+ ...getCoreTools(context),
132
+ ...getSearchTools(context),
133
+ ...getAnalyticsTools(context),
134
+ ...getRelationshipTools(context),
135
+ ...getExportTools(context),
136
+ ...getAdminTools(context),
137
+ ...getGitHubTools(context),
138
+ ...getBackupTools(context),
139
+ ...getTeamTools(context),
2932
140
  ];
2933
141
  }
2934
142
  //# sourceMappingURL=index.js.map