@hir4ta/mneme 0.22.0 → 0.22.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mneme",
3
3
  "description": "A plugin that provides long-term memory for Claude Code. It automatically saves context lost during auto-compact, offering features for session restoration, recording technical decisions, and learning developer patterns.",
4
- "version": "0.22.0",
4
+ "version": "0.22.2",
5
5
  "author": {
6
6
  "name": "hir4ta"
7
7
  },
package/README.ja.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # mneme
2
2
 
3
+ ![Version](https://img.shields.io/badge/version-0.22.2-blue)
4
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D22.5.0-brightgreen)
3
5
  [![NPM Version](https://img.shields.io/npm/v/%40hir4ta%2Fmneme)](https://www.npmjs.com/package/@hir4ta/mneme)
4
6
  [![MIT License](https://img.shields.io/npm/l/%40hir4ta%2Fmneme)](https://github.com/hir4ta/mneme/blob/main/LICENSE)
5
7
 
@@ -24,7 +26,11 @@ Claude Codeの長期記憶を実現するプラグイン
24
26
 
25
27
  Claude Codeのセッションは終了やAuto-Compactで文脈が失われ、過去の判断が追えず、知見の再利用が困難です。
26
28
 
27
- **mnemeでの解決**: 自動保存と再開、毎プロンプトでの自動記憶検索、判断・パターン履歴の検索・ダッシュボード参照。`.mneme/` のJSONはGit管理可能で、チームでの知見共有にも対応します。
29
+ **よくある問題**: セッション間での文脈喪失、同じミスの繰り返し、不透明な設計判断
30
+
31
+ **mnemeでの解決**: 自動保存と再開、毎プロンプトでの自動記憶検索、判断・パターン履歴の検索
32
+
33
+ **チームでの利点**: `.mneme/` のJSONファイルはGit管理され、判断やセッション履歴をチームで共有できます。
28
34
 
29
35
  ## インストール
30
36
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mneme
2
2
 
3
- ![Version](https://img.shields.io/badge/version-0.22.0-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.22.2-blue)
4
4
  ![Node.js](https://img.shields.io/badge/node-%3E%3D22.5.0-brightgreen)
5
5
  [![NPM Version](https://img.shields.io/npm/v/%40hir4ta%2Fmneme)](https://www.npmjs.com/package/@hir4ta/mneme)
6
6
  [![MIT License](https://img.shields.io/npm/l/%40hir4ta%2Fmneme)](https://github.com/hir4ta/mneme/blob/main/LICENSE)
@@ -875,15 +875,26 @@ async function sessionFinalize(sessionId, cwd, transcriptPath, cleanupPolicy, gr
875
875
  console.error(
876
876
  `[mneme] Session ended without /mneme:save - cleaned up ${cleanupResult.count} interactions`
877
877
  );
878
+ fs3.unlinkSync(sessionFile);
879
+ const linkFile = path3.join(sessionLinksDir, `${sessionShortId}.json`);
880
+ if (fs3.existsSync(linkFile)) {
881
+ fs3.unlinkSync(linkFile);
882
+ }
883
+ console.error(
884
+ "[mneme] Session completed (not saved, cleaned up immediately)"
885
+ );
886
+ } else {
887
+ data.status = "complete";
888
+ data.endedAt = now;
889
+ data.updatedAt = now;
890
+ delete data.uncommitted;
891
+ delete data.interactions;
892
+ delete data.preCompactBackups;
893
+ safeWriteJson(sessionFile, data);
894
+ console.error(
895
+ "[mneme] Session completed (committed in SQLite, kept despite missing summary)"
896
+ );
878
897
  }
879
- fs3.unlinkSync(sessionFile);
880
- const linkFile = path3.join(sessionLinksDir, `${sessionShortId}.json`);
881
- if (fs3.existsSync(linkFile)) {
882
- fs3.unlinkSync(linkFile);
883
- }
884
- console.error(
885
- "[mneme] Session completed (not saved, cleaned up immediately)"
886
- );
887
898
  } else if (cleanupPolicy === "never") {
888
899
  console.error(
889
900
  "[mneme] Session completed (not saved, kept as uncommitted)"
@@ -31069,6 +31069,14 @@ async function saveInteractions(claudeSessionId, mnemeSessionId) {
31069
31069
  ...interaction,
31070
31070
  id: `int-${String(idx + 1).padStart(3, "0")}`
31071
31071
  }));
31072
+ if (finalInteractions.length === 0) {
31073
+ return {
31074
+ success: true,
31075
+ savedCount: 0,
31076
+ mergedFromBackup: backupInteractions.length,
31077
+ message: "No interactions to save (transcript may have no text user messages). Existing data preserved."
31078
+ };
31079
+ }
31072
31080
  try {
31073
31081
  const deleteStmt = database.prepare(
31074
31082
  "DELETE FROM interactions WHERE session_id = ?"
@@ -31377,6 +31385,147 @@ server.registerTool(
31377
31385
  };
31378
31386
  }
31379
31387
  );
31388
+ server.registerTool(
31389
+ "mneme_update_session_summary",
31390
+ {
31391
+ description: "Update session JSON file with summary data. MUST be called during /mneme:save Phase 3 to persist session metadata. Creates the session file if it does not exist (e.g. when SessionStart hook was skipped).",
31392
+ inputSchema: {
31393
+ claudeSessionId: external_exports3.string().min(8).describe("Full Claude Code session UUID (36 chars)"),
31394
+ title: external_exports3.string().describe("Session title"),
31395
+ summary: external_exports3.object({
31396
+ goal: external_exports3.string().describe("What the session aimed to accomplish"),
31397
+ outcome: external_exports3.string().describe("What was actually accomplished"),
31398
+ description: external_exports3.string().optional().describe("Detailed description of the session")
31399
+ }).describe("Session summary object"),
31400
+ tags: external_exports3.array(external_exports3.string()).optional().describe("Semantic tags for the session"),
31401
+ sessionType: external_exports3.string().optional().describe(
31402
+ "Session type (e.g. implementation, research, bugfix, refactor)"
31403
+ )
31404
+ }
31405
+ },
31406
+ async ({ claudeSessionId, title, summary, tags, sessionType }) => {
31407
+ if (!claudeSessionId.trim()) {
31408
+ return fail("claudeSessionId must not be empty.");
31409
+ }
31410
+ const projectPath = getProjectPath();
31411
+ const sessionsDir = path4.join(projectPath, ".mneme", "sessions");
31412
+ const shortId = claudeSessionId.slice(0, 8);
31413
+ let sessionFile = null;
31414
+ const searchDir = (dir) => {
31415
+ if (!fs4.existsSync(dir)) return null;
31416
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
31417
+ const fullPath = path4.join(dir, entry.name);
31418
+ if (entry.isDirectory()) {
31419
+ const result = searchDir(fullPath);
31420
+ if (result) return result;
31421
+ } else if (entry.name === `${shortId}.json`) {
31422
+ return fullPath;
31423
+ }
31424
+ }
31425
+ return null;
31426
+ };
31427
+ sessionFile = searchDir(sessionsDir);
31428
+ if (!sessionFile) {
31429
+ const now = /* @__PURE__ */ new Date();
31430
+ const yearMonth = path4.join(
31431
+ sessionsDir,
31432
+ String(now.getFullYear()),
31433
+ String(now.getMonth() + 1).padStart(2, "0")
31434
+ );
31435
+ if (!fs4.existsSync(yearMonth)) {
31436
+ fs4.mkdirSync(yearMonth, { recursive: true });
31437
+ }
31438
+ sessionFile = path4.join(yearMonth, `${shortId}.json`);
31439
+ const initial = {
31440
+ id: shortId,
31441
+ sessionId: claudeSessionId,
31442
+ createdAt: now.toISOString(),
31443
+ title: "",
31444
+ tags: [],
31445
+ context: {
31446
+ projectDir: projectPath,
31447
+ projectName: path4.basename(projectPath)
31448
+ },
31449
+ metrics: {
31450
+ userMessages: 0,
31451
+ assistantResponses: 0,
31452
+ thinkingBlocks: 0,
31453
+ toolUsage: []
31454
+ },
31455
+ files: [],
31456
+ status: null
31457
+ };
31458
+ fs4.writeFileSync(sessionFile, JSON.stringify(initial, null, 2));
31459
+ }
31460
+ const data = readJsonFile(sessionFile) ?? {};
31461
+ data.title = title;
31462
+ data.summary = summary;
31463
+ data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
31464
+ if (tags) data.tags = tags;
31465
+ if (sessionType) data.sessionType = sessionType;
31466
+ const transcriptPath = getTranscriptPath(claudeSessionId);
31467
+ if (transcriptPath) {
31468
+ try {
31469
+ const parsed = await parseTranscript(transcriptPath);
31470
+ data.metrics = {
31471
+ userMessages: parsed.metrics.userMessages,
31472
+ assistantResponses: parsed.metrics.assistantResponses,
31473
+ thinkingBlocks: parsed.metrics.thinkingBlocks,
31474
+ toolUsage: parsed.toolUsage
31475
+ };
31476
+ if (parsed.files.length > 0) {
31477
+ data.files = parsed.files;
31478
+ }
31479
+ } catch {
31480
+ }
31481
+ }
31482
+ const ctx = data.context;
31483
+ if (ctx && !ctx.repository) {
31484
+ try {
31485
+ const { execSync } = await import("node:child_process");
31486
+ const cwd = ctx.projectDir || projectPath;
31487
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
31488
+ encoding: "utf8",
31489
+ cwd
31490
+ }).trim();
31491
+ if (branch) ctx.branch = branch;
31492
+ const remoteUrl = execSync("git remote get-url origin", {
31493
+ encoding: "utf8",
31494
+ cwd
31495
+ }).trim();
31496
+ const repoMatch = remoteUrl.match(/[:/]([^/]+\/[^/]+?)(\.git)?$/);
31497
+ if (repoMatch) ctx.repository = repoMatch[1].replace(/\.git$/, "");
31498
+ const userName = execSync("git config user.name", {
31499
+ encoding: "utf8",
31500
+ cwd
31501
+ }).trim();
31502
+ const userEmail = execSync("git config user.email", {
31503
+ encoding: "utf8",
31504
+ cwd
31505
+ }).trim();
31506
+ if (userName)
31507
+ ctx.user = {
31508
+ name: userName,
31509
+ ...userEmail ? { email: userEmail } : {}
31510
+ };
31511
+ } catch {
31512
+ }
31513
+ }
31514
+ fs4.writeFileSync(sessionFile, JSON.stringify(data, null, 2));
31515
+ markSessionCommitted(claudeSessionId);
31516
+ return ok(
31517
+ JSON.stringify(
31518
+ {
31519
+ success: true,
31520
+ sessionFile: sessionFile.replace(projectPath, "."),
31521
+ shortId
31522
+ },
31523
+ null,
31524
+ 2
31525
+ )
31526
+ );
31527
+ }
31528
+ );
31380
31529
  server.registerTool(
31381
31530
  "mneme_mark_session_committed",
31382
31531
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hir4ta/mneme",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "description": "Long-term memory plugin for Claude Code - automated session saving, recording technical decisions, and web dashboard",
5
5
  "keywords": [
6
6
  "claude",
@@ -45,6 +45,7 @@ interface UnitItem {
45
45
  tags: string[];
46
46
  sourceType: "decision" | "pattern" | "rule";
47
47
  sourceId: string;
48
+ sourceRefs: Array<{ type: "decision" | "pattern" | "rule"; id: string }>;
48
49
  status: "pending" | "approved" | "rejected";
49
50
  createdAt: string;
50
51
  updatedAt: string;
@@ -271,11 +272,11 @@ function renderHtml(params: {
271
272
  {
272
273
  en:
273
274
  pendingUnits.length > 0
274
- ? `Pending approvals remain (${pendingUnits.length}). Prioritize high-impact units first.`
275
- : "No pending units. Approval queue is healthy.",
275
+ ? `Pending approvals remain (${pendingUnits.length}). Prioritize high-impact dev rules first.`
276
+ : "No pending dev rules. Approval queue is healthy.",
276
277
  ja:
277
278
  pendingUnits.length > 0
278
- ? `承認待ちユニットが ${pendingUnits.length} 件あります。影響の大きいものから優先承認してください。`
279
+ ? `承認待ちの開発ルールが ${pendingUnits.length} 件あります。影響の大きいものから優先承認してください。`
279
280
  : "承認待ちはありません。承認キューは健全です。",
280
281
  },
281
282
  {
@@ -291,11 +292,11 @@ function renderHtml(params: {
291
292
  {
292
293
  en:
293
294
  newPatterns.length > 0
294
- ? `New patterns detected (${newPatterns.length}). Promote stable ones to approved units.`
295
+ ? `New patterns detected (${newPatterns.length}). Promote stable ones to approved dev rules.`
295
296
  : "Low pattern capture. Run /mneme:save more frequently during debugging sessions.",
296
297
  ja:
297
298
  newPatterns.length > 0
298
- ? `今週の新規パターンは ${newPatterns.length} 件です。安定したものは承認済みユニットへ昇格してください。`
299
+ ? `今週の新規パターンは ${newPatterns.length} 件です。安定したものは承認済み開発ルールへ昇格してください。`
299
300
  : "パターンの蓄積が少なめです。デバッグ中は /mneme:save をこまめに実行してください。",
300
301
  },
301
302
  ];
@@ -652,8 +653,8 @@ function renderHtml(params: {
652
653
  <span data-i18n="heroTitleJa">週次ナレッジスナップショット</span>
653
654
  </h1>
654
655
  <p>
655
- <span data-i18n="heroDescEn">This week, your team captured and refined project knowledge across sessions, units, and source artifacts.</span>
656
- <span data-i18n="heroDescJa">この1週間で、チームはセッション・ユニット・元データを通じて知見を蓄積し、整理しました。</span>
656
+ <span data-i18n="heroDescEn">This week, your team captured and refined project knowledge across sessions, development rules, and source artifacts.</span>
657
+ <span data-i18n="heroDescJa">この1週間で、チームはセッション・開発ルール・元データを通じて知見を蓄積し、整理しました。</span>
657
658
  </p>
658
659
  <div class="meta">
659
660
  <span data-i18n="metaEn">Period: ${formatDate(from)} to ${formatDate(to)} | Generated: ${generatedAt.toISOString()}</span>
@@ -671,9 +672,9 @@ function renderHtml(params: {
671
672
  <article class="kpi"><div class="label"><span data-i18n="kpiDecisionEn">New Decisions</span><span data-i18n="kpiDecisionJa">新規意思決定</span></div><div class="value">${newDecisions.length}</div></article>
672
673
  <article class="kpi"><div class="label"><span data-i18n="kpiPatternEn">New Patterns</span><span data-i18n="kpiPatternJa">新規パターン</span></div><div class="value">${newPatterns.length}</div></article>
673
674
  <article class="kpi"><div class="label"><span data-i18n="kpiRuleEn">Changed Rules</span><span data-i18n="kpiRuleJa">変更ルール</span></div><div class="value">${changedRules.length}</div></article>
674
- <article class="kpi"><div class="label"><span data-i18n="kpiTouchedEn">Touched Units</span><span data-i18n="kpiTouchedJa">更新ユニット</span></div><div class="value">${touchedUnits.length}</div></article>
675
- <article class="kpi"><div class="label"><span data-i18n="kpiApprovedEn">Approved Units</span><span data-i18n="kpiApprovedJa">承認済みユニット</span></div><div class="value">${approvedUnits.length}</div></article>
676
- <article class="kpi"><div class="label"><span data-i18n="kpiPendingEn">Pending Units</span><span data-i18n="kpiPendingJa">承認待ちユニット</span></div><div class="value">${pendingUnits.length}</div></article>
675
+ <article class="kpi"><div class="label"><span data-i18n="kpiTouchedEn">Touched Dev Rules</span><span data-i18n="kpiTouchedJa">更新された開発ルール</span></div><div class="value">${touchedUnits.length}</div></article>
676
+ <article class="kpi"><div class="label"><span data-i18n="kpiApprovedEn">Approved Dev Rules</span><span data-i18n="kpiApprovedJa">承認済み開発ルール</span></div><div class="value">${approvedUnits.length}</div></article>
677
+ <article class="kpi"><div class="label"><span data-i18n="kpiPendingEn">Pending Dev Rules</span><span data-i18n="kpiPendingJa">承認待ち開発ルール</span></div><div class="value">${pendingUnits.length}</div></article>
677
678
  </section>
678
679
 
679
680
  <section class="section">
@@ -684,8 +685,8 @@ function renderHtml(params: {
684
685
  <div class="pulse-value">${approvalRate}%</div>
685
686
  <div class="meter"><i style="width:${approvalRate}%"></i></div>
686
687
  <div class="pulse-note">
687
- <span data-i18n="pulseApprovalNoteEn">Among units touched this week, how many were approved.</span>
688
- <span data-i18n="pulseApprovalNoteJa">今週更新されたユニットのうち、承認済みになった割合です。</span>
688
+ <span data-i18n="pulseApprovalNoteEn">Among dev rules touched this week, how many were approved.</span>
689
+ <span data-i18n="pulseApprovalNoteJa">今週更新された開発ルールのうち、承認済みになった割合です。</span>
689
690
  </div>
690
691
  </article>
691
692
  <article class="pulse">
@@ -702,8 +703,8 @@ function renderHtml(params: {
702
703
  <div class="pulse-value">${pendingUnits.length}</div>
703
704
  <div class="meter"><i style="width:${Math.min(100, pendingUnits.length * 10)}%"></i></div>
704
705
  <div class="pulse-note">
705
- <span data-i18n="pulseQueueNoteEn">Units waiting for approval in the project-wide queue.</span>
706
- <span data-i18n="pulseQueueNoteJa">プロジェクト全体で現在承認待ちのユニット件数です。</span>
706
+ <span data-i18n="pulseQueueNoteEn">Dev rules waiting for approval in the project-wide queue.</span>
707
+ <span data-i18n="pulseQueueNoteJa">プロジェクト全体で現在承認待ちの開発ルール件数です。</span>
707
708
  </div>
708
709
  </article>
709
710
  </div>
@@ -1009,6 +1009,17 @@ async function saveInteractions(
1009
1009
  id: `int-${String(idx + 1).padStart(3, "0")}`,
1010
1010
  }));
1011
1011
 
1012
+ // Guard: don't delete existing data when there's nothing to insert
1013
+ if (finalInteractions.length === 0) {
1014
+ return {
1015
+ success: true,
1016
+ savedCount: 0,
1017
+ mergedFromBackup: backupInteractions.length,
1018
+ message:
1019
+ "No interactions to save (transcript may have no text user messages). Existing data preserved.",
1020
+ };
1021
+ }
1022
+
1012
1023
  // Delete existing interactions for this session
1013
1024
  try {
1014
1025
  const deleteStmt = database.prepare(
@@ -1422,6 +1433,185 @@ server.registerTool(
1422
1433
  },
1423
1434
  );
1424
1435
 
1436
+ // Tool: mneme_update_session_summary
1437
+ server.registerTool(
1438
+ "mneme_update_session_summary",
1439
+ {
1440
+ description:
1441
+ "Update session JSON file with summary data. " +
1442
+ "MUST be called during /mneme:save Phase 3 to persist session metadata. " +
1443
+ "Creates the session file if it does not exist (e.g. when SessionStart hook was skipped).",
1444
+ inputSchema: {
1445
+ claudeSessionId: z
1446
+ .string()
1447
+ .min(8)
1448
+ .describe("Full Claude Code session UUID (36 chars)"),
1449
+ title: z.string().describe("Session title"),
1450
+ summary: z
1451
+ .object({
1452
+ goal: z.string().describe("What the session aimed to accomplish"),
1453
+ outcome: z.string().describe("What was actually accomplished"),
1454
+ description: z
1455
+ .string()
1456
+ .optional()
1457
+ .describe("Detailed description of the session"),
1458
+ })
1459
+ .describe("Session summary object"),
1460
+ tags: z
1461
+ .array(z.string())
1462
+ .optional()
1463
+ .describe("Semantic tags for the session"),
1464
+ sessionType: z
1465
+ .string()
1466
+ .optional()
1467
+ .describe(
1468
+ "Session type (e.g. implementation, research, bugfix, refactor)",
1469
+ ),
1470
+ },
1471
+ },
1472
+ async ({ claudeSessionId, title, summary, tags, sessionType }) => {
1473
+ if (!claudeSessionId.trim()) {
1474
+ return fail("claudeSessionId must not be empty.");
1475
+ }
1476
+
1477
+ const projectPath = getProjectPath();
1478
+ const sessionsDir = path.join(projectPath, ".mneme", "sessions");
1479
+ const shortId = claudeSessionId.slice(0, 8);
1480
+
1481
+ // Find existing session file
1482
+ let sessionFile: string | null = null;
1483
+ const searchDir = (dir: string): string | null => {
1484
+ if (!fs.existsSync(dir)) return null;
1485
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1486
+ const fullPath = path.join(dir, entry.name);
1487
+ if (entry.isDirectory()) {
1488
+ const result = searchDir(fullPath);
1489
+ if (result) return result;
1490
+ } else if (entry.name === `${shortId}.json`) {
1491
+ return fullPath;
1492
+ }
1493
+ }
1494
+ return null;
1495
+ };
1496
+ sessionFile = searchDir(sessionsDir);
1497
+
1498
+ // Create session file if not found (SessionStart hook may not have run)
1499
+ if (!sessionFile) {
1500
+ const now = new Date();
1501
+ const yearMonth = path.join(
1502
+ sessionsDir,
1503
+ String(now.getFullYear()),
1504
+ String(now.getMonth() + 1).padStart(2, "0"),
1505
+ );
1506
+ if (!fs.existsSync(yearMonth)) {
1507
+ fs.mkdirSync(yearMonth, { recursive: true });
1508
+ }
1509
+ sessionFile = path.join(yearMonth, `${shortId}.json`);
1510
+ // Write minimal session JSON
1511
+ const initial = {
1512
+ id: shortId,
1513
+ sessionId: claudeSessionId,
1514
+ createdAt: now.toISOString(),
1515
+ title: "",
1516
+ tags: [],
1517
+ context: {
1518
+ projectDir: projectPath,
1519
+ projectName: path.basename(projectPath),
1520
+ },
1521
+ metrics: {
1522
+ userMessages: 0,
1523
+ assistantResponses: 0,
1524
+ thinkingBlocks: 0,
1525
+ toolUsage: [],
1526
+ },
1527
+ files: [],
1528
+ status: null,
1529
+ };
1530
+ fs.writeFileSync(sessionFile, JSON.stringify(initial, null, 2));
1531
+ }
1532
+
1533
+ // Read, update, write
1534
+ const data = readJsonFile<Record<string, unknown>>(sessionFile) ?? {};
1535
+ data.title = title;
1536
+ data.summary = summary;
1537
+ data.updatedAt = new Date().toISOString();
1538
+ if (tags) data.tags = tags;
1539
+ if (sessionType) data.sessionType = sessionType;
1540
+
1541
+ // Enrich with transcript metrics/files if available
1542
+ const transcriptPath = getTranscriptPath(claudeSessionId);
1543
+ if (transcriptPath) {
1544
+ try {
1545
+ const parsed = await parseTranscript(transcriptPath);
1546
+ data.metrics = {
1547
+ userMessages: parsed.metrics.userMessages,
1548
+ assistantResponses: parsed.metrics.assistantResponses,
1549
+ thinkingBlocks: parsed.metrics.thinkingBlocks,
1550
+ toolUsage: parsed.toolUsage,
1551
+ };
1552
+ if (parsed.files.length > 0) {
1553
+ data.files = parsed.files;
1554
+ }
1555
+ } catch {
1556
+ // Transcript parse failed, keep existing values
1557
+ }
1558
+ }
1559
+
1560
+ // Enrich context if minimal (missing repository info)
1561
+ const ctx = data.context as Record<string, unknown> | undefined;
1562
+ if (ctx && !ctx.repository) {
1563
+ try {
1564
+ const { execSync } = await import("node:child_process");
1565
+ const cwd = (ctx.projectDir as string) || projectPath;
1566
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1567
+ encoding: "utf8",
1568
+ cwd,
1569
+ }).trim();
1570
+ if (branch) ctx.branch = branch;
1571
+ const remoteUrl = execSync("git remote get-url origin", {
1572
+ encoding: "utf8",
1573
+ cwd,
1574
+ }).trim();
1575
+ const repoMatch = remoteUrl.match(/[:/]([^/]+\/[^/]+?)(\.git)?$/);
1576
+ if (repoMatch) ctx.repository = repoMatch[1].replace(/\.git$/, "");
1577
+ const userName = execSync("git config user.name", {
1578
+ encoding: "utf8",
1579
+ cwd,
1580
+ }).trim();
1581
+ const userEmail = execSync("git config user.email", {
1582
+ encoding: "utf8",
1583
+ cwd,
1584
+ }).trim();
1585
+ if (userName)
1586
+ ctx.user = {
1587
+ name: userName,
1588
+ ...(userEmail ? { email: userEmail } : {}),
1589
+ };
1590
+ } catch {
1591
+ // Not a git repo or git not available
1592
+ }
1593
+ }
1594
+
1595
+ fs.writeFileSync(sessionFile, JSON.stringify(data, null, 2));
1596
+
1597
+ // Auto-commit: mark session as committed after successful summary write
1598
+ // This ensures committed flag is set even if mneme_mark_session_committed is not called explicitly
1599
+ markSessionCommitted(claudeSessionId);
1600
+
1601
+ return ok(
1602
+ JSON.stringify(
1603
+ {
1604
+ success: true,
1605
+ sessionFile: sessionFile.replace(projectPath, "."),
1606
+ shortId,
1607
+ },
1608
+ null,
1609
+ 2,
1610
+ ),
1611
+ );
1612
+ },
1613
+ );
1614
+
1425
1615
  // Tool: mneme_mark_session_committed
1426
1616
  server.registerTool(
1427
1617
  "mneme_mark_session_committed",
@@ -4,7 +4,7 @@ description: |
4
4
  Extract and persist session outputs, then generate development rule candidates (decision/pattern/rule).
5
5
  Use when: (1) finishing meaningful implementation work, (2) capturing reusable guidance,
6
6
  (3) before ending a long session.
7
- disable-model-invocation: true
7
+ disable-model-invocation: false
8
8
  ---
9
9
 
10
10
  # /mneme:save
@@ -41,11 +41,20 @@ Always render missing required fields as blocking errors before write.
41
41
  - Merge linked/resumed child sessions into the master session.
42
42
 
43
43
  2. **Interactions commit**
44
- - Save transcript interactions to `.mneme/local.db`.
45
- - Mark the Claude session committed.
44
+ - Save transcript interactions to `.mneme/local.db` via `mneme_save_interactions`.
45
+ - Do NOT call `mneme_mark_session_committed` yet (wait until after Phase 3).
46
46
 
47
- 3. **Session summary extraction**
48
- - Update master session JSON (`title/goal/outcome/description/tags/sessionType`).
47
+ 3. **Session summary extraction (required MCP)**
48
+ - Extract from the conversation: `title`, `goal`, `outcome`, `description`, `tags`, `sessionType`.
49
+ - **MUST call `mneme_update_session_summary`** MCP tool with the extracted data.
50
+ This writes the summary to `.mneme/sessions/` JSON file, ensuring the session is preserved on SessionEnd.
51
+ - **Then call `mneme_mark_session_committed`** to finalize the commit.
52
+
53
+ <required>
54
+ - Call `mneme_update_session_summary` with: `claudeSessionId`, `title`, `summary` (`goal`, `outcome`), `tags`, `sessionType`
55
+ - Call `mneme_mark_session_committed` AFTER `mneme_update_session_summary` succeeds
56
+ - Do NOT skip this step even for short/research sessions
57
+ </required>
49
58
 
50
59
  4. **Decision extraction (source)**
51
60
  - Persist concrete choices and rationale to `decisions/YYYY/MM/*.json`.