memoir-cli 2.1.0 → 2.2.0

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 CHANGED
@@ -7,6 +7,7 @@ import { initCommand } from '../src/commands/init.js';
7
7
  import { pushCommand } from '../src/commands/push.js';
8
8
  import { restoreCommand } from '../src/commands/restore.js';
9
9
  import { statusCommand } from '../src/commands/status.js';
10
+ import { doctorCommand } from '../src/commands/doctor.js';
10
11
  import { viewCommand } from '../src/commands/view.js';
11
12
  import { diffCommand } from '../src/commands/diff.js';
12
13
  import { migrateCommand } from '../src/commands/migrate.js';
@@ -26,11 +27,17 @@ async function checkForUpdate() {
26
27
  clearTimeout(timeout);
27
28
  const data = await res.json();
28
29
  const latest = data.version;
29
- if (latest && latest !== VERSION) {
30
+ // Only notify if remote is actually newer (not just different)
31
+ const isNewer = (a, b) => {
32
+ const [a1, a2, a3] = a.split('.').map(Number);
33
+ const [b1, b2, b3] = b.split('.').map(Number);
34
+ return a1 > b1 || (a1 === b1 && a2 > b2) || (a1 === b1 && a2 === b2 && a3 > b3);
35
+ };
36
+ if (latest && isNewer(latest, VERSION)) {
30
37
  console.log(
31
38
  '\n' + boxen(
32
39
  chalk.yellow(`Update available: ${VERSION} → ${chalk.green.bold(latest)}`) + '\n' +
33
- chalk.gray('Run: ') + chalk.cyan('npm install -g memoir-cli'),
40
+ chalk.gray('Run: ') + chalk.cyan('memoir update'),
34
41
  { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'yellow', dimBorder: true }
35
42
  )
36
43
  );
@@ -49,7 +56,8 @@ if (process.argv.length <= 2) {
49
56
  chalk.cyan(' memoir restore ') + chalk.gray('— restore on a new machine') + '\n' +
50
57
  chalk.cyan(' memoir snapshot ') + chalk.gray('— capture your current session') + '\n' +
51
58
  chalk.cyan(' memoir resume ') + chalk.gray('— pick up where you left off') + '\n' +
52
- chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n\n' +
59
+ chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
60
+ chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n\n' +
53
61
  chalk.gray(' Tip: use --only claude,gemini to sync specific tools') + '\n\n' +
54
62
  chalk.gray(`v${VERSION}`),
55
63
  { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
@@ -122,6 +130,19 @@ program
122
130
  }
123
131
  });
124
132
 
133
+ program
134
+ .command('doctor')
135
+ .alias('diagnose')
136
+ .description('Diagnose common issues with your memoir setup')
137
+ .action(async () => {
138
+ try {
139
+ await doctorCommand();
140
+ } catch (err) {
141
+ console.error(chalk.red('\n✖ Error:'), err.message);
142
+ process.exit(1);
143
+ }
144
+ });
145
+
125
146
  program
126
147
  .command('view')
127
148
  .alias('ls')
@@ -177,6 +198,47 @@ program
177
198
  }
178
199
  });
179
200
 
201
+ program
202
+ .command('update')
203
+ .alias('upgrade')
204
+ .description('Update memoir to the latest version')
205
+ .action(async () => {
206
+ try {
207
+ const res = await fetch('https://registry.npmjs.org/memoir-cli/latest');
208
+ const data = await res.json();
209
+ const latest = data.version;
210
+
211
+ if (latest === VERSION) {
212
+ console.log('\n' + boxen(
213
+ chalk.green('✔ Already up to date!') + '\n' +
214
+ chalk.gray(`v${VERSION}`),
215
+ { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'green', dimBorder: true }
216
+ ) + '\n');
217
+ return;
218
+ }
219
+
220
+ console.log('\n' + chalk.cyan(`Updating memoir ${VERSION} → ${chalk.green.bold(latest)}...`) + '\n');
221
+
222
+ const { execSync } = await import('child_process');
223
+ // Detect package manager — prefer the one that installed memoir
224
+ const execPath = process.argv[1] || '';
225
+ const useBun = execPath.includes('.bun') || process.env.BUN_INSTALL;
226
+ const cmd = useBun ? 'bun install -g memoir-cli' : 'npm install -g memoir-cli';
227
+
228
+ execSync(cmd, { stdio: 'inherit' });
229
+
230
+ console.log('\n' + boxen(
231
+ gradient.pastel(' Updated! ') + '\n\n' +
232
+ chalk.white(`memoir ${VERSION} → ${chalk.green.bold(latest)}`),
233
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
234
+ ) + '\n');
235
+ } catch (err) {
236
+ console.error(chalk.red('\n✖ Update failed:'), err.message);
237
+ console.log(chalk.gray('Try manually: ') + chalk.cyan('npm install -g memoir-cli'));
238
+ process.exit(1);
239
+ }
240
+ });
241
+
180
242
  program
181
243
  .command('migrate')
182
244
  .description('Translate memory between AI tools (Claude, Gemini, Codex, Cursor, etc.)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
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",
@@ -127,6 +127,80 @@ export const adapters = [
127
127
  return false;
128
128
  }
129
129
  },
130
+ {
131
+ name: 'Zed',
132
+ icon: '🔶',
133
+ source: isWin
134
+ ? path.join(appData, 'Zed')
135
+ : path.join(home, '.config', 'zed'),
136
+ filter: (src) => {
137
+ const zedDir = isWin
138
+ ? path.join(appData, 'Zed')
139
+ : path.join(home, '.config', 'zed');
140
+ const rel = path.relative(zedDir, src);
141
+ if (src === zedDir) return true;
142
+ const basename = path.basename(src);
143
+ // Skip known heavy/non-config directories
144
+ const skipDirs = ['extensions', 'themes', 'logs', 'db', 'copilot', 'node', 'languages'];
145
+ const topDir = rel.split(path.sep)[0];
146
+ if (skipDirs.includes(topDir)) return false;
147
+ // Only sync specific config files in root
148
+ const allowed = ['settings.json', 'keymap.json', 'tasks.json'];
149
+ if (allowed.includes(basename) && !rel.includes(path.sep)) return true;
150
+ // Allow .md files in root
151
+ if (basename.endsWith('.md') && !rel.includes(path.sep)) return true;
152
+ return false;
153
+ }
154
+ },
155
+ {
156
+ name: 'Cline',
157
+ icon: '🤖',
158
+ source: isWin
159
+ ? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
160
+ : path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
161
+ filter: (src) => {
162
+ const clineDir = isWin
163
+ ? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
164
+ : path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev');
165
+ const rel = path.relative(clineDir, src);
166
+ if (src === clineDir) return true;
167
+ const basename = path.basename(src);
168
+ const topDir = rel.split(path.sep)[0];
169
+ // Skip known heavy/non-config directories
170
+ const skipDirs = ['tasks', 'checkpoints', '.cache', 'images'];
171
+ if (skipDirs.includes(topDir)) return false;
172
+ // Allow settings/ and rules/ directories
173
+ if (topDir === 'settings' || topDir === 'rules') return true;
174
+ // Allow .md files in root
175
+ if (basename.endsWith('.md') && !rel.includes(path.sep)) return true;
176
+ return false;
177
+ }
178
+ },
179
+ {
180
+ name: 'Continue.dev',
181
+ icon: '🔄',
182
+ source: isWin
183
+ ? path.join(process.env.USERPROFILE || home, '.continue')
184
+ : path.join(home, '.continue'),
185
+ filter: (src) => {
186
+ const continueDir = isWin
187
+ ? path.join(process.env.USERPROFILE || home, '.continue')
188
+ : path.join(home, '.continue');
189
+ const rel = path.relative(continueDir, src);
190
+ if (src === continueDir) return true;
191
+ const basename = path.basename(src);
192
+ // Skip known heavy/non-config directories
193
+ const skipDirs = ['sessions', 'dev_data', 'logs', 'index', 'cache', 'types'];
194
+ const topDir = rel.split(path.sep)[0];
195
+ if (skipDirs.includes(topDir)) return false;
196
+ // Only sync specific config files in root
197
+ const allowed = ['config.json', 'config.ts', 'config.yaml', '.continuerules'];
198
+ if (allowed.includes(basename) && !rel.includes(path.sep)) return true;
199
+ // Allow .md files in root
200
+ if (basename.endsWith('.md') && !rel.includes(path.sep)) return true;
201
+ return false;
202
+ }
203
+ },
130
204
  {
131
205
  name: 'Aider',
132
206
  icon: '🔧',
@@ -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
 
@@ -0,0 +1,223 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import boxen from 'boxen';
5
+ import ora from 'ora';
6
+ import gradient from 'gradient-string';
7
+ import os from 'os';
8
+ import { execSync } from 'child_process';
9
+ import { getConfig } from '../config.js';
10
+ import { adapters } from '../adapters/index.js';
11
+
12
+ const SECRET_PATTERNS = [
13
+ { pattern: /sk-[a-zA-Z0-9]{20,}/, label: 'OpenAI/Stripe secret key' },
14
+ { pattern: /key-[a-zA-Z0-9]{20,}/, label: 'API key' },
15
+ { pattern: /ghp_[a-zA-Z0-9]{36,}/, label: 'GitHub personal access token' },
16
+ { pattern: /gho_[a-zA-Z0-9]{36,}/, label: 'GitHub OAuth token' },
17
+ { pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS access key' },
18
+ { pattern: /Bearer\s+[a-zA-Z0-9._\-]{20,}/, label: 'Bearer token' },
19
+ ];
20
+
21
+ const SENSITIVE_FILENAMES = ['.env', 'credentials', 'token.json'];
22
+
23
+ function formatSize(bytes) {
24
+ if (bytes < 1024) return `${bytes}B`;
25
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kb`;
26
+ return `${(bytes / (1024 * 1024)).toFixed(1)}mb`;
27
+ }
28
+
29
+ async function collectFiles(dir, filter) {
30
+ const files = [];
31
+ async function walk(d) {
32
+ let entries;
33
+ try {
34
+ entries = await fs.readdir(d, { withFileTypes: true });
35
+ } catch { return; }
36
+ for (const entry of entries) {
37
+ const fullPath = path.join(d, entry.name);
38
+ if (entry.isDirectory()) {
39
+ if (filter && !filter(fullPath)) continue;
40
+ await walk(fullPath);
41
+ } else {
42
+ if (filter && !filter(fullPath)) continue;
43
+ files.push(fullPath);
44
+ }
45
+ }
46
+ }
47
+ await walk(dir);
48
+ return files;
49
+ }
50
+
51
+ async function scanForSecrets(files) {
52
+ const warnings = [];
53
+ for (const filePath of files) {
54
+ const basename = path.basename(filePath);
55
+ if (SENSITIVE_FILENAMES.includes(basename)) {
56
+ warnings.push({ file: filePath, reason: `Sensitive filename: ${basename}` });
57
+ continue;
58
+ }
59
+ try {
60
+ const stat = await fs.stat(filePath);
61
+ // Skip files larger than 1MB
62
+ if (stat.size > 1024 * 1024) continue;
63
+ const content = await fs.readFile(filePath, 'utf-8');
64
+ for (const { pattern, label } of SECRET_PATTERNS) {
65
+ if (pattern.test(content)) {
66
+ warnings.push({ file: filePath, reason: label });
67
+ break;
68
+ }
69
+ }
70
+ } catch {
71
+ // Skip unreadable files
72
+ }
73
+ }
74
+ return warnings;
75
+ }
76
+
77
+ export async function doctorCommand() {
78
+ const spinner = ora({ text: 'Running diagnostics...', color: 'cyan' }).start();
79
+ const lines = [];
80
+ let passCount = 0;
81
+ let warnCount = 0;
82
+ let failCount = 0;
83
+
84
+ const pass = (msg) => { passCount++; return chalk.green(' ✔ ') + msg; };
85
+ const warn = (msg) => { warnCount++; return chalk.yellow(' ⚠ ') + msg; };
86
+ const fail = (msg) => { failCount++; return chalk.red(' ✖ ') + msg; };
87
+
88
+ // 1. Config check
89
+ spinner.text = 'Checking configuration...';
90
+ const config = await getConfig();
91
+ if (config) {
92
+ const providerLabel = config.provider === 'git' ? 'git' : 'local';
93
+ const dest = config.provider === 'git' ? config.gitRepo : config.localPath;
94
+ lines.push(pass(`Config: ${chalk.cyan(providerLabel)} → ${chalk.gray(dest)}`));
95
+ } else {
96
+ lines.push(fail(`Config: not initialized — run ${chalk.cyan('memoir init')}`));
97
+ }
98
+
99
+ // 2. Git check
100
+ spinner.text = 'Checking git...';
101
+ let gitInstalled = false;
102
+ try {
103
+ execSync('git --version', { stdio: 'pipe' });
104
+ gitInstalled = true;
105
+ lines.push(pass('Git: installed'));
106
+ } catch {
107
+ lines.push(fail('Git: not installed'));
108
+ }
109
+
110
+ if (config?.provider === 'git' && gitInstalled && config.gitRepo) {
111
+ spinner.text = 'Testing remote connectivity...';
112
+ try {
113
+ execSync(`git ls-remote ${config.gitRepo} HEAD`, { stdio: 'pipe', timeout: 10000 });
114
+ lines.push(pass(`Remote: ${chalk.gray(config.gitRepo)} reachable`));
115
+ } catch {
116
+ lines.push(fail(`Remote: cannot reach ${chalk.gray(config.gitRepo)}`));
117
+ }
118
+ }
119
+
120
+ // 3. AI Tools scan
121
+ spinner.text = 'Scanning AI tools...';
122
+ lines.push('');
123
+ lines.push(chalk.bold.white(' AI Tools'));
124
+
125
+ const allSyncFiles = [];
126
+ let totalSize = 0;
127
+
128
+ for (const adapter of adapters) {
129
+ let found = false;
130
+ let fileCount = 0;
131
+ let size = 0;
132
+ let adapterFiles = [];
133
+
134
+ if (adapter.customExtract) {
135
+ for (const file of adapter.files) {
136
+ const filePath = path.join(adapter.source, file);
137
+ if (await fs.pathExists(filePath)) {
138
+ found = true;
139
+ fileCount++;
140
+ try {
141
+ const stat = await fs.stat(filePath);
142
+ size += stat.size;
143
+ adapterFiles.push(filePath);
144
+ } catch {}
145
+ }
146
+ }
147
+ } else if (await fs.pathExists(adapter.source)) {
148
+ found = true;
149
+ adapterFiles = await collectFiles(adapter.source, adapter.filter);
150
+ fileCount = adapterFiles.length;
151
+ for (const f of adapterFiles) {
152
+ try {
153
+ const stat = await fs.stat(f);
154
+ size += stat.size;
155
+ } catch {}
156
+ }
157
+ }
158
+
159
+ if (found) {
160
+ lines.push(pass(`${adapter.name}: ${chalk.gray(`${fileCount} files, ${formatSize(size)}`)}`));
161
+ allSyncFiles.push(...adapterFiles);
162
+ totalSize += size;
163
+ } else {
164
+ lines.push(chalk.gray(' ○ ') + chalk.gray(adapter.name + ': not found'));
165
+ }
166
+ }
167
+
168
+ // 4. Secrets scan
169
+ spinner.text = 'Scanning for secrets...';
170
+ lines.push('');
171
+ lines.push(chalk.bold.white(' Security'));
172
+
173
+ const secretWarnings = await scanForSecrets(allSyncFiles);
174
+ if (secretWarnings.length === 0) {
175
+ lines.push(pass('No secrets detected in sync files'));
176
+ } else {
177
+ lines.push(warn(`${secretWarnings.length} potential secret${secretWarnings.length !== 1 ? 's' : ''} found:`));
178
+ for (const w of secretWarnings.slice(0, 5)) {
179
+ lines.push(chalk.yellow(' → ') + chalk.gray(path.basename(w.file)) + chalk.yellow(` (${w.reason})`));
180
+ }
181
+ if (secretWarnings.length > 5) {
182
+ lines.push(chalk.gray(` ...and ${secretWarnings.length - 5} more`));
183
+ }
184
+ }
185
+
186
+ // 5. Disk usage
187
+ lines.push('');
188
+ lines.push(chalk.bold.white(' Disk'));
189
+ lines.push(pass(`Total backup size: ${chalk.cyan(formatSize(totalSize))} across ${chalk.cyan(allSyncFiles.length)} files`));
190
+
191
+ // 6. Last sync
192
+ if (config?.provider === 'git' && gitInstalled && config.gitRepo) {
193
+ spinner.text = 'Checking last sync...';
194
+ lines.push('');
195
+ lines.push(chalk.bold.white(' Last Sync'));
196
+ try {
197
+ const tmpDir = path.join(os.tmpdir(), 'memoir-doctor-' + Date.now());
198
+ execSync(`git clone --depth 1 ${config.gitRepo} ${tmpDir}`, { stdio: 'pipe', timeout: 15000 });
199
+ const lastCommit = execSync('git log -1 --format=%cr', { cwd: tmpDir, stdio: 'pipe' }).toString().trim();
200
+ const lastMsg = execSync('git log -1 --format=%s', { cwd: tmpDir, stdio: 'pipe' }).toString().trim();
201
+ await fs.remove(tmpDir);
202
+ lines.push(pass(`Last backup: ${chalk.cyan(lastCommit)} — ${chalk.gray(lastMsg)}`));
203
+ } catch {
204
+ lines.push(warn('Could not determine last sync time'));
205
+ }
206
+ }
207
+
208
+ spinner.stop();
209
+
210
+ // Summary
211
+ const summaryParts = [];
212
+ if (passCount > 0) summaryParts.push(chalk.green(`${passCount} passed`));
213
+ if (warnCount > 0) summaryParts.push(chalk.yellow(`${warnCount} warning${warnCount !== 1 ? 's' : ''}`));
214
+ if (failCount > 0) summaryParts.push(chalk.red(`${failCount} failed`));
215
+
216
+ console.log('\n' + boxen(
217
+ gradient.pastel(' memoir doctor ') + '\n\n' +
218
+ lines.join('\n') + '\n\n' +
219
+ chalk.gray('─'.repeat(36)) + '\n' +
220
+ ' ' + summaryParts.join(chalk.gray(' · ')),
221
+ { padding: 1, borderStyle: 'round', borderColor: failCount > 0 ? 'red' : warnCount > 0 ? 'yellow' : 'green', dimBorder: true }
222
+ ) + '\n');
223
+ }
@@ -87,7 +87,25 @@ export async function initCommand() {
87
87
  const repo = answers.repo.trim();
88
88
 
89
89
  config.gitRepo = `https://github.com/${username}/${repo}.git`;
90
- console.log(chalk.gray(` → ${config.gitRepo}\n`));
90
+ console.log(chalk.gray(` → ${config.gitRepo}`));
91
+
92
+ // Auto-create the repo if gh CLI is available and repo doesn't exist
93
+ if (direction === 'upload') {
94
+ try {
95
+ execFileSync('gh', ['repo', 'view', `${username}/${repo}`], { stdio: 'ignore' });
96
+ console.log(chalk.gray(' ✔ Repo exists\n'));
97
+ } catch {
98
+ // Repo doesn't exist — try to create it
99
+ try {
100
+ execFileSync('gh', ['repo', 'create', `${username}/${repo}`, '--private', '--description', 'AI memory backup (memoir-cli)'], { stdio: 'ignore' });
101
+ console.log(chalk.green(' ✔ Created private repo\n'));
102
+ } catch {
103
+ console.log(chalk.yellow(' ⚠ Could not auto-create repo. Create it manually on GitHub.\n'));
104
+ }
105
+ }
106
+ } else {
107
+ console.log('');
108
+ }
91
109
  }
92
110
 
93
111
  await saveConfig(config);
@@ -23,8 +23,8 @@ export async function statusCommand() {
23
23
  }
24
24
 
25
25
  // Detected tools
26
- const lines = [];
27
- let detected = 0;
26
+ const foundTools = [];
27
+ const notFound = [];
28
28
 
29
29
  for (const adapter of adapters) {
30
30
  let found = false;
@@ -40,22 +40,30 @@ export async function statusCommand() {
40
40
  }
41
41
 
42
42
  if (found) {
43
- lines.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
44
- detected++;
43
+ foundTools.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
45
44
  } else {
46
- lines.push(chalk.gray(' ○ ' + adapter.name));
45
+ notFound.push(adapter.name);
47
46
  }
48
47
  }
49
48
 
50
- const summary = detected > 0
51
- ? chalk.white(`${detected} tool${detected !== 1 ? 's' : ''} ready to sync`)
52
- : chalk.yellow('No AI tools detected');
49
+ const lines = foundTools.length > 0
50
+ ? foundTools
51
+ : [chalk.yellow(' No AI tools detected')];
52
+
53
+ const summary = foundTools.length > 0
54
+ ? chalk.white(`${foundTools.length} tool${foundTools.length !== 1 ? 's' : ''} ready to sync`)
55
+ : chalk.gray(`Supports: ${adapters.map(a => a.name).join(', ')}`);
56
+
57
+ // Show not-found tools as a compact line if there are found tools
58
+ const notFoundLine = foundTools.length > 0 && notFound.length > 0
59
+ ? '\n' + chalk.gray(` Also supports: ${notFound.join(', ')}`)
60
+ : '';
53
61
 
54
62
  console.log(boxen(
55
63
  gradient.pastel(' memoir status ') + '\n\n' +
56
64
  configLine + '\n\n' +
57
65
  chalk.bold.white('AI Tools') + '\n' +
58
- lines.join('\n') + '\n\n' +
66
+ lines.join('\n') + notFoundLine + '\n\n' +
59
67
  chalk.gray('─'.repeat(30)) + '\n' +
60
68
  summary,
61
69
  { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
@@ -0,0 +1,72 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const home = os.homedir();
6
+ const cwd = process.cwd();
7
+
8
+ export default {
9
+ key: 'cline',
10
+ name: 'Cline',
11
+ icon: '🤖',
12
+ format: 'Configuration and rules for Cline AI coding assistant. Includes settings for AI behavior and custom rules files for project-specific instructions.',
13
+
14
+ discover() {
15
+ const files = [];
16
+ const clineDir = process.platform === 'win32'
17
+ ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
18
+ : path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev');
19
+
20
+ // Check for .clinerules in project
21
+ const projectFile = path.join(cwd, '.clinerules');
22
+ if (fs.existsSync(projectFile)) {
23
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
24
+ }
25
+
26
+ // Discover settings and rules from Cline extension storage
27
+ if (fs.existsSync(clineDir)) {
28
+ const scanDir = (dir, scope) => {
29
+ try {
30
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
31
+ for (const entry of entries) {
32
+ const filePath = path.join(dir, entry.name);
33
+ if (entry.isFile()) {
34
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope });
35
+ } else if (entry.isDirectory()) {
36
+ scanDir(filePath, scope);
37
+ }
38
+ }
39
+ } catch {}
40
+ };
41
+
42
+ const settingsDir = path.join(clineDir, 'settings');
43
+ if (fs.existsSync(settingsDir)) {
44
+ scanDir(settingsDir, 'user');
45
+ }
46
+
47
+ const rulesDir = path.join(clineDir, 'rules');
48
+ if (fs.existsSync(rulesDir)) {
49
+ scanDir(rulesDir, 'user');
50
+ }
51
+
52
+ // Discover .md files in root
53
+ try {
54
+ const entries = fs.readdirSync(clineDir);
55
+ for (const entry of entries) {
56
+ if (entry.endsWith('.md')) {
57
+ const filePath = path.join(clineDir, entry);
58
+ if (fs.statSync(filePath).isFile()) {
59
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
60
+ }
61
+ }
62
+ }
63
+ } catch {}
64
+ }
65
+
66
+ return files;
67
+ },
68
+
69
+ targetPath() {
70
+ return path.join(cwd, '.clinerules');
71
+ }
72
+ };
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const home = os.homedir();
6
+ const cwd = process.cwd();
7
+
8
+ export default {
9
+ key: 'continuedev',
10
+ name: 'Continue.dev',
11
+ icon: '🔄',
12
+ format: 'JSON, TypeScript, or YAML configuration for Continue.dev AI assistant. Includes config files for model selection, context providers, and slash commands. Supports .continuerules for project-specific instructions.',
13
+
14
+ discover() {
15
+ const files = [];
16
+ const continueDir = process.platform === 'win32'
17
+ ? path.join(process.env.USERPROFILE || home, '.continue')
18
+ : path.join(home, '.continue');
19
+
20
+ // Check for .continuerules in project
21
+ const projectFile = path.join(cwd, '.continuerules');
22
+ if (fs.existsSync(projectFile)) {
23
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
24
+ }
25
+
26
+ if (fs.existsSync(continueDir)) {
27
+ const configFiles = ['config.json', 'config.ts', 'config.yaml', '.continuerules'];
28
+ for (const file of configFiles) {
29
+ const filePath = path.join(continueDir, file);
30
+ if (fs.existsSync(filePath)) {
31
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
32
+ }
33
+ }
34
+
35
+ // Discover .md files in root
36
+ try {
37
+ const entries = fs.readdirSync(continueDir);
38
+ for (const entry of entries) {
39
+ if (entry.endsWith('.md')) {
40
+ const filePath = path.join(continueDir, entry);
41
+ if (fs.statSync(filePath).isFile()) {
42
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
43
+ }
44
+ }
45
+ }
46
+ } catch {}
47
+ }
48
+
49
+ return files;
50
+ },
51
+
52
+ targetPath() {
53
+ return path.join(cwd, '.continuerules');
54
+ }
55
+ };
@@ -4,10 +4,13 @@ import codex from './codex.js';
4
4
  import cursor from './cursor.js';
5
5
  import copilot from './copilot.js';
6
6
  import windsurf from './windsurf.js';
7
+ import zed from './zed.js';
8
+ import cline from './cline.js';
9
+ import continuedev from './continuedev.js';
7
10
  import aider from './aider.js';
8
11
 
9
12
  const registry = {};
10
- for (const tool of [claude, gemini, codex, cursor, copilot, windsurf, aider]) {
13
+ for (const tool of [claude, gemini, codex, cursor, copilot, windsurf, zed, cline, continuedev, aider]) {
11
14
  registry[tool.key] = tool;
12
15
  }
13
16
 
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const home = os.homedir();
6
+ const cwd = process.cwd();
7
+
8
+ export default {
9
+ key: 'zed',
10
+ name: 'Zed',
11
+ icon: '🔶',
12
+ format: 'JSON or markdown configuration files for Zed editor. Includes settings.json for editor preferences, keymap.json for keybindings, and tasks.json for task runner configs.',
13
+
14
+ discover() {
15
+ const files = [];
16
+ const zedDir = process.platform === 'win32'
17
+ ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Zed')
18
+ : path.join(home, '.config', 'zed');
19
+
20
+ if (fs.existsSync(zedDir)) {
21
+ const configFiles = ['settings.json', 'keymap.json', 'tasks.json'];
22
+ for (const file of configFiles) {
23
+ const filePath = path.join(zedDir, file);
24
+ if (fs.existsSync(filePath)) {
25
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
26
+ }
27
+ }
28
+ // Also discover .md files in root
29
+ try {
30
+ const entries = fs.readdirSync(zedDir);
31
+ for (const entry of entries) {
32
+ if (entry.endsWith('.md')) {
33
+ const filePath = path.join(zedDir, entry);
34
+ if (fs.statSync(filePath).isFile()) {
35
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
36
+ }
37
+ }
38
+ }
39
+ } catch {}
40
+ }
41
+ return files;
42
+ },
43
+
44
+ targetPath() {
45
+ const zedDir = process.platform === 'win32'
46
+ ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Zed')
47
+ : path.join(home, '.config', 'zed');
48
+ return path.join(zedDir, 'settings.json');
49
+ }
50
+ };