memoir-cli 3.3.0 → 3.3.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/bin/memoir.js +50 -15
- package/package.json +1 -1
- package/src/cloud/auth.js +58 -1
- package/src/commands/init.js +79 -50
- package/src/commands/login.js +41 -1
package/bin/memoir.js
CHANGED
|
@@ -14,7 +14,7 @@ import { migrateCommand } from '../src/commands/migrate.js';
|
|
|
14
14
|
import { snapshotCommand } from '../src/commands/snapshot.js';
|
|
15
15
|
import { resumeCommand } from '../src/commands/resume.js';
|
|
16
16
|
import { profileListCommand, profileCreateCommand, profileSwitchCommand, profileDeleteCommand } from '../src/commands/profile.js';
|
|
17
|
-
import { loginCommand, logoutCommand, forgotPasswordCommand } from '../src/commands/login.js';
|
|
17
|
+
import { loginCommand, logoutCommand, forgotPasswordCommand, deleteAccountCommand } from '../src/commands/login.js';
|
|
18
18
|
import { cloudPushCommand, cloudRestoreCommand } from '../src/commands/cloud.js';
|
|
19
19
|
import { shareCommand } from '../src/commands/share.js';
|
|
20
20
|
import { historyCommand } from '../src/commands/history.js';
|
|
@@ -97,9 +97,16 @@ program
|
|
|
97
97
|
program
|
|
98
98
|
.command('init')
|
|
99
99
|
.description('Set up memoir with your storage provider')
|
|
100
|
-
.
|
|
100
|
+
.option('--direction <direction>', 'Upload or download (upload, download)')
|
|
101
|
+
.option('--provider <provider>', 'Storage provider (git, local)')
|
|
102
|
+
.option('--local-path <path>', 'Local folder path (for local provider)')
|
|
103
|
+
.option('--username <name>', 'GitHub username (for git provider)')
|
|
104
|
+
.option('--repo <name>', 'GitHub repo name (for git provider)')
|
|
105
|
+
.option('--encrypt', 'Enable E2E encryption')
|
|
106
|
+
.option('--no-encrypt', 'Disable E2E encryption')
|
|
107
|
+
.action(async (options) => {
|
|
101
108
|
try {
|
|
102
|
-
await initCommand();
|
|
109
|
+
await initCommand(options);
|
|
103
110
|
} catch (err) {
|
|
104
111
|
console.error(chalk.red('\n✖ Error during initialization:'), err.message);
|
|
105
112
|
process.exit(1);
|
|
@@ -294,7 +301,9 @@ program
|
|
|
294
301
|
program
|
|
295
302
|
.command('encrypt')
|
|
296
303
|
.description('Toggle E2E encryption for your backups')
|
|
297
|
-
.
|
|
304
|
+
.option('--on', 'Enable encryption without prompting')
|
|
305
|
+
.option('--off', 'Disable encryption without prompting')
|
|
306
|
+
.action(async (options) => {
|
|
298
307
|
try {
|
|
299
308
|
const { getConfig, getRawConfig, saveConfig, migrateConfigToV2 } = await import('../src/config.js');
|
|
300
309
|
const config = await getConfig();
|
|
@@ -304,24 +313,34 @@ program
|
|
|
304
313
|
}
|
|
305
314
|
const current = config.encrypt || false;
|
|
306
315
|
console.log(chalk.white(`\n Encryption is currently: ${current ? chalk.green('ON') : chalk.red('OFF')}`));
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
316
|
+
|
|
317
|
+
let newValue;
|
|
318
|
+
if (options.on) {
|
|
319
|
+
newValue = true;
|
|
320
|
+
} else if (options.off) {
|
|
321
|
+
newValue = false;
|
|
322
|
+
} else {
|
|
323
|
+
const inquirer = (await import('inquirer')).default;
|
|
324
|
+
const { toggle } = await inquirer.prompt([{
|
|
325
|
+
type: 'confirm',
|
|
326
|
+
name: 'toggle',
|
|
327
|
+
message: current ? 'Disable encryption?' : 'Enable encryption?',
|
|
328
|
+
default: !current
|
|
329
|
+
}]);
|
|
330
|
+
newValue = toggle ? !current : current;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (newValue !== current) {
|
|
315
334
|
let raw = await getRawConfig();
|
|
316
335
|
if (!raw.version || raw.version < 2) raw = migrateConfigToV2(raw);
|
|
317
336
|
const profileName = raw.activeProfile || 'default';
|
|
318
337
|
if (raw.profiles?.[profileName]) {
|
|
319
|
-
raw.profiles[profileName].encrypt =
|
|
338
|
+
raw.profiles[profileName].encrypt = newValue;
|
|
320
339
|
} else {
|
|
321
|
-
raw.encrypt =
|
|
340
|
+
raw.encrypt = newValue;
|
|
322
341
|
}
|
|
323
342
|
await saveConfig(raw);
|
|
324
|
-
console.log(chalk.green(`\n ✔ Encryption ${
|
|
343
|
+
console.log(chalk.green(`\n ✔ Encryption ${newValue ? 'enabled' : 'disabled'}. Next push will ${newValue ? 'encrypt' : 'skip encryption'}.\n`));
|
|
325
344
|
}
|
|
326
345
|
} catch (err) {
|
|
327
346
|
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
@@ -386,6 +405,22 @@ program
|
|
|
386
405
|
}
|
|
387
406
|
});
|
|
388
407
|
|
|
408
|
+
// Account management
|
|
409
|
+
const account = program.command('account').description('Manage your memoir account');
|
|
410
|
+
|
|
411
|
+
account
|
|
412
|
+
.command('delete')
|
|
413
|
+
.description('Permanently delete your account and all cloud data')
|
|
414
|
+
.option('--confirm', 'Skip interactive prompt (Node 25 workaround)')
|
|
415
|
+
.action(async (options) => {
|
|
416
|
+
try {
|
|
417
|
+
await deleteAccountCommand(options);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
389
424
|
// Cloud sync
|
|
390
425
|
const cloud = program.command('cloud').description('Cloud backup and restore (Pro)');
|
|
391
426
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Persistent memory for AI coding tools via MCP. Your AI remembers across sessions, tools, and machines. Works with Claude, Cursor, Gemini, Windsurf, and 7 more tools.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cloud/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import { SUPABASE_URL, SUPABASE_ANON_KEY } from './constants.js';
|
|
4
|
+
import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET } from './constants.js';
|
|
5
5
|
|
|
6
6
|
const isWin = process.platform === 'win32';
|
|
7
7
|
const configDir = isWin
|
|
@@ -131,4 +131,61 @@ export async function resetPassword(email) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export async function deleteAccount(session) {
|
|
135
|
+
// 1. Delete all backups (storage files + metadata rows)
|
|
136
|
+
const backupsRes = await fetch(
|
|
137
|
+
`${SUPABASE_URL}/rest/v1/backups?select=*&user_id=eq.${session.user.id}`,
|
|
138
|
+
{
|
|
139
|
+
headers: {
|
|
140
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
141
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (backupsRes.ok) {
|
|
147
|
+
const backups = await backupsRes.json();
|
|
148
|
+
for (const backup of backups) {
|
|
149
|
+
// Delete storage object
|
|
150
|
+
await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}/${backup.storage_path}`, {
|
|
151
|
+
method: 'DELETE',
|
|
152
|
+
headers: {
|
|
153
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
154
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Delete metadata row
|
|
159
|
+
await fetch(`${SUPABASE_URL}/rest/v1/backups?id=eq.${backup.id}`, {
|
|
160
|
+
method: 'DELETE',
|
|
161
|
+
headers: {
|
|
162
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
163
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. Delete shared links
|
|
170
|
+
await fetch(`${SUPABASE_URL}/rest/v1/shared_links?user_id=eq.${session.user.id}`, {
|
|
171
|
+
method: 'DELETE',
|
|
172
|
+
headers: {
|
|
173
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
174
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 3. Delete subscription
|
|
179
|
+
await fetch(`${SUPABASE_URL}/rest/v1/subscriptions?user_id=eq.${session.user.id}`, {
|
|
180
|
+
method: 'DELETE',
|
|
181
|
+
headers: {
|
|
182
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
183
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 4. Sign out locally
|
|
188
|
+
await logout();
|
|
189
|
+
}
|
|
190
|
+
|
|
134
191
|
export { AUTH_FILE, supaFetch };
|
package/src/commands/init.js
CHANGED
|
@@ -23,7 +23,7 @@ function getGitHubUsername() {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export async function initCommand() {
|
|
26
|
+
export async function initCommand(options = {}) {
|
|
27
27
|
console.log('');
|
|
28
28
|
console.log(boxen(
|
|
29
29
|
gradient.pastel('memoir') + '\n' +
|
|
@@ -34,57 +34,80 @@ export async function initCommand() {
|
|
|
34
34
|
|
|
35
35
|
const detectedUser = getGitHubUsername();
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
type: 'list',
|
|
40
|
-
name: 'direction',
|
|
41
|
-
message: 'Upload or download?',
|
|
42
|
-
choices: [
|
|
43
|
-
{ name: 'Upload — back up this machine', value: 'upload' },
|
|
44
|
-
{ name: 'Download — restore from backup', value: 'download' }
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: 'list',
|
|
49
|
-
name: 'provider',
|
|
50
|
-
message: (answers) => answers.direction === 'upload' ? 'Back up to?' : 'Restore from?',
|
|
51
|
-
choices: [
|
|
52
|
-
{ name: 'GitHub', value: 'git' },
|
|
53
|
-
{ name: 'Local folder', value: 'local' }
|
|
54
|
-
]
|
|
55
|
-
}
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
let config = { provider };
|
|
37
|
+
let direction, provider;
|
|
59
38
|
|
|
60
|
-
if (provider
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
name: 'localPath',
|
|
65
|
-
message: msg,
|
|
66
|
-
validate: (input) => input.trim() ? true : 'Required'
|
|
67
|
-
}]);
|
|
68
|
-
config.localPath = localPath;
|
|
39
|
+
if (options.provider) {
|
|
40
|
+
// Non-interactive: use flags directly
|
|
41
|
+
provider = options.provider;
|
|
42
|
+
direction = options.direction || 'upload';
|
|
69
43
|
} else {
|
|
44
|
+
// Interactive: prompt the user
|
|
70
45
|
const answers = await inquirer.prompt([
|
|
71
46
|
{
|
|
72
|
-
type: '
|
|
73
|
-
name: '
|
|
74
|
-
message: '
|
|
75
|
-
|
|
76
|
-
|
|
47
|
+
type: 'list',
|
|
48
|
+
name: 'direction',
|
|
49
|
+
message: 'Upload or download?',
|
|
50
|
+
choices: [
|
|
51
|
+
{ name: 'Upload — back up this machine', value: 'upload' },
|
|
52
|
+
{ name: 'Download — restore from backup', value: 'download' }
|
|
53
|
+
]
|
|
77
54
|
},
|
|
78
55
|
{
|
|
79
|
-
type: '
|
|
80
|
-
name: '
|
|
81
|
-
message: '
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
type: 'list',
|
|
57
|
+
name: 'provider',
|
|
58
|
+
message: (a) => a.direction === 'upload' ? 'Back up to?' : 'Restore from?',
|
|
59
|
+
choices: [
|
|
60
|
+
{ name: 'GitHub', value: 'git' },
|
|
61
|
+
{ name: 'Local folder', value: 'local' }
|
|
62
|
+
]
|
|
84
63
|
}
|
|
85
64
|
]);
|
|
86
|
-
|
|
87
|
-
|
|
65
|
+
direction = answers.direction;
|
|
66
|
+
provider = answers.provider;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let config = { provider };
|
|
70
|
+
|
|
71
|
+
if (provider === 'local') {
|
|
72
|
+
let localPath;
|
|
73
|
+
if (options.localPath) {
|
|
74
|
+
localPath = options.localPath;
|
|
75
|
+
} else {
|
|
76
|
+
const msg = direction === 'upload' ? 'Save to:' : 'Backup folder:';
|
|
77
|
+
const answers = await inquirer.prompt([{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'localPath',
|
|
80
|
+
message: msg,
|
|
81
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
82
|
+
}]);
|
|
83
|
+
localPath = answers.localPath;
|
|
84
|
+
}
|
|
85
|
+
config.localPath = localPath;
|
|
86
|
+
} else {
|
|
87
|
+
let username, repo;
|
|
88
|
+
if (options.username) {
|
|
89
|
+
username = options.username.trim();
|
|
90
|
+
repo = (options.repo || 'ai-memory').trim();
|
|
91
|
+
} else {
|
|
92
|
+
const answers = await inquirer.prompt([
|
|
93
|
+
{
|
|
94
|
+
type: 'input',
|
|
95
|
+
name: 'username',
|
|
96
|
+
message: 'GitHub username:',
|
|
97
|
+
default: detectedUser || undefined,
|
|
98
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'input',
|
|
102
|
+
name: 'repo',
|
|
103
|
+
message: 'Repo name:',
|
|
104
|
+
default: 'ai-memory',
|
|
105
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
106
|
+
}
|
|
107
|
+
]);
|
|
108
|
+
username = answers.username.trim();
|
|
109
|
+
repo = answers.repo.trim();
|
|
110
|
+
}
|
|
88
111
|
|
|
89
112
|
config.gitRepo = `https://github.com/${username}/${repo}.git`;
|
|
90
113
|
console.log(chalk.gray(` → ${config.gitRepo}`));
|
|
@@ -109,12 +132,18 @@ export async function initCommand() {
|
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
// Ask about encryption
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
let encrypt;
|
|
136
|
+
if (options.encrypt !== undefined) {
|
|
137
|
+
encrypt = options.encrypt;
|
|
138
|
+
} else {
|
|
139
|
+
const answers = await inquirer.prompt([{
|
|
140
|
+
type: 'confirm',
|
|
141
|
+
name: 'encrypt',
|
|
142
|
+
message: 'Enable E2E encryption? (protects your data even if backup is compromised)',
|
|
143
|
+
default: true
|
|
144
|
+
}]);
|
|
145
|
+
encrypt = answers.encrypt;
|
|
146
|
+
}
|
|
118
147
|
config.encrypt = encrypt;
|
|
119
148
|
|
|
120
149
|
if (encrypt) {
|
package/src/commands/login.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import boxen from 'boxen';
|
|
3
3
|
import gradient from 'gradient-string';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import { signIn, signUp, saveSession, getSession, logout, getSubscription, resetPassword } from '../cloud/auth.js';
|
|
5
|
+
import { signIn, signUp, saveSession, getSession, logout, getSubscription, resetPassword, deleteAccount } from '../cloud/auth.js';
|
|
6
6
|
|
|
7
7
|
export async function loginCommand(options = {}) {
|
|
8
8
|
// Check if already logged in
|
|
@@ -131,3 +131,43 @@ export async function logoutCommand() {
|
|
|
131
131
|
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
132
132
|
) + '\n');
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
export async function deleteAccountCommand(options = {}) {
|
|
136
|
+
const session = await getSession();
|
|
137
|
+
if (!session) {
|
|
138
|
+
console.log('\n' + boxen(
|
|
139
|
+
chalk.red('✖ Not logged in. Run ') + chalk.cyan('memoir login') + chalk.red(' first.'),
|
|
140
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
141
|
+
) + '\n');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!options.confirm) {
|
|
146
|
+
const answer = await inquirer.prompt([{
|
|
147
|
+
type: 'input',
|
|
148
|
+
name: 'confirmation',
|
|
149
|
+
message: 'Type DELETE to confirm account deletion:',
|
|
150
|
+
}]);
|
|
151
|
+
|
|
152
|
+
if (answer.confirmation !== 'DELETE') {
|
|
153
|
+
console.log('\n' + chalk.gray(' Account deletion cancelled.') + '\n');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await deleteAccount(session);
|
|
160
|
+
|
|
161
|
+
console.log('\n' + boxen(
|
|
162
|
+
gradient.pastel(' memoir cloud ') + '\n\n' +
|
|
163
|
+
chalk.green('✔ Account deleted.') + '\n' +
|
|
164
|
+
chalk.gray('All backups, shared links, and data have been removed.'),
|
|
165
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
166
|
+
) + '\n');
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.log('\n' + boxen(
|
|
169
|
+
chalk.red('✖ ' + error.message),
|
|
170
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
171
|
+
) + '\n');
|
|
172
|
+
}
|
|
173
|
+
}
|