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.
@@ -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 } 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
- .action(async () => {
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.2.2",
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.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
@@ -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,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 };
@@ -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
  }
@@ -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
- console.log();
21
+ let action, email, password;
22
22
 
23
- const { action } = await inquirer.prompt([{
24
- type: 'list',
25
- name: 'action',
26
- message: 'Sign in or create account?',
27
- choices: [
28
- { name: 'Sign in (existing account)', value: 'signin' },
29
- { name: 'Create account', value: 'signup' },
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
- const { email } = await inquirer.prompt([{
34
- type: 'input',
35
- name: 'email',
36
- message: 'Email:',
37
- validate: v => v.includes('@') ? true : 'Enter a valid email',
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
- const { password } = await inquirer.prompt([{
41
- type: 'password',
42
- name: 'password',
43
- message: 'Password:',
44
- mask: '*',
45
- validate: v => v.length >= 6 ? true : 'Password must be at least 6 characters',
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(
@@ -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 { getSession, getSubscription } from '../cloud/auth.js';
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 pricing page
100
+ // If free and logged in, open Stripe checkout
79
101
  if (session && currentPlan === 'free') {
80
- console.log('\n' + chalk.cyan(' Opening pricing page...') + '\n');
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
- exec(cmd, () => {});
96
-
97
- console.log(
98
- chalk.gray(' Once you\'ve completed payment, run ') +
99
- chalk.cyan('memoir login') +
100
- chalk.gray(' to refresh your plan.') + '\n'
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(' Sign up at ') + chalk.cyan('memoir.sh/pricing') + chalk.gray(' or run ') + chalk.cyan('memoir login') + chalk.gray(' to get started.') + '\n');
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
  }