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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "2.1.0",
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",
@@ -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 converts the path from the backup machine to match the current machine
10
- function remapProjectPath(backupDir, adapterSource) {
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 null;
55
+ if (!fs.existsSync(projectsDir)) return [];
13
56
 
14
- const entries = fs.readdirSync(projectsDir);
15
- // Find the backed-up home dir key (e.g., "-Users-camarthur")
16
- const oldHomeKey = entries.find(e => {
17
- return fs.statSync(path.join(projectsDir, e)).isDirectory();
18
- });
19
- if (!oldHomeKey) return null;
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
- // Build the current machine's home dir key
22
- // Claude uses the homedir path with / replaced by - and leading -
23
- const home = os.homedir();
24
- const newHomeKey = '-' + home.replace(/^\//, '').replace(/\\/g, '-').replace(/\//g, '-').replace(/:/g, '');
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
- if (oldHomeKey === newHomeKey) return null; // Same machine, no remap needed
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 { oldHomeKey, newHomeKey };
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 remap = remapProjectPath(backupDir, adapter.source);
101
- if (remap) {
188
+ const remaps = remapProjectPaths(backupDir, adapter.source);
189
+ if (remaps.length > 0) {
102
190
  spinner.stop();
103
- console.log(chalk.gray(` Remapping project path: ${remap.oldHomeKey} → ${remap.newHomeKey}`));
104
- spinner.start();
105
- // Rename the directory in staging so it restores to the right place
106
- const oldDir = path.join(backupDir, 'projects', remap.oldHomeKey);
107
- const newDir = path.join(backupDir, 'projects', remap.newHomeKey);
108
- if (await fs.pathExists(oldDir) && !(await fs.pathExists(newDir))) {
109
- await fs.move(oldDir, newDir);
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