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.
- package/.claude-plugin/plugin.json +9 -4
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/session-start.mjs +31 -6
- package/scripts/sync.mjs +6 -2
- package/scripts/utils.mjs +29 -14
- package/scripts/utils.test.mjs +122 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mneme",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
15
|
-
"
|
|
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="
|
|
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
|
@@ -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
|
-
|
|
92
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
23
|
-
* Uses git repo root
|
|
22
|
+
* Get the project root directory (absolute path).
|
|
23
|
+
* Uses git repo root if available, otherwise cwd.
|
|
24
24
|
*/
|
|
25
|
-
export function
|
|
25
|
+
export function getProjectRoot(cwd = process.cwd()) {
|
|
26
26
|
try {
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
return basename(cwd);
|
|
33
|
+
return cwd;
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
|
-
* Get the project
|
|
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
|
|
45
|
-
//
|
|
46
|
-
const safeName =
|
|
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
|
-
|
|
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 {
|
package/scripts/utils.test.mjs
CHANGED
|
@@ -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
|
+
});
|