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,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Journal MCP Server - Resource Handlers
|
|
3
|
+
*
|
|
4
|
+
* Exports all MCP resources with annotations following MCP 2025-11-25 spec.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SqliteAdapter } from '../../database/SqliteAdapter.js';
|
|
8
|
+
import type { VectorSearchManager } from '../../vector/VectorSearchManager.js';
|
|
9
|
+
import type { ToolFilterConfig } from '../../filtering/ToolFilter.js';
|
|
10
|
+
import type { Tag } from '../../types/index.js';
|
|
11
|
+
import type { GitHubIntegration } from '../../github/GitHubIntegration.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resource context for handlers that need extended access
|
|
15
|
+
*/
|
|
16
|
+
export interface ResourceContext {
|
|
17
|
+
db: SqliteAdapter;
|
|
18
|
+
vectorManager?: VectorSearchManager;
|
|
19
|
+
filterConfig?: ToolFilterConfig | null;
|
|
20
|
+
github?: GitHubIntegration | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Internal resource definition with db handler
|
|
25
|
+
*/
|
|
26
|
+
interface InternalResourceDef {
|
|
27
|
+
uri: string;
|
|
28
|
+
name: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
annotations?: {
|
|
33
|
+
audience?: ('user' | 'assistant')[];
|
|
34
|
+
priority?: number;
|
|
35
|
+
};
|
|
36
|
+
handler: (uri: string, context: ResourceContext) => unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all resource definitions for MCP list
|
|
41
|
+
*/
|
|
42
|
+
export function getResources(): object[] {
|
|
43
|
+
const resources = getAllResourceDefinitions();
|
|
44
|
+
return resources.map(r => ({
|
|
45
|
+
uri: r.uri,
|
|
46
|
+
name: r.name,
|
|
47
|
+
description: r.description,
|
|
48
|
+
mimeType: r.mimeType,
|
|
49
|
+
annotations: r.annotations,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read a resource by URI
|
|
55
|
+
*/
|
|
56
|
+
export function readResource(
|
|
57
|
+
uri: string,
|
|
58
|
+
db: SqliteAdapter,
|
|
59
|
+
vectorManager?: VectorSearchManager,
|
|
60
|
+
filterConfig?: ToolFilterConfig | null,
|
|
61
|
+
github?: GitHubIntegration | null
|
|
62
|
+
): Promise<unknown> {
|
|
63
|
+
const resources = getAllResourceDefinitions();
|
|
64
|
+
const context: ResourceContext = { db, vectorManager, filterConfig, github };
|
|
65
|
+
|
|
66
|
+
// Check for exact match first
|
|
67
|
+
const exactMatch = resources.find(r => r.uri === uri);
|
|
68
|
+
if (exactMatch) {
|
|
69
|
+
return Promise.resolve(exactMatch.handler(uri, context));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for template matches
|
|
73
|
+
for (const resource of resources) {
|
|
74
|
+
if (resource.uri.includes('{')) {
|
|
75
|
+
const pattern = resource.uri.replace(/\{[^}]+\}/g, '([^/]+)');
|
|
76
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
77
|
+
if (regex.test(uri)) {
|
|
78
|
+
return Promise.resolve(resource.handler(uri, context));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute a raw SQL query on the database
|
|
88
|
+
*/
|
|
89
|
+
function execQuery(db: SqliteAdapter, sql: string, params: unknown[] = []): Record<string, unknown>[] {
|
|
90
|
+
const rawDb = db.getRawDb();
|
|
91
|
+
const result = rawDb.exec(sql, params);
|
|
92
|
+
if (result.length === 0) return [];
|
|
93
|
+
|
|
94
|
+
const columns = result[0]?.columns ?? [];
|
|
95
|
+
return (result[0]?.values ?? []).map((values: unknown[]) => {
|
|
96
|
+
const obj: Record<string, unknown> = {};
|
|
97
|
+
columns.forEach((col: string, i: number) => {
|
|
98
|
+
obj[col] = values[i];
|
|
99
|
+
});
|
|
100
|
+
return obj;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get total tool count for health status
|
|
106
|
+
*/
|
|
107
|
+
function getTotalToolCount(): number {
|
|
108
|
+
// Import dynamically to avoid circular dependency
|
|
109
|
+
return 27; // 24 original + 3 backup tools
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get all resource definitions
|
|
114
|
+
*/
|
|
115
|
+
function getAllResourceDefinitions(): InternalResourceDef[] {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
uri: 'memory://recent',
|
|
119
|
+
name: 'Recent Entries',
|
|
120
|
+
title: 'Recent Journal Entries',
|
|
121
|
+
description: '10 most recent journal entries',
|
|
122
|
+
mimeType: 'application/json',
|
|
123
|
+
annotations: {
|
|
124
|
+
audience: ['assistant'],
|
|
125
|
+
priority: 0.8,
|
|
126
|
+
},
|
|
127
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
128
|
+
const entries = context.db.getRecentEntries(10);
|
|
129
|
+
return { entries, count: entries.length };
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
uri: 'memory://significant',
|
|
134
|
+
name: 'Significant Entries',
|
|
135
|
+
title: 'Significant Milestones',
|
|
136
|
+
description: 'Significant milestones and breakthroughs',
|
|
137
|
+
mimeType: 'application/json',
|
|
138
|
+
annotations: {
|
|
139
|
+
audience: ['assistant'],
|
|
140
|
+
priority: 0.7,
|
|
141
|
+
},
|
|
142
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
143
|
+
const rows = execQuery(context.db, `
|
|
144
|
+
SELECT * FROM memory_journal
|
|
145
|
+
WHERE significance_type IS NOT NULL
|
|
146
|
+
AND deleted_at IS NULL
|
|
147
|
+
ORDER BY timestamp DESC
|
|
148
|
+
LIMIT 20
|
|
149
|
+
`);
|
|
150
|
+
return { entries: rows, count: rows.length };
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
uri: 'memory://graph/recent',
|
|
155
|
+
name: 'Recent Relationship Graph',
|
|
156
|
+
title: 'Live Mermaid Diagram',
|
|
157
|
+
description: 'Live Mermaid diagram of recent relationships',
|
|
158
|
+
mimeType: 'text/plain',
|
|
159
|
+
annotations: {
|
|
160
|
+
audience: ['user', 'assistant'],
|
|
161
|
+
priority: 0.5,
|
|
162
|
+
},
|
|
163
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
164
|
+
// Get recent relationships from database
|
|
165
|
+
const relationships = execQuery(context.db, `
|
|
166
|
+
SELECT
|
|
167
|
+
r.id, r.from_entry_id, r.to_entry_id, r.relationship_type, r.description,
|
|
168
|
+
e1.content as from_content,
|
|
169
|
+
e2.content as to_content
|
|
170
|
+
FROM relationships r
|
|
171
|
+
JOIN memory_journal e1 ON r.from_entry_id = e1.id
|
|
172
|
+
JOIN memory_journal e2 ON r.to_entry_id = e2.id
|
|
173
|
+
WHERE e1.deleted_at IS NULL AND e2.deleted_at IS NULL
|
|
174
|
+
ORDER BY r.created_at DESC
|
|
175
|
+
LIMIT 20
|
|
176
|
+
`) as {
|
|
177
|
+
from_entry_id: number;
|
|
178
|
+
to_entry_id: number;
|
|
179
|
+
relationship_type: string;
|
|
180
|
+
from_content: string;
|
|
181
|
+
to_content: string;
|
|
182
|
+
}[];
|
|
183
|
+
|
|
184
|
+
if (relationships.length === 0) {
|
|
185
|
+
return {
|
|
186
|
+
format: 'mermaid',
|
|
187
|
+
diagram: 'graph TD\n NoData[No relationships found]',
|
|
188
|
+
message: 'No entry relationships exist yet. Use link_entries tool to create relationships.',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build Mermaid graph
|
|
193
|
+
const lines: string[] = ['graph TD'];
|
|
194
|
+
const seenNodes = new Set<number>();
|
|
195
|
+
|
|
196
|
+
// Relationship type to arrow style mapping
|
|
197
|
+
const arrowStyles: Record<string, string> = {
|
|
198
|
+
'references': '-->',
|
|
199
|
+
'implements': '-.->',
|
|
200
|
+
'evolves_from': '==>',
|
|
201
|
+
'related_to': '<-->',
|
|
202
|
+
'depends_on': '-->',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
for (const rel of relationships) {
|
|
206
|
+
// Add node definitions if not seen
|
|
207
|
+
if (!seenNodes.has(rel.from_entry_id)) {
|
|
208
|
+
const label = rel.from_content.slice(0, 30).replace(/[\]"'`[]/g, ' ').trim();
|
|
209
|
+
lines.push(` E${String(rel.from_entry_id)}["#${String(rel.from_entry_id)}: ${label}..."]`);
|
|
210
|
+
seenNodes.add(rel.from_entry_id);
|
|
211
|
+
}
|
|
212
|
+
if (!seenNodes.has(rel.to_entry_id)) {
|
|
213
|
+
const label = rel.to_content.slice(0, 30).replace(/[\]"'`[]/g, ' ').trim();
|
|
214
|
+
lines.push(` E${String(rel.to_entry_id)}["#${String(rel.to_entry_id)}: ${label}..."]`);
|
|
215
|
+
seenNodes.add(rel.to_entry_id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add edge with relationship label
|
|
219
|
+
const arrow = arrowStyles[rel.relationship_type] ?? '-->';
|
|
220
|
+
lines.push(` E${String(rel.from_entry_id)} ${arrow}|${rel.relationship_type}| E${String(rel.to_entry_id)}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
format: 'mermaid',
|
|
225
|
+
diagram: lines.join('\n'),
|
|
226
|
+
relationshipCount: relationships.length,
|
|
227
|
+
nodeCount: seenNodes.size,
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
uri: 'memory://team/recent',
|
|
233
|
+
name: 'Team Entries',
|
|
234
|
+
title: 'Recent Team-Shared Entries',
|
|
235
|
+
description: 'Recent team-shared entries',
|
|
236
|
+
mimeType: 'application/json',
|
|
237
|
+
annotations: {
|
|
238
|
+
audience: ['assistant'],
|
|
239
|
+
priority: 0.6,
|
|
240
|
+
},
|
|
241
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
242
|
+
const entries = context.db.getRecentEntries(10, false);
|
|
243
|
+
return { entries, count: entries.length };
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
uri: 'memory://projects/{number}/timeline',
|
|
248
|
+
name: 'Project Timeline',
|
|
249
|
+
title: 'Project Activity Timeline',
|
|
250
|
+
description: 'Project activity timeline',
|
|
251
|
+
mimeType: 'application/json',
|
|
252
|
+
annotations: {
|
|
253
|
+
audience: ['assistant'],
|
|
254
|
+
priority: 0.6,
|
|
255
|
+
},
|
|
256
|
+
handler: (uri: string, context: ResourceContext) => {
|
|
257
|
+
const match = /memory:\/\/projects\/(\d+)\/timeline/.exec(uri);
|
|
258
|
+
const projectNumber = match?.[1] ? parseInt(match[1], 10) : null;
|
|
259
|
+
|
|
260
|
+
if (projectNumber === null) {
|
|
261
|
+
return { error: 'Invalid project number' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const entries = execQuery(context.db, `
|
|
265
|
+
SELECT * FROM memory_journal
|
|
266
|
+
WHERE project_number = ?
|
|
267
|
+
AND deleted_at IS NULL
|
|
268
|
+
ORDER BY timestamp DESC
|
|
269
|
+
LIMIT 50
|
|
270
|
+
`, [projectNumber]);
|
|
271
|
+
return { projectNumber, entries, count: entries.length };
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
uri: 'memory://issues/{issue_number}/entries',
|
|
276
|
+
name: 'Issue Entries',
|
|
277
|
+
title: 'Entries Linked to Issue',
|
|
278
|
+
description: 'All entries linked to a specific issue',
|
|
279
|
+
mimeType: 'application/json',
|
|
280
|
+
annotations: {
|
|
281
|
+
audience: ['assistant'],
|
|
282
|
+
priority: 0.6,
|
|
283
|
+
},
|
|
284
|
+
handler: (uri: string, context: ResourceContext) => {
|
|
285
|
+
const match = /memory:\/\/issues\/(\d+)\/entries/.exec(uri);
|
|
286
|
+
const issueNumber = match?.[1] ? parseInt(match[1], 10) : null;
|
|
287
|
+
|
|
288
|
+
if (issueNumber === null) {
|
|
289
|
+
return { error: 'Invalid issue number' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const entries = execQuery(context.db, `
|
|
293
|
+
SELECT * FROM memory_journal
|
|
294
|
+
WHERE issue_number = ?
|
|
295
|
+
AND deleted_at IS NULL
|
|
296
|
+
ORDER BY timestamp DESC
|
|
297
|
+
`, [issueNumber]);
|
|
298
|
+
return { issueNumber, entries, count: entries.length };
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
uri: 'memory://prs/{pr_number}/entries',
|
|
303
|
+
name: 'PR Entries',
|
|
304
|
+
title: 'Entries Linked to PR',
|
|
305
|
+
description: 'All entries linked to a specific pull request',
|
|
306
|
+
mimeType: 'application/json',
|
|
307
|
+
annotations: {
|
|
308
|
+
audience: ['assistant'],
|
|
309
|
+
priority: 0.6,
|
|
310
|
+
},
|
|
311
|
+
handler: (uri: string, context: ResourceContext) => {
|
|
312
|
+
const match = /memory:\/\/prs\/(\d+)\/entries/.exec(uri);
|
|
313
|
+
const prNumber = match?.[1] ? parseInt(match[1], 10) : null;
|
|
314
|
+
|
|
315
|
+
if (prNumber === null) {
|
|
316
|
+
return { error: 'Invalid PR number' };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const entries = execQuery(context.db, `
|
|
320
|
+
SELECT * FROM memory_journal
|
|
321
|
+
WHERE pr_number = ?
|
|
322
|
+
AND deleted_at IS NULL
|
|
323
|
+
ORDER BY timestamp DESC
|
|
324
|
+
`, [prNumber]);
|
|
325
|
+
return { prNumber, entries, count: entries.length };
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
uri: 'memory://prs/{pr_number}/timeline',
|
|
330
|
+
name: 'PR Timeline',
|
|
331
|
+
title: 'Combined PR and Journal Timeline',
|
|
332
|
+
description: 'Combined PR + journal timeline',
|
|
333
|
+
mimeType: 'application/json',
|
|
334
|
+
annotations: {
|
|
335
|
+
audience: ['assistant'],
|
|
336
|
+
priority: 0.5,
|
|
337
|
+
},
|
|
338
|
+
handler: (uri: string, context: ResourceContext) => {
|
|
339
|
+
const match = /memory:\/\/prs\/(\d+)\/timeline/.exec(uri);
|
|
340
|
+
const prNumber = match?.[1] ? parseInt(match[1], 10) : null;
|
|
341
|
+
|
|
342
|
+
if (prNumber === null) {
|
|
343
|
+
return { error: 'Invalid PR number' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const entries = execQuery(context.db, `
|
|
347
|
+
SELECT * FROM memory_journal
|
|
348
|
+
WHERE pr_number = ?
|
|
349
|
+
AND deleted_at IS NULL
|
|
350
|
+
ORDER BY timestamp DESC
|
|
351
|
+
`, [prNumber]);
|
|
352
|
+
return { prNumber, entries, count: entries.length };
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
uri: 'memory://graph/actions',
|
|
357
|
+
name: 'Actions Graph',
|
|
358
|
+
title: 'CI/CD Narrative Graph',
|
|
359
|
+
description: 'CI/CD narrative graph: commits → runs → failures → entries → fixes → deployments',
|
|
360
|
+
mimeType: 'text/plain',
|
|
361
|
+
annotations: {
|
|
362
|
+
audience: ['user', 'assistant'],
|
|
363
|
+
priority: 0.5,
|
|
364
|
+
},
|
|
365
|
+
handler: async (_uri: string, context: ResourceContext) => {
|
|
366
|
+
// Check if GitHub integration is available
|
|
367
|
+
if (!context.github) {
|
|
368
|
+
return {
|
|
369
|
+
format: 'mermaid',
|
|
370
|
+
diagram: 'graph LR\n NoGitHub[GitHub integration not available]',
|
|
371
|
+
message: 'GitHub integration not configured. Set GITHUB_TOKEN and GITHUB_REPO_PATH.',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Get repository info and workflow runs
|
|
376
|
+
const repoInfo = await context.github.getRepoInfo();
|
|
377
|
+
if (!repoInfo.owner || !repoInfo.repo) {
|
|
378
|
+
return {
|
|
379
|
+
format: 'mermaid',
|
|
380
|
+
diagram: 'graph LR\n NoRepo[Repository not detected]',
|
|
381
|
+
message: 'Could not detect repository. Set GITHUB_REPO_PATH in your config.',
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const workflowRuns = await context.github.getWorkflowRuns(repoInfo.owner, repoInfo.repo, 10);
|
|
386
|
+
|
|
387
|
+
if (workflowRuns.length === 0) {
|
|
388
|
+
return {
|
|
389
|
+
format: 'mermaid',
|
|
390
|
+
diagram: 'graph LR\n NoRuns[No workflow runs found]',
|
|
391
|
+
message: 'No GitHub Actions workflow runs found for this repository.',
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Build Mermaid graph showing workflow runs
|
|
396
|
+
const lines: string[] = ['graph LR'];
|
|
397
|
+
|
|
398
|
+
// Status to styling map
|
|
399
|
+
const statusStyles: Record<string, string> = {
|
|
400
|
+
'success': ':::success',
|
|
401
|
+
'failure': ':::failure',
|
|
402
|
+
'cancelled': ':::cancelled',
|
|
403
|
+
'skipped': ':::skipped',
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Add style definitions
|
|
407
|
+
lines.push(' classDef success fill:#28a745,color:#fff');
|
|
408
|
+
lines.push(' classDef failure fill:#dc3545,color:#fff');
|
|
409
|
+
lines.push(' classDef cancelled fill:#6c757d,color:#fff');
|
|
410
|
+
lines.push(' classDef skipped fill:#ffc107,color:#000');
|
|
411
|
+
|
|
412
|
+
for (const run of workflowRuns) {
|
|
413
|
+
const shortSha = run.headSha.slice(0, 7);
|
|
414
|
+
const nodeId = `R${String(run.id)}`;
|
|
415
|
+
const commitId = `C${shortSha}`;
|
|
416
|
+
const style = statusStyles[run.conclusion ?? 'skipped'] ?? '';
|
|
417
|
+
const statusIcon = run.conclusion === 'success' ? '✓' : run.conclusion === 'failure' ? '✗' : '○';
|
|
418
|
+
|
|
419
|
+
// Add commit and run nodes
|
|
420
|
+
lines.push(` ${commitId}["${shortSha}"]`);
|
|
421
|
+
lines.push(` ${nodeId}["${statusIcon} ${run.name}"]${style}`);
|
|
422
|
+
lines.push(` ${commitId} --> ${nodeId}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
format: 'mermaid',
|
|
427
|
+
diagram: lines.join('\n'),
|
|
428
|
+
workflowRunCount: workflowRuns.length,
|
|
429
|
+
repository: `${repoInfo.owner}/${repoInfo.repo}`,
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
uri: 'memory://actions/recent',
|
|
435
|
+
name: 'Recent Actions',
|
|
436
|
+
title: 'Recent Workflow Runs',
|
|
437
|
+
description: 'Recent workflow runs with CI status',
|
|
438
|
+
mimeType: 'application/json',
|
|
439
|
+
annotations: {
|
|
440
|
+
audience: ['assistant'],
|
|
441
|
+
priority: 0.5,
|
|
442
|
+
},
|
|
443
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
444
|
+
const entries = execQuery(context.db, `
|
|
445
|
+
SELECT * FROM memory_journal
|
|
446
|
+
WHERE workflow_run_id IS NOT NULL
|
|
447
|
+
AND deleted_at IS NULL
|
|
448
|
+
ORDER BY timestamp DESC
|
|
449
|
+
LIMIT 10
|
|
450
|
+
`);
|
|
451
|
+
return { entries, count: entries.length };
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
uri: 'memory://tags',
|
|
456
|
+
name: 'All Tags',
|
|
457
|
+
title: 'Tag List',
|
|
458
|
+
description: 'All available tags with usage counts',
|
|
459
|
+
mimeType: 'application/json',
|
|
460
|
+
annotations: {
|
|
461
|
+
audience: ['assistant'],
|
|
462
|
+
priority: 0.4,
|
|
463
|
+
},
|
|
464
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
465
|
+
const tags: Tag[] = context.db.listTags();
|
|
466
|
+
return { tags, count: tags.length };
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
uri: 'memory://statistics',
|
|
471
|
+
name: 'Statistics',
|
|
472
|
+
title: 'Journal Statistics',
|
|
473
|
+
description: 'Overall journal statistics',
|
|
474
|
+
mimeType: 'application/json',
|
|
475
|
+
annotations: {
|
|
476
|
+
audience: ['assistant'],
|
|
477
|
+
priority: 0.4,
|
|
478
|
+
},
|
|
479
|
+
handler: (_uri: string, context: ResourceContext) => {
|
|
480
|
+
return context.db.getStatistics('week');
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
uri: 'memory://health',
|
|
485
|
+
name: 'Server Health',
|
|
486
|
+
title: 'Server Health & Diagnostics',
|
|
487
|
+
description: 'Server health status including database, backups, vector index, and tool filter status',
|
|
488
|
+
mimeType: 'application/json',
|
|
489
|
+
annotations: {
|
|
490
|
+
audience: ['assistant'],
|
|
491
|
+
priority: 0.9,
|
|
492
|
+
},
|
|
493
|
+
handler: async (_uri: string, context: ResourceContext) => {
|
|
494
|
+
const dbHealth = context.db.getHealthStatus();
|
|
495
|
+
|
|
496
|
+
// Get vector index status if available
|
|
497
|
+
let vectorIndex: { available: boolean; indexedEntries: number; modelName: string | null } | null = null;
|
|
498
|
+
if (context.vectorManager) {
|
|
499
|
+
try {
|
|
500
|
+
const stats = await context.vectorManager.getStats();
|
|
501
|
+
vectorIndex = {
|
|
502
|
+
available: true,
|
|
503
|
+
indexedEntries: stats.itemCount,
|
|
504
|
+
modelName: stats.modelName,
|
|
505
|
+
};
|
|
506
|
+
} catch {
|
|
507
|
+
vectorIndex = { available: false, indexedEntries: 0, modelName: null };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Get tool filter status
|
|
512
|
+
const totalTools = getTotalToolCount();
|
|
513
|
+
const toolFilter = {
|
|
514
|
+
active: context.filterConfig !== null && context.filterConfig !== undefined,
|
|
515
|
+
enabledCount: context.filterConfig?.enabledTools.size ?? totalTools,
|
|
516
|
+
totalCount: totalTools,
|
|
517
|
+
filterString: context.filterConfig?.raw ?? null,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
...dbHealth,
|
|
522
|
+
vectorIndex,
|
|
523
|
+
toolFilter,
|
|
524
|
+
timestamp: new Date().toISOString(),
|
|
525
|
+
};
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
];
|
|
529
|
+
}
|