@hir4ta/mneme 0.24.0 → 0.24.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.24.0",
4
+ "version": "0.24.2",
5
5
  "author": {
6
6
  "name": "hir4ta"
7
7
  },
package/README.ja.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mneme
2
2
 
3
- ![Version](https://img.shields.io/badge/version-0.24.0-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.24.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)
@@ -127,9 +127,8 @@ implement → save → approve rules
127
127
  ```
128
128
 
129
129
  1. **implement**: コードを実装
130
- 2. **save**: 元データを抽出して開発ルール候補を生成
131
- 3. **validate**: `npm run validate:sources` で必須項目/priority/tags を検証
132
- 4. **approve rules**: 生成された開発ルールをインラインで確認・承認/却下
130
+ 2. **save**: 元データを抽出して開発ルール候補を生成(バリデーションはMCP経由で自動実行)
131
+ 3. **approve rules**: 生成された開発ルールをインラインで確認・承認/却下
133
132
 
134
133
  ランタイム詳細(Hook分岐、未保存終了、Auto-Compact)は以下:
135
134
  - `docs/mneme-runtime-flow.md`
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mneme
2
2
 
3
- ![Version](https://img.shields.io/badge/version-0.24.0-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.24.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)
@@ -127,9 +127,8 @@ implement → save → approve rules
127
127
  ```
128
128
 
129
129
  1. **implement**: Write code
130
- 2. **save**: Extract source knowledge and generate development rule candidates
131
- 3. **validate**: Run `npm run validate:sources` to enforce required fields/priority/tags
132
- 4. **approve rules**: Review and approve/reject generated development rules inline
130
+ 2. **save**: Extract source knowledge and generate development rule candidates (validation runs automatically via MCP)
131
+ 3. **approve rules**: Review and approve/reject generated development rules inline
133
132
 
134
133
  Detailed runtime flow (hooks, uncommitted policy, auto-compact path):
135
134
  - `docs/mneme-runtime-flow.md`
@@ -56,13 +56,13 @@ async function getGitInfo(projectPath) {
56
56
  return { owner, repository, repositoryUrl, repositoryRoot };
57
57
  }
58
58
  function resolveMnemeSessionId(projectPath, claudeSessionId) {
59
- const shortId = claudeSessionId.slice(0, 8);
60
- const sessionLinkPath = path.join(
61
- projectPath,
62
- ".mneme",
63
- "session-links",
64
- `${shortId}.json`
59
+ const sessionLinksDir = path.join(projectPath, ".mneme", "session-links");
60
+ const fullPath = path.join(sessionLinksDir, `${claudeSessionId}.json`);
61
+ const shortPath = path.join(
62
+ sessionLinksDir,
63
+ `${claudeSessionId.slice(0, 8)}.json`
65
64
  );
65
+ const sessionLinkPath = fs.existsSync(fullPath) ? fullPath : shortPath;
66
66
  if (fs.existsSync(sessionLinkPath)) {
67
67
  try {
68
68
  const link = JSON.parse(fs.readFileSync(sessionLinkPath, "utf8"));
@@ -72,24 +72,29 @@ function resolveMnemeSessionId(projectPath, claudeSessionId) {
72
72
  } catch {
73
73
  }
74
74
  }
75
- return shortId;
75
+ return claudeSessionId;
76
76
  }
77
77
  function findSessionFileById(projectPath, mnemeSessionId) {
78
78
  const sessionsDir = path.join(projectPath, ".mneme", "sessions");
79
- const searchDir = (dir) => {
79
+ const searchDirFor = (dir, fileName) => {
80
80
  if (!fs.existsSync(dir)) return null;
81
81
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
82
82
  const fullPath = path.join(dir, entry.name);
83
83
  if (entry.isDirectory()) {
84
- const result = searchDir(fullPath);
85
- if (result) return result;
86
- } else if (entry.name === `${mnemeSessionId}.json`) {
84
+ const result2 = searchDirFor(fullPath, fileName);
85
+ if (result2) return result2;
86
+ } else if (entry.name === fileName) {
87
87
  return fullPath;
88
88
  }
89
89
  }
90
90
  return null;
91
91
  };
92
- return searchDir(sessionsDir);
92
+ const result = searchDirFor(sessionsDir, `${mnemeSessionId}.json`);
93
+ if (result) return result;
94
+ if (mnemeSessionId.length > 8) {
95
+ return searchDirFor(sessionsDir, `${mnemeSessionId.slice(0, 8)}.json`);
96
+ }
97
+ return null;
93
98
  }
94
99
  function hasSessionSummary(sessionFile) {
95
100
  if (!sessionFile) return false;
@@ -181,12 +186,19 @@ function cleanupStaleUncommittedSessions(projectPath, graceDays) {
181
186
  } catch {
182
187
  }
183
188
  }
184
- const linkPath = path2.join(
189
+ const fullLinkPath = path2.join(
190
+ projectPath,
191
+ ".mneme",
192
+ "session-links",
193
+ `${row.claude_session_id}.json`
194
+ );
195
+ const shortLinkPath = path2.join(
185
196
  projectPath,
186
197
  ".mneme",
187
198
  "session-links",
188
199
  `${row.claude_session_id.slice(0, 8)}.json`
189
200
  );
201
+ const linkPath = fs2.existsSync(fullLinkPath) ? fullLinkPath : shortLinkPath;
190
202
  if (fs2.existsSync(linkPath)) {
191
203
  try {
192
204
  fs2.unlinkSync(linkPath);
@@ -322,6 +334,24 @@ function migrateDatabase(db) {
322
334
  `);
323
335
  console.error("[mneme] Migrated: created session_save_state table");
324
336
  }
337
+ try {
338
+ db.exec("SELECT 1 FROM file_index LIMIT 1");
339
+ } catch {
340
+ db.exec(`
341
+ CREATE TABLE IF NOT EXISTS file_index (
342
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
343
+ session_id TEXT NOT NULL,
344
+ project_path TEXT NOT NULL,
345
+ file_path TEXT NOT NULL,
346
+ tool_name TEXT,
347
+ timestamp TEXT NOT NULL,
348
+ created_at TEXT DEFAULT (datetime('now'))
349
+ );
350
+ CREATE INDEX IF NOT EXISTS idx_file_index_session ON file_index(session_id);
351
+ CREATE INDEX IF NOT EXISTS idx_file_index_project_file ON file_index(project_path, file_path);
352
+ `);
353
+ console.error("[mneme] Migrated: created file_index table");
354
+ }
325
355
  }
326
356
  function initDatabase(dbPath) {
327
357
  const mnemeDir = path3.dirname(dbPath);
@@ -568,6 +598,52 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
568
598
 
569
599
  // lib/save/index.ts
570
600
  var { DatabaseSync: DatabaseSync3 } = await import("node:sqlite");
601
+ function normalizeFilePath(absPath, projectPath) {
602
+ if (!absPath.startsWith(projectPath)) return null;
603
+ return absPath.slice(projectPath.length).replace(/^\//, "");
604
+ }
605
+ var IGNORED_PREFIXES = [
606
+ "node_modules/",
607
+ "dist/",
608
+ ".git/",
609
+ ".mneme/",
610
+ ".claude/"
611
+ ];
612
+ var IGNORED_FILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
613
+ function isIgnoredPath(relativePath) {
614
+ return IGNORED_PREFIXES.some((p) => relativePath.startsWith(p)) || IGNORED_FILES.includes(relativePath);
615
+ }
616
+ function indexFilePaths(fileIndexStmt, interaction, mnemeSessionId, projectPath) {
617
+ const seen = /* @__PURE__ */ new Set();
618
+ const add = (absPath, toolName) => {
619
+ const normalized = normalizeFilePath(absPath, projectPath);
620
+ if (!normalized || isIgnoredPath(normalized) || seen.has(normalized))
621
+ return;
622
+ seen.add(normalized);
623
+ try {
624
+ fileIndexStmt.run(
625
+ mnemeSessionId,
626
+ projectPath,
627
+ normalized,
628
+ toolName,
629
+ interaction.timestamp
630
+ );
631
+ } catch {
632
+ }
633
+ };
634
+ for (const td of interaction.toolDetails) {
635
+ if (typeof td.detail === "string" && td.detail.startsWith("/")) {
636
+ add(td.detail, td.name);
637
+ }
638
+ }
639
+ if (interaction.toolResults) {
640
+ for (const tr of interaction.toolResults) {
641
+ if (tr.filePath?.startsWith("/")) {
642
+ add(tr.filePath, tr.toolName || "");
643
+ }
644
+ }
645
+ }
646
+ }
571
647
  async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
572
648
  if (!claudeSessionId || !transcriptPath || !projectPath) {
573
649
  return {
@@ -613,6 +689,10 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
613
689
  owner, role, content, thinking, tool_calls, timestamp, is_compact_summary
614
690
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
615
691
  `);
692
+ const fileIndexStmt = db.prepare(`
693
+ INSERT INTO file_index (session_id, project_path, file_path, tool_name, timestamp)
694
+ VALUES (?, ?, ?, ?, ?)
695
+ `);
616
696
  let insertedCount = 0;
617
697
  let lastTimestamp = saveState.lastSavedTimestamp || "";
618
698
  for (const interaction of interactions) {
@@ -676,6 +756,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
676
756
  );
677
757
  insertedCount++;
678
758
  }
759
+ indexFilePaths(fileIndexStmt, interaction, mnemeSessionId, projectPath);
679
760
  lastTimestamp = interaction.timestamp;
680
761
  } catch (error) {
681
762
  console.error(`[mneme] Error inserting interaction: ${error}`);
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // lib/search/prompt.ts
4
- import * as fs6 from "node:fs";
5
- import * as path6 from "node:path";
4
+ import * as fs7 from "node:fs";
5
+ import * as path7 from "node:path";
6
6
 
7
7
  // lib/search/approved-rules.ts
8
8
  import * as fs4 from "node:fs";
@@ -282,6 +282,158 @@ function walkJsonFiles(dir, callback) {
282
282
  }
283
283
  }
284
284
 
285
+ // lib/search/stopwords.ts
286
+ var ENGLISH_STOPWORDS = /* @__PURE__ */ new Set([
287
+ // Articles & determiners
288
+ "the",
289
+ "this",
290
+ "that",
291
+ "these",
292
+ "those",
293
+ "some",
294
+ "any",
295
+ "all",
296
+ "each",
297
+ "every",
298
+ "both",
299
+ "other",
300
+ // Pronouns
301
+ "you",
302
+ "your",
303
+ "its",
304
+ "his",
305
+ "her",
306
+ "our",
307
+ "their",
308
+ "them",
309
+ "they",
310
+ "who",
311
+ "whom",
312
+ "which",
313
+ // Prepositions & conjunctions
314
+ "for",
315
+ "with",
316
+ "from",
317
+ "into",
318
+ "about",
319
+ "between",
320
+ "through",
321
+ "during",
322
+ "before",
323
+ "after",
324
+ "above",
325
+ "below",
326
+ "under",
327
+ "and",
328
+ "but",
329
+ "nor",
330
+ "not",
331
+ "than",
332
+ "then",
333
+ "also",
334
+ "once",
335
+ "again",
336
+ "further",
337
+ "such",
338
+ "same",
339
+ "few",
340
+ "more",
341
+ "most",
342
+ "too",
343
+ "own",
344
+ // Common verbs (non-technical)
345
+ "are",
346
+ "was",
347
+ "were",
348
+ "been",
349
+ "being",
350
+ "have",
351
+ "has",
352
+ "had",
353
+ "does",
354
+ "did",
355
+ "will",
356
+ "would",
357
+ "could",
358
+ "should",
359
+ "shall",
360
+ "might",
361
+ "must",
362
+ "can",
363
+ // Prompt noise
364
+ "please",
365
+ "help",
366
+ "want",
367
+ "need",
368
+ "like",
369
+ "just",
370
+ "only",
371
+ "very",
372
+ "really",
373
+ "here",
374
+ "there",
375
+ "what",
376
+ "when",
377
+ "where",
378
+ "how",
379
+ "why",
380
+ "let",
381
+ "make",
382
+ "way",
383
+ "tell"
384
+ ]);
385
+ var JAPANESE_STOPWORDS = /* @__PURE__ */ new Set([
386
+ // Common prompt phrases (split on whitespace, so these appear as tokens)
387
+ "\u304F\u3060\u3055\u3044",
388
+ "\u306B\u3064\u3044\u3066",
389
+ "\u3064\u3044\u3066",
390
+ "\u3057\u307E\u3059",
391
+ "\u3057\u3066\u3044\u308B",
392
+ "\u3057\u3066\u308B",
393
+ "\u3067\u304D\u308B",
394
+ "\u3067\u304D\u307E\u3059",
395
+ "\u3067\u3059\u304B",
396
+ "\u3042\u308A\u307E\u3059\u304B",
397
+ "\u3042\u308A\u307E\u305B\u3093",
398
+ "\u3042\u308A\u307E\u3057\u305F",
399
+ "\u3067\u3059\u304C",
400
+ "\u3067\u3059\u3051\u3069",
401
+ "\u3067\u3059\u306E\u3067",
402
+ "\u3067\u3059\u304B\u3089",
403
+ "\u3067\u3059\u306D",
404
+ "\u3067\u3059\u3088",
405
+ "\u3057\u307E\u3057\u3087\u3046",
406
+ "\u3057\u3088\u3046",
407
+ "\u3057\u305F\u3044",
408
+ "\u3057\u305F\u3044\u3067\u3059",
409
+ "\u307B\u3057\u3044",
410
+ "\u307B\u3057\u3044\u3067\u3059",
411
+ "\u3042\u308A\u304C\u3068\u3046",
412
+ "\u304A\u306D\u304C\u3044",
413
+ "\u304A\u9858\u3044",
414
+ "\u6559\u3048\u3066",
415
+ "\u898B\u305B\u3066",
416
+ "\u3084\u3063\u3066",
417
+ "\u3069\u3046\u3084\u3063\u3066",
418
+ "\u306A\u305C",
419
+ "\u3069\u3046",
420
+ "\u3069\u306E",
421
+ "\u305D\u306E",
422
+ "\u3053\u306E",
423
+ "\u3042\u306E",
424
+ "\u305D\u308C",
425
+ "\u3053\u308C",
426
+ "\u3042\u308C"
427
+ ]);
428
+ var ALL_STOPWORDS = /* @__PURE__ */ new Set([...ENGLISH_STOPWORDS, ...JAPANESE_STOPWORDS]);
429
+ function isStopword(token) {
430
+ return ALL_STOPWORDS.has(token.toLowerCase());
431
+ }
432
+ function removeStopwords(tokens) {
433
+ const filtered = tokens.filter((t) => !isStopword(t));
434
+ return filtered.length > 0 ? filtered : tokens;
435
+ }
436
+
285
437
  // lib/search/approved-rules.ts
286
438
  function isApproved(status) {
287
439
  if (typeof status !== "string") return false;
@@ -428,7 +580,9 @@ function searchPatternFiles(mnemeDir, pattern) {
428
580
  }
429
581
  function searchApprovedRules(options) {
430
582
  const { query, mnemeDir, limit = 5 } = options;
431
- const keywords = query.toLowerCase().split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2);
583
+ const keywords = removeStopwords(
584
+ query.toLowerCase().split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2)
585
+ );
432
586
  if (keywords.length === 0) return [];
433
587
  const expanded = expandKeywordsWithAliases(keywords, loadTags(mnemeDir));
434
588
  const pattern = new RegExp(expanded.map(escapeRegex).join("|"), "i");
@@ -563,6 +717,14 @@ function searchSessions(mnemeDir, keywords, limit = 5, detail = "compact") {
563
717
  score += 2;
564
718
  matchedFields.push("errors");
565
719
  }
720
+ if (session.technologies?.some((t) => pattern.test(t))) {
721
+ score += 1.5;
722
+ matchedFields.push("technologies");
723
+ }
724
+ if (session.filesModified?.some((f) => pattern.test(f.path))) {
725
+ score += 1;
726
+ matchedFields.push("filesModified");
727
+ }
566
728
  if (score === 0 && keywords.length <= 2) {
567
729
  const titleWords = (title || "").toLowerCase().split(/\s+/);
568
730
  const tagWords = session.tags || [];
@@ -613,7 +775,9 @@ function searchKnowledge(options) {
613
775
  offset = 0,
614
776
  detail = "compact"
615
777
  } = options;
616
- const keywords = query.toLowerCase().split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 2);
778
+ const keywords = removeStopwords(
779
+ query.toLowerCase().split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 2)
780
+ );
617
781
  if (keywords.length === 0) return [];
618
782
  const expandedKeywords = expandKeywordsWithAliases(
619
783
  keywords,
@@ -648,6 +812,55 @@ function searchKnowledge(options) {
648
812
  }).slice(safeOffset, safeOffset + limit);
649
813
  }
650
814
 
815
+ // lib/search/file-search.ts
816
+ import * as fs6 from "node:fs";
817
+ import * as path6 from "node:path";
818
+ function getSessionTitle(mnemeDir, sessionId) {
819
+ const sessionsDir = path6.join(mnemeDir, "sessions");
820
+ let title = sessionId;
821
+ walkJsonFiles(sessionsDir, (filePath) => {
822
+ if (!path6.basename(filePath, ".json").startsWith(sessionId)) return;
823
+ try {
824
+ const data = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
825
+ if (data.title) title = data.title;
826
+ } catch {
827
+ }
828
+ });
829
+ return title;
830
+ }
831
+ function searchByFiles(database, projectPath, filePaths, mnemeDir, limit = 3) {
832
+ if (filePaths.length === 0) return [];
833
+ try {
834
+ const placeholders = filePaths.map(() => "?").join(",");
835
+ const rows = database.prepare(
836
+ `SELECT session_id, file_path, COUNT(*) as cnt
837
+ FROM file_index
838
+ WHERE project_path = ? AND file_path IN (${placeholders})
839
+ GROUP BY session_id, file_path`
840
+ ).all(projectPath, ...filePaths);
841
+ const sessionMap = /* @__PURE__ */ new Map();
842
+ for (const row of rows) {
843
+ const entry = sessionMap.get(row.session_id) || {
844
+ files: /* @__PURE__ */ new Set(),
845
+ count: 0
846
+ };
847
+ entry.files.add(row.file_path);
848
+ entry.count += row.cnt;
849
+ sessionMap.set(row.session_id, entry);
850
+ }
851
+ return [...sessionMap.entries()].sort(
852
+ (a, b) => b[1].files.size - a[1].files.size || b[1].count - a[1].count
853
+ ).slice(0, limit).map(([sessionId, data]) => ({
854
+ sessionId,
855
+ title: getSessionTitle(mnemeDir, sessionId),
856
+ matchedFiles: [...data.files],
857
+ fileCount: data.count
858
+ }));
859
+ } catch {
860
+ return [];
861
+ }
862
+ }
863
+
651
864
  // lib/search/prompt.ts
652
865
  var originalEmit = process.emit;
653
866
  process.emit = (event, ...args) => {
@@ -666,17 +879,18 @@ function main() {
666
879
  const query = getArg(args, "query");
667
880
  const projectPath = getArg(args, "project");
668
881
  const limit = Number.parseInt(getArg(args, "limit") || "5", 10);
882
+ const files = getArg(args, "files");
669
883
  if (!query || !projectPath) {
670
884
  console.log(
671
885
  JSON.stringify({ success: false, error: "Missing required args" })
672
886
  );
673
887
  process.exit(1);
674
888
  }
675
- const mnemeDir = path6.join(projectPath, ".mneme");
676
- const dbPath = path6.join(mnemeDir, "local.db");
889
+ const mnemeDir = path7.join(projectPath, ".mneme");
890
+ const dbPath = path7.join(mnemeDir, "local.db");
677
891
  let database = null;
678
892
  try {
679
- if (fs6.existsSync(dbPath)) {
893
+ if (fs7.existsSync(dbPath)) {
680
894
  database = new DatabaseSync(dbPath);
681
895
  database.exec("PRAGMA journal_mode = WAL");
682
896
  }
@@ -688,7 +902,10 @@ function main() {
688
902
  limit: Number.isFinite(limit) ? Math.max(1, Math.min(limit, 10)) : 5
689
903
  });
690
904
  const rules = searchApprovedRules({ query, mnemeDir, limit: 5 });
691
- console.log(JSON.stringify({ success: true, results, rules }));
905
+ const fileRecommendations = files && database ? searchByFiles(database, projectPath, files.split(","), mnemeDir) : [];
906
+ console.log(
907
+ JSON.stringify({ success: true, results, rules, fileRecommendations })
908
+ );
692
909
  } catch (error) {
693
910
  console.log(
694
911
  JSON.stringify({ success: false, error: error.message })