memoir-cli 3.3.0 → 3.4.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/README.md CHANGED
@@ -2,37 +2,27 @@
2
2
 
3
3
  # memoir
4
4
 
5
- **Persistent memory for AI coding tools.**
5
+ **Portable memory for every AI coding tool.**
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
9
  [![GitHub stars](https://img.shields.io/github/stars/camgitt/memoir?style=flat-square&color=7c6ef0)](https://github.com/camgitt/memoir/stargazers)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
11
- [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](https://nodejs.org)
12
-
13
- Your AI forgets everything between sessions. memoir gives it long-term memory via MCP.
14
-
15
- **11 tools supported** • **E2E encrypted** • **Cross-platform** • **Open source**
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)
20
-
21
- <br />
22
-
23
- <img src="demo.svg" alt="memoir demo — push, restore, and sync AI memory" width="700" />
24
11
 
25
12
  </div>
26
13
 
27
- ## The Problem
14
+ ```bash
15
+ npm install -g memoir-cli
16
+ memoir activate
17
+ ```
28
18
 
29
- You told Claude your project uses Zustand, not Redux. You explained your auth middleware to Cursor. You spent weeks building up context across your AI tools.
19
+ Your AI now remembers across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini, Copilot, and 6 more tools.
30
20
 
31
- Next session? **Gone.** New machine? **Gone.** Switch tools? **Gone.**
21
+ ---
32
22
 
33
- ## How memoir fixes it
23
+ ## What it does
34
24
 
35
- memoir is a **persistent memory layer** that runs via [MCP](https://modelcontextprotocol.io) inside your AI tools. Your AI can search, read, and save memories automatically — across sessions, tools, and machines.
25
+ memoir is an [MCP server](https://modelcontextprotocol.io) that gives your AI tools persistent memory. Your AI can search, save, and recall context automatically.
36
26
 
37
27
  ```
38
28
  you: how does auth work in this project?
@@ -47,38 +37,15 @@ claude: Based on your previous sessions: this project uses JWT auth
47
37
 
48
38
  No re-explaining. memoir remembered.
49
39
 
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.
40
+ ## Setup
70
41
 
71
- ## Quick Start
42
+ ### 1. Install
72
43
 
73
44
  ```bash
74
- # Install
75
45
  npm install -g memoir-cli
76
-
77
- # Setup
78
- memoir init
79
46
  ```
80
47
 
81
- ### Add MCP to your AI tools
48
+ ### 2. Add MCP to your AI tool
82
49
 
83
50
  **Claude Code** — add to `~/.mcp.json`:
84
51
  ```json
@@ -98,7 +65,13 @@ memoir init
98
65
  }
99
66
  ```
100
67
 
101
- That's it. Your AI tools now have 6 memory abilities:
68
+ ### 3. Activate in your project
69
+
70
+ ```bash
71
+ memoir activate
72
+ ```
73
+
74
+ That's it. Your AI now has 6 memory tools:
102
75
 
103
76
  | MCP Tool | What it does |
104
77
  |----------|-------------|
@@ -109,49 +82,28 @@ That's it. Your AI tools now have 6 memory abilities:
109
82
  | `memoir_status` | See which AI tools are detected |
110
83
  | `memoir_profiles` | Switch between work/personal |
111
84
 
112
- ## What Gets Synced
113
-
114
- memoir syncs three layers that no other tool connects:
85
+ ## Why memoir
115
86
 
116
- ### Layer 1: AI Memory (via MCP + CLI)
117
- Configs, instructions, and project knowledge across 11 tools — searchable and writable from inside any AI conversation.
87
+ Your AI forgets everything between sessions. You re-explain your codebase, your conventions, your decisions — every time.
118
88
 
119
- | Tool | What gets synced |
120
- |------|-----------------|
121
- | **Claude Code** | ~/.claude/ settings, memory, CLAUDE.md files |
122
- | **Gemini CLI** | ~/.gemini/ config, GEMINI.md files |
123
- | **ChatGPT** | CHATGPT.md custom instructions |
124
- | **OpenAI Codex** | ~/.codex/ config, AGENTS.md |
125
- | **Cursor** | Settings, keybindings, .cursorrules |
126
- | **GitHub Copilot** | Config, copilot-instructions.md |
127
- | **Windsurf** | Settings, keybindings, .windsurfrules |
128
- | **Zed** | Settings, keymap, tasks |
129
- | **Cline** | Settings, .clinerules |
130
- | **Continue.dev** | Config, .continuerules |
131
- | **Aider** | .aider.conf.yml, system prompt |
89
+ memoir fixes this by giving your AI a shared memory layer that works across **every tool you use**. Tell Claude something once. Cursor knows it too.
132
90
 
133
- ### Layer 2: Session State
134
- What you were **doing** — not just what your AI knows, but the active context.
91
+ **11 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev
135
92
 
136
- - Last coding session captured automatically
137
- - What files you changed, what errors you hit, what decisions you made
138
- - Injected into your AI on restore so it picks up mid-conversation
139
- - Secrets auto-redacted (API keys, tokens, passwords stripped before sync)
93
+ ## Sync across machines
140
94
 
141
- ### Layer 3: Workspace
142
- Your actual projects — code, files, everything.
95
+ ```bash
96
+ # Back up
97
+ memoir push
143
98
 
144
- - **Git projects:** Remote URLs saved, auto-cloned on restore
145
- - **Non-git projects:** Bundled as compressed archives, unpacked on restore
146
- - **Uncommitted work:** Saved as patches, applied after clone
147
- - **Zero git commands needed** — memoir handles it all
99
+ # Restore on any machine
100
+ memoir restore -y
101
+ ```
148
102
 
149
- ## Key Features
103
+ Push syncs AI memory, session context, workspace (git repos + uncommitted work), and project configs. E2E encrypted with AES-256-GCM.
150
104
 
151
- ### Cross-tool memory
152
- Tell Claude something once. Cursor knows it too. memoir is the shared memory layer between all your AI tools.
105
+ ## Translate between AI tools
153
106
 
154
- ### Translate between AI tools
155
107
  ```bash
156
108
  memoir migrate --from chatgpt --to claude
157
109
  # AI-powered — rewrites conventions, not copy-paste
@@ -160,56 +112,25 @@ memoir migrate --from chatgpt --to all
160
112
  # Translate to every tool at once
161
113
  ```
162
114
 
163
- ### Cross-machine sync
164
- ```bash
165
- # On your main machine
166
- memoir push
167
-
168
- # On any other machine
169
- memoir restore -y
170
- # ✔ AI memory restored (Claude, Gemini, Cursor, 11 tools)
171
- # ✔ 44 projects cloned & unpacked
172
- # ✔ Uncommitted changes applied
173
- # ✔ Session context injected — AI picks up mid-conversation
174
- ```
175
-
176
- ### E2E Encryption
177
- ```bash
178
- memoir encrypt # toggle encryption on/off
179
- memoir push # prompted for passphrase, AES-256-GCM encrypted
180
- ```
181
-
182
- Your backup is encrypted before it leaves your machine. Secret scanning auto-redacts API keys, tokens, and passwords.
183
-
184
- ### Profiles (personal / work)
185
- ```bash
186
- memoir profile create work
187
- memoir push --profile work
188
- memoir profile switch personal
189
- ```
115
+ ## Cloud sync
190
116
 
191
- ### Cloud sync
192
117
  ```bash
193
118
  memoir login
194
119
  memoir cloud push # encrypted cloud backup
195
120
  memoir cloud restore # restore from any version
121
+ memoir history # view backup versions
122
+ memoir share # create encrypted shareable link
196
123
  ```
197
124
 
198
- ### Cross-platform (Mac / Windows / Linux)
199
- Paths remap automatically between platforms. Push from Mac, restore on Windows. It just works.
200
-
201
125
  ## All Commands
202
126
 
203
127
  | Command | What it does |
204
128
  |---------|-------------|
205
- | `memoir init` | Setup wizard GitHub or local storage |
129
+ | `memoir activate` | Enable auto-recall in this project |
130
+ | `memoir deactivate` | Remove memoir from this project |
206
131
  | `memoir push` | Back up AI memory + workspace + session |
207
132
  | `memoir restore` | Restore everything on a new machine |
208
- | `memoir mcp` | Start MCP server for editor integration |
209
133
  | `memoir status` | Show detected AI tools |
210
- | `memoir doctor` | Diagnose issues, scan for secrets |
211
- | `memoir view` | Preview what's in your backup |
212
- | `memoir diff` | Show changes since last backup |
213
134
  | `memoir migrate` | Translate memory between tools via AI |
214
135
  | `memoir snapshot` | Capture current coding session |
215
136
  | `memoir resume` | Pick up where you left off |
@@ -217,95 +138,24 @@ Paths remap automatically between platforms. Push from Mac, restore on Windows.
217
138
  | `memoir profile` | Manage profiles (personal/work) |
218
139
  | `memoir cloud push` | Back up to memoir cloud |
219
140
  | `memoir cloud restore` | Restore from memoir cloud |
220
- | `memoir history` | View cloud backup versions |
221
- | `memoir login` | Sign in to memoir cloud |
222
141
  | `memoir share` | Create encrypted shareable link |
223
- | `memoir upgrade` | View plans and upgrade |
142
+ | `memoir doctor` | Diagnose issues |
143
+ | `memoir diff` | Show changes since last backup |
144
+ | `memoir view` | Preview what's in your backup |
224
145
  | `memoir update` | Self-update to latest version |
225
146
 
226
- ## How memoir compares
227
-
228
- | Feature | memoir | dotfiles managers | ai-rulez | memories.sh |
229
- |---------|--------|-------------------|----------|-------------|
230
- | MCP memory layer | **6 tools** | No | No | No |
231
- | AI memory sync | **11 tools** | No | 18 tools | 3 tools |
232
- | Cross-tool recall | **Yes** | No | No | No |
233
- | Workspace sync | **Yes** | No | No | No |
234
- | Session handoff | **Yes** | No | No | No |
235
- | AI-powered migration | **Yes** | No | No | No |
236
- | E2E encryption | **Yes** | No | No | No |
237
- | Secret scanning | **Yes** | Some | No | No |
238
- | Cross-platform remap | **Yes** | Some | No | No |
239
- | Cloud backup | **Yes** | No | No | Yes ($15/mo) |
240
- | Profiles | **Yes** | No | No | No |
241
- | Free & open source | **Yes** | Yes | Yes | No |
242
-
243
- ## Common Workflows
244
-
245
- ### Your AI remembers across sessions
246
- ```
247
- # Monday — you explain your auth setup to Claude
248
- # ...Claude calls memoir_remember to save the decision
249
-
250
- # Thursday — new conversation
251
- you: "add a protected route"
252
- # Claude calls memoir_recall, finds your auth architecture
253
- # No re-explaining needed
254
- ```
255
-
256
- ### New machine setup
257
- ```bash
258
- npm install -g memoir-cli && memoir init && memoir restore -y
259
- # Done. Everything's back.
260
- ```
261
-
262
- ### Switching AI tools
263
- ```bash
264
- memoir migrate --from chatgpt --to claude
265
- # Your custom instructions become a proper CLAUDE.md
266
- ```
267
-
268
- ### Team onboarding
269
- ```bash
270
- # Senior dev pushes team config
271
- memoir push --profile team
272
-
273
- # New hire runs one command
274
- memoir restore --profile team
275
- # Every project cloned. Every AI tool configured. Day one productive.
276
- ```
277
-
278
147
  ## Security
279
148
 
280
149
  - **E2E encryption** — AES-256-GCM with scrypt key derivation
281
- - **Secret scanning** — 20+ patterns detect API keys, tokens, passwords, connection strings
282
- - **Auto-redaction** — secrets stripped from session handoffs before sync
283
- - **No credentials synced** — .env files, auth tokens, and API keys are never included
284
- - **Passphrase verified** — wrong passphrase caught before decrypt attempt
285
- - **Local MCP server** — runs on your machine, no data sent to external services
286
-
287
- ## Requirements
288
-
289
- - Node.js >= 18
290
- - Git (for workspace sync)
291
- - Works on macOS, Windows, Linux
292
-
293
- ## Contributing
294
-
295
- Contributions welcome — especially new tool adapters and MCP improvements.
296
-
297
- 1. Fork the repo
298
- 2. Create your branch (`git checkout -b feature/my-feature`)
299
- 3. Commit and push
300
- 4. Open a PR
150
+ - **Secret scanning** — API keys, tokens, passwords auto-redacted before sync
151
+ - **Local MCP server** — runs on your machine, no data sent externally
152
+ - **Zero-knowledge cloud** — encrypted before upload
301
153
 
302
154
  ## Links
303
155
 
304
156
  - **Website:** [memoir.sh](https://memoir.sh)
305
157
  - **npm:** [memoir-cli](https://npmjs.org/package/memoir-cli)
306
- - **Blog:** [memoir.sh/blog](https://memoir.sh/blog)
307
158
  - **Issues:** [GitHub Issues](https://github.com/camgitt/memoir/issues)
159
+ - **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
308
160
 
309
- ## License
310
-
311
- MIT
161
+ MIT Licensed
package/bin/memoir.js CHANGED
@@ -14,12 +14,13 @@ 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, forgotPasswordCommand } 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';
21
21
  import { projectsListCommand, projectsTodoCommand } from '../src/commands/projects.js';
22
22
  import { upgradeCommand } from '../src/commands/upgrade.js';
23
+ import { activateCommand, deactivateCommand } from '../src/commands/activate.js';
23
24
  import { createRequire } from 'module';
24
25
 
25
26
  const require = createRequire(import.meta.url);
@@ -66,6 +67,7 @@ if (process.argv.length <= 2) {
66
67
  chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
67
68
  chalk.cyan(' memoir profile ') + chalk.gray('— manage profiles (personal/work)') + '\n' +
68
69
  chalk.cyan(' memoir projects ') + chalk.gray('— see all your projects at a glance') + '\n' +
70
+ chalk.cyan(' memoir activate ') + chalk.gray('— enable auto-recall in this project') + '\n' +
69
71
  chalk.cyan(' memoir encrypt ') + chalk.gray('— toggle E2E encryption') + '\n' +
70
72
  chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n' +
71
73
  chalk.cyan(' memoir upgrade ') + chalk.gray('— view plans & upgrade') + '\n\n' +
@@ -97,9 +99,16 @@ program
97
99
  program
98
100
  .command('init')
99
101
  .description('Set up memoir with your storage provider')
100
- .action(async () => {
102
+ .option('--direction <direction>', 'Upload or download (upload, download)')
103
+ .option('--provider <provider>', 'Storage provider (git, local)')
104
+ .option('--local-path <path>', 'Local folder path (for local provider)')
105
+ .option('--username <name>', 'GitHub username (for git provider)')
106
+ .option('--repo <name>', 'GitHub repo name (for git provider)')
107
+ .option('--encrypt', 'Enable E2E encryption')
108
+ .option('--no-encrypt', 'Disable E2E encryption')
109
+ .action(async (options) => {
101
110
  try {
102
- await initCommand();
111
+ await initCommand(options);
103
112
  } catch (err) {
104
113
  console.error(chalk.red('\n✖ Error during initialization:'), err.message);
105
114
  process.exit(1);
@@ -291,10 +300,36 @@ program
291
300
  }
292
301
  });
293
302
 
303
+ program
304
+ .command('activate')
305
+ .description('Add memoir instructions to this project so your AI uses it automatically')
306
+ .action(async () => {
307
+ try {
308
+ await activateCommand();
309
+ } catch (err) {
310
+ console.error(chalk.red('\n✖ Error:'), err.message);
311
+ process.exit(1);
312
+ }
313
+ });
314
+
315
+ program
316
+ .command('deactivate')
317
+ .description('Remove memoir instructions from this project')
318
+ .action(async () => {
319
+ try {
320
+ await deactivateCommand();
321
+ } catch (err) {
322
+ console.error(chalk.red('\n✖ Error:'), err.message);
323
+ process.exit(1);
324
+ }
325
+ });
326
+
294
327
  program
295
328
  .command('encrypt')
296
329
  .description('Toggle E2E encryption for your backups')
297
- .action(async () => {
330
+ .option('--on', 'Enable encryption without prompting')
331
+ .option('--off', 'Disable encryption without prompting')
332
+ .action(async (options) => {
298
333
  try {
299
334
  const { getConfig, getRawConfig, saveConfig, migrateConfigToV2 } = await import('../src/config.js');
300
335
  const config = await getConfig();
@@ -304,24 +339,34 @@ program
304
339
  }
305
340
  const current = config.encrypt || false;
306
341
  console.log(chalk.white(`\n Encryption is currently: ${current ? chalk.green('ON') : chalk.red('OFF')}`));
307
- const inquirer = (await import('inquirer')).default;
308
- const { toggle } = await inquirer.prompt([{
309
- type: 'confirm',
310
- name: 'toggle',
311
- message: current ? 'Disable encryption?' : 'Enable encryption?',
312
- default: !current
313
- }]);
314
- if (toggle !== current) {
342
+
343
+ let newValue;
344
+ if (options.on) {
345
+ newValue = true;
346
+ } else if (options.off) {
347
+ newValue = false;
348
+ } else {
349
+ const inquirer = (await import('inquirer')).default;
350
+ const { toggle } = await inquirer.prompt([{
351
+ type: 'confirm',
352
+ name: 'toggle',
353
+ message: current ? 'Disable encryption?' : 'Enable encryption?',
354
+ default: !current
355
+ }]);
356
+ newValue = toggle ? !current : current;
357
+ }
358
+
359
+ if (newValue !== current) {
315
360
  let raw = await getRawConfig();
316
361
  if (!raw.version || raw.version < 2) raw = migrateConfigToV2(raw);
317
362
  const profileName = raw.activeProfile || 'default';
318
363
  if (raw.profiles?.[profileName]) {
319
- raw.profiles[profileName].encrypt = !current;
364
+ raw.profiles[profileName].encrypt = newValue;
320
365
  } else {
321
- raw.encrypt = !current;
366
+ raw.encrypt = newValue;
322
367
  }
323
368
  await saveConfig(raw);
324
- console.log(chalk.green(`\n ✔ Encryption ${!current ? 'enabled' : 'disabled'}. Next push will ${!current ? 'encrypt' : 'skip encryption'}.\n`));
369
+ console.log(chalk.green(`\n ✔ Encryption ${newValue ? 'enabled' : 'disabled'}. Next push will ${newValue ? 'encrypt' : 'skip encryption'}.\n`));
325
370
  }
326
371
  } catch (err) {
327
372
  console.error(chalk.red('\n✖ Error:'), err.message);
@@ -386,6 +431,22 @@ program
386
431
  }
387
432
  });
388
433
 
434
+ // Account management
435
+ const account = program.command('account').description('Manage your memoir account');
436
+
437
+ account
438
+ .command('delete')
439
+ .description('Permanently delete your account and all cloud data')
440
+ .option('--confirm', 'Skip interactive prompt (Node 25 workaround)')
441
+ .action(async (options) => {
442
+ try {
443
+ await deleteAccountCommand(options);
444
+ } catch (err) {
445
+ console.error(chalk.red('\n✖ Error:'), err.message);
446
+ process.exit(1);
447
+ }
448
+ });
449
+
389
450
  // Cloud sync
390
451
  const cloud = program.command('cloud').description('Cloud backup and restore (Pro)');
391
452
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "mcpName": "io.github.camgitt/memoir",
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.",
5
+ "description": "MCP server that gives Claude, Cursor, and Gemini long-term memory across sessions. Your AI remembers your codebase, decisions, and preferences across tools and machines.",
6
6
  "main": "src/index.js",
7
7
  "type": "module",
8
8
  "bin": {
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "scripts": {
21
21
  "start": "node bin/memoir.js",
22
- "test": "bash test-local.sh"
22
+ "test": "bash test-local.sh",
23
+ "postinstall": "node -e \"try{const c='\\x1b[36m',r='\\x1b[0m',g='\\x1b[90m';console.log('\\n '+c+'memoir'+r+' installed.\\n Run '+c+'memoir activate'+r+' in any project to give your AI long-term memory.\\n '+g+'https://memoir.sh'+r+'\\n')}catch{}\""
23
24
  },
24
25
  "keywords": [
25
26
  "mcp",
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
@@ -131,4 +131,61 @@ export async function resetPassword(email) {
131
131
  }
132
132
  }
133
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
+
134
191
  export { AUTH_FILE, supaFetch };
@@ -1,5 +1,5 @@
1
1
  export const SUPABASE_URL = process.env.MEMOIR_SUPABASE_URL || 'https://oqrkxytbahfwjhcbyzrx.supabase.co';
2
2
  export const SUPABASE_ANON_KEY = process.env.MEMOIR_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xcmt4eXRiYWhmd2poY2J5enJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyMTQ4MzMsImV4cCI6MjA4ODc5MDgzM30.jOKOi73OJgIgi1zj0VOIQkGp0xqS3ee4gfCjpdqCnvM';
3
3
  export const STORAGE_BUCKET = 'memoir-backups';
4
- export const MAX_BACKUPS_FREE = 3;
4
+ export const MAX_BACKUPS_FREE = 100;
5
5
  export const MAX_BACKUPS_PRO = 50;
@@ -0,0 +1,232 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import inquirer from 'inquirer';
6
+
7
+ // The instruction files each AI tool reads, in priority order
8
+ const INSTRUCTION_FILES = [
9
+ { file: 'CLAUDE.md', tool: 'Claude' },
10
+ { file: '.cursorrules', tool: 'Cursor' },
11
+ { file: '.windsurfrules', tool: 'Windsurf' },
12
+ { file: '.github/copilot-instructions.md', tool: 'Copilot' },
13
+ { file: '.clinerules', tool: 'Cline' },
14
+ { file: 'GEMINI.md', tool: 'Gemini' },
15
+ { file: '.aider.system-prompt.md', tool: 'Aider' },
16
+ ];
17
+
18
+ const BLOCK_START = '<!-- memoir:start -->';
19
+ const BLOCK_END = '<!-- memoir:end -->';
20
+
21
+ const MEMOIR_BLOCK = `${BLOCK_START}
22
+ # Memoir — Persistent Memory
23
+ <!-- Cross-session memory for AI tools — https://memoir.sh -->
24
+ <!-- Install: npm i -g memoir-cli -->
25
+ Use memoir_recall to search past context before answering project questions.
26
+ Use memoir_remember to save important decisions, architecture choices, or context worth keeping.
27
+ ${BLOCK_END}`;
28
+
29
+ /**
30
+ * Detect which instruction files exist in the current project.
31
+ * Returns array of { file, tool, fullPath, exists }
32
+ */
33
+ function detectInstructionFiles(projectDir) {
34
+ return INSTRUCTION_FILES.map(({ file, tool }) => {
35
+ const fullPath = path.join(projectDir, file);
36
+ return { file, tool, fullPath, exists: fs.existsSync(fullPath) };
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Check if a file already has the memoir block
42
+ */
43
+ function hasMemoir(content) {
44
+ return content.includes(BLOCK_START);
45
+ }
46
+
47
+ /**
48
+ * Inject memoir block into a file (append, or create)
49
+ */
50
+ async function injectBlock(filePath) {
51
+ await fs.ensureDir(path.dirname(filePath));
52
+
53
+ if (await fs.pathExists(filePath)) {
54
+ const content = await fs.readFile(filePath, 'utf-8');
55
+ if (hasMemoir(content)) {
56
+ return 'already';
57
+ }
58
+ // Append with spacing
59
+ const separator = content.endsWith('\n') ? '\n' : '\n\n';
60
+ await fs.writeFile(filePath, content + separator + MEMOIR_BLOCK + '\n');
61
+ return 'appended';
62
+ } else {
63
+ await fs.writeFile(filePath, MEMOIR_BLOCK + '\n');
64
+ return 'created';
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Remove memoir block from a file
70
+ */
71
+ async function removeBlock(filePath) {
72
+ if (!await fs.pathExists(filePath)) return 'not_found';
73
+
74
+ const content = await fs.readFile(filePath, 'utf-8');
75
+ if (!hasMemoir(content)) return 'not_present';
76
+
77
+ const startIdx = content.indexOf(BLOCK_START);
78
+ const endIdx = content.indexOf(BLOCK_END);
79
+ if (startIdx === -1 || endIdx === -1) return 'not_present';
80
+
81
+ const before = content.slice(0, startIdx).replace(/\n+$/, '');
82
+ const after = content.slice(endIdx + BLOCK_END.length).replace(/^\n+/, '');
83
+ const cleaned = (before + (before && after ? '\n\n' : '') + after).trim();
84
+
85
+ if (!cleaned) {
86
+ // File would be empty — delete it
87
+ await fs.remove(filePath);
88
+ return 'deleted';
89
+ }
90
+
91
+ await fs.writeFile(filePath, cleaned + '\n');
92
+ return 'removed';
93
+ }
94
+
95
+ /**
96
+ * Track which projects have been activated
97
+ */
98
+ function getActivatedPath() {
99
+ return path.join(os.homedir(), '.config', 'memoir', 'activated-projects.json');
100
+ }
101
+
102
+ async function loadActivated() {
103
+ const p = getActivatedPath();
104
+ if (await fs.pathExists(p)) {
105
+ const data = await fs.readJson(p);
106
+ return Array.isArray(data) ? data : [];
107
+ }
108
+ return [];
109
+ }
110
+
111
+ async function markActivated(projectDir) {
112
+ const activated = await loadActivated();
113
+ if (!activated.includes(projectDir)) {
114
+ activated.push(projectDir);
115
+ await fs.ensureDir(path.dirname(getActivatedPath()));
116
+ await fs.writeJson(getActivatedPath(), activated);
117
+ }
118
+ }
119
+
120
+ async function markDeactivated(projectDir) {
121
+ let activated = await loadActivated();
122
+ activated = activated.filter(p => p !== projectDir);
123
+ await fs.ensureDir(path.dirname(getActivatedPath()));
124
+ await fs.writeJson(getActivatedPath(), activated);
125
+ }
126
+
127
+ export async function isActivated(projectDir) {
128
+ const activated = await loadActivated();
129
+ return activated.includes(projectDir);
130
+ }
131
+
132
+ /**
133
+ * memoir activate — inject memoir instructions into project AI config files
134
+ */
135
+ export async function activateCommand(options = {}) {
136
+ const projectDir = process.cwd();
137
+ const detected = detectInstructionFiles(projectDir);
138
+ const existing = detected.filter(d => d.exists);
139
+
140
+ if (existing.length === 0) {
141
+ // No instruction files exist — create CLAUDE.md by default
142
+ const result = await injectBlock(path.join(projectDir, 'CLAUDE.md'));
143
+ console.log(chalk.green('\n ✔ Created CLAUDE.md with memoir instructions'));
144
+ console.log(chalk.gray(' Your AI will use memoir_recall and memoir_remember automatically.\n'));
145
+ await markActivated(projectDir);
146
+ return;
147
+ }
148
+
149
+ // Inject into all existing instruction files
150
+ let injected = 0;
151
+ for (const { file, tool, fullPath } of existing) {
152
+ const result = await injectBlock(fullPath);
153
+ if (result === 'appended') {
154
+ console.log(chalk.green(` ✔ Added memoir to ${file}`) + chalk.gray(` (${tool})`));
155
+ injected++;
156
+ } else if (result === 'created') {
157
+ console.log(chalk.green(` ✔ Created ${file} with memoir instructions`) + chalk.gray(` (${tool})`));
158
+ injected++;
159
+ } else if (result === 'already') {
160
+ console.log(chalk.gray(` · ${file} already has memoir`) + chalk.gray(` (${tool})`));
161
+ }
162
+ }
163
+
164
+ if (injected > 0) {
165
+ console.log(chalk.gray('\n Your AI tools will now use memoir automatically.\n'));
166
+ } else {
167
+ console.log(chalk.gray('\n memoir is already active in this project.\n'));
168
+ }
169
+
170
+ await markActivated(projectDir);
171
+ }
172
+
173
+ /**
174
+ * memoir deactivate — remove memoir instructions from project AI config files
175
+ */
176
+ export async function deactivateCommand(options = {}) {
177
+ const projectDir = process.cwd();
178
+ const detected = detectInstructionFiles(projectDir);
179
+
180
+ let removed = 0;
181
+ for (const { file, tool, fullPath } of detected) {
182
+ const result = await removeBlock(fullPath);
183
+ if (result === 'removed') {
184
+ console.log(chalk.yellow(` ✔ Removed memoir from ${file}`) + chalk.gray(` (${tool})`));
185
+ removed++;
186
+ } else if (result === 'deleted') {
187
+ console.log(chalk.yellow(` ✔ Deleted ${file}`) + chalk.gray(' (was only memoir block)'));
188
+ removed++;
189
+ }
190
+ }
191
+
192
+ if (removed === 0) {
193
+ console.log(chalk.gray(' · memoir is not active in this project.\n'));
194
+ } else {
195
+ console.log(chalk.gray('\n memoir instructions removed.\n'));
196
+ }
197
+
198
+ await markDeactivated(projectDir);
199
+ }
200
+
201
+ /**
202
+ * Prompt to activate — called from push on first push per project
203
+ */
204
+ export async function promptActivate() {
205
+ const projectDir = process.cwd();
206
+
207
+ // Don't prompt if already activated or if not in a project directory
208
+ if (await isActivated(projectDir)) return;
209
+
210
+ // Check if we're in a project (has git, or has instruction files, or has package.json etc.)
211
+ const projectSignals = ['.git', 'package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'Makefile'];
212
+ const isProject = projectSignals.some(f => fs.existsSync(path.join(projectDir, f)));
213
+ if (!isProject) {
214
+ await markActivated(projectDir); // Don't ask again for non-projects
215
+ return;
216
+ }
217
+
218
+ console.log('');
219
+ const { activate } = await inquirer.prompt([{
220
+ type: 'confirm',
221
+ name: 'activate',
222
+ message: 'Add memoir instructions to this project so your AI uses it automatically?',
223
+ default: true,
224
+ }]);
225
+
226
+ if (activate) {
227
+ await activateCommand();
228
+ } else {
229
+ await markActivated(projectDir); // Don't ask again
230
+ console.log(chalk.gray(' · Skipped. Run ') + chalk.cyan('memoir activate') + chalk.gray(' anytime to enable.\n'));
231
+ }
232
+ }
@@ -23,7 +23,7 @@ function getGitHubUsername() {
23
23
  }
24
24
  }
25
25
 
26
- export async function initCommand() {
26
+ export async function initCommand(options = {}) {
27
27
  console.log('');
28
28
  console.log(boxen(
29
29
  gradient.pastel('memoir') + '\n' +
@@ -34,57 +34,80 @@ export async function initCommand() {
34
34
 
35
35
  const detectedUser = getGitHubUsername();
36
36
 
37
- const { direction, provider } = await inquirer.prompt([
38
- {
39
- type: 'list',
40
- name: 'direction',
41
- message: 'Upload or download?',
42
- choices: [
43
- { name: 'Upload — back up this machine', value: 'upload' },
44
- { name: 'Download — restore from backup', value: 'download' }
45
- ]
46
- },
47
- {
48
- type: 'list',
49
- name: 'provider',
50
- message: (answers) => answers.direction === 'upload' ? 'Back up to?' : 'Restore from?',
51
- choices: [
52
- { name: 'GitHub', value: 'git' },
53
- { name: 'Local folder', value: 'local' }
54
- ]
55
- }
56
- ]);
57
-
58
- let config = { provider };
37
+ let direction, provider;
59
38
 
60
- if (provider === 'local') {
61
- const msg = direction === 'upload' ? 'Save to:' : 'Backup folder:';
62
- const { localPath } = await inquirer.prompt([{
63
- type: 'input',
64
- name: 'localPath',
65
- message: msg,
66
- validate: (input) => input.trim() ? true : 'Required'
67
- }]);
68
- config.localPath = localPath;
39
+ if (options.provider) {
40
+ // Non-interactive: use flags directly
41
+ provider = options.provider;
42
+ direction = options.direction || 'upload';
69
43
  } else {
44
+ // Interactive: prompt the user
70
45
  const answers = await inquirer.prompt([
71
46
  {
72
- type: 'input',
73
- name: 'username',
74
- message: 'GitHub username:',
75
- default: detectedUser || undefined,
76
- validate: (input) => input.trim() ? true : 'Required'
47
+ type: 'list',
48
+ name: 'direction',
49
+ message: 'Upload or download?',
50
+ choices: [
51
+ { name: 'Upload back up this machine', value: 'upload' },
52
+ { name: 'Download — restore from backup', value: 'download' }
53
+ ]
77
54
  },
78
55
  {
79
- type: 'input',
80
- name: 'repo',
81
- message: 'Repo name:',
82
- default: 'ai-memory',
83
- validate: (input) => input.trim() ? true : 'Required'
56
+ type: 'list',
57
+ name: 'provider',
58
+ message: (a) => a.direction === 'upload' ? 'Back up to?' : 'Restore from?',
59
+ choices: [
60
+ { name: 'GitHub', value: 'git' },
61
+ { name: 'Local folder', value: 'local' }
62
+ ]
84
63
  }
85
64
  ]);
86
- const username = answers.username.trim();
87
- const repo = answers.repo.trim();
65
+ direction = answers.direction;
66
+ provider = answers.provider;
67
+ }
68
+
69
+ let config = { provider };
70
+
71
+ if (provider === 'local') {
72
+ let localPath;
73
+ if (options.localPath) {
74
+ localPath = options.localPath;
75
+ } else {
76
+ const msg = direction === 'upload' ? 'Save to:' : 'Backup folder:';
77
+ const answers = await inquirer.prompt([{
78
+ type: 'input',
79
+ name: 'localPath',
80
+ message: msg,
81
+ validate: (input) => input.trim() ? true : 'Required'
82
+ }]);
83
+ localPath = answers.localPath;
84
+ }
85
+ config.localPath = localPath;
86
+ } else {
87
+ let username, repo;
88
+ if (options.username) {
89
+ username = options.username.trim();
90
+ repo = (options.repo || 'ai-memory').trim();
91
+ } else {
92
+ const answers = await inquirer.prompt([
93
+ {
94
+ type: 'input',
95
+ name: 'username',
96
+ message: 'GitHub username:',
97
+ default: detectedUser || undefined,
98
+ validate: (input) => input.trim() ? true : 'Required'
99
+ },
100
+ {
101
+ type: 'input',
102
+ name: 'repo',
103
+ message: 'Repo name:',
104
+ default: 'ai-memory',
105
+ validate: (input) => input.trim() ? true : 'Required'
106
+ }
107
+ ]);
108
+ username = answers.username.trim();
109
+ repo = answers.repo.trim();
110
+ }
88
111
 
89
112
  config.gitRepo = `https://github.com/${username}/${repo}.git`;
90
113
  console.log(chalk.gray(` → ${config.gitRepo}`));
@@ -109,12 +132,18 @@ export async function initCommand() {
109
132
  }
110
133
 
111
134
  // Ask about encryption
112
- const { encrypt } = await inquirer.prompt([{
113
- type: 'confirm',
114
- name: 'encrypt',
115
- message: 'Enable E2E encryption? (protects your data even if backup is compromised)',
116
- default: true
117
- }]);
135
+ let encrypt;
136
+ if (options.encrypt !== undefined) {
137
+ encrypt = options.encrypt;
138
+ } else {
139
+ const answers = await inquirer.prompt([{
140
+ type: 'confirm',
141
+ name: 'encrypt',
142
+ message: 'Enable E2E encryption? (protects your data even if backup is compromised)',
143
+ default: true
144
+ }]);
145
+ encrypt = answers.encrypt;
146
+ }
118
147
  config.encrypt = encrypt;
119
148
 
120
149
  if (encrypt) {
@@ -2,7 +2,7 @@ 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, resetPassword } from '../cloud/auth.js';
5
+ import { signIn, signUp, saveSession, getSession, logout, getSubscription, resetPassword, deleteAccount } from '../cloud/auth.js';
6
6
 
7
7
  export async function loginCommand(options = {}) {
8
8
  // Check if already logged in
@@ -131,3 +131,43 @@ export async function logoutCommand() {
131
131
  { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
132
132
  ) + '\n');
133
133
  }
134
+
135
+ export async function deleteAccountCommand(options = {}) {
136
+ const session = await getSession();
137
+ if (!session) {
138
+ console.log('\n' + boxen(
139
+ chalk.red('✖ Not logged in. Run ') + chalk.cyan('memoir login') + chalk.red(' first.'),
140
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
141
+ ) + '\n');
142
+ return;
143
+ }
144
+
145
+ if (!options.confirm) {
146
+ const answer = await inquirer.prompt([{
147
+ type: 'input',
148
+ name: 'confirmation',
149
+ message: 'Type DELETE to confirm account deletion:',
150
+ }]);
151
+
152
+ if (answer.confirmation !== 'DELETE') {
153
+ console.log('\n' + chalk.gray(' Account deletion cancelled.') + '\n');
154
+ return;
155
+ }
156
+ }
157
+
158
+ try {
159
+ await deleteAccount(session);
160
+
161
+ console.log('\n' + boxen(
162
+ gradient.pastel(' memoir cloud ') + '\n\n' +
163
+ chalk.green('✔ Account deleted.') + '\n' +
164
+ chalk.gray('All backups, shared links, and data have been removed.'),
165
+ { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
166
+ ) + '\n');
167
+ } catch (error) {
168
+ console.log('\n' + boxen(
169
+ chalk.red('✖ ' + error.message),
170
+ { padding: 1, borderStyle: 'round', borderColor: 'red' }
171
+ ) + '\n');
172
+ }
173
+ }
@@ -14,6 +14,7 @@ import { scanForSecrets, printSecurityReport } from '../security/scanner.js';
14
14
  import { encryptDirectory, createVerifyToken } from '../security/encryption.js';
15
15
  import { getRawConfig, saveConfig, migrateConfigToV2 } from '../config.js';
16
16
  import { scanWorkspace } from '../workspace/tracker.js';
17
+ import { promptActivate } from './activate.js';
17
18
 
18
19
  export async function pushCommand(options = {}) {
19
20
  const config = await getConfig(options.profile);
@@ -262,6 +263,13 @@ export async function pushCommand(options = {}) {
262
263
  chalk.gray('Restore on another machine with: ') + chalk.cyan('memoir restore'),
263
264
  { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
264
265
  ) + '\n');
266
+
267
+ // Prompt to activate memoir in this project (first push only)
268
+ try {
269
+ await promptActivate();
270
+ } catch {
271
+ // Activation prompt is best-effort
272
+ }
265
273
  } catch (error) {
266
274
  spinner.fail(chalk.red('Sync failed: ') + error.message);
267
275
  } finally {
@@ -63,7 +63,7 @@ export async function upgradeCommand() {
63
63
  const sep = chalk.gray('─'.repeat(col1 + col2 + 18));
64
64
 
65
65
  const rows = [
66
- [chalk.gray('3 cloud backups'), chalk.white('50 cloud backups'), chalk.white('Unlimited backups')],
66
+ [chalk.gray('100 cloud backups'), chalk.white('Unlimited backups'), chalk.white('Unlimited backups')],
67
67
  [chalk.gray('Local only'), chalk.white('Unlimited machines'), chalk.white('Shared team context')],
68
68
  [chalk.gray('Manual snapshots'), chalk.white('Auto snapshots'), chalk.white('Team dashboard')],
69
69
  [chalk.gray('Community support'), chalk.white('Priority support'), chalk.white('Audit log')],