memoir-cli 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,9 +28,12 @@ No locked-in SaaS, no lost context, no complex shell scripts.
28
28
 
29
29
  ### Supported Integrations
30
30
  - [x] **Gemini CLI**
31
- - [x] **Claude CLI**
32
- - [ ] *Cursor (Coming Soon)*
33
- - [ ] *GitHub Copilot (Coming Soon)*
31
+ - [x] **Claude Code**
32
+ - [x] **OpenAI Codex CLI**
33
+ - [x] **Cursor**
34
+ - [x] **GitHub Copilot**
35
+ - [x] **Windsurf**
36
+ - [x] **Aider**
34
37
 
35
38
  ---
36
39
 
package/bin/memoir.js CHANGED
@@ -6,8 +6,9 @@ import gradient from 'gradient-string';
6
6
  import { initCommand } from '../src/commands/init.js';
7
7
  import { pushCommand } from '../src/commands/push.js';
8
8
  import { restoreCommand } from '../src/commands/restore.js';
9
+ import { statusCommand } from '../src/commands/status.js';
9
10
 
10
- const VERSION = '1.0.0';
11
+ const VERSION = '1.1.1';
11
12
 
12
13
  program
13
14
  .name('memoir')
@@ -52,6 +53,18 @@ program
52
53
  }
53
54
  });
54
55
 
56
+ program
57
+ .command('status')
58
+ .description('Show detected AI tools and configuration status')
59
+ .action(async () => {
60
+ try {
61
+ await statusCommand();
62
+ } catch (err) {
63
+ console.error(chalk.red('\n✖ Error:'), err.message);
64
+ process.exit(1);
65
+ }
66
+ });
67
+
55
68
  program
56
69
  .command('migrate')
57
70
  .description('Migrate memory/context from one AI bot to another (e.g. Claude to Gemini)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Your AI remembers everything. Sync it everywhere.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -5,6 +5,9 @@ import chalk from 'chalk';
5
5
 
6
6
  const home = os.homedir();
7
7
 
8
+ const isWin = process.platform === 'win32';
9
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
10
+
8
11
  export const adapters = [
9
12
  {
10
13
  name: 'Gemini CLI',
@@ -22,21 +25,86 @@ export const adapters = [
22
25
  const basename = path.basename(src);
23
26
  return !basename.endsWith('.key') && basename !== '.env';
24
27
  }
28
+ },
29
+ {
30
+ name: 'OpenAI Codex',
31
+ source: path.join(home, '.codex'),
32
+ filter: (src) => {
33
+ const basename = path.basename(src);
34
+ const ignored = ['.git', 'sessions', 'cache'];
35
+ return !ignored.includes(basename) && !basename.endsWith('.key') && basename !== '.env';
36
+ }
37
+ },
38
+ {
39
+ name: 'Cursor',
40
+ source: isWin
41
+ ? path.join(appData, 'Cursor', 'User')
42
+ : path.join(home, 'Library', 'Application Support', 'Cursor', 'User'),
43
+ filter: (src) => {
44
+ const basename = path.basename(src);
45
+ const ignored = ['globalStorage', 'workspaceStorage', 'CachedData', 'Cache', 'GPUCache', 'logs', 'History', 'Backups', 'snippets'];
46
+ return !ignored.includes(basename);
47
+ }
48
+ },
49
+ {
50
+ name: 'GitHub Copilot',
51
+ source: isWin
52
+ ? path.join(appData, 'GitHub Copilot')
53
+ : path.join(home, '.config', 'github-copilot'),
54
+ filter: (src) => {
55
+ const basename = path.basename(src);
56
+ const ignored = ['hosts.json', 'apps.json', 'versions.json'];
57
+ return !ignored.includes(basename);
58
+ }
59
+ },
60
+ {
61
+ name: 'Windsurf',
62
+ source: isWin
63
+ ? path.join(appData, 'Windsurf', 'User')
64
+ : path.join(home, 'Library', 'Application Support', 'Windsurf', 'User'),
65
+ filter: (src) => {
66
+ const basename = path.basename(src);
67
+ const ignored = ['workspaceStorage', 'CachedData', 'Cache', 'GPUCache', 'logs', 'History', 'Backups', 'memories', 'snippets'];
68
+ return !ignored.includes(basename);
69
+ }
70
+ },
71
+ {
72
+ name: 'Aider',
73
+ source: home,
74
+ customExtract: true,
75
+ files: ['.aider.conf.yml', '.aider.system-prompt.md'],
76
+ filter: () => true
25
77
  }
26
78
  ];
27
79
 
28
80
  export async function extractMemories(stagingDir, spinner) {
29
81
  let foundAny = false;
30
-
82
+
31
83
  for (const adapter of adapters) {
32
- if (await fs.pathExists(adapter.source)) {
84
+ if (adapter.customExtract) {
85
+ // Handle tools with individual files (e.g. Aider)
86
+ const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
87
+ let foundFile = false;
88
+ for (const file of adapter.files) {
89
+ const filePath = path.join(adapter.source, file);
90
+ if (await fs.pathExists(filePath)) {
91
+ if (!foundFile) {
92
+ spinner.text = `Found ${chalk.cyan(adapter.name)} config... copying to staging`;
93
+ await fs.ensureDir(dest);
94
+ foundFile = true;
95
+ }
96
+ await fs.copy(filePath, path.join(dest, file));
97
+ }
98
+ }
99
+ if (foundFile) foundAny = true;
100
+ } else if (await fs.pathExists(adapter.source)) {
33
101
  spinner.text = `Found ${chalk.cyan(adapter.name)} memory... copying to staging`;
34
- const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(' ', '-'));
102
+ const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
35
103
  await fs.ensureDir(dest);
36
104
  await fs.copy(adapter.source, dest, { filter: adapter.filter });
37
105
  foundAny = true;
38
106
  }
39
107
  }
40
-
108
+
41
109
  return foundAny;
42
110
  }
@@ -9,11 +9,11 @@ export async function restoreMemories(sourceDir, spinner) {
9
9
  let restoredAny = false;
10
10
 
11
11
  for (const adapter of adapters) {
12
- const backupDir = path.join(sourceDir, adapter.name.toLowerCase().replace(' ', '-'));
13
-
12
+ const backupDir = path.join(sourceDir, adapter.name.toLowerCase().replace(/ /g, '-'));
13
+
14
14
  if (await fs.pathExists(backupDir)) {
15
15
  spinner.stop();
16
-
16
+
17
17
  console.log('\\n' + chalk.yellow(`⚠ Found backup for ${chalk.bold(adapter.name)}.`));
18
18
  const { confirm } = await inquirer.prompt([
19
19
  {
@@ -25,12 +25,20 @@ export async function restoreMemories(sourceDir, spinner) {
25
25
  ]);
26
26
 
27
27
  spinner.start();
28
-
28
+
29
29
  if (confirm) {
30
- spinner.text = `Restoring ${chalk.cyan(adapter.name)} memory to ${adapter.source}...`;
31
- await fs.ensureDir(adapter.source);
32
- // Copy files from backup to the real source directory
33
- await fs.copy(backupDir, adapter.source, { overwrite: true });
30
+ if (adapter.customExtract) {
31
+ // Restore individual files back to their original locations
32
+ const files = await fs.readdir(backupDir);
33
+ for (const file of files) {
34
+ const dest = path.join(adapter.source, file);
35
+ await fs.copy(path.join(backupDir, file), dest, { overwrite: true });
36
+ }
37
+ } else {
38
+ spinner.text = `Restoring ${chalk.cyan(adapter.name)} memory to ${adapter.source}...`;
39
+ await fs.ensureDir(adapter.source);
40
+ await fs.copy(backupDir, adapter.source, { overwrite: true });
41
+ }
34
42
  restoredAny = true;
35
43
  } else {
36
44
  spinner.info(chalk.gray(`Skipped restoring ${adapter.name}.`));
@@ -0,0 +1,56 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { getConfig } from '../config.js';
5
+ import { adapters } from '../adapters/index.js';
6
+
7
+ export async function statusCommand() {
8
+ const config = await getConfig();
9
+
10
+ console.log();
11
+
12
+ // Config status
13
+ if (config) {
14
+ const provider = config.provider === 'git' ? `Git (${config.gitRepo})` : `Local (${config.localPath})`;
15
+ console.log(chalk.green('✔ Configured') + chalk.gray(` — ${provider}`));
16
+ } else {
17
+ console.log(chalk.red('✖ Not configured') + chalk.gray(' — run memoir init'));
18
+ console.log();
19
+ return;
20
+ }
21
+
22
+ console.log();
23
+
24
+ // Detected tools
25
+ console.log(chalk.bold('Detected AI tools:\n'));
26
+
27
+ let detected = 0;
28
+ for (const adapter of adapters) {
29
+ if (adapter.customExtract) {
30
+ let hasFiles = false;
31
+ for (const file of adapter.files) {
32
+ if (await fs.pathExists(path.join(adapter.source, file))) {
33
+ hasFiles = true;
34
+ break;
35
+ }
36
+ }
37
+ if (hasFiles) {
38
+ console.log(chalk.green(' ✔ ') + adapter.name);
39
+ detected++;
40
+ } else {
41
+ console.log(chalk.gray(' ○ ') + chalk.gray(adapter.name + ' — not found'));
42
+ }
43
+ } else {
44
+ if (await fs.pathExists(adapter.source)) {
45
+ console.log(chalk.green(' ✔ ') + adapter.name);
46
+ detected++;
47
+ } else {
48
+ console.log(chalk.gray(' ○ ') + chalk.gray(adapter.name + ' — not found'));
49
+ }
50
+ }
51
+ }
52
+
53
+ console.log();
54
+ console.log(chalk.white(`${detected} tool${detected !== 1 ? 's' : ''} detected on this machine.`));
55
+ console.log();
56
+ }
package/src/config.js CHANGED
@@ -2,7 +2,9 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
 
5
- const CONFIG_DIR = path.join(os.homedir(), '.config', 'memoir');
5
+ const CONFIG_DIR = process.platform === 'win32'
6
+ ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'memoir')
7
+ : path.join(os.homedir(), '.config', 'memoir');
6
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
9
 
8
10
  export async function getConfig() {
@@ -24,19 +24,46 @@ export async function syncToGit(config, stagingDir, spinner) {
24
24
 
25
25
  spinner.text = `Authenticating and syncing with Git remote: ${chalk.cyan(repoUrl)}`;
26
26
 
27
+ // Clone existing repo to preserve history, then replace contents
28
+ const gitDir = path.join(os.tmpdir(), `memoir-git-${Date.now()}`);
29
+ await fs.ensureDir(gitDir);
30
+
27
31
  try {
28
- execSync('git init', { cwd: stagingDir, stdio: 'ignore' });
29
- execSync('git branch -m main', { cwd: stagingDir, stdio: 'ignore' });
30
- execSync('git add .', { cwd: stagingDir, stdio: 'ignore' });
31
- execSync('git config user.name "memoir" && git config user.email "bot@memoir.dev"', { cwd: stagingDir, stdio: 'ignore' });
32
- execSync('git commit -m "chore: memoir backup"', { cwd: stagingDir, stdio: 'ignore' });
33
-
32
+ try {
33
+ execSync(`git clone --depth 10 ${repoUrl} .`, { cwd: gitDir, stdio: 'ignore' });
34
+ // Remove old files so deleted configs don't persist
35
+ const files = await fs.readdir(gitDir);
36
+ for (const f of files) {
37
+ if (f !== '.git') await fs.remove(path.join(gitDir, f));
38
+ }
39
+ } 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' });
43
+ }
44
+
45
+ // Copy staged memories into the git dir
46
+ await fs.copy(stagingDir, gitDir);
47
+
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
+
52
+ const timestamp = new Date().toISOString().split('T')[0];
53
+ try {
54
+ execSync(`git commit -m "memoir backup ${timestamp}"`, { cwd: gitDir, stdio: 'ignore' });
55
+ } catch {
56
+ spinner.succeed(chalk.green('Already up to date! ') + chalk.gray('No changes to push.'));
57
+ return;
58
+ }
59
+
34
60
  spinner.text = `Pushing data to ${chalk.cyan(repoUrl)}...`;
35
- // We ignore stdio to prevent spam, but if it fails it will throw
36
- execSync(`git push --force ${repoUrl} main`, { cwd: stagingDir, stdio: 'ignore' });
37
-
61
+ execSync(`git push ${repoUrl} main`, { cwd: gitDir, stdio: 'ignore' });
62
+
38
63
  spinner.succeed(chalk.green('Sync complete! ') + chalk.gray('(Uploaded securely to GitHub)'));
39
64
  } catch (err) {
40
- throw new Error('Failed to push to git repository. Ensure your SSH keys are configured and the repository exists.');
65
+ throw new Error('Failed to push to git repository. Ensure your credentials are configured and the repository exists.');
66
+ } finally {
67
+ await fs.remove(gitDir);
41
68
  }
42
69
  }