memoir-cli 1.5.2 → 2.0.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 +2 -0
- package/SHOW_HN.md +23 -0
- package/bin/memoir.js +52 -4
- package/demo.cast +269 -0
- package/demo.gif +0 -0
- package/demo.sh +99 -0
- package/package.json +7 -4
- package/src/adapters/restore.js +25 -14
- package/src/commands/diff.js +221 -0
- package/src/commands/restore.js +4 -2
- package/src/commands/resume.js +166 -0
- package/src/commands/snapshot.js +382 -0
- package/src/providers/restore.js +4 -4
- package/CLAUDE.md +0 -1
- package/TODO.md +0 -39
|
@@ -0,0 +1,382 @@
|
|
|
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 { getConfig, getGeminiApiKey } from '../config.js';
|
|
9
|
+
import { syncToLocal, syncToGit } from '../providers/index.js';
|
|
10
|
+
|
|
11
|
+
const home = os.homedir();
|
|
12
|
+
|
|
13
|
+
// Find all Claude session files, sorted newest first
|
|
14
|
+
function findClaudeSessions() {
|
|
15
|
+
const projectsDir = path.join(home, '.claude', 'projects');
|
|
16
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
17
|
+
|
|
18
|
+
const sessions = [];
|
|
19
|
+
const scanDir = (dir) => {
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = path.join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
scanDir(full);
|
|
25
|
+
} else if (entry.name.endsWith('.jsonl') && !entry.name.includes('subagent')) {
|
|
26
|
+
const stat = fs.statSync(full);
|
|
27
|
+
sessions.push({ path: full, mtime: stat.mtimeMs, size: stat.size });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
scanDir(projectsDir);
|
|
32
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
33
|
+
return sessions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse a Claude .jsonl session file
|
|
37
|
+
function parseClaudeSession(sessionPath) {
|
|
38
|
+
const raw = fs.readFileSync(sessionPath, 'utf8').trim();
|
|
39
|
+
const lines = raw.split('\n');
|
|
40
|
+
|
|
41
|
+
const result = {
|
|
42
|
+
sessionId: null,
|
|
43
|
+
slug: null,
|
|
44
|
+
gitBranch: null,
|
|
45
|
+
cwd: null,
|
|
46
|
+
firstTimestamp: null,
|
|
47
|
+
lastTimestamp: null,
|
|
48
|
+
model: null,
|
|
49
|
+
userMessages: [],
|
|
50
|
+
filesWritten: new Set(),
|
|
51
|
+
filesRead: new Set(),
|
|
52
|
+
bashCommands: [],
|
|
53
|
+
errors: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
let obj;
|
|
58
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
59
|
+
|
|
60
|
+
if (!result.sessionId && obj.sessionId) result.sessionId = obj.sessionId;
|
|
61
|
+
if (!result.slug && obj.slug) result.slug = obj.slug;
|
|
62
|
+
if (!result.gitBranch && obj.gitBranch) result.gitBranch = obj.gitBranch;
|
|
63
|
+
if (!result.cwd && obj.cwd) result.cwd = obj.cwd;
|
|
64
|
+
if (!result.firstTimestamp && obj.timestamp) result.firstTimestamp = obj.timestamp;
|
|
65
|
+
if (obj.timestamp) result.lastTimestamp = obj.timestamp;
|
|
66
|
+
|
|
67
|
+
// User messages (skip system/task notifications)
|
|
68
|
+
if (obj.type === 'user' && obj.message?.content) {
|
|
69
|
+
const content = typeof obj.message.content === 'string' ? obj.message.content : '';
|
|
70
|
+
if (content.length > 3 && !content.startsWith('<task-notification>')) {
|
|
71
|
+
result.userMessages.push(content);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Tool uses from assistant messages
|
|
76
|
+
if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
|
|
77
|
+
for (const block of obj.message.content) {
|
|
78
|
+
if (block.type !== 'tool_use') continue;
|
|
79
|
+
const name = block.name;
|
|
80
|
+
const input = block.input || {};
|
|
81
|
+
|
|
82
|
+
if (name === 'Write' || name === 'Edit') {
|
|
83
|
+
const fp = input.file_path || '';
|
|
84
|
+
if (fp && !fp.startsWith('/tmp/') && !fp.startsWith('/private/tmp/')) {
|
|
85
|
+
result.filesWritten.add(fp);
|
|
86
|
+
}
|
|
87
|
+
} else if (name === 'Read') {
|
|
88
|
+
const fp = input.file_path || '';
|
|
89
|
+
if (fp && !fp.startsWith('/tmp/') && !fp.startsWith('/private/tmp/')) {
|
|
90
|
+
result.filesRead.add(fp);
|
|
91
|
+
}
|
|
92
|
+
} else if (name === 'Bash') {
|
|
93
|
+
const cmd = (input.command || '').trim();
|
|
94
|
+
if (cmd && !cmd.startsWith('sleep') && !cmd.startsWith('cat /private/tmp')) {
|
|
95
|
+
result.bashCommands.push(cmd.length > 120 ? cmd.slice(0, 120) + '...' : cmd);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Capture errors from tool results
|
|
102
|
+
if (obj.type === 'tool_result' && obj.message?.content) {
|
|
103
|
+
const content = typeof obj.message.content === 'string' ? obj.message.content : '';
|
|
104
|
+
if (content.includes('Error') || content.includes('error') || content.includes('FAIL')) {
|
|
105
|
+
const errorLine = content.split('\n').find(l => /error|fail/i.test(l));
|
|
106
|
+
if (errorLine && errorLine.length < 200) {
|
|
107
|
+
result.errors.push(errorLine.trim());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Convert sets to arrays
|
|
114
|
+
result.filesWritten = [...result.filesWritten];
|
|
115
|
+
result.filesRead = [...result.filesRead];
|
|
116
|
+
// Deduplicate errors
|
|
117
|
+
result.errors = [...new Set(result.errors)].slice(0, 10);
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Shorten file paths relative to cwd for readability
|
|
123
|
+
function shortenPath(filePath, cwd) {
|
|
124
|
+
if (filePath.startsWith(cwd + '/')) {
|
|
125
|
+
return filePath.slice(cwd.length + 1);
|
|
126
|
+
}
|
|
127
|
+
if (filePath.startsWith(home + '/')) {
|
|
128
|
+
return '~/' + filePath.slice(home.length + 1);
|
|
129
|
+
}
|
|
130
|
+
return filePath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Format duration between two ISO timestamps
|
|
134
|
+
function formatDuration(start, end) {
|
|
135
|
+
const ms = new Date(end) - new Date(start);
|
|
136
|
+
const mins = Math.floor(ms / 60000);
|
|
137
|
+
if (mins < 60) return `${mins}m`;
|
|
138
|
+
const hours = Math.floor(mins / 60);
|
|
139
|
+
const remaining = mins % 60;
|
|
140
|
+
return `${hours}h ${remaining}m`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generate handoff markdown from parsed session
|
|
144
|
+
function generateHandoff(parsed, options = {}) {
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const dateStr = now.toISOString().split('T')[0];
|
|
147
|
+
const timeStr = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
148
|
+
const hostname = os.hostname();
|
|
149
|
+
const platform = process.platform === 'darwin' ? 'macOS' : process.platform === 'win32' ? 'Windows' : 'Linux';
|
|
150
|
+
const duration = formatDuration(parsed.firstTimestamp, parsed.lastTimestamp);
|
|
151
|
+
const cwd = parsed.cwd || home;
|
|
152
|
+
|
|
153
|
+
// Build user message summary (filter noise, keep substance)
|
|
154
|
+
const meaningfulMessages = parsed.userMessages
|
|
155
|
+
.filter(m => m.length > 10 && !m.startsWith('ok') && !m.startsWith('yes'))
|
|
156
|
+
.map(m => m.length > 200 ? m.slice(0, 200) + '...' : m);
|
|
157
|
+
|
|
158
|
+
// YAML frontmatter
|
|
159
|
+
let md = `---
|
|
160
|
+
memoir_version: "2.0"
|
|
161
|
+
source_tool: claude
|
|
162
|
+
session_id: ${parsed.sessionId || 'unknown'}
|
|
163
|
+
session_name: ${parsed.slug || 'unknown'}
|
|
164
|
+
timestamp: ${now.toISOString()}
|
|
165
|
+
machine: ${hostname} (${platform})
|
|
166
|
+
project: ${cwd}
|
|
167
|
+
branch: ${parsed.gitBranch || 'unknown'}
|
|
168
|
+
duration: ${duration}
|
|
169
|
+
files_modified: ${parsed.filesWritten.length}
|
|
170
|
+
files_read: ${parsed.filesRead.length}
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
# Session Handoff
|
|
174
|
+
|
|
175
|
+
**From:** ${hostname} (${platform})
|
|
176
|
+
**When:** ${dateStr} ${timeStr}
|
|
177
|
+
**Tool:** Claude Code
|
|
178
|
+
**Branch:** ${parsed.gitBranch || 'unknown'}
|
|
179
|
+
**Duration:** ${duration}
|
|
180
|
+
**Project:** ${cwd}
|
|
181
|
+
|
|
182
|
+
## What was discussed
|
|
183
|
+
${meaningfulMessages.map(m => `- ${m}`).join('\n')}
|
|
184
|
+
|
|
185
|
+
## Files modified
|
|
186
|
+
${parsed.filesWritten.length > 0
|
|
187
|
+
? parsed.filesWritten.map(f => `- \`${shortenPath(f, cwd)}\``).join('\n')
|
|
188
|
+
: '_None_'}
|
|
189
|
+
|
|
190
|
+
## Files referenced
|
|
191
|
+
${parsed.filesRead.length > 0
|
|
192
|
+
? parsed.filesRead.map(f => `- \`${shortenPath(f, cwd)}\``).join('\n')
|
|
193
|
+
: '_None_'}
|
|
194
|
+
|
|
195
|
+
## Commands run
|
|
196
|
+
${parsed.bashCommands.length > 0
|
|
197
|
+
? parsed.bashCommands.slice(0, 20).map(c => `- \`${c}\``).join('\n')
|
|
198
|
+
: '_None_'}
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
if (parsed.errors.length > 0) {
|
|
202
|
+
md += `\n## Errors encountered\n${parsed.errors.map(e => `- ${e}`).join('\n')}\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (options.goal) {
|
|
206
|
+
md += `\n## Goal for next session\n${options.goal}\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
md += `\n## Context for next session\nThis handoff was captured from a Claude Code session on ${platform}. `;
|
|
210
|
+
md += `The session touched ${parsed.filesWritten.length} files and ran ${parsed.bashCommands.length} commands. `;
|
|
211
|
+
if (parsed.filesWritten.length > 0) {
|
|
212
|
+
md += `Key files to review: ${parsed.filesWritten.slice(0, 5).map(f => '`' + shortenPath(f, cwd) + '`').join(', ')}.`;
|
|
213
|
+
}
|
|
214
|
+
md += '\n';
|
|
215
|
+
|
|
216
|
+
return md;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Use Gemini API to create a smart summary
|
|
220
|
+
async function smartSummarize(parsed, apiKey) {
|
|
221
|
+
const prompt = `You are summarizing a coding session for handoff to another machine. Be concise and actionable.
|
|
222
|
+
|
|
223
|
+
Session info:
|
|
224
|
+
- Duration: ${formatDuration(parsed.firstTimestamp, parsed.lastTimestamp)}
|
|
225
|
+
- Branch: ${parsed.gitBranch || 'unknown'}
|
|
226
|
+
- Project dir: ${parsed.cwd || 'unknown'}
|
|
227
|
+
|
|
228
|
+
User messages (what they asked for):
|
|
229
|
+
${parsed.userMessages.filter(m => m.length > 10).map(m => `- ${m.slice(0, 200)}`).join('\n')}
|
|
230
|
+
|
|
231
|
+
Files modified:
|
|
232
|
+
${parsed.filesWritten.map(f => `- ${f}`).join('\n')}
|
|
233
|
+
|
|
234
|
+
Write a structured summary with these exact sections:
|
|
235
|
+
## Summary
|
|
236
|
+
(2-3 sentences of what was accomplished)
|
|
237
|
+
|
|
238
|
+
## Key decisions
|
|
239
|
+
(Bullet list of important decisions made)
|
|
240
|
+
|
|
241
|
+
## Current state
|
|
242
|
+
(What's done, what's in progress, what's left)
|
|
243
|
+
|
|
244
|
+
## Next steps
|
|
245
|
+
(What should be done next to continue this work)
|
|
246
|
+
|
|
247
|
+
Keep it under 300 words total. Be specific about file names and features.`;
|
|
248
|
+
|
|
249
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: { 'Content-Type': 'application/json' },
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
254
|
+
generationConfig: { maxOutputTokens: 1000, temperature: 0.3 }
|
|
255
|
+
})
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw new Error(`Gemini API error: ${response.status}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const data = await response.json();
|
|
263
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text || null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function snapshotCommand(options = {}) {
|
|
267
|
+
const config = await getConfig();
|
|
268
|
+
|
|
269
|
+
console.log();
|
|
270
|
+
const spinner = ora({ text: chalk.gray('Finding latest session...'), spinner: 'dots' }).start();
|
|
271
|
+
|
|
272
|
+
// Find sessions
|
|
273
|
+
const sessions = findClaudeSessions();
|
|
274
|
+
if (sessions.length === 0) {
|
|
275
|
+
spinner.fail(chalk.red('No Claude Code sessions found.'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const latest = sessions[0];
|
|
280
|
+
spinner.text = chalk.gray('Parsing session...');
|
|
281
|
+
|
|
282
|
+
// Parse the session
|
|
283
|
+
const parsed = parseClaudeSession(latest.path);
|
|
284
|
+
|
|
285
|
+
if (parsed.userMessages.length === 0) {
|
|
286
|
+
spinner.fail(chalk.red('Session has no user messages.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
spinner.text = chalk.gray('Generating handoff...');
|
|
291
|
+
|
|
292
|
+
// Generate handoff markdown
|
|
293
|
+
let handoff;
|
|
294
|
+
|
|
295
|
+
if (options.smart) {
|
|
296
|
+
const apiKey = await getGeminiApiKey();
|
|
297
|
+
if (!apiKey) {
|
|
298
|
+
spinner.warn(chalk.yellow('No Gemini API key found. Using local extraction.'));
|
|
299
|
+
spinner.start();
|
|
300
|
+
handoff = generateHandoff(parsed, options);
|
|
301
|
+
} else {
|
|
302
|
+
spinner.text = chalk.gray('Generating AI-powered summary...');
|
|
303
|
+
try {
|
|
304
|
+
const smartSummary = await smartSummarize(parsed, apiKey);
|
|
305
|
+
// Generate base handoff then inject smart summary
|
|
306
|
+
handoff = generateHandoff(parsed, options);
|
|
307
|
+
if (smartSummary) {
|
|
308
|
+
// Insert smart summary after the frontmatter header
|
|
309
|
+
const insertPoint = handoff.indexOf('## What was discussed');
|
|
310
|
+
handoff = handoff.slice(0, insertPoint) +
|
|
311
|
+
'## AI Summary\n' + smartSummary + '\n\n' +
|
|
312
|
+
handoff.slice(insertPoint);
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
spinner.warn(chalk.yellow(`AI summary failed: ${err.message}. Using local extraction.`));
|
|
316
|
+
spinner.start();
|
|
317
|
+
handoff = generateHandoff(parsed, options);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
handoff = generateHandoff(parsed, options);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Save handoff
|
|
325
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
326
|
+
const filename = `${timestamp}-claude.md`;
|
|
327
|
+
|
|
328
|
+
// If config exists, push to backup
|
|
329
|
+
if (config) {
|
|
330
|
+
const stagingDir = path.join(os.tmpdir(), `memoir-handoff-${Date.now()}`);
|
|
331
|
+
await fs.ensureDir(path.join(stagingDir, 'handoffs'));
|
|
332
|
+
await fs.writeFile(path.join(stagingDir, 'handoffs', filename), handoff);
|
|
333
|
+
|
|
334
|
+
spinner.text = chalk.gray('Pushing handoff to backup...');
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
if (config.provider === 'local' || config.provider.includes('local')) {
|
|
338
|
+
await syncToLocal(config, stagingDir, spinner);
|
|
339
|
+
} else if (config.provider === 'git' || config.provider.includes('git')) {
|
|
340
|
+
await syncToGit(config, stagingDir, spinner);
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
spinner.warn(chalk.yellow(`Push failed: ${err.message}. Saved locally.`));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await fs.remove(stagingDir);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Also save locally for immediate access
|
|
350
|
+
const localHandoffDir = path.join(home, '.config', 'memoir', 'handoffs');
|
|
351
|
+
await fs.ensureDir(localHandoffDir);
|
|
352
|
+
await fs.writeFile(path.join(localHandoffDir, filename), handoff);
|
|
353
|
+
|
|
354
|
+
// Also save as "latest" for easy access
|
|
355
|
+
await fs.writeFile(path.join(localHandoffDir, 'latest.md'), handoff);
|
|
356
|
+
|
|
357
|
+
spinner.stop();
|
|
358
|
+
|
|
359
|
+
// Display summary
|
|
360
|
+
const duration = formatDuration(parsed.firstTimestamp, parsed.lastTimestamp);
|
|
361
|
+
console.log('\n' + boxen(
|
|
362
|
+
gradient.pastel(' Snapshot captured ') + '\n\n' +
|
|
363
|
+
chalk.white(`Session: ${parsed.slug || 'unnamed'}`) + '\n' +
|
|
364
|
+
chalk.gray(`Duration: ${duration} | Branch: ${parsed.gitBranch || '?'} | ${parsed.filesWritten.length} files changed`) + '\n\n' +
|
|
365
|
+
chalk.white.bold('Files modified:') + '\n' +
|
|
366
|
+
(parsed.filesWritten.length > 0
|
|
367
|
+
? parsed.filesWritten.slice(0, 8).map(f => chalk.cyan(` ${shortenPath(f, parsed.cwd || home)}`)).join('\n')
|
|
368
|
+
: chalk.gray(' None')) +
|
|
369
|
+
(parsed.filesWritten.length > 8 ? chalk.gray(`\n ...and ${parsed.filesWritten.length - 8} more`) : '') + '\n\n' +
|
|
370
|
+
chalk.white.bold('User requests:') + '\n' +
|
|
371
|
+
parsed.userMessages
|
|
372
|
+
.filter(m => m.length > 10 && !m.startsWith('ok') && !m.startsWith('yes'))
|
|
373
|
+
.slice(0, 5)
|
|
374
|
+
.map(m => chalk.gray(` "${m.slice(0, 80)}${m.length > 80 ? '...' : ''}"`))
|
|
375
|
+
.join('\n') + '\n\n' +
|
|
376
|
+
chalk.gray(`Saved to: ${localHandoffDir}/${filename}`) +
|
|
377
|
+
(config ? '\n' + chalk.gray(`Pushed to: ${config.provider === 'git' ? config.gitRepo : config.localPath}`) : ''),
|
|
378
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
379
|
+
) + '\n');
|
|
380
|
+
|
|
381
|
+
console.log(chalk.gray(' Restore on another machine with: ') + chalk.cyan('memoir resume') + '\n');
|
|
382
|
+
}
|
package/src/providers/restore.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import { execFileSync } from 'child_process';
|
|
6
6
|
import { restoreMemories } from '../adapters/restore.js';
|
|
7
7
|
|
|
8
|
-
export async function fetchFromLocal(config, stagingDir, spinner, onlyFilter = null) {
|
|
8
|
+
export async function fetchFromLocal(config, stagingDir, spinner, onlyFilter = null, autoYes = false) {
|
|
9
9
|
const sourceDir = config.localPath;
|
|
10
10
|
if (!sourceDir) throw new Error('Local path is not configured.');
|
|
11
11
|
|
|
@@ -18,10 +18,10 @@ export async function fetchFromLocal(config, stagingDir, spinner, onlyFilter = n
|
|
|
18
18
|
spinner.text = `Fetching data from local directory: ${chalk.cyan(resolvedSource)}`;
|
|
19
19
|
await fs.copy(resolvedSource, stagingDir);
|
|
20
20
|
|
|
21
|
-
return await restoreMemories(stagingDir, spinner, onlyFilter);
|
|
21
|
+
return await restoreMemories(stagingDir, spinner, onlyFilter, autoYes);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function fetchFromGit(config, stagingDir, spinner, onlyFilter = null) {
|
|
24
|
+
export async function fetchFromGit(config, stagingDir, spinner, onlyFilter = null, autoYes = false) {
|
|
25
25
|
const repoUrl = config.gitRepo;
|
|
26
26
|
if (!repoUrl) throw new Error('Git repository is not configured.');
|
|
27
27
|
|
|
@@ -33,5 +33,5 @@ export async function fetchFromGit(config, stagingDir, spinner, onlyFilter = nul
|
|
|
33
33
|
throw new Error('Failed to pull from git repository. Ensure your SSH keys are configured and the repository is accessible.');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return await restoreMemories(stagingDir, spinner, onlyFilter);
|
|
36
|
+
return await restoreMemories(stagingDir, spinner, onlyFilter, autoYes);
|
|
37
37
|
}
|
package/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Old Claude Instructions
|
package/TODO.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# memoir Roadmap & TODO
|
|
2
|
-
|
|
3
|
-
This list tracks planned features, identified bugs, and architectural improvements for the `memoir` CLI.
|
|
4
|
-
|
|
5
|
-
## 🔴 High Priority: Security, Reliability & Bug Fixes
|
|
6
|
-
- [ ] **`memoir doctor` Command:** Implement a diagnostic utility to verify:
|
|
7
|
-
- [ ] Correct installation of supported AI tools (Claude, Cursor, etc.).
|
|
8
|
-
- [ ] File permissions for all memory directories.
|
|
9
|
-
- [ ] Git connectivity and API key validity (Gemini).
|
|
10
|
-
- [ ] Environment variable health.
|
|
11
|
-
- [ ] **Secret & PII Guard:** Add a pre-push scan to detect API keys or PII in memory files. Implement a `redact` flag.
|
|
12
|
-
- [ ] **Linux Path Support:** Add path detection for Cursor and Windsurf on Linux (currently macOS/Windows only).
|
|
13
|
-
- [ ] **Aider Local Discovery:** Update the Aider adapter to look for `.aider/` in the current project repo, not just global config.
|
|
14
|
-
- [ ] **Robust Claude Pathing:** Replace fragile string-replacement in `src/tools/claude.js` with more reliable hashing/matching for `~/.claude/projects`.
|
|
15
|
-
- [ ] **Add `-y` / `--yes` Flags:** Enable non-interactive mode for `push`, `restore`, and `migrate` to support automation.
|
|
16
|
-
|
|
17
|
-
## 🟡 Medium Priority: UX & Workflow
|
|
18
|
-
- [ ] **Local LLM Migration (Privacy):**
|
|
19
|
-
- [ ] Add support for **Ollama** and **LM Studio** as translation engines in `migrate`.
|
|
20
|
-
- [ ] Add a `--local` flag to `migrate` to bypass external APIs.
|
|
21
|
-
- [ ] **`memoir watch` (Background Sync):** Create a lightweight daemon to detect changes in local memory files and trigger auto-sync.
|
|
22
|
-
- [ ] **Project Bootstrapping (`memoir init --template`):** Seed new projects with "Golden Rule" templates (e.g., "Strict TypeScript," "React/Tailwind Best Practices").
|
|
23
|
-
- [ ] **Interactive Merge/Diff:** Replace "Overwrite/Append" in `migrate` with a side-by-side diff view.
|
|
24
|
-
- [ ] **Silent Mode:** Add a `--silent` flag to suppress all output except errors.
|
|
25
|
-
|
|
26
|
-
## 🟢 Low Priority: Intelligence & Advanced Features
|
|
27
|
-
- [ ] **Unified Memory Format (UMF):** Architect an internal JSON schema to represent "Coding Context" to simplify adding new tool adapters.
|
|
28
|
-
- [ ] **Cross-Tool Search:** Implement `memoir search <query>` to find specific instructions across all tool backups.
|
|
29
|
-
- [ ] **Context Compression:** AI-powered `memoir optimize` command to summarize long instruction files and save tokens.
|
|
30
|
-
- [ ] **Organization Sync:** Support for shared "Team Memories" stored in a central repository.
|
|
31
|
-
- [ ] **Memory Analytics:** `memoir stats` to visualize the growth and "personality" of your AI instructions over time.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## 📝 Technical Observations & Bug Log
|
|
36
|
-
* **Circular Dependency:** `package.json` lists `memoir-cli` as its own dependency. Needs cleanup.
|
|
37
|
-
* **Git Performance:** `push` currently clones the entire repo every time. Optimize with `git clone --depth 1` or local cache.
|
|
38
|
-
* **Performance:** Refactor `fs.readFileSync` inside loops (e.g., in `src/tools/claude.js`) to use `fs.promises` for better handling of large projects.
|
|
39
|
-
* **CLI Friction:** The `migrate` command's interactive prompt for multiple files can be tedious; batching confirmations would improve UX.
|