@hir4ta/mneme 0.22.0 → 0.22.3
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 +1 -1
- package/README.ja.md +7 -1
- package/README.md +1 -1
- package/dist/lib/incremental-save.js +2 -2
- package/dist/lib/prompt-search.js +1 -64
- package/dist/lib/search-core.js +1 -64
- package/dist/lib/session-finalize.js +21 -10
- package/dist/public/assets/index-DZyzcWMg.js +351 -0
- package/dist/public/assets/index-g8Lvi94K.css +1 -0
- package/dist/public/assets/{react-force-graph-2d-Dlcfvz01.js → react-force-graph-2d-CAP2m3Y0.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/server.js +190 -243
- package/dist/servers/db-server.js +140 -436
- package/dist/servers/search-server.js +3 -95
- package/package.json +1 -1
- package/scripts/export-weekly-knowledge-html.ts +15 -14
- package/servers/db-server.ts +180 -515
- package/servers/search-server.ts +3 -46
- package/skills/save/SKILL.md +20 -21
- package/dist/public/assets/index-Bvl_IrPy.css +0 -1
- package/dist/public/assets/index-k5JYSPV6.js +0 -351
package/servers/db-server.ts
CHANGED
|
@@ -76,45 +76,8 @@ const LIST_LIMIT_MIN = 1;
|
|
|
76
76
|
const LIST_LIMIT_MAX = 200;
|
|
77
77
|
const INTERACTION_OFFSET_MIN = 0;
|
|
78
78
|
const QUERY_MAX_LENGTH = 500;
|
|
79
|
-
const UNIT_LIMIT_MAX = 500;
|
|
80
79
|
const SEARCH_EVAL_DEFAULT_LIMIT = 5;
|
|
81
80
|
|
|
82
|
-
type UnitStatus = "pending" | "approved" | "rejected";
|
|
83
|
-
type UnitType = "decision" | "pattern" | "rule";
|
|
84
|
-
type RuleKind = "policy" | "pitfall" | "playbook";
|
|
85
|
-
|
|
86
|
-
interface Unit {
|
|
87
|
-
id: string;
|
|
88
|
-
type: UnitType;
|
|
89
|
-
kind: RuleKind;
|
|
90
|
-
title: string;
|
|
91
|
-
summary: string;
|
|
92
|
-
tags: string[];
|
|
93
|
-
sourceId: string;
|
|
94
|
-
sourceType: UnitType;
|
|
95
|
-
sourceRefs: Array<{ type: UnitType; id: string }>;
|
|
96
|
-
status: UnitStatus;
|
|
97
|
-
createdAt: string;
|
|
98
|
-
updatedAt: string;
|
|
99
|
-
reviewedAt?: string;
|
|
100
|
-
reviewedBy?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface UnitsFile {
|
|
104
|
-
schemaVersion: number;
|
|
105
|
-
updatedAt: string;
|
|
106
|
-
items: Unit[];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface AuditEntry {
|
|
110
|
-
timestamp: string;
|
|
111
|
-
actor?: string;
|
|
112
|
-
entity: "session" | "decision" | "pattern" | "rule" | "unit";
|
|
113
|
-
action: "create" | "update" | "delete";
|
|
114
|
-
targetId: string;
|
|
115
|
-
detail?: Record<string, unknown>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
81
|
interface RuleDoc {
|
|
119
82
|
items?: Array<Record<string, unknown>>;
|
|
120
83
|
rules?: Array<Record<string, unknown>>;
|
|
@@ -170,25 +133,6 @@ function listJsonFiles(dir: string): string[] {
|
|
|
170
133
|
});
|
|
171
134
|
}
|
|
172
135
|
|
|
173
|
-
function readUnits(): UnitsFile {
|
|
174
|
-
const unitsPath = path.join(getMnemeDir(), "units", "units.json");
|
|
175
|
-
const parsed = readJsonFile<UnitsFile>(unitsPath);
|
|
176
|
-
if (!parsed || !Array.isArray(parsed.items)) {
|
|
177
|
-
return {
|
|
178
|
-
schemaVersion: 1,
|
|
179
|
-
updatedAt: new Date().toISOString(),
|
|
180
|
-
items: [],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
return parsed;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function writeUnits(doc: UnitsFile): void {
|
|
187
|
-
const unitsPath = path.join(getMnemeDir(), "units", "units.json");
|
|
188
|
-
fs.mkdirSync(path.dirname(unitsPath), { recursive: true });
|
|
189
|
-
fs.writeFileSync(unitsPath, JSON.stringify(doc, null, 2));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
136
|
function readRuleItems(
|
|
193
137
|
ruleType: "dev-rules" | "review-guidelines",
|
|
194
138
|
): Array<Record<string, unknown>> {
|
|
@@ -198,42 +142,6 @@ function readRuleItems(
|
|
|
198
142
|
return Array.isArray(items) ? items : [];
|
|
199
143
|
}
|
|
200
144
|
|
|
201
|
-
function readAuditEntries(
|
|
202
|
-
options: { from?: string; to?: string; entity?: string } = {},
|
|
203
|
-
): AuditEntry[] {
|
|
204
|
-
const auditDir = path.join(getMnemeDir(), "audit");
|
|
205
|
-
if (!fs.existsSync(auditDir)) return [];
|
|
206
|
-
|
|
207
|
-
const files = fs
|
|
208
|
-
.readdirSync(auditDir)
|
|
209
|
-
.filter((name) => name.endsWith(".jsonl"))
|
|
210
|
-
.sort();
|
|
211
|
-
const fromTime = options.from ? new Date(options.from).getTime() : null;
|
|
212
|
-
const toTime = options.to ? new Date(options.to).getTime() : null;
|
|
213
|
-
|
|
214
|
-
const entries: AuditEntry[] = [];
|
|
215
|
-
for (const name of files) {
|
|
216
|
-
const fullPath = path.join(auditDir, name);
|
|
217
|
-
const lines = fs.readFileSync(fullPath, "utf-8").split("\n");
|
|
218
|
-
for (const line of lines) {
|
|
219
|
-
if (!line.trim()) continue;
|
|
220
|
-
try {
|
|
221
|
-
const parsed = JSON.parse(line) as AuditEntry;
|
|
222
|
-
const ts = new Date(parsed.timestamp).getTime();
|
|
223
|
-
if (fromTime !== null && ts < fromTime) continue;
|
|
224
|
-
if (toTime !== null && ts > toTime) continue;
|
|
225
|
-
if (options.entity && parsed.entity !== options.entity) continue;
|
|
226
|
-
entries.push(parsed);
|
|
227
|
-
} catch {
|
|
228
|
-
// skip malformed lines
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return entries.sort(
|
|
233
|
-
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
145
|
function readSessionsById(): Map<string, Record<string, unknown>> {
|
|
238
146
|
const sessionsDir = path.join(getMnemeDir(), "sessions");
|
|
239
147
|
const map = new Map<string, Record<string, unknown>>();
|
|
@@ -246,107 +154,6 @@ function readSessionsById(): Map<string, Record<string, unknown>> {
|
|
|
246
154
|
return map;
|
|
247
155
|
}
|
|
248
156
|
|
|
249
|
-
function inferUnitPriority(unit: Unit): "p0" | "p1" | "p2" {
|
|
250
|
-
if (unit.sourceType === "rule") {
|
|
251
|
-
const [ruleFile, ruleId] = unit.sourceId.split(":", 2);
|
|
252
|
-
if (
|
|
253
|
-
(ruleFile === "dev-rules" || ruleFile === "review-guidelines") &&
|
|
254
|
-
ruleId
|
|
255
|
-
) {
|
|
256
|
-
const rule = readRuleItems(ruleFile).find((item) => item.id === ruleId);
|
|
257
|
-
const priority =
|
|
258
|
-
typeof rule?.priority === "string" ? rule.priority.toLowerCase() : "";
|
|
259
|
-
if (priority === "p0" || priority === "p1" || priority === "p2") {
|
|
260
|
-
return priority;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const text =
|
|
265
|
-
`${unit.title} ${unit.summary} ${unit.tags.join(" ")}`.toLowerCase();
|
|
266
|
-
if (
|
|
267
|
-
/(security|auth|token|secret|password|injection|xss|csrf|compliance|outage|data[- ]?loss)/.test(
|
|
268
|
-
text,
|
|
269
|
-
)
|
|
270
|
-
) {
|
|
271
|
-
return "p0";
|
|
272
|
-
}
|
|
273
|
-
if (/(crash|error|correct|reliab|timeout|retry|integrity)/.test(text)) {
|
|
274
|
-
return "p1";
|
|
275
|
-
}
|
|
276
|
-
return "p2";
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function extractChangedFilesFromDiff(diffText: string): string[] {
|
|
280
|
-
const files = new Set<string>();
|
|
281
|
-
const lines = diffText.split("\n");
|
|
282
|
-
for (const line of lines) {
|
|
283
|
-
if (!line.startsWith("diff --git ")) continue;
|
|
284
|
-
const parts = line.split(" ");
|
|
285
|
-
if (parts.length >= 4) {
|
|
286
|
-
const bPath = parts[3].replace(/^b\//, "");
|
|
287
|
-
if (bPath) files.add(bPath);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return Array.from(files);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function scoreUnitAgainstDiff(
|
|
294
|
-
unit: Unit,
|
|
295
|
-
diffText: string,
|
|
296
|
-
changedFiles: string[],
|
|
297
|
-
): { score: number; reasons: string[] } {
|
|
298
|
-
const reasons: string[] = [];
|
|
299
|
-
let score = 0;
|
|
300
|
-
const corpus = `${unit.title} ${unit.summary}`.toLowerCase();
|
|
301
|
-
const diffLower = diffText.toLowerCase();
|
|
302
|
-
|
|
303
|
-
for (const tag of unit.tags) {
|
|
304
|
-
if (!tag) continue;
|
|
305
|
-
const tagLower = tag.toLowerCase();
|
|
306
|
-
if (diffLower.includes(tagLower)) {
|
|
307
|
-
score += 3;
|
|
308
|
-
reasons.push(`tag:${tag}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const keywords = corpus
|
|
313
|
-
.split(/[^a-zA-Z0-9_-]+/)
|
|
314
|
-
.filter((token) => token.length >= 5)
|
|
315
|
-
.slice(0, 20);
|
|
316
|
-
for (const token of keywords) {
|
|
317
|
-
if (diffLower.includes(token)) {
|
|
318
|
-
score += 1;
|
|
319
|
-
reasons.push(`keyword:${token}`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
for (const filePath of changedFiles) {
|
|
324
|
-
const lower = filePath.toLowerCase();
|
|
325
|
-
if (corpus.includes("test") && lower.includes("test")) {
|
|
326
|
-
score += 1;
|
|
327
|
-
reasons.push("path:test");
|
|
328
|
-
}
|
|
329
|
-
if (
|
|
330
|
-
(corpus.includes("api") || unit.tags.includes("api")) &&
|
|
331
|
-
(lower.includes("api") || lower.includes("route"))
|
|
332
|
-
) {
|
|
333
|
-
score += 1;
|
|
334
|
-
reasons.push("path:api");
|
|
335
|
-
}
|
|
336
|
-
if (
|
|
337
|
-
(corpus.includes("db") || corpus.includes("sql")) &&
|
|
338
|
-
(lower.includes("db") ||
|
|
339
|
-
lower.includes("prisma") ||
|
|
340
|
-
lower.includes("migration"))
|
|
341
|
-
) {
|
|
342
|
-
score += 1;
|
|
343
|
-
reasons.push("path:db");
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return { score, reasons: Array.from(new Set(reasons)) };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
157
|
// Database connection (lazy initialization)
|
|
351
158
|
let db: DatabaseSyncType | null = null;
|
|
352
159
|
|
|
@@ -1009,6 +816,17 @@ async function saveInteractions(
|
|
|
1009
816
|
id: `int-${String(idx + 1).padStart(3, "0")}`,
|
|
1010
817
|
}));
|
|
1011
818
|
|
|
819
|
+
// Guard: don't delete existing data when there's nothing to insert
|
|
820
|
+
if (finalInteractions.length === 0) {
|
|
821
|
+
return {
|
|
822
|
+
success: true,
|
|
823
|
+
savedCount: 0,
|
|
824
|
+
mergedFromBackup: backupInteractions.length,
|
|
825
|
+
message:
|
|
826
|
+
"No interactions to save (transcript may have no text user messages). Existing data preserved.",
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
1012
830
|
// Delete existing interactions for this session
|
|
1013
831
|
try {
|
|
1014
832
|
const deleteStmt = database.prepare(
|
|
@@ -1227,26 +1045,6 @@ function runSearchBenchmark(limit = SEARCH_EVAL_DEFAULT_LIMIT): {
|
|
|
1227
1045
|
};
|
|
1228
1046
|
}
|
|
1229
1047
|
|
|
1230
|
-
function buildUnitGraph(units: Unit[]) {
|
|
1231
|
-
const approved = units.filter((unit) => unit.status === "approved");
|
|
1232
|
-
const edges: Array<{ source: string; target: string; weight: number }> = [];
|
|
1233
|
-
for (let i = 0; i < approved.length; i++) {
|
|
1234
|
-
for (let j = i + 1; j < approved.length; j++) {
|
|
1235
|
-
const shared = approved[i].tags.filter((tag) =>
|
|
1236
|
-
approved[j].tags.includes(tag),
|
|
1237
|
-
);
|
|
1238
|
-
if (shared.length > 0) {
|
|
1239
|
-
edges.push({
|
|
1240
|
-
source: approved[i].id,
|
|
1241
|
-
target: approved[j].id,
|
|
1242
|
-
weight: shared.length,
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
return { nodes: approved, edges };
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
1048
|
// MCP Server setup
|
|
1251
1049
|
const server = new McpServer({
|
|
1252
1050
|
name: "mneme-db",
|
|
@@ -1422,160 +1220,177 @@ server.registerTool(
|
|
|
1422
1220
|
},
|
|
1423
1221
|
);
|
|
1424
1222
|
|
|
1425
|
-
// Tool:
|
|
1223
|
+
// Tool: mneme_update_session_summary
|
|
1426
1224
|
server.registerTool(
|
|
1427
|
-
"
|
|
1225
|
+
"mneme_update_session_summary",
|
|
1428
1226
|
{
|
|
1429
1227
|
description:
|
|
1430
|
-
"
|
|
1431
|
-
"
|
|
1432
|
-
"
|
|
1228
|
+
"Update session JSON file with summary data. " +
|
|
1229
|
+
"MUST be called during /mneme:save Phase 3 to persist session metadata. " +
|
|
1230
|
+
"Creates the session file if it does not exist (e.g. when SessionStart hook was skipped).",
|
|
1433
1231
|
inputSchema: {
|
|
1434
1232
|
claudeSessionId: z
|
|
1435
1233
|
.string()
|
|
1436
1234
|
.min(8)
|
|
1437
1235
|
.describe("Full Claude Code session UUID (36 chars)"),
|
|
1236
|
+
title: z.string().describe("Session title"),
|
|
1237
|
+
summary: z
|
|
1238
|
+
.object({
|
|
1239
|
+
goal: z.string().describe("What the session aimed to accomplish"),
|
|
1240
|
+
outcome: z.string().describe("What was actually accomplished"),
|
|
1241
|
+
description: z
|
|
1242
|
+
.string()
|
|
1243
|
+
.optional()
|
|
1244
|
+
.describe("Detailed description of the session"),
|
|
1245
|
+
})
|
|
1246
|
+
.describe("Session summary object"),
|
|
1247
|
+
tags: z
|
|
1248
|
+
.array(z.string())
|
|
1249
|
+
.optional()
|
|
1250
|
+
.describe("Semantic tags for the session"),
|
|
1251
|
+
sessionType: z
|
|
1252
|
+
.string()
|
|
1253
|
+
.optional()
|
|
1254
|
+
.describe(
|
|
1255
|
+
"Session type (e.g. implementation, research, bugfix, refactor)",
|
|
1256
|
+
),
|
|
1438
1257
|
},
|
|
1439
1258
|
},
|
|
1440
|
-
async ({ claudeSessionId }) => {
|
|
1259
|
+
async ({ claudeSessionId, title, summary, tags, sessionType }) => {
|
|
1441
1260
|
if (!claudeSessionId.trim()) {
|
|
1442
1261
|
return fail("claudeSessionId must not be empty.");
|
|
1443
1262
|
}
|
|
1444
|
-
const success = markSessionCommitted(claudeSessionId);
|
|
1445
|
-
return {
|
|
1446
|
-
...ok(JSON.stringify({ success, claudeSessionId }, null, 2)),
|
|
1447
|
-
isError: !success,
|
|
1448
|
-
};
|
|
1449
|
-
},
|
|
1450
|
-
);
|
|
1451
1263
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
.
|
|
1462
|
-
.
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
.
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1264
|
+
const projectPath = getProjectPath();
|
|
1265
|
+
const sessionsDir = path.join(projectPath, ".mneme", "sessions");
|
|
1266
|
+
const shortId = claudeSessionId.slice(0, 8);
|
|
1267
|
+
|
|
1268
|
+
// Find existing session file
|
|
1269
|
+
let sessionFile: string | null = null;
|
|
1270
|
+
const searchDir = (dir: string): string | null => {
|
|
1271
|
+
if (!fs.existsSync(dir)) return null;
|
|
1272
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1273
|
+
const fullPath = path.join(dir, entry.name);
|
|
1274
|
+
if (entry.isDirectory()) {
|
|
1275
|
+
const result = searchDir(fullPath);
|
|
1276
|
+
if (result) return result;
|
|
1277
|
+
} else if (entry.name === `${shortId}.json`) {
|
|
1278
|
+
return fullPath;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return null;
|
|
1282
|
+
};
|
|
1283
|
+
sessionFile = searchDir(sessionsDir);
|
|
1284
|
+
|
|
1285
|
+
// Create session file if not found (SessionStart hook may not have run)
|
|
1286
|
+
if (!sessionFile) {
|
|
1287
|
+
const now = new Date();
|
|
1288
|
+
const yearMonth = path.join(
|
|
1289
|
+
sessionsDir,
|
|
1290
|
+
String(now.getFullYear()),
|
|
1291
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
1292
|
+
);
|
|
1293
|
+
if (!fs.existsSync(yearMonth)) {
|
|
1294
|
+
fs.mkdirSync(yearMonth, { recursive: true });
|
|
1295
|
+
}
|
|
1296
|
+
sessionFile = path.join(yearMonth, `${shortId}.json`);
|
|
1297
|
+
// Write minimal session JSON
|
|
1298
|
+
const initial = {
|
|
1299
|
+
id: shortId,
|
|
1300
|
+
sessionId: claudeSessionId,
|
|
1301
|
+
createdAt: now.toISOString(),
|
|
1302
|
+
title: "",
|
|
1303
|
+
tags: [],
|
|
1304
|
+
context: {
|
|
1305
|
+
projectDir: projectPath,
|
|
1306
|
+
projectName: path.basename(projectPath),
|
|
1483
1307
|
},
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
server.registerTool(
|
|
1493
|
-
"mneme_unit_queue_update_status",
|
|
1494
|
-
{
|
|
1495
|
-
description:
|
|
1496
|
-
"Update unit status (approve/reject/pending) in bulk or single item.",
|
|
1497
|
-
inputSchema: {
|
|
1498
|
-
unitIds: z.array(z.string().min(1)).min(1).describe("Target unit IDs"),
|
|
1499
|
-
status: z
|
|
1500
|
-
.enum(["pending", "approved", "rejected"])
|
|
1501
|
-
.describe("New status"),
|
|
1502
|
-
reviewedBy: z.string().optional().describe("Reviewer name (optional)"),
|
|
1503
|
-
},
|
|
1504
|
-
},
|
|
1505
|
-
async ({ unitIds, status, reviewedBy }) => {
|
|
1506
|
-
const doc = readUnits();
|
|
1507
|
-
const target = new Set(unitIds);
|
|
1508
|
-
const now = new Date().toISOString();
|
|
1509
|
-
let updated = 0;
|
|
1510
|
-
doc.items = doc.items.map((item) => {
|
|
1511
|
-
if (!target.has(item.id)) return item;
|
|
1512
|
-
updated += 1;
|
|
1513
|
-
return {
|
|
1514
|
-
...item,
|
|
1515
|
-
status,
|
|
1516
|
-
reviewedAt: now,
|
|
1517
|
-
reviewedBy,
|
|
1518
|
-
updatedAt: now,
|
|
1308
|
+
metrics: {
|
|
1309
|
+
userMessages: 0,
|
|
1310
|
+
assistantResponses: 0,
|
|
1311
|
+
thinkingBlocks: 0,
|
|
1312
|
+
toolUsage: [],
|
|
1313
|
+
},
|
|
1314
|
+
files: [],
|
|
1315
|
+
status: null,
|
|
1519
1316
|
};
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
writeUnits(doc);
|
|
1523
|
-
return ok(
|
|
1524
|
-
JSON.stringify(
|
|
1525
|
-
{ updated, status, requested: unitIds.length, updatedAt: now },
|
|
1526
|
-
null,
|
|
1527
|
-
2,
|
|
1528
|
-
),
|
|
1529
|
-
);
|
|
1530
|
-
},
|
|
1531
|
-
);
|
|
1317
|
+
fs.writeFileSync(sessionFile, JSON.stringify(initial, null, 2));
|
|
1318
|
+
}
|
|
1532
1319
|
|
|
1533
|
-
//
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
.
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1320
|
+
// Read, update, write
|
|
1321
|
+
const data = readJsonFile<Record<string, unknown>>(sessionFile) ?? {};
|
|
1322
|
+
data.title = title;
|
|
1323
|
+
data.summary = summary;
|
|
1324
|
+
data.updatedAt = new Date().toISOString();
|
|
1325
|
+
if (tags) data.tags = tags;
|
|
1326
|
+
if (sessionType) data.sessionType = sessionType;
|
|
1327
|
+
|
|
1328
|
+
// Enrich with transcript metrics/files if available
|
|
1329
|
+
const transcriptPath = getTranscriptPath(claudeSessionId);
|
|
1330
|
+
if (transcriptPath) {
|
|
1331
|
+
try {
|
|
1332
|
+
const parsed = await parseTranscript(transcriptPath);
|
|
1333
|
+
data.metrics = {
|
|
1334
|
+
userMessages: parsed.metrics.userMessages,
|
|
1335
|
+
assistantResponses: parsed.metrics.assistantResponses,
|
|
1336
|
+
thinkingBlocks: parsed.metrics.thinkingBlocks,
|
|
1337
|
+
toolUsage: parsed.toolUsage,
|
|
1338
|
+
};
|
|
1339
|
+
if (parsed.files.length > 0) {
|
|
1340
|
+
data.files = parsed.files;
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
// Transcript parse failed, keep existing values
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Enrich context if minimal (missing repository info)
|
|
1348
|
+
const ctx = data.context as Record<string, unknown> | undefined;
|
|
1349
|
+
if (ctx && !ctx.repository) {
|
|
1350
|
+
try {
|
|
1351
|
+
const { execSync } = await import("node:child_process");
|
|
1352
|
+
const cwd = (ctx.projectDir as string) || projectPath;
|
|
1353
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1354
|
+
encoding: "utf8",
|
|
1355
|
+
cwd,
|
|
1356
|
+
}).trim();
|
|
1357
|
+
if (branch) ctx.branch = branch;
|
|
1358
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
1359
|
+
encoding: "utf8",
|
|
1360
|
+
cwd,
|
|
1361
|
+
}).trim();
|
|
1362
|
+
const repoMatch = remoteUrl.match(/[:/]([^/]+\/[^/]+?)(\.git)?$/);
|
|
1363
|
+
if (repoMatch) ctx.repository = repoMatch[1].replace(/\.git$/, "");
|
|
1364
|
+
const userName = execSync("git config user.name", {
|
|
1365
|
+
encoding: "utf8",
|
|
1366
|
+
cwd,
|
|
1367
|
+
}).trim();
|
|
1368
|
+
const userEmail = execSync("git config user.email", {
|
|
1369
|
+
encoding: "utf8",
|
|
1370
|
+
cwd,
|
|
1371
|
+
}).trim();
|
|
1372
|
+
if (userName)
|
|
1373
|
+
ctx.user = {
|
|
1374
|
+
name: userName,
|
|
1375
|
+
...(userEmail ? { email: userEmail } : {}),
|
|
1376
|
+
};
|
|
1377
|
+
} catch {
|
|
1378
|
+
// Not a git repo or git not available
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
fs.writeFileSync(sessionFile, JSON.stringify(data, null, 2));
|
|
1383
|
+
|
|
1384
|
+
// Auto-commit: mark session as committed after successful summary write
|
|
1385
|
+
// This ensures committed flag is set even if mneme_mark_session_committed is not called explicitly
|
|
1386
|
+
markSessionCommitted(claudeSessionId);
|
|
1567
1387
|
|
|
1568
1388
|
return ok(
|
|
1569
1389
|
JSON.stringify(
|
|
1570
1390
|
{
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
title: item.unit.title,
|
|
1575
|
-
score: item.score,
|
|
1576
|
-
reasons: item.reasons,
|
|
1577
|
-
source: `${item.unit.sourceType}:${item.unit.sourceId}`,
|
|
1578
|
-
})),
|
|
1391
|
+
success: true,
|
|
1392
|
+
sessionFile: sessionFile.replace(projectPath, "."),
|
|
1393
|
+
shortId,
|
|
1579
1394
|
},
|
|
1580
1395
|
null,
|
|
1581
1396
|
2,
|
|
@@ -1584,35 +1399,30 @@ server.registerTool(
|
|
|
1584
1399
|
},
|
|
1585
1400
|
);
|
|
1586
1401
|
|
|
1587
|
-
// Tool:
|
|
1402
|
+
// Tool: mneme_mark_session_committed
|
|
1588
1403
|
server.registerTool(
|
|
1589
|
-
"
|
|
1404
|
+
"mneme_mark_session_committed",
|
|
1590
1405
|
{
|
|
1591
|
-
description:
|
|
1406
|
+
description:
|
|
1407
|
+
"Mark a session as committed (saved with /mneme:save). " +
|
|
1408
|
+
"This prevents the session's interactions from being deleted on SessionEnd. " +
|
|
1409
|
+
"Call this after successfully saving session data.",
|
|
1592
1410
|
inputSchema: {
|
|
1593
|
-
|
|
1594
|
-
|
|
1411
|
+
claudeSessionId: z
|
|
1412
|
+
.string()
|
|
1413
|
+
.min(8)
|
|
1414
|
+
.describe("Full Claude Code session UUID (36 chars)"),
|
|
1595
1415
|
},
|
|
1596
1416
|
},
|
|
1597
|
-
async ({
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const
|
|
1602
|
-
return
|
|
1603
|
-
JSON.stringify(
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
title: unit.title,
|
|
1607
|
-
score: scored.score,
|
|
1608
|
-
reasons: scored.reasons,
|
|
1609
|
-
priority: inferUnitPriority(unit),
|
|
1610
|
-
changedFiles,
|
|
1611
|
-
},
|
|
1612
|
-
null,
|
|
1613
|
-
2,
|
|
1614
|
-
),
|
|
1615
|
-
);
|
|
1417
|
+
async ({ claudeSessionId }) => {
|
|
1418
|
+
if (!claudeSessionId.trim()) {
|
|
1419
|
+
return fail("claudeSessionId must not be empty.");
|
|
1420
|
+
}
|
|
1421
|
+
const success = markSessionCommitted(claudeSessionId);
|
|
1422
|
+
return {
|
|
1423
|
+
...ok(JSON.stringify({ success, claudeSessionId }, null, 2)),
|
|
1424
|
+
isError: !success,
|
|
1425
|
+
};
|
|
1616
1426
|
},
|
|
1617
1427
|
);
|
|
1618
1428
|
|
|
@@ -1815,83 +1625,6 @@ server.registerTool(
|
|
|
1815
1625
|
},
|
|
1816
1626
|
);
|
|
1817
1627
|
|
|
1818
|
-
// Tool: mneme_graph_insights
|
|
1819
|
-
server.registerTool(
|
|
1820
|
-
"mneme_graph_insights",
|
|
1821
|
-
{
|
|
1822
|
-
description:
|
|
1823
|
-
"Compute graph insights from approved units: central units, tag communities, orphan units.",
|
|
1824
|
-
inputSchema: {
|
|
1825
|
-
limit: z
|
|
1826
|
-
.number()
|
|
1827
|
-
.int()
|
|
1828
|
-
.min(LIST_LIMIT_MIN)
|
|
1829
|
-
.max(100)
|
|
1830
|
-
.optional()
|
|
1831
|
-
.describe("Limit for ranked outputs (default: 10)"),
|
|
1832
|
-
},
|
|
1833
|
-
},
|
|
1834
|
-
async ({ limit }) => {
|
|
1835
|
-
const k = limit ?? 10;
|
|
1836
|
-
const units = readUnits().items.filter(
|
|
1837
|
-
(item) => item.status === "approved",
|
|
1838
|
-
);
|
|
1839
|
-
const graph = buildUnitGraph(units);
|
|
1840
|
-
const degree = new Map<string, number>();
|
|
1841
|
-
for (const unit of graph.nodes) degree.set(unit.id, 0);
|
|
1842
|
-
for (const edge of graph.edges) {
|
|
1843
|
-
degree.set(edge.source, (degree.get(edge.source) || 0) + 1);
|
|
1844
|
-
degree.set(edge.target, (degree.get(edge.target) || 0) + 1);
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
const topCentral = graph.nodes
|
|
1848
|
-
.map((unit) => ({
|
|
1849
|
-
id: unit.id,
|
|
1850
|
-
title: unit.title,
|
|
1851
|
-
degree: degree.get(unit.id) || 0,
|
|
1852
|
-
}))
|
|
1853
|
-
.sort((a, b) => b.degree - a.degree)
|
|
1854
|
-
.slice(0, k);
|
|
1855
|
-
|
|
1856
|
-
const tagCounts = new Map<string, number>();
|
|
1857
|
-
for (const unit of graph.nodes) {
|
|
1858
|
-
for (const tag of unit.tags) {
|
|
1859
|
-
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
const communities = Array.from(tagCounts.entries())
|
|
1863
|
-
.map(([tag, count]) => ({
|
|
1864
|
-
tag,
|
|
1865
|
-
count,
|
|
1866
|
-
}))
|
|
1867
|
-
.sort((a, b) => b.count - a.count)
|
|
1868
|
-
.slice(0, k);
|
|
1869
|
-
|
|
1870
|
-
const orphans = graph.nodes
|
|
1871
|
-
.filter((unit) => (degree.get(unit.id) || 0) === 0)
|
|
1872
|
-
.map((unit) => ({
|
|
1873
|
-
id: unit.id,
|
|
1874
|
-
title: unit.title,
|
|
1875
|
-
tags: unit.tags,
|
|
1876
|
-
}))
|
|
1877
|
-
.slice(0, k);
|
|
1878
|
-
|
|
1879
|
-
return ok(
|
|
1880
|
-
JSON.stringify(
|
|
1881
|
-
{
|
|
1882
|
-
approvedUnits: graph.nodes.length,
|
|
1883
|
-
edges: graph.edges.length,
|
|
1884
|
-
topCentral,
|
|
1885
|
-
tagCommunities: communities,
|
|
1886
|
-
orphanUnits: orphans,
|
|
1887
|
-
},
|
|
1888
|
-
null,
|
|
1889
|
-
2,
|
|
1890
|
-
),
|
|
1891
|
-
);
|
|
1892
|
-
},
|
|
1893
|
-
);
|
|
1894
|
-
|
|
1895
1628
|
// Tool: mneme_search_eval
|
|
1896
1629
|
server.registerTool(
|
|
1897
1630
|
"mneme_search_eval",
|
|
@@ -1954,74 +1687,6 @@ server.registerTool(
|
|
|
1954
1687
|
},
|
|
1955
1688
|
);
|
|
1956
1689
|
|
|
1957
|
-
// Tool: mneme_audit_query
|
|
1958
|
-
server.registerTool(
|
|
1959
|
-
"mneme_audit_query",
|
|
1960
|
-
{
|
|
1961
|
-
description: "Query unit-related audit logs and summarize change history.",
|
|
1962
|
-
inputSchema: {
|
|
1963
|
-
from: z.string().optional().describe("Start ISO date/time"),
|
|
1964
|
-
to: z.string().optional().describe("End ISO date/time"),
|
|
1965
|
-
targetId: z.string().optional().describe("Filter by target unit ID"),
|
|
1966
|
-
summaryMode: z
|
|
1967
|
-
.enum(["changes", "actors", "target"])
|
|
1968
|
-
.optional()
|
|
1969
|
-
.describe(
|
|
1970
|
-
"changes=list, actors=aggregate by actor, target=single target history",
|
|
1971
|
-
),
|
|
1972
|
-
},
|
|
1973
|
-
},
|
|
1974
|
-
async ({ from, to, targetId, summaryMode }) => {
|
|
1975
|
-
const entries = readAuditEntries({ from, to, entity: "unit" }).filter(
|
|
1976
|
-
(entry) => (targetId ? entry.targetId === targetId : true),
|
|
1977
|
-
);
|
|
1978
|
-
|
|
1979
|
-
if ((summaryMode || "changes") === "actors") {
|
|
1980
|
-
const byActor = new Map<string, number>();
|
|
1981
|
-
for (const entry of entries) {
|
|
1982
|
-
const actor = entry.actor || "unknown";
|
|
1983
|
-
byActor.set(actor, (byActor.get(actor) || 0) + 1);
|
|
1984
|
-
}
|
|
1985
|
-
return ok(
|
|
1986
|
-
JSON.stringify(
|
|
1987
|
-
{
|
|
1988
|
-
total: entries.length,
|
|
1989
|
-
actors: Array.from(byActor.entries())
|
|
1990
|
-
.map(([actor, count]) => ({ actor, count }))
|
|
1991
|
-
.sort((a, b) => b.count - a.count),
|
|
1992
|
-
},
|
|
1993
|
-
null,
|
|
1994
|
-
2,
|
|
1995
|
-
),
|
|
1996
|
-
);
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
if ((summaryMode || "changes") === "target" && targetId) {
|
|
2000
|
-
return ok(
|
|
2001
|
-
JSON.stringify(
|
|
2002
|
-
{
|
|
2003
|
-
targetId,
|
|
2004
|
-
history: entries,
|
|
2005
|
-
},
|
|
2006
|
-
null,
|
|
2007
|
-
2,
|
|
2008
|
-
),
|
|
2009
|
-
);
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
return ok(
|
|
2013
|
-
JSON.stringify(
|
|
2014
|
-
{
|
|
2015
|
-
total: entries.length,
|
|
2016
|
-
changes: entries,
|
|
2017
|
-
},
|
|
2018
|
-
null,
|
|
2019
|
-
2,
|
|
2020
|
-
),
|
|
2021
|
-
);
|
|
2022
|
-
},
|
|
2023
|
-
);
|
|
2024
|
-
|
|
2025
1690
|
// Start server
|
|
2026
1691
|
async function main() {
|
|
2027
1692
|
const transport = new StdioServerTransport();
|