memoir-cli 3.6.0 → 3.6.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": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "mcpName": "io.github.camgitt/memoir",
5
5
  "description": "MCP server that gives Claude, Cursor, and Gemini long-term memory across sessions. Your AI remembers your codebase, decisions, and preferences — across tools and machines.",
6
6
  "main": "src/index.js",
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "scripts": {
21
21
  "start": "node bin/memoir.js",
22
- "test": "bash test-local.sh",
22
+ "test": "node test-cross-machine.mjs && bash test-cross-machine-e2e.sh",
23
+ "test:legacy": "bash test-local.sh",
23
24
  "postinstall": "node -e \"try{const c='\\x1b[36m',r='\\x1b[0m',g='\\x1b[90m';console.log('\\n '+c+'memoir'+r+' installed.\\n Run '+c+'memoir activate'+r+' in any project to give your AI long-term memory.\\n '+g+'https://memoir.sh'+r+'\\n')}catch{}\""
24
25
  },
25
26
  "keywords": [
@@ -14,9 +14,19 @@ export function detectLocalHomeKey(adapterSource) {
14
14
  if (!fs.existsSync(localProjectsDir)) return null;
15
15
 
16
16
  const entries = fs.readdirSync(localProjectsDir)
17
+ .filter(e => !e.startsWith('.'))
17
18
  .filter(e => fs.statSync(path.join(localProjectsDir, e)).isDirectory());
18
19
  if (entries.length === 0) return null;
19
20
 
21
+ // Prefer the key that matches this machine's homedir encoding.
22
+ // Stale foreign dirs (from older memoir versions) can have newer mtimes,
23
+ // so mtime alone is unreliable — the encoded homedir is the ground truth.
24
+ const home = os.homedir();
25
+ const expectedKey = process.platform === 'win32'
26
+ ? home.replace(/\\/g, '-').replace(/:/g, '-')
27
+ : '-' + home.replace(/^\//, '').replace(/\//g, '-');
28
+ if (entries.includes(expectedKey)) return expectedKey;
29
+
20
30
  // Find dirs with a memory/ subfolder that aren't sub-projects of another dir
21
31
  const candidates = entries.filter(entry => {
22
32
  const hasMemory = fs.existsSync(path.join(localProjectsDir, entry, 'memory'));
@@ -143,6 +153,57 @@ function remapProjectPaths(backupDir, adapterSource) {
143
153
  return remaps;
144
154
  }
145
155
 
156
+ // Scan local ~/.claude/projects/ for foreign home-key dirs left behind by older
157
+ // memoir versions (when remap was unreliable). Merge their memory/ into the local
158
+ // home key and move the stale dir into .memoir-archived-{ts}/ so nothing is lost.
159
+ export async function cleanupLocalForeignKeys(adapterSource) {
160
+ const projectsDir = path.join(adapterSource, 'projects');
161
+ if (!await fs.pathExists(projectsDir)) return { archived: [], merged: 0 };
162
+
163
+ const localHomeKey = detectLocalHomeKey(adapterSource);
164
+ if (!localHomeKey) return { archived: [], merged: 0 };
165
+
166
+ const home = os.homedir();
167
+ const localUsername = path.basename(home).toLowerCase();
168
+
169
+ const entries = (await fs.readdir(projectsDir, { withFileTypes: true }))
170
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
171
+ .map(e => e.name);
172
+
173
+ const foreignKeys = [];
174
+ for (const entry of entries) {
175
+ if (entry === localHomeKey) continue;
176
+ if (entry.startsWith(localHomeKey + '-')) continue;
177
+ // Leave alt encodings of THIS machine alone (contain local username)
178
+ if (entry.toLowerCase().includes(localUsername)) continue;
179
+ // Must have memory/ — dirs without it are project dirs that Claude won't read anyway
180
+ if (!await fs.pathExists(path.join(projectsDir, entry, 'memory'))) continue;
181
+ foreignKeys.push(entry);
182
+ }
183
+
184
+ if (foreignKeys.length === 0) return { archived: [], merged: 0 };
185
+
186
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
187
+ const archiveDir = path.join(projectsDir, `.memoir-archived-${ts}`);
188
+ await fs.ensureDir(archiveDir);
189
+
190
+ const localMemDir = path.join(projectsDir, localHomeKey, 'memory');
191
+ await fs.ensureDir(localMemDir);
192
+
193
+ let merged = 0;
194
+ for (const key of foreignKeys) {
195
+ const foreignMemDir = path.join(projectsDir, key, 'memory');
196
+ if (await fs.pathExists(foreignMemDir)) {
197
+ const before = (await fs.readdir(localMemDir)).length;
198
+ await mergeMemoryDirs(foreignMemDir, localMemDir);
199
+ merged += (await fs.readdir(localMemDir)).length - before;
200
+ }
201
+ await fs.move(path.join(projectsDir, key), path.join(archiveDir, key));
202
+ }
203
+
204
+ return { archived: foreignKeys, merged };
205
+ }
206
+
146
207
  // Merge memory dirs from a foreign machine — copies files that don't exist locally,
147
208
  // and for files that exist on both, keeps the newer version.
148
209
  async function mergeMemoryDirs(src, dest) {
@@ -291,6 +352,21 @@ export async function restoreMemories(sourceDir, spinner, onlyFilter = null, aut
291
352
  if (confirm) {
292
353
  const changes = { added: [], updated: [], skipped: [] };
293
354
 
355
+ // Clean up stale foreign home-key dirs left on this machine by older
356
+ // memoir versions. Must run before remap so detectLocalHomeKey sees a
357
+ // clean local state.
358
+ if (adapter.name === 'Claude CLI') {
359
+ try {
360
+ const { archived, merged } = await cleanupLocalForeignKeys(adapter.source);
361
+ if (archived.length > 0) {
362
+ spinner.stop();
363
+ console.log(chalk.gray(` Cleaned up ${archived.length} stale foreign dir(s) on this machine${merged > 0 ? ` (merged ${merged} new file(s))` : ''}`));
364
+ for (const k of archived) console.log(chalk.gray(` archived: ${k}`));
365
+ spinner.start();
366
+ }
367
+ } catch {}
368
+ }
369
+
294
370
  // Remap Claude project paths from source machine to this machine
295
371
  if (adapter.name === 'Claude CLI') {
296
372
  const remaps = remapProjectPaths(backupDir, adapter.source);