memoir-cli 2.1.0 → 2.1.1
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/package.json +1 -1
- package/src/adapters/restore.js +119 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Sync AI memory across devices. Back up and restore Claude, Gemini, Codex, Cursor, Copilot, Windsurf configs. Snapshot coding sessions and resume on another machine. Migrate instructions between AI assistants.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
package/src/adapters/restore.js
CHANGED
|
@@ -5,27 +5,115 @@ import os from 'os';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { adapters } from '../adapters/index.js';
|
|
7
7
|
|
|
8
|
+
// Detect the local home key by looking at what Claude has ALREADY created
|
|
9
|
+
// on this machine, rather than trying to compute the encoding ourselves.
|
|
10
|
+
// Claude's path encoding varies across platforms and versions, so detection
|
|
11
|
+
// is the only reliable approach.
|
|
12
|
+
function detectLocalHomeKey(adapterSource) {
|
|
13
|
+
const localProjectsDir = path.join(adapterSource, 'projects');
|
|
14
|
+
if (!fs.existsSync(localProjectsDir)) return null;
|
|
15
|
+
|
|
16
|
+
const entries = fs.readdirSync(localProjectsDir)
|
|
17
|
+
.filter(e => fs.statSync(path.join(localProjectsDir, e)).isDirectory());
|
|
18
|
+
if (entries.length === 0) return null;
|
|
19
|
+
|
|
20
|
+
// Find dirs with a memory/ subfolder that aren't sub-projects of another dir
|
|
21
|
+
const candidates = entries.filter(entry => {
|
|
22
|
+
const hasMemory = fs.existsSync(path.join(localProjectsDir, entry, 'memory'));
|
|
23
|
+
if (!hasMemory) return false;
|
|
24
|
+
// A sub-project dir starts with another dir + '-'
|
|
25
|
+
const isSubProject = entries.some(other =>
|
|
26
|
+
other !== entry && entry.startsWith(other + '-')
|
|
27
|
+
);
|
|
28
|
+
return !isSubProject;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (candidates.length === 1) return candidates[0];
|
|
32
|
+
|
|
33
|
+
if (candidates.length > 1) {
|
|
34
|
+
// Multiple home-key candidates (e.g. encoding changed between Claude versions)
|
|
35
|
+
// Pick the most recently modified one — that's what Claude is actively using
|
|
36
|
+
return candidates.sort((a, b) => {
|
|
37
|
+
const aDir = path.join(localProjectsDir, a, 'memory');
|
|
38
|
+
const bDir = path.join(localProjectsDir, b, 'memory');
|
|
39
|
+
return fs.statSync(bDir).mtimeMs - fs.statSync(aDir).mtimeMs;
|
|
40
|
+
})[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// No dir has memory/ — fall back to shortest dir that's a prefix of others
|
|
44
|
+
const prefixDirs = entries.filter(entry =>
|
|
45
|
+
entries.some(other => other !== entry && other.startsWith(entry + '-'))
|
|
46
|
+
).sort((a, b) => a.length - b.length);
|
|
47
|
+
|
|
48
|
+
return prefixDirs[0] || entries[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
8
51
|
// Claude CLI stores projects under paths like `projects/-Users-camarthur/`
|
|
9
|
-
// This
|
|
10
|
-
function
|
|
52
|
+
// This remaps ALL foreign machine dirs to match the current machine.
|
|
53
|
+
function remapProjectPaths(backupDir, adapterSource) {
|
|
11
54
|
const projectsDir = path.join(backupDir, 'projects');
|
|
12
|
-
if (!fs.existsSync(projectsDir)) return
|
|
55
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
13
56
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
57
|
+
const backupEntries = fs.readdirSync(projectsDir)
|
|
58
|
+
.filter(e => fs.statSync(path.join(projectsDir, e)).isDirectory());
|
|
59
|
+
if (backupEntries.length === 0) return [];
|
|
60
|
+
|
|
61
|
+
// Step 1: Detect the local home key from existing Claude dirs
|
|
62
|
+
let localHomeKey = detectLocalHomeKey(adapterSource);
|
|
63
|
+
|
|
64
|
+
// Step 2: Fallback — compute from homedir (only for fresh installs)
|
|
65
|
+
if (!localHomeKey) {
|
|
66
|
+
const home = os.homedir();
|
|
67
|
+
// Use the same encoding Claude uses: path with separators → dashes
|
|
68
|
+
localHomeKey = '-' + home.replace(/^\//, '').replace(/\\/g, '-').replace(/\//g, '-').replace(/:/g, '');
|
|
69
|
+
}
|
|
20
70
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
71
|
+
// Step 3: Identify foreign home keys in the backup
|
|
72
|
+
// A "home key" is a dir that: has memory/, OR is a prefix of other dirs, AND is not a sub-project
|
|
73
|
+
const foreignHomeKeys = new Set();
|
|
74
|
+
|
|
75
|
+
for (const entry of backupEntries) {
|
|
76
|
+
// Skip dirs that already belong to this machine
|
|
77
|
+
if (entry === localHomeKey || entry.startsWith(localHomeKey + '-')) continue;
|
|
78
|
+
|
|
79
|
+
// Is this a sub-project of another backup dir? Then skip — its parent handles it
|
|
80
|
+
const isSubProject = backupEntries.some(other =>
|
|
81
|
+
other !== entry && entry.startsWith(other + '-')
|
|
82
|
+
);
|
|
83
|
+
if (isSubProject) continue;
|
|
84
|
+
|
|
85
|
+
// Has memory/ subfolder = definitely a home key
|
|
86
|
+
const hasMemory = fs.existsSync(path.join(projectsDir, entry, 'memory'));
|
|
87
|
+
// Is a prefix of other dirs = likely a home key
|
|
88
|
+
const isPrefix = backupEntries.some(other =>
|
|
89
|
+
other !== entry && other.startsWith(entry + '-')
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (hasMemory || isPrefix) {
|
|
93
|
+
foreignHomeKeys.add(entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
25
96
|
|
|
26
|
-
|
|
97
|
+
// Step 4: Build remaps — remap each foreign home key and its sub-projects
|
|
98
|
+
const remaps = [];
|
|
99
|
+
const processed = new Set();
|
|
100
|
+
|
|
101
|
+
for (const foreignKey of foreignHomeKeys) {
|
|
102
|
+
// Find all dirs belonging to this foreign home key
|
|
103
|
+
for (const dir of backupEntries) {
|
|
104
|
+
if (processed.has(dir)) continue;
|
|
105
|
+
if (dir !== foreignKey && !dir.startsWith(foreignKey + '-')) continue;
|
|
106
|
+
|
|
107
|
+
processed.add(dir);
|
|
108
|
+
const suffix = dir.slice(foreignKey.length); // "" or "-alfred" etc.
|
|
109
|
+
const newName = localHomeKey + suffix;
|
|
110
|
+
if (dir !== newName) {
|
|
111
|
+
remaps.push({ oldName: dir, newName });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
27
115
|
|
|
28
|
-
return
|
|
116
|
+
return remaps;
|
|
29
117
|
}
|
|
30
118
|
|
|
31
119
|
async function syncFiles(src, dest, changes) {
|
|
@@ -97,17 +185,24 @@ export async function restoreMemories(sourceDir, spinner, onlyFilter = null, aut
|
|
|
97
185
|
|
|
98
186
|
// Remap Claude project paths from source machine to this machine
|
|
99
187
|
if (adapter.name === 'Claude CLI') {
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
188
|
+
const remaps = remapProjectPaths(backupDir, adapter.source);
|
|
189
|
+
if (remaps.length > 0) {
|
|
102
190
|
spinner.stop();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
191
|
+
for (const remap of remaps) {
|
|
192
|
+
console.log(chalk.gray(` Remapping: ${remap.oldName} → ${remap.newName}`));
|
|
193
|
+
const oldDir = path.join(backupDir, 'projects', remap.oldName);
|
|
194
|
+
const newDir = path.join(backupDir, 'projects', remap.newName);
|
|
195
|
+
if (await fs.pathExists(oldDir)) {
|
|
196
|
+
if (await fs.pathExists(newDir)) {
|
|
197
|
+
// Merge into existing directory
|
|
198
|
+
await syncFiles(oldDir, newDir, { added: [], updated: [], skipped: [] });
|
|
199
|
+
await fs.remove(oldDir);
|
|
200
|
+
} else {
|
|
201
|
+
await fs.move(oldDir, newDir);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
110
204
|
}
|
|
205
|
+
spinner.start();
|
|
111
206
|
}
|
|
112
207
|
}
|
|
113
208
|
|