memoir-cli 2.2.0 → 2.4.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # 🧠 memoir
3
+ # memoir
4
4
  **Your AI Remembers Everything. Sync It Everywhere.**
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/memoir-cli.svg?style=flat-square)](https://npmjs.org/package/memoir-cli)
@@ -14,186 +14,164 @@
14
14
 
15
15
  ---
16
16
 
17
- ## 💡 The Problem
17
+ ## The Problem
18
18
 
19
- You spend weeks teaching your local AI CLI exactly how you like your code formatted, your preferred architectural patterns, and your project's unique context.
19
+ You spend weeks teaching your AI tools how you code your preferred patterns, project context, coding standards.
20
20
 
21
- Then, you switch laptops. Or you want to share that setup with your team.
21
+ Then you switch laptops. Or try a new AI tool. Or want your team on the same page.
22
22
 
23
- Suddenly, you're starting from scratch. Your AI's "memory" is trapped in hidden `.gemini` or `.claude` folders on a single machine.
23
+ All that context is trapped in hidden dotfiles on one machine.
24
24
 
25
- ## 🚀 The Solution
25
+ ## The Solution
26
26
 
27
- `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.
28
-
29
- No locked-in SaaS, no lost context, no complex shell scripts. Switch from Claude to Gemini in one command.
30
-
31
- ### Supported Integrations
32
- - [x] **Gemini CLI**
33
- - [x] **Claude Code**
34
- - [x] **OpenAI Codex CLI**
35
- - [x] **Cursor**
36
- - [x] **GitHub Copilot**
37
- - [x] **Windsurf**
38
- - [x] **Aider**
39
-
40
- ---
41
-
42
- ## 🛠️ Installation
43
-
44
- Install globally via npm so you can use it anywhere on your machine:
27
+ `memoir` extracts, backs up, restores, and **translates** your AI memory across any computer and any tool. One command to save. One command to restore. One command to translate between tools.
45
28
 
46
29
  ```bash
47
30
  npm install -g memoir-cli
48
31
  ```
49
32
 
50
- ## Quick Start
33
+ ### Supported Tools (11)
34
+ | Tool | Config synced |
35
+ |------|--------------|
36
+ | **ChatGPT** | `CHATGPT.md` — custom instructions, preferences |
37
+ | **Claude Code** | `~/.claude/` — settings, projects, memory files |
38
+ | **Gemini CLI** | `~/.gemini/` — settings, GEMINI.md |
39
+ | **OpenAI Codex** | `~/.codex/` — config, instructions |
40
+ | **Cursor** | Settings, keybindings, rules |
41
+ | **GitHub Copilot** | Config, settings |
42
+ | **Windsurf** | Settings, keybindings, rules |
43
+ | **Zed** | Settings, keymap, tasks |
44
+ | **Cline** | Settings, rules |
45
+ | **Continue.dev** | Config, rules |
46
+ | **Aider** | `.aider.conf.yml`, system prompt |
47
+
48
+ Plus **per-project configs**: automatically finds `CLAUDE.md`, `GEMINI.md`, `CHATGPT.md`, `.cursorrules`, `AGENTS.md` across all your projects.
51
49
 
52
- ### 1. Initialize
53
- Run the setup wizard. We'll help you securely link a private GitHub repository or a local sync folder.
50
+ ---
54
51
 
52
+ ## Quick Start
53
+
54
+ ### 1. Initialize
55
55
  ```bash
56
56
  memoir init
57
+ # Walks you through setup — GitHub repo or local folder
58
+ # Auto-creates a private repo if you have gh CLI
57
59
  ```
58
60
 
59
- ### 2. Backup Your Memory
60
- Just had a great session? Save your AI's learned context to the cloud:
61
-
61
+ ### 2. Back up your memory
62
62
  ```bash
63
63
  memoir push
64
- # or simply use the alias:
65
- memoir remember
66
64
  ```
67
65
 
68
- ### 3. Restore Anywhere
69
- Got a new machine? Pull your brain down instantly:
70
-
66
+ ### 3. Restore on a new machine
71
67
  ```bash
72
68
  memoir restore
73
- # or:
74
- memoir pull
75
69
  ```
76
70
 
77
- ### 4. Translate Between Tools
78
- Switch AI tools without losing context. Memoir uses Gemini AI to intelligently rewrite your memory files for any supported tool:
79
-
71
+ ### 4. Translate between tools
80
72
  ```bash
81
73
  memoir migrate --from claude --to gemini
82
- # or run interactively:
83
- memoir migrate
74
+ # AI-powered translation — not copy-paste, real rewriting
84
75
  ```
85
76
 
86
- Your Claude instructions become a proper `GEMINI.md` — not a copy-paste, but a real translation that follows each tool's conventions.
87
-
88
77
  ---
89
78
 
90
- ## 📖 All Commands
79
+ ## All Commands
91
80
 
92
81
  | Command | What it does |
93
82
  |---------|-------------|
94
- | `memoir init` | Setup wizard — pick GitHub or local folder, upload or download |
95
- | `memoir push` | Extract all AI tool configs, back up to GitHub/local |
96
- | `memoir restore` | Pull backup down, restore missing files (non-destructive) |
97
- | `memoir status` | Show which AI tools are detected on this machine |
98
- | `memoir view` | Preview backup contents with diffs against local |
99
- | `memoir migrate` | Translate memory between tools via Gemini AI |
83
+ | `memoir init` | Setup wizard — GitHub or local, upload or download |
84
+ | `memoir push` | Back up all AI configs |
85
+ | `memoir restore` | Restore on a new machine (non-destructive) |
86
+ | `memoir status` | Show detected AI tools |
87
+ | `memoir doctor` | Diagnose issues, scan for secrets |
88
+ | `memoir view` | Preview what's in your backup |
89
+ | `memoir diff` | Show changes since last backup |
90
+ | `memoir migrate` | Translate memory between tools via AI |
91
+ | `memoir snapshot` | Capture current coding session |
92
+ | `memoir resume` | Pick up where you left off |
93
+ | `memoir profile` | Manage profiles (personal/work) |
94
+ | `memoir update` | Self-update to latest version |
100
95
 
101
96
  ---
102
97
 
103
- ## 🎯 Common Workflows
98
+ ## Profiles
99
+
100
+ Separate personal and work configs — different repos, different tools.
104
101
 
105
- ### New laptop setup
106
102
  ```bash
107
- # Old machine save everything
108
- memoir init # Upload → GitHub
103
+ memoir profile create work # set up work profile with its own repo
104
+ memoir profile create personal # personal side projects
109
105
 
110
- # New machine restore everything
111
- memoir init # Download → GitHub
112
- # All your .claude/, .gemini/, .cursorrules configs restored in 30 seconds
113
- ```
106
+ memoir push --profile work # sync only work configs
107
+ memoir restore --profile personal
114
108
 
115
- ### Switch from Claude to Gemini (or any tool)
116
- ```bash
117
- memoir migrate --from claude --to gemini
109
+ memoir profile list # see all profiles
110
+ memoir profile switch work # change default
118
111
  ```
119
- 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.
120
112
 
121
- ### Keep your whole team in sync
122
- ```bash
123
- # Team lead writes CLAUDE.md, then generates for everyone else:
124
- memoir migrate --from claude --to cursor
125
- memoir migrate --from claude --to copilot
126
- memoir migrate --from claude --to codex
127
- ```
113
+ Each profile can filter which tools to sync, so your personal side project memory never mixes with work.
128
114
 
129
- ### Fan out to every tool at once
130
- ```bash
131
- memoir migrate --from claude --to gemini
132
- memoir migrate --from claude --to codex
133
- memoir migrate --from claude --to cursor
134
- memoir migrate --from claude --to windsurf
135
- memoir migrate --from claude --to aider
136
- ```
137
- Use one tool as the source of truth, propagate to all others.
115
+ ---
116
+
117
+ ## Common Workflows
138
118
 
139
- ### Preview before committing
119
+ ### New laptop setup
140
120
  ```bash
141
- memoir migrate --from gemini --to claude --dry-run
142
- # Shows translated output but writes nothing
121
+ # Old machine
122
+ memoir push
123
+
124
+ # New machine
125
+ memoir init # → Download → same GitHub repo
126
+ memoir restore # All configs restored in seconds
143
127
  ```
144
128
 
145
- ### Protect existing files
129
+ ### Translate between tools
146
130
  ```bash
147
- memoir migrate --from claude --to gemini
148
- # "GEMINI.md already exists."
149
- # → Overwrite / Append / Skip
150
- # Append adds a dated separator so you keep your existing instructions
131
+ memoir migrate --from chatgpt --to claude
132
+ # Your ChatGPT custom instructions become a proper CLAUDE.md
151
133
  ```
152
134
 
153
- ### Daily sync across machines
135
+ ### Fan out to every tool
154
136
  ```bash
155
- # End of day
156
- memoir push
157
-
158
- # Next morning, different machine
159
- memoir pull
137
+ memoir migrate --from chatgpt --to all
138
+ # One source of truth, every tool gets its own format
160
139
  ```
161
140
 
162
- ### Check what's on this machine
141
+ ### Daily sync
163
142
  ```bash
164
- memoir status
165
- # Shows checkmarks for every detected AI tool and their config locations
143
+ memoir push # end of day
144
+ memoir restore # next morning, different machine
166
145
  ```
167
146
 
168
147
  ---
169
148
 
170
- ## 🔒 Security First
149
+ ## Security
171
150
 
172
- Your AI memory files often sit right next to sensitive API keys and OAuth tokens. **`memoir` is designed to be paranoid.**
151
+ Memoir **only** syncs config files, instructions, and memory markdown. It never touches credentials, API keys, `.env` files, or auth tokens.
173
152
 
174
- Our specialized adapters intelligently filter your directories. We **only** sync configuration files, custom prompts, and markdown memory (`GEMINI.md`, `CLAUDE.md`). We will never touch, copy, or push `.env` files, `.key` files, or credential caches.
153
+ Run `memoir doctor` to see exactly what would be synced and scan for accidental secrets before pushing.
175
154
 
176
155
  ---
177
156
 
178
- ## 🗺️ Roadmap
157
+ ## Roadmap
179
158
 
180
- **What's next:**
181
- - Team sharingsync a shared memory repo across your whole team
182
- - Auto-detect new AI tools as they appear
183
- - Two-way merge combine memories from multiple tools into one
159
+ - **Universal format** — write one `MEMOIR.md`, generate all tool-specific configs
160
+ - **Cloud sync**no GitHub needed, encrypted backups
161
+ - **Teams** shared coding standards across your whole team
162
+ - **Templates**community-shared AI tool configs
184
163
 
185
164
  ---
186
165
 
187
- ## 🤝 Contributing
166
+ ## Contributing
188
167
 
189
- We welcome contributions! Whether it's adding an adapter for a new AI CLI, improving the UI, or helping build the Migration Engine.
168
+ Contributions welcome especially new tool adapters and migration improvements.
190
169
 
191
- 1. Fork the Project
192
- 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
193
- 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
194
- 4. Push to the Branch (`git push origin feature/AmazingFeature`)
195
- 5. Open a Pull Request
170
+ 1. Fork the repo
171
+ 2. Create your branch (`git checkout -b feature/my-feature`)
172
+ 3. Commit and push
173
+ 4. Open a PR
196
174
 
197
- ## 📄 License
175
+ ## License
198
176
 
199
- Distributed under the MIT License. See `LICENSE` for more information.
177
+ MIT
package/bin/memoir.js CHANGED
@@ -13,6 +13,7 @@ import { diffCommand } from '../src/commands/diff.js';
13
13
  import { migrateCommand } from '../src/commands/migrate.js';
14
14
  import { snapshotCommand } from '../src/commands/snapshot.js';
15
15
  import { resumeCommand } from '../src/commands/resume.js';
16
+ import { profileListCommand, profileCreateCommand, profileSwitchCommand, profileDeleteCommand } from '../src/commands/profile.js';
16
17
  import { createRequire } from 'module';
17
18
 
18
19
  const require = createRequire(import.meta.url);
@@ -57,8 +58,9 @@ if (process.argv.length <= 2) {
57
58
  chalk.cyan(' memoir snapshot ') + chalk.gray('— capture your current session') + '\n' +
58
59
  chalk.cyan(' memoir resume ') + chalk.gray('— pick up where you left off') + '\n' +
59
60
  chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
61
+ chalk.cyan(' memoir profile ') + chalk.gray('— manage profiles (personal/work)') + '\n' +
60
62
  chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n\n' +
61
- chalk.gray(' Tip: use --only claude,gemini to sync specific tools') + '\n\n' +
63
+ chalk.gray(' Tip: use --profile work to sync a specific profile') + '\n\n' +
62
64
  chalk.gray(`v${VERSION}`),
63
65
  { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
64
66
  ) + '\n');
@@ -93,7 +95,8 @@ program
93
95
  .command('push')
94
96
  .alias('remember')
95
97
  .description('Back up your AI memory to the cloud')
96
- .option('--only <tools>', 'Only sync specific tools (comma-separated: claude,gemini,codex,cursor,copilot,windsurf,aider)')
98
+ .option('--only <tools>', 'Only sync specific tools (comma-separated)')
99
+ .option('-p, --profile <name>', 'Use a specific profile')
97
100
  .action(async (options) => {
98
101
  try {
99
102
  await pushCommand(options);
@@ -107,8 +110,9 @@ program
107
110
  .command('restore')
108
111
  .alias('pull')
109
112
  .description('Restore your AI memory on this machine')
110
- .option('--only <tools>', 'Only restore specific tools (comma-separated: claude,gemini,codex,cursor,copilot,windsurf,aider)')
113
+ .option('--only <tools>', 'Only restore specific tools (comma-separated)')
111
114
  .option('-y, --yes', 'Skip confirmation prompts (restore all)')
115
+ .option('-p, --profile <name>', 'Use a specific profile')
112
116
  .action(async (options) => {
113
117
  try {
114
118
  await restoreCommand(options);
@@ -121,9 +125,10 @@ program
121
125
  program
122
126
  .command('status')
123
127
  .description('See what AI tools are on this machine')
124
- .action(async () => {
128
+ .option('-p, --profile <name>', 'Use a specific profile')
129
+ .action(async (options) => {
125
130
  try {
126
- await statusCommand();
131
+ await statusCommand(options);
127
132
  } catch (err) {
128
133
  console.error(chalk.red('\n✖ Error:'), err.message);
129
134
  process.exit(1);
@@ -134,9 +139,10 @@ program
134
139
  .command('doctor')
135
140
  .alias('diagnose')
136
141
  .description('Diagnose common issues with your memoir setup')
137
- .action(async () => {
142
+ .option('-p, --profile <name>', 'Use a specific profile')
143
+ .action(async (options) => {
138
144
  try {
139
- await doctorCommand();
145
+ await doctorCommand(options);
140
146
  } catch (err) {
141
147
  console.error(chalk.red('\n✖ Error:'), err.message);
142
148
  process.exit(1);
@@ -147,9 +153,10 @@ program
147
153
  .command('view')
148
154
  .alias('ls')
149
155
  .description('Preview what files are in your backup')
150
- .action(async () => {
156
+ .option('-p, --profile <name>', 'Use a specific profile')
157
+ .action(async (options) => {
151
158
  try {
152
- await viewCommand();
159
+ await viewCommand(options);
153
160
  } catch (err) {
154
161
  console.error(chalk.red('\n✖ Error:'), err.message);
155
162
  process.exit(1);
@@ -160,9 +167,10 @@ program
160
167
  .command('diff')
161
168
  .alias('changes')
162
169
  .description('Show what changed since your last backup')
163
- .action(async () => {
170
+ .option('-p, --profile <name>', 'Use a specific profile')
171
+ .action(async (options) => {
164
172
  try {
165
- await diffCommand();
173
+ await diffCommand(options);
166
174
  } catch (err) {
167
175
  console.error(chalk.red('\n✖ Error:'), err.message);
168
176
  process.exit(1);
@@ -175,6 +183,7 @@ program
175
183
  .description('Capture your current coding session for handoff')
176
184
  .option('--smart', 'Use AI to generate a better summary (requires Gemini API key)')
177
185
  .option('--goal <goal>', 'What you want to do next (goal-directed handoff)')
186
+ .option('-p, --profile <name>', 'Use a specific profile')
178
187
  .action(async (options) => {
179
188
  try {
180
189
  await snapshotCommand(options);
@@ -189,6 +198,7 @@ program
189
198
  .description('Pick up where you left off on another machine')
190
199
  .option('--inject', 'Write the handoff where your AI tool will read it')
191
200
  .option('--to <tool>', 'Target tool for injection (claude, gemini, cursor, codex)')
201
+ .option('-p, --profile <name>', 'Use a specific profile')
192
202
  .action(async (options) => {
193
203
  try {
194
204
  await resumeCommand(options);
@@ -220,7 +230,6 @@ program
220
230
  console.log('\n' + chalk.cyan(`Updating memoir ${VERSION} → ${chalk.green.bold(latest)}...`) + '\n');
221
231
 
222
232
  const { execSync } = await import('child_process');
223
- // Detect package manager — prefer the one that installed memoir
224
233
  const execPath = process.argv[1] || '';
225
234
  const useBun = execPath.includes('.bun') || process.env.BUN_INSTALL;
226
235
  const cmd = useBun ? 'bun install -g memoir-cli' : 'npm install -g memoir-cli';
@@ -254,6 +263,60 @@ program
254
263
  }
255
264
  });
256
265
 
266
+ // Profile management
267
+ const profile = program.command('profile').description('Manage profiles (personal, work, etc.)');
268
+
269
+ profile
270
+ .command('list')
271
+ .alias('ls')
272
+ .description('List all profiles')
273
+ .action(async () => {
274
+ try {
275
+ await profileListCommand();
276
+ } catch (err) {
277
+ console.error(chalk.red('\n✖ Error:'), err.message);
278
+ process.exit(1);
279
+ }
280
+ });
281
+
282
+ profile
283
+ .command('create <name>')
284
+ .description('Create a new profile')
285
+ .action(async (name) => {
286
+ try {
287
+ await profileCreateCommand(name);
288
+ } catch (err) {
289
+ console.error(chalk.red('\n✖ Error:'), err.message);
290
+ process.exit(1);
291
+ }
292
+ });
293
+
294
+ profile
295
+ .command('switch <name>')
296
+ .alias('use')
297
+ .description('Switch to a profile')
298
+ .action(async (name) => {
299
+ try {
300
+ await profileSwitchCommand(name);
301
+ } catch (err) {
302
+ console.error(chalk.red('\n✖ Error:'), err.message);
303
+ process.exit(1);
304
+ }
305
+ });
306
+
307
+ profile
308
+ .command('delete <name>')
309
+ .alias('rm')
310
+ .description('Delete a profile')
311
+ .action(async (name) => {
312
+ try {
313
+ await profileDeleteCommand(name);
314
+ } catch (err) {
315
+ console.error(chalk.red('\n✖ Error:'), err.message);
316
+ process.exit(1);
317
+ }
318
+ });
319
+
257
320
  program.hook('postAction', async () => {
258
321
  await checkForUpdate();
259
322
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "2.2.0",
3
+ "version": "2.4.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",
@@ -35,6 +35,10 @@
35
35
  "copilot",
36
36
  "windsurf",
37
37
  "aider",
38
+ "zed",
39
+ "cline",
40
+ "continue-dev",
41
+ "profiles",
38
42
  "ai-memory",
39
43
  "ai-tools",
40
44
  "dotfiles",
@@ -42,6 +46,7 @@
42
46
  "claude-code",
43
47
  "gemini-cli",
44
48
  "openai",
49
+ "chatgpt",
45
50
  "ai-assistant",
46
51
  "coding-assistant",
47
52
  "context-sync",
@@ -58,7 +63,6 @@
58
63
  "fs-extra": "^11.2.0",
59
64
  "gradient-string": "^3.0.0",
60
65
  "inquirer": "^9.2.15",
61
- "open": "^11.0.0",
62
- "ora": "^7.0.1"
66
+ "ora": "^7.0.1"
63
67
  }
64
68
  }
@@ -298,7 +298,7 @@ export async function extractMemories(stagingDir, spinner, onlyFilter = null) {
298
298
  spinner.text = `📁 Scanning for project-level AI configs...`;
299
299
 
300
300
  const projectFiles = [
301
- 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md', '.cursorrules',
301
+ 'CLAUDE.md', 'GEMINI.md', 'CHATGPT.md', 'AGENTS.md', '.cursorrules',
302
302
  '.github/copilot-instructions.md', '.windsurfrules',
303
303
  '.aider.conf.yml', '.clinerules'
304
304
  ];
@@ -50,8 +50,8 @@ function simpleDiff(oldText, newText) {
50
50
  return output;
51
51
  }
52
52
 
53
- export async function diffCommand() {
54
- const config = await getConfig();
53
+ export async function diffCommand(options = {}) {
54
+ const config = await getConfig(options.profile);
55
55
  if (!config) {
56
56
  console.log(chalk.red('\n✖ Not configured yet. Run: memoir init\n'));
57
57
  return;
@@ -74,7 +74,7 @@ async function scanForSecrets(files) {
74
74
  return warnings;
75
75
  }
76
76
 
77
- export async function doctorCommand() {
77
+ export async function doctorCommand(options = {}) {
78
78
  const spinner = ora({ text: 'Running diagnostics...', color: 'cyan' }).start();
79
79
  const lines = [];
80
80
  let passCount = 0;
@@ -87,7 +87,7 @@ export async function doctorCommand() {
87
87
 
88
88
  // 1. Config check
89
89
  spinner.text = 'Checking configuration...';
90
- const config = await getConfig();
90
+ const config = await getConfig(options.profile);
91
91
  if (config) {
92
92
  const providerLabel = config.provider === 'git' ? 'git' : 'local';
93
93
  const dest = config.provider === 'git' ? config.gitRepo : config.localPath;
@@ -0,0 +1,199 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import inquirer from 'inquirer';
4
+ import { execFileSync } from 'child_process';
5
+ import {
6
+ getRawConfig, listProfiles, getActiveProfileName,
7
+ createProfile, switchProfile, deleteProfile
8
+ } from '../config.js';
9
+
10
+ function getGitHubUsername() {
11
+ try {
12
+ return execFileSync('gh', ['api', 'user', '--jq', '.login'], { encoding: 'utf8' }).trim();
13
+ } catch {
14
+ try {
15
+ return execFileSync('git', ['config', '--global', 'user.name'], { encoding: 'utf8' }).trim();
16
+ } catch { return ''; }
17
+ }
18
+ }
19
+
20
+ export async function profileListCommand() {
21
+ const profiles = await listProfiles();
22
+ const active = await getActiveProfileName();
23
+ const raw = await getRawConfig();
24
+
25
+ if (profiles.length === 0) {
26
+ console.log('\n' + chalk.yellow('No profiles configured. Run ') + chalk.cyan('memoir init') + chalk.yellow(' first.\n'));
27
+ return;
28
+ }
29
+
30
+ console.log();
31
+ console.log(chalk.bold.white(' Profiles:\n'));
32
+
33
+ for (const name of profiles) {
34
+ const isActive = name === active;
35
+ const marker = isActive ? chalk.green(' ✔ ') : chalk.gray(' ');
36
+ const label = isActive ? chalk.white.bold(name) : chalk.white(name);
37
+
38
+ // Get profile details
39
+ let detail = '';
40
+ if (raw.version >= 2 && raw.profiles?.[name]) {
41
+ const p = raw.profiles[name];
42
+ const dest = p.provider === 'git' ? p.gitRepo : p.localPath;
43
+ detail = chalk.gray(` → ${dest}`);
44
+ if (p.only) detail += chalk.gray(` (${p.only.join(', ')})`);
45
+ } else if (!raw.version) {
46
+ const dest = raw.provider === 'git' ? raw.gitRepo : raw.localPath;
47
+ detail = chalk.gray(` → ${dest}`);
48
+ }
49
+
50
+ console.log(`${marker}${label}${detail}`);
51
+ }
52
+ console.log();
53
+ }
54
+
55
+ export async function profileCreateCommand(name) {
56
+ const profiles = await listProfiles();
57
+ if (profiles.includes(name)) {
58
+ console.log(chalk.red(`\n✖ Profile "${name}" already exists.\n`));
59
+ return;
60
+ }
61
+
62
+ console.log('\n' + chalk.cyan(`Creating profile: ${chalk.bold(name)}\n`));
63
+
64
+ const detectedUser = getGitHubUsername();
65
+
66
+ const { provider } = await inquirer.prompt([{
67
+ type: 'list',
68
+ name: 'provider',
69
+ message: 'Storage for this profile?',
70
+ choices: [
71
+ { name: 'GitHub', value: 'git' },
72
+ { name: 'Local folder', value: 'local' }
73
+ ]
74
+ }]);
75
+
76
+ const profileConfig = { provider };
77
+
78
+ if (provider === 'local') {
79
+ const { localPath } = await inquirer.prompt([{
80
+ type: 'input',
81
+ name: 'localPath',
82
+ message: 'Save to:',
83
+ validate: (input) => input.trim() ? true : 'Required'
84
+ }]);
85
+ profileConfig.localPath = localPath;
86
+ } else {
87
+ const answers = await inquirer.prompt([
88
+ {
89
+ type: 'input',
90
+ name: 'username',
91
+ message: 'GitHub username:',
92
+ default: detectedUser || undefined,
93
+ validate: (input) => input.trim() ? true : 'Required'
94
+ },
95
+ {
96
+ type: 'input',
97
+ name: 'repo',
98
+ message: 'Repo name:',
99
+ default: `ai-memory-${name}`,
100
+ validate: (input) => input.trim() ? true : 'Required'
101
+ }
102
+ ]);
103
+ const username = answers.username.trim();
104
+ const repo = answers.repo.trim();
105
+ profileConfig.gitRepo = `https://github.com/${username}/${repo}.git`;
106
+
107
+ // Auto-create repo if possible
108
+ try {
109
+ execFileSync('gh', ['repo', 'view', `${username}/${repo}`], { stdio: 'ignore' });
110
+ console.log(chalk.gray(` ✔ Repo exists`));
111
+ } catch {
112
+ try {
113
+ execFileSync('gh', ['repo', 'create', `${username}/${repo}`, '--private', '--description', `AI memory backup - ${name} (memoir-cli)`], { stdio: 'ignore' });
114
+ console.log(chalk.green(` ✔ Created private repo`));
115
+ } catch {
116
+ console.log(chalk.yellow(` ⚠ Could not auto-create repo. Create it manually on GitHub.`));
117
+ }
118
+ }
119
+ }
120
+
121
+ // Ask which tools to sync (optional filter)
122
+ const { filterTools } = await inquirer.prompt([{
123
+ type: 'confirm',
124
+ name: 'filterTools',
125
+ message: 'Limit this profile to specific tools?',
126
+ default: false
127
+ }]);
128
+
129
+ if (filterTools) {
130
+ const { tools } = await inquirer.prompt([{
131
+ type: 'checkbox',
132
+ name: 'tools',
133
+ message: 'Which tools should this profile sync?',
134
+ choices: [
135
+ { name: 'Claude Code', value: 'claude' },
136
+ { name: 'Gemini CLI', value: 'gemini' },
137
+ { name: 'OpenAI Codex', value: 'codex' },
138
+ { name: 'Cursor', value: 'cursor' },
139
+ { name: 'GitHub Copilot', value: 'copilot' },
140
+ { name: 'Windsurf', value: 'windsurf' },
141
+ { name: 'Zed', value: 'zed' },
142
+ { name: 'Cline', value: 'cline' },
143
+ { name: 'Continue.dev', value: 'continue' },
144
+ { name: 'Aider', value: 'aider' }
145
+ ]
146
+ }]);
147
+ if (tools.length > 0) {
148
+ profileConfig.only = tools;
149
+ }
150
+ }
151
+
152
+ await createProfile(name, profileConfig);
153
+
154
+ // Ask if they want to switch to it
155
+ const { switchNow } = await inquirer.prompt([{
156
+ type: 'confirm',
157
+ name: 'switchNow',
158
+ message: `Switch to "${name}" now?`,
159
+ default: true
160
+ }]);
161
+
162
+ if (switchNow) {
163
+ await switchProfile(name);
164
+ }
165
+
166
+ console.log('\n' + boxen(
167
+ chalk.green(`✔ Profile "${name}" created`) +
168
+ (switchNow ? chalk.gray(` (now active)`) : ''),
169
+ { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'green', dimBorder: true }
170
+ ) + '\n');
171
+ }
172
+
173
+ export async function profileSwitchCommand(name) {
174
+ try {
175
+ await switchProfile(name);
176
+ console.log('\n' + chalk.green(`✔ Switched to profile "${name}"\n`));
177
+ } catch (err) {
178
+ console.log('\n' + chalk.red(`✖ ${err.message}\n`));
179
+ }
180
+ }
181
+
182
+ export async function profileDeleteCommand(name) {
183
+ try {
184
+ const { confirm } = await inquirer.prompt([{
185
+ type: 'confirm',
186
+ name: 'confirm',
187
+ message: `Delete profile "${name}"? This cannot be undone.`,
188
+ default: false
189
+ }]);
190
+ if (!confirm) {
191
+ console.log(chalk.gray('\nCancelled.\n'));
192
+ return;
193
+ }
194
+ await deleteProfile(name);
195
+ console.log('\n' + chalk.green(`✔ Profile "${name}" deleted\n`));
196
+ } catch (err) {
197
+ console.log('\n' + chalk.red(`✖ ${err.message}\n`));
198
+ }
199
+ }
@@ -10,7 +10,7 @@ import { extractMemories, adapters } from '../adapters/index.js';
10
10
  import { syncToLocal, syncToGit } from '../providers/index.js';
11
11
 
12
12
  export async function pushCommand(options = {}) {
13
- const config = await getConfig();
13
+ const config = await getConfig(options.profile);
14
14
 
15
15
  if (!config) {
16
16
  console.log('\n' + boxen(
@@ -28,7 +28,9 @@ export async function pushCommand(options = {}) {
28
28
  await fs.ensureDir(stagingDir);
29
29
 
30
30
  try {
31
- const onlyFilter = options.only ? options.only.split(',').map(t => t.trim().toLowerCase()) : null;
31
+ // Profile-level tool filter (config.only) merged with CLI --only flag
32
+ const onlyRaw = options.only || (config.only ? config.only.join(',') : null);
33
+ const onlyFilter = onlyRaw ? onlyRaw.split(',').map(t => t.trim().toLowerCase()) : null;
32
34
  const foundAny = await extractMemories(stagingDir, spinner, onlyFilter);
33
35
 
34
36
  if (!foundAny) {
@@ -9,7 +9,7 @@ import { getConfig } from '../config.js';
9
9
  import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
10
10
 
11
11
  export async function restoreCommand(options = {}) {
12
- const config = await getConfig();
12
+ const config = await getConfig(options.profile);
13
13
 
14
14
  if (!config) {
15
15
  console.log('\n' + boxen(
@@ -92,7 +92,7 @@ async function injectHandoff(content, tool) {
92
92
  }
93
93
 
94
94
  export async function resumeCommand(options = {}) {
95
- const config = await getConfig();
95
+ const config = await getConfig(options.profile);
96
96
 
97
97
  if (!config) {
98
98
  console.log('\n' + boxen(
@@ -264,7 +264,7 @@ Keep it under 300 words total. Be specific about file names and features.`;
264
264
  }
265
265
 
266
266
  export async function snapshotCommand(options = {}) {
267
- const config = await getConfig();
267
+ const config = await getConfig(options.profile);
268
268
 
269
269
  console.log();
270
270
  const spinner = ora({ text: chalk.gray('Finding latest session...'), spinner: 'dots' }).start();
@@ -6,8 +6,8 @@ import gradient from 'gradient-string';
6
6
  import { getConfig } from '../config.js';
7
7
  import { adapters } from '../adapters/index.js';
8
8
 
9
- export async function statusCommand() {
10
- const config = await getConfig();
9
+ export async function statusCommand(options = {}) {
10
+ const config = await getConfig(options.profile);
11
11
 
12
12
  console.log();
13
13
 
@@ -36,8 +36,8 @@ function isBinaryFile(filePath) {
36
36
  return binaryExts.includes(path.extname(filePath).toLowerCase());
37
37
  }
38
38
 
39
- export async function viewCommand() {
40
- const config = await getConfig();
39
+ export async function viewCommand(options = {}) {
40
+ const config = await getConfig(options.profile);
41
41
  if (!config) {
42
42
  console.log(chalk.red('\n✖ Not configured yet. Run: memoir init\n'));
43
43
  return;
package/src/config.js CHANGED
@@ -7,7 +7,8 @@ const CONFIG_DIR = process.platform === 'win32'
7
7
  : path.join(os.homedir(), '.config', 'memoir');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
9
 
10
- export async function getConfig() {
10
+ // Read the raw config file as-is
11
+ export async function getRawConfig() {
11
12
  if (await fs.pathExists(CONFIG_FILE)) {
12
13
  try {
13
14
  return await fs.readJson(CONFIG_FILE);
@@ -18,22 +19,108 @@ export async function getConfig() {
18
19
  return null;
19
20
  }
20
21
 
22
+ // Get resolved config for a specific profile (or active profile)
23
+ // Backwards compatible: v1 flat configs return as-is
24
+ export async function getConfig(profileName = null) {
25
+ const raw = await getRawConfig();
26
+ if (!raw) return null;
27
+
28
+ // v1 flat config — no profiles, return as-is
29
+ if (!raw.version || raw.version < 2) return raw;
30
+
31
+ // v2 — resolve profile
32
+ const name = profileName || raw.activeProfile || 'default';
33
+ const profile = raw.profiles?.[name];
34
+ if (!profile) return null;
35
+
36
+ // Merge top-level shared keys into profile
37
+ return { ...profile, geminiApiKey: raw.geminiApiKey };
38
+ }
39
+
40
+ // Save entire raw config
21
41
  export async function saveConfig(config) {
22
42
  await fs.ensureDir(CONFIG_DIR);
23
43
  await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
24
- // Restrict permissions — config may contain API keys
25
44
  if (process.platform !== 'win32') {
26
45
  await fs.chmod(CONFIG_FILE, 0o600);
27
46
  }
28
47
  }
29
48
 
49
+ // Save config for a specific profile (creates v2 format if needed)
50
+ export async function saveProfileConfig(profileName, profileData) {
51
+ let raw = await getRawConfig() || {};
52
+ if (!raw.version || raw.version < 2) {
53
+ raw = migrateConfigToV2(raw);
54
+ }
55
+ raw.profiles[profileName] = profileData;
56
+ await saveConfig(raw);
57
+ }
58
+
59
+ // Migrate v1 flat config to v2 profiles format
60
+ export function migrateConfigToV2(flat) {
61
+ const { provider, gitRepo, localPath, geminiApiKey, ...rest } = flat;
62
+ return {
63
+ version: 2,
64
+ activeProfile: 'default',
65
+ geminiApiKey: geminiApiKey || undefined,
66
+ profiles: {
67
+ default: { provider, gitRepo, localPath, ...rest }
68
+ }
69
+ };
70
+ }
71
+
72
+ export async function getActiveProfileName() {
73
+ const raw = await getRawConfig();
74
+ if (!raw || !raw.version || raw.version < 2) return 'default';
75
+ return raw.activeProfile || 'default';
76
+ }
77
+
78
+ export async function listProfiles() {
79
+ const raw = await getRawConfig();
80
+ if (!raw) return [];
81
+ if (!raw.version || raw.version < 2) return ['default'];
82
+ return Object.keys(raw.profiles || {});
83
+ }
84
+
85
+ export async function createProfile(name, profileConfig) {
86
+ let raw = await getRawConfig() || {};
87
+ if (!raw.version || raw.version < 2) {
88
+ raw = migrateConfigToV2(raw);
89
+ }
90
+ raw.profiles[name] = profileConfig;
91
+ await saveConfig(raw);
92
+ }
93
+
94
+ export async function switchProfile(name) {
95
+ let raw = await getRawConfig();
96
+ if (!raw) throw new Error('Not configured. Run memoir init first.');
97
+ if (!raw.version || raw.version < 2) {
98
+ raw = migrateConfigToV2(raw);
99
+ }
100
+ if (!raw.profiles[name]) throw new Error(`Profile "${name}" does not exist.`);
101
+ raw.activeProfile = name;
102
+ await saveConfig(raw);
103
+ }
104
+
105
+ export async function deleteProfile(name) {
106
+ const raw = await getRawConfig();
107
+ if (!raw || !raw.version || raw.version < 2) {
108
+ throw new Error('No profiles configured.');
109
+ }
110
+ if (!raw.profiles[name]) throw new Error(`Profile "${name}" does not exist.`);
111
+ if (raw.activeProfile === name) throw new Error(`Cannot delete the active profile. Switch first with: memoir profile switch <name>`);
112
+ if (Object.keys(raw.profiles).length <= 1) throw new Error('Cannot delete the last profile.');
113
+ delete raw.profiles[name];
114
+ await saveConfig(raw);
115
+ }
116
+
30
117
  export async function getGeminiApiKey() {
31
- const config = await getConfig();
32
- return config?.geminiApiKey || process.env.GEMINI_API_KEY || null;
118
+ const raw = await getRawConfig();
119
+ return raw?.geminiApiKey || process.env.GEMINI_API_KEY || null;
33
120
  }
34
121
 
35
122
  export async function saveGeminiApiKey(apiKey) {
36
- const config = await getConfig() || {};
37
- config.geminiApiKey = apiKey;
38
- await saveConfig(config);
123
+ let raw = await getRawConfig() || {};
124
+ raw.geminiApiKey = apiKey;
125
+ await saveConfig(raw);
39
126
  }
@@ -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: 'chatgpt',
8
+ name: 'ChatGPT',
9
+ icon: '💬',
10
+ format: 'Markdown custom instructions in CHATGPT.md. Written as instructions for ChatGPT — your preferences, coding style, response format, and project context. Paste into ChatGPT\'s Custom Instructions or Memory settings.',
11
+
12
+ discover() {
13
+ const files = [];
14
+ const projectFile = path.join(cwd, 'CHATGPT.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, 'CHATGPT.md');
23
+ }
24
+ };
@@ -1,5 +1,6 @@
1
1
  import claude from './claude.js';
2
2
  import gemini from './gemini.js';
3
+ import chatgpt from './chatgpt.js';
3
4
  import codex from './codex.js';
4
5
  import cursor from './cursor.js';
5
6
  import copilot from './copilot.js';
@@ -10,7 +11,7 @@ import continuedev from './continuedev.js';
10
11
  import aider from './aider.js';
11
12
 
12
13
  const registry = {};
13
- for (const tool of [claude, gemini, codex, cursor, copilot, windsurf, zed, cline, continuedev, aider]) {
14
+ for (const tool of [claude, gemini, chatgpt, codex, cursor, copilot, windsurf, zed, cline, continuedev, aider]) {
14
15
  registry[tool.key] = tool;
15
16
  }
16
17