@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.ja.md +7 -1
- package/README.md +1 -1
- package/dist/lib/session-finalize.js +19 -8
- package/dist/servers/db-server.js +149 -0
- package/package.json +1 -1
- package/scripts/export-weekly-knowledge-html.ts +15 -14
- package/servers/db-server.ts +190 -0
- package/skills/save/SKILL.md +14 -5
|
@@ -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.
|
|
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
|
+

|
|
4
|
+

|
|
3
5
|
[](https://www.npmjs.com/package/@hir4ta/mneme)
|
|
4
6
|
[](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
|
-
|
|
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
|
-

|
|
4
4
|

|
|
5
5
|
[](https://www.npmjs.com/package/@hir4ta/mneme)
|
|
6
6
|
[](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
|
@@ -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
|
|
275
|
-
: "No pending
|
|
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
|
-
?
|
|
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
|
|
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,
|
|
656
|
-
<span data-i18n="heroDescJa">この1
|
|
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
|
|
675
|
-
<article class="kpi"><div class="label"><span data-i18n="kpiApprovedEn">Approved
|
|
676
|
-
<article class="kpi"><div class="label"><span data-i18n="kpiPendingEn">Pending
|
|
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
|
|
688
|
-
<span data-i18n="pulseApprovalNoteJa"
|
|
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">
|
|
706
|
-
<span data-i18n="pulseQueueNoteJa"
|
|
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>
|
package/servers/db-server.ts
CHANGED
|
@@ -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",
|
package/skills/save/SKILL.md
CHANGED
|
@@ -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:
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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`.
|