@hir4ta/mneme 0.17.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/.claude-plugin/plugin.json +29 -0
- package/.mcp.json +18 -0
- package/README.ja.md +400 -0
- package/README.md +410 -0
- package/bin/mneme.js +203 -0
- package/dist/lib/db.js +340 -0
- package/dist/lib/fuzzy-search.js +214 -0
- package/dist/lib/github.js +121 -0
- package/dist/lib/similarity.js +193 -0
- package/dist/lib/utils.js +62 -0
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/assets/index-BgqCALAg.css +1 -0
- package/dist/public/assets/index-EMvn4VEa.js +330 -0
- package/dist/public/assets/react-force-graph-2d-DWoBaKmT.js +46 -0
- package/dist/public/favicon-128-max.png +0 -0
- package/dist/public/favicon-256-max.png +0 -0
- package/dist/public/favicon-32-max.png +0 -0
- package/dist/public/favicon-512-max.png +0 -0
- package/dist/public/favicon-64-max.png +0 -0
- package/dist/public/index.html +15 -0
- package/dist/server.js +4791 -0
- package/dist/servers/db-server.js +30558 -0
- package/dist/servers/search-server.js +30366 -0
- package/hooks/default-tags.json +1055 -0
- package/hooks/hooks.json +61 -0
- package/hooks/post-tool-use.sh +96 -0
- package/hooks/pre-compact.sh +187 -0
- package/hooks/session-end.sh +567 -0
- package/hooks/session-start.sh +380 -0
- package/hooks/user-prompt-submit.sh +253 -0
- package/package.json +77 -0
- package/servers/db-server.ts +993 -0
- package/servers/search-server.ts +675 -0
- package/skills/AGENTS.override.md +5 -0
- package/skills/harvest/skill.md +295 -0
- package/skills/init-mneme/skill.md +101 -0
- package/skills/plan/skill.md +422 -0
- package/skills/report/skill.md +74 -0
- package/skills/resume/skill.md +278 -0
- package/skills/review/skill.md +419 -0
- package/skills/save/skill.md +482 -0
- package/skills/search/skill.md +175 -0
- package/skills/using-mneme/skill.md +185 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mneme MCP Search Server
|
|
5
|
+
*
|
|
6
|
+
* Provides fast, unified search across mneme's knowledge base:
|
|
7
|
+
* - SQLite FTS5 for interactions
|
|
8
|
+
* - JSON file search for sessions, decisions, patterns
|
|
9
|
+
* - Tag alias resolution
|
|
10
|
+
* - Unified scoring
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
// Suppress Node.js SQLite experimental warning
|
|
20
|
+
const originalEmit = process.emit;
|
|
21
|
+
// @ts-expect-error - Suppressing experimental warning
|
|
22
|
+
process.emit = (event, ...args) => {
|
|
23
|
+
if (
|
|
24
|
+
event === "warning" &&
|
|
25
|
+
typeof args[0] === "object" &&
|
|
26
|
+
args[0] !== null &&
|
|
27
|
+
"name" in args[0] &&
|
|
28
|
+
(args[0] as { name: string }).name === "ExperimentalWarning" &&
|
|
29
|
+
"message" in args[0] &&
|
|
30
|
+
typeof (args[0] as { message: string }).message === "string" &&
|
|
31
|
+
(args[0] as { message: string }).message.includes("SQLite")
|
|
32
|
+
) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return originalEmit.apply(process, [event, ...args] as unknown as Parameters<
|
|
36
|
+
typeof process.emit
|
|
37
|
+
>);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Import after warning suppression is set up
|
|
41
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
42
|
+
type DatabaseSyncType = InstanceType<typeof DatabaseSync>;
|
|
43
|
+
|
|
44
|
+
// Types
|
|
45
|
+
interface SearchResult {
|
|
46
|
+
type: "session" | "decision" | "pattern" | "interaction";
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
snippet: string;
|
|
50
|
+
score: number;
|
|
51
|
+
matchedFields: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface TagDefinition {
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
aliases?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TagsFile {
|
|
61
|
+
tags: TagDefinition[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SessionFile {
|
|
65
|
+
id: string;
|
|
66
|
+
title?: string;
|
|
67
|
+
tags?: string[];
|
|
68
|
+
summary?: {
|
|
69
|
+
title?: string;
|
|
70
|
+
goal?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
};
|
|
73
|
+
discussions?: Array<{
|
|
74
|
+
topic?: string;
|
|
75
|
+
decision?: string;
|
|
76
|
+
reasoning?: string;
|
|
77
|
+
}>;
|
|
78
|
+
errors?: Array<{
|
|
79
|
+
error?: string;
|
|
80
|
+
cause?: string;
|
|
81
|
+
solution?: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface DecisionFile {
|
|
86
|
+
id: string;
|
|
87
|
+
title?: string;
|
|
88
|
+
decision?: string;
|
|
89
|
+
reasoning?: string;
|
|
90
|
+
tags?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface PatternFile {
|
|
94
|
+
patterns?: Array<{
|
|
95
|
+
errorPattern?: string;
|
|
96
|
+
solution?: string;
|
|
97
|
+
tags?: string[];
|
|
98
|
+
}>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get project path from env or current working directory
|
|
102
|
+
function getProjectPath(): string {
|
|
103
|
+
return process.env.MNEME_PROJECT_PATH || process.cwd();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMnemeDir(): string {
|
|
107
|
+
return path.join(getProjectPath(), ".mneme");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getLocalDbPath(): string {
|
|
111
|
+
return path.join(getMnemeDir(), "local.db");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Database connection (lazy initialization)
|
|
115
|
+
let db: DatabaseSyncType | null = null;
|
|
116
|
+
|
|
117
|
+
function getDb(): DatabaseSyncType | null {
|
|
118
|
+
if (db) return db;
|
|
119
|
+
|
|
120
|
+
const dbPath = getLocalDbPath();
|
|
121
|
+
if (!fs.existsSync(dbPath)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
db = new DatabaseSync(dbPath);
|
|
127
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
128
|
+
return db;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Tag alias resolution
|
|
135
|
+
function loadTags(): TagsFile | null {
|
|
136
|
+
const tagsPath = path.join(getMnemeDir(), "tags.json");
|
|
137
|
+
if (!fs.existsSync(tagsPath)) return null;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const content = fs.readFileSync(tagsPath, "utf-8");
|
|
141
|
+
return JSON.parse(content) as TagsFile;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function expandKeywordsWithAliases(
|
|
148
|
+
keywords: string[],
|
|
149
|
+
tags: TagsFile | null,
|
|
150
|
+
): string[] {
|
|
151
|
+
if (!tags) return keywords;
|
|
152
|
+
|
|
153
|
+
const expanded = new Set(keywords.map((k) => k.toLowerCase()));
|
|
154
|
+
|
|
155
|
+
for (const keyword of keywords) {
|
|
156
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
157
|
+
|
|
158
|
+
for (const tag of tags.tags) {
|
|
159
|
+
const matches =
|
|
160
|
+
tag.id.toLowerCase() === lowerKeyword ||
|
|
161
|
+
tag.label.toLowerCase() === lowerKeyword ||
|
|
162
|
+
tag.aliases?.some((a) => a.toLowerCase() === lowerKeyword);
|
|
163
|
+
|
|
164
|
+
if (matches) {
|
|
165
|
+
expanded.add(tag.id.toLowerCase());
|
|
166
|
+
expanded.add(tag.label.toLowerCase());
|
|
167
|
+
for (const alias of tag.aliases || []) {
|
|
168
|
+
expanded.add(alias.toLowerCase());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return Array.from(expanded);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Search functions
|
|
178
|
+
function searchInteractions(
|
|
179
|
+
keywords: string[],
|
|
180
|
+
projectPath: string,
|
|
181
|
+
limit = 5,
|
|
182
|
+
): SearchResult[] {
|
|
183
|
+
const database = getDb();
|
|
184
|
+
if (!database) return [];
|
|
185
|
+
|
|
186
|
+
const results: SearchResult[] = [];
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Try FTS5 first
|
|
190
|
+
const ftsQuery = keywords.join(" OR ");
|
|
191
|
+
const ftsStmt = database.prepare(`
|
|
192
|
+
SELECT
|
|
193
|
+
i.session_id,
|
|
194
|
+
i.content,
|
|
195
|
+
i.thinking,
|
|
196
|
+
i.timestamp,
|
|
197
|
+
highlight(interactions_fts, 0, '[', ']') as content_highlight
|
|
198
|
+
FROM interactions_fts
|
|
199
|
+
JOIN interactions i ON interactions_fts.rowid = i.id
|
|
200
|
+
WHERE interactions_fts MATCH ?
|
|
201
|
+
AND i.project_path = ?
|
|
202
|
+
ORDER BY rank
|
|
203
|
+
LIMIT ?
|
|
204
|
+
`);
|
|
205
|
+
|
|
206
|
+
const rows = ftsStmt.all(ftsQuery, projectPath, limit) as Array<{
|
|
207
|
+
session_id: string;
|
|
208
|
+
content: string;
|
|
209
|
+
thinking: string | null;
|
|
210
|
+
timestamp: string;
|
|
211
|
+
content_highlight: string;
|
|
212
|
+
}>;
|
|
213
|
+
|
|
214
|
+
for (const row of rows) {
|
|
215
|
+
const snippet = row.content_highlight || row.content.substring(0, 100);
|
|
216
|
+
results.push({
|
|
217
|
+
type: "interaction",
|
|
218
|
+
id: row.session_id,
|
|
219
|
+
title: `Interaction from ${row.timestamp}`,
|
|
220
|
+
snippet: snippet.substring(0, 150),
|
|
221
|
+
score: 5,
|
|
222
|
+
matchedFields: ["content"],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// FTS5 failed, fallback to LIKE
|
|
227
|
+
try {
|
|
228
|
+
const likePattern = `%${keywords.join("%")}%`;
|
|
229
|
+
const stmt = database.prepare(`
|
|
230
|
+
SELECT DISTINCT session_id, substr(content, 1, 100) as snippet, timestamp
|
|
231
|
+
FROM interactions
|
|
232
|
+
WHERE project_path = ?
|
|
233
|
+
AND (content LIKE ? OR thinking LIKE ?)
|
|
234
|
+
ORDER BY timestamp DESC
|
|
235
|
+
LIMIT ?
|
|
236
|
+
`);
|
|
237
|
+
|
|
238
|
+
const rows = stmt.all(
|
|
239
|
+
projectPath,
|
|
240
|
+
likePattern,
|
|
241
|
+
likePattern,
|
|
242
|
+
limit,
|
|
243
|
+
) as Array<{
|
|
244
|
+
session_id: string;
|
|
245
|
+
snippet: string;
|
|
246
|
+
timestamp: string;
|
|
247
|
+
}>;
|
|
248
|
+
|
|
249
|
+
for (const row of rows) {
|
|
250
|
+
results.push({
|
|
251
|
+
type: "interaction",
|
|
252
|
+
id: row.session_id,
|
|
253
|
+
title: `Interaction from ${row.timestamp}`,
|
|
254
|
+
snippet: row.snippet,
|
|
255
|
+
score: 3,
|
|
256
|
+
matchedFields: ["content"],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// Database error, return empty
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return results;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function searchSessions(keywords: string[], limit = 5): SearchResult[] {
|
|
268
|
+
const sessionsDir = path.join(getMnemeDir(), "sessions");
|
|
269
|
+
if (!fs.existsSync(sessionsDir)) return [];
|
|
270
|
+
|
|
271
|
+
const results: SearchResult[] = [];
|
|
272
|
+
const pattern = new RegExp(keywords.join("|"), "i");
|
|
273
|
+
|
|
274
|
+
function walkDir(dir: string) {
|
|
275
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
const fullPath = path.join(dir, entry.name);
|
|
278
|
+
if (entry.isDirectory()) {
|
|
279
|
+
walkDir(fullPath);
|
|
280
|
+
} else if (entry.name.endsWith(".json")) {
|
|
281
|
+
try {
|
|
282
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
283
|
+
const session = JSON.parse(content) as SessionFile;
|
|
284
|
+
|
|
285
|
+
let score = 0;
|
|
286
|
+
const matchedFields: string[] = [];
|
|
287
|
+
|
|
288
|
+
// Score title matches higher
|
|
289
|
+
const title = session.title || session.summary?.title || "";
|
|
290
|
+
if (title && pattern.test(title)) {
|
|
291
|
+
score += 3;
|
|
292
|
+
matchedFields.push("title");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check tags
|
|
296
|
+
if (session.tags?.some((t) => pattern.test(t))) {
|
|
297
|
+
score += 1;
|
|
298
|
+
matchedFields.push("tags");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check summary
|
|
302
|
+
if (session.summary?.goal && pattern.test(session.summary.goal)) {
|
|
303
|
+
score += 2;
|
|
304
|
+
matchedFields.push("summary.goal");
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
session.summary?.description &&
|
|
308
|
+
pattern.test(session.summary.description)
|
|
309
|
+
) {
|
|
310
|
+
score += 2;
|
|
311
|
+
matchedFields.push("summary.description");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check discussions
|
|
315
|
+
if (
|
|
316
|
+
session.discussions?.some(
|
|
317
|
+
(d) =>
|
|
318
|
+
pattern.test(d.topic || "") || pattern.test(d.decision || ""),
|
|
319
|
+
)
|
|
320
|
+
) {
|
|
321
|
+
score += 2;
|
|
322
|
+
matchedFields.push("discussions");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check errors
|
|
326
|
+
if (
|
|
327
|
+
session.errors?.some(
|
|
328
|
+
(e) =>
|
|
329
|
+
pattern.test(e.error || "") || pattern.test(e.solution || ""),
|
|
330
|
+
)
|
|
331
|
+
) {
|
|
332
|
+
score += 2;
|
|
333
|
+
matchedFields.push("errors");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (score > 0) {
|
|
337
|
+
results.push({
|
|
338
|
+
type: "session",
|
|
339
|
+
id: session.id,
|
|
340
|
+
title: title || session.id,
|
|
341
|
+
snippet:
|
|
342
|
+
session.summary?.description || session.summary?.goal || "",
|
|
343
|
+
score,
|
|
344
|
+
matchedFields,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
// Skip invalid files
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
walkDir(sessionsDir);
|
|
355
|
+
|
|
356
|
+
// Sort by score and limit
|
|
357
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function searchDecisions(keywords: string[], limit = 5): SearchResult[] {
|
|
361
|
+
const decisionsDir = path.join(getMnemeDir(), "decisions");
|
|
362
|
+
if (!fs.existsSync(decisionsDir)) return [];
|
|
363
|
+
|
|
364
|
+
const results: SearchResult[] = [];
|
|
365
|
+
const pattern = new RegExp(keywords.join("|"), "i");
|
|
366
|
+
|
|
367
|
+
function walkDir(dir: string) {
|
|
368
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const fullPath = path.join(dir, entry.name);
|
|
371
|
+
if (entry.isDirectory()) {
|
|
372
|
+
walkDir(fullPath);
|
|
373
|
+
} else if (entry.name.endsWith(".json")) {
|
|
374
|
+
try {
|
|
375
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
376
|
+
const decision = JSON.parse(content) as DecisionFile;
|
|
377
|
+
|
|
378
|
+
let score = 0;
|
|
379
|
+
const matchedFields: string[] = [];
|
|
380
|
+
|
|
381
|
+
if (decision.title && pattern.test(decision.title)) {
|
|
382
|
+
score += 3;
|
|
383
|
+
matchedFields.push("title");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (decision.decision && pattern.test(decision.decision)) {
|
|
387
|
+
score += 2;
|
|
388
|
+
matchedFields.push("decision");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (decision.reasoning && pattern.test(decision.reasoning)) {
|
|
392
|
+
score += 2;
|
|
393
|
+
matchedFields.push("reasoning");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (decision.tags?.some((t) => pattern.test(t))) {
|
|
397
|
+
score += 1;
|
|
398
|
+
matchedFields.push("tags");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (score > 0) {
|
|
402
|
+
results.push({
|
|
403
|
+
type: "decision",
|
|
404
|
+
id: decision.id,
|
|
405
|
+
title: decision.title || decision.id,
|
|
406
|
+
snippet: decision.decision || "",
|
|
407
|
+
score,
|
|
408
|
+
matchedFields,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
// Skip invalid files
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
walkDir(decisionsDir);
|
|
419
|
+
|
|
420
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function searchPatterns(keywords: string[], limit = 5): SearchResult[] {
|
|
424
|
+
const patternsDir = path.join(getMnemeDir(), "patterns");
|
|
425
|
+
if (!fs.existsSync(patternsDir)) return [];
|
|
426
|
+
|
|
427
|
+
const results: SearchResult[] = [];
|
|
428
|
+
const pattern = new RegExp(keywords.join("|"), "i");
|
|
429
|
+
|
|
430
|
+
const files = fs.readdirSync(patternsDir).filter((f) => f.endsWith(".json"));
|
|
431
|
+
|
|
432
|
+
for (const file of files) {
|
|
433
|
+
try {
|
|
434
|
+
const content = fs.readFileSync(path.join(patternsDir, file), "utf-8");
|
|
435
|
+
const patternFile = JSON.parse(content) as PatternFile;
|
|
436
|
+
|
|
437
|
+
for (const p of patternFile.patterns || []) {
|
|
438
|
+
let score = 0;
|
|
439
|
+
const matchedFields: string[] = [];
|
|
440
|
+
|
|
441
|
+
if (p.errorPattern && pattern.test(p.errorPattern)) {
|
|
442
|
+
score += 3;
|
|
443
|
+
matchedFields.push("errorPattern");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (p.solution && pattern.test(p.solution)) {
|
|
447
|
+
score += 2;
|
|
448
|
+
matchedFields.push("solution");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (p.tags?.some((t) => pattern.test(t))) {
|
|
452
|
+
score += 1;
|
|
453
|
+
matchedFields.push("tags");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (score > 0) {
|
|
457
|
+
results.push({
|
|
458
|
+
type: "pattern",
|
|
459
|
+
id: `${file}:${p.errorPattern?.substring(0, 20)}`,
|
|
460
|
+
title: p.errorPattern?.substring(0, 50) || "Pattern",
|
|
461
|
+
snippet: p.solution || "",
|
|
462
|
+
score,
|
|
463
|
+
matchedFields,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
// Skip invalid files
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Unified search
|
|
476
|
+
function search(
|
|
477
|
+
query: string,
|
|
478
|
+
options: { types?: string[]; limit?: number } = {},
|
|
479
|
+
): SearchResult[] {
|
|
480
|
+
const {
|
|
481
|
+
types = ["session", "decision", "pattern", "interaction"],
|
|
482
|
+
limit = 10,
|
|
483
|
+
} = options;
|
|
484
|
+
|
|
485
|
+
// Extract keywords
|
|
486
|
+
const keywords = query
|
|
487
|
+
.toLowerCase()
|
|
488
|
+
.split(/\s+/)
|
|
489
|
+
.filter((w) => w.length > 2);
|
|
490
|
+
|
|
491
|
+
if (keywords.length === 0) return [];
|
|
492
|
+
|
|
493
|
+
// Expand with tag aliases
|
|
494
|
+
const tags = loadTags();
|
|
495
|
+
const expandedKeywords = expandKeywordsWithAliases(keywords, tags);
|
|
496
|
+
|
|
497
|
+
const results: SearchResult[] = [];
|
|
498
|
+
const projectPath = getProjectPath();
|
|
499
|
+
|
|
500
|
+
// Search each type
|
|
501
|
+
if (types.includes("session")) {
|
|
502
|
+
results.push(...searchSessions(expandedKeywords, limit));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (types.includes("decision")) {
|
|
506
|
+
results.push(...searchDecisions(expandedKeywords, limit));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (types.includes("pattern")) {
|
|
510
|
+
results.push(...searchPatterns(expandedKeywords, limit));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (types.includes("interaction")) {
|
|
514
|
+
results.push(...searchInteractions(expandedKeywords, projectPath, limit));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Sort by score and deduplicate
|
|
518
|
+
const seen = new Set<string>();
|
|
519
|
+
return results
|
|
520
|
+
.sort((a, b) => b.score - a.score)
|
|
521
|
+
.filter((r) => {
|
|
522
|
+
const key = `${r.type}:${r.id}`;
|
|
523
|
+
if (seen.has(key)) return false;
|
|
524
|
+
seen.add(key);
|
|
525
|
+
return true;
|
|
526
|
+
})
|
|
527
|
+
.slice(0, limit);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Get specific items
|
|
531
|
+
function getSession(sessionId: string): SessionFile | null {
|
|
532
|
+
const sessionsDir = path.join(getMnemeDir(), "sessions");
|
|
533
|
+
if (!fs.existsSync(sessionsDir)) return null;
|
|
534
|
+
|
|
535
|
+
function findSession(dir: string): SessionFile | null {
|
|
536
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
537
|
+
for (const entry of entries) {
|
|
538
|
+
const fullPath = path.join(dir, entry.name);
|
|
539
|
+
if (entry.isDirectory()) {
|
|
540
|
+
const result = findSession(fullPath);
|
|
541
|
+
if (result) return result;
|
|
542
|
+
} else if (entry.name.endsWith(".json")) {
|
|
543
|
+
try {
|
|
544
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
545
|
+
const session = JSON.parse(content) as SessionFile;
|
|
546
|
+
if (session.id === sessionId) return session;
|
|
547
|
+
} catch {
|
|
548
|
+
// Skip
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return findSession(sessionsDir);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getDecision(decisionId: string): DecisionFile | null {
|
|
559
|
+
const decisionsDir = path.join(getMnemeDir(), "decisions");
|
|
560
|
+
if (!fs.existsSync(decisionsDir)) return null;
|
|
561
|
+
|
|
562
|
+
function findDecision(dir: string): DecisionFile | null {
|
|
563
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
564
|
+
for (const entry of entries) {
|
|
565
|
+
const fullPath = path.join(dir, entry.name);
|
|
566
|
+
if (entry.isDirectory()) {
|
|
567
|
+
const result = findDecision(fullPath);
|
|
568
|
+
if (result) return result;
|
|
569
|
+
} else if (entry.name.endsWith(".json")) {
|
|
570
|
+
try {
|
|
571
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
572
|
+
const decision = JSON.parse(content) as DecisionFile;
|
|
573
|
+
if (decision.id === decisionId) return decision;
|
|
574
|
+
} catch {
|
|
575
|
+
// Skip
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return findDecision(decisionsDir);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// MCP Server setup
|
|
586
|
+
const server = new McpServer({
|
|
587
|
+
name: "mneme-search",
|
|
588
|
+
version: "0.1.0",
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Tool: mneme_search
|
|
592
|
+
server.registerTool(
|
|
593
|
+
"mneme_search",
|
|
594
|
+
{
|
|
595
|
+
description:
|
|
596
|
+
"Search mneme's knowledge base for sessions, decisions, patterns, and interactions. Returns scored results with matched fields.",
|
|
597
|
+
inputSchema: {
|
|
598
|
+
query: z.string().describe("Search query (keywords)"),
|
|
599
|
+
types: z
|
|
600
|
+
.array(z.enum(["session", "decision", "pattern", "interaction"]))
|
|
601
|
+
.optional()
|
|
602
|
+
.describe("Types to search (default: all)"),
|
|
603
|
+
limit: z.number().optional().describe("Maximum results (default: 10)"),
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
async ({ query, types, limit }) => {
|
|
607
|
+
const results = search(query, { types, limit });
|
|
608
|
+
return {
|
|
609
|
+
content: [
|
|
610
|
+
{
|
|
611
|
+
type: "text",
|
|
612
|
+
text: JSON.stringify(results, null, 2),
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// Tool: mneme_get_session
|
|
620
|
+
server.registerTool(
|
|
621
|
+
"mneme_get_session",
|
|
622
|
+
{
|
|
623
|
+
description: "Get full details of a specific session by ID",
|
|
624
|
+
inputSchema: {
|
|
625
|
+
sessionId: z.string().describe("Session ID"),
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
async ({ sessionId }) => {
|
|
629
|
+
const session = getSession(sessionId);
|
|
630
|
+
if (!session) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
633
|
+
isError: true,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
content: [{ type: "text", text: JSON.stringify(session, null, 2) }],
|
|
638
|
+
};
|
|
639
|
+
},
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// Tool: mneme_get_decision
|
|
643
|
+
server.registerTool(
|
|
644
|
+
"mneme_get_decision",
|
|
645
|
+
{
|
|
646
|
+
description: "Get full details of a specific decision by ID",
|
|
647
|
+
inputSchema: {
|
|
648
|
+
decisionId: z.string().describe("Decision ID"),
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
async ({ decisionId }) => {
|
|
652
|
+
const decision = getDecision(decisionId);
|
|
653
|
+
if (!decision) {
|
|
654
|
+
return {
|
|
655
|
+
content: [{ type: "text", text: `Decision not found: ${decisionId}` }],
|
|
656
|
+
isError: true,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
content: [{ type: "text", text: JSON.stringify(decision, null, 2) }],
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// Start server
|
|
666
|
+
async function main() {
|
|
667
|
+
const transport = new StdioServerTransport();
|
|
668
|
+
await server.connect(transport);
|
|
669
|
+
console.error("mneme-search MCP server running");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
main().catch((error) => {
|
|
673
|
+
console.error("Server error:", error);
|
|
674
|
+
process.exit(1);
|
|
675
|
+
});
|