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