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 +103 -0
- package/bin/memoir.js +68 -0
- package/package.json +37 -0
- package/src/adapters/index.js +42 -0
- package/src/adapters/restore.js +43 -0
- package/src/commands/init.js +81 -0
- package/src/commands/push.js +50 -0
- package/src/commands/restore.js +49 -0
- package/src/config.js +18 -0
- package/src/index.js +4 -0
- package/src/providers/index.js +42 -0
- package/src/providers/restore.js +38 -0
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
|
+
[](https://npmjs.org/package/memoir-cli)
|
|
7
|
+
[](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,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
|
+
}
|