claude-mneme 2.9.1 → 2.10.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": "claude-mneme",
3
- "version": "2.8.0",
4
- "description": "Persistent memory system for Claude Code - remembers context across sessions",
3
+ "version": "2.10.2",
4
+ "description": "Automatic session memory for Claude Code every session picks up where the last one left off",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"
7
7
  },
@@ -11,7 +11,12 @@
11
11
  "memory",
12
12
  "context",
13
13
  "persistence",
14
- "mneme",
15
- "claude-code"
14
+ "session",
15
+ "history",
16
+ "remember",
17
+ "summarization",
18
+ "project-context",
19
+ "claude-code",
20
+ "mneme"
16
21
  ]
17
22
  }
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="../assets/claude-mneme-mascot-128.png" alt="Claude Mneme Mascot" width="128">
2
+ <img src="https://raw.githubusercontent.com/edimuj/claude-mneme/main/assets/claude-mneme-mascot-128.png" alt="Claude Mneme Mascot" width="128">
3
3
  </p>
4
4
 
5
5
  # Claude Mneme — Plugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mneme",
3
- "version": "2.9.1",
3
+ "version": "2.10.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "install-deps": "npm install",
@@ -83,17 +83,21 @@ async function main() {
83
83
  // MEDIUM PRIORITY - Inject if relevant/recent
84
84
  // ============================================================================
85
85
 
86
+ // Read last session timestamp (used for git changes and temporal header)
87
+ let lastSessionTs = null;
88
+ if (existsSync(paths.lastSession)) {
89
+ try {
90
+ lastSessionTs = readFileSync(paths.lastSession, 'utf-8').trim() || null;
91
+ } catch {}
92
+ }
93
+
86
94
  // Git changes since last session
87
95
  let gitChanges = '';
88
96
  const gcConfig = sections.gitChanges || { enabled: true };
89
97
  if (gcConfig.enabled !== false) {
90
98
  try {
91
- let sinceArg = null;
92
- if (existsSync(paths.lastSession)) {
93
- sinceArg = readFileSync(paths.lastSession, 'utf-8').trim();
94
- }
95
- if (sinceArg) {
96
- const log = execFileSync('git', ['log', '--oneline', `--since=${sinceArg}`], {
99
+ if (lastSessionTs) {
100
+ const log = execFileSync('git', ['log', '--oneline', `--since=${lastSessionTs}`], {
97
101
  encoding: 'utf8',
98
102
  cwd,
99
103
  stdio: ['ignore', 'pipe', 'ignore']
@@ -176,6 +180,27 @@ async function main() {
176
180
  if (hasContent) {
177
181
  console.log(`<claude-mneme project="${escapeAttr(projectName)}">`);
178
182
 
183
+ // Temporal header — session time + last session reference
184
+ const now = new Date();
185
+ const sessionTime = now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
186
+ let temporalLine = `Session started: ${sessionTime}`;
187
+ if (lastSessionTs) {
188
+ const lastDate = new Date(lastSessionTs);
189
+ const lastFormatted = lastDate.toLocaleString(undefined, {
190
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false
191
+ });
192
+ const diffMin = Math.floor((now - lastDate) / 60000);
193
+ let relative;
194
+ if (diffMin < 2) relative = 'just now';
195
+ else if (diffMin < 60) relative = `${diffMin} minutes ago`;
196
+ else {
197
+ const diffHours = Math.floor(diffMin / 60);
198
+ relative = `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
199
+ }
200
+ temporalLine += ` | Last session: ${lastFormatted} (${relative})`;
201
+ }
202
+ console.log(`\n${temporalLine}`);
203
+
179
204
  // HANDOFF from previous session (highest immediate value)
180
205
  if (handoff) {
181
206
  console.log('\n## Last Session\n');
package/scripts/sync.mjs CHANGED
@@ -11,7 +11,7 @@ import { hostname } from 'os';
11
11
  import { randomUUID } from 'crypto';
12
12
  import http from 'http';
13
13
  import https from 'https';
14
- import { ensureMemoryDirs, getProjectName, logError } from './utils.mjs';
14
+ import { ensureMemoryDirs, getProjectRoot, logError } from './utils.mjs';
15
15
 
16
16
  // ============================================================================
17
17
  // Client ID Management
@@ -180,7 +180,11 @@ class SyncClient {
180
180
 
181
181
  this.cwd = cwd;
182
182
  this.paths = ensureMemoryDirs(cwd);
183
- this.projectId = syncConfig.projectId || getProjectName(cwd);
183
+ // Default projectId uses sanitized full path to match local dir naming.
184
+ // Breaking change for sync users without explicit projectId — server-side
185
+ // data needs manual rename or re-sync.
186
+ const root = getProjectRoot(cwd);
187
+ this.projectId = syncConfig.projectId || root.replace(/^\//, '-').replace(/\//g, '-');
184
188
  this.clientId = getClientId(this.paths.base);
185
189
 
186
190
  this.http = this.enabled
package/scripts/utils.mjs CHANGED
@@ -19,36 +19,44 @@ export function escapeAttr(str) {
19
19
  }
20
20
 
21
21
  /**
22
- * Get the project name from cwd
23
- * Uses git repo root name if available, otherwise directory name
22
+ * Get the project root directory (absolute path).
23
+ * Uses git repo root if available, otherwise cwd.
24
24
  */
25
- export function getProjectName(cwd = process.cwd()) {
25
+ export function getProjectRoot(cwd = process.cwd()) {
26
26
  try {
27
- // Try to get git repo root using execFileSync (safer than execSync)
28
- const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
27
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
29
28
  encoding: 'utf8',
30
29
  cwd,
31
30
  stdio: ['ignore', 'pipe', 'ignore']
32
31
  }).trim();
33
- return basename(gitRoot);
34
32
  } catch {
35
- // Not a git repo, use directory name
36
- return basename(cwd);
33
+ return cwd;
37
34
  }
38
35
  }
39
36
 
40
37
  /**
41
- * Get the project-specific memory directory
38
+ * Get the project name from cwd (display name only — basename of root)
39
+ */
40
+ export function getProjectName(cwd = process.cwd()) {
41
+ return basename(getProjectRoot(cwd));
42
+ }
43
+
44
+ /**
45
+ * Get the project-specific memory directory.
46
+ * Uses full absolute path as dirname to avoid collisions between
47
+ * projects with the same basename (e.g. ~/work/api vs ~/personal/api).
48
+ * Convention: /home/foo/bar → -home-foo-bar (matches Claude Code's own auto-memory).
42
49
  */
43
50
  function getProjectMemoryDir(cwd = process.cwd()) {
44
- const projectName = getProjectName(cwd);
45
- // Sanitize project name for filesystem
46
- const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '_');
51
+ const projectRoot = getProjectRoot(cwd);
52
+ // Convert absolute path to safe dirname: /home/foo/bar → -home-foo-bar
53
+ const safeName = projectRoot.replace(/^\//, '-').replace(/\//g, '-');
47
54
  return join(MEMORY_BASE, 'projects', safeName);
48
55
  }
49
56
 
50
57
  /**
51
- * Ensure memory directories exist and return paths
58
+ * Ensure memory directories exist and return paths.
59
+ * Migrates old-style (basename-only) dirs to new-style (full-path) dirs.
52
60
  */
53
61
  export function ensureMemoryDirs(cwd = process.cwd()) {
54
62
  const projectDir = getProjectMemoryDir(cwd);
@@ -57,8 +65,15 @@ export function ensureMemoryDirs(cwd = process.cwd()) {
57
65
  mkdirSync(MEMORY_BASE, { recursive: true });
58
66
  }
59
67
 
68
+ // Migrate old-style (basename-only) dir to new-style (full-path) dir
60
69
  if (!existsSync(projectDir)) {
61
- mkdirSync(projectDir, { recursive: true });
70
+ const oldName = getProjectName(cwd).replace(/[^a-zA-Z0-9_-]/g, '_');
71
+ const oldDir = join(MEMORY_BASE, 'projects', oldName);
72
+ if (existsSync(oldDir)) {
73
+ renameSync(oldDir, projectDir);
74
+ } else {
75
+ mkdirSync(projectDir, { recursive: true });
76
+ }
62
77
  }
63
78
 
64
79
  return {
@@ -29,6 +29,9 @@ import {
29
29
  loadConfig,
30
30
  flushPendingLog,
31
31
  ensureMemoryDirs,
32
+ getProjectRoot,
33
+ getProjectName,
34
+ MEMORY_BASE,
32
35
  calculateRecencyScore,
33
36
  calculateFileRelevanceScore,
34
37
  calculateTypePriorityScore,
@@ -1463,3 +1466,122 @@ describe('stripMarkdown', () => {
1463
1466
  assert.ok(result.includes('needs review'));
1464
1467
  });
1465
1468
  });
1469
+
1470
+ // ============================================================================
1471
+ // getProjectRoot
1472
+ // ============================================================================
1473
+
1474
+ describe('getProjectRoot', () => {
1475
+ it('returns an absolute path', () => {
1476
+ const root = getProjectRoot();
1477
+ assert.ok(root.startsWith('/'), `Expected absolute path, got ${root}`);
1478
+ });
1479
+
1480
+ it('returns git root when inside a git repo', () => {
1481
+ // We're running from inside the claude-mneme repo
1482
+ const root = getProjectRoot();
1483
+ assert.ok(root.endsWith('claude-mneme'), `Expected git root ending in claude-mneme, got ${root}`);
1484
+ });
1485
+
1486
+ it('returns cwd for non-git directories', () => {
1487
+ const tmp = mkdtempSync(join(tmpdir(), 'mneme-root-'));
1488
+ try {
1489
+ const root = getProjectRoot(tmp);
1490
+ assert.equal(root, tmp, 'Should return cwd for non-git dir');
1491
+ } finally {
1492
+ rmSync(tmp, { recursive: true, force: true });
1493
+ }
1494
+ });
1495
+ });
1496
+
1497
+ // ============================================================================
1498
+ // getProjectName (display name — basename only)
1499
+ // ============================================================================
1500
+
1501
+ describe('getProjectName', () => {
1502
+ it('returns basename of git root', () => {
1503
+ const name = getProjectName();
1504
+ assert.equal(name, 'claude-mneme');
1505
+ });
1506
+ });
1507
+
1508
+ // ============================================================================
1509
+ // getProjectMemoryDir (full-path-based naming + migration)
1510
+ // ============================================================================
1511
+
1512
+ describe('ensureMemoryDirs (full-path naming + migration)', () => {
1513
+ it('creates dir with full-path-based name', () => {
1514
+ const paths = ensureMemoryDirs();
1515
+ // Should contain the full path sanitized, not just basename
1516
+ assert.ok(paths.project.includes('-home-') || paths.project.includes('-Users-'),
1517
+ `Expected full-path dir name, got ${paths.project}`);
1518
+ assert.ok(!paths.project.endsWith('/projects/claude-mneme'),
1519
+ `Should NOT be old-style basename-only dir: ${paths.project}`);
1520
+ });
1521
+
1522
+ it('migrates old-style dir to new-style dir', () => {
1523
+ // Create a temp directory to simulate a project root
1524
+ const tmp = mkdtempSync(join(tmpdir(), 'mneme-migrate-'));
1525
+ const projectsDir = join(MEMORY_BASE, 'projects');
1526
+
1527
+ // Derive what old-style and new-style names would be for this temp dir
1528
+ const oldName = tmp.split('/').pop().replace(/[^a-zA-Z0-9_-]/g, '_');
1529
+ const newName = tmp.replace(/^\//, '-').replace(/\//g, '-');
1530
+ const oldDir = join(projectsDir, oldName);
1531
+ const newDir = join(projectsDir, newName);
1532
+
1533
+ try {
1534
+ // Clean up any pre-existing dirs
1535
+ if (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });
1536
+ if (existsSync(oldDir)) rmSync(oldDir, { recursive: true, force: true });
1537
+
1538
+ // Create old-style dir with a marker file
1539
+ mkdirSync(oldDir, { recursive: true });
1540
+ writeFileSync(join(oldDir, 'marker.txt'), 'migrated');
1541
+
1542
+ // Call ensureMemoryDirs — should migrate old → new
1543
+ const paths = ensureMemoryDirs(tmp);
1544
+
1545
+ assert.ok(existsSync(newDir), 'New-style dir should exist after migration');
1546
+ assert.ok(!existsSync(oldDir), 'Old-style dir should be gone after migration');
1547
+ assert.ok(existsSync(join(newDir, 'marker.txt')), 'Marker file should survive migration');
1548
+ assert.equal(paths.project, newDir);
1549
+ } finally {
1550
+ // Clean up
1551
+ if (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });
1552
+ if (existsSync(oldDir)) rmSync(oldDir, { recursive: true, force: true });
1553
+ rmSync(tmp, { recursive: true, force: true });
1554
+ }
1555
+ });
1556
+
1557
+ it('does not migrate if new-style dir already exists', () => {
1558
+ const tmp = mkdtempSync(join(tmpdir(), 'mneme-nomigrate-'));
1559
+ const projectsDir = join(MEMORY_BASE, 'projects');
1560
+
1561
+ const oldName = tmp.split('/').pop().replace(/[^a-zA-Z0-9_-]/g, '_');
1562
+ const newName = tmp.replace(/^\//, '-').replace(/\//g, '-');
1563
+ const oldDir = join(projectsDir, oldName);
1564
+ const newDir = join(projectsDir, newName);
1565
+
1566
+ try {
1567
+ if (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });
1568
+ if (existsSync(oldDir)) rmSync(oldDir, { recursive: true, force: true });
1569
+
1570
+ // Create BOTH dirs
1571
+ mkdirSync(oldDir, { recursive: true });
1572
+ writeFileSync(join(oldDir, 'old-marker.txt'), 'old');
1573
+ mkdirSync(newDir, { recursive: true });
1574
+ writeFileSync(join(newDir, 'new-marker.txt'), 'new');
1575
+
1576
+ ensureMemoryDirs(tmp);
1577
+
1578
+ // Old dir should still exist (no migration attempted)
1579
+ assert.ok(existsSync(oldDir), 'Old dir should remain when new dir already exists');
1580
+ assert.ok(existsSync(join(newDir, 'new-marker.txt')), 'New dir contents should be untouched');
1581
+ } finally {
1582
+ if (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });
1583
+ if (existsSync(oldDir)) rmSync(oldDir, { recursive: true, force: true });
1584
+ rmSync(tmp, { recursive: true, force: true });
1585
+ }
1586
+ });
1587
+ });