memoir-cli 2.1.1 → 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.1",
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: '🔧',
@@ -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
+ };