memoir-cli 3.2.2 → 3.3.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/.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 +20 -3
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/cloud/auth.js +23 -1
- package/src/cloud/constants.js +2 -2
- package/src/cloud/storage.js +24 -4
- package/src/commands/login.js +65 -25
- 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 } 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';
|
|
@@ -348,9 +348,12 @@ program
|
|
|
348
348
|
program
|
|
349
349
|
.command('login')
|
|
350
350
|
.description('Sign in to memoir cloud')
|
|
351
|
-
.
|
|
351
|
+
.option('--email <email>', 'Email address (skip interactive prompt)')
|
|
352
|
+
.option('--password <password>', 'Password (skip interactive prompt)')
|
|
353
|
+
.option('--signup', 'Create a new account instead of signing in')
|
|
354
|
+
.action(async (options) => {
|
|
352
355
|
try {
|
|
353
|
-
await loginCommand();
|
|
356
|
+
await loginCommand(options);
|
|
354
357
|
} catch (err) {
|
|
355
358
|
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
356
359
|
process.exit(1);
|
|
@@ -369,6 +372,20 @@ program
|
|
|
369
372
|
}
|
|
370
373
|
});
|
|
371
374
|
|
|
375
|
+
program
|
|
376
|
+
.command('forgot-password')
|
|
377
|
+
.alias('reset-password')
|
|
378
|
+
.description('Send a password reset email')
|
|
379
|
+
.option('--email <email>', 'Email address (skip interactive prompt)')
|
|
380
|
+
.action(async (options) => {
|
|
381
|
+
try {
|
|
382
|
+
await forgotPasswordCommand(options);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
372
389
|
// Cloud sync
|
|
373
390
|
const cloud = program.command('cloud').description('Cloud backup and restore (Pro)');
|
|
374
391
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
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
|
@@ -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,20 @@ 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
|
+
|
|
112
134
|
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/login.js
CHANGED
|
@@ -2,9 +2,9 @@ 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 } from '../cloud/auth.js';
|
|
5
|
+
import { signIn, signUp, saveSession, getSession, logout, getSubscription, resetPassword } from '../cloud/auth.js';
|
|
6
6
|
|
|
7
|
-
export async function loginCommand() {
|
|
7
|
+
export async function loginCommand(options = {}) {
|
|
8
8
|
// Check if already logged in
|
|
9
9
|
const existing = await getSession();
|
|
10
10
|
if (existing) {
|
|
@@ -18,32 +18,44 @@ export async function loginCommand() {
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
let action, email, password;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
],
|
|
31
|
-
}]);
|
|
23
|
+
// Support non-interactive login via flags
|
|
24
|
+
if (options.email && options.password) {
|
|
25
|
+
action = options.signup ? 'signup' : 'signin';
|
|
26
|
+
email = options.email;
|
|
27
|
+
password = options.password;
|
|
28
|
+
} else {
|
|
29
|
+
console.log();
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const actionAnswer = await inquirer.prompt([{
|
|
32
|
+
type: 'list',
|
|
33
|
+
name: 'action',
|
|
34
|
+
message: 'Sign in or create account?',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: 'Sign in (existing account)', value: 'signin' },
|
|
37
|
+
{ name: 'Create account', value: 'signup' },
|
|
38
|
+
],
|
|
39
|
+
}]);
|
|
40
|
+
action = actionAnswer.action;
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const emailAnswer = await inquirer.prompt([{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'email',
|
|
45
|
+
message: 'Email:',
|
|
46
|
+
validate: v => v.includes('@') ? true : 'Enter a valid email',
|
|
47
|
+
}]);
|
|
48
|
+
email = emailAnswer.email;
|
|
49
|
+
|
|
50
|
+
const passwordAnswer = await inquirer.prompt([{
|
|
51
|
+
type: 'password',
|
|
52
|
+
name: 'password',
|
|
53
|
+
message: 'Password:',
|
|
54
|
+
mask: '*',
|
|
55
|
+
validate: v => v.length >= 6 ? true : 'Password must be at least 6 characters',
|
|
56
|
+
}]);
|
|
57
|
+
password = passwordAnswer.password;
|
|
58
|
+
}
|
|
47
59
|
|
|
48
60
|
try {
|
|
49
61
|
let session;
|
|
@@ -84,6 +96,34 @@ export async function loginCommand() {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
export async function forgotPasswordCommand(options = {}) {
|
|
100
|
+
let email = options.email;
|
|
101
|
+
|
|
102
|
+
if (!email) {
|
|
103
|
+
const emailAnswer = await inquirer.prompt([{
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'email',
|
|
106
|
+
message: 'Email:',
|
|
107
|
+
validate: v => v.includes('@') ? true : 'Enter a valid email',
|
|
108
|
+
}]);
|
|
109
|
+
email = emailAnswer.email;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await resetPassword(email);
|
|
114
|
+
console.log('\n' + boxen(
|
|
115
|
+
chalk.green('✔ Password reset email sent!') + '\n\n' +
|
|
116
|
+
chalk.white('Check ') + chalk.cyan(email) + chalk.white(' for a reset link.'),
|
|
117
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
118
|
+
) + '\n');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.log('\n' + boxen(
|
|
121
|
+
chalk.red('✖ ' + error.message),
|
|
122
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
123
|
+
) + '\n');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
87
127
|
export async function logoutCommand() {
|
|
88
128
|
await logout();
|
|
89
129
|
console.log('\n' + boxen(
|
package/src/commands/upgrade.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import boxen from 'boxen';
|
|
3
3
|
import gradient from 'gradient-string';
|
|
4
|
-
import
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { getSession, getSubscription, supaFetch } from '../cloud/auth.js';
|
|
6
|
+
import { SUPABASE_URL } from '../cloud/constants.js';
|
|
7
|
+
|
|
8
|
+
async function createCheckoutSession(session) {
|
|
9
|
+
const res = await fetch(`${SUPABASE_URL}/functions/v1/stripe-checkout`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const data = await res.json();
|
|
17
|
+
if (!res.ok) throw new Error(data.error || 'Failed to create checkout session');
|
|
18
|
+
return data.url;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function openUrl(url) {
|
|
22
|
+
const { exec } = require('child_process');
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
if (platform === 'darwin') exec(`open "${url}"`);
|
|
25
|
+
else if (platform === 'win32') exec(`start "" "${url}"`);
|
|
26
|
+
else exec(`xdg-open "${url}"`);
|
|
27
|
+
}
|
|
5
28
|
|
|
6
29
|
export async function upgradeCommand() {
|
|
7
30
|
const session = await getSession();
|
|
@@ -23,7 +46,6 @@ export async function upgradeCommand() {
|
|
|
23
46
|
const col2 = 22;
|
|
24
47
|
|
|
25
48
|
const pad = (str, width) => {
|
|
26
|
-
// Strip ANSI for length calculation
|
|
27
49
|
const stripped = str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
28
50
|
const diff = width - stripped.length;
|
|
29
51
|
return diff > 0 ? str + ' '.repeat(diff) : str;
|
|
@@ -75,32 +97,32 @@ export async function upgradeCommand() {
|
|
|
75
97
|
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
76
98
|
));
|
|
77
99
|
|
|
78
|
-
// If free and logged in, open
|
|
100
|
+
// If free and logged in, open Stripe checkout
|
|
79
101
|
if (session && currentPlan === 'free') {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const { exec } = await import('child_process');
|
|
83
|
-
const url = 'https://memoir.sh/pricing';
|
|
84
|
-
|
|
85
|
-
const platform = process.platform;
|
|
86
|
-
let cmd;
|
|
87
|
-
if (platform === 'darwin') {
|
|
88
|
-
cmd = `open "${url}"`;
|
|
89
|
-
} else if (platform === 'win32') {
|
|
90
|
-
cmd = `start "${url}"`;
|
|
91
|
-
} else {
|
|
92
|
-
cmd = `xdg-open "${url}"`;
|
|
93
|
-
}
|
|
102
|
+
const spinner = ora(chalk.cyan(' Creating checkout session...')).start();
|
|
94
103
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
try {
|
|
105
|
+
const url = await createCheckoutSession(session);
|
|
106
|
+
spinner.succeed(chalk.green(' Opening Stripe checkout...'));
|
|
107
|
+
|
|
108
|
+
const { exec } = await import('child_process');
|
|
109
|
+
const platform = process.platform;
|
|
110
|
+
if (platform === 'darwin') exec(`open "${url}"`);
|
|
111
|
+
else if (platform === 'win32') exec(`start "" "${url}"`);
|
|
112
|
+
else exec(`xdg-open "${url}"`);
|
|
113
|
+
|
|
114
|
+
console.log(
|
|
115
|
+
'\n' + chalk.gray(' Complete payment in your browser.') + '\n' +
|
|
116
|
+
chalk.gray(' Your plan updates automatically — run ') +
|
|
117
|
+
chalk.cyan('memoir upgrade') +
|
|
118
|
+
chalk.gray(' again to verify.') + '\n'
|
|
119
|
+
);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
spinner.fail(chalk.red(' ' + err.message));
|
|
122
|
+
console.log(chalk.gray('\n Fallback: visit ') + chalk.cyan('https://memoir.sh/pricing') + '\n');
|
|
123
|
+
}
|
|
102
124
|
} else if (!session) {
|
|
103
|
-
console.log('\n' + chalk.gray('
|
|
125
|
+
console.log('\n' + chalk.gray(' Run ') + chalk.cyan('memoir login') + chalk.gray(' to create an account, then ') + chalk.cyan('memoir upgrade') + chalk.gray(' to subscribe.') + '\n');
|
|
104
126
|
} else {
|
|
105
127
|
console.log();
|
|
106
128
|
}
|