memoir-cli 3.0.0 → 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/bin/memoir.js +2 -3
- package/package.json +1 -1
- package/src/adapters/restore.js +70 -2
- package/src/commands/push.js +5 -8
- package/src/commands/restore.js +14 -3
- package/src/commands/resume.js +7 -1
package/bin/memoir.js
CHANGED
|
@@ -239,9 +239,8 @@ program
|
|
|
239
239
|
console.log('\n' + chalk.cyan(`Updating memoir ${VERSION} → ${chalk.green.bold(latest)}...`) + '\n');
|
|
240
240
|
|
|
241
241
|
const { execSync } = await import('child_process');
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const cmd = useBun ? 'bun install -g memoir-cli' : 'npm install -g memoir-cli';
|
|
242
|
+
// Always use npm — bun installs to a different location and can cause PATH conflicts
|
|
243
|
+
const cmd = 'npm install -g memoir-cli';
|
|
245
244
|
|
|
246
245
|
execSync(cmd, { stdio: 'inherit' });
|
|
247
246
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.0.
|
|
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",
|
package/src/adapters/restore.js
CHANGED
|
@@ -9,7 +9,7 @@ import { adapters } from '../adapters/index.js';
|
|
|
9
9
|
// on this machine, rather than trying to compute the encoding ourselves.
|
|
10
10
|
// Claude's path encoding varies across platforms and versions, so detection
|
|
11
11
|
// is the only reliable approach.
|
|
12
|
-
function detectLocalHomeKey(adapterSource) {
|
|
12
|
+
export function detectLocalHomeKey(adapterSource) {
|
|
13
13
|
const localProjectsDir = path.join(adapterSource, 'projects');
|
|
14
14
|
if (!fs.existsSync(localProjectsDir)) return null;
|
|
15
15
|
|
|
@@ -65,7 +65,14 @@ function remapProjectPaths(backupDir, adapterSource) {
|
|
|
65
65
|
if (!localHomeKey) {
|
|
66
66
|
const home = os.homedir();
|
|
67
67
|
// Use the same encoding Claude uses: path with separators → dashes
|
|
68
|
-
|
|
68
|
+
// Claude encodes paths: each separator (/ \ :) → dash
|
|
69
|
+
// macOS /Users/cam → -Users-cam (leading / stripped, prefixed with -)
|
|
70
|
+
// Windows C:\Users\X → C--Users-X (C + dash for colon + dash for backslash)
|
|
71
|
+
if (process.platform === 'win32') {
|
|
72
|
+
localHomeKey = home.replace(/\\/g, '-').replace(/:/g, '-');
|
|
73
|
+
} else {
|
|
74
|
+
localHomeKey = '-' + home.replace(/^\//, '').replace(/\//g, '-');
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
// Step 3: Identify foreign home keys in the backup
|
|
@@ -163,6 +170,60 @@ async function mergeMemoryDirs(src, dest) {
|
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
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
|
+
|
|
166
227
|
async function syncFiles(src, dest, changes) {
|
|
167
228
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
168
229
|
for (const entry of entries) {
|
|
@@ -277,6 +338,13 @@ export async function restoreMemories(sourceDir, spinner, onlyFilter = null, aut
|
|
|
277
338
|
await syncFiles(backupDir, adapter.source, changes);
|
|
278
339
|
}
|
|
279
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
|
+
|
|
280
348
|
// Show summary of changes
|
|
281
349
|
spinner.stop();
|
|
282
350
|
const totalChanged = changes.added.length + changes.updated.length;
|
package/src/commands/push.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
236
|
-
|
|
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
|
}
|
package/src/commands/restore.js
CHANGED
|
@@ -9,6 +9,7 @@ import inquirer from 'inquirer';
|
|
|
9
9
|
import { getConfig } from '../config.js';
|
|
10
10
|
import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
|
|
11
11
|
import { decryptDirectory, verifyPassphrase } from '../security/encryption.js';
|
|
12
|
+
import { detectLocalHomeKey } from '../adapters/restore.js';
|
|
12
13
|
|
|
13
14
|
const home = os.homedir();
|
|
14
15
|
|
|
@@ -135,9 +136,19 @@ export async function restoreCommand(options = {}) {
|
|
|
135
136
|
await fs.writeFile(path.join(localHandoffDir, 'latest.md'), handoffContent);
|
|
136
137
|
|
|
137
138
|
// Inject into Claude's home-level memory so it's always loaded
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
if (await fs.pathExists(
|
|
139
|
+
// Use detection (reads what Claude actually created) with corrected fallback
|
|
140
|
+
const claudeDir = path.join(home, '.claude');
|
|
141
|
+
if (await fs.pathExists(claudeDir)) {
|
|
142
|
+
let homeKey = detectLocalHomeKey(claudeDir);
|
|
143
|
+
if (!homeKey) {
|
|
144
|
+
// Fallback: compute key matching Claude's actual encoding
|
|
145
|
+
if (process.platform === 'win32') {
|
|
146
|
+
homeKey = home.replace(/\\/g, '-').replace(/:/g, '-');
|
|
147
|
+
} else {
|
|
148
|
+
homeKey = '-' + home.replace(/^\//, '').replace(/\//g, '-');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const claudeMemDir = path.join(claudeDir, 'projects', homeKey, 'memory');
|
|
141
152
|
await fs.ensureDir(claudeMemDir);
|
|
142
153
|
await fs.writeFile(path.join(claudeMemDir, 'handoff.md'), handoffContent);
|
|
143
154
|
handoffInjected = true;
|
package/src/commands/resume.js
CHANGED
|
@@ -51,7 +51,13 @@ async function injectHandoff(content, tool) {
|
|
|
51
51
|
claude: () => {
|
|
52
52
|
// Write to Claude's project memory dir so it's auto-loaded
|
|
53
53
|
const cwd = process.cwd();
|
|
54
|
-
|
|
54
|
+
// Match Claude's actual path encoding: each separator → dash
|
|
55
|
+
let cwdKey;
|
|
56
|
+
if (process.platform === 'win32') {
|
|
57
|
+
cwdKey = cwd.replace(/\\/g, '-').replace(/:/g, '-');
|
|
58
|
+
} else {
|
|
59
|
+
cwdKey = '-' + cwd.replace(/^\//, '').replace(/\//g, '-');
|
|
60
|
+
}
|
|
55
61
|
const memDir = path.join(home, '.claude', 'projects', cwdKey, 'memory');
|
|
56
62
|
return path.join(memDir, 'handoff.md');
|
|
57
63
|
},
|