memoir-cli 3.0.1 → 3.0.3

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.0.1",
3
+ "version": "3.0.3",
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",
@@ -170,6 +170,60 @@ async function mergeMemoryDirs(src, dest) {
170
170
  }
171
171
  }
172
172
 
173
+ // After restore, ensure every memory .md file is referenced in its MEMORY.md index.
174
+ // Without this, files synced from another machine exist but Claude won't know about them.
175
+ async function reconcileMemoryIndexes(claudeSource) {
176
+ const projectsDir = path.join(claudeSource, 'projects');
177
+ if (!fs.existsSync(projectsDir)) return;
178
+
179
+ const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
180
+ for (const entry of entries) {
181
+ if (!entry.isDirectory()) continue;
182
+ const memDir = path.join(projectsDir, entry.name, 'memory');
183
+ if (!fs.existsSync(memDir)) continue;
184
+
185
+ const memoryMdPath = path.join(memDir, 'MEMORY.md');
186
+ let memoryMd = '';
187
+ if (fs.existsSync(memoryMdPath)) {
188
+ memoryMd = fs.readFileSync(memoryMdPath, 'utf8');
189
+ }
190
+
191
+ // Find all .md files in this memory dir
192
+ const mdFiles = fs.readdirSync(memDir)
193
+ .filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
194
+
195
+ // Check which ones are NOT referenced in MEMORY.md
196
+ const unreferenced = mdFiles.filter(f => {
197
+ const name = f.replace('.md', '');
198
+ // Check for markdown link [text](file.md) or plain filename mention
199
+ return !memoryMd.includes(f) && !memoryMd.includes(`(${f})`);
200
+ });
201
+
202
+ if (unreferenced.length === 0) continue;
203
+
204
+ // Read each unreferenced file to get its name/description from frontmatter
205
+ let additions = '\n\n## Synced from another machine\n';
206
+ for (const file of unreferenced) {
207
+ const content = fs.readFileSync(path.join(memDir, file), 'utf8');
208
+ // Try to extract name from frontmatter
209
+ const nameMatch = content.match(/^name:\s*(.+)/m);
210
+ const descMatch = content.match(/^description:\s*(.+)/m);
211
+ const name = nameMatch ? nameMatch[1].trim() : file.replace('.md', '').replace(/-/g, ' ');
212
+ const desc = descMatch ? descMatch[1].trim() : '';
213
+ additions += `- [${name}](${file})${desc ? ' — ' + desc : ''}\n`;
214
+ }
215
+
216
+ // Append to MEMORY.md
217
+ if (!memoryMd) {
218
+ memoryMd = '# Project Memory\n';
219
+ }
220
+ // Remove old "Synced from another machine" section if it exists, then re-add
221
+ memoryMd = memoryMd.replace(/\n\n## Synced from another machine\n[\s\S]*$/, '');
222
+ memoryMd = memoryMd.trimEnd() + additions;
223
+ fs.writeFileSync(memoryMdPath, memoryMd);
224
+ }
225
+ }
226
+
173
227
  async function syncFiles(src, dest, changes) {
174
228
  const entries = await fs.readdir(src, { withFileTypes: true });
175
229
  for (const entry of entries) {
@@ -284,6 +338,13 @@ export async function restoreMemories(sourceDir, spinner, onlyFilter = null, aut
284
338
  await syncFiles(backupDir, adapter.source, changes);
285
339
  }
286
340
 
341
+ // After syncing, reconcile MEMORY.md files
342
+ // MEMORY.md is an INDEX — it must reference all memory files from both machines
343
+ // This MUST run after syncFiles so newly copied files are included
344
+ if (adapter.name === 'Claude CLI') {
345
+ await reconcileMemoryIndexes(adapter.source);
346
+ }
347
+
287
348
  // Show summary of changes
288
349
  spinner.stop();
289
350
  const totalChanged = changes.added.length + changes.updated.length;
@@ -32,6 +32,8 @@ export async function pushCommand(options = {}) {
32
32
  const stagingDir = path.join(os.tmpdir(), `memoir-staging-${Date.now()}`);
33
33
  await fs.ensureDir(stagingDir);
34
34
 
35
+ let encryptedDir = null;
36
+
35
37
  try {
36
38
  // Profile-level tool filter (config.only) merged with CLI --only flag
37
39
  const onlyRaw = options.only || (config.only ? config.only.join(',') : null);
@@ -160,7 +162,7 @@ export async function pushCommand(options = {}) {
160
162
  }]);
161
163
  spinner.start(chalk.gray('Encrypting...'));
162
164
 
163
- const encryptedDir = path.join(os.tmpdir(), `memoir-encrypted-${Date.now()}`);
165
+ encryptedDir = path.join(os.tmpdir(), `memoir-encrypted-${Date.now()}`);
164
166
  await fs.ensureDir(encryptedDir);
165
167
  await encryptDirectory(stagingDir, encryptedDir, passphrase);
166
168
 
@@ -232,13 +234,8 @@ export async function pushCommand(options = {}) {
232
234
  } finally {
233
235
  await fs.remove(stagingDir);
234
236
  // Clean up encrypted dir if it was created
235
- if (true) {
236
- const encDirs = await fs.readdir(os.tmpdir());
237
- for (const d of encDirs) {
238
- if (d.startsWith('memoir-encrypted-')) {
239
- await fs.remove(path.join(os.tmpdir(), d)).catch(() => {});
240
- }
241
- }
237
+ if (encryptedDir) {
238
+ await fs.remove(encryptedDir).catch(() => {});
242
239
  }
243
240
  }
244
241
  }