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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
- package/CONTRIBUTING.md +47 -0
- package/LICENSE +21 -0
- package/README.md +27 -3
- package/bin/memoir.js +69 -17
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/cloud/auth.js +81 -2
- package/src/cloud/constants.js +2 -2
- package/src/cloud/storage.js +24 -4
- package/src/commands/init.js +79 -50
- package/src/commands/login.js +108 -28
- package/src/commands/upgrade.js +47 -25
- package/GAMEPLAN.md +0 -235
- package/LAUNCH_POSTS.md +0 -247
- package/MARKETING.md +0 -143
- package/POSTS-READY-TO-GO.md +0 -215
- package/VISION.md +0 -722
- package/landing-page-v2.html +0 -690
- package/mcp-publisher +0 -0
|
@@ -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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
[](https://npmjs.org/package/memoir-cli)
|
|
8
8
|
[](https://npmjs.org/package/memoir-cli)
|
|
9
|
-
[](https://github.com/camgitt/memoir/stargazers)
|
|
10
|
+
[](LICENSE)
|
|
10
11
|
[](https://nodejs.org)
|
|
11
12
|
|
|
12
13
|
Your AI forgets everything between sessions. memoir gives it long-term memory via MCP.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
**11 tools supported** • **E2E encrypted** • **Cross-platform** • **Open source**
|
|
15
16
|
|
|
16
|
-
|
|
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
|
-
.
|
|
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);
|
|
@@ -348,9 +367,12 @@ program
|
|
|
348
367
|
program
|
|
349
368
|
.command('login')
|
|
350
369
|
.description('Sign in to memoir cloud')
|
|
351
|
-
.
|
|
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.
|
|
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.
|
|
9
|
+
"version": "3.2.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "memoir-cli",
|
|
14
|
-
"version": "3.2.
|
|
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({
|
|
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 };
|
package/src/cloud/constants.js
CHANGED
|
@@ -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;
|
package/src/cloud/storage.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
}
|
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) {
|