memory-journal-mcp 3.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 (107) hide show
  1. package/.dockerignore +88 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +76 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +89 -0
  5. package/.github/ISSUE_TEMPLATE/question.md +63 -0
  6. package/.github/dependabot.yml +110 -0
  7. package/.github/pull_request_template.md +110 -0
  8. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +346 -0
  9. package/.github/workflows/codeql.yml +45 -0
  10. package/.github/workflows/dependabot-auto-merge.yml +42 -0
  11. package/.github/workflows/docker-publish.yml +277 -0
  12. package/.github/workflows/lint-and-test.yml +58 -0
  13. package/.github/workflows/publish-npm.yml +75 -0
  14. package/.github/workflows/secrets-scanning.yml +32 -0
  15. package/.github/workflows/security-update.yml +99 -0
  16. package/.memory-journal-team.db +0 -0
  17. package/.trivyignore +18 -0
  18. package/CHANGELOG.md +19 -0
  19. package/CODE_OF_CONDUCT.md +128 -0
  20. package/CONTRIBUTING.md +209 -0
  21. package/DOCKER_README.md +377 -0
  22. package/Dockerfile +64 -0
  23. package/LICENSE +21 -0
  24. package/README.md +461 -0
  25. package/SECURITY.md +200 -0
  26. package/VERSION +1 -0
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +42 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/constants/ServerInstructions.d.ts +8 -0
  32. package/dist/constants/ServerInstructions.d.ts.map +1 -0
  33. package/dist/constants/ServerInstructions.js +26 -0
  34. package/dist/constants/ServerInstructions.js.map +1 -0
  35. package/dist/database/SqliteAdapter.d.ts +198 -0
  36. package/dist/database/SqliteAdapter.d.ts.map +1 -0
  37. package/dist/database/SqliteAdapter.js +736 -0
  38. package/dist/database/SqliteAdapter.js.map +1 -0
  39. package/dist/filtering/ToolFilter.d.ts +63 -0
  40. package/dist/filtering/ToolFilter.d.ts.map +1 -0
  41. package/dist/filtering/ToolFilter.js +242 -0
  42. package/dist/filtering/ToolFilter.js.map +1 -0
  43. package/dist/github/GitHubIntegration.d.ts +91 -0
  44. package/dist/github/GitHubIntegration.d.ts.map +1 -0
  45. package/dist/github/GitHubIntegration.js +317 -0
  46. package/dist/github/GitHubIntegration.js.map +1 -0
  47. package/dist/handlers/prompts/index.d.ts +28 -0
  48. package/dist/handlers/prompts/index.d.ts.map +1 -0
  49. package/dist/handlers/prompts/index.js +366 -0
  50. package/dist/handlers/prompts/index.js.map +1 -0
  51. package/dist/handlers/resources/index.d.ts +27 -0
  52. package/dist/handlers/resources/index.d.ts.map +1 -0
  53. package/dist/handlers/resources/index.js +453 -0
  54. package/dist/handlers/resources/index.js.map +1 -0
  55. package/dist/handlers/tools/index.d.ts +26 -0
  56. package/dist/handlers/tools/index.d.ts.map +1 -0
  57. package/dist/handlers/tools/index.js +982 -0
  58. package/dist/handlers/tools/index.js.map +1 -0
  59. package/dist/index.d.ts +11 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +13 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server/McpServer.d.ts +18 -0
  64. package/dist/server/McpServer.d.ts.map +1 -0
  65. package/dist/server/McpServer.js +171 -0
  66. package/dist/server/McpServer.js.map +1 -0
  67. package/dist/types/index.d.ts +300 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +15 -0
  70. package/dist/types/index.js.map +1 -0
  71. package/dist/utils/McpLogger.d.ts +61 -0
  72. package/dist/utils/McpLogger.d.ts.map +1 -0
  73. package/dist/utils/McpLogger.js +113 -0
  74. package/dist/utils/McpLogger.js.map +1 -0
  75. package/dist/utils/logger.d.ts +30 -0
  76. package/dist/utils/logger.d.ts.map +1 -0
  77. package/dist/utils/logger.js +70 -0
  78. package/dist/utils/logger.js.map +1 -0
  79. package/dist/vector/VectorSearchManager.d.ts +63 -0
  80. package/dist/vector/VectorSearchManager.d.ts.map +1 -0
  81. package/dist/vector/VectorSearchManager.js +235 -0
  82. package/dist/vector/VectorSearchManager.js.map +1 -0
  83. package/docker-compose.yml +37 -0
  84. package/eslint.config.js +86 -0
  85. package/mcp-config-example.json +21 -0
  86. package/package.json +71 -0
  87. package/releases/release-notes-v2.2.0.md +165 -0
  88. package/releases/release-notes.md +214 -0
  89. package/releases/v3.0.0.md +236 -0
  90. package/server.json +42 -0
  91. package/src/cli.ts +52 -0
  92. package/src/constants/ServerInstructions.ts +25 -0
  93. package/src/database/SqliteAdapter.ts +952 -0
  94. package/src/filtering/ToolFilter.ts +271 -0
  95. package/src/github/GitHubIntegration.ts +409 -0
  96. package/src/handlers/prompts/index.ts +420 -0
  97. package/src/handlers/resources/index.ts +529 -0
  98. package/src/handlers/tools/index.ts +1081 -0
  99. package/src/index.ts +53 -0
  100. package/src/server/McpServer.ts +230 -0
  101. package/src/types/index.ts +435 -0
  102. package/src/types/sql.js.d.ts +34 -0
  103. package/src/utils/McpLogger.ts +155 -0
  104. package/src/utils/logger.ts +98 -0
  105. package/src/vector/VectorSearchManager.ts +277 -0
  106. package/tools.json +300 -0
  107. package/tsconfig.json +51 -0
@@ -0,0 +1,1081 @@
1
+ /**
2
+ * Memory Journal MCP Server - Tool Handlers
3
+ *
4
+ * Exports all MCP tools with annotations following MCP 2025-11-25 spec.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import type { SqliteAdapter } from '../../database/SqliteAdapter.js';
9
+ import type { ToolFilterConfig } from '../../filtering/ToolFilter.js';
10
+ import type { ToolDefinition, EntryType, SignificanceType, RelationshipType } from '../../types/index.js';
11
+ import type { VectorSearchManager } from '../../vector/VectorSearchManager.js';
12
+ import type { GitHubIntegration } from '../../github/GitHubIntegration.js';
13
+
14
+ /**
15
+ * Tool execution context
16
+ */
17
+ export interface ToolContext {
18
+ db: SqliteAdapter;
19
+ vectorManager?: VectorSearchManager;
20
+ github?: GitHubIntegration;
21
+ }
22
+
23
+ // ============================================================================
24
+ // Zod Schemas for Input Validation
25
+ // ============================================================================
26
+
27
+ const CreateEntrySchema = z.object({
28
+ content: z.string().min(1).max(50000),
29
+ entry_type: z.string().optional().default('personal_reflection'),
30
+ tags: z.array(z.string()).optional().default([]),
31
+ is_personal: z.boolean().optional().default(true),
32
+ significance_type: z.string().optional(),
33
+ auto_context: z.boolean().optional().default(true),
34
+ project_number: z.number().optional(),
35
+ project_owner: z.string().optional(),
36
+ issue_number: z.number().optional(),
37
+ issue_url: z.string().optional(),
38
+ pr_number: z.number().optional(),
39
+ pr_url: z.string().optional(),
40
+ pr_status: z.enum(['draft', 'open', 'merged', 'closed']).optional(),
41
+ workflow_run_id: z.number().optional(),
42
+ workflow_name: z.string().optional(),
43
+ workflow_status: z.enum(['queued', 'in_progress', 'completed']).optional(),
44
+ share_with_team: z.boolean().optional().default(false),
45
+ });
46
+
47
+ const GetEntryByIdSchema = z.object({
48
+ entry_id: z.number(),
49
+ include_relationships: z.boolean().optional().default(true),
50
+ });
51
+
52
+ const GetRecentEntriesSchema = z.object({
53
+ limit: z.number().optional().default(5),
54
+ is_personal: z.boolean().optional(),
55
+ });
56
+
57
+ const CreateEntryMinimalSchema = z.object({
58
+ content: z.string().min(1).max(50000),
59
+ });
60
+
61
+ const TestSimpleSchema = z.object({
62
+ message: z.string().optional().default('Hello'),
63
+ });
64
+
65
+ const SearchEntriesSchema = z.object({
66
+ query: z.string().optional(),
67
+ limit: z.number().optional().default(10),
68
+ is_personal: z.boolean().optional(),
69
+ project_number: z.number().optional(),
70
+ issue_number: z.number().optional(),
71
+ pr_number: z.number().optional(),
72
+ pr_status: z.enum(['draft', 'open', 'merged', 'closed']).optional(),
73
+ workflow_run_id: z.number().optional(),
74
+ });
75
+
76
+ const SearchByDateRangeSchema = z.object({
77
+ start_date: z.string(),
78
+ end_date: z.string(),
79
+ entry_type: z.string().optional(),
80
+ tags: z.array(z.string()).optional(),
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
+ workflow_run_id: z.number().optional(),
86
+ });
87
+
88
+ const SemanticSearchSchema = z.object({
89
+ query: z.string(),
90
+ limit: z.number().optional().default(10),
91
+ similarity_threshold: z.number().optional().default(0.3),
92
+ is_personal: z.boolean().optional(),
93
+ });
94
+
95
+ const GetStatisticsSchema = z.object({
96
+ group_by: z.enum(['day', 'week', 'month']).optional().default('week'),
97
+ start_date: z.string().optional(),
98
+ end_date: z.string().optional(),
99
+ project_breakdown: z.boolean().optional().default(false),
100
+ });
101
+
102
+ const LinkEntriesSchema = z.object({
103
+ from_entry_id: z.number(),
104
+ to_entry_id: z.number(),
105
+ relationship_type: z.enum(['evolves_from', 'references', 'implements', 'clarifies', 'response_to']).optional().default('references'),
106
+ description: z.string().optional(),
107
+ });
108
+
109
+ const ExportEntriesSchema = z.object({
110
+ format: z.enum(['json', 'markdown']).optional().default('json'),
111
+ start_date: z.string().optional(),
112
+ end_date: z.string().optional(),
113
+ entry_types: z.array(z.string()).optional(),
114
+ tags: z.array(z.string()).optional(),
115
+ });
116
+
117
+ const UpdateEntrySchema = z.object({
118
+ entry_id: z.number(),
119
+ content: z.string().optional(),
120
+ entry_type: z.string().optional(),
121
+ is_personal: z.boolean().optional(),
122
+ tags: z.array(z.string()).optional(),
123
+ });
124
+
125
+ const DeleteEntrySchema = z.object({
126
+ entry_id: z.number(),
127
+ permanent: z.boolean().optional().default(false),
128
+ });
129
+
130
+ // ============================================================================
131
+ // Tool Definitions with MCP 2025-11-25 Annotations
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Get all tool definitions
136
+ */
137
+ export function getTools(db: SqliteAdapter, filterConfig: ToolFilterConfig | null, vectorManager?: VectorSearchManager, github?: GitHubIntegration): object[] {
138
+ const context: ToolContext = { db, vectorManager, github };
139
+ const allTools = getAllToolDefinitions(context);
140
+
141
+ // Filter if config provided
142
+ if (filterConfig) {
143
+ return allTools
144
+ .filter(t => filterConfig.enabledTools.has(t.name))
145
+ .map(t => ({
146
+ name: t.name,
147
+ description: t.description,
148
+ inputSchema: t.inputSchema,
149
+ annotations: t.annotations,
150
+ }));
151
+ }
152
+
153
+ return allTools.map(t => ({
154
+ name: t.name,
155
+ description: t.description,
156
+ inputSchema: t.inputSchema,
157
+ annotations: t.annotations,
158
+ }));
159
+ }
160
+
161
+ /**
162
+ * Call a tool by name
163
+ */
164
+ export async function callTool(
165
+ name: string,
166
+ args: Record<string, unknown>,
167
+ db: SqliteAdapter,
168
+ vectorManager?: VectorSearchManager,
169
+ github?: GitHubIntegration
170
+ ): Promise<unknown> {
171
+ const context: ToolContext = { db, vectorManager, github };
172
+ const tools = getAllToolDefinitions(context);
173
+ const tool = tools.find(t => t.name === name);
174
+
175
+ if (!tool) {
176
+ throw new Error(`Unknown tool: ${name}`);
177
+ }
178
+
179
+ return tool.handler(args);
180
+ }
181
+
182
+ /**
183
+ * Get all tool definitions
184
+ */
185
+ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
186
+ const { db, vectorManager, github } = context;
187
+ return [
188
+ // Core tools
189
+ {
190
+ name: 'create_entry',
191
+ title: 'Create Journal Entry',
192
+ description: 'Create a new journal entry with context and tags (v2.1.0: GitHub Actions support)',
193
+ group: 'core',
194
+ inputSchema: CreateEntrySchema,
195
+ annotations: { readOnlyHint: false, idempotentHint: false },
196
+ handler: (params: unknown) => {
197
+ const input = CreateEntrySchema.parse(params);
198
+ const entry = db.createEntry({
199
+ content: input.content,
200
+ entryType: input.entry_type as EntryType,
201
+ tags: input.tags,
202
+ isPersonal: input.is_personal,
203
+ significanceType: input.significance_type as SignificanceType ?? null,
204
+ projectNumber: input.project_number,
205
+ projectOwner: input.project_owner,
206
+ issueNumber: input.issue_number,
207
+ issueUrl: input.issue_url,
208
+ prNumber: input.pr_number,
209
+ prUrl: input.pr_url,
210
+ prStatus: input.pr_status,
211
+ workflowRunId: input.workflow_run_id,
212
+ workflowName: input.workflow_name,
213
+ workflowStatus: input.workflow_status,
214
+ });
215
+ return Promise.resolve({ success: true, entry });
216
+ },
217
+ },
218
+ {
219
+ name: 'get_entry_by_id',
220
+ title: 'Get Entry by ID',
221
+ description: 'Get a specific journal entry by ID with full details',
222
+ group: 'core',
223
+ inputSchema: GetEntryByIdSchema,
224
+ annotations: { readOnlyHint: true, idempotentHint: true },
225
+ handler: (params: unknown) => {
226
+ const { entry_id, include_relationships } = GetEntryByIdSchema.parse(params);
227
+ const entry = db.getEntryById(entry_id);
228
+ if (!entry) {
229
+ return Promise.resolve({ error: `Entry ${entry_id} not found` });
230
+ }
231
+ const result: Record<string, unknown> = { entry };
232
+ if (include_relationships) {
233
+ result['relationships'] = db.getRelationships(entry_id);
234
+ }
235
+ return Promise.resolve(result);
236
+ },
237
+ },
238
+ {
239
+ name: 'get_recent_entries',
240
+ title: 'Get Recent Entries',
241
+ description: 'Get recent journal entries',
242
+ group: 'core',
243
+ inputSchema: GetRecentEntriesSchema,
244
+ annotations: { readOnlyHint: true, idempotentHint: true },
245
+ handler: (params: unknown) => {
246
+ const { limit, is_personal } = GetRecentEntriesSchema.parse(params);
247
+ const entries = db.getRecentEntries(limit, is_personal);
248
+ return Promise.resolve({ entries, count: entries.length });
249
+ },
250
+ },
251
+ {
252
+ name: 'create_entry_minimal',
253
+ title: 'Create Entry (Minimal)',
254
+ description: 'Minimal entry creation without context or tags',
255
+ group: 'core',
256
+ inputSchema: CreateEntryMinimalSchema,
257
+ annotations: { readOnlyHint: false, idempotentHint: false },
258
+ handler: (params: unknown) => {
259
+ const { content } = CreateEntryMinimalSchema.parse(params);
260
+ const entry = db.createEntry({ content });
261
+ return Promise.resolve({ success: true, entry });
262
+ },
263
+ },
264
+ {
265
+ name: 'test_simple',
266
+ title: 'Test Simple',
267
+ description: 'Simple test tool that just returns a message',
268
+ group: 'core',
269
+ inputSchema: TestSimpleSchema,
270
+ annotations: { readOnlyHint: true, idempotentHint: true },
271
+ handler: (params: unknown) => {
272
+ const { message } = TestSimpleSchema.parse(params);
273
+ return Promise.resolve({ message: `Test response: ${message}` });
274
+ },
275
+ },
276
+ // Search tools
277
+ {
278
+ name: 'search_entries',
279
+ title: 'Search Entries',
280
+ description: 'Search journal entries with optional filters for GitHub Projects, Issues, PRs, and Actions',
281
+ group: 'search',
282
+ inputSchema: SearchEntriesSchema,
283
+ annotations: { readOnlyHint: true, idempotentHint: true },
284
+ handler: (params: unknown) => {
285
+ const input = SearchEntriesSchema.parse(params);
286
+ if (!input.query) {
287
+ const entries = db.getRecentEntries(input.limit, input.is_personal);
288
+ return Promise.resolve({ entries, count: entries.length });
289
+ }
290
+ const entries = db.searchEntries(input.query, {
291
+ limit: input.limit,
292
+ isPersonal: input.is_personal,
293
+ projectNumber: input.project_number,
294
+ issueNumber: input.issue_number,
295
+ prNumber: input.pr_number,
296
+ });
297
+ return Promise.resolve({ entries, count: entries.length });
298
+ },
299
+ },
300
+ {
301
+ name: 'search_by_date_range',
302
+ title: 'Search by Date Range',
303
+ description: 'Search journal entries within a date range with optional filters',
304
+ group: 'search',
305
+ inputSchema: SearchByDateRangeSchema,
306
+ annotations: { readOnlyHint: true, idempotentHint: true },
307
+ handler: (params: unknown) => {
308
+ const input = SearchByDateRangeSchema.parse(params);
309
+ const entries = db.searchByDateRange(input.start_date, input.end_date, {
310
+ entryType: input.entry_type as EntryType | undefined,
311
+ tags: input.tags,
312
+ isPersonal: input.is_personal,
313
+ projectNumber: input.project_number,
314
+ });
315
+ return Promise.resolve({ entries, count: entries.length });
316
+ },
317
+ },
318
+ {
319
+ name: 'semantic_search',
320
+ title: 'Semantic Search',
321
+ description: 'Perform semantic/vector search on journal entries using AI embeddings',
322
+ group: 'search',
323
+ inputSchema: SemanticSearchSchema,
324
+ annotations: { readOnlyHint: true, idempotentHint: true },
325
+ handler: async (params: unknown) => {
326
+ const input = SemanticSearchSchema.parse(params);
327
+
328
+ // Check if vector search is available
329
+ if (!vectorManager) {
330
+ return {
331
+ error: 'Semantic search not initialized. Vector search manager is not available.',
332
+ query: input.query,
333
+ entries: [],
334
+ count: 0
335
+ };
336
+ }
337
+
338
+ // Perform semantic search
339
+ const results = await vectorManager.search(
340
+ input.query,
341
+ input.limit ?? 10,
342
+ input.similarity_threshold ?? 0.3
343
+ );
344
+
345
+ // Fetch full entries for matching IDs
346
+ const entries = results
347
+ .map(r => {
348
+ const entry = db.getEntryById(r.entryId);
349
+ if (!entry) return null;
350
+ return {
351
+ ...entry,
352
+ similarity: Math.round(r.score * 100) / 100
353
+ };
354
+ })
355
+ .filter((e): e is NonNullable<typeof e> => e !== null);
356
+
357
+ return {
358
+ query: input.query,
359
+ entries,
360
+ count: entries.length,
361
+ ...(results.length === 0 ? { hint: 'No entries in vector index. Use rebuild_vector_index to index existing entries.' } : {})
362
+ };
363
+ },
364
+ },
365
+ // Analytics tools
366
+ {
367
+ name: 'get_statistics',
368
+ title: 'Get Statistics',
369
+ description: 'Get journal statistics and analytics (Phase 2: includes project breakdown)',
370
+ group: 'analytics',
371
+ inputSchema: GetStatisticsSchema,
372
+ annotations: { readOnlyHint: true, idempotentHint: true },
373
+ handler: (params: unknown) => {
374
+ const { group_by } = GetStatisticsSchema.parse(params);
375
+ return Promise.resolve(db.getStatistics(group_by));
376
+ },
377
+ },
378
+ {
379
+ name: 'get_cross_project_insights',
380
+ title: 'Get Cross-Project Insights',
381
+ description: 'Analyze patterns across all GitHub Projects tracked in journal entries',
382
+ group: 'analytics',
383
+ inputSchema: z.object({
384
+ start_date: z.string().optional().describe('Start date (YYYY-MM-DD)'),
385
+ end_date: z.string().optional().describe('End date (YYYY-MM-DD)'),
386
+ min_entries: z.number().optional().default(3).describe('Minimum entries to include project'),
387
+ }),
388
+ annotations: { readOnlyHint: true, idempotentHint: true },
389
+ handler: (params: unknown) => {
390
+ const input = z.object({
391
+ start_date: z.string().optional(),
392
+ end_date: z.string().optional(),
393
+ min_entries: z.number().optional().default(3),
394
+ }).parse(params);
395
+
396
+ const rawDb = db.getRawDb();
397
+
398
+ // Build WHERE clause
399
+ let where = 'WHERE deleted_at IS NULL AND project_number IS NOT NULL';
400
+ const sqlParams: unknown[] = [];
401
+
402
+ if (input.start_date) {
403
+ where += " AND DATE(timestamp) >= DATE(?)";
404
+ sqlParams.push(input.start_date);
405
+ }
406
+ if (input.end_date) {
407
+ where += " AND DATE(timestamp) <= DATE(?)";
408
+ sqlParams.push(input.end_date);
409
+ }
410
+
411
+ // Get active projects with stats
412
+ const projectsResult = rawDb.exec(`
413
+ SELECT project_number, COUNT(*) as entry_count,
414
+ MIN(DATE(timestamp)) as first_entry,
415
+ MAX(DATE(timestamp)) as last_entry,
416
+ COUNT(DISTINCT DATE(timestamp)) as active_days
417
+ FROM memory_journal ${where}
418
+ GROUP BY project_number
419
+ HAVING entry_count >= ?
420
+ ORDER BY entry_count DESC
421
+ `, [...sqlParams, input.min_entries]);
422
+
423
+ if (!projectsResult[0] || projectsResult[0].values.length === 0) {
424
+ return Promise.resolve({
425
+ message: `No projects found with at least ${String(input.min_entries)} entries`,
426
+ projects: [],
427
+ });
428
+ }
429
+
430
+ const columns = projectsResult[0].columns;
431
+ const projects = projectsResult[0].values.map(row => {
432
+ const obj: Record<string, unknown> = {};
433
+ columns.forEach((col, i) => { obj[col] = row[i]; });
434
+ return obj;
435
+ });
436
+
437
+ // Get top tags per project
438
+ const projectTags: Record<number, { name: string; count: number }[]> = {};
439
+ for (const proj of projects) {
440
+ const projNum = proj['project_number'] as number;
441
+ const tagsResult = rawDb.exec(`
442
+ SELECT t.name, COUNT(*) as count
443
+ FROM tags t
444
+ JOIN entry_tags et ON t.id = et.tag_id
445
+ JOIN memory_journal m ON et.entry_id = m.id
446
+ WHERE m.project_number = ? AND m.deleted_at IS NULL
447
+ GROUP BY t.name
448
+ ORDER BY count DESC
449
+ LIMIT 5
450
+ `, [projNum]);
451
+ if (tagsResult[0]) {
452
+ projectTags[projNum] = tagsResult[0].values.map(row => ({
453
+ name: row[0] as string,
454
+ count: row[1] as number,
455
+ }));
456
+ }
457
+ }
458
+
459
+ // Find inactive projects (last entry > 7 days ago)
460
+ const cutoffDate = new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0];
461
+ const inactiveResult = rawDb.exec(`
462
+ SELECT project_number, MAX(DATE(timestamp)) as last_entry_date
463
+ FROM memory_journal
464
+ WHERE deleted_at IS NULL AND project_number IS NOT NULL
465
+ GROUP BY project_number
466
+ HAVING last_entry_date < ?
467
+ `, [cutoffDate]);
468
+
469
+ const inactiveProjects = inactiveResult[0]?.values.map(row => ({
470
+ project_number: row[0] as number,
471
+ last_entry_date: row[1] as string,
472
+ })) ?? [];
473
+
474
+ // Calculate time distribution
475
+ const totalEntries = projects.reduce((sum, p) => sum + (p['entry_count'] as number), 0);
476
+ const distribution = projects.slice(0, 5).map(p => ({
477
+ project_number: p['project_number'],
478
+ percentage: (((p['entry_count'] as number) / totalEntries) * 100).toFixed(1),
479
+ }));
480
+
481
+ return Promise.resolve({
482
+ project_count: projects.length,
483
+ total_entries: totalEntries,
484
+ projects: projects.map(p => ({
485
+ ...p,
486
+ top_tags: projectTags[p['project_number'] as number] ?? [],
487
+ })),
488
+ inactive_projects: inactiveProjects,
489
+ time_distribution: distribution,
490
+ });
491
+ },
492
+ },
493
+ // Relationship tools
494
+ {
495
+ name: 'link_entries',
496
+ title: 'Link Entries',
497
+ description: 'Create a relationship between two journal entries',
498
+ group: 'relationships',
499
+ inputSchema: LinkEntriesSchema,
500
+ annotations: { readOnlyHint: false, idempotentHint: false },
501
+ handler: (params: unknown) => {
502
+ const input = LinkEntriesSchema.parse(params);
503
+ const relationship = db.linkEntries(
504
+ input.from_entry_id,
505
+ input.to_entry_id,
506
+ input.relationship_type as RelationshipType,
507
+ input.description
508
+ );
509
+ return Promise.resolve({ success: true, relationship });
510
+ },
511
+ },
512
+ {
513
+ name: 'visualize_relationships',
514
+ title: 'Visualize Relationships',
515
+ description: 'Generate a Mermaid diagram visualization of entry relationships',
516
+ group: 'relationships',
517
+ inputSchema: z.object({
518
+ entry_id: z.number().optional().describe('Specific entry ID to visualize (shows connected entries)'),
519
+ tags: z.array(z.string()).optional().describe('Filter entries by tags'),
520
+ depth: z.number().min(1).max(3).optional().default(2).describe('Relationship traversal depth'),
521
+ limit: z.number().optional().default(20).describe('Maximum entries to include'),
522
+ }),
523
+ annotations: { readOnlyHint: true, idempotentHint: true },
524
+ handler: (params: unknown) => {
525
+ const input = z.object({
526
+ entry_id: z.number().optional(),
527
+ tags: z.array(z.string()).optional(),
528
+ depth: z.number().optional().default(2),
529
+ limit: z.number().optional().default(20),
530
+ }).parse(params);
531
+
532
+ const rawDb = db.getRawDb();
533
+ let entriesResult;
534
+
535
+ if (input.entry_id !== undefined) {
536
+ // Use recursive CTE to get connected entries up to depth
537
+ entriesResult = rawDb.exec(`
538
+ WITH RECURSIVE connected_entries(id, distance) AS (
539
+ SELECT id, 0 FROM memory_journal WHERE id = ? AND deleted_at IS NULL
540
+ UNION
541
+ SELECT DISTINCT
542
+ CASE
543
+ WHEN r.from_entry_id = ce.id THEN r.to_entry_id
544
+ ELSE r.from_entry_id
545
+ END,
546
+ ce.distance + 1
547
+ FROM connected_entries ce
548
+ JOIN relationships r ON r.from_entry_id = ce.id OR r.to_entry_id = ce.id
549
+ WHERE ce.distance < ?
550
+ )
551
+ SELECT DISTINCT mj.id, mj.entry_type, mj.content, mj.is_personal
552
+ FROM memory_journal mj
553
+ JOIN connected_entries ce ON mj.id = ce.id
554
+ WHERE mj.deleted_at IS NULL
555
+ LIMIT ?
556
+ `, [input.entry_id, input.depth, input.limit]);
557
+ } else if (input.tags && input.tags.length > 0) {
558
+ // Filter by tags
559
+ const placeholders = input.tags.map(() => '?').join(',');
560
+ entriesResult = rawDb.exec(`
561
+ SELECT DISTINCT mj.id, mj.entry_type, mj.content, mj.is_personal
562
+ FROM memory_journal mj
563
+ WHERE mj.deleted_at IS NULL
564
+ AND mj.id IN (
565
+ SELECT et.entry_id FROM entry_tags et
566
+ JOIN tags t ON et.tag_id = t.id
567
+ WHERE t.name IN (${placeholders})
568
+ )
569
+ LIMIT ?
570
+ `, [...input.tags, input.limit]);
571
+ } else {
572
+ // Get recent entries with relationships
573
+ entriesResult = rawDb.exec(`
574
+ SELECT DISTINCT mj.id, mj.entry_type, mj.content, mj.is_personal
575
+ FROM memory_journal mj
576
+ WHERE mj.deleted_at IS NULL
577
+ AND mj.id IN (
578
+ SELECT DISTINCT from_entry_id FROM relationships
579
+ UNION
580
+ SELECT DISTINCT to_entry_id FROM relationships
581
+ )
582
+ ORDER BY mj.id DESC
583
+ LIMIT ?
584
+ `, [input.limit]);
585
+ }
586
+
587
+ if (!entriesResult[0] || entriesResult[0].values.length === 0) {
588
+ return Promise.resolve({
589
+ message: 'No entries found with relationships matching your criteria',
590
+ mermaid: null,
591
+ });
592
+ }
593
+
594
+ // Build entries map
595
+ const entries: Record<number, { id: number; entry_type: string; content: string; is_personal: boolean }> = {};
596
+ const cols = entriesResult[0].columns;
597
+ for (const row of entriesResult[0].values) {
598
+ const id = row[cols.indexOf('id')] as number;
599
+ entries[id] = {
600
+ id,
601
+ entry_type: row[cols.indexOf('entry_type')] as string,
602
+ content: row[cols.indexOf('content')] as string,
603
+ is_personal: Boolean(row[cols.indexOf('is_personal')]),
604
+ };
605
+ }
606
+
607
+ const entryIds = Object.keys(entries).map(Number);
608
+ const placeholders = entryIds.map(() => '?').join(',');
609
+
610
+ // Get relationships between these entries
611
+ const relsResult = rawDb.exec(`
612
+ SELECT from_entry_id, to_entry_id, relationship_type
613
+ FROM relationships
614
+ WHERE from_entry_id IN (${placeholders})
615
+ AND to_entry_id IN (${placeholders})
616
+ `, [...entryIds, ...entryIds]);
617
+
618
+ const relationships = relsResult[0]?.values ?? [];
619
+
620
+ // Generate Mermaid diagram
621
+ let mermaid = '```mermaid\\ngraph TD\\n';
622
+
623
+ // Add nodes
624
+ for (const [idStr, entry] of Object.entries(entries)) {
625
+ let contentPreview = entry.content.slice(0, 40).replace(/\\n/g, ' ');
626
+ if (entry.content.length > 40) contentPreview += '...';
627
+ // Escape for Mermaid
628
+ contentPreview = contentPreview.replace(/"/g, "'").replace(/\\[/g, '(').replace(/\\]/g, ')');
629
+ const entryTypeShort = entry.entry_type.slice(0, 20);
630
+ mermaid += ` E${idStr}["#${idStr}: ${contentPreview}<br/>${entryTypeShort}"]\\n`;
631
+ }
632
+
633
+ mermaid += '\\n';
634
+
635
+ // Add relationships with arrows
636
+ const relSymbols: Record<string, string> = {
637
+ 'references': '-->',
638
+ 'implements': '==>',
639
+ 'clarifies': '-.->',
640
+ 'evolves_from': '-->',
641
+ 'response_to': '<-->',
642
+ };
643
+
644
+ for (const rel of relationships) {
645
+ const fromId = rel[0] as number;
646
+ const toId = rel[1] as number;
647
+ const relType = rel[2] as string;
648
+ const arrow = relSymbols[relType] ?? '-->';
649
+ mermaid += ` E${String(fromId)} ${arrow}|${relType}| E${String(toId)}\\n`;
650
+ }
651
+
652
+ // Add styling
653
+ mermaid += '\\n';
654
+ for (const [idStr, entry] of Object.entries(entries)) {
655
+ if (entry.is_personal) {
656
+ mermaid += ` style E${idStr} fill:#E3F2FD\\n`;
657
+ } else {
658
+ mermaid += ` style E${idStr} fill:#FFF3E0\\n`;
659
+ }
660
+ }
661
+ mermaid += '```';
662
+
663
+ return Promise.resolve({
664
+ entry_count: Object.keys(entries).length,
665
+ relationship_count: relationships.length,
666
+ root_entry: input.entry_id ?? null,
667
+ depth: input.depth,
668
+ mermaid,
669
+ legend: {
670
+ blue: 'Personal entries',
671
+ orange: 'Project entries',
672
+ arrows: {
673
+ '-->': 'references / evolves_from',
674
+ '==>': 'implements',
675
+ '-.->': 'clarifies',
676
+ '<-->': 'response_to',
677
+ },
678
+ },
679
+ });
680
+ },
681
+ },
682
+ // Export tools
683
+ {
684
+ name: 'export_entries',
685
+ title: 'Export Entries',
686
+ description: 'Export journal entries to JSON or Markdown format',
687
+ group: 'export',
688
+ inputSchema: ExportEntriesSchema,
689
+ annotations: { readOnlyHint: true, idempotentHint: true },
690
+ handler: (params: unknown) => {
691
+ const input = ExportEntriesSchema.parse(params);
692
+ const entries = db.getRecentEntries(100);
693
+ if (input.format === 'markdown') {
694
+ const md = entries.map(e =>
695
+ `## ${e.timestamp}\n\n**Type:** ${e.entryType}\n\n${e.content}\n\n---`
696
+ ).join('\n\n');
697
+ return Promise.resolve({ format: 'markdown', content: md });
698
+ }
699
+ return Promise.resolve({ format: 'json', entries });
700
+ },
701
+ },
702
+ // Admin tools
703
+ {
704
+ name: 'update_entry',
705
+ title: 'Update Entry',
706
+ description: 'Update an existing journal entry',
707
+ group: 'admin',
708
+ inputSchema: UpdateEntrySchema,
709
+ annotations: { readOnlyHint: false, idempotentHint: false },
710
+ handler: (params: unknown) => {
711
+ const input = UpdateEntrySchema.parse(params);
712
+ const entry = db.updateEntry(input.entry_id, {
713
+ content: input.content,
714
+ entryType: input.entry_type as EntryType | undefined,
715
+ isPersonal: input.is_personal,
716
+ tags: input.tags,
717
+ });
718
+ if (!entry) {
719
+ return Promise.resolve({ error: `Entry ${input.entry_id} not found` });
720
+ }
721
+ return Promise.resolve({ success: true, entry });
722
+ },
723
+ },
724
+ {
725
+ name: 'delete_entry',
726
+ title: 'Delete Entry',
727
+ description: 'Delete a journal entry (soft delete with timestamp)',
728
+ group: 'admin',
729
+ inputSchema: DeleteEntrySchema,
730
+ annotations: { readOnlyHint: false, destructiveHint: true },
731
+ handler: (params: unknown) => {
732
+ const { entry_id, permanent } = DeleteEntrySchema.parse(params);
733
+ const success = db.deleteEntry(entry_id, permanent);
734
+ return Promise.resolve({ success, entryId: entry_id, permanent });
735
+ },
736
+ },
737
+ // Utility tools
738
+ {
739
+ name: 'list_tags',
740
+ title: 'List Tags',
741
+ description: 'List all available tags',
742
+ group: 'core',
743
+ inputSchema: z.object({}),
744
+ annotations: { readOnlyHint: true, idempotentHint: true },
745
+ handler: (_params: unknown) => {
746
+ const tags = db.listTags();
747
+ return Promise.resolve({ tags, count: tags.length });
748
+ },
749
+ },
750
+ // Vector index management tools
751
+ {
752
+ name: 'rebuild_vector_index',
753
+ title: 'Rebuild Vector Index',
754
+ description: 'Rebuild the semantic search vector index from all existing entries',
755
+ group: 'admin',
756
+ inputSchema: z.object({}),
757
+ annotations: { readOnlyHint: false, idempotentHint: false },
758
+ handler: async (_params: unknown) => {
759
+ if (!vectorManager) {
760
+ return { error: 'Vector search not available' };
761
+ }
762
+ const indexed = await vectorManager.rebuildIndex(db);
763
+ return { success: true, entriesIndexed: indexed };
764
+ },
765
+ },
766
+ {
767
+ name: 'add_to_vector_index',
768
+ title: 'Add Entry to Vector Index',
769
+ description: 'Add a specific entry to the semantic search vector index',
770
+ group: 'admin',
771
+ inputSchema: z.object({ entry_id: z.number() }),
772
+ annotations: { readOnlyHint: false, idempotentHint: true },
773
+ handler: async (params: unknown) => {
774
+ const { entry_id } = z.object({ entry_id: z.number() }).parse(params);
775
+ if (!vectorManager) {
776
+ return { error: 'Vector search not available' };
777
+ }
778
+ const entry = db.getEntryById(entry_id);
779
+ if (!entry) {
780
+ return { error: `Entry ${String(entry_id)} not found` };
781
+ }
782
+ const success = await vectorManager.addEntry(entry_id, entry.content);
783
+ return { success, entryId: entry_id };
784
+ },
785
+ },
786
+ {
787
+ name: 'get_vector_index_stats',
788
+ title: 'Get Vector Index Stats',
789
+ description: 'Get statistics about the semantic search vector index',
790
+ group: 'search',
791
+ inputSchema: z.object({}),
792
+ annotations: { readOnlyHint: true, idempotentHint: true },
793
+ handler: async (_params: unknown) => {
794
+ if (!vectorManager) {
795
+ return { available: false, error: 'Vector search not available' };
796
+ }
797
+ const stats = await vectorManager.getStats();
798
+ return { available: true, ...stats };
799
+ },
800
+ },
801
+ // GitHub integration tools
802
+ {
803
+ name: 'get_github_issues',
804
+ title: 'Get GitHub Issues',
805
+ description: 'List issues from a GitHub repository. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
806
+ group: 'github',
807
+ inputSchema: z.object({
808
+ owner: z.string().optional().describe('Repository owner - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
809
+ repo: z.string().optional().describe('Repository name - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
810
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
811
+ limit: z.number().optional().default(20),
812
+ }),
813
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
814
+ handler: async (params: unknown) => {
815
+ const input = z.object({
816
+ owner: z.string().optional(),
817
+ repo: z.string().optional(),
818
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
819
+ limit: z.number().optional().default(20),
820
+ }).parse(params);
821
+
822
+ if (!github) {
823
+ return { error: 'GitHub integration not available' };
824
+ }
825
+
826
+ // Get owner/repo from input or from current repo
827
+ const repoInfo = await github.getRepoInfo();
828
+ const detectedOwner = repoInfo.owner;
829
+ const detectedRepo = repoInfo.repo;
830
+
831
+ const owner = input.owner ?? detectedOwner ?? undefined;
832
+ const repo = input.repo ?? detectedRepo ?? undefined;
833
+
834
+ if (!owner || !repo) {
835
+ return {
836
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
837
+ requiresUserInput: true,
838
+ detectedOwner,
839
+ detectedRepo,
840
+ instruction: 'Ask the user: "What GitHub repository would you like to query? Please provide the owner and repo name (e.g., owner/repo)."'
841
+ };
842
+ }
843
+
844
+ const issues = await github.getIssues(owner, repo, input.state, input.limit);
845
+ return { owner, repo, detectedOwner, detectedRepo, issues, count: issues.length };
846
+ },
847
+ },
848
+ {
849
+ name: 'get_github_prs',
850
+ title: 'Get GitHub Pull Requests',
851
+ description: 'List pull requests from a GitHub repository. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
852
+ group: 'github',
853
+ inputSchema: z.object({
854
+ owner: z.string().optional().describe('Repository owner - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
855
+ repo: z.string().optional().describe('Repository name - LEAVE EMPTY to auto-detect from git. Only specify if user explicitly provides.'),
856
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
857
+ limit: z.number().optional().default(20),
858
+ }),
859
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
860
+ handler: async (params: unknown) => {
861
+ const input = z.object({
862
+ owner: z.string().optional(),
863
+ repo: z.string().optional(),
864
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
865
+ limit: z.number().optional().default(20),
866
+ }).parse(params);
867
+
868
+ if (!github) {
869
+ return { error: 'GitHub integration not available' };
870
+ }
871
+
872
+ const repoInfo = await github.getRepoInfo();
873
+ const detectedOwner = repoInfo.owner;
874
+ const detectedRepo = repoInfo.repo;
875
+
876
+ const owner = input.owner ?? detectedOwner ?? undefined;
877
+ const repo = input.repo ?? detectedRepo ?? undefined;
878
+
879
+ if (!owner || !repo) {
880
+ return {
881
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
882
+ requiresUserInput: true,
883
+ detectedOwner,
884
+ detectedRepo,
885
+ instruction: 'Ask the user: "What GitHub repository would you like to query? Please provide the owner and repo name (e.g., owner/repo)."'
886
+ };
887
+ }
888
+
889
+ const pullRequests = await github.getPullRequests(owner, repo, input.state, input.limit);
890
+ return { owner, repo, detectedOwner, detectedRepo, pullRequests, count: pullRequests.length };
891
+ },
892
+ },
893
+ {
894
+ name: 'get_github_issue',
895
+ title: 'Get GitHub Issue Details',
896
+ description: 'Get detailed information about a specific GitHub issue. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
897
+ group: 'github',
898
+ inputSchema: z.object({
899
+ issue_number: z.number(),
900
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
901
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
902
+ }),
903
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
904
+ handler: async (params: unknown) => {
905
+ const input = z.object({
906
+ issue_number: z.number(),
907
+ owner: z.string().optional(),
908
+ repo: z.string().optional(),
909
+ }).parse(params);
910
+
911
+ if (!github) {
912
+ return { error: 'GitHub integration not available' };
913
+ }
914
+
915
+ const repoInfo = await github.getRepoInfo();
916
+ const detectedOwner = repoInfo.owner;
917
+ const detectedRepo = repoInfo.repo;
918
+
919
+ const owner = input.owner ?? detectedOwner ?? undefined;
920
+ const repo = input.repo ?? detectedRepo ?? undefined;
921
+
922
+ if (!owner || !repo) {
923
+ return {
924
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
925
+ requiresUserInput: true,
926
+ detectedOwner,
927
+ detectedRepo,
928
+ instruction: 'Ask the user: "What GitHub repository is this issue from? Please provide the owner and repo name (e.g., owner/repo)."'
929
+ };
930
+ }
931
+
932
+ const issue = await github.getIssue(owner, repo, input.issue_number);
933
+ if (!issue) {
934
+ return { error: `Issue #${String(input.issue_number)} not found`, owner, repo, detectedOwner, detectedRepo };
935
+ }
936
+ return { issue, owner, repo, detectedOwner, detectedRepo };
937
+ },
938
+ },
939
+ {
940
+ name: 'get_github_pr',
941
+ title: 'Get GitHub PR Details',
942
+ description: 'Get detailed information about a specific GitHub pull request. IMPORTANT: Do NOT guess owner/repo values - leave them empty to auto-detect from the current git repository.',
943
+ group: 'github',
944
+ inputSchema: z.object({
945
+ pr_number: z.number(),
946
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
947
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
948
+ }),
949
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
950
+ handler: async (params: unknown) => {
951
+ const input = z.object({
952
+ pr_number: z.number(),
953
+ owner: z.string().optional(),
954
+ repo: z.string().optional(),
955
+ }).parse(params);
956
+
957
+ if (!github) {
958
+ return { error: 'GitHub integration not available' };
959
+ }
960
+
961
+ const repoInfo = await github.getRepoInfo();
962
+ const detectedOwner = repoInfo.owner;
963
+ const detectedRepo = repoInfo.repo;
964
+
965
+ const owner = input.owner ?? detectedOwner ?? undefined;
966
+ const repo = input.repo ?? detectedRepo ?? undefined;
967
+
968
+ if (!owner || !repo) {
969
+ return {
970
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
971
+ requiresUserInput: true,
972
+ detectedOwner,
973
+ detectedRepo,
974
+ instruction: 'Ask the user: "What GitHub repository is this PR from? Please provide the owner and repo name (e.g., owner/repo)."'
975
+ };
976
+ }
977
+
978
+ const pullRequest = await github.getPullRequest(owner, repo, input.pr_number);
979
+ if (!pullRequest) {
980
+ return { error: `PR #${String(input.pr_number)} not found`, owner, repo, detectedOwner, detectedRepo };
981
+ }
982
+ return { pullRequest, owner, repo, detectedOwner, detectedRepo };
983
+ },
984
+ },
985
+ {
986
+ name: 'get_github_context',
987
+ title: 'Get GitHub Repository Context',
988
+ description: 'Get current repository context including branch, issues, and PRs',
989
+ group: 'github',
990
+ inputSchema: z.object({}),
991
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
992
+ handler: async (_params: unknown) => {
993
+ if (!github) {
994
+ return { error: 'GitHub integration not available' };
995
+ }
996
+
997
+ const context = await github.getRepoContext();
998
+ return {
999
+ repoName: context.repoName,
1000
+ branch: context.branch,
1001
+ commit: context.commit,
1002
+ remoteUrl: context.remoteUrl,
1003
+ issues: context.issues,
1004
+ pullRequests: context.pullRequests,
1005
+ issueCount: context.issues.length,
1006
+ prCount: context.pullRequests.length,
1007
+ };
1008
+ },
1009
+ },
1010
+ // Backup tools
1011
+ {
1012
+ name: 'backup_journal',
1013
+ title: 'Backup Journal Database',
1014
+ description: 'Create a timestamped backup of the journal database. Backups are stored in the backups/ directory.',
1015
+ group: 'backup',
1016
+ inputSchema: z.object({
1017
+ name: z.string().optional().describe('Custom backup name (optional, defaults to timestamp)'),
1018
+ }),
1019
+ annotations: { readOnlyHint: false, idempotentHint: true },
1020
+ handler: (params: unknown) => {
1021
+ const input = z.object({
1022
+ name: z.string().optional(),
1023
+ }).parse(params);
1024
+ const result = db.exportToFile(input.name);
1025
+ return Promise.resolve({
1026
+ success: true,
1027
+ message: `Backup created successfully`,
1028
+ filename: result.filename,
1029
+ path: result.path,
1030
+ sizeBytes: result.sizeBytes,
1031
+ });
1032
+ },
1033
+ },
1034
+ {
1035
+ name: 'list_backups',
1036
+ title: 'List Journal Backups',
1037
+ description: 'List all available backup files with their sizes and creation dates',
1038
+ group: 'backup',
1039
+ inputSchema: z.object({}),
1040
+ annotations: { readOnlyHint: true, idempotentHint: true },
1041
+ handler: (_params: unknown) => {
1042
+ const backups = db.listBackups();
1043
+ return Promise.resolve({
1044
+ backups,
1045
+ total: backups.length,
1046
+ backupsDirectory: db.getBackupsDir(),
1047
+ hint: backups.length === 0
1048
+ ? 'No backups found. Use backup_journal to create one.'
1049
+ : undefined,
1050
+ });
1051
+ },
1052
+ },
1053
+ {
1054
+ name: 'restore_backup',
1055
+ title: 'Restore Journal from Backup',
1056
+ description: 'Restore the journal database from a backup file. WARNING: This replaces all current data. An automatic backup is created before restore.',
1057
+ group: 'backup',
1058
+ inputSchema: z.object({
1059
+ filename: z.string().describe('Backup filename to restore from (e.g., backup_2025-01-01.db)'),
1060
+ confirm: z.literal(true).describe('Must be set to true to confirm the restore operation'),
1061
+ }),
1062
+ annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true },
1063
+ handler: async (params: unknown) => {
1064
+ const input = z.object({
1065
+ filename: z.string(),
1066
+ confirm: z.literal(true),
1067
+ }).parse(params);
1068
+
1069
+ const result = await db.restoreFromFile(input.filename);
1070
+ return {
1071
+ success: true,
1072
+ message: `Database restored from ${input.filename}`,
1073
+ restoredFrom: result.restoredFrom,
1074
+ previousEntryCount: result.previousEntryCount,
1075
+ newEntryCount: result.newEntryCount,
1076
+ warning: 'A pre-restore backup was automatically created.',
1077
+ };
1078
+ },
1079
+ },
1080
+ ];
1081
+ }