memoir-cli 2.6.0 → 3.0.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/bin/memoir.js CHANGED
@@ -62,6 +62,7 @@ if (process.argv.length <= 2) {
62
62
  chalk.cyan(' memoir resume ') + chalk.gray('— pick up where you left off') + '\n' +
63
63
  chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
64
64
  chalk.cyan(' memoir profile ') + chalk.gray('— manage profiles (personal/work)') + '\n' +
65
+ chalk.cyan(' memoir encrypt ') + chalk.gray('— toggle E2E encryption') + '\n' +
65
66
  chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n\n' +
66
67
  chalk.white.bold('Cloud (Pro):') + '\n' +
67
68
  chalk.cyan(' memoir login ') + chalk.gray('— sign in to memoir cloud') + '\n' +
@@ -257,6 +258,44 @@ program
257
258
  }
258
259
  });
259
260
 
261
+ program
262
+ .command('encrypt')
263
+ .description('Toggle E2E encryption for your backups')
264
+ .action(async () => {
265
+ try {
266
+ const { getConfig, getRawConfig, saveConfig, migrateConfigToV2 } = await import('../src/config.js');
267
+ const config = await getConfig();
268
+ if (!config) {
269
+ console.error(chalk.red('\n✖ Not configured. Run memoir init first.'));
270
+ process.exit(1);
271
+ }
272
+ const current = config.encrypt || false;
273
+ console.log(chalk.white(`\n Encryption is currently: ${current ? chalk.green('ON') : chalk.red('OFF')}`));
274
+ const inquirer = (await import('inquirer')).default;
275
+ const { toggle } = await inquirer.prompt([{
276
+ type: 'confirm',
277
+ name: 'toggle',
278
+ message: current ? 'Disable encryption?' : 'Enable encryption?',
279
+ default: !current
280
+ }]);
281
+ if (toggle !== current) {
282
+ let raw = await getRawConfig();
283
+ if (!raw.version || raw.version < 2) raw = migrateConfigToV2(raw);
284
+ const profileName = raw.activeProfile || 'default';
285
+ if (raw.profiles?.[profileName]) {
286
+ raw.profiles[profileName].encrypt = !current;
287
+ } else {
288
+ raw.encrypt = !current;
289
+ }
290
+ await saveConfig(raw);
291
+ console.log(chalk.green(`\n ✔ Encryption ${!current ? 'enabled' : 'disabled'}. Next push will ${!current ? 'encrypt' : 'skip encryption'}.\n`));
292
+ }
293
+ } catch (err) {
294
+ console.error(chalk.red('\n✖ Error:'), err.message);
295
+ process.exit(1);
296
+ }
297
+ });
298
+
260
299
  program
261
300
  .command('migrate')
262
301
  .description('Translate memory between AI tools (Claude, Gemini, Codex, Cursor, etc.)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
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",
@@ -3,6 +3,7 @@ import nodeFs from 'node:fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import chalk from 'chalk';
6
+ import { shouldIgnoreProject } from '../context/capture.js';
6
7
 
7
8
  const home = os.homedir();
8
9
 
@@ -332,7 +333,7 @@ export async function extractMemories(stagingDir, spinner, onlyFilter = null) {
332
333
  }
333
334
  }
334
335
 
335
- if (foundFiles.length > 0 && dir !== home) {
336
+ if (foundFiles.length > 0 && dir !== home && !shouldIgnoreProject(dir)) {
336
337
  // This is a project with AI configs
337
338
  const projectName = path.basename(dir);
338
339
  const projectDestDir = path.join(projectsDest, projectName);
@@ -9,9 +9,10 @@ import { getConfig } from '../config.js';
9
9
  import { extractMemories, adapters } from '../adapters/index.js';
10
10
  import { syncToLocal, syncToGit } from '../providers/index.js';
11
11
  import inquirer from 'inquirer';
12
- import { findClaudeSessions, parseSession, generateContextHandoff } from '../context/capture.js';
12
+ import { findClaudeSessions, parseSession, generateContextHandoff, shouldIgnoreProject } from '../context/capture.js';
13
13
  import { scanForSecrets, printSecurityReport } from '../security/scanner.js';
14
14
  import { encryptDirectory, createVerifyToken } from '../security/encryption.js';
15
+ import { getRawConfig, saveConfig, migrateConfigToV2 } from '../config.js';
15
16
 
16
17
  export async function pushCommand(options = {}) {
17
18
  const config = await getConfig(options.profile);
@@ -113,10 +114,42 @@ export async function pushCommand(options = {}) {
113
114
  }
114
115
  }
115
116
 
116
- // Encrypt if enabled
117
+ // Encrypt if enabled (or ask on first push if not configured)
117
118
  let uploadDir = stagingDir;
118
119
  let encrypted = false;
119
- if (config.encrypt) {
120
+ let shouldEncrypt = config.encrypt;
121
+
122
+ if (shouldEncrypt === undefined) {
123
+ // First push since encryption was added — ask once and save preference
124
+ spinner.stop();
125
+ const { wantEncrypt } = await inquirer.prompt([{
126
+ type: 'confirm',
127
+ name: 'wantEncrypt',
128
+ message: 'Enable E2E encryption? (protects your backup even if compromised)',
129
+ default: true
130
+ }]);
131
+ shouldEncrypt = wantEncrypt;
132
+
133
+ // Save to config so we don't ask again
134
+ try {
135
+ let raw = await getRawConfig();
136
+ if (raw) {
137
+ if (!raw.version || raw.version < 2) {
138
+ raw = migrateConfigToV2(raw);
139
+ }
140
+ const profileName = options.profile || raw.activeProfile || 'default';
141
+ if (raw.profiles?.[profileName]) {
142
+ raw.profiles[profileName].encrypt = shouldEncrypt;
143
+ } else {
144
+ raw.encrypt = shouldEncrypt;
145
+ }
146
+ await saveConfig(raw);
147
+ }
148
+ } catch {}
149
+ spinner.start();
150
+ }
151
+
152
+ if (shouldEncrypt) {
120
153
  spinner.stop();
121
154
  const { passphrase } = await inquirer.prompt([{
122
155
  type: 'password',
@@ -199,7 +232,7 @@ export async function pushCommand(options = {}) {
199
232
  } finally {
200
233
  await fs.remove(stagingDir);
201
234
  // Clean up encrypted dir if it was created
202
- if (config.encrypt) {
235
+ if (true) {
203
236
  const encDirs = await fs.readdir(os.tmpdir());
204
237
  for (const d of encDirs) {
205
238
  if (d.startsWith('memoir-encrypted-')) {
@@ -143,13 +143,13 @@ export async function restoreCommand(options = {}) {
143
143
  handoffInjected = true;
144
144
  }
145
145
 
146
- // Extract info for display
147
- const fromMatch = handoffContent.match(/\*\*From:\*\*\s*(.+)/);
148
- const whenMatch = handoffContent.match(/\*\*When:\*\*\s*(.+)/);
149
- const durationMatch = handoffContent.match(/\*\*Duration:\*\*\s*(.+)/);
146
+ // Extract info for display — handles both old and new handoff formats
147
+ const fromMatch = handoffContent.match(/\*\*From:\*\*\s*(.+)/) || handoffContent.match(/from \*\*(.+?)\*\*/);
148
+ const whenMatch = handoffContent.match(/\*\*When:\*\*\s*(.+)/) || handoffContent.match(/on (\d{4}-\d{2}-\d{2}) at (.+)/);
149
+ const durationMatch = handoffContent.match(/\*\*Duration:\*\*\s*(.+)/) || handoffContent.match(/Session: (\w+)/);
150
150
  handoffInfo = {
151
151
  from: fromMatch ? fromMatch[1] : 'another machine',
152
- when: whenMatch ? whenMatch[1] : 'recently',
152
+ when: whenMatch ? (whenMatch[2] ? `${whenMatch[1]} ${whenMatch[2]}` : whenMatch[1]) : 'recently',
153
153
  duration: durationMatch ? durationMatch[1] : null,
154
154
  };
155
155
  }
@@ -168,47 +168,41 @@ export function generateContextHandoff(parsed) {
168
168
  .filter(m => m.length > 10 && !/^(ok|yes|no|sure|yea|yeah|yep|nah|nope|thanks|ty|thx|good|great|nice|cool|done|hmm)$/i.test(m.trim()))
169
169
  .map(m => m.length > 150 ? m.slice(0, 150) + '...' : m);
170
170
 
171
+ // Build a concise, actionable handoff
171
172
  let md = `---
172
173
  name: Session Handoff
173
- description: Auto-generated context from last coding session for seamless cross-device continuity
174
+ description: Coding session context resume on any machine, any AI tool
174
175
  type: project
175
176
  ---
176
177
 
177
- # Session Handoff
178
+ # Continue where I left off
178
179
 
179
- **From:** ${hostname} (${platform})
180
- **When:** ${now.toISOString().split('T')[0]} ${now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
181
- **Duration:** ${duration}
182
- **Project:** ${cwd}
183
- **Branch:** ${parsed.gitBranch || 'unknown'}
180
+ > Handed off from **${hostname}** (${platform}) on ${now.toISOString().split('T')[0]} at ${now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
181
+ > Session: ${duration} | Branch: \`${parsed.gitBranch || 'unknown'}\` | Project: \`${cwd}\`
184
182
 
185
- ## What was being worked on
183
+ ## What I was working on
186
184
  ${meaningful.length > 0 ? meaningful.slice(0, 8).map(m => `- ${m}`).join('\n') : '_No significant messages captured_'}
187
185
 
188
- ## Files modified
186
+ ## Files I changed
189
187
  ${parsed.filesWritten.length > 0
190
188
  ? parsed.filesWritten.slice(0, 15).map(f => `- \`${shorten(f)}\``).join('\n')
191
189
  : '_None_'}
192
-
193
- ## Key files referenced
194
- ${parsed.filesRead.length > 0
195
- ? parsed.filesRead.filter(f => !parsed.filesWritten.includes(f)).slice(0, 10).map(f => `- \`${shorten(f)}\``).join('\n')
196
- : '_None_'}
197
190
  `;
198
191
 
199
- if (parsed.errors.length > 0) {
200
- md += `\n## Errors hit\n${parsed.errors.slice(0, 5).map(e => `- ${e}`).join('\n')}\n`;
192
+ // Only show referenced files that weren't also modified
193
+ const readOnly = parsed.filesRead.filter(f => !parsed.filesWritten.includes(f));
194
+ if (readOnly.length > 0) {
195
+ md += `\n## Files I was looking at\n${readOnly.slice(0, 10).map(f => `- \`${shorten(f)}\``).join('\n')}\n`;
201
196
  }
202
197
 
203
- md += `\n## Context for continuing
204
- This session ran for ${duration} on ${platform}. ${parsed.filesWritten.length} files were modified and ${parsed.bashCommands.length} commands were run.`;
198
+ if (parsed.errors.length > 0) {
199
+ md += `\n## Issues I ran into\n${parsed.errors.slice(0, 5).map(e => `- ${e}`).join('\n')}\n`;
200
+ }
205
201
 
206
202
  if (parsed.filesWritten.length > 0) {
207
- md += ` Start by reviewing: ${parsed.filesWritten.slice(0, 3).map(f => '`' + shorten(f) + '`').join(', ')}.`;
203
+ md += `\n## Pick up here\nStart by reviewing: ${parsed.filesWritten.slice(0, 3).map(f => '`' + shorten(f) + '`').join(', ')}. ${parsed.filesWritten.length} files were modified in total.\n`;
208
204
  }
209
205
 
210
- md += '\n';
211
-
212
206
  return md;
213
207
  }
214
208