memoir-cli 3.1.1 → 3.2.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/GAMEPLAN.md +235 -0
- package/LAUNCH_POSTS.md +247 -0
- package/MARKETING.md +143 -0
- package/POSTS-READY-TO-GO.md +215 -0
- package/README.md +95 -89
- package/bin/memoir.js +78 -3
- package/demo.svg +201 -0
- package/landing-page-v2.html +690 -0
- package/mcp-publisher +0 -0
- package/package.json +28 -23
- package/server.json +20 -0
- package/src/commands/projects.js +240 -0
- package/src/commands/push.js +5 -3
- package/src/commands/restore.js +197 -3
- package/src/commands/share.js +192 -0
- package/src/commands/upgrade.js +107 -0
- package/src/context/capture.js +77 -0
- package/src/mcp.js +429 -0
- package/src/providers/index.js +6 -6
- package/src/security/encryption.js +98 -46
- package/src/workspace/tracker.js +4 -4
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import boxen from 'boxen';
|
|
8
|
+
import gradient from 'gradient-string';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
import { getSession } from '../cloud/auth.js';
|
|
11
|
+
import { extractMemories, adapters } from '../adapters/index.js';
|
|
12
|
+
import { encryptDirectory, createVerifyToken } from '../security/encryption.js';
|
|
13
|
+
import { bundleDir } from '../cloud/storage.js';
|
|
14
|
+
import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET } from '../cloud/constants.js';
|
|
15
|
+
|
|
16
|
+
export async function shareCommand(options = {}) {
|
|
17
|
+
// Must be logged in to share
|
|
18
|
+
const session = await getSession();
|
|
19
|
+
if (!session) {
|
|
20
|
+
console.log('\n' + boxen(
|
|
21
|
+
chalk.red('✖ Not logged in\n\n') +
|
|
22
|
+
chalk.white('Sharing requires a memoir cloud account.\n') +
|
|
23
|
+
chalk.white('Run ') + chalk.cyan.bold('memoir login') + chalk.white(' to sign in.'),
|
|
24
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
25
|
+
) + '\n');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log();
|
|
30
|
+
const spinner = ora({ text: chalk.gray('Scanning for AI tools...'), spinner: 'dots' }).start();
|
|
31
|
+
|
|
32
|
+
const stagingDir = path.join(os.tmpdir(), `memoir-share-${Date.now()}`);
|
|
33
|
+
await fs.ensureDir(stagingDir);
|
|
34
|
+
|
|
35
|
+
let encryptedDir = null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Scan and extract memories (same as push)
|
|
39
|
+
const onlyFilter = options.only ? options.only.split(',').map(t => t.trim().toLowerCase()) : null;
|
|
40
|
+
const foundAny = await extractMemories(stagingDir, spinner, onlyFilter);
|
|
41
|
+
|
|
42
|
+
if (!foundAny) {
|
|
43
|
+
spinner.stop();
|
|
44
|
+
console.log('\n' + boxen(
|
|
45
|
+
chalk.yellow('No AI tools detected on this machine.\n\n') +
|
|
46
|
+
chalk.gray('Supported: Claude, Gemini, Codex, Cursor, Copilot, Windsurf, Aider'),
|
|
47
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'yellow' }
|
|
48
|
+
) + '\n');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Count what was found
|
|
53
|
+
const found = [];
|
|
54
|
+
for (const adapter of adapters) {
|
|
55
|
+
if (adapter.customExtract) {
|
|
56
|
+
for (const file of adapter.files) {
|
|
57
|
+
if (await fs.pathExists(path.join(adapter.source, file))) {
|
|
58
|
+
found.push(adapter.name);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else if (await fs.pathExists(adapter.source)) {
|
|
63
|
+
found.push(adapter.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Ask for encryption passphrase
|
|
68
|
+
spinner.stop();
|
|
69
|
+
const { passphrase } = await inquirer.prompt([{
|
|
70
|
+
type: 'password',
|
|
71
|
+
name: 'passphrase',
|
|
72
|
+
message: '🔒 Set a passphrase for this share link (recipient will need it):',
|
|
73
|
+
mask: '*',
|
|
74
|
+
validate: (input) => input.length >= 6 ? true : 'Passphrase must be at least 6 characters'
|
|
75
|
+
}]);
|
|
76
|
+
|
|
77
|
+
spinner.start(chalk.gray('Encrypting...'));
|
|
78
|
+
|
|
79
|
+
encryptedDir = path.join(os.tmpdir(), `memoir-share-enc-${Date.now()}`);
|
|
80
|
+
await fs.ensureDir(encryptedDir);
|
|
81
|
+
await encryptDirectory(stagingDir, encryptedDir, passphrase, spinner);
|
|
82
|
+
|
|
83
|
+
// Save verify token so recipient can check passphrase
|
|
84
|
+
const token = await createVerifyToken(passphrase);
|
|
85
|
+
await fs.writeFile(path.join(encryptedDir, 'verify.enc'), token);
|
|
86
|
+
|
|
87
|
+
// Bundle and upload to Supabase Storage
|
|
88
|
+
spinner.text = chalk.gray('Uploading share bundle...');
|
|
89
|
+
const gzipped = await bundleDir(encryptedDir);
|
|
90
|
+
|
|
91
|
+
const shareToken = crypto.randomUUID();
|
|
92
|
+
const storagePath = `shares/${session.user.id}/${shareToken}.gz`;
|
|
93
|
+
|
|
94
|
+
const uploadRes = await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}/${storagePath}`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Authorization': `Bearer ${session.access_token}`,
|
|
98
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
99
|
+
'Content-Type': 'application/octet-stream',
|
|
100
|
+
},
|
|
101
|
+
body: gzipped,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!uploadRes.ok) {
|
|
105
|
+
const err = await uploadRes.text();
|
|
106
|
+
throw new Error(`Upload failed: ${err}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse options
|
|
110
|
+
const expiresHours = parseInt(options.expires) || 24;
|
|
111
|
+
const maxUses = parseInt(options.uses) || 5;
|
|
112
|
+
const expiresAt = new Date(Date.now() + expiresHours * 60 * 60 * 1000).toISOString();
|
|
113
|
+
|
|
114
|
+
// Store share metadata in shared_links table
|
|
115
|
+
spinner.text = chalk.gray('Creating share link...');
|
|
116
|
+
const metaRes = await fetch(`${SUPABASE_URL}/rest/v1/shared_links`, {
|
|
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
|
+
token: shareToken,
|
|
126
|
+
backup_id: storagePath,
|
|
127
|
+
created_by: session.user.id,
|
|
128
|
+
expires_at: expiresAt,
|
|
129
|
+
max_uses: maxUses,
|
|
130
|
+
use_count: 0,
|
|
131
|
+
tools: found,
|
|
132
|
+
size_bytes: gzipped.length,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!metaRes.ok) {
|
|
137
|
+
const err = await metaRes.text();
|
|
138
|
+
throw new Error(`Failed to create share link: ${err}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
spinner.stop();
|
|
142
|
+
|
|
143
|
+
// Count total files
|
|
144
|
+
let totalFiles = 0;
|
|
145
|
+
for (const adapter of adapters) {
|
|
146
|
+
const adapterDir = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
147
|
+
if (await fs.pathExists(adapterDir)) {
|
|
148
|
+
const countDir = async (dir) => {
|
|
149
|
+
let c = 0;
|
|
150
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
151
|
+
for (const e of entries) {
|
|
152
|
+
if (e.isDirectory()) c += await countDir(path.join(dir, e.name));
|
|
153
|
+
else c++;
|
|
154
|
+
}
|
|
155
|
+
return c;
|
|
156
|
+
};
|
|
157
|
+
totalFiles += await countDir(adapterDir);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Format expiry for display
|
|
162
|
+
const expiryDate = new Date(expiresAt);
|
|
163
|
+
const expiryStr = expiryDate.toLocaleString();
|
|
164
|
+
|
|
165
|
+
// Success output
|
|
166
|
+
const shareUrl = `https://memoir.sh/share/${shareToken}`;
|
|
167
|
+
const restoreCmd = `memoir restore --from ${shareToken}`;
|
|
168
|
+
const toolList = found.map(t => chalk.cyan(' ✔ ' + t)).join('\n');
|
|
169
|
+
|
|
170
|
+
console.log('\n' + boxen(
|
|
171
|
+
gradient.pastel(' Shared! ') + '\n\n' +
|
|
172
|
+
toolList + '\n' +
|
|
173
|
+
chalk.white(`${totalFiles} files from ${found.length} tool${found.length !== 1 ? 's' : ''}`) + '\n' +
|
|
174
|
+
chalk.green(' 🔒 E2E encrypted') + '\n\n' +
|
|
175
|
+
chalk.white.bold('Share link:') + '\n' +
|
|
176
|
+
chalk.cyan(` ${shareUrl}`) + '\n\n' +
|
|
177
|
+
chalk.white.bold('Recipient runs:') + '\n' +
|
|
178
|
+
chalk.cyan(` ${restoreCmd}`) + '\n\n' +
|
|
179
|
+
chalk.gray(`Expires: ${expiryStr} (${expiresHours}h)`) + '\n' +
|
|
180
|
+
chalk.gray(`Max uses: ${maxUses}`),
|
|
181
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
182
|
+
) + '\n');
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
spinner.fail(chalk.red('Share failed: ') + error.message);
|
|
186
|
+
} finally {
|
|
187
|
+
await fs.remove(stagingDir);
|
|
188
|
+
if (encryptedDir) {
|
|
189
|
+
await fs.remove(encryptedDir).catch(() => {});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
import gradient from 'gradient-string';
|
|
4
|
+
import { getSession, getSubscription } from '../cloud/auth.js';
|
|
5
|
+
|
|
6
|
+
export async function upgradeCommand() {
|
|
7
|
+
const session = await getSession();
|
|
8
|
+
let currentPlan = 'free';
|
|
9
|
+
let email = null;
|
|
10
|
+
|
|
11
|
+
if (session) {
|
|
12
|
+
email = session.user.email;
|
|
13
|
+
try {
|
|
14
|
+
const sub = await getSubscription(session);
|
|
15
|
+
currentPlan = sub.status === 'pro' ? 'pro' : 'free';
|
|
16
|
+
} catch {
|
|
17
|
+
// Fall through as free
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Build comparison table
|
|
22
|
+
const col1 = 22;
|
|
23
|
+
const col2 = 22;
|
|
24
|
+
|
|
25
|
+
const pad = (str, width) => {
|
|
26
|
+
// Strip ANSI for length calculation
|
|
27
|
+
const stripped = str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
28
|
+
const diff = width - stripped.length;
|
|
29
|
+
return diff > 0 ? str + ' '.repeat(diff) : str;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const freeLabel = currentPlan === 'free' ? 'Free (current)' : 'Free';
|
|
33
|
+
const proLabel = currentPlan === 'pro' ? 'Pro (current)' : 'Pro $15/mo';
|
|
34
|
+
const teamsLabel = 'Teams $29/seat';
|
|
35
|
+
|
|
36
|
+
const header =
|
|
37
|
+
pad(chalk.bold.white(freeLabel), col1) +
|
|
38
|
+
pad(chalk.bold.cyan(proLabel), col2) +
|
|
39
|
+
chalk.bold.magenta(teamsLabel);
|
|
40
|
+
|
|
41
|
+
const sep = chalk.gray('─'.repeat(col1 + col2 + 18));
|
|
42
|
+
|
|
43
|
+
const rows = [
|
|
44
|
+
[chalk.gray('3 cloud backups'), chalk.white('50 cloud backups'), chalk.white('Unlimited backups')],
|
|
45
|
+
[chalk.gray('Local only'), chalk.white('Unlimited machines'), chalk.white('Shared team context')],
|
|
46
|
+
[chalk.gray('Manual snapshots'), chalk.white('Auto snapshots'), chalk.white('Team dashboard')],
|
|
47
|
+
[chalk.gray('Community support'), chalk.white('Priority support'), chalk.white('Audit log')],
|
|
48
|
+
[chalk.gray('—'), chalk.white('E2E encryption'), chalk.white('SSO & RBAC')],
|
|
49
|
+
[chalk.gray('—'), chalk.white('Version history'), chalk.white('Priority onboarding')],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const tableRows = rows.map(([a, b, c]) =>
|
|
53
|
+
pad(a, col1) + pad(b, col2) + c
|
|
54
|
+
).join('\n');
|
|
55
|
+
|
|
56
|
+
const table =
|
|
57
|
+
'\n' + header + '\n' +
|
|
58
|
+
sep + '\n' +
|
|
59
|
+
tableRows + '\n';
|
|
60
|
+
|
|
61
|
+
// Status line
|
|
62
|
+
let statusLine;
|
|
63
|
+
if (!session) {
|
|
64
|
+
statusLine = chalk.yellow('Not logged in.') + chalk.gray(' Run ') + chalk.cyan('memoir login') + chalk.gray(' first.');
|
|
65
|
+
} else if (currentPlan === 'pro') {
|
|
66
|
+
statusLine = chalk.green('You\'re on Pro!') + ' ' + chalk.gray('Teams is coming soon — join the waitlist at memoir.sh/teams');
|
|
67
|
+
} else {
|
|
68
|
+
statusLine = chalk.gray('Logged in as ') + chalk.cyan(email);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('\n' + boxen(
|
|
72
|
+
gradient.pastel(' memoir upgrade ') + '\n\n' +
|
|
73
|
+
table + '\n' +
|
|
74
|
+
statusLine,
|
|
75
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
76
|
+
));
|
|
77
|
+
|
|
78
|
+
// If free and logged in, open pricing page
|
|
79
|
+
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
|
+
}
|
|
94
|
+
|
|
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
|
+
);
|
|
102
|
+
} 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');
|
|
104
|
+
} else {
|
|
105
|
+
console.log();
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/context/capture.js
CHANGED
|
@@ -287,6 +287,83 @@ ${section}`;
|
|
|
287
287
|
return fresh.length;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Promote memories from project-scoped dirs to the home-level scope.
|
|
292
|
+
* Claude scopes memory per working directory — memories saved in ~/memoir
|
|
293
|
+
* are invisible from ~/btc-trader. This copies important .md files to the
|
|
294
|
+
* home-level scope so they're accessible from ANY directory.
|
|
295
|
+
*
|
|
296
|
+
* Only promotes files with frontmatter type: user or type: project (not ephemeral ones).
|
|
297
|
+
*/
|
|
298
|
+
export function promoteMemoriesToGlobal() {
|
|
299
|
+
const claudeDir = path.join(home, '.claude');
|
|
300
|
+
const projectsDir = path.join(claudeDir, 'projects');
|
|
301
|
+
if (!fs.existsSync(projectsDir)) return 0;
|
|
302
|
+
|
|
303
|
+
// Find the home-level key
|
|
304
|
+
let homeKey;
|
|
305
|
+
if (process.platform === 'win32') {
|
|
306
|
+
homeKey = home.replace(/\\/g, '-').replace(/:/g, '-');
|
|
307
|
+
} else {
|
|
308
|
+
homeKey = '-' + home.replace(/^\//, '').replace(/\//g, '-');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const homeMemDir = path.join(projectsDir, homeKey, 'memory');
|
|
312
|
+
fs.mkdirSync(homeMemDir, { recursive: true });
|
|
313
|
+
|
|
314
|
+
const homeMemoryMdPath = path.join(homeMemDir, 'MEMORY.md');
|
|
315
|
+
let homeMemoryMd = '';
|
|
316
|
+
if (fs.existsSync(homeMemoryMdPath)) {
|
|
317
|
+
homeMemoryMd = fs.readFileSync(homeMemoryMdPath, 'utf8');
|
|
318
|
+
} else {
|
|
319
|
+
homeMemoryMd = '# Project Memory\n';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let promoted = 0;
|
|
323
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true })
|
|
324
|
+
.filter(e => e.isDirectory() && e.name !== homeKey);
|
|
325
|
+
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
const memDir = path.join(projectsDir, entry.name, 'memory');
|
|
328
|
+
if (!fs.existsSync(memDir)) continue;
|
|
329
|
+
|
|
330
|
+
const files = fs.readdirSync(memDir)
|
|
331
|
+
.filter(f => f.endsWith('.md') && f !== 'MEMORY.md' && f !== 'handoff.md');
|
|
332
|
+
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
const destPath = path.join(homeMemDir, file);
|
|
335
|
+
// Skip if already exists in home scope
|
|
336
|
+
if (fs.existsSync(destPath)) continue;
|
|
337
|
+
|
|
338
|
+
const content = fs.readFileSync(path.join(memDir, file), 'utf8');
|
|
339
|
+
|
|
340
|
+
// Only promote files with type: user or type: project
|
|
341
|
+
const typeMatch = content.match(/^type:\s*(user|project)/m);
|
|
342
|
+
if (!typeMatch) continue;
|
|
343
|
+
|
|
344
|
+
// Copy to home scope
|
|
345
|
+
fs.writeFileSync(destPath, content);
|
|
346
|
+
|
|
347
|
+
// Add to MEMORY.md if not already referenced
|
|
348
|
+
if (!homeMemoryMd.includes(file)) {
|
|
349
|
+
const nameMatch = content.match(/^name:\s*(.+)/m);
|
|
350
|
+
const descMatch = content.match(/^description:\s*(.+)/m);
|
|
351
|
+
const name = nameMatch ? nameMatch[1].trim() : file.replace('.md', '').replace(/-/g, ' ');
|
|
352
|
+
const desc = descMatch ? descMatch[1].trim() : '';
|
|
353
|
+
homeMemoryMd += `- [${name}](${file})${desc ? ' — ' + desc : ''}\n`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
promoted++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (promoted > 0) {
|
|
361
|
+
fs.writeFileSync(homeMemoryMdPath, homeMemoryMd);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return promoted;
|
|
365
|
+
}
|
|
366
|
+
|
|
290
367
|
/**
|
|
291
368
|
* Generate a concise handoff markdown from parsed session
|
|
292
369
|
* This is what gets injected into the AI tool on the other machine
|