memoir-cli 1.4.3 → 1.4.5

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm version](https://img.shields.io/npm/v/memoir-cli.svg?style=flat-square)](https://npmjs.org/package/memoir-cli)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
8
8
 
9
- *Never lose your AI's context again. Sync Gemini CLI, Claude Code, and more across all your devices instantly.*
9
+ *Never lose your AI's context again. Sync and translate your AI memory across every device and tool.*
10
10
 
11
11
  </div>
12
12
 
@@ -22,9 +22,9 @@ Suddenly, you're starting from scratch. Your AI's "memory" is trapped in hidden
22
22
 
23
23
  ## 🚀 The Solution
24
24
 
25
- `memoir` is a zero-friction CLI tool that seamlessly extracts, backs up, and restores your AI's memory across any computer. You bring your own storage (a private GitHub repo or an iCloud/Dropbox folder), and `memoir` handles the rest safely and securely.
25
+ `memoir` is a zero-friction CLI that extracts, backs up, restores, and **translates** your AI's memory across any computer and any tool. Bring your own storage (a private GitHub repo or an iCloud/Dropbox folder), and `memoir` handles the rest.
26
26
 
27
- No locked-in SaaS, no lost context, no complex shell scripts.
27
+ No locked-in SaaS, no lost context, no complex shell scripts. Switch from Claude to Gemini in one command.
28
28
 
29
29
  ### Supported Integrations
30
30
  - [x] **Gemini CLI**
@@ -72,6 +72,97 @@ memoir restore
72
72
  memoir pull
73
73
  ```
74
74
 
75
+ ### 4. Translate Between Tools
76
+ Switch AI tools without losing context. Memoir uses Gemini AI to intelligently rewrite your memory files for any supported tool:
77
+
78
+ ```bash
79
+ memoir migrate --from claude --to gemini
80
+ # or run interactively:
81
+ memoir migrate
82
+ ```
83
+
84
+ Your Claude instructions become a proper `GEMINI.md` — not a copy-paste, but a real translation that follows each tool's conventions.
85
+
86
+ ---
87
+
88
+ ## 📖 All Commands
89
+
90
+ | Command | What it does |
91
+ |---------|-------------|
92
+ | `memoir init` | Setup wizard — pick GitHub or local folder, upload or download |
93
+ | `memoir push` | Extract all AI tool configs, back up to GitHub/local |
94
+ | `memoir restore` | Pull backup down, restore missing files (non-destructive) |
95
+ | `memoir status` | Show which AI tools are detected on this machine |
96
+ | `memoir view` | Preview backup contents with diffs against local |
97
+ | `memoir migrate` | Translate memory between tools via Gemini AI |
98
+
99
+ ---
100
+
101
+ ## 🎯 Common Workflows
102
+
103
+ ### New laptop setup
104
+ ```bash
105
+ # Old machine — save everything
106
+ memoir init # → Upload → GitHub
107
+
108
+ # New machine — restore everything
109
+ memoir init # → Download → GitHub
110
+ # All your .claude/, .gemini/, .cursorrules configs restored in 30 seconds
111
+ ```
112
+
113
+ ### Switch from Claude to Gemini (or any tool)
114
+ ```bash
115
+ memoir migrate --from claude --to gemini
116
+ ```
117
+ Your CLAUDE.md + Claude memory files get intelligently rewritten as a proper GEMINI.md — not a copy-paste, but a real translation that follows Gemini's conventions.
118
+
119
+ ### Keep your whole team in sync
120
+ ```bash
121
+ # Team lead writes CLAUDE.md, then generates for everyone else:
122
+ memoir migrate --from claude --to cursor
123
+ memoir migrate --from claude --to copilot
124
+ memoir migrate --from claude --to codex
125
+ ```
126
+
127
+ ### Fan out to every tool at once
128
+ ```bash
129
+ memoir migrate --from claude --to gemini
130
+ memoir migrate --from claude --to codex
131
+ memoir migrate --from claude --to cursor
132
+ memoir migrate --from claude --to windsurf
133
+ memoir migrate --from claude --to aider
134
+ ```
135
+ Use one tool as the source of truth, propagate to all others.
136
+
137
+ ### Preview before committing
138
+ ```bash
139
+ memoir migrate --from gemini --to claude --dry-run
140
+ # Shows translated output but writes nothing
141
+ ```
142
+
143
+ ### Protect existing files
144
+ ```bash
145
+ memoir migrate --from claude --to gemini
146
+ # → "GEMINI.md already exists."
147
+ # → Overwrite / Append / Skip
148
+ # Append adds a dated separator so you keep your existing instructions
149
+ ```
150
+
151
+ ### Daily sync across machines
152
+ ```bash
153
+ # End of day
154
+ memoir push
155
+
156
+ # Next morning, different machine
157
+ memoir pull
158
+ ```
159
+
160
+ ### Check what's on this machine
161
+ ```bash
162
+ memoir status
163
+ # Shows checkmarks for every detected AI tool and their config locations
164
+ ```
165
+
75
166
  ---
76
167
 
77
168
  ## 🔒 Security First
@@ -82,12 +173,12 @@ Our specialized adapters intelligently filter your directories. We **only** sync
82
173
 
83
174
  ---
84
175
 
85
- ## 🗺️ Roadmap: The Future of Data Portability
86
-
87
- We believe developers shouldn't be locked into a single AI ecosystem.
176
+ ## 🗺️ Roadmap
88
177
 
89
- **Coming in v2.0: The Migration Engine**
90
- Currently, `memoir` backs up your files exactly as they are. Soon, you will be able to run `memoir migrate --from claude --to gemini`. The CLI will automatically translate your Claude Code instructions into Gemini CLI facts, allowing you to fluidly swap AI providers without losing a drop of context.
178
+ **What's next:**
179
+ - Team sharing sync a shared memory repo across your whole team
180
+ - Auto-detect new AI tools as they appear
181
+ - Two-way merge — combine memories from multiple tools into one
91
182
 
92
183
  ---
93
184
 
package/bin/memoir.js CHANGED
@@ -8,8 +8,11 @@ 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
10
  import { viewCommand } from '../src/commands/view.js';
11
+ import { migrateCommand } from '../src/commands/migrate.js';
12
+ import { createRequire } from 'module';
11
13
 
12
- const VERSION = '1.2.0';
14
+ const require = createRequire(import.meta.url);
15
+ const { version: VERSION } = require('../package.json');
13
16
 
14
17
  // Custom help banner
15
18
  program.addHelpText('beforeAll', '\n' + boxen(
@@ -88,15 +91,17 @@ program
88
91
 
89
92
  program
90
93
  .command('migrate')
91
- .description('Translate memory between AI providers')
92
- .action(() => {
93
- console.log('\n' + boxen(
94
- gradient.pastel(' memoir migrate ') + '\n\n' +
95
- chalk.white('Instantly translate your context between') + '\n' +
96
- chalk.white('Claude, Gemini, Codex, and more.') + '\n\n' +
97
- chalk.cyan.bold('Coming soon.'),
98
- { padding: 1, borderStyle: 'round', borderColor: 'yellow', align: 'center' }
99
- ) + '\n');
94
+ .description('Translate memory between AI tools (Claude, Gemini, Codex, Cursor, etc.)')
95
+ .option('--from <tool>', 'Source tool (claude, gemini, codex, cursor, copilot, windsurf, aider)')
96
+ .option('--to <tool>', 'Target tool (claude, gemini, codex, cursor, copilot, windsurf, aider, all)')
97
+ .option('--dry-run', 'Preview translation without writing files')
98
+ .action(async (options) => {
99
+ try {
100
+ await migrateCommand(options);
101
+ } catch (err) {
102
+ console.error(chalk.red('\n✖ Error during migration:'), err.message);
103
+ process.exit(1);
104
+ }
100
105
  });
101
106
 
102
107
  program.parse();
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "1.4.3",
4
- "description": "Your AI remembers everything. Sync it everywhere.",
3
+ "version": "1.4.5",
4
+ "description": "Sync and translate AI memory across devices and tools. Back up Claude, Gemini, Codex, Cursor, Copilot, Windsurf, and Aider configs. Migrate instructions between AI coding assistants with one command.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "memoir": "bin/memoir.js"
9
9
  },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/camgitt/memoir.git"
13
+ },
14
+ "homepage": "https://github.com/camgitt/memoir#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/camgitt/memoir/issues"
17
+ },
10
18
  "scripts": {
11
19
  "start": "node bin/memoir.js",
12
20
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -16,14 +24,30 @@
16
24
  "cli",
17
25
  "sync",
18
26
  "memory",
19
- "memoir",
20
- "gemini",
21
- "claude",
22
27
  "backup",
23
- "restore"
28
+ "restore",
29
+ "migrate",
30
+ "translate",
31
+ "claude",
32
+ "gemini",
33
+ "codex",
34
+ "cursor",
35
+ "copilot",
36
+ "windsurf",
37
+ "aider",
38
+ "ai-memory",
39
+ "ai-tools",
40
+ "dotfiles",
41
+ "developer-tools",
42
+ "claude-code",
43
+ "gemini-cli",
44
+ "openai",
45
+ "ai-assistant",
46
+ "coding-assistant",
47
+ "context-sync"
24
48
  ],
25
- "author": "",
26
- "license": "ISC",
49
+ "author": "camgitt",
50
+ "license": "MIT",
27
51
  "dependencies": {
28
52
  "boxen": "^7.1.1",
29
53
  "chalk": "^5.3.0",
@@ -31,6 +55,7 @@
31
55
  "fs-extra": "^11.2.0",
32
56
  "gradient-string": "^3.0.0",
33
57
  "inquirer": "^9.2.15",
58
+ "memoir-cli": "^1.4.4",
34
59
  "open": "^11.0.0",
35
60
  "ora": "^7.0.1"
36
61
  }
@@ -11,6 +11,7 @@ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
11
11
  export const adapters = [
12
12
  {
13
13
  name: 'Gemini CLI',
14
+ icon: '🔵',
14
15
  source: path.join(home, '.gemini'),
15
16
  filter: (src) => {
16
17
  const basename = path.basename(src);
@@ -20,6 +21,7 @@ export const adapters = [
20
21
  },
21
22
  {
22
23
  name: 'Claude CLI',
24
+ icon: '🟣',
23
25
  source: path.join(home, '.claude'),
24
26
  filter: (src) => {
25
27
  const basename = path.basename(src);
@@ -28,6 +30,7 @@ export const adapters = [
28
30
  },
29
31
  {
30
32
  name: 'OpenAI Codex',
33
+ icon: '🟢',
31
34
  source: path.join(home, '.codex'),
32
35
  filter: (src) => {
33
36
  const basename = path.basename(src);
@@ -37,6 +40,7 @@ export const adapters = [
37
40
  },
38
41
  {
39
42
  name: 'Cursor',
43
+ icon: '⚡',
40
44
  source: isWin
41
45
  ? path.join(appData, 'Cursor', 'User')
42
46
  : path.join(home, 'Library', 'Application Support', 'Cursor', 'User'),
@@ -48,6 +52,7 @@ export const adapters = [
48
52
  },
49
53
  {
50
54
  name: 'GitHub Copilot',
55
+ icon: '🐙',
51
56
  source: isWin
52
57
  ? path.join(appData, 'GitHub Copilot')
53
58
  : path.join(home, '.config', 'github-copilot'),
@@ -59,6 +64,7 @@ export const adapters = [
59
64
  },
60
65
  {
61
66
  name: 'Windsurf',
67
+ icon: '🏄',
62
68
  source: isWin
63
69
  ? path.join(appData, 'Windsurf', 'User')
64
70
  : path.join(home, 'Library', 'Application Support', 'Windsurf', 'User'),
@@ -70,6 +76,7 @@ export const adapters = [
70
76
  },
71
77
  {
72
78
  name: 'Aider',
79
+ icon: '🔧',
73
80
  source: home,
74
81
  customExtract: true,
75
82
  files: ['.aider.conf.yml', '.aider.system-prompt.md'],
@@ -77,33 +84,96 @@ export const adapters = [
77
84
  }
78
85
  ];
79
86
 
87
+ async function countFiles(dir) {
88
+ let count = 0;
89
+ const entries = await fs.readdir(dir, { withFileTypes: true });
90
+ for (const entry of entries) {
91
+ if (entry.isDirectory()) {
92
+ count += await countFiles(path.join(dir, entry.name));
93
+ } else {
94
+ count++;
95
+ }
96
+ }
97
+ return count;
98
+ }
99
+
100
+ async function dirSize(dir) {
101
+ let size = 0;
102
+ const entries = await fs.readdir(dir, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ const fullPath = path.join(dir, entry.name);
105
+ if (entry.isDirectory()) {
106
+ size += await dirSize(fullPath);
107
+ } else {
108
+ const stat = await fs.stat(fullPath);
109
+ size += stat.size;
110
+ }
111
+ }
112
+ return size;
113
+ }
114
+
115
+ function formatSize(bytes) {
116
+ if (bytes < 1024) return `${bytes}B`;
117
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kb`;
118
+ return `${(bytes / (1024 * 1024)).toFixed(1)}mb`;
119
+ }
120
+
80
121
  export async function extractMemories(stagingDir, spinner) {
81
122
  let foundAny = false;
123
+ const results = [];
82
124
 
83
125
  for (const adapter of adapters) {
84
126
  if (adapter.customExtract) {
85
- // Handle tools with individual files (e.g. Aider)
86
127
  const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
87
128
  let foundFile = false;
129
+ let fileCount = 0;
130
+
88
131
  for (const file of adapter.files) {
89
132
  const filePath = path.join(adapter.source, file);
90
133
  if (await fs.pathExists(filePath)) {
91
134
  if (!foundFile) {
92
- spinner.text = `Found ${chalk.cyan(adapter.name)} config... copying to staging`;
135
+ spinner.text = `${adapter.icon} Scanning ${chalk.cyan(adapter.name)}...`;
93
136
  await fs.ensureDir(dest);
94
137
  foundFile = true;
95
138
  }
96
139
  await fs.copy(filePath, path.join(dest, file));
140
+ fileCount++;
97
141
  }
98
142
  }
99
- if (foundFile) foundAny = true;
143
+
144
+ if (foundFile) {
145
+ foundAny = true;
146
+ results.push({ adapter, fileCount, size: await dirSize(dest) });
147
+ spinner.text = `${adapter.icon} ${chalk.green(adapter.name)} ${chalk.gray(`(${fileCount} files)`)}`;
148
+ }
100
149
  } else if (await fs.pathExists(adapter.source)) {
101
- spinner.text = `Found ${chalk.cyan(adapter.name)} memory... copying to staging`;
150
+ spinner.text = `${adapter.icon} Scanning ${chalk.cyan(adapter.name)}...`;
102
151
  const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
103
152
  await fs.ensureDir(dest);
104
153
  await fs.copy(adapter.source, dest, { filter: adapter.filter });
154
+
155
+ const fileCount = await countFiles(dest);
156
+ const size = await dirSize(dest);
105
157
  foundAny = true;
158
+ results.push({ adapter, fileCount, size });
159
+ spinner.text = `${adapter.icon} ${chalk.green(adapter.name)} ${chalk.gray(`(${fileCount} files, ${formatSize(size)})`)}`;
160
+ }
161
+ }
162
+
163
+ // Print tree after scanning
164
+ if (results.length > 0) {
165
+ spinner.stop();
166
+ console.log('');
167
+ console.log(chalk.white.bold(' Detected AI tools:\n'));
168
+ for (let i = 0; i < results.length; i++) {
169
+ const r = results[i];
170
+ const isLast = i === results.length - 1;
171
+ const branch = isLast ? ' └─' : ' ├─';
172
+ const detail = chalk.gray(` ${r.fileCount} files, ${formatSize(r.size)}`);
173
+ console.log(`${branch} ${r.adapter.icon} ${chalk.cyan(r.adapter.name)}${detail}`);
106
174
  }
175
+ console.log('');
176
+ spinner.start(chalk.gray('Uploading...'));
107
177
  }
108
178
 
109
179
  return foundAny;
@@ -2,17 +2,27 @@ import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
3
  import boxen from 'boxen';
4
4
  import gradient from 'gradient-string';
5
- import { execSync } from 'child_process';
5
+ import { execFileSync } from 'child_process';
6
6
  import { saveConfig } from '../config.js';
7
7
  import { pushCommand } from './push.js';
8
8
  import { restoreCommand } from './restore.js';
9
9
 
10
10
  function getGitUsername() {
11
11
  try {
12
- return execSync('git config --global user.name', { encoding: 'utf8' }).trim();
12
+ return execFileSync('git', ['config', '--global', 'user.name'], { encoding: 'utf8' }).trim();
13
13
  } catch { return ''; }
14
14
  }
15
15
 
16
+ function getGitHubUsername() {
17
+ try {
18
+ // Try gh CLI first
19
+ return execFileSync('gh', ['api', 'user', '--jq', '.login'], { encoding: 'utf8' }).trim();
20
+ } catch {
21
+ // Fall back to git config
22
+ return getGitUsername();
23
+ }
24
+ }
25
+
16
26
  export async function initCommand() {
17
27
  console.log('');
18
28
  console.log(boxen(
@@ -22,7 +32,7 @@ export async function initCommand() {
22
32
  ));
23
33
  console.log('');
24
34
 
25
- const gitUser = getGitUsername();
35
+ const detectedUser = getGitHubUsername();
26
36
 
27
37
  const { direction, provider } = await inquirer.prompt([
28
38
  {
@@ -57,27 +67,46 @@ export async function initCommand() {
57
67
  }]);
58
68
  config.localPath = localPath;
59
69
  } else {
60
- const { username, repo } = await inquirer.prompt([
61
- {
62
- type: 'input',
63
- name: 'username',
64
- message: 'GitHub username:',
65
- validate: (input) => input.trim() ? true : 'Required'
66
- },
67
- {
70
+ // Pre-fill username if detected, just ask for repo name
71
+ const prompts = [];
72
+
73
+ if (detectedUser) {
74
+ console.log(chalk.gray(` GitHub user: ${chalk.cyan(detectedUser)}`));
75
+ prompts.push({
68
76
  type: 'input',
69
77
  name: 'repo',
70
- message: 'Repo name:',
78
+ message: `Repo name (${detectedUser}/???):`,
71
79
  default: 'ai-memory',
72
80
  validate: (input) => input.trim() ? true : 'Required'
73
- }
74
- ]);
81
+ });
82
+ } else {
83
+ prompts.push(
84
+ {
85
+ type: 'input',
86
+ name: 'username',
87
+ message: 'GitHub username:',
88
+ validate: (input) => input.trim() ? true : 'Required'
89
+ },
90
+ {
91
+ type: 'input',
92
+ name: 'repo',
93
+ message: 'Repo name:',
94
+ default: 'ai-memory',
95
+ validate: (input) => input.trim() ? true : 'Required'
96
+ }
97
+ );
98
+ }
99
+
100
+ const answers = await inquirer.prompt(prompts);
101
+ const username = (answers.username || detectedUser).trim();
102
+ const repo = answers.repo.trim();
75
103
 
76
- config.gitRepo = `https://github.com/${username.trim()}/${repo.trim()}.git`;
104
+ config.gitRepo = `https://github.com/${username}/${repo}.git`;
105
+ console.log(chalk.gray(` → ${config.gitRepo}\n`));
77
106
  }
78
107
 
79
108
  await saveConfig(config);
80
- console.log(chalk.green('Saved!\n'));
109
+ console.log(chalk.green('Saved!\n'));
81
110
 
82
111
  if (direction === 'upload') {
83
112
  await pushCommand();
@@ -0,0 +1,241 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import ora from 'ora';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import boxen from 'boxen';
8
+ import gradient from 'gradient-string';
9
+ import { getProfile, getProfileKeys, getProfileChoices } from '../tools/index.js';
10
+ import { resolveApiKey, translateMemory } from '../migrate/translator.js';
11
+
12
+ const TOOL_ICONS = {
13
+ claude: '🟣', gemini: '🔵', codex: '🟢', cursor: '⚡',
14
+ copilot: '🐙', windsurf: '🏄', aider: '🔧'
15
+ };
16
+
17
+ function toolLabel(key) {
18
+ return `${TOOL_ICONS[key] || '●'} ${getProfile(key)?.name || key}`;
19
+ }
20
+
21
+ async function translateToTarget(targetKey, sourceFiles, sourceProfile, apiKey, dryRun) {
22
+ const targetProfile = getProfile(targetKey);
23
+ const spinner = ora();
24
+ const translated = [];
25
+ const failed = [];
26
+
27
+ for (const file of sourceFiles) {
28
+ const basename = path.basename(file.filePath);
29
+ spinner.start(chalk.cyan(` ${TOOL_ICONS[targetKey] || '●'} Translating ${basename} → ${targetProfile.name}...`));
30
+
31
+ try {
32
+ const result = await translateMemory(file.content, sourceProfile, targetProfile, apiKey);
33
+ translated.push({ source: file, result });
34
+ spinner.succeed(chalk.green(` ${TOOL_ICONS[targetKey] || '✔'} ${basename} → ${targetProfile.name}`));
35
+ } catch (err) {
36
+ failed.push({ source: file, error: err.message });
37
+ spinner.fail(chalk.red(` ✖ ${basename} → ${targetProfile.name}: ${err.message}`));
38
+ }
39
+ }
40
+
41
+ if (translated.length === 0) {
42
+ return { targetKey, translated: 0, failed: failed.length, written: false };
43
+ }
44
+
45
+ // Combine content
46
+ let finalContent;
47
+ if (translated.length === 1) {
48
+ finalContent = translated[0].result;
49
+ } else {
50
+ finalContent = translated.map((t, i) => {
51
+ const header = `# From ${path.basename(t.source.filePath)}`;
52
+ return i === 0 ? `${header}\n\n${t.result}` : `\n\n${header}\n\n${t.result}`;
53
+ }).join('');
54
+ }
55
+
56
+ if (dryRun) {
57
+ return { targetKey, translated: translated.length, failed: failed.length, written: false, content: finalContent };
58
+ }
59
+
60
+ // Write output
61
+ const targetPath = targetProfile.targetPath();
62
+ let writeMode = 'write';
63
+
64
+ if (await fs.pathExists(targetPath)) {
65
+ const { action } = await inquirer.prompt([{
66
+ type: 'list',
67
+ name: 'action',
68
+ message: `${TOOL_ICONS[targetKey]} ${path.basename(targetPath)} already exists.`,
69
+ choices: [
70
+ { name: 'Overwrite', value: 'overwrite' },
71
+ { name: 'Append', value: 'append' },
72
+ { name: 'Skip', value: 'skip' }
73
+ ]
74
+ }]);
75
+ writeMode = action;
76
+ }
77
+
78
+ if (writeMode === 'skip') {
79
+ return { targetKey, translated: translated.length, failed: failed.length, written: false };
80
+ }
81
+
82
+ await fs.ensureDir(path.dirname(targetPath));
83
+
84
+ if (writeMode === 'append') {
85
+ const existing = await fs.readFile(targetPath, 'utf-8');
86
+ const separator = `\n\n---\n<!-- Translated from ${sourceProfile.name} by memoir on ${new Date().toISOString().split('T')[0]} -->\n\n`;
87
+ await fs.writeFile(targetPath, existing + separator + finalContent);
88
+ } else {
89
+ await fs.writeFile(targetPath, finalContent);
90
+ }
91
+
92
+ return { targetKey, translated: translated.length, failed: failed.length, written: true, path: targetPath };
93
+ }
94
+
95
+ export async function migrateCommand(options = {}) {
96
+ let { from, to, dryRun } = options;
97
+
98
+ // 1. Pick source tool
99
+ if (!from) {
100
+ const answer = await inquirer.prompt([{
101
+ type: 'list',
102
+ name: 'from',
103
+ message: 'Translate from:',
104
+ choices: getProfileChoices().map(c => ({ ...c, name: `${TOOL_ICONS[c.value] || '●'} ${c.name}` }))
105
+ }]);
106
+ from = answer.from;
107
+ }
108
+
109
+ // 2. Pick target tool(s)
110
+ let targets = [];
111
+ if (to === 'all') {
112
+ targets = getProfileKeys().filter(k => k !== from);
113
+ } else if (to) {
114
+ targets = [to];
115
+ } else {
116
+ const choices = [
117
+ { name: '🌐 All tools', value: '_all' },
118
+ ...getProfileChoices()
119
+ .filter(c => c.value !== from)
120
+ .map(c => ({ ...c, name: `${TOOL_ICONS[c.value] || '●'} ${c.name}` }))
121
+ ];
122
+ const answer = await inquirer.prompt([{
123
+ type: 'list',
124
+ name: 'to',
125
+ message: 'Translate to:',
126
+ choices
127
+ }]);
128
+ targets = answer.to === '_all'
129
+ ? getProfileKeys().filter(k => k !== from)
130
+ : [answer.to];
131
+ }
132
+
133
+ // Validate
134
+ const sourceProfile = getProfile(from);
135
+ if (!sourceProfile) {
136
+ console.log(chalk.red(`\nUnknown source tool: ${from}`));
137
+ console.log(chalk.gray(`Available: ${getProfileKeys().join(', ')}`));
138
+ return;
139
+ }
140
+ for (const t of targets) {
141
+ if (!getProfile(t)) {
142
+ console.log(chalk.red(`\nUnknown target tool: ${t}`));
143
+ console.log(chalk.gray(`Available: ${getProfileKeys().join(', ')}`));
144
+ return;
145
+ }
146
+ }
147
+ if (targets.includes(from)) {
148
+ console.log(chalk.yellow('\nSource and target are the same tool.'));
149
+ return;
150
+ }
151
+
152
+ // 3. Resolve API key
153
+ const apiKey = await resolveApiKey(inquirer);
154
+
155
+ // 4. Discover source files
156
+ const sourceFiles = sourceProfile.discover();
157
+
158
+ if (sourceFiles.length === 0) {
159
+ console.log(chalk.yellow(`\nNo ${sourceProfile.name} memory files found.`));
160
+ console.log(chalk.gray('Make sure you\'re in the right directory or that the tool has been configured.'));
161
+ return;
162
+ }
163
+
164
+ // 5. Show source files
165
+ console.log('');
166
+ console.log(boxen(
167
+ `${TOOL_ICONS[from]} ${chalk.bold(sourceProfile.name)} ${chalk.gray('→')} ${targets.map(t => TOOL_ICONS[t]).join(' ')}\n\n` +
168
+ sourceFiles.map(f => {
169
+ const display = f.filePath.replace(os.homedir(), '~');
170
+ const size = chalk.gray(`(${(f.content.length / 1024).toFixed(1)}kb)`);
171
+ return ` ${chalk.cyan('◆')} ${display} ${size}`;
172
+ }).join('\n'),
173
+ { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
174
+ ));
175
+
176
+ const { proceed } = await inquirer.prompt([{
177
+ type: 'confirm',
178
+ name: 'proceed',
179
+ message: `Translate ${sourceFiles.length} file${sourceFiles.length > 1 ? 's' : ''} to ${targets.length} tool${targets.length > 1 ? 's' : ''}?`,
180
+ default: true
181
+ }]);
182
+
183
+ if (!proceed) {
184
+ console.log(chalk.gray('\nCancelled.'));
185
+ return;
186
+ }
187
+
188
+ console.log('');
189
+
190
+ // 6. Translate to each target
191
+ const results = [];
192
+ for (const targetKey of targets) {
193
+ const result = await translateToTarget(targetKey, sourceFiles, sourceProfile, apiKey, dryRun);
194
+ results.push(result);
195
+ }
196
+
197
+ // 7. Summary
198
+ const succeeded = results.filter(r => r.translated > 0);
199
+ const written = results.filter(r => r.written);
200
+ const totalFailed = results.reduce((sum, r) => sum + r.failed, 0);
201
+
202
+ console.log('');
203
+
204
+ if (dryRun) {
205
+ // Show preview for dry run
206
+ for (const r of succeeded) {
207
+ if (r.content) {
208
+ const preview = r.content.split('\n').slice(0, 10).join('\n');
209
+ console.log(chalk.cyan(`--- ${getProfile(r.targetKey).name} preview ---`));
210
+ console.log(chalk.gray(preview));
211
+ const totalLines = r.content.split('\n').length;
212
+ if (totalLines > 10) console.log(chalk.gray(` ... (${totalLines - 10} more lines)`));
213
+ console.log('');
214
+ }
215
+ }
216
+ console.log(chalk.yellow('Dry run — no files written.\n'));
217
+ return;
218
+ }
219
+
220
+ // Build summary box
221
+ const summaryLines = results.map(r => {
222
+ const profile = getProfile(r.targetKey);
223
+ const icon = TOOL_ICONS[r.targetKey] || '●';
224
+ if (r.written) {
225
+ const display = r.path.replace(os.homedir(), '~');
226
+ return ` ${icon} ${chalk.green('✔')} ${profile.name} ${chalk.gray('→ ' + display)}`;
227
+ } else if (r.translated > 0) {
228
+ return ` ${icon} ${chalk.gray('⏭')} ${profile.name} ${chalk.gray('(skipped)')}`;
229
+ } else {
230
+ return ` ${icon} ${chalk.red('✖')} ${profile.name} ${chalk.gray('(failed)')}`;
231
+ }
232
+ }).join('\n');
233
+
234
+ console.log(boxen(
235
+ gradient.pastel(' Translated! ') + '\n\n' +
236
+ summaryLines + '\n\n' +
237
+ chalk.gray(`${written.length} written, ${succeeded.length - written.length} skipped${totalFailed > 0 ? `, ${totalFailed} failed` : ''}`),
238
+ { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'green', dimBorder: true }
239
+ ));
240
+ console.log('');
241
+ }
package/src/config.js CHANGED
@@ -9,7 +9,11 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
9
 
10
10
  export async function getConfig() {
11
11
  if (await fs.pathExists(CONFIG_FILE)) {
12
- return fs.readJson(CONFIG_FILE);
12
+ try {
13
+ return await fs.readJson(CONFIG_FILE);
14
+ } catch {
15
+ return null;
16
+ }
13
17
  }
14
18
  return null;
15
19
  }
@@ -17,4 +21,19 @@ export async function getConfig() {
17
21
  export async function saveConfig(config) {
18
22
  await fs.ensureDir(CONFIG_DIR);
19
23
  await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
24
+ // Restrict permissions — config may contain API keys
25
+ if (process.platform !== 'win32') {
26
+ await fs.chmod(CONFIG_FILE, 0o600);
27
+ }
28
+ }
29
+
30
+ export async function getGeminiApiKey() {
31
+ const config = await getConfig();
32
+ return config?.geminiApiKey || process.env.GEMINI_API_KEY || null;
33
+ }
34
+
35
+ export async function saveGeminiApiKey(apiKey) {
36
+ const config = await getConfig() || {};
37
+ config.geminiApiKey = apiKey;
38
+ await saveConfig(config);
20
39
  }
@@ -0,0 +1,2 @@
1
+ // Re-export from new plugin registry for backwards compatibility
2
+ export { getProfile, getProfileKeys, getProfileChoices, profiles } from '../tools/index.js';
@@ -0,0 +1,102 @@
1
+ import { getConfig, saveConfig } from '../config.js';
2
+
3
+ const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
4
+ const RETRYABLE_CODES = [429, 500, 502, 503];
5
+
6
+ export async function resolveApiKey(inquirer) {
7
+ // 1. Check env var
8
+ if (process.env.GEMINI_API_KEY) {
9
+ return process.env.GEMINI_API_KEY;
10
+ }
11
+
12
+ // 2. Check memoir config
13
+ const config = await getConfig() || {};
14
+ if (config.geminiApiKey) {
15
+ return config.geminiApiKey;
16
+ }
17
+
18
+ // 3. Prompt user
19
+ const { apiKey } = await inquirer.prompt([{
20
+ type: 'password',
21
+ name: 'apiKey',
22
+ message: 'Gemini API key (free at aistudio.google.com):',
23
+ mask: '*',
24
+ validate: (v) => v.length > 10 || 'Please enter a valid API key'
25
+ }]);
26
+
27
+ // Save for next time
28
+ config.geminiApiKey = apiKey;
29
+ await saveConfig(config);
30
+
31
+ return apiKey;
32
+ }
33
+
34
+ async function callGeminiApi(prompt, apiKey) {
35
+ const response = await fetch(GEMINI_API_URL, {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'x-goog-api-key': apiKey
40
+ },
41
+ body: JSON.stringify({
42
+ contents: [{ parts: [{ text: prompt }] }],
43
+ generationConfig: {
44
+ temperature: 0.3,
45
+ maxOutputTokens: 8192
46
+ }
47
+ })
48
+ });
49
+
50
+ if (!response.ok) {
51
+ const err = await response.text();
52
+ if (response.status === 400 || response.status === 403) {
53
+ throw new Error('Invalid Gemini API key. Get a free key at https://aistudio.google.com');
54
+ }
55
+ if (RETRYABLE_CODES.includes(response.status)) {
56
+ const error = new Error(`Gemini API error (${response.status}): ${err}`);
57
+ error.retryable = true;
58
+ throw error;
59
+ }
60
+ throw new Error(`Gemini API error (${response.status}): ${err}`);
61
+ }
62
+
63
+ const data = await response.json();
64
+ const text = data?.candidates?.[0]?.content?.parts?.[0]?.text;
65
+
66
+ if (!text) {
67
+ throw new Error('Empty response from Gemini API');
68
+ }
69
+
70
+ return text.trim();
71
+ }
72
+
73
+ export async function translateMemory(content, sourceProfile, targetProfile, apiKey) {
74
+ const prompt = `You are an expert at translating AI coding assistant memory/instruction files between different tools.
75
+
76
+ SOURCE TOOL: ${sourceProfile.name}
77
+ SOURCE FORMAT: ${sourceProfile.format}
78
+
79
+ TARGET TOOL: ${targetProfile.name}
80
+ TARGET FORMAT: ${targetProfile.format}
81
+
82
+ INSTRUCTIONS:
83
+ - Translate the content below so it works perfectly as a ${targetProfile.name} instruction file.
84
+ - Preserve ALL information, preferences, conventions, and context from the source.
85
+ - Adapt the structure and phrasing to match ${targetProfile.name}'s conventions.
86
+ - Remove any tool-specific references that don't apply to ${targetProfile.name}.
87
+ - Keep the tone direct and instructional.
88
+ - Output ONLY the translated content, no explanations or wrapping.
89
+
90
+ SOURCE CONTENT:
91
+ ${content}`;
92
+
93
+ try {
94
+ return await callGeminiApi(prompt, apiKey);
95
+ } catch (err) {
96
+ if (err.retryable) {
97
+ await new Promise(r => setTimeout(r, 3000));
98
+ return await callGeminiApi(prompt, apiKey);
99
+ }
100
+ throw err;
101
+ }
102
+ }
@@ -2,66 +2,70 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import chalk from 'chalk';
5
- import { execSync } from 'child_process';
5
+ import { execFileSync } from 'child_process';
6
+
7
+ function sanitizeUrl(url) {
8
+ // Reject URLs with shell metacharacters
9
+ if (/[`$;|&()<>!]/.test(url)) {
10
+ throw new Error('Repository URL contains invalid characters.');
11
+ }
12
+ return url;
13
+ }
6
14
 
7
15
  export async function syncToLocal(config, stagingDir, spinner) {
8
16
  const destDir = config.localPath;
9
17
  if (!destDir) throw new Error('Local path is not configured.');
10
-
11
- // Expand tilde if user used it
18
+
12
19
  const resolvedDest = destDir.replace(/^~/, os.homedir());
13
-
20
+
14
21
  spinner.text = `Syncing files to local directory: ${chalk.cyan(resolvedDest)}`;
15
22
  await fs.ensureDir(resolvedDest);
16
-
23
+
17
24
  await fs.copy(stagingDir, resolvedDest);
18
25
  spinner.succeed(chalk.green('Sync complete! ') + chalk.gray(`(Saved to ${resolvedDest})`));
19
26
  }
20
27
 
21
28
  export async function syncToGit(config, stagingDir, spinner) {
22
- const repoUrl = config.gitRepo;
29
+ const repoUrl = sanitizeUrl(config.gitRepo);
23
30
  if (!repoUrl) throw new Error('Git repository is not configured.');
24
-
31
+
25
32
  spinner.text = `Authenticating and syncing with Git remote: ${chalk.cyan(repoUrl)}`;
26
-
27
- // Clone existing repo to preserve history, then replace contents
33
+
28
34
  const gitDir = path.join(os.tmpdir(), `memoir-git-${Date.now()}`);
29
35
  await fs.ensureDir(gitDir);
30
36
 
31
37
  try {
32
38
  try {
33
- execSync(`git clone --depth 10 ${repoUrl} .`, { cwd: gitDir, stdio: 'ignore' });
34
- // Remove old files so deleted configs don't persist
39
+ execFileSync('git', ['clone', '--depth', '1', repoUrl, '.'], { cwd: gitDir, stdio: 'ignore' });
35
40
  const files = await fs.readdir(gitDir);
36
41
  for (const f of files) {
37
42
  if (f !== '.git') await fs.remove(path.join(gitDir, f));
38
43
  }
39
44
  } catch {
40
- // Repo is empty or doesn't exist yet — init fresh
41
- execSync('git init', { cwd: gitDir, stdio: 'ignore' });
42
- execSync('git branch -m main', { cwd: gitDir, stdio: 'ignore' });
45
+ execFileSync('git', ['init'], { cwd: gitDir, stdio: 'ignore' });
46
+ execFileSync('git', ['branch', '-m', 'main'], { cwd: gitDir, stdio: 'ignore' });
43
47
  }
44
48
 
45
- // Copy staged memories into the git dir
46
49
  await fs.copy(stagingDir, gitDir);
47
50
 
48
- execSync('git add -A', { cwd: gitDir, stdio: 'ignore' });
49
- execSync('git config user.name "memoir"', { cwd: gitDir, stdio: 'ignore' });
50
- execSync('git config user.email "bot@memoir.dev"', { cwd: gitDir, stdio: 'ignore' });
51
+ execFileSync('git', ['add', '-A'], { cwd: gitDir, stdio: 'ignore' });
52
+ execFileSync('git', ['config', 'user.name', 'memoir'], { cwd: gitDir, stdio: 'ignore' });
53
+ execFileSync('git', ['config', 'user.email', 'bot@memoir.dev'], { cwd: gitDir, stdio: 'ignore' });
51
54
 
52
55
  const timestamp = new Date().toISOString().split('T')[0];
53
56
  try {
54
- execSync(`git commit -m "memoir backup ${timestamp}"`, { cwd: gitDir, stdio: 'ignore' });
57
+ execFileSync('git', ['commit', '-m', `memoir backup ${timestamp}`], { cwd: gitDir, stdio: 'ignore' });
55
58
  } catch {
56
59
  spinner.succeed(chalk.green('Already up to date! ') + chalk.gray('No changes to push.'));
57
60
  return;
58
61
  }
59
62
 
60
63
  spinner.text = `Pushing data to ${chalk.cyan(repoUrl)}...`;
61
- execSync(`git push ${repoUrl} main`, { cwd: gitDir, stdio: 'ignore' });
64
+ execFileSync('git', ['push', repoUrl, 'main'], { cwd: gitDir, stdio: 'ignore' });
62
65
 
63
66
  spinner.succeed(chalk.green('Sync complete! ') + chalk.gray('(Uploaded securely to GitHub)'));
64
67
  } catch (err) {
68
+ if (err.message.includes('invalid characters')) throw err;
65
69
  throw new Error('Failed to push to git repository. Ensure your credentials are configured and the repository exists.');
66
70
  } finally {
67
71
  await fs.remove(gitDir);
@@ -2,34 +2,33 @@ import chalk from 'chalk';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import { execSync } from 'child_process';
5
+ import { execFileSync } from 'child_process';
6
6
  import { restoreMemories } from '../adapters/restore.js';
7
7
 
8
8
  export async function fetchFromLocal(config, stagingDir, spinner) {
9
9
  const sourceDir = config.localPath;
10
10
  if (!sourceDir) throw new Error('Local path is not configured.');
11
-
11
+
12
12
  const resolvedSource = sourceDir.replace(/^~/, os.homedir());
13
-
13
+
14
14
  if (!(await fs.pathExists(resolvedSource))) {
15
15
  throw new Error(`The backup directory does not exist: ${resolvedSource}`);
16
16
  }
17
17
 
18
18
  spinner.text = `Fetching data from local directory: ${chalk.cyan(resolvedSource)}`;
19
19
  await fs.copy(resolvedSource, stagingDir);
20
-
20
+
21
21
  return await restoreMemories(stagingDir, spinner);
22
22
  }
23
23
 
24
24
  export async function fetchFromGit(config, stagingDir, spinner) {
25
25
  const repoUrl = config.gitRepo;
26
26
  if (!repoUrl) throw new Error('Git repository is not configured.');
27
-
27
+
28
28
  spinner.text = `Cloning memory from Git remote: ${chalk.cyan(repoUrl)}`;
29
-
29
+
30
30
  try {
31
- // Clone depth 1 to make it fast
32
- execSync(`git clone --depth 1 ${repoUrl} .`, { cwd: stagingDir, stdio: 'ignore' });
31
+ execFileSync('git', ['clone', '--depth', '1', repoUrl, '.'], { cwd: stagingDir, stdio: 'ignore' });
33
32
  } catch (err) {
34
33
  throw new Error('Failed to pull from git repository. Ensure your SSH keys are configured and the repository is accessible.');
35
34
  }
@@ -0,0 +1,31 @@
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: 'aider',
10
+ name: 'Aider',
11
+ icon: '🔧',
12
+ format: 'Markdown system prompt in .aider.system-prompt.md. Contains instructions that get injected into Aider\'s system prompt. Supports coding style, conventions, and project context.',
13
+
14
+ discover() {
15
+ const files = [];
16
+ const projectFile = path.join(cwd, '.aider.system-prompt.md');
17
+ if (fs.existsSync(projectFile)) {
18
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
19
+ }
20
+ const homeFile = path.join(home, '.aider.system-prompt.md');
21
+ const alreadyFound = files.some(f => path.resolve(f.filePath) === path.resolve(homeFile));
22
+ if (fs.existsSync(homeFile) && !alreadyFound) {
23
+ files.push({ filePath: homeFile, content: fs.readFileSync(homeFile, 'utf-8'), scope: 'user' });
24
+ }
25
+ return files;
26
+ },
27
+
28
+ targetPath() {
29
+ return path.join(cwd, '.aider.system-prompt.md');
30
+ }
31
+ };
@@ -0,0 +1,51 @@
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: 'claude',
10
+ name: 'Claude',
11
+ icon: '🟣',
12
+ format: 'Markdown instructions in CLAUDE.md. Supports sections for project context, coding conventions, tool preferences, and workflow rules. Written as direct instructions to Claude.',
13
+
14
+ discover() {
15
+ const files = [];
16
+
17
+ const projectFile = path.join(cwd, 'CLAUDE.md');
18
+ if (fs.existsSync(projectFile)) {
19
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
20
+ }
21
+
22
+ const memoryBase = path.join(home, '.claude', 'projects');
23
+ if (fs.existsSync(memoryBase)) {
24
+ const cwdEncoded = cwd.replace(/\//g, '-');
25
+ const projectDirs = fs.readdirSync(memoryBase).filter(d => {
26
+ if (!fs.statSync(path.join(memoryBase, d)).isDirectory()) return false;
27
+ return d === cwdEncoded || cwdEncoded.startsWith(d) || d.startsWith(cwdEncoded);
28
+ });
29
+ for (const dir of projectDirs) {
30
+ const memoryDir = path.join(memoryBase, dir, 'memory');
31
+ if (fs.existsSync(memoryDir)) {
32
+ const mdFiles = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
33
+ for (const f of mdFiles) {
34
+ const filePath = path.join(memoryDir, f);
35
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
36
+ }
37
+ }
38
+ const claudeMd = path.join(memoryBase, dir, 'CLAUDE.md');
39
+ if (fs.existsSync(claudeMd)) {
40
+ files.push({ filePath: claudeMd, content: fs.readFileSync(claudeMd, 'utf-8'), scope: 'user' });
41
+ }
42
+ }
43
+ }
44
+
45
+ return files;
46
+ },
47
+
48
+ targetPath() {
49
+ return path.join(cwd, 'CLAUDE.md');
50
+ }
51
+ };
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const cwd = process.cwd();
5
+
6
+ export default {
7
+ key: 'codex',
8
+ name: 'Codex',
9
+ icon: '🟢',
10
+ format: 'Markdown instructions in AGENTS.md. Written as instructions for OpenAI Codex agent. Supports project context, coding conventions, and task guidance.',
11
+
12
+ discover() {
13
+ const files = [];
14
+ const projectFile = path.join(cwd, 'AGENTS.md');
15
+ if (fs.existsSync(projectFile)) {
16
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
17
+ }
18
+ return files;
19
+ },
20
+
21
+ targetPath() {
22
+ return path.join(cwd, 'AGENTS.md');
23
+ }
24
+ };
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const cwd = process.cwd();
5
+
6
+ export default {
7
+ key: 'copilot',
8
+ name: 'GitHub Copilot',
9
+ icon: '🐙',
10
+ format: 'Markdown instructions in .github/copilot-instructions.md. Written as instructions for GitHub Copilot. Supports coding style, project context, and language preferences.',
11
+
12
+ discover() {
13
+ const files = [];
14
+ const projectFile = path.join(cwd, '.github', 'copilot-instructions.md');
15
+ if (fs.existsSync(projectFile)) {
16
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
17
+ }
18
+ return files;
19
+ },
20
+
21
+ targetPath() {
22
+ return path.join(cwd, '.github', 'copilot-instructions.md');
23
+ }
24
+ };
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const cwd = process.cwd();
5
+
6
+ export default {
7
+ key: 'cursor',
8
+ name: 'Cursor',
9
+ icon: '⚡',
10
+ format: 'Plain text or markdown rules in .cursorrules. Contains coding conventions, style preferences, and project-specific instructions for Cursor AI.',
11
+
12
+ discover() {
13
+ const files = [];
14
+ const projectFile = path.join(cwd, '.cursorrules');
15
+ if (fs.existsSync(projectFile)) {
16
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
17
+ }
18
+ return files;
19
+ },
20
+
21
+ targetPath() {
22
+ return path.join(cwd, '.cursorrules');
23
+ }
24
+ };
@@ -0,0 +1,31 @@
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: 'gemini',
10
+ name: 'Gemini',
11
+ icon: '🔵',
12
+ format: 'Markdown instructions in GEMINI.md. Written as direct instructions to Gemini. Supports project context, coding style, preferences, and behavioral rules.',
13
+
14
+ discover() {
15
+ const files = [];
16
+ const projectFile = path.join(cwd, 'GEMINI.md');
17
+ if (fs.existsSync(projectFile)) {
18
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
19
+ }
20
+ const homeFile = path.join(home, 'GEMINI.md');
21
+ const alreadyFound = files.some(f => path.resolve(f.filePath) === path.resolve(homeFile));
22
+ if (fs.existsSync(homeFile) && !alreadyFound) {
23
+ files.push({ filePath: homeFile, content: fs.readFileSync(homeFile, 'utf-8'), scope: 'user' });
24
+ }
25
+ return files;
26
+ },
27
+
28
+ targetPath() {
29
+ return path.join(cwd, 'GEMINI.md');
30
+ }
31
+ };
@@ -0,0 +1,26 @@
1
+ import claude from './claude.js';
2
+ import gemini from './gemini.js';
3
+ import codex from './codex.js';
4
+ import cursor from './cursor.js';
5
+ import copilot from './copilot.js';
6
+ import windsurf from './windsurf.js';
7
+ import aider from './aider.js';
8
+
9
+ const registry = {};
10
+ for (const tool of [claude, gemini, codex, cursor, copilot, windsurf, aider]) {
11
+ registry[tool.key] = tool;
12
+ }
13
+
14
+ export function getProfile(key) {
15
+ return registry[key] || null;
16
+ }
17
+
18
+ export function getProfileKeys() {
19
+ return Object.keys(registry);
20
+ }
21
+
22
+ export function getProfileChoices() {
23
+ return Object.values(registry).map(t => ({ name: t.name, value: t.key }));
24
+ }
25
+
26
+ export { registry as profiles };
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const cwd = process.cwd();
5
+
6
+ export default {
7
+ key: 'windsurf',
8
+ name: 'Windsurf',
9
+ icon: '🏄',
10
+ format: 'Plain text or markdown rules in .windsurfrules. Contains coding conventions, style preferences, and project-specific instructions for Windsurf AI.',
11
+
12
+ discover() {
13
+ const files = [];
14
+ const projectFile = path.join(cwd, '.windsurfrules');
15
+ if (fs.existsSync(projectFile)) {
16
+ files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
17
+ }
18
+ return files;
19
+ },
20
+
21
+ targetPath() {
22
+ return path.join(cwd, '.windsurfrules');
23
+ }
24
+ };