memoir-cli 2.4.0 → 2.5.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/README.md CHANGED
@@ -1,88 +1,129 @@
1
1
  <div align="center">
2
2
 
3
3
  # memoir
4
- **Your AI Remembers Everything. Sync It Everywhere.**
5
4
 
6
- [![npm version](https://img.shields.io/npm/v/memoir-cli.svg?style=flat-square)](https://npmjs.org/package/memoir-cli)
5
+ **Sync your AI memory across every device and every tool.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/memoir-cli.svg?style=flat-square&color=7c6ef0)](https://npmjs.org/package/memoir-cli)
8
+ [![npm downloads](https://img.shields.io/npm/dm/memoir-cli.svg?style=flat-square&color=7c6ef0)](https://npmjs.org/package/memoir-cli)
7
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
10
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](https://nodejs.org)
11
+
12
+ Your AI tools forget you on every new machine. memoir fixes that.
8
13
 
9
- *Never lose your AI's context again. Sync and translate your AI memory across every device and tool.*
14
+ [Website](https://memoir.sh) &bull; [npm](https://npmjs.org/package/memoir-cli) &bull; [Blog](https://memoir.sh/blog)
15
+
16
+ <br />
10
17
 
11
18
  ![memoir demo](demo.gif)
12
19
 
13
20
  </div>
14
21
 
15
- ---
22
+ ## Why memoir
16
23
 
17
- ## The Problem
24
+ You spend weeks teaching your AI tools how you code. Your CLAUDE.md is dialed in. Your .cursorrules are perfect. Your ChatGPT custom instructions know your stack.
18
25
 
19
- You spend weeks teaching your AI tools how you code your preferred patterns, project context, coding standards.
26
+ Then you get a new laptop. **Everything is gone.**
20
27
 
21
- Then you switch laptops. Or try a new AI tool. Or want your team on the same page.
28
+ memoir backs up, restores, and **translates** your AI memory across any machine and any tool. One command to save. One command to restore.
22
29
 
23
- All that context is trapped in hidden dotfiles on one machine.
30
+ ```bash
31
+ npm install -g memoir-cli
32
+ ```
24
33
 
25
- ## The Solution
34
+ ## Supported Tools (11)
35
+
36
+ | Tool | What gets synced |
37
+ |------|-----------------|
38
+ | **ChatGPT** | CHATGPT.md custom instructions |
39
+ | **Claude Code** | ~/.claude/ settings, memory, CLAUDE.md files |
40
+ | **Gemini CLI** | ~/.gemini/ config, GEMINI.md files |
41
+ | **OpenAI Codex** | ~/.codex/ config, AGENTS.md, codex.md |
42
+ | **Cursor** | Settings, keybindings, .cursorrules |
43
+ | **GitHub Copilot** | Config, copilot-instructions.md |
44
+ | **Windsurf** | Settings, keybindings, .windsurfrules |
45
+ | **Zed** | Settings, keymap, tasks |
46
+ | **Cline** | Settings, .clinerules |
47
+ | **Continue.dev** | Config, .continuerules |
48
+ | **Aider** | .aider.conf.yml, system prompt |
26
49
 
27
- `memoir` extracts, backs up, restores, and **translates** your AI memory across any computer and any tool. One command to save. One command to restore. One command to translate between tools.
50
+ Plus **per-project configs** memoir scans your filesystem for CLAUDE.md, GEMINI.md, CHATGPT.md, .cursorrules, and AGENTS.md across all your projects.
51
+
52
+ ## Quick Start
28
53
 
29
54
  ```bash
55
+ # Install
30
56
  npm install -g memoir-cli
57
+
58
+ # First-time setup (GitHub repo or local)
59
+ memoir init
60
+
61
+ # Back up all your AI configs
62
+ memoir push
63
+
64
+ # Restore on a new machine
65
+ memoir restore
31
66
  ```
32
67
 
33
- ### Supported Tools (11)
34
- | Tool | Config synced |
35
- |------|--------------|
36
- | **ChatGPT** | `CHATGPT.md` — custom instructions, preferences |
37
- | **Claude Code** | `~/.claude/` — settings, projects, memory files |
38
- | **Gemini CLI** | `~/.gemini/` — settings, GEMINI.md |
39
- | **OpenAI Codex** | `~/.codex/` — config, instructions |
40
- | **Cursor** | Settings, keybindings, rules |
41
- | **GitHub Copilot** | Config, settings |
42
- | **Windsurf** | Settings, keybindings, rules |
43
- | **Zed** | Settings, keymap, tasks |
44
- | **Cline** | Settings, rules |
45
- | **Continue.dev** | Config, rules |
46
- | **Aider** | `.aider.conf.yml`, system prompt |
68
+ That's it. Every AI tool gets its memory back.
47
69
 
48
- Plus **per-project configs**: automatically finds `CLAUDE.md`, `GEMINI.md`, `CHATGPT.md`, `.cursorrules`, `AGENTS.md` across all your projects.
70
+ ## Key Features
49
71
 
50
- ---
72
+ ### Translate between AI tools
73
+ ```bash
74
+ memoir migrate --from chatgpt --to claude
75
+ # AI-powered — rewrites conventions, not copy-paste
76
+ ```
51
77
 
52
- ## Quick Start
78
+ Works between any combination: ChatGPT, Claude, Gemini, Cursor, Copilot, Codex, Windsurf, Aider, and more. Translate to one tool or all at once:
53
79
 
54
- ### 1. Initialize
55
80
  ```bash
56
- memoir init
57
- # Walks you through setup — GitHub repo or local folder
58
- # Auto-creates a private repo if you have gh CLI
81
+ memoir migrate --from chatgpt --to all
59
82
  ```
60
83
 
61
- ### 2. Back up your memory
84
+ ### Session handoff
62
85
  ```bash
63
- memoir push
86
+ # Laptop dying? Capture your session
87
+ memoir snapshot
88
+
89
+ # Pick up on another machine
90
+ memoir resume --inject --to claude
64
91
  ```
65
92
 
66
- ### 3. Restore on a new machine
93
+ ### Profiles (personal / work)
67
94
  ```bash
68
- memoir restore
95
+ memoir profile create work
96
+ memoir push --profile work
97
+ memoir profile switch personal
69
98
  ```
70
99
 
71
- ### 4. Translate between tools
100
+ Each profile has its own repo and tool filters. Work configs never mix with personal.
101
+
102
+ ### Cloud sync (Pro)
72
103
  ```bash
73
- memoir migrate --from claude --to gemini
74
- # AI-powered translation not copy-paste, real rewriting
104
+ memoir login
105
+ memoir cloud push # encrypted cloud backup
106
+ memoir cloud restore # restore from any version
107
+ memoir history # view all backup versions
75
108
  ```
76
109
 
77
- ---
110
+ Free tier: 3 cloud backups. Pro ($5/mo): unlimited + version history.
111
+
112
+ ### Security
113
+ ```bash
114
+ memoir doctor
115
+ # Scans for secrets, API keys, .env files before pushing
116
+ ```
117
+
118
+ memoir **never** syncs credentials, API keys, .env files, or auth tokens.
78
119
 
79
120
  ## All Commands
80
121
 
81
122
  | Command | What it does |
82
123
  |---------|-------------|
83
- | `memoir init` | Setup wizard — GitHub or local, upload or download |
124
+ | `memoir init` | Setup wizard — GitHub or local storage |
84
125
  | `memoir push` | Back up all AI configs |
85
- | `memoir restore` | Restore on a new machine (non-destructive) |
126
+ | `memoir restore` | Restore on a new machine |
86
127
  | `memoir status` | Show detected AI tools |
87
128
  | `memoir doctor` | Diagnose issues, scan for secrets |
88
129
  | `memoir view` | Preview what's in your backup |
@@ -91,28 +132,27 @@ memoir migrate --from claude --to gemini
91
132
  | `memoir snapshot` | Capture current coding session |
92
133
  | `memoir resume` | Pick up where you left off |
93
134
  | `memoir profile` | Manage profiles (personal/work) |
135
+ | `memoir cloud push` | Back up to memoir cloud |
136
+ | `memoir cloud restore` | Restore from memoir cloud |
137
+ | `memoir history` | View cloud backup versions |
138
+ | `memoir login` | Sign in to memoir cloud |
94
139
  | `memoir update` | Self-update to latest version |
95
140
 
96
- ---
97
-
98
- ## Profiles
141
+ ## How memoir compares
99
142
 
100
- Separate personal and work configs different repos, different tools.
143
+ | Feature | memoir | skillshare | ai-rulez | memories.sh |
144
+ |---------|--------|-----------|----------|-------------|
145
+ | Cross-device sync | **Yes** | Yes (git) | No | Yes |
146
+ | AI-powered translation | **Yes** | No | No | No |
147
+ | Tools supported | **11** | 40+ | 18 | 3 |
148
+ | Cloud backup | **Yes** | No | No | Yes ($15/mo) |
149
+ | Version history | **Yes** | No | No | No |
150
+ | Session handoff | **Yes** | No | No | No |
151
+ | Profiles | **Yes** | No | No | No |
152
+ | Secret scanning | **Yes** | Yes | No | No |
153
+ | Free & open source | **Yes** | Yes | Yes | No |
101
154
 
102
- ```bash
103
- memoir profile create work # set up work profile with its own repo
104
- memoir profile create personal # personal side projects
105
-
106
- memoir push --profile work # sync only work configs
107
- memoir restore --profile personal
108
-
109
- memoir profile list # see all profiles
110
- memoir profile switch work # change default
111
- ```
112
-
113
- Each profile can filter which tools to sync, so your personal side project memory never mixes with work.
114
-
115
- ---
155
+ memoir is free and open source. Cloud sync starts at $0 (3 backups free).
116
156
 
117
157
  ## Common Workflows
118
158
 
@@ -122,46 +162,36 @@ Each profile can filter which tools to sync, so your personal side project memor
122
162
  memoir push
123
163
 
124
164
  # New machine
125
- memoir init # Download → same GitHub repo
126
- memoir restore # All configs restored in seconds
165
+ npm install -g memoir-cli
166
+ memoir init # connect to same repo
167
+ memoir restore # all configs restored in seconds
127
168
  ```
128
169
 
129
- ### Translate between tools
170
+ ### Switching from ChatGPT to Claude
130
171
  ```bash
131
172
  memoir migrate --from chatgpt --to claude
132
- # Your ChatGPT custom instructions become a proper CLAUDE.md
173
+ # Your custom instructions become a proper CLAUDE.md
133
174
  ```
134
175
 
135
- ### Fan out to every tool
176
+ ### Daily sync between machines
136
177
  ```bash
137
- memoir migrate --from chatgpt --to all
138
- # One source of truth, every tool gets its own format
178
+ memoir push # end of day on laptop
179
+ memoir restore # next morning on desktop
139
180
  ```
140
181
 
141
- ### Daily sync
182
+ ### Cross-platform (Mac ↔ Windows ↔ Linux)
142
183
  ```bash
143
- memoir push # end of day
144
- memoir restore # next morning, different machine
145
- ```
146
-
147
- ---
148
-
149
- ## Security
150
-
151
- Memoir **only** syncs config files, instructions, and memory markdown. It never touches credentials, API keys, `.env` files, or auth tokens.
152
-
153
- Run `memoir doctor` to see exactly what would be synced and scan for accidental secrets before pushing.
154
-
155
- ---
184
+ # Push from Mac
185
+ memoir push
156
186
 
157
- ## Roadmap
187
+ # Restore on Windows — paths remap automatically
188
+ memoir restore
189
+ ```
158
190
 
159
- - **Universal format** — write one `MEMOIR.md`, generate all tool-specific configs
160
- - **Cloud sync** — no GitHub needed, encrypted backups
161
- - **Teams** — shared coding standards across your whole team
162
- - **Templates** — community-shared AI tool configs
191
+ ## Requirements
163
192
 
164
- ---
193
+ - Node.js >= 18
194
+ - Works on macOS, Windows, Linux
165
195
 
166
196
  ## Contributing
167
197
 
@@ -172,6 +202,13 @@ Contributions welcome — especially new tool adapters and migration improvement
172
202
  3. Commit and push
173
203
  4. Open a PR
174
204
 
205
+ ## Links
206
+
207
+ - **Website:** [memoir.sh](https://memoir.sh)
208
+ - **npm:** [memoir-cli](https://npmjs.org/package/memoir-cli)
209
+ - **Blog:** [memoir.sh/blog](https://memoir.sh/blog)
210
+ - **Issues:** [GitHub Issues](https://github.com/camgitt/memoir/issues)
211
+
175
212
  ## License
176
213
 
177
214
  MIT
package/bin/memoir.js CHANGED
@@ -14,6 +14,9 @@ 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';
18
+ import { cloudPushCommand, cloudRestoreCommand } from '../src/commands/cloud.js';
19
+ import { historyCommand } from '../src/commands/history.js';
17
20
  import { createRequire } from 'module';
18
21
 
19
22
  const require = createRequire(import.meta.url);
@@ -60,6 +63,11 @@ if (process.argv.length <= 2) {
60
63
  chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
61
64
  chalk.cyan(' memoir profile ') + chalk.gray('— manage profiles (personal/work)') + '\n' +
62
65
  chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n\n' +
66
+ chalk.white.bold('Cloud (Pro):') + '\n' +
67
+ chalk.cyan(' memoir login ') + chalk.gray('— sign in to memoir cloud') + '\n' +
68
+ chalk.cyan(' memoir cloud push ') + chalk.gray('— back up to the cloud') + '\n' +
69
+ chalk.cyan(' memoir cloud restore ') + chalk.gray('— restore from cloud') + '\n' +
70
+ chalk.cyan(' memoir history ') + chalk.gray('— view backup versions') + '\n\n' +
63
71
  chalk.gray(' Tip: use --profile work to sync a specific profile') + '\n\n' +
64
72
  chalk.gray(`v${VERSION}`),
65
73
  { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
@@ -263,6 +271,74 @@ program
263
271
  }
264
272
  });
265
273
 
274
+ // Cloud auth
275
+ program
276
+ .command('login')
277
+ .description('Sign in to memoir cloud')
278
+ .action(async () => {
279
+ try {
280
+ await loginCommand();
281
+ } catch (err) {
282
+ console.error(chalk.red('\n✖ Error:'), err.message);
283
+ process.exit(1);
284
+ }
285
+ });
286
+
287
+ program
288
+ .command('logout')
289
+ .description('Sign out of memoir cloud')
290
+ .action(async () => {
291
+ try {
292
+ await logoutCommand();
293
+ } catch (err) {
294
+ console.error(chalk.red('\n✖ Error:'), err.message);
295
+ process.exit(1);
296
+ }
297
+ });
298
+
299
+ // Cloud sync
300
+ const cloud = program.command('cloud').description('Cloud backup and restore (Pro)');
301
+
302
+ cloud
303
+ .command('push')
304
+ .description('Back up your AI memory to the cloud')
305
+ .option('--only <tools>', 'Only sync specific tools (comma-separated)')
306
+ .action(async (options) => {
307
+ try {
308
+ await cloudPushCommand(options);
309
+ } catch (err) {
310
+ console.error(chalk.red('\n✖ Error:'), err.message);
311
+ process.exit(1);
312
+ }
313
+ });
314
+
315
+ cloud
316
+ .command('restore')
317
+ .description('Restore your AI memory from the cloud')
318
+ .option('--only <tools>', 'Only restore specific tools (comma-separated)')
319
+ .option('-y, --yes', 'Skip confirmation prompts')
320
+ .option('--version <number>', 'Restore a specific version')
321
+ .action(async (options) => {
322
+ try {
323
+ await cloudRestoreCommand(options);
324
+ } catch (err) {
325
+ console.error(chalk.red('\n✖ Error:'), err.message);
326
+ process.exit(1);
327
+ }
328
+ });
329
+
330
+ program
331
+ .command('history')
332
+ .description('View your cloud backup history')
333
+ .action(async () => {
334
+ try {
335
+ await historyCommand();
336
+ } catch (err) {
337
+ console.error(chalk.red('\n✖ Error:'), err.message);
338
+ process.exit(1);
339
+ }
340
+ });
341
+
266
342
  // Profile management
267
343
  const profile = program.command('profile').description('Manage profiles (personal, work, etc.)');
268
344
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Sync AI memory across devices. Back up and restore Claude, Gemini, Codex, Cursor, Copilot, Windsurf configs. Snapshot coding sessions and resume on another machine. Migrate instructions between AI assistants.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -70,18 +70,36 @@ function remapProjectPaths(backupDir, adapterSource) {
70
70
 
71
71
  // Step 3: Identify foreign home keys in the backup
72
72
  // A "home key" is a dir that: has memory/, OR is a prefix of other dirs, AND is not a sub-project
73
+ // Also detect alternate local encodings (e.g. -C-Users-X and C--Users-X are the same machine)
73
74
  const foreignHomeKeys = new Set();
75
+ const localAltKeys = new Set(); // alternate encodings of local home dir
76
+
77
+ // Detect alternate local encodings by checking if a dir resolves to the same homedir
78
+ const home = os.homedir();
79
+ const homeNormalized = home.toLowerCase().replace(/[\\/:]/g, '');
74
80
 
75
81
  for (const entry of backupEntries) {
76
- // Skip dirs that already belong to this machine
82
+ // Skip dirs that already match the primary local key
77
83
  if (entry === localHomeKey || entry.startsWith(localHomeKey + '-')) continue;
78
84
 
85
+ // Check if this entry is an alternate encoding of the local home dir
86
+ const entryNormalized = entry.replace(/^[-]/, '').toLowerCase().replace(/[-]/g, '');
87
+ if (entryNormalized === homeNormalized || homeNormalized.endsWith(entryNormalized) || entryNormalized.endsWith(homeNormalized)) {
88
+ // This is an alternate encoding of the local home — treat as local, not foreign
89
+ localAltKeys.add(entry);
90
+ continue;
91
+ }
92
+
79
93
  // Is this a sub-project of another backup dir? Then skip — its parent handles it
80
94
  const isSubProject = backupEntries.some(other =>
81
95
  other !== entry && entry.startsWith(other + '-')
82
96
  );
83
97
  if (isSubProject) continue;
84
98
 
99
+ // Is this a sub-project of an alternate local key? Skip too
100
+ const isAltSubProject = [...localAltKeys].some(alt => entry.startsWith(alt + '-'));
101
+ if (isAltSubProject) continue;
102
+
85
103
  // Has memory/ subfolder = definitely a home key
86
104
  const hasMemory = fs.existsSync(path.join(projectsDir, entry, 'memory'));
87
105
  // Is a prefix of other dirs = likely a home key
@@ -116,6 +134,33 @@ function remapProjectPaths(backupDir, adapterSource) {
116
134
  return remaps;
117
135
  }
118
136
 
137
+ // Merge memory dirs from a foreign machine — copies files that don't exist locally,
138
+ // and for files that exist on both, keeps the newer version.
139
+ async function mergeMemoryDirs(src, dest) {
140
+ const entries = await fs.readdir(src, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ const srcPath = path.join(src, entry.name);
143
+ const destPath = path.join(dest, entry.name);
144
+
145
+ if (entry.isDirectory()) {
146
+ await fs.ensureDir(destPath);
147
+ await mergeMemoryDirs(srcPath, destPath);
148
+ } else {
149
+ if (await fs.pathExists(destPath)) {
150
+ // Both machines have this file — keep the newer one
151
+ const srcStat = await fs.stat(srcPath);
152
+ const destStat = await fs.stat(destPath);
153
+ if (srcStat.mtimeMs > destStat.mtimeMs) {
154
+ await fs.copy(srcPath, destPath);
155
+ }
156
+ } else {
157
+ // File only exists on foreign machine — always copy it
158
+ await fs.copy(srcPath, destPath);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
119
164
  async function syncFiles(src, dest, changes) {
120
165
  const entries = await fs.readdir(src, { withFileTypes: true });
121
166
  for (const entry of entries) {
@@ -194,8 +239,8 @@ export async function restoreMemories(sourceDir, spinner, onlyFilter = null, aut
194
239
  const newDir = path.join(backupDir, 'projects', remap.newName);
195
240
  if (await fs.pathExists(oldDir)) {
196
241
  if (await fs.pathExists(newDir)) {
197
- // Merge into existing directory
198
- await syncFiles(oldDir, newDir, { added: [], updated: [], skipped: [] });
242
+ // Merge into existing directory — force-copy new files from foreign machine
243
+ await mergeMemoryDirs(oldDir, newDir);
199
244
  await fs.remove(oldDir);
200
245
  } else {
201
246
  await fs.move(oldDir, newDir);
@@ -0,0 +1,112 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { SUPABASE_URL, SUPABASE_ANON_KEY } from './constants.js';
5
+
6
+ const isWin = process.platform === 'win32';
7
+ const configDir = isWin
8
+ ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'memoir')
9
+ : path.join(os.homedir(), '.config', 'memoir');
10
+ const AUTH_FILE = path.join(configDir, 'auth.json');
11
+
12
+ async function supaFetch(endpoint, options = {}) {
13
+ const url = `${SUPABASE_URL}${endpoint}`;
14
+ const res = await fetch(url, {
15
+ ...options,
16
+ headers: {
17
+ 'apikey': SUPABASE_ANON_KEY,
18
+ 'Content-Type': 'application/json',
19
+ ...options.headers,
20
+ },
21
+ });
22
+ return res;
23
+ }
24
+
25
+ export async function signUp(email, password) {
26
+ const res = await supaFetch('/auth/v1/signup', {
27
+ method: 'POST',
28
+ body: JSON.stringify({ email, password }),
29
+ });
30
+ const data = await res.json();
31
+ if (!res.ok) throw new Error(data.error_description || data.msg || 'Sign up failed');
32
+ return data;
33
+ }
34
+
35
+ export async function signIn(email, password) {
36
+ const res = await supaFetch('/auth/v1/token?grant_type=password', {
37
+ method: 'POST',
38
+ body: JSON.stringify({ email, password }),
39
+ });
40
+ const data = await res.json();
41
+ if (!res.ok) throw new Error(data.error_description || data.msg || 'Sign in failed');
42
+ return data;
43
+ }
44
+
45
+ export async function refreshSession(refreshToken) {
46
+ const res = await supaFetch('/auth/v1/token?grant_type=refresh_token', {
47
+ method: 'POST',
48
+ body: JSON.stringify({ refresh_token: refreshToken }),
49
+ });
50
+ const data = await res.json();
51
+ if (!res.ok) throw new Error(data.error_description || data.msg || 'Token refresh failed');
52
+ return data;
53
+ }
54
+
55
+ export async function saveSession(session) {
56
+ await fs.ensureDir(configDir);
57
+ const payload = {
58
+ access_token: session.access_token,
59
+ refresh_token: session.refresh_token,
60
+ expires_at: Date.now() + (session.expires_in * 1000),
61
+ user: {
62
+ id: session.user.id,
63
+ email: session.user.email,
64
+ },
65
+ };
66
+ await fs.writeFile(AUTH_FILE, JSON.stringify(payload, null, 2), { mode: 0o600 });
67
+ return payload;
68
+ }
69
+
70
+ export async function getSession() {
71
+ if (!await fs.pathExists(AUTH_FILE)) return null;
72
+
73
+ const stored = await fs.readJson(AUTH_FILE);
74
+
75
+ // If token expires within 60 seconds, refresh
76
+ if (stored.expires_at < Date.now() + 60000) {
77
+ try {
78
+ const refreshed = await refreshSession(stored.refresh_token);
79
+ return await saveSession(refreshed);
80
+ } catch {
81
+ // Refresh failed — session is dead
82
+ await fs.remove(AUTH_FILE);
83
+ return null;
84
+ }
85
+ }
86
+
87
+ return stored;
88
+ }
89
+
90
+ export async function logout() {
91
+ if (await fs.pathExists(AUTH_FILE)) {
92
+ await fs.remove(AUTH_FILE);
93
+ }
94
+ }
95
+
96
+ export async function isLoggedIn() {
97
+ const session = await getSession();
98
+ return !!session;
99
+ }
100
+
101
+ export async function getSubscription(session) {
102
+ const res = await supaFetch('/rest/v1/subscriptions?select=*&user_id=eq.' + session.user.id, {
103
+ headers: {
104
+ 'Authorization': `Bearer ${session.access_token}`,
105
+ },
106
+ });
107
+ const data = await res.json();
108
+ if (!res.ok || !data.length) return { status: 'free' };
109
+ return data[0];
110
+ }
111
+
112
+ export { AUTH_FILE, supaFetch };
@@ -0,0 +1,5 @@
1
+ export const SUPABASE_URL = 'https://oqrkxytbahfwjhcbyzrx.supabase.co';
2
+ export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xcmt4eXRiYWhmd2poY2J5enJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyMTQ4MzMsImV4cCI6MjA4ODc5MDgzM30.jOKOi73OJgIgi1zj0VOIQkGp0xqS3ee4gfCjpdqCnvM';
3
+ export const STORAGE_BUCKET = 'memoir-backups';
4
+ export const MAX_BACKUPS_FREE = 3;
5
+ export const MAX_BACKUPS_PRO = 50;
@@ -0,0 +1,212 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { createGzip, createGunzip } from 'zlib';
5
+ import { pipeline } from 'stream/promises';
6
+ import { Readable, Writable } from 'stream';
7
+ import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET, MAX_BACKUPS_FREE, MAX_BACKUPS_PRO } from './constants.js';
8
+
9
+ // Bundle a directory into a JSON manifest + gzip
10
+ async function bundleDir(dir) {
11
+ const files = [];
12
+
13
+ async function walk(currentDir, prefix = '') {
14
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
15
+ for (const entry of entries) {
16
+ const fullPath = path.join(currentDir, entry.name);
17
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
18
+ if (entry.isDirectory()) {
19
+ await walk(fullPath, relPath);
20
+ } else {
21
+ const content = await fs.readFile(fullPath);
22
+ files.push({
23
+ path: relPath,
24
+ content: content.toString('base64'),
25
+ });
26
+ }
27
+ }
28
+ }
29
+
30
+ await walk(dir);
31
+ const json = JSON.stringify(files);
32
+ const buffer = Buffer.from(json, 'utf-8');
33
+
34
+ // Gzip
35
+ return new Promise((resolve, reject) => {
36
+ const chunks = [];
37
+ const gzip = createGzip({ level: 9 });
38
+ gzip.on('data', chunk => chunks.push(chunk));
39
+ gzip.on('end', () => resolve(Buffer.concat(chunks)));
40
+ gzip.on('error', reject);
41
+ gzip.end(buffer);
42
+ });
43
+ }
44
+
45
+ // Unbundle gzipped JSON back to a directory
46
+ async function unbundleToDir(gzipped, destDir) {
47
+ const decompressed = await new Promise((resolve, reject) => {
48
+ const chunks = [];
49
+ const gunzip = createGunzip();
50
+ gunzip.on('data', chunk => chunks.push(chunk));
51
+ gunzip.on('end', () => resolve(Buffer.concat(chunks)));
52
+ gunzip.on('error', reject);
53
+ gunzip.end(gzipped);
54
+ });
55
+
56
+ const files = JSON.parse(decompressed.toString('utf-8'));
57
+
58
+ for (const file of files) {
59
+ const fullPath = path.join(destDir, file.path);
60
+ await fs.ensureDir(path.dirname(fullPath));
61
+ await fs.writeFile(fullPath, Buffer.from(file.content, 'base64'));
62
+ }
63
+
64
+ return files.length;
65
+ }
66
+
67
+ // Upload backup to Supabase Storage + insert metadata
68
+ export async function uploadBackup(stagingDir, session, toolResults) {
69
+ const gzipped = await bundleDir(stagingDir);
70
+
71
+ const backupId = crypto.randomUUID();
72
+ const storagePath = `${session.user.id}/${backupId}.gz`;
73
+
74
+ // Upload to Storage
75
+ const uploadRes = await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}/${storagePath}`, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Authorization': `Bearer ${session.access_token}`,
79
+ 'apikey': SUPABASE_ANON_KEY,
80
+ 'Content-Type': 'application/octet-stream',
81
+ },
82
+ body: gzipped,
83
+ });
84
+
85
+ if (!uploadRes.ok) {
86
+ const err = await uploadRes.text();
87
+ throw new Error(`Upload failed: ${err}`);
88
+ }
89
+
90
+ // Get next version number
91
+ const versionRes = await fetch(
92
+ `${SUPABASE_URL}/rest/v1/backups?select=version&user_id=eq.${session.user.id}&order=version.desc&limit=1`,
93
+ {
94
+ headers: {
95
+ 'Authorization': `Bearer ${session.access_token}`,
96
+ 'apikey': SUPABASE_ANON_KEY,
97
+ },
98
+ }
99
+ );
100
+ const versionData = await versionRes.json();
101
+ const nextVersion = (versionData.length > 0 ? versionData[0].version : 0) + 1;
102
+
103
+ // Count files in staging dir
104
+ let fileCount = 0;
105
+ const countFiles = async (dir) => {
106
+ const entries = await fs.readdir(dir, { withFileTypes: true });
107
+ for (const e of entries) {
108
+ if (e.isDirectory()) await countFiles(path.join(dir, e.name));
109
+ else fileCount++;
110
+ }
111
+ };
112
+ await countFiles(stagingDir);
113
+
114
+ // Insert metadata
115
+ const tools = toolResults.map(r => r.adapter.name);
116
+ const metaRes = await fetch(`${SUPABASE_URL}/rest/v1/backups`, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Authorization': `Bearer ${session.access_token}`,
120
+ 'apikey': SUPABASE_ANON_KEY,
121
+ 'Content-Type': 'application/json',
122
+ 'Prefer': 'return=representation',
123
+ },
124
+ body: JSON.stringify({
125
+ user_id: session.user.id,
126
+ tool_count: tools.length,
127
+ file_count: fileCount,
128
+ size_bytes: gzipped.length,
129
+ tools,
130
+ storage_path: storagePath,
131
+ machine_name: os.hostname(),
132
+ version: nextVersion,
133
+ }),
134
+ });
135
+
136
+ if (!metaRes.ok) {
137
+ const err = await metaRes.text();
138
+ throw new Error(`Failed to save backup metadata: ${err}`);
139
+ }
140
+
141
+ const backup = (await metaRes.json())[0];
142
+ return { ...backup, sizeBytes: gzipped.length };
143
+ }
144
+
145
+ // Download a specific backup
146
+ export async function downloadBackup(backup, destDir, session) {
147
+ const res = await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}/${backup.storage_path}`, {
148
+ headers: {
149
+ 'Authorization': `Bearer ${session.access_token}`,
150
+ 'apikey': SUPABASE_ANON_KEY,
151
+ },
152
+ });
153
+
154
+ if (!res.ok) throw new Error(`Download failed: ${await res.text()}`);
155
+
156
+ const gzipped = Buffer.from(await res.arrayBuffer());
157
+ const fileCount = await unbundleToDir(gzipped, destDir);
158
+ return fileCount;
159
+ }
160
+
161
+ // List backups for user
162
+ export async function listBackups(session) {
163
+ const res = await fetch(
164
+ `${SUPABASE_URL}/rest/v1/backups?select=*&user_id=eq.${session.user.id}&order=created_at.desc`,
165
+ {
166
+ headers: {
167
+ 'Authorization': `Bearer ${session.access_token}`,
168
+ 'apikey': SUPABASE_ANON_KEY,
169
+ },
170
+ }
171
+ );
172
+
173
+ if (!res.ok) throw new Error('Failed to fetch backup history');
174
+ return res.json();
175
+ }
176
+
177
+ // Delete old backups beyond the limit
178
+ export async function cleanupOldBackups(session, isPro) {
179
+ const maxBackups = isPro ? MAX_BACKUPS_PRO : MAX_BACKUPS_FREE;
180
+ const backups = await listBackups(session);
181
+
182
+ if (backups.length <= maxBackups) return 0;
183
+
184
+ const toDelete = backups.slice(maxBackups);
185
+ let deleted = 0;
186
+
187
+ for (const backup of toDelete) {
188
+ // Delete from storage
189
+ await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}/${backup.storage_path}`, {
190
+ method: 'DELETE',
191
+ headers: {
192
+ 'Authorization': `Bearer ${session.access_token}`,
193
+ 'apikey': SUPABASE_ANON_KEY,
194
+ },
195
+ });
196
+
197
+ // Delete metadata row
198
+ await fetch(`${SUPABASE_URL}/rest/v1/backups?id=eq.${backup.id}`, {
199
+ method: 'DELETE',
200
+ headers: {
201
+ 'Authorization': `Bearer ${session.access_token}`,
202
+ 'apikey': SUPABASE_ANON_KEY,
203
+ },
204
+ });
205
+
206
+ deleted++;
207
+ }
208
+
209
+ return deleted;
210
+ }
211
+
212
+ export { bundleDir, unbundleToDir };
@@ -0,0 +1,173 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import ora from 'ora';
6
+ import boxen from 'boxen';
7
+ import gradient from 'gradient-string';
8
+ import { getSession, getSubscription } from '../cloud/auth.js';
9
+ import { uploadBackup, downloadBackup, listBackups, cleanupOldBackups } from '../cloud/storage.js';
10
+ import { extractMemories } from '../adapters/index.js';
11
+ import { MAX_BACKUPS_FREE } from '../cloud/constants.js';
12
+
13
+ export async function cloudPushCommand(options = {}) {
14
+ const session = await getSession();
15
+ if (!session) {
16
+ console.log('\n' + boxen(
17
+ chalk.red('✖ Not logged in') + '\n\n' +
18
+ chalk.white('Run ') + chalk.cyan('memoir login') + chalk.white(' first.'),
19
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
20
+ ) + '\n');
21
+ return;
22
+ }
23
+
24
+ const sub = await getSubscription(session);
25
+ const isPro = sub.status === 'pro';
26
+
27
+ // Check backup count for free users
28
+ if (!isPro) {
29
+ const existing = await listBackups(session);
30
+ if (existing.length >= MAX_BACKUPS_FREE) {
31
+ console.log('\n' + boxen(
32
+ chalk.yellow('Free plan limit reached') + '\n\n' +
33
+ chalk.white(`You have ${existing.length}/${MAX_BACKUPS_FREE} backups.`) + '\n' +
34
+ chalk.white('Oldest backup will be replaced.') + '\n\n' +
35
+ chalk.gray('Upgrade to Pro for 50 backups + version history.'),
36
+ { padding: 1, borderStyle: 'round', borderColor: 'yellow' }
37
+ ) + '\n');
38
+ }
39
+ }
40
+
41
+ console.log();
42
+ const spinner = ora({ text: chalk.gray('Scanning AI tools...'), spinner: 'dots' }).start();
43
+
44
+ const stagingDir = path.join(os.tmpdir(), `memoir-cloud-${Date.now()}`);
45
+ await fs.ensureDir(stagingDir);
46
+
47
+ try {
48
+ const onlyFilter = options.only ? options.only.split(',').map(t => t.trim().toLowerCase()) : null;
49
+ const foundAny = await extractMemories(stagingDir, spinner, onlyFilter);
50
+
51
+ if (!foundAny) {
52
+ spinner.fail(chalk.yellow('No AI tools found to back up.'));
53
+ return;
54
+ }
55
+
56
+ // Collect tool results for metadata
57
+ const toolResults = [];
58
+ const entries = await fs.readdir(stagingDir, { withFileTypes: true });
59
+ for (const entry of entries) {
60
+ if (entry.isDirectory()) {
61
+ toolResults.push({ adapter: { name: entry.name } });
62
+ }
63
+ }
64
+
65
+ spinner.start(chalk.gray('Uploading to memoir cloud...'));
66
+
67
+ const backup = await uploadBackup(stagingDir, session, toolResults);
68
+
69
+ // Cleanup old backups
70
+ const deleted = await cleanupOldBackups(session, isPro);
71
+
72
+ spinner.stop();
73
+
74
+ const sizeStr = backup.sizeBytes < 1024
75
+ ? `${backup.sizeBytes}B`
76
+ : backup.sizeBytes < 1024 * 1024
77
+ ? `${(backup.sizeBytes / 1024).toFixed(1)}KB`
78
+ : `${(backup.sizeBytes / (1024 * 1024)).toFixed(1)}MB`;
79
+
80
+ console.log(boxen(
81
+ gradient.pastel(' Backed up to cloud ') + '\n\n' +
82
+ chalk.green('✔ ') + chalk.white(`Version ${backup.version}`) + '\n' +
83
+ chalk.gray(` ${backup.file_count} files, ${sizeStr}`) + '\n' +
84
+ chalk.gray(` from ${os.hostname()}`) +
85
+ (deleted > 0 ? '\n' + chalk.gray(` ${deleted} old backup${deleted > 1 ? 's' : ''} cleaned up`) : '') + '\n\n' +
86
+ chalk.gray('Restore with: ') + chalk.cyan('memoir cloud restore'),
87
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
88
+ ) + '\n');
89
+
90
+ } catch (error) {
91
+ spinner.fail(chalk.red('Cloud push failed: ') + error.message);
92
+ } finally {
93
+ await fs.remove(stagingDir);
94
+ }
95
+ }
96
+
97
+ export async function cloudRestoreCommand(options = {}) {
98
+ const session = await getSession();
99
+ if (!session) {
100
+ console.log('\n' + boxen(
101
+ chalk.red('✖ Not logged in') + '\n\n' +
102
+ chalk.white('Run ') + chalk.cyan('memoir login') + chalk.white(' first.'),
103
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
104
+ ) + '\n');
105
+ return;
106
+ }
107
+
108
+ console.log();
109
+ const spinner = ora({ text: chalk.gray('Fetching from memoir cloud...'), spinner: 'dots' }).start();
110
+
111
+ try {
112
+ const backups = await listBackups(session);
113
+
114
+ if (backups.length === 0) {
115
+ spinner.fail(chalk.yellow('No backups found in the cloud.'));
116
+ console.log(chalk.gray('\n Run ') + chalk.cyan('memoir cloud push') + chalk.gray(' to create your first backup.\n'));
117
+ return;
118
+ }
119
+
120
+ // Use specified version or latest
121
+ let backup;
122
+ if (options.version) {
123
+ backup = backups.find(b => b.version === parseInt(options.version));
124
+ if (!backup) {
125
+ spinner.fail(chalk.red(`Version ${options.version} not found.`));
126
+ console.log(chalk.gray('\n Run ') + chalk.cyan('memoir history') + chalk.gray(' to see available versions.\n'));
127
+ return;
128
+ }
129
+ } else {
130
+ backup = backups[0]; // Latest
131
+ }
132
+
133
+ spinner.text = chalk.gray(`Downloading version ${backup.version}...`);
134
+
135
+ const stagingDir = path.join(os.tmpdir(), `memoir-cloud-restore-${Date.now()}`);
136
+ await fs.ensureDir(stagingDir);
137
+
138
+ const fileCount = await downloadBackup(backup, stagingDir, session);
139
+
140
+ spinner.text = chalk.gray('Restoring files...');
141
+
142
+ // Use the existing restore logic
143
+ const { restoreMemories } = await import('../adapters/restore.js');
144
+ const onlyFilter = options.only ? options.only.split(',').map(t => t.trim().toLowerCase()) : null;
145
+ const autoYes = options.yes || false;
146
+
147
+ const restored = await restoreMemories(stagingDir, spinner, onlyFilter, autoYes);
148
+
149
+ spinner.stop();
150
+
151
+ if (restored) {
152
+ const date = new Date(backup.created_at).toLocaleDateString();
153
+ console.log(boxen(
154
+ gradient.pastel(' Restored from cloud ') + '\n\n' +
155
+ chalk.green('✔ ') + chalk.white(`Version ${backup.version}`) + chalk.gray(` from ${date}`) + '\n' +
156
+ chalk.gray(` ${backup.tools.join(', ')}`) + '\n' +
157
+ (backup.machine_name ? chalk.gray(` Originally from ${backup.machine_name}`) + '\n' : '') + '\n' +
158
+ chalk.gray('Restart your AI tools to pick up the changes.'),
159
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
160
+ ) + '\n');
161
+ } else {
162
+ console.log('\n' + boxen(
163
+ chalk.yellow('Nothing was restored.'),
164
+ { padding: 1, borderStyle: 'round', borderColor: 'yellow' }
165
+ ) + '\n');
166
+ }
167
+
168
+ await fs.remove(stagingDir);
169
+
170
+ } catch (error) {
171
+ spinner.fail(chalk.red('Cloud restore failed: ') + error.message);
172
+ }
173
+ }
@@ -0,0 +1,65 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import gradient from 'gradient-string';
4
+ import { getSession } from '../cloud/auth.js';
5
+ import { listBackups } from '../cloud/storage.js';
6
+
7
+ export async function historyCommand() {
8
+ const session = await getSession();
9
+ if (!session) {
10
+ console.log('\n' + boxen(
11
+ chalk.red('✖ Not logged in') + '\n\n' +
12
+ chalk.white('Run ') + chalk.cyan('memoir login') + chalk.white(' first.'),
13
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
14
+ ) + '\n');
15
+ return;
16
+ }
17
+
18
+ try {
19
+ const backups = await listBackups(session);
20
+
21
+ if (backups.length === 0) {
22
+ console.log('\n' + boxen(
23
+ chalk.yellow('No backups yet.') + '\n\n' +
24
+ chalk.gray('Run ') + chalk.cyan('memoir cloud push') + chalk.gray(' to create your first backup.'),
25
+ { padding: 1, borderStyle: 'round', borderColor: 'yellow' }
26
+ ) + '\n');
27
+ return;
28
+ }
29
+
30
+ console.log();
31
+
32
+ const lines = backups.map((b, i) => {
33
+ const date = new Date(b.created_at);
34
+ const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
35
+ const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
36
+
37
+ const sizeStr = b.size_bytes < 1024
38
+ ? `${b.size_bytes}B`
39
+ : b.size_bytes < 1024 * 1024
40
+ ? `${(b.size_bytes / 1024).toFixed(1)}KB`
41
+ : `${(b.size_bytes / (1024 * 1024)).toFixed(1)}MB`;
42
+
43
+ const latest = i === 0 ? chalk.green(' ← latest') : '';
44
+ const machine = b.machine_name ? chalk.gray(` from ${b.machine_name}`) : '';
45
+
46
+ return (
47
+ chalk.white.bold(` v${b.version}`) + ` ${dateStr} ${timeStr}` + latest + '\n' +
48
+ chalk.gray(` ${b.file_count} files, ${sizeStr}`) + machine + '\n' +
49
+ chalk.gray(` ${b.tools.join(', ')}`)
50
+ );
51
+ });
52
+
53
+ console.log(boxen(
54
+ gradient.pastel(' memoir history ') + '\n\n' +
55
+ chalk.gray(`${session.user.email} — ${backups.length} backup${backups.length !== 1 ? 's' : ''}`) + '\n\n' +
56
+ lines.join('\n\n') + '\n\n' +
57
+ chalk.gray('─'.repeat(36)) + '\n' +
58
+ chalk.gray('Restore a version: ') + chalk.cyan('memoir cloud restore --version 3'),
59
+ { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
60
+ ) + '\n');
61
+
62
+ } catch (error) {
63
+ console.log('\n' + chalk.red('Error: ') + error.message + '\n');
64
+ }
65
+ }
@@ -0,0 +1,93 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import gradient from 'gradient-string';
4
+ import inquirer from 'inquirer';
5
+ import { signIn, signUp, saveSession, getSession, logout, getSubscription } from '../cloud/auth.js';
6
+
7
+ export async function loginCommand() {
8
+ // Check if already logged in
9
+ const existing = await getSession();
10
+ if (existing) {
11
+ const sub = await getSubscription(existing);
12
+ console.log('\n' + boxen(
13
+ gradient.pastel(' memoir cloud ') + '\n\n' +
14
+ chalk.green('✔ Already logged in as ') + chalk.cyan(existing.user.email) + '\n' +
15
+ chalk.gray('Plan: ') + (sub.status === 'pro' ? chalk.green('Pro') : chalk.yellow('Free')),
16
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
17
+ ) + '\n');
18
+ return;
19
+ }
20
+
21
+ console.log();
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
+ }]);
32
+
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
+ }]);
39
+
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
+ }]);
47
+
48
+ try {
49
+ let session;
50
+
51
+ if (action === 'signup') {
52
+ const result = await signUp(email, password);
53
+ if (result.access_token) {
54
+ session = await saveSession(result);
55
+ } else {
56
+ // Email confirmation required
57
+ console.log('\n' + boxen(
58
+ chalk.green('✔ Account created!') + '\n\n' +
59
+ chalk.white('Check your email to confirm, then run ') + chalk.cyan('memoir login') + chalk.white(' again.'),
60
+ { padding: 1, borderStyle: 'round', borderColor: 'green' }
61
+ ) + '\n');
62
+ return;
63
+ }
64
+ } else {
65
+ const result = await signIn(email, password);
66
+ session = await saveSession(result);
67
+ }
68
+
69
+ const sub = await getSubscription(session);
70
+
71
+ console.log('\n' + boxen(
72
+ gradient.pastel(' memoir cloud ') + '\n\n' +
73
+ chalk.green('✔ Logged in as ') + chalk.cyan(session.user.email) + '\n' +
74
+ chalk.gray('Plan: ') + (sub.status === 'pro' ? chalk.green('Pro') : chalk.yellow('Free')) + '\n\n' +
75
+ chalk.gray('Try: ') + chalk.cyan('memoir cloud push') + chalk.gray(' to back up to the cloud'),
76
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
77
+ ) + '\n');
78
+
79
+ } catch (error) {
80
+ console.log('\n' + boxen(
81
+ chalk.red('✖ ' + error.message),
82
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
83
+ ) + '\n');
84
+ }
85
+ }
86
+
87
+ export async function logoutCommand() {
88
+ await logout();
89
+ console.log('\n' + boxen(
90
+ chalk.green('✔ Logged out'),
91
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
92
+ ) + '\n');
93
+ }