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