memoir-cli 1.0.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 ADDED
@@ -0,0 +1,103 @@
1
+ <div align="center">
2
+
3
+ # 🧠 memoir
4
+ **Your AI Remembers Everything. Sync It Everywhere.**
5
+
6
+ [![npm version](https://img.shields.io/npm/v/memoir-cli.svg?style=flat-square)](https://npmjs.org/package/memoir-cli)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
8
+
9
+ *Never lose your AI's context again. Sync Gemini CLI, Claude Code, and more across all your devices instantly.*
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## πŸ’‘ The Problem
16
+
17
+ 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.
18
+
19
+ Then, you switch laptops. Or you want to share that setup with your team.
20
+
21
+ Suddenly, you're starting from scratch. Your AI's "memory" is trapped in hidden `.gemini` or `.claude` folders on a single machine.
22
+
23
+ ## πŸš€ The Solution
24
+
25
+ `memoir` is a zero-friction CLI tool that seamlessly extracts, backs up, and restores your AI's memory across any computer. You bring your own storage (a private GitHub repo or an iCloud/Dropbox folder), and `memoir` handles the rest safely and securely.
26
+
27
+ No locked-in SaaS, no lost context, no complex shell scripts.
28
+
29
+ ### Supported Integrations
30
+ - [x] **Gemini CLI**
31
+ - [x] **Claude CLI**
32
+ - [ ] *Cursor (Coming Soon)*
33
+ - [ ] *GitHub Copilot (Coming Soon)*
34
+
35
+ ---
36
+
37
+ ## πŸ› οΈ Installation
38
+
39
+ Install globally via npm so you can use it anywhere on your machine:
40
+
41
+ ```bash
42
+ npm install -g memoir-cli
43
+ ```
44
+
45
+ ## ⚑ Quick Start
46
+
47
+ ### 1. Initialize
48
+ Run the setup wizard. We'll help you securely link a private GitHub repository or a local sync folder.
49
+
50
+ ```bash
51
+ memoir init
52
+ ```
53
+
54
+ ### 2. Backup Your Memory
55
+ Just had a great session? Save your AI's learned context to the cloud:
56
+
57
+ ```bash
58
+ memoir push
59
+ # or simply use the alias:
60
+ memoir remember
61
+ ```
62
+
63
+ ### 3. Restore Anywhere
64
+ Got a new machine? Pull your brain down instantly:
65
+
66
+ ```bash
67
+ memoir restore
68
+ # or:
69
+ memoir pull
70
+ ```
71
+
72
+ ---
73
+
74
+ ## πŸ”’ Security First
75
+
76
+ Your AI memory files often sit right next to sensitive API keys and OAuth tokens. **`memoir` is designed to be paranoid.**
77
+
78
+ 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.
79
+
80
+ ---
81
+
82
+ ## πŸ—ΊοΈ Roadmap: The Future of Data Portability
83
+
84
+ We believe developers shouldn't be locked into a single AI ecosystem.
85
+
86
+ **Coming in v2.0: The Migration Engine**
87
+ Currently, `memoir` backs up your files exactly as they are. Soon, you will be able to run `memoir migrate --from claude --to gemini`. The CLI will automatically translate your Claude Code instructions into Gemini CLI facts, allowing you to fluidly swap AI providers without losing a drop of context.
88
+
89
+ ---
90
+
91
+ ## 🀝 Contributing
92
+
93
+ We welcome contributions! Whether it's adding an adapter for a new AI CLI, improving the UI, or helping build the Migration Engine.
94
+
95
+ 1. Fork the Project
96
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
97
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
98
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
99
+ 5. Open a Pull Request
100
+
101
+ ## πŸ“„ License
102
+
103
+ Distributed under the MIT License. See `LICENSE` for more information.
package/bin/memoir.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import chalk from 'chalk';
4
+ import boxen from 'boxen';
5
+ import gradient from 'gradient-string';
6
+ import { initCommand } from '../src/commands/init.js';
7
+ import { pushCommand } from '../src/commands/push.js';
8
+ import { restoreCommand } from '../src/commands/restore.js';
9
+
10
+ const VERSION = '1.0.0';
11
+
12
+ program
13
+ .name('memoir')
14
+ .description('Your AI remembers everything. Sync it everywhere.')
15
+ .version(VERSION);
16
+
17
+ program
18
+ .command('init')
19
+ .description('Initialize and configure memoir storage preferences')
20
+ .action(async () => {
21
+ try {
22
+ await initCommand();
23
+ } catch (err) {
24
+ console.error(chalk.red('\nβœ– Error during initialization:'), err.message);
25
+ process.exit(1);
26
+ }
27
+ });
28
+
29
+ program
30
+ .command('push')
31
+ .alias('remember')
32
+ .description('Sync your AI memory to your configured storage')
33
+ .action(async () => {
34
+ try {
35
+ await pushCommand();
36
+ } catch (err) {
37
+ console.error(chalk.red('\nβœ– Error during sync:'), err.message);
38
+ process.exit(1);
39
+ }
40
+ });
41
+
42
+ program
43
+ .command('restore')
44
+ .alias('pull')
45
+ .description('Restore your AI memory from your configured storage')
46
+ .action(async () => {
47
+ try {
48
+ await restoreCommand();
49
+ } catch (err) {
50
+ console.error(chalk.red('\nβœ– Error during restore:'), err.message);
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ program
56
+ .command('migrate')
57
+ .description('Migrate memory/context from one AI bot to another (e.g. Claude to Gemini)')
58
+ .action(() => {
59
+ 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!'),
64
+ { padding: 1, borderStyle: 'round', borderColor: 'yellow', align: 'center' }
65
+ ) + '\n');
66
+ });
67
+
68
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "memoir-cli",
3
+ "version": "1.0.0",
4
+ "description": "Your AI remembers everything. Sync it everywhere.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "memoir": "bin/memoir.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/memoir.js",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": [
15
+ "ai",
16
+ "cli",
17
+ "sync",
18
+ "memory",
19
+ "memoir",
20
+ "gemini",
21
+ "claude",
22
+ "backup",
23
+ "restore"
24
+ ],
25
+ "author": "",
26
+ "license": "ISC",
27
+ "dependencies": {
28
+ "boxen": "^7.1.1",
29
+ "chalk": "^5.3.0",
30
+ "commander": "^12.0.0",
31
+ "fs-extra": "^11.2.0",
32
+ "gradient-string": "^3.0.0",
33
+ "inquirer": "^9.2.15",
34
+ "open": "^11.0.0",
35
+ "ora": "^7.0.1"
36
+ }
37
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import chalk from 'chalk';
5
+
6
+ const home = os.homedir();
7
+
8
+ export const adapters = [
9
+ {
10
+ name: 'Gemini CLI',
11
+ source: path.join(home, '.gemini'),
12
+ filter: (src) => {
13
+ const basename = path.basename(src);
14
+ const ignored = ['.git', 'oauth_creds.json', 'google_accounts.json', 'tmp', 'history'];
15
+ return !ignored.includes(basename);
16
+ }
17
+ },
18
+ {
19
+ name: 'Claude CLI',
20
+ source: path.join(home, '.claude'),
21
+ filter: (src) => {
22
+ const basename = path.basename(src);
23
+ return !basename.endsWith('.key') && basename !== '.env';
24
+ }
25
+ }
26
+ ];
27
+
28
+ export async function extractMemories(stagingDir, spinner) {
29
+ let foundAny = false;
30
+
31
+ for (const adapter of adapters) {
32
+ if (await fs.pathExists(adapter.source)) {
33
+ spinner.text = `Found ${chalk.cyan(adapter.name)} memory... copying to staging`;
34
+ const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(' ', '-'));
35
+ await fs.ensureDir(dest);
36
+ await fs.copy(adapter.source, dest, { filter: adapter.filter });
37
+ foundAny = true;
38
+ }
39
+ }
40
+
41
+ return foundAny;
42
+ }
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import inquirer from 'inquirer';
6
+ import { adapters } from '../adapters/index.js';
7
+
8
+ export async function restoreMemories(sourceDir, spinner) {
9
+ let restoredAny = false;
10
+
11
+ for (const adapter of adapters) {
12
+ const backupDir = path.join(sourceDir, adapter.name.toLowerCase().replace(' ', '-'));
13
+
14
+ if (await fs.pathExists(backupDir)) {
15
+ spinner.stop();
16
+
17
+ console.log('\\n' + chalk.yellow(`⚠ Found backup for ${chalk.bold(adapter.name)}.`));
18
+ const { confirm } = await inquirer.prompt([
19
+ {
20
+ type: 'confirm',
21
+ name: 'confirm',
22
+ message: `Restore ${adapter.name} memory? This will overwrite existing configuration files!`,
23
+ default: false
24
+ }
25
+ ]);
26
+
27
+ spinner.start();
28
+
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 });
34
+ restoredAny = true;
35
+ } else {
36
+ spinner.info(chalk.gray(`Skipped restoring ${adapter.name}.`));
37
+ spinner.start();
38
+ }
39
+ }
40
+ }
41
+
42
+ return restoredAny;
43
+ }
@@ -0,0 +1,81 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import open from 'open';
4
+ import boxen from 'boxen';
5
+ import gradient from 'gradient-string';
6
+ import { saveConfig } from '../config.js';
7
+
8
+ export async function initCommand() {
9
+ 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',
14
+ borderColor: 'cyan',
15
+ align: 'center'
16
+ }));
17
+
18
+ console.log(chalk.gray("Let's configure where your AI knowledge will be safely stored.\\n"));
19
+
20
+ const answers = await inquirer.prompt([
21
+ {
22
+ type: 'list',
23
+ name: 'provider',
24
+ message: 'Choose your storage provider:',
25
+ 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
+ ]
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
+ },
37
+ {
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
43
+ }
44
+ ]);
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
+ }
50
+
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.');
61
+ }
62
+ return true;
63
+ }
64
+ }
65
+ ]);
66
+
67
+ const config = {
68
+ provider: answers.provider,
69
+ localPath: answers.localPath,
70
+ gitRepo: finalAnswers.gitRepo
71
+ };
72
+
73
+ await saveConfig(config);
74
+
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
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import ora from 'ora';
6
+ import { getConfig } from '../config.js';
7
+ import { extractMemories } from '../adapters/index.js';
8
+ import { syncToLocal, syncToGit } from '../providers/index.js';
9
+
10
+ export async function pushCommand() {
11
+ const config = await getConfig();
12
+
13
+ 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
+ return;
17
+ }
18
+
19
+ console.log();
20
+ const spinner = ora('Initializing AI memory sync...').start();
21
+
22
+ // Create a temporary staging directory
23
+ const stagingDir = path.join(os.tmpdir(), `memoir-staging-${Date.now()}`);
24
+ await fs.ensureDir(stagingDir);
25
+
26
+ try {
27
+ spinner.text = 'Scanning system for AI configurations...';
28
+
29
+ // Pass spinner so adapter can update it
30
+ const foundAny = await extractMemories(stagingDir, spinner);
31
+
32
+ if (!foundAny) {
33
+ spinner.warn(chalk.yellow('No supported AI memory folders found on this system.'));
34
+ return;
35
+ }
36
+
37
+ if (config.provider === 'local' || config.provider.includes('local')) {
38
+ await syncToLocal(config, stagingDir, spinner);
39
+ } else if (config.provider === 'git' || config.provider.includes('git')) {
40
+ await syncToGit(config, stagingDir, spinner);
41
+ } else {
42
+ spinner.fail(chalk.red(`Unknown provider: ${config.provider}`));
43
+ }
44
+ } catch (error) {
45
+ spinner.fail(chalk.red('Sync failed: ') + error.message);
46
+ } finally {
47
+ // Clean up staging directory
48
+ await fs.remove(stagingDir);
49
+ }
50
+ }
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import ora from 'ora';
6
+ import { getConfig } from '../config.js';
7
+ import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
8
+
9
+ export async function restoreCommand() {
10
+ const config = await getConfig();
11
+
12
+ 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
+ return;
16
+ }
17
+
18
+ console.log();
19
+ const spinner = ora('Initializing AI memory restore...').start();
20
+
21
+ // Create a temporary staging directory to hold the downloaded files
22
+ const stagingDir = path.join(os.tmpdir(), `memoir-restore-${Date.now()}`);
23
+ await fs.ensureDir(stagingDir);
24
+
25
+ try {
26
+ let restored = false;
27
+
28
+ if (config.provider === 'local' || config.provider.includes('local')) {
29
+ restored = await fetchFromLocal(config, stagingDir, spinner);
30
+ } else if (config.provider === 'git' || config.provider.includes('git')) {
31
+ restored = await fetchFromGit(config, stagingDir, spinner);
32
+ } else {
33
+ spinner.fail(chalk.red(`Unknown provider: ${config.provider}`));
34
+ return;
35
+ }
36
+
37
+ if (restored) {
38
+ spinner.succeed(chalk.green('Restore complete! Your AI bots have their memories back.'));
39
+ } else {
40
+ spinner.info(chalk.yellow('No memories were restored.'));
41
+ }
42
+
43
+ } catch (error) {
44
+ spinner.fail(chalk.red('Restore failed: ') + error.message);
45
+ } finally {
46
+ // Clean up staging directory
47
+ await fs.remove(stagingDir);
48
+ }
49
+ }
package/src/config.js ADDED
@@ -0,0 +1,18 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'memoir');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ export async function getConfig() {
9
+ if (await fs.pathExists(CONFIG_FILE)) {
10
+ return fs.readJson(CONFIG_FILE);
11
+ }
12
+ return null;
13
+ }
14
+
15
+ export async function saveConfig(config) {
16
+ await fs.ensureDir(CONFIG_DIR);
17
+ await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
18
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Entry point for the memoir library, if used programmatically
2
+ export { getConfig, saveConfig } from './config.js';
3
+ export { extractMemories } from './adapters/index.js';
4
+ export { syncToLocal, syncToGit } from './providers/index.js';
@@ -0,0 +1,42 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import chalk from 'chalk';
5
+ import { execSync } from 'child_process';
6
+
7
+ export async function syncToLocal(config, stagingDir, spinner) {
8
+ const destDir = config.localPath;
9
+ if (!destDir) throw new Error('Local path is not configured.');
10
+
11
+ // Expand tilde if user used it
12
+ const resolvedDest = destDir.replace(/^~/, os.homedir());
13
+
14
+ spinner.text = `Syncing files to local directory: ${chalk.cyan(resolvedDest)}`;
15
+ await fs.ensureDir(resolvedDest);
16
+
17
+ await fs.copy(stagingDir, resolvedDest);
18
+ spinner.succeed(chalk.green('Sync complete! ') + chalk.gray(`(Saved to ${resolvedDest})`));
19
+ }
20
+
21
+ export async function syncToGit(config, stagingDir, spinner) {
22
+ const repoUrl = config.gitRepo;
23
+ if (!repoUrl) throw new Error('Git repository is not configured.');
24
+
25
+ spinner.text = `Authenticating and syncing with Git remote: ${chalk.cyan(repoUrl)}`;
26
+
27
+ 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
+
34
+ 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
+
38
+ spinner.succeed(chalk.green('Sync complete! ') + chalk.gray('(Uploaded securely to GitHub)'));
39
+ } catch (err) {
40
+ throw new Error('Failed to push to git repository. Ensure your SSH keys are configured and the repository exists.');
41
+ }
42
+ }
@@ -0,0 +1,38 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { execSync } from 'child_process';
6
+ import { restoreMemories } from '../adapters/restore.js';
7
+
8
+ export async function fetchFromLocal(config, stagingDir, spinner) {
9
+ const sourceDir = config.localPath;
10
+ if (!sourceDir) throw new Error('Local path is not configured.');
11
+
12
+ const resolvedSource = sourceDir.replace(/^~/, os.homedir());
13
+
14
+ if (!(await fs.pathExists(resolvedSource))) {
15
+ throw new Error(`The backup directory does not exist: ${resolvedSource}`);
16
+ }
17
+
18
+ spinner.text = `Fetching data from local directory: ${chalk.cyan(resolvedSource)}`;
19
+ await fs.copy(resolvedSource, stagingDir);
20
+
21
+ return await restoreMemories(stagingDir, spinner);
22
+ }
23
+
24
+ export async function fetchFromGit(config, stagingDir, spinner) {
25
+ const repoUrl = config.gitRepo;
26
+ if (!repoUrl) throw new Error('Git repository is not configured.');
27
+
28
+ spinner.text = `Cloning memory from Git remote: ${chalk.cyan(repoUrl)}`;
29
+
30
+ try {
31
+ // Clone depth 1 to make it fast
32
+ execSync(`git clone --depth 1 ${repoUrl} .`, { cwd: stagingDir, stdio: 'ignore' });
33
+ } catch (err) {
34
+ throw new Error('Failed to pull from git repository. Ensure your SSH keys are configured and the repository is accessible.');
35
+ }
36
+
37
+ return await restoreMemories(stagingDir, spinner);
38
+ }