memoir-cli 3.1.0 → 3.2.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/GAMEPLAN.md +235 -0
- package/README.md +152 -54
- package/bin/memoir.js +78 -3
- package/package.json +9 -4
- package/src/commands/projects.js +240 -0
- package/src/commands/push.js +18 -4
- 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 +223 -1
- 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
package/bin/memoir.js
CHANGED
|
@@ -16,7 +16,10 @@ import { resumeCommand } from '../src/commands/resume.js';
|
|
|
16
16
|
import { profileListCommand, profileCreateCommand, profileSwitchCommand, profileDeleteCommand } from '../src/commands/profile.js';
|
|
17
17
|
import { loginCommand, logoutCommand } from '../src/commands/login.js';
|
|
18
18
|
import { cloudPushCommand, cloudRestoreCommand } from '../src/commands/cloud.js';
|
|
19
|
+
import { shareCommand } from '../src/commands/share.js';
|
|
19
20
|
import { historyCommand } from '../src/commands/history.js';
|
|
21
|
+
import { projectsListCommand, projectsTodoCommand } from '../src/commands/projects.js';
|
|
22
|
+
import { upgradeCommand } from '../src/commands/upgrade.js';
|
|
20
23
|
import { createRequire } from 'module';
|
|
21
24
|
|
|
22
25
|
const require = createRequire(import.meta.url);
|
|
@@ -62,12 +65,15 @@ if (process.argv.length <= 2) {
|
|
|
62
65
|
chalk.cyan(' memoir resume ') + chalk.gray('— pick up where you left off') + '\n' +
|
|
63
66
|
chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
|
|
64
67
|
chalk.cyan(' memoir profile ') + chalk.gray('— manage profiles (personal/work)') + '\n' +
|
|
68
|
+
chalk.cyan(' memoir projects ') + chalk.gray('— see all your projects at a glance') + '\n' +
|
|
65
69
|
chalk.cyan(' memoir encrypt ') + chalk.gray('— toggle E2E encryption') + '\n' +
|
|
66
|
-
chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n
|
|
70
|
+
chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n' +
|
|
71
|
+
chalk.cyan(' memoir upgrade ') + chalk.gray('— view plans & upgrade') + '\n\n' +
|
|
67
72
|
chalk.white.bold('Cloud (Pro):') + '\n' +
|
|
68
73
|
chalk.cyan(' memoir login ') + chalk.gray('— sign in to memoir cloud') + '\n' +
|
|
69
74
|
chalk.cyan(' memoir cloud push ') + chalk.gray('— back up to the cloud') + '\n' +
|
|
70
75
|
chalk.cyan(' memoir cloud restore ') + chalk.gray('— restore from cloud') + '\n' +
|
|
76
|
+
chalk.cyan(' memoir share ') + chalk.gray('— share memory via secure link') + '\n' +
|
|
71
77
|
chalk.cyan(' memoir history ') + chalk.gray('— view backup versions') + '\n\n' +
|
|
72
78
|
chalk.gray(' Tip: use --profile work to sync a specific profile') + '\n\n' +
|
|
73
79
|
chalk.gray(`v${VERSION}`),
|
|
@@ -120,8 +126,9 @@ program
|
|
|
120
126
|
.alias('pull')
|
|
121
127
|
.description('Restore your AI memory on this machine')
|
|
122
128
|
.option('--only <tools>', 'Only restore specific tools (comma-separated)')
|
|
123
|
-
.option('-
|
|
129
|
+
.option('-i, --interactive', 'Confirm each tool before restoring')
|
|
124
130
|
.option('-p, --profile <name>', 'Use a specific profile')
|
|
131
|
+
.option('--from <token>', 'Restore from a share link token')
|
|
125
132
|
.action(async (options) => {
|
|
126
133
|
try {
|
|
127
134
|
await restoreCommand(options);
|
|
@@ -131,6 +138,21 @@ program
|
|
|
131
138
|
}
|
|
132
139
|
});
|
|
133
140
|
|
|
141
|
+
program
|
|
142
|
+
.command('share')
|
|
143
|
+
.description('Share your AI memory via a secure link')
|
|
144
|
+
.option('--only <tools>', 'Only share specific tools (comma-separated)')
|
|
145
|
+
.option('--expires <hours>', 'Link expiry in hours (default: 24)')
|
|
146
|
+
.option('--uses <number>', 'Max number of uses (default: 5)')
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
try {
|
|
149
|
+
await shareCommand(options);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(chalk.red('\n✖ Error during share:'), err.message);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
134
156
|
program
|
|
135
157
|
.command('status')
|
|
136
158
|
.description('See what AI tools are on this machine')
|
|
@@ -219,7 +241,6 @@ program
|
|
|
219
241
|
|
|
220
242
|
program
|
|
221
243
|
.command('update')
|
|
222
|
-
.alias('upgrade')
|
|
223
244
|
.description('Update memoir to the latest version')
|
|
224
245
|
.action(async () => {
|
|
225
246
|
try {
|
|
@@ -257,6 +278,19 @@ program
|
|
|
257
278
|
}
|
|
258
279
|
});
|
|
259
280
|
|
|
281
|
+
program
|
|
282
|
+
.command('upgrade')
|
|
283
|
+
.alias('pro')
|
|
284
|
+
.description('View plans and upgrade your memoir subscription')
|
|
285
|
+
.action(async () => {
|
|
286
|
+
try {
|
|
287
|
+
await upgradeCommand();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
260
294
|
program
|
|
261
295
|
.command('encrypt')
|
|
262
296
|
.description('Toggle E2E encryption for your backups')
|
|
@@ -432,6 +466,47 @@ profile
|
|
|
432
466
|
}
|
|
433
467
|
});
|
|
434
468
|
|
|
469
|
+
// Project tracker
|
|
470
|
+
const projects = program.command('projects').alias('p').description('Track and manage your projects');
|
|
471
|
+
|
|
472
|
+
projects
|
|
473
|
+
.command('list', { isDefault: true })
|
|
474
|
+
.alias('ls')
|
|
475
|
+
.description('List all projects with recent activity')
|
|
476
|
+
.option('--all', 'Show all projects (default: top 15)')
|
|
477
|
+
.option('-v, --verbose', 'Show more commits and todos')
|
|
478
|
+
.option('--json', 'Output as JSON')
|
|
479
|
+
.action(async (options) => {
|
|
480
|
+
try {
|
|
481
|
+
await projectsListCommand(options);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
projects
|
|
489
|
+
.command('todo <project> [text]')
|
|
490
|
+
.description('Add or manage todos for a project')
|
|
491
|
+
.option('--done <index>', 'Mark a todo as done by number')
|
|
492
|
+
.option('--clear', 'Clear all todos for this project')
|
|
493
|
+
.action(async (project, text, options) => {
|
|
494
|
+
try {
|
|
495
|
+
await projectsTodoCommand(project, text, options);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
program
|
|
503
|
+
.command('mcp')
|
|
504
|
+
.description('Start the MCP server (for Claude Code, Cursor, VS Code integration)')
|
|
505
|
+
.action(async () => {
|
|
506
|
+
// Import and run the MCP server directly
|
|
507
|
+
await import('../src/mcp.js');
|
|
508
|
+
});
|
|
509
|
+
|
|
435
510
|
program.hook('postAction', async () => {
|
|
436
511
|
await checkForUpdate();
|
|
437
512
|
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"memoir": "bin/memoir.js"
|
|
8
|
+
"memoir": "bin/memoir.js",
|
|
9
|
+
"memoir-mcp": "src/mcp.js"
|
|
9
10
|
},
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
@@ -52,17 +53,21 @@
|
|
|
52
53
|
"context-sync",
|
|
53
54
|
"session-handoff",
|
|
54
55
|
"snapshot",
|
|
55
|
-
"resume"
|
|
56
|
+
"resume",
|
|
57
|
+
"mcp",
|
|
58
|
+
"mcp-server",
|
|
59
|
+
"model-context-protocol"
|
|
56
60
|
],
|
|
57
61
|
"author": "camgitt",
|
|
58
62
|
"license": "MIT",
|
|
59
63
|
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
60
65
|
"boxen": "^7.1.1",
|
|
61
66
|
"chalk": "^5.3.0",
|
|
62
67
|
"commander": "^12.0.0",
|
|
63
68
|
"fs-extra": "^11.2.0",
|
|
64
69
|
"gradient-string": "^3.0.0",
|
|
65
70
|
"inquirer": "^9.2.15",
|
|
66
|
-
"ora": "^7.0.1"
|
|
71
|
+
"ora": "^7.0.1"
|
|
67
72
|
}
|
|
68
73
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import boxen from 'boxen';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
const home = os.homedir();
|
|
9
|
+
const TODOS_PATH = path.join(home, '.config', 'memoir', 'project-todos.json');
|
|
10
|
+
|
|
11
|
+
// ─── Helpers ───
|
|
12
|
+
|
|
13
|
+
async function loadTodos() {
|
|
14
|
+
try {
|
|
15
|
+
return await fs.readJson(TODOS_PATH);
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function saveTodos(todos) {
|
|
22
|
+
await fs.ensureDir(path.dirname(TODOS_PATH));
|
|
23
|
+
await fs.writeJson(TODOS_PATH, todos, { spaces: 2 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function git(args, cwd) {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync('git', args, {
|
|
29
|
+
cwd,
|
|
30
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
}).toString().trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function timeAgo(dateStr) {
|
|
39
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
40
|
+
const mins = Math.floor(diff / 60000);
|
|
41
|
+
if (mins < 60) return `${mins}m ago`;
|
|
42
|
+
const hrs = Math.floor(mins / 60);
|
|
43
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
44
|
+
const days = Math.floor(hrs / 24);
|
|
45
|
+
if (days < 30) return `${days}d ago`;
|
|
46
|
+
return `${Math.floor(days / 30)}mo ago`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Scan ───
|
|
50
|
+
|
|
51
|
+
async function discoverProjects() {
|
|
52
|
+
const maxDepth = 3;
|
|
53
|
+
const skipDirs = new Set([
|
|
54
|
+
'node_modules', '.git', '.next', '.vercel', 'dist', 'build',
|
|
55
|
+
'__pycache__', '.venv', 'venv', '.cache', '.npm', '.bun',
|
|
56
|
+
'Library', '.Trash', 'Applications', 'Pictures', 'Music',
|
|
57
|
+
'Movies', 'Public', 'Downloads', '.local', '.cargo', '.rustup',
|
|
58
|
+
'.docker', '.ssh', '.config', '.claude', '.gemini',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const projectMarkers = [
|
|
62
|
+
'package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml',
|
|
63
|
+
'requirements.txt', 'Gemfile', 'pom.xml', 'build.gradle',
|
|
64
|
+
'Makefile', 'CMakeLists.txt', '.project', 'CLAUDE.md',
|
|
65
|
+
'GEMINI.md', 'AGENTS.md',
|
|
66
|
+
'.gitignore', 'index.html', 'main.py', 'app.py', 'index.js',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const projects = [];
|
|
70
|
+
|
|
71
|
+
const scanDir = async (dir, depth = 0) => {
|
|
72
|
+
if (depth > maxDepth) return;
|
|
73
|
+
let entries;
|
|
74
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
75
|
+
|
|
76
|
+
const hasMarker = entries.some(e => !e.isDirectory() && projectMarkers.includes(e.name));
|
|
77
|
+
if (hasMarker && dir !== home) {
|
|
78
|
+
projects.push(dir);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isDirectory()) continue;
|
|
84
|
+
if (entry.name.startsWith('.') && entry.name !== '.github') continue;
|
|
85
|
+
if (skipDirs.has(entry.name)) continue;
|
|
86
|
+
await scanDir(path.join(dir, entry.name), depth + 1);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await scanDir(home);
|
|
91
|
+
return projects;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getProjectStatus(dir) {
|
|
95
|
+
const name = path.basename(dir);
|
|
96
|
+
const hasGit = fs.pathExistsSync(path.join(dir, '.git'));
|
|
97
|
+
|
|
98
|
+
const info = { name, path: dir, hasGit, branch: null, dirty: false, logs: [] };
|
|
99
|
+
|
|
100
|
+
if (!hasGit) return info;
|
|
101
|
+
|
|
102
|
+
info.branch = git(['branch', '--show-current'], dir) || 'unknown';
|
|
103
|
+
|
|
104
|
+
const status = git(['status', '--porcelain'], dir);
|
|
105
|
+
info.dirty = status ? status.length > 0 : false;
|
|
106
|
+
|
|
107
|
+
const logRaw = git(['log', '-5', '--format=%ad|%s', '--date=format:%b %d, %H:%M'], dir);
|
|
108
|
+
if (logRaw) {
|
|
109
|
+
info.logs = logRaw.split('\n').filter(Boolean).map(line => {
|
|
110
|
+
const sep = line.indexOf('|');
|
|
111
|
+
return { date: line.slice(0, sep), msg: line.slice(sep + 1) };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lastDate = git(['log', '-1', '--format=%aI'], dir);
|
|
116
|
+
if (lastDate) info.lastActivity = lastDate;
|
|
117
|
+
|
|
118
|
+
return info;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Commands ───
|
|
122
|
+
|
|
123
|
+
export async function projectsListCommand(options) {
|
|
124
|
+
const dirs = await discoverProjects();
|
|
125
|
+
const todos = await loadTodos();
|
|
126
|
+
|
|
127
|
+
// Gather status for all projects
|
|
128
|
+
const projects = dirs.map(d => {
|
|
129
|
+
const s = getProjectStatus(d);
|
|
130
|
+
s.todos = todos[s.name] || [];
|
|
131
|
+
return s;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Sort by last activity (most recent first)
|
|
135
|
+
projects.sort((a, b) => {
|
|
136
|
+
if (!a.lastActivity && !b.lastActivity) return 0;
|
|
137
|
+
if (!a.lastActivity) return 1;
|
|
138
|
+
if (!b.lastActivity) return -1;
|
|
139
|
+
return new Date(b.lastActivity) - new Date(a.lastActivity);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (options.json) {
|
|
143
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('\n' + boxen(
|
|
148
|
+
chalk.white.bold(' projects ') + chalk.gray(` ${projects.length} found`),
|
|
149
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
150
|
+
));
|
|
151
|
+
|
|
152
|
+
const limit = options.all ? projects.length : Math.min(projects.length, 15);
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < limit; i++) {
|
|
155
|
+
const p = projects[i];
|
|
156
|
+
const dot = p.hasGit
|
|
157
|
+
? (p.dirty ? chalk.yellow('●') : chalk.green('●'))
|
|
158
|
+
: chalk.gray('●');
|
|
159
|
+
|
|
160
|
+
const age = p.lastActivity ? chalk.gray(` ${timeAgo(p.lastActivity)}`) : '';
|
|
161
|
+
const branchTag = p.branch && p.branch !== 'main' && p.branch !== 'master'
|
|
162
|
+
? chalk.magenta(` [${p.branch}]`)
|
|
163
|
+
: '';
|
|
164
|
+
const dirtyTag = p.dirty ? chalk.yellow(' *') : '';
|
|
165
|
+
const todoTag = p.todos.length > 0
|
|
166
|
+
? chalk.yellow(` (${p.todos.length} todo${p.todos.length > 1 ? 's' : ''})`)
|
|
167
|
+
: '';
|
|
168
|
+
|
|
169
|
+
console.log(`\n ${dot} ${chalk.white.bold(p.name)}${branchTag}${dirtyTag}${todoTag}${age}`);
|
|
170
|
+
|
|
171
|
+
// Show last few commits
|
|
172
|
+
const logCount = options.verbose ? 5 : 2;
|
|
173
|
+
for (let j = 0; j < Math.min(p.logs.length, logCount); j++) {
|
|
174
|
+
const log = p.logs[j];
|
|
175
|
+
console.log(` ${chalk.gray(log.date)} ${chalk.dim(log.msg)}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Show todos inline
|
|
179
|
+
if (p.todos.length > 0 && options.verbose) {
|
|
180
|
+
for (const t of p.todos) {
|
|
181
|
+
console.log(` ${chalk.yellow('□')} ${chalk.yellow(t)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!options.all && projects.length > 15) {
|
|
187
|
+
console.log(chalk.gray(`\n ... and ${projects.length - 15} more (use --all to show all)`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(chalk.gray(`\n ${chalk.green('●')} clean ${chalk.yellow('●')} dirty ${chalk.gray('●')} no git\n`));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function projectsTodoCommand(projectName, text, options) {
|
|
194
|
+
const todos = await loadTodos();
|
|
195
|
+
|
|
196
|
+
// List todos for a project
|
|
197
|
+
if (!text && !options.done && !options.clear) {
|
|
198
|
+
const items = todos[projectName] || [];
|
|
199
|
+
if (items.length === 0) {
|
|
200
|
+
console.log(chalk.gray(`\n No todos for ${projectName}\n`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
console.log(chalk.white.bold(`\n ${projectName} todos:\n`));
|
|
204
|
+
items.forEach((t, i) => {
|
|
205
|
+
console.log(` ${chalk.gray(`${i + 1}.`)} ${chalk.yellow('□')} ${t}`);
|
|
206
|
+
});
|
|
207
|
+
console.log('');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Mark done
|
|
212
|
+
if (options.done !== undefined) {
|
|
213
|
+
const idx = parseInt(options.done, 10) - 1;
|
|
214
|
+
const items = todos[projectName] || [];
|
|
215
|
+
if (idx < 0 || idx >= items.length) {
|
|
216
|
+
console.error(chalk.red(`\n ✖ Invalid index. ${projectName} has ${items.length} todo(s).\n`));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const removed = items.splice(idx, 1)[0];
|
|
220
|
+
todos[projectName] = items;
|
|
221
|
+
if (items.length === 0) delete todos[projectName];
|
|
222
|
+
await saveTodos(todos);
|
|
223
|
+
console.log(chalk.green(`\n ✔ Done: ${removed}\n`));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Clear all
|
|
228
|
+
if (options.clear) {
|
|
229
|
+
delete todos[projectName];
|
|
230
|
+
await saveTodos(todos);
|
|
231
|
+
console.log(chalk.green(`\n ✔ Cleared all todos for ${projectName}\n`));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Add todo
|
|
236
|
+
if (!todos[projectName]) todos[projectName] = [];
|
|
237
|
+
todos[projectName].push(text);
|
|
238
|
+
await saveTodos(todos);
|
|
239
|
+
console.log(chalk.green(`\n ✔ Added to ${projectName}: ${text}\n`));
|
|
240
|
+
}
|
package/src/commands/push.js
CHANGED
|
@@ -9,7 +9,7 @@ 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, shouldIgnoreProject } from '../context/capture.js';
|
|
12
|
+
import { findClaudeSessions, parseSession, generateContextHandoff, shouldIgnoreProject, persistDecisions } from '../context/capture.js';
|
|
13
13
|
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';
|
|
@@ -77,10 +77,19 @@ export async function pushCommand(options = {}) {
|
|
|
77
77
|
await fs.writeFile(path.join(localHandoffDir, `${timestamp}-claude.md`), clean);
|
|
78
78
|
await fs.writeFile(path.join(localHandoffDir, 'latest.md'), clean);
|
|
79
79
|
|
|
80
|
+
// Persist decisions to Claude's memory so they survive across sessions
|
|
81
|
+
let decisionCount = 0;
|
|
82
|
+
if (parsed.decisions.length > 0) {
|
|
83
|
+
try {
|
|
84
|
+
decisionCount = persistDecisions(parsed.decisions);
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
contextCaptured = true;
|
|
81
89
|
sessionInfo = {
|
|
82
90
|
slug: parsed.slug,
|
|
83
91
|
filesModified: parsed.filesWritten.length,
|
|
92
|
+
decisions: decisionCount,
|
|
84
93
|
duration: parsed.firstTimestamp && parsed.lastTimestamp
|
|
85
94
|
? (() => {
|
|
86
95
|
const ms = new Date(parsed.lastTimestamp) - new Date(parsed.firstTimestamp);
|
|
@@ -170,14 +179,16 @@ export async function pushCommand(options = {}) {
|
|
|
170
179
|
mask: '*',
|
|
171
180
|
validate: (input) => input.length >= 6 ? true : 'Passphrase must be at least 6 characters'
|
|
172
181
|
}]);
|
|
173
|
-
spinner.start(chalk.gray('
|
|
182
|
+
spinner.start(chalk.gray('Deriving encryption key...'));
|
|
174
183
|
|
|
175
184
|
encryptedDir = path.join(os.tmpdir(), `memoir-encrypted-${Date.now()}`);
|
|
176
185
|
await fs.ensureDir(encryptedDir);
|
|
177
|
-
await encryptDirectory(stagingDir, encryptedDir, passphrase);
|
|
186
|
+
const encryptedCount = await encryptDirectory(stagingDir, encryptedDir, passphrase, spinner);
|
|
187
|
+
spinner.succeed(chalk.green(spinner.text));
|
|
188
|
+
spinner.start();
|
|
178
189
|
|
|
179
190
|
// Save verify token so restore can check passphrase before decrypting
|
|
180
|
-
const token = createVerifyToken(passphrase);
|
|
191
|
+
const token = await createVerifyToken(passphrase);
|
|
181
192
|
await fs.writeFile(path.join(encryptedDir, 'verify.enc'), token);
|
|
182
193
|
|
|
183
194
|
uploadDir = encryptedDir;
|
|
@@ -226,6 +237,9 @@ export async function pushCommand(options = {}) {
|
|
|
226
237
|
if (sessionInfo.duration) parts.push(sessionInfo.duration);
|
|
227
238
|
if (sessionInfo.filesModified) parts.push(`${sessionInfo.filesModified} files changed`);
|
|
228
239
|
contextLine = '\n' + chalk.green(' ✔ Session Context') + chalk.gray(` (${parts.join(', ')})`) + '\n';
|
|
240
|
+
if (sessionInfo.decisions > 0) {
|
|
241
|
+
contextLine += chalk.green(` ✔ ${sessionInfo.decisions} decision(s) saved to persistent memory`) + '\n';
|
|
242
|
+
}
|
|
229
243
|
if (sessionInfo.secretsRedacted > 0) {
|
|
230
244
|
contextLine += chalk.yellow(` 🔒 ${sessionInfo.secretsRedacted} secret(s) auto-redacted`) + '\n';
|
|
231
245
|
}
|