memoir-cli 1.2.0 → 1.3.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/bin/memoir.js CHANGED
@@ -8,16 +8,23 @@ 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
 
11
- const VERSION = '1.1.1';
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');
12
19
 
13
20
  program
14
21
  .name('memoir')
15
- .description('Your AI remembers everything. Sync it everywhere.')
22
+ .description(chalk.white('Sync your AI memory across every device.'))
16
23
  .version(VERSION);
17
24
 
18
25
  program
19
26
  .command('init')
20
- .description('Initialize and configure memoir storage preferences')
27
+ .description('Set up memoir with your storage provider')
21
28
  .action(async () => {
22
29
  try {
23
30
  await initCommand();
@@ -30,7 +37,7 @@ program
30
37
  program
31
38
  .command('push')
32
39
  .alias('remember')
33
- .description('Sync your AI memory to your configured storage')
40
+ .description('Back up your AI memory to the cloud')
34
41
  .action(async () => {
35
42
  try {
36
43
  await pushCommand();
@@ -43,7 +50,7 @@ program
43
50
  program
44
51
  .command('restore')
45
52
  .alias('pull')
46
- .description('Restore your AI memory from your configured storage')
53
+ .description('Restore your AI memory on this machine')
47
54
  .action(async () => {
48
55
  try {
49
56
  await restoreCommand();
@@ -55,7 +62,7 @@ program
55
62
 
56
63
  program
57
64
  .command('status')
58
- .description('Show detected AI tools and configuration status')
65
+ .description('See what AI tools are on this machine')
59
66
  .action(async () => {
60
67
  try {
61
68
  await statusCommand();
@@ -67,13 +74,13 @@ program
67
74
 
68
75
  program
69
76
  .command('migrate')
70
- .description('Migrate memory/context from one AI bot to another (e.g. Claude to Gemini)')
77
+ .description('Translate memory between AI providers')
71
78
  .action(() => {
72
79
  console.log('\n' + boxen(
73
- gradient.pastel('memoir migrate (Coming Soon)') + '\n\n' +
74
- chalk.white('We are actively developing the ability to instantly translate') + '\n' +
75
- chalk.white('and swap your context/memories between different AI providers.') + '\n\n' +
76
- 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.'),
77
84
  { padding: 1, borderStyle: 'round', borderColor: 'yellow', align: 'center' }
78
85
  ) + '\n');
79
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Your AI remembers everything. Sync it everywhere.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -5,6 +5,25 @@ import os from 'os';
5
5
  import inquirer from 'inquirer';
6
6
  import { adapters } from '../adapters/index.js';
7
7
 
8
+ async function copyMissing(src, dest, spinner) {
9
+ const entries = await fs.readdir(src, { withFileTypes: true });
10
+ for (const entry of entries) {
11
+ const srcPath = path.join(src, entry.name);
12
+ const destPath = path.join(dest, entry.name);
13
+
14
+ if (entry.isDirectory()) {
15
+ await fs.ensureDir(destPath);
16
+ await copyMissing(srcPath, destPath, spinner);
17
+ } else {
18
+ if (await fs.pathExists(destPath)) {
19
+ // skip existing files
20
+ } else {
21
+ await fs.copy(srcPath, destPath);
22
+ }
23
+ }
24
+ }
25
+ }
26
+
8
27
  export async function restoreMemories(sourceDir, spinner) {
9
28
  let restoredAny = false;
10
29
 
@@ -19,7 +38,7 @@ export async function restoreMemories(sourceDir, spinner) {
19
38
  {
20
39
  type: 'confirm',
21
40
  name: 'confirm',
22
- message: `Restore ${adapter.name} memory? This will overwrite existing configuration files!`,
41
+ message: `Restore ${adapter.name} memory? (only adds missing files, won't overwrite)`,
23
42
  default: false
24
43
  }
25
44
  ]);
@@ -28,16 +47,22 @@ export async function restoreMemories(sourceDir, spinner) {
28
47
 
29
48
  if (confirm) {
30
49
  if (adapter.customExtract) {
31
- // Restore individual files back to their original locations
50
+ // Restore individual files only add missing ones
32
51
  const files = await fs.readdir(backupDir);
33
52
  for (const file of files) {
34
53
  const dest = path.join(adapter.source, file);
35
- await fs.copy(path.join(backupDir, file), dest, { overwrite: true });
54
+ if (await fs.pathExists(dest)) {
55
+ spinner.info(chalk.gray(` Skipped ${file} (already exists)`));
56
+ spinner.start();
57
+ } else {
58
+ await fs.copy(path.join(backupDir, file), dest);
59
+ }
36
60
  }
37
61
  } else {
38
62
  spinner.text = `Restoring ${chalk.cyan(adapter.name)} memory to ${adapter.source}...`;
39
63
  await fs.ensureDir(adapter.source);
40
- await fs.copy(backupDir, adapter.source, { overwrite: true });
64
+ // Merge — only copy files that don't already exist
65
+ await copyMissing(backupDir, adapter.source, spinner);
41
66
  }
42
67
  restoredAny = true;
43
68
  } else {
@@ -4,78 +4,88 @@ import open from 'open';
4
4
  import boxen from 'boxen';
5
5
  import gradient from 'gradient-string';
6
6
  import { saveConfig } from '../config.js';
7
+ import { pushCommand } from './push.js';
8
+ import { restoreCommand } from './restore.js';
7
9
 
8
10
  export async function initCommand() {
9
11
  const title = gradient.pastel.multiline('memoir \\nYour AI Remembers Everything');
10
- console.log('\\n' + boxen(title, {
11
- padding: 1,
12
- margin: 1,
13
- borderStyle: 'round',
12
+ console.log('\\n' + boxen(title, {
13
+ padding: 1,
14
+ margin: 1,
15
+ borderStyle: 'round',
14
16
  borderColor: 'cyan',
15
17
  align: 'center'
16
18
  }));
17
19
 
18
- console.log(chalk.gray("Let's configure where your AI knowledge will be safely stored.\\n"));
20
+ console.log(chalk.gray("Let's get your AI memory set up.\\n"));
19
21
 
20
- const answers = await inquirer.prompt([
22
+ const { direction } = await inquirer.prompt([
21
23
  {
22
24
  type: 'list',
23
- name: 'provider',
24
- message: 'Choose your storage provider:',
25
+ name: 'direction',
26
+ message: 'What do you want to do?',
25
27
  choices: [
26
- { name: '☁️ Git Repository ' + chalk.gray('(GitHub, GitLab - Best for syncing across computers)'), value: 'git' },
27
- { name: '📂 Local Directory ' + chalk.gray('(Dropbox, iCloud - Best for local backups)'), value: 'local' }
28
+ { name: '⬆️ Upload ' + chalk.gray('(backup this machine\'s AI memory)'), value: 'upload' },
29
+ { name: '⬇️ Download ' + chalk.gray('(restore AI memory to this machine)'), value: 'download' }
28
30
  ]
29
- },
30
- {
31
- type: 'input',
32
- name: 'localPath',
33
- message: 'Enter the full path to your sync directory ' + chalk.gray('(e.g., ~/Dropbox/memoir):'),
34
- when: (answers) => answers.provider === 'local',
35
- validate: (input) => input.trim() !== '' ? true : chalk.red('✖ Path is required')
36
- },
31
+ }
32
+ ]);
33
+
34
+ const { provider } = await inquirer.prompt([
37
35
  {
38
- type: 'confirm',
39
- name: 'openBrowser',
40
- message: 'Need to create an empty GitHub repository right now?',
41
- when: (answers) => answers.provider === 'git',
42
- default: false
36
+ type: 'list',
37
+ name: 'provider',
38
+ message: 'Where do you want to store it?',
39
+ choices: [
40
+ { name: '☁️ GitHub ' + chalk.gray('(sync across computers)'), value: 'git' },
41
+ { name: '📂 Local Directory ' + chalk.gray('(Dropbox, iCloud, etc.)'), value: 'local' }
42
+ ]
43
43
  }
44
44
  ]);
45
45
 
46
- if (answers.openBrowser) {
47
- console.log(chalk.cyan('\\n↗ Opening GitHub... Create an empty private repository, then return here.\\n'));
48
- await open('https://github.com/new');
49
- }
46
+ let config = { provider };
50
47
 
51
- const finalAnswers = await inquirer.prompt([
52
- {
53
- type: 'input',
54
- name: 'gitRepo',
55
- message: 'Repository URL ' + chalk.gray('(e.g., git@github.com:username/ai-memory.git):'),
56
- when: () => answers.provider === 'git',
57
- validate: (input) => {
58
- if (input.trim() === '') return chalk.red('✖ Repo URL is required');
59
- if (!input.includes('github.com') && !input.includes('gitlab.com')) {
60
- return chalk.yellow('⚠ Warning: This does not look like a standard GitHub/GitLab URL. Please verify.');
48
+ if (provider === 'local') {
49
+ const { localPath } = await inquirer.prompt([
50
+ {
51
+ type: 'input',
52
+ name: 'localPath',
53
+ message: 'Path to sync directory ' + chalk.gray('(e.g., ~/Dropbox/memoir):'),
54
+ validate: (input) => input.trim() !== '' ? true : chalk.red('✖ Path is required')
55
+ }
56
+ ]);
57
+ config.localPath = localPath;
58
+ } else {
59
+ const { repoInput } = await inquirer.prompt([
60
+ {
61
+ type: 'input',
62
+ name: 'repoInput',
63
+ message: 'GitHub repo ' + chalk.gray('(e.g., camgitt/brain):'),
64
+ validate: (input) => {
65
+ if (input.trim() === '') return chalk.red('✖ Repo is required');
66
+ return true;
61
67
  }
62
- return true;
63
68
  }
64
- }
65
- ]);
69
+ ]);
66
70
 
67
- const config = {
68
- provider: answers.provider,
69
- localPath: answers.localPath,
70
- gitRepo: finalAnswers.gitRepo
71
- };
71
+ // Accept shorthand like "camgitt/brain" or full URLs
72
+ let gitRepo = repoInput.trim();
73
+ if (!gitRepo.includes('github.com') && !gitRepo.includes('gitlab.com')) {
74
+ gitRepo = `https://github.com/${gitRepo}.git`;
75
+ }
76
+ config.gitRepo = gitRepo;
77
+ }
72
78
 
73
79
  await saveConfig(config);
74
80
 
75
- console.log('\\n' + boxen(
76
- chalk.green('✔ Configuration saved successfully!') + '\\n\\n' +
77
- chalk.white('To backup your memory, run:') + '\\n' +
78
- chalk.cyan.bold('memoir push'),
79
- { padding: 1, borderStyle: 'single', borderColor: 'green' }
80
- ) + '\\n');
81
+ console.log('\\n' + chalk.green('✔ Configuration saved!'));
82
+
83
+ // Immediately run the chosen action
84
+ if (direction === 'upload') {
85
+ console.log(chalk.cyan('\\n↗ Uploading your AI memory...\\n'));
86
+ await pushCommand();
87
+ } else {
88
+ console.log(chalk.cyan('\\n↙ Downloading your AI memory...\\n'));
89
+ await restoreCommand();
90
+ }
81
91
  }
@@ -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
  }
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
+ import boxen from 'boxen';
5
+ import gradient from 'gradient-string';
4
6
  import { getConfig } from '../config.js';
5
7
  import { adapters } from '../adapters/index.js';
6
8
 
@@ -10,47 +12,52 @@ export async function statusCommand() {
10
12
  console.log();
11
13
 
12
14
  // Config status
15
+ let configLine;
13
16
  if (config) {
14
- const provider = config.provider === 'git' ? `Git (${config.gitRepo})` : `Local (${config.localPath})`;
15
- console.log(chalk.green('✔ Configured') + chalk.gray(` — ${provider}`));
17
+ const provider = config.provider === 'git'
18
+ ? chalk.cyan(config.gitRepo)
19
+ : chalk.cyan(config.localPath);
20
+ configLine = chalk.green('✔ Connected') + chalk.gray(' → ') + provider;
16
21
  } else {
17
- console.log(chalk.red('✖ Not configured') + chalk.gray(' run memoir init'));
18
- console.log();
19
- return;
22
+ configLine = chalk.red('✖ Not configured') + chalk.gray(' run ') + chalk.cyan('memoir init');
20
23
  }
21
24
 
22
- console.log();
23
-
24
25
  // Detected tools
25
- console.log(chalk.bold('Detected AI tools:\n'));
26
-
26
+ const lines = [];
27
27
  let detected = 0;
28
+
28
29
  for (const adapter of adapters) {
30
+ let found = false;
29
31
  if (adapter.customExtract) {
30
- let hasFiles = false;
31
32
  for (const file of adapter.files) {
32
33
  if (await fs.pathExists(path.join(adapter.source, file))) {
33
- hasFiles = true;
34
+ found = true;
34
35
  break;
35
36
  }
36
37
  }
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
38
  } 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
- }
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));
50
47
  }
51
48
  }
52
49
 
53
- console.log();
54
- console.log(chalk.white(`${detected} tool${detected !== 1 ? 's' : ''} detected on this machine.`));
55
- console.log();
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');
56
63
  }