memoir-cli 3.2.2 → 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.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something isn't working
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ **What happened?**
10
+ A clear description of the bug.
11
+
12
+ **Steps to reproduce**
13
+ 1. Run `memoir ...`
14
+ 2. See error
15
+
16
+ **Expected behavior**
17
+ What you expected to happen.
18
+
19
+ **Environment**
20
+ - OS: [e.g. macOS 15, Windows 11, Ubuntu 24]
21
+ - Node: [e.g. 20.11.0]
22
+ - memoir version: [e.g. 3.2.2]
23
+ - AI tools: [e.g. Claude Code, Cursor]
24
+
25
+ **Logs / screenshots**
26
+ Paste any error output here.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for memoir
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ **What would you like?**
10
+ A clear description of the feature.
11
+
12
+ **Why?**
13
+ What problem does this solve for you?
14
+
15
+ **Alternatives considered**
16
+ Any workarounds you've tried.
@@ -0,0 +1,47 @@
1
+ # Contributing to memoir
2
+
3
+ Thanks for your interest in contributing! memoir is open source and welcomes contributions.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ git clone https://github.com/camgitt/memoir.git
9
+ cd memoir
10
+ npm install
11
+ node bin/memoir.js status
12
+ ```
13
+
14
+ ## What to work on
15
+
16
+ - **New tool adapters** — add support for more AI tools in `src/tools/`
17
+ - **MCP improvements** — enhance the MCP server in `src/mcp.js`
18
+ - **Bug fixes** — check [open issues](https://github.com/camgitt/memoir/issues)
19
+ - **Documentation** — improve README, add examples
20
+
21
+ ## Submitting changes
22
+
23
+ 1. Fork the repo
24
+ 2. Create a branch (`git checkout -b feature/my-feature`)
25
+ 3. Make your changes
26
+ 4. Test locally: `npm test`
27
+ 5. Commit and push
28
+ 6. Open a PR with a clear description
29
+
30
+ ## Code style
31
+
32
+ - ES modules (`import`/`export`)
33
+ - No TypeScript (plain JS)
34
+ - Keep dependencies minimal
35
+
36
+ ## Adding a tool adapter
37
+
38
+ See `src/tools/claude.js` for an example. Each adapter exports:
39
+ - `name` — display name
40
+ - `icon` — emoji
41
+ - `source` — path to the tool's config directory
42
+ - `files` — specific files to sync (if `customExtract` is true)
43
+ - `filter` — function to include/exclude files
44
+
45
+ ## Questions?
46
+
47
+ Open an issue or start a discussion.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 camgitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -6,14 +6,17 @@
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/memoir-cli.svg?style=flat-square&color=7c6ef0)](https://npmjs.org/package/memoir-cli)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/memoir-cli.svg?style=flat-square&color=7c6ef0)](https://npmjs.org/package/memoir-cli)
9
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
9
+ [![GitHub stars](https://img.shields.io/github/stars/camgitt/memoir?style=flat-square&color=7c6ef0)](https://github.com/camgitt/memoir/stargazers)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
10
11
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](https://nodejs.org)
11
12
 
12
13
  Your AI forgets everything between sessions. memoir gives it long-term memory via MCP.
13
14
 
14
- Works with Claude Code, Cursor, Windsurf, Gemini, and 7 more tools.
15
+ **11 tools supported** • **E2E encrypted** • **Cross-platform** • **Open source**
15
16
 
16
- [Website](https://memoir.sh) • [npm](https://npmjs.org/package/memoir-cli) • [Blog](https://memoir.sh/blog)
17
+ Works with Claude Code, Cursor, Windsurf, Gemini, Copilot, Codex, ChatGPT, Aider, Zed, Cline, and Continue.dev.
18
+
19
+ [Website](https://memoir.sh) • [npm](https://npmjs.org/package/memoir-cli) • [Blog](https://memoir.sh/blog) • [Contributing](CONTRIBUTING.md)
17
20
 
18
21
  <br />
19
22
 
@@ -44,6 +47,27 @@ claude: Based on your previous sessions: this project uses JWT auth
44
47
 
45
48
  No re-explaining. memoir remembered.
46
49
 
50
+ ## Architecture
51
+
52
+ ```mermaid
53
+ graph LR
54
+ A[Claude Code] --> M[memoir MCP]
55
+ B[Cursor] --> M
56
+ C[Gemini CLI] --> M
57
+ D[Windsurf] --> M
58
+ E[+ 7 more] --> M
59
+
60
+ M --> R[recall / remember / list / read]
61
+ R --> S[(Local Memory Store)]
62
+
63
+ S --> P[memoir push]
64
+ P --> G[GitHub / Cloud]
65
+ G --> Q[memoir restore]
66
+ Q --> S2[(New Machine)]
67
+ ```
68
+
69
+ memoir runs as a local MCP server inside your AI tools. Your AI can search, save, and recall memories automatically. Push to sync across machines.
70
+
47
71
  ## Quick Start
48
72
 
49
73
  ```bash
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 } 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);
@@ -348,9 +367,12 @@ program
348
367
  program
349
368
  .command('login')
350
369
  .description('Sign in to memoir cloud')
351
- .action(async () => {
370
+ .option('--email <email>', 'Email address (skip interactive prompt)')
371
+ .option('--password <password>', 'Password (skip interactive prompt)')
372
+ .option('--signup', 'Create a new account instead of signing in')
373
+ .action(async (options) => {
352
374
  try {
353
- await loginCommand();
375
+ await loginCommand(options);
354
376
  } catch (err) {
355
377
  console.error(chalk.red('\n✖ Error:'), err.message);
356
378
  process.exit(1);
@@ -369,6 +391,36 @@ program
369
391
  }
370
392
  });
371
393
 
394
+ program
395
+ .command('forgot-password')
396
+ .alias('reset-password')
397
+ .description('Send a password reset email')
398
+ .option('--email <email>', 'Email address (skip interactive prompt)')
399
+ .action(async (options) => {
400
+ try {
401
+ await forgotPasswordCommand(options);
402
+ } catch (err) {
403
+ console.error(chalk.red('\n✖ Error:'), err.message);
404
+ process.exit(1);
405
+ }
406
+ });
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
+
372
424
  // Cloud sync
373
425
  const cloud = program.command('cloud').description('Cloud backup and restore (Pro)');
374
426
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "3.2.2",
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/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/camgitt/memoir",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.2.0",
9
+ "version": "3.2.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "memoir-cli",
14
- "version": "3.2.0",
14
+ "version": "3.2.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
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
@@ -25,7 +25,13 @@ async function supaFetch(endpoint, options = {}) {
25
25
  export async function signUp(email, password) {
26
26
  const res = await supaFetch('/auth/v1/signup', {
27
27
  method: 'POST',
28
- body: JSON.stringify({ email, password }),
28
+ body: JSON.stringify({
29
+ email,
30
+ password,
31
+ options: {
32
+ emailRedirectTo: 'https://memoir.sh/confirmed',
33
+ },
34
+ }),
29
35
  });
30
36
  const data = await res.json();
31
37
  if (!res.ok) throw new Error(data.error_description || data.msg || 'Sign up failed');
@@ -109,4 +115,77 @@ export async function getSubscription(session) {
109
115
  return data[0];
110
116
  }
111
117
 
118
+ export async function resetPassword(email) {
119
+ const res = await supaFetch('/auth/v1/recover', {
120
+ method: 'POST',
121
+ body: JSON.stringify({
122
+ email,
123
+ options: {
124
+ redirectTo: 'https://memoir.sh/reset-password',
125
+ },
126
+ }),
127
+ });
128
+ if (!res.ok) {
129
+ const data = await res.json();
130
+ throw new Error(data.error_description || data.msg || 'Password reset failed');
131
+ }
132
+ }
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
+
112
191
  export { AUTH_FILE, supaFetch };
@@ -1,5 +1,5 @@
1
- export const SUPABASE_URL = 'https://oqrkxytbahfwjhcbyzrx.supabase.co';
2
- export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xcmt4eXRiYWhmd2poY2J5enJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyMTQ4MzMsImV4cCI6MjA4ODc5MDgzM30.jOKOi73OJgIgi1zj0VOIQkGp0xqS3ee4gfCjpdqCnvM';
1
+ export const SUPABASE_URL = process.env.MEMOIR_SUPABASE_URL || 'https://oqrkxytbahfwjhcbyzrx.supabase.co';
2
+ export const SUPABASE_ANON_KEY = process.env.MEMOIR_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xcmt4eXRiYWhmd2poY2J5enJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyMTQ4MzMsImV4cCI6MjA4ODc5MDgzM30.jOKOi73OJgIgi1zj0VOIQkGp0xqS3ee4gfCjpdqCnvM';
3
3
  export const STORAGE_BUCKET = 'memoir-backups';
4
4
  export const MAX_BACKUPS_FREE = 3;
5
5
  export const MAX_BACKUPS_PRO = 50;
@@ -5,6 +5,7 @@ import { createGzip, createGunzip } from 'zlib';
5
5
  import { pipeline } from 'stream/promises';
6
6
  import { Readable, Writable } from 'stream';
7
7
  import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET, MAX_BACKUPS_FREE, MAX_BACKUPS_PRO } from './constants.js';
8
+ import { encryptBuffer, decryptBuffer } from '../security/encryption.js';
8
9
 
9
10
  // Bundle a directory into a JSON manifest + gzip
10
11
  async function bundleDir(dir) {
@@ -64,10 +65,19 @@ async function unbundleToDir(gzipped, destDir) {
64
65
  return files.length;
65
66
  }
66
67
 
68
+ // Derive a stable encryption passphrase from the user's identity
69
+ // Uses only user_id (immutable) — NOT email, which can change
70
+ function cloudPassphrase(session) {
71
+ return `memoir-cloud:${session.user.id}`;
72
+ }
73
+
67
74
  // Upload backup to Supabase Storage + insert metadata
68
75
  export async function uploadBackup(stagingDir, session, toolResults) {
69
76
  const gzipped = await bundleDir(stagingDir);
70
77
 
78
+ // Encrypt before upload (AES-256-GCM, keyed to user identity)
79
+ const encrypted = await encryptBuffer(gzipped, cloudPassphrase(session));
80
+
71
81
  const backupId = crypto.randomUUID();
72
82
  const storagePath = `${session.user.id}/${backupId}.gz`;
73
83
 
@@ -79,7 +89,7 @@ export async function uploadBackup(stagingDir, session, toolResults) {
79
89
  'apikey': SUPABASE_ANON_KEY,
80
90
  'Content-Type': 'application/octet-stream',
81
91
  },
82
- body: gzipped,
92
+ body: encrypted,
83
93
  });
84
94
 
85
95
  if (!uploadRes.ok) {
@@ -125,7 +135,7 @@ export async function uploadBackup(stagingDir, session, toolResults) {
125
135
  user_id: session.user.id,
126
136
  tool_count: tools.length,
127
137
  file_count: fileCount,
128
- size_bytes: gzipped.length,
138
+ size_bytes: encrypted.length,
129
139
  tools,
130
140
  storage_path: storagePath,
131
141
  machine_name: os.hostname(),
@@ -139,7 +149,7 @@ export async function uploadBackup(stagingDir, session, toolResults) {
139
149
  }
140
150
 
141
151
  const backup = (await metaRes.json())[0];
142
- return { ...backup, sizeBytes: gzipped.length };
152
+ return { ...backup, sizeBytes: encrypted.length };
143
153
  }
144
154
 
145
155
  // Download a specific backup
@@ -153,7 +163,17 @@ export async function downloadBackup(backup, destDir, session) {
153
163
 
154
164
  if (!res.ok) throw new Error(`Download failed: ${await res.text()}`);
155
165
 
156
- const gzipped = Buffer.from(await res.arrayBuffer());
166
+ const raw = Buffer.from(await res.arrayBuffer());
167
+
168
+ // Decrypt if encrypted (check for MEMOIR01 magic header)
169
+ let gzipped;
170
+ if (raw.length >= 8 && raw.subarray(0, 8).toString() === 'MEMOIR01') {
171
+ gzipped = await decryptBuffer(raw, cloudPassphrase(session));
172
+ } else {
173
+ // Legacy unencrypted backup
174
+ gzipped = raw;
175
+ }
176
+
157
177
  const fileCount = await unbundleToDir(gzipped, destDir);
158
178
  return fileCount;
159
179
  }
@@ -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) {