memoir-cli 1.1.1 → 1.2.1

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,10 +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**
31
+ - [x] **Claude Code**
32
32
  - [x] **OpenAI Codex CLI**
33
- - [ ] *Cursor (Coming Soon)*
34
- - [ ] *GitHub Copilot (Coming Soon)*
33
+ - [x] **Cursor**
34
+ - [x] **GitHub Copilot**
35
+ - [x] **Windsurf**
36
+ - [x] **Aider**
35
37
 
36
38
  ---
37
39
 
package/bin/memoir.js CHANGED
@@ -6,17 +6,25 @@ 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.2.0';
12
+
13
+ // Custom help banner
14
+ program.addHelpText('beforeAll', '\n' + boxen(
15
+ gradient.pastel.multiline(' memoir ') + '\n' +
16
+ chalk.gray(' Your AI remembers everything.'),
17
+ { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
18
+ ) + '\n');
11
19
 
12
20
  program
13
21
  .name('memoir')
14
- .description('Your AI remembers everything. Sync it everywhere.')
22
+ .description(chalk.white('Sync your AI memory across every device.'))
15
23
  .version(VERSION);
16
24
 
17
25
  program
18
26
  .command('init')
19
- .description('Initialize and configure memoir storage preferences')
27
+ .description('Set up memoir with your storage provider')
20
28
  .action(async () => {
21
29
  try {
22
30
  await initCommand();
@@ -29,7 +37,7 @@ program
29
37
  program
30
38
  .command('push')
31
39
  .alias('remember')
32
- .description('Sync your AI memory to your configured storage')
40
+ .description('Back up your AI memory to the cloud')
33
41
  .action(async () => {
34
42
  try {
35
43
  await pushCommand();
@@ -42,7 +50,7 @@ program
42
50
  program
43
51
  .command('restore')
44
52
  .alias('pull')
45
- .description('Restore your AI memory from your configured storage')
53
+ .description('Restore your AI memory on this machine')
46
54
  .action(async () => {
47
55
  try {
48
56
  await restoreCommand();
@@ -52,15 +60,27 @@ program
52
60
  }
53
61
  });
54
62
 
63
+ program
64
+ .command('status')
65
+ .description('See what AI tools are on this machine')
66
+ .action(async () => {
67
+ try {
68
+ await statusCommand();
69
+ } catch (err) {
70
+ console.error(chalk.red('\n✖ Error:'), err.message);
71
+ process.exit(1);
72
+ }
73
+ });
74
+
55
75
  program
56
76
  .command('migrate')
57
- .description('Migrate memory/context from one AI bot to another (e.g. Claude to Gemini)')
77
+ .description('Translate memory between AI providers')
58
78
  .action(() => {
59
79
  console.log('\n' + boxen(
60
- gradient.pastel('memoir migrate (Coming Soon)') + '\n\n' +
61
- chalk.white('We are actively developing the ability to instantly translate') + '\n' +
62
- chalk.white('and swap your context/memories between different AI providers.') + '\n\n' +
63
- chalk.cyan('Stay tuned for updates!'),
80
+ gradient.pastel(' memoir migrate ') + '\n\n' +
81
+ chalk.white('Instantly translate your context between') + '\n' +
82
+ chalk.white('Claude, Gemini, Codex, and more.') + '\n\n' +
83
+ chalk.cyan.bold('Coming soon.'),
64
84
  { padding: 1, borderStyle: 'round', borderColor: 'yellow', align: 'center' }
65
85
  ) + '\n');
66
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
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',
@@ -31,21 +34,77 @@ export const adapters = [
31
34
  const ignored = ['.git', 'sessions', 'cache'];
32
35
  return !ignored.includes(basename) && !basename.endsWith('.key') && basename !== '.env';
33
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
34
77
  }
35
78
  ];
36
79
 
37
80
  export async function extractMemories(stagingDir, spinner) {
38
81
  let foundAny = false;
39
-
82
+
40
83
  for (const adapter of adapters) {
41
- 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)) {
42
101
  spinner.text = `Found ${chalk.cyan(adapter.name)} memory... copying to staging`;
43
- const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(' ', '-'));
102
+ const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
44
103
  await fs.ensureDir(dest);
45
104
  await fs.copy(adapter.source, dest, { filter: adapter.filter });
46
105
  foundAny = true;
47
106
  }
48
107
  }
49
-
108
+
50
109
  return foundAny;
51
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}.`));
@@ -3,48 +3,82 @@ import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import ora from 'ora';
6
+ import boxen from 'boxen';
7
+ import gradient from 'gradient-string';
6
8
  import { getConfig } from '../config.js';
7
- import { extractMemories } from '../adapters/index.js';
9
+ import { extractMemories, adapters } from '../adapters/index.js';
8
10
  import { syncToLocal, syncToGit } from '../providers/index.js';
9
11
 
10
12
  export async function pushCommand() {
11
13
  const config = await getConfig();
12
-
14
+
13
15
  if (!config) {
14
- console.log('\\n' + chalk.red('✖ memoir is not configured.'));
15
- console.log(`Run ${chalk.cyan('memoir init')} to set up your storage provider.\\n`);
16
+ console.log('\n' + boxen(
17
+ chalk.red(' Not configured yet\n\n') +
18
+ chalk.white('Run ') + chalk.cyan.bold('memoir init') + chalk.white(' to get started.'),
19
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
20
+ ) + '\n');
16
21
  return;
17
22
  }
18
23
 
19
24
  console.log();
20
- const spinner = ora('Initializing AI memory sync...').start();
25
+ const spinner = ora({ text: chalk.gray('Scanning for AI tools...'), spinner: 'dots' }).start();
21
26
 
22
- // Create a temporary staging directory
23
27
  const stagingDir = path.join(os.tmpdir(), `memoir-staging-${Date.now()}`);
24
28
  await fs.ensureDir(stagingDir);
25
29
 
26
30
  try {
27
- spinner.text = 'Scanning system for AI configurations...';
28
-
29
- // Pass spinner so adapter can update it
30
31
  const foundAny = await extractMemories(stagingDir, spinner);
31
-
32
+
32
33
  if (!foundAny) {
33
- spinner.warn(chalk.yellow('No supported AI memory folders found on this system.'));
34
+ spinner.stop();
35
+ console.log('\n' + boxen(
36
+ chalk.yellow('No AI tools detected on this machine.\n\n') +
37
+ chalk.gray('Supported: Claude, Gemini, Codex, Cursor, Copilot, Windsurf, Aider'),
38
+ { padding: 1, borderStyle: 'round', borderColor: 'yellow' }
39
+ ) + '\n');
34
40
  return;
35
41
  }
36
42
 
43
+ // Count what was found
44
+ const found = [];
45
+ for (const adapter of adapters) {
46
+ if (adapter.customExtract) {
47
+ for (const file of adapter.files) {
48
+ if (await fs.pathExists(path.join(adapter.source, file))) {
49
+ found.push(adapter.name);
50
+ break;
51
+ }
52
+ }
53
+ } else if (await fs.pathExists(adapter.source)) {
54
+ found.push(adapter.name);
55
+ }
56
+ }
57
+
58
+ spinner.text = chalk.gray('Uploading to ' + (config.provider === 'git' ? 'GitHub' : 'local storage') + '...');
59
+
37
60
  if (config.provider === 'local' || config.provider.includes('local')) {
38
61
  await syncToLocal(config, stagingDir, spinner);
39
62
  } else if (config.provider === 'git' || config.provider.includes('git')) {
40
63
  await syncToGit(config, stagingDir, spinner);
41
64
  } else {
42
65
  spinner.fail(chalk.red(`Unknown provider: ${config.provider}`));
66
+ return;
43
67
  }
68
+
69
+ spinner.stop();
70
+
71
+ // Success output
72
+ const toolList = found.map(t => chalk.cyan(' ✔ ' + t)).join('\n');
73
+ console.log('\n' + boxen(
74
+ gradient.pastel(' Backed up! ') + '\n\n' +
75
+ toolList + '\n\n' +
76
+ chalk.gray(`${found.length} tool${found.length !== 1 ? 's' : ''} synced to ${config.provider === 'git' ? 'GitHub' : 'local storage'}`),
77
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
78
+ ) + '\n');
44
79
  } catch (error) {
45
80
  spinner.fail(chalk.red('Sync failed: ') + error.message);
46
81
  } finally {
47
- // Clean up staging directory
48
82
  await fs.remove(stagingDir);
49
83
  }
50
84
  }
@@ -3,22 +3,26 @@ import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import ora from 'ora';
6
+ import boxen from 'boxen';
7
+ import gradient from 'gradient-string';
6
8
  import { getConfig } from '../config.js';
7
9
  import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
8
10
 
9
11
  export async function restoreCommand() {
10
12
  const config = await getConfig();
11
-
13
+
12
14
  if (!config) {
13
- console.log('\\n' + chalk.red('✖ memoir is not configured.'));
14
- console.log(`Run ${chalk.cyan('memoir init')} to set up your storage provider and fetch your files.\\n`);
15
+ console.log('\n' + boxen(
16
+ chalk.red(' Not configured yet\n\n') +
17
+ chalk.white('Run ') + chalk.cyan.bold('memoir init') + chalk.white(' to get started.'),
18
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
19
+ ) + '\n');
15
20
  return;
16
21
  }
17
22
 
18
23
  console.log();
19
- const spinner = ora('Initializing AI memory restore...').start();
24
+ const spinner = ora({ text: chalk.gray('Fetching memories from ' + (config.provider === 'git' ? 'GitHub' : 'local storage') + '...'), spinner: 'dots' }).start();
20
25
 
21
- // Create a temporary staging directory to hold the downloaded files
22
26
  const stagingDir = path.join(os.tmpdir(), `memoir-restore-${Date.now()}`);
23
27
  await fs.ensureDir(stagingDir);
24
28
 
@@ -34,16 +38,26 @@ export async function restoreCommand() {
34
38
  return;
35
39
  }
36
40
 
41
+ spinner.stop();
42
+
37
43
  if (restored) {
38
- spinner.succeed(chalk.green('Restore complete! Your AI bots have their memories back.'));
44
+ console.log('\n' + boxen(
45
+ gradient.pastel(' Restored! ') + '\n\n' +
46
+ chalk.white('Your AI tools have their memories back.') + '\n' +
47
+ chalk.gray('They remember everything.'),
48
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
49
+ ) + '\n');
39
50
  } else {
40
- spinner.info(chalk.yellow('No memories were restored.'));
51
+ console.log('\n' + boxen(
52
+ chalk.yellow('No memories were restored.\n\n') +
53
+ chalk.gray('Run ') + chalk.cyan('memoir push') + chalk.gray(' on another machine first.'),
54
+ { padding: 1, borderStyle: 'round', borderColor: 'yellow' }
55
+ ) + '\n');
41
56
  }
42
57
 
43
58
  } catch (error) {
44
59
  spinner.fail(chalk.red('Restore failed: ') + error.message);
45
60
  } finally {
46
- // Clean up staging directory
47
61
  await fs.remove(stagingDir);
48
62
  }
49
63
  }
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import boxen from 'boxen';
5
+ import gradient from 'gradient-string';
6
+ import { getConfig } from '../config.js';
7
+ import { adapters } from '../adapters/index.js';
8
+
9
+ export async function statusCommand() {
10
+ const config = await getConfig();
11
+
12
+ console.log();
13
+
14
+ // Config status
15
+ let configLine;
16
+ if (config) {
17
+ const provider = config.provider === 'git'
18
+ ? chalk.cyan(config.gitRepo)
19
+ : chalk.cyan(config.localPath);
20
+ configLine = chalk.green('✔ Connected') + chalk.gray(' → ') + provider;
21
+ } else {
22
+ configLine = chalk.red('✖ Not configured') + chalk.gray(' → run ') + chalk.cyan('memoir init');
23
+ }
24
+
25
+ // Detected tools
26
+ const lines = [];
27
+ let detected = 0;
28
+
29
+ for (const adapter of adapters) {
30
+ let found = false;
31
+ if (adapter.customExtract) {
32
+ for (const file of adapter.files) {
33
+ if (await fs.pathExists(path.join(adapter.source, file))) {
34
+ found = true;
35
+ break;
36
+ }
37
+ }
38
+ } else {
39
+ found = await fs.pathExists(adapter.source);
40
+ }
41
+
42
+ if (found) {
43
+ lines.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
44
+ detected++;
45
+ } else {
46
+ lines.push(chalk.gray(' ○ ' + adapter.name));
47
+ }
48
+ }
49
+
50
+ const summary = detected > 0
51
+ ? chalk.white(`${detected} tool${detected !== 1 ? 's' : ''} ready to sync`)
52
+ : chalk.yellow('No AI tools detected');
53
+
54
+ console.log(boxen(
55
+ gradient.pastel(' memoir status ') + '\n\n' +
56
+ configLine + '\n\n' +
57
+ chalk.bold.white('AI Tools') + '\n' +
58
+ lines.join('\n') + '\n\n' +
59
+ chalk.gray('─'.repeat(30)) + '\n' +
60
+ summary,
61
+ { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
62
+ ) + '\n');
63
+ }
@@ -24,20 +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"', { cwd: stagingDir, stdio: 'ignore' });
32
- execSync('git config user.email "bot@memoir.dev"', { cwd: stagingDir, stdio: 'ignore' });
33
- execSync('git commit -m "chore: memoir backup"', { cwd: stagingDir, stdio: 'ignore' });
34
-
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
+
35
60
  spinner.text = `Pushing data to ${chalk.cyan(repoUrl)}...`;
36
- // We ignore stdio to prevent spam, but if it fails it will throw
37
- execSync(`git push --force ${repoUrl} main`, { cwd: stagingDir, stdio: 'ignore' });
38
-
61
+ execSync(`git push ${repoUrl} main`, { cwd: gitDir, stdio: 'ignore' });
62
+
39
63
  spinner.succeed(chalk.green('Sync complete! ') + chalk.gray('(Uploaded securely to GitHub)'));
40
64
  } catch (err) {
41
- 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);
42
68
  }
43
69
  }