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 +6 -3
- package/bin/memoir.js +14 -1
- package/package.json +1 -1
- package/src/adapters/index.js +72 -4
- package/src/adapters/restore.js +16 -8
- package/src/commands/status.js +56 -0
- package/src/config.js +3 -1
- package/src/providers/index.js +37 -10
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
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
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.
|
|
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
package/src/adapters/index.js
CHANGED
|
@@ -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 (
|
|
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
|
}
|
package/src/adapters/restore.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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() {
|
package/src/providers/index.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
}
|