memoir-cli 2.2.0 → 2.5.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.
@@ -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