memoir-cli 3.1.0 → 3.1.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 +122 -55
- package/package.json +1 -1
- package/src/commands/push.js +13 -1
- package/src/context/capture.js +146 -1
package/README.md
CHANGED
|
@@ -2,43 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
# memoir
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Your AI remembers everything. On every machine.**
|
|
6
6
|
|
|
7
7
|
[](https://npmjs.org/package/memoir-cli)
|
|
8
8
|
[](https://npmjs.org/package/memoir-cli)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Close your laptop. Open another one. **Your AI picks up exactly where you left off.**
|
|
13
13
|
|
|
14
14
|
[Website](https://memoir.sh) • [npm](https://npmjs.org/package/memoir-cli) • [Blog](https://memoir.sh/blog)
|
|
15
15
|
|
|
16
16
|
<br />
|
|
17
17
|
|
|
18
|
-

|
|
19
|
-
|
|
20
18
|
</div>
|
|
21
19
|
|
|
22
|
-
##
|
|
20
|
+
## The Problem
|
|
23
21
|
|
|
24
|
-
You spend weeks teaching
|
|
22
|
+
You spend weeks teaching Claude how you code. Your projects are dialed in. Your AI knows your stack, your decisions, your preferences.
|
|
25
23
|
|
|
26
|
-
Then you
|
|
24
|
+
Then you switch machines. **Everything is gone.** Your AI has amnesia. Your projects aren't there. You start from zero.
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
## The Fix
|
|
29
27
|
|
|
30
28
|
```bash
|
|
29
|
+
# On your main machine
|
|
30
|
+
memoir push
|
|
31
|
+
|
|
32
|
+
# On any other machine
|
|
31
33
|
npm install -g memoir-cli
|
|
34
|
+
memoir restore -y
|
|
35
|
+
|
|
36
|
+
# Done. Everything's back:
|
|
37
|
+
# ✔ AI memory restored (Claude, Gemini, Cursor, 11 tools)
|
|
38
|
+
# ✔ 44 projects cloned & unpacked
|
|
39
|
+
# ✔ Uncommitted changes applied
|
|
40
|
+
# ✔ Session context injected — AI picks up mid-conversation
|
|
32
41
|
```
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
One command to save. One command to restore. That's it.
|
|
44
|
+
|
|
45
|
+
## What Gets Synced
|
|
46
|
+
|
|
47
|
+
memoir syncs three layers that no other tool connects:
|
|
48
|
+
|
|
49
|
+
### Layer 1: AI Memory
|
|
50
|
+
Your AI tool configs, preferences, and project knowledge across 11 tools.
|
|
35
51
|
|
|
36
52
|
| Tool | What gets synced |
|
|
37
53
|
|------|-----------------|
|
|
38
|
-
| **ChatGPT** | CHATGPT.md custom instructions |
|
|
39
54
|
| **Claude Code** | ~/.claude/ settings, memory, CLAUDE.md files |
|
|
40
55
|
| **Gemini CLI** | ~/.gemini/ config, GEMINI.md files |
|
|
41
|
-
| **
|
|
56
|
+
| **ChatGPT** | CHATGPT.md custom instructions |
|
|
57
|
+
| **OpenAI Codex** | ~/.codex/ config, AGENTS.md |
|
|
42
58
|
| **Cursor** | Settings, keybindings, .cursorrules |
|
|
43
59
|
| **GitHub Copilot** | Config, copilot-instructions.md |
|
|
44
60
|
| **Windsurf** | Settings, keybindings, .windsurfrules |
|
|
@@ -47,7 +63,37 @@ npm install -g memoir-cli
|
|
|
47
63
|
| **Continue.dev** | Config, .continuerules |
|
|
48
64
|
| **Aider** | .aider.conf.yml, system prompt |
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
### Layer 2: Session State
|
|
67
|
+
What you were **doing** — not just what your AI knows, but the active context.
|
|
68
|
+
|
|
69
|
+
- Last coding session captured automatically
|
|
70
|
+
- What files you changed, what errors you hit, what decisions you made
|
|
71
|
+
- Injected into your AI on restore so it picks up mid-conversation
|
|
72
|
+
- Secrets auto-redacted (API keys, tokens, passwords stripped before sync)
|
|
73
|
+
|
|
74
|
+
### Layer 3: Workspace (NEW in v3.1)
|
|
75
|
+
Your actual projects — code, files, everything.
|
|
76
|
+
|
|
77
|
+
- **Git projects:** Remote URLs saved, auto-cloned on restore
|
|
78
|
+
- **Non-git projects:** Bundled as compressed archives, unpacked on restore
|
|
79
|
+
- **Uncommitted work:** Saved as patches, applied after clone
|
|
80
|
+
- **Zero git commands needed** — memoir handles it all
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
memoir push on Mac:
|
|
84
|
+
✔ AI memory backed up
|
|
85
|
+
✔ Session context captured
|
|
86
|
+
✔ Workspace: 44 projects (17 git, 23 bundled)
|
|
87
|
+
🔒 E2E encrypted
|
|
88
|
+
|
|
89
|
+
memoir restore on Windows:
|
|
90
|
+
✔ AI memory restored
|
|
91
|
+
✔ stock-market-book → C:\Users\You\stock-market-book (cloned)
|
|
92
|
+
✔ socialslink → C:\Users\You\socialslink (cloned)
|
|
93
|
+
✔ btc-trader → C:\Users\You\btc-trader (unpacked)
|
|
94
|
+
✔ 41 more projects restored
|
|
95
|
+
📋 Session context injected — Claude picks up where you left off
|
|
96
|
+
```
|
|
51
97
|
|
|
52
98
|
## Quick Start
|
|
53
99
|
|
|
@@ -55,41 +101,52 @@ Plus **per-project configs** — memoir scans your filesystem for CLAUDE.md, GEM
|
|
|
55
101
|
# Install
|
|
56
102
|
npm install -g memoir-cli
|
|
57
103
|
|
|
58
|
-
# First-time setup
|
|
104
|
+
# First-time setup
|
|
59
105
|
memoir init
|
|
60
106
|
|
|
61
|
-
# Back up
|
|
107
|
+
# Back up everything
|
|
62
108
|
memoir push
|
|
63
109
|
|
|
64
|
-
# Restore on
|
|
110
|
+
# Restore on any machine
|
|
65
111
|
memoir restore
|
|
66
112
|
```
|
|
67
113
|
|
|
68
|
-
That's it. Every AI tool gets its memory back.
|
|
69
|
-
|
|
70
114
|
## Key Features
|
|
71
115
|
|
|
72
|
-
###
|
|
116
|
+
### Workspace sync
|
|
73
117
|
```bash
|
|
74
|
-
memoir
|
|
75
|
-
#
|
|
118
|
+
memoir push # scans all projects, saves git URLs + bundles non-git projects
|
|
119
|
+
memoir restore # auto-clones repos, unpacks bundles, applies uncommitted patches
|
|
76
120
|
```
|
|
77
121
|
|
|
78
|
-
|
|
122
|
+
No manual git commands. memoir detects your projects, tracks their remotes, and restores them anywhere.
|
|
79
123
|
|
|
124
|
+
### Translate between AI tools
|
|
80
125
|
```bash
|
|
126
|
+
memoir migrate --from chatgpt --to claude
|
|
127
|
+
# AI-powered — rewrites conventions, not copy-paste
|
|
128
|
+
|
|
81
129
|
memoir migrate --from chatgpt --to all
|
|
130
|
+
# Translate to every tool at once
|
|
82
131
|
```
|
|
83
132
|
|
|
84
133
|
### Session handoff
|
|
85
134
|
```bash
|
|
86
|
-
#
|
|
135
|
+
# Capture your session (automatic on push, or manual)
|
|
87
136
|
memoir snapshot
|
|
88
137
|
|
|
89
138
|
# Pick up on another machine
|
|
90
139
|
memoir resume --inject --to claude
|
|
91
140
|
```
|
|
92
141
|
|
|
142
|
+
### E2E Encryption
|
|
143
|
+
```bash
|
|
144
|
+
memoir encrypt # toggle encryption on/off
|
|
145
|
+
memoir push # prompted for passphrase, AES-256-GCM encrypted
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Your backup is encrypted before it leaves your machine. Even if your storage is compromised, your data is safe. Secret scanning auto-redacts API keys, tokens, and passwords.
|
|
149
|
+
|
|
93
150
|
### Profiles (personal / work)
|
|
94
151
|
```bash
|
|
95
152
|
memoir profile create work
|
|
@@ -107,23 +164,24 @@ memoir cloud restore # restore from any version
|
|
|
107
164
|
memoir history # view all backup versions
|
|
108
165
|
```
|
|
109
166
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
### Security
|
|
167
|
+
### Cross-platform (Mac / Windows / Linux)
|
|
113
168
|
```bash
|
|
114
|
-
|
|
115
|
-
|
|
169
|
+
# Push from Mac
|
|
170
|
+
memoir push
|
|
171
|
+
|
|
172
|
+
# Restore on Windows — paths remap automatically
|
|
173
|
+
memoir restore
|
|
116
174
|
```
|
|
117
175
|
|
|
118
|
-
|
|
176
|
+
Claude's memory paths are automatically remapped between platforms. Projects are cloned to the right locations. It just works.
|
|
119
177
|
|
|
120
178
|
## All Commands
|
|
121
179
|
|
|
122
180
|
| Command | What it does |
|
|
123
181
|
|---------|-------------|
|
|
124
182
|
| `memoir init` | Setup wizard — GitHub or local storage |
|
|
125
|
-
| `memoir push` | Back up
|
|
126
|
-
| `memoir restore` | Restore on a new machine |
|
|
183
|
+
| `memoir push` | Back up AI memory + workspace + session |
|
|
184
|
+
| `memoir restore` | Restore everything on a new machine |
|
|
127
185
|
| `memoir status` | Show detected AI tools |
|
|
128
186
|
| `memoir doctor` | Diagnose issues, scan for secrets |
|
|
129
187
|
| `memoir view` | Preview what's in your backup |
|
|
@@ -131,6 +189,7 @@ memoir **never** syncs credentials, API keys, .env files, or auth tokens.
|
|
|
131
189
|
| `memoir migrate` | Translate memory between tools via AI |
|
|
132
190
|
| `memoir snapshot` | Capture current coding session |
|
|
133
191
|
| `memoir resume` | Pick up where you left off |
|
|
192
|
+
| `memoir encrypt` | Toggle E2E encryption |
|
|
134
193
|
| `memoir profile` | Manage profiles (personal/work) |
|
|
135
194
|
| `memoir cloud push` | Back up to memoir cloud |
|
|
136
195
|
| `memoir cloud restore` | Restore from memoir cloud |
|
|
@@ -140,57 +199,65 @@ memoir **never** syncs credentials, API keys, .env files, or auth tokens.
|
|
|
140
199
|
|
|
141
200
|
## How memoir compares
|
|
142
201
|
|
|
143
|
-
| Feature | memoir |
|
|
144
|
-
|
|
145
|
-
|
|
|
202
|
+
| Feature | memoir | dotfiles managers | ai-rulez | memories.sh |
|
|
203
|
+
|---------|--------|-------------------|----------|-------------|
|
|
204
|
+
| AI memory sync | **11 tools** | No | 18 tools | 3 tools |
|
|
205
|
+
| Workspace sync | **Yes** | No | No | No |
|
|
206
|
+
| Session handoff | **Yes** | No | No | No |
|
|
146
207
|
| AI-powered translation | **Yes** | No | No | No |
|
|
147
|
-
|
|
|
208
|
+
| E2E encryption | **Yes** | No | No | No |
|
|
209
|
+
| Secret scanning | **Yes** | Some | No | No |
|
|
210
|
+
| Cross-platform remap | **Yes** | Some | No | No |
|
|
211
|
+
| Uncommitted work patches | **Yes** | No | No | No |
|
|
148
212
|
| Cloud backup | **Yes** | No | No | Yes ($15/mo) |
|
|
149
|
-
| Version history | **Yes** | No | No | No |
|
|
150
|
-
| Session handoff | **Yes** | No | No | No |
|
|
151
213
|
| Profiles | **Yes** | No | No | No |
|
|
152
|
-
| Secret scanning | **Yes** | Yes | No | No |
|
|
153
214
|
| Free & open source | **Yes** | Yes | Yes | No |
|
|
154
215
|
|
|
155
|
-
memoir is free and open source. Cloud sync starts at $0 (3 backups free).
|
|
156
|
-
|
|
157
216
|
## Common Workflows
|
|
158
217
|
|
|
159
|
-
### New
|
|
218
|
+
### New machine setup
|
|
160
219
|
```bash
|
|
161
220
|
# Old machine
|
|
162
221
|
memoir push
|
|
163
222
|
|
|
164
|
-
# New machine
|
|
165
|
-
npm install -g memoir-cli
|
|
166
|
-
memoir init # connect to same repo
|
|
167
|
-
memoir restore # all configs restored in seconds
|
|
223
|
+
# New machine — one command, everything's back
|
|
224
|
+
npm install -g memoir-cli && memoir init && memoir restore -y
|
|
168
225
|
```
|
|
169
226
|
|
|
170
|
-
###
|
|
227
|
+
### Daily sync between machines
|
|
171
228
|
```bash
|
|
172
|
-
memoir
|
|
173
|
-
#
|
|
229
|
+
memoir push # end of day on laptop
|
|
230
|
+
memoir restore # next morning on desktop — AI knows what you were doing
|
|
174
231
|
```
|
|
175
232
|
|
|
176
|
-
###
|
|
233
|
+
### Switching AI tools
|
|
177
234
|
```bash
|
|
178
|
-
memoir
|
|
179
|
-
|
|
235
|
+
memoir migrate --from chatgpt --to claude
|
|
236
|
+
# Your custom instructions become a proper CLAUDE.md
|
|
180
237
|
```
|
|
181
238
|
|
|
182
|
-
###
|
|
239
|
+
### Team onboarding
|
|
183
240
|
```bash
|
|
184
|
-
#
|
|
185
|
-
memoir push
|
|
241
|
+
# Senior dev pushes team config
|
|
242
|
+
memoir push --profile team
|
|
186
243
|
|
|
187
|
-
#
|
|
188
|
-
memoir restore
|
|
244
|
+
# New hire runs one command
|
|
245
|
+
memoir restore --profile team
|
|
246
|
+
# Every project cloned. Every AI tool configured. Day one productive.
|
|
189
247
|
```
|
|
190
248
|
|
|
249
|
+
## Security
|
|
250
|
+
|
|
251
|
+
- **E2E encryption** — AES-256-GCM with scrypt key derivation
|
|
252
|
+
- **Secret scanning** — 20+ patterns detect API keys, tokens, passwords, connection strings
|
|
253
|
+
- **Auto-redaction** — secrets stripped from session handoffs before sync
|
|
254
|
+
- **No credentials synced** — .env files, auth tokens, and API keys are never included
|
|
255
|
+
- **Passphrase verified** — wrong passphrase caught before decrypt attempt
|
|
256
|
+
|
|
191
257
|
## Requirements
|
|
192
258
|
|
|
193
259
|
- Node.js >= 18
|
|
260
|
+
- Git (for workspace sync)
|
|
194
261
|
- Works on macOS, Windows, Linux
|
|
195
262
|
|
|
196
263
|
## Contributing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
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",
|
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);
|
|
@@ -226,6 +235,9 @@ export async function pushCommand(options = {}) {
|
|
|
226
235
|
if (sessionInfo.duration) parts.push(sessionInfo.duration);
|
|
227
236
|
if (sessionInfo.filesModified) parts.push(`${sessionInfo.filesModified} files changed`);
|
|
228
237
|
contextLine = '\n' + chalk.green(' ✔ Session Context') + chalk.gray(` (${parts.join(', ')})`) + '\n';
|
|
238
|
+
if (sessionInfo.decisions > 0) {
|
|
239
|
+
contextLine += chalk.green(` ✔ ${sessionInfo.decisions} decision(s) saved to persistent memory`) + '\n';
|
|
240
|
+
}
|
|
229
241
|
if (sessionInfo.secretsRedacted > 0) {
|
|
230
242
|
contextLine += chalk.yellow(` 🔒 ${sessionInfo.secretsRedacted} secret(s) auto-redacted`) + '\n';
|
|
231
243
|
}
|
package/src/context/capture.js
CHANGED
|
@@ -61,6 +61,7 @@ export function parseSession(sessionPath, maxSizeMB = 10) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function parseLines(lines) {
|
|
64
|
+
const assistantTexts = [];
|
|
64
65
|
const result = {
|
|
65
66
|
sessionId: null,
|
|
66
67
|
slug: null,
|
|
@@ -95,9 +96,14 @@ function parseLines(lines) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
// Tool uses from assistant
|
|
99
|
+
// Tool uses and text from assistant
|
|
99
100
|
if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
|
|
100
101
|
for (const block of obj.message.content) {
|
|
102
|
+
if (block.type === 'text' && block.text) {
|
|
103
|
+
// Capture assistant text for decision extraction (limit size)
|
|
104
|
+
if (block.text.length < 2000) assistantTexts.push(block.text);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
101
107
|
if (block.type !== 'tool_use') continue;
|
|
102
108
|
const name = block.name;
|
|
103
109
|
const input = block.input || {};
|
|
@@ -139,9 +145,148 @@ function parseLines(lines) {
|
|
|
139
145
|
result.filesRead = [...result.filesRead];
|
|
140
146
|
result.errors = [...new Set(result.errors)].slice(0, 10);
|
|
141
147
|
|
|
148
|
+
// Extract decisions from user + assistant messages
|
|
149
|
+
result.decisions = extractDecisions(result.userMessages, assistantTexts);
|
|
150
|
+
|
|
142
151
|
return result;
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Extract durable decisions from session conversation.
|
|
156
|
+
* These are things like renames, tech choices, preferences — stuff that should persist.
|
|
157
|
+
*/
|
|
158
|
+
function extractDecisions(userMessages, assistantTexts) {
|
|
159
|
+
const decisions = [];
|
|
160
|
+
const allText = [...userMessages, ...assistantTexts].join('\n');
|
|
161
|
+
|
|
162
|
+
// Patterns that indicate a decision was made
|
|
163
|
+
const patterns = [
|
|
164
|
+
// Renames / naming
|
|
165
|
+
{ regex: /(?:rename|call|name)\s+(?:it|this|the (?:project|app|tool|product))\s+(?:to\s+)?["']?([A-Z][a-zA-Z0-9_-]+)["']?/gi, type: 'rename' },
|
|
166
|
+
{ regex: /(?:the\s+)?(?:new\s+)?name\s+(?:is|will be|should be)\s+["']?([A-Z][a-zA-Z0-9_-]+)["']?/gi, type: 'rename' },
|
|
167
|
+
{ regex: /(?:rebrand|rebranding)\s+(?:to|as)\s+["']?([A-Z][a-zA-Z0-9_-]+)["']?/gi, type: 'rename' },
|
|
168
|
+
// Tech choices
|
|
169
|
+
{ regex: /(?:let'?s|we(?:'ll| will| should)?|going to|decided to)\s+use\s+([A-Z][a-zA-Z0-9_./-]+)\s+(?:for|instead|as|to)/gi, type: 'tech' },
|
|
170
|
+
{ regex: /(?:switch|migrate|move)\s+(?:from\s+\S+\s+)?to\s+([A-Z][a-zA-Z0-9_./-]+)/gi, type: 'tech' },
|
|
171
|
+
// Architecture / design
|
|
172
|
+
{ regex: /(?:let'?s|we(?:'ll| will| should)?)\s+(?:go with|pick|choose)\s+(.{5,60}?)(?:\.|$|,|\n)/gi, type: 'design' },
|
|
173
|
+
// Stack choices
|
|
174
|
+
{ regex: /(?:stack|framework|database|backend|frontend)\s+(?:is|will be|should be)\s+(.{5,60}?)(?:\.|$|,|\n)/gi, type: 'stack' },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const { regex, type } of patterns) {
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = regex.exec(allText)) !== null) {
|
|
180
|
+
const value = match[1].trim().replace(/["']+$/, '');
|
|
181
|
+
if (value.length > 2 && value.length < 80) {
|
|
182
|
+
// Avoid duplicates
|
|
183
|
+
const existing = decisions.find(d => d.value.toLowerCase() === value.toLowerCase());
|
|
184
|
+
if (!existing) {
|
|
185
|
+
decisions.push({ type, value, context: match[0].trim().slice(0, 120) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Look for explicit "remember this" instructions from the user
|
|
192
|
+
for (const msg of userMessages) {
|
|
193
|
+
// Only match when user is clearly asking to remember something
|
|
194
|
+
const rememberMatch = msg.match(/(?:remember (?:that|this)|note that|keep in mind that|from now on)[:\s]+(.{10,150})/i);
|
|
195
|
+
if (rememberMatch) {
|
|
196
|
+
decisions.push({ type: 'user-note', value: rememberMatch[1].trim(), context: msg.slice(0, 120) });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return decisions.slice(0, 20); // Cap at 20 decisions per session
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Write extracted decisions to Claude's persistent memory.
|
|
205
|
+
* This ensures decisions survive across sessions and machines.
|
|
206
|
+
*/
|
|
207
|
+
export function persistDecisions(decisions, claudeSource) {
|
|
208
|
+
if (!decisions || decisions.length === 0) return 0;
|
|
209
|
+
|
|
210
|
+
const claudeDir = claudeSource || path.join(home, '.claude');
|
|
211
|
+
const projectsDir = path.join(claudeDir, 'projects');
|
|
212
|
+
if (!fs.existsSync(projectsDir)) return 0;
|
|
213
|
+
|
|
214
|
+
// Find the HOME-level memory dir (not project-specific)
|
|
215
|
+
// This is the dir that matches the user's home path encoding
|
|
216
|
+
let homeKey;
|
|
217
|
+
if (process.platform === 'win32') {
|
|
218
|
+
homeKey = home.replace(/\\/g, '-').replace(/:/g, '-');
|
|
219
|
+
} else {
|
|
220
|
+
homeKey = '-' + home.replace(/^\//, '').replace(/\//g, '-');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Try exact match first, then detect from existing dirs
|
|
224
|
+
let memDir = path.join(projectsDir, homeKey, 'memory');
|
|
225
|
+
if (!fs.existsSync(memDir)) {
|
|
226
|
+
// Fallback: find dirs with memory/ subfolder, pick shortest name (likely home-level)
|
|
227
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true })
|
|
228
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(projectsDir, e.name, 'memory')));
|
|
229
|
+
if (entries.length === 0) return 0;
|
|
230
|
+
// Shortest dir name is most likely the home key (not a sub-project)
|
|
231
|
+
const homeEntry = entries.sort((a, b) => a.name.length - b.name.length)[0];
|
|
232
|
+
memDir = path.join(projectsDir, homeEntry.name, 'memory');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
236
|
+
const decisionsFile = path.join(memDir, 'session-decisions.md');
|
|
237
|
+
const memoryMdPath = path.join(memDir, 'MEMORY.md');
|
|
238
|
+
|
|
239
|
+
// Read existing decisions file or create new
|
|
240
|
+
let existing = '';
|
|
241
|
+
if (fs.existsSync(decisionsFile)) {
|
|
242
|
+
existing = fs.readFileSync(decisionsFile, 'utf8');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Format new decisions
|
|
246
|
+
const date = new Date().toISOString().split('T')[0];
|
|
247
|
+
const newEntries = decisions.map(d => {
|
|
248
|
+
if (d.type === 'rename') return `- **Renamed:** ${d.context}`;
|
|
249
|
+
if (d.type === 'tech') return `- **Tech choice:** ${d.context}`;
|
|
250
|
+
if (d.type === 'design') return `- **Decision:** ${d.context}`;
|
|
251
|
+
if (d.type === 'stack') return `- **Stack:** ${d.context}`;
|
|
252
|
+
if (d.type === 'user-note') return `- **Note:** ${d.value}`;
|
|
253
|
+
return `- ${d.context}`;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Check for duplicates against existing content
|
|
257
|
+
const fresh = newEntries.filter(entry => !existing.includes(entry));
|
|
258
|
+
if (fresh.length === 0) return 0;
|
|
259
|
+
|
|
260
|
+
const section = `\n### ${date}\n${fresh.join('\n')}\n`;
|
|
261
|
+
|
|
262
|
+
if (!existing) {
|
|
263
|
+
// Create new file with frontmatter
|
|
264
|
+
const content = `---
|
|
265
|
+
name: Session Decisions
|
|
266
|
+
description: Project decisions extracted from coding sessions — renames, tech choices, architecture
|
|
267
|
+
type: project
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
# Decisions from coding sessions
|
|
271
|
+
${section}`;
|
|
272
|
+
fs.writeFileSync(decisionsFile, content);
|
|
273
|
+
} else {
|
|
274
|
+
// Append to existing
|
|
275
|
+
fs.writeFileSync(decisionsFile, existing.trimEnd() + '\n' + section);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Ensure MEMORY.md references the decisions file
|
|
279
|
+
if (fs.existsSync(memoryMdPath)) {
|
|
280
|
+
const memoryMd = fs.readFileSync(memoryMdPath, 'utf8');
|
|
281
|
+
if (!memoryMd.includes('session-decisions.md')) {
|
|
282
|
+
const addition = `\n- [Session Decisions](session-decisions.md) — project renames, tech choices, architecture decisions from coding sessions\n`;
|
|
283
|
+
fs.writeFileSync(memoryMdPath, memoryMd.trimEnd() + addition);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return fresh.length;
|
|
288
|
+
}
|
|
289
|
+
|
|
145
290
|
/**
|
|
146
291
|
* Generate a concise handoff markdown from parsed session
|
|
147
292
|
* This is what gets injected into the AI tool on the other machine
|