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 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
- .action(async () => {
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
- .action(async () => {
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
- const inquirer = (await import('inquirer')).default;
308
- const { toggle } = await inquirer.prompt([{
309
- type: 'confirm',
310
- name: 'toggle',
311
- message: current ? 'Disable encryption?' : 'Enable encryption?',
312
- default: !current
313
- }]);
314
- if (toggle !== current) {
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 = !current;
338
+ raw.profiles[profileName].encrypt = newValue;
320
339
  } else {
321
- raw.encrypt = !current;
340
+ raw.encrypt = newValue;
322
341
  }
323
342
  await saveConfig(raw);
324
- console.log(chalk.green(`\n ✔ Encryption ${!current ? 'enabled' : 'disabled'}. Next push will ${!current ? 'encrypt' : 'skip encryption'}.\n`));
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.0",
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 };
@@ -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
- const { direction, provider } = await inquirer.prompt([
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 === 'local') {
61
- const msg = direction === 'upload' ? 'Save to:' : 'Backup folder:';
62
- const { localPath } = await inquirer.prompt([{
63
- type: 'input',
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: 'input',
73
- name: 'username',
74
- message: 'GitHub username:',
75
- default: detectedUser || undefined,
76
- validate: (input) => input.trim() ? true : 'Required'
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: 'input',
80
- name: 'repo',
81
- message: 'Repo name:',
82
- default: 'ai-memory',
83
- validate: (input) => input.trim() ? true : 'Required'
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
- const username = answers.username.trim();
87
- const repo = answers.repo.trim();
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
- const { encrypt } = await inquirer.prompt([{
113
- type: 'confirm',
114
- name: 'encrypt',
115
- message: 'Enable E2E encryption? (protects your data even if backup is compromised)',
116
- default: true
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) {
@@ -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
+ }