memoir-cli 3.6.1 → 3.8.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/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  npx memoir-cli
16
16
  ```
17
17
 
18
- One command. No install, no config, no API keys. Your AI now has persistent memory across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, and 8 more tools.
18
+ One command. No install, no config, no API keys. Your AI now has persistent memory across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, and 6 more tools.
19
19
 
20
20
  ---
21
21
 
@@ -44,7 +44,7 @@ npx memoir-cli
44
44
 
45
45
  That's it. memoir detects your AI tools, configures MCP, and activates memory. No global install needed.
46
46
 
47
- Your AI gets 7 memory tools:
47
+ Your AI gets 14 memory tools:
48
48
 
49
49
  | MCP Tool | What it does |
50
50
  |----------|-------------|
@@ -55,6 +55,13 @@ Your AI gets 7 memory tools:
55
55
  | `memoir_consolidate` | Analyze memories for duplicates, staleness, and bloat |
56
56
  | `memoir_status` | See which AI tools are detected |
57
57
  | `memoir_profiles` | Switch between work/personal |
58
+ | `memoir_set_goal` | Set the current session goal (pinned into CLAUDE.md) |
59
+ | `memoir_add_next` | Add a next action to the current session |
60
+ | `memoir_complete_next` | Mark a next action as done |
61
+ | `memoir_note` | Record a decision with its rationale |
62
+ | `memoir_ask` | Capture an open question for later |
63
+ | `memoir_session` | Show goals, next actions, decisions, and recent sessions |
64
+ | `memoir_why` | Look up why a past decision was made |
58
65
 
59
66
  ## Why memoir
60
67
 
@@ -62,7 +69,7 @@ Your AI forgets everything between sessions. You re-explain your codebase, your
62
69
 
63
70
  memoir fixes this by giving your AI a shared memory layer that works across **every tool you use**. Tell Claude something once. Cursor knows it too. Sync AI memory between tools, back it up to the cloud, restore it on any machine. And when your memories pile up, `memoir consolidate` cleans house — finds duplicates, flags stale context, and optionally uses AI to merge and prune.
64
71
 
65
- **13 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev, Augment, Trae.
72
+ **11 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev.
66
73
 
67
74
  ## Sync across machines
68
75
 
package/bin/memoir.js CHANGED
@@ -22,6 +22,19 @@ import { projectsListCommand, projectsTodoCommand } from '../src/commands/projec
22
22
  import { upgradeCommand } from '../src/commands/upgrade.js';
23
23
  import { activateCommand, deactivateCommand } from '../src/commands/activate.js';
24
24
  import { consolidateCommand } from '../src/commands/consolidate.js';
25
+ import {
26
+ goalCommand,
27
+ nextCommand,
28
+ doneCommand,
29
+ noteCommand,
30
+ askCommand,
31
+ sessionShowCommand,
32
+ sessionClearCommand,
33
+ } from '../src/commands/session.js';
34
+ import { autopushCommand } from '../src/commands/autopush.js';
35
+ import { whyCommand } from '../src/commands/why.js';
36
+ import { autoRefreshCommand } from '../src/commands/auto-refresh.js';
37
+ import { hooksInstallCommand, hooksUninstallCommand, hooksStatusCommand } from '../src/commands/hooks.js';
25
38
  import { createRequire } from 'module';
26
39
 
27
40
  const require = createRequire(import.meta.url);
@@ -99,6 +112,7 @@ program
99
112
  .description('Back up your AI memory to the cloud')
100
113
  .option('--only <tools>', 'Only sync specific tools (comma-separated)')
101
114
  .option('-p, --profile <name>', 'Use a specific profile')
115
+ .option('--redact', 'Strip detected secrets from synced files before they are backed up')
102
116
  .action(async (options) => {
103
117
  try {
104
118
  await pushCommand(options);
@@ -153,6 +167,106 @@ program
153
167
  }
154
168
  });
155
169
 
170
+ // ── Session continuity ──────────────────────────────────────────
171
+ program
172
+ .command('goal <text...>')
173
+ .description('Set your current goal (pinned into CLAUDE.md, syncs across machines)')
174
+ .action(async (text) => {
175
+ try { await goalCommand(text.join(' ')); }
176
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
177
+ });
178
+
179
+ program
180
+ .command('next <text...>')
181
+ .description('Add a next action')
182
+ .action(async (text) => {
183
+ try { await nextCommand(text.join(' ')); }
184
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
185
+ });
186
+
187
+ program
188
+ .command('done <text...>')
189
+ .description('Mark a next action complete (substring match)')
190
+ .action(async (text) => {
191
+ try { await doneCommand(text.join(' ')); }
192
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
193
+ });
194
+
195
+ program
196
+ .command('note <text...>')
197
+ .description('Record a decision with rationale (--why) and rejected alternative (--rejected)')
198
+ .option('--why <rationale>', 'Why this decision was made')
199
+ .option('--rejected <alternative>', 'The alternative you considered and rejected')
200
+ .action(async (text, options) => {
201
+ try { await noteCommand(text.join(' '), options); }
202
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
203
+ });
204
+
205
+ program
206
+ .command('ask <text...>')
207
+ .description('Capture an open question for later')
208
+ .action(async (text) => {
209
+ try { await askCommand(text.join(' ')); }
210
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
211
+ });
212
+
213
+ program
214
+ .command('why [query...]')
215
+ .description('Look up decisions by keyword — returns what was decided + why + what was rejected')
216
+ .action(async (query) => {
217
+ try { await whyCommand((query || []).join(' ')); }
218
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
219
+ });
220
+
221
+ program
222
+ .command('session')
223
+ .description('Show the current session state')
224
+ .action(async () => {
225
+ try { await sessionShowCommand(); }
226
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
227
+ });
228
+
229
+ program
230
+ .command('session-clear')
231
+ .description('Clear the current session (history retained)')
232
+ .action(async () => {
233
+ try { await sessionClearCommand(); }
234
+ catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
235
+ });
236
+
237
+ // ── Hooks + auto-sync (called by Claude Code hooks or the user) ─────
238
+ program
239
+ .command('autopush')
240
+ .description('Debounced auto-push — called by the Claude Code Stop hook')
241
+ .option('--debounce <seconds>', 'Minimum seconds between auto-pushes', '30')
242
+ .option('-v, --verbose', 'Print debounce state')
243
+ .action(async (options) => {
244
+ try { await autopushCommand(options); }
245
+ catch (err) { if (options.verbose) console.error(chalk.red(err.message)); /* silent by default */ }
246
+ });
247
+
248
+ program
249
+ .command('auto-refresh')
250
+ .description('Re-render the pinned session block — called by the SessionStart hook')
251
+ .option('-v, --verbose', 'Print what changed')
252
+ .action(async (options) => {
253
+ try { await autoRefreshCommand(options); }
254
+ catch (err) { if (options.verbose) console.error(chalk.red(err.message)); }
255
+ });
256
+
257
+ program
258
+ .command('hooks')
259
+ .description('Manage Claude Code hooks (install | uninstall | status)')
260
+ .argument('[subcommand]', 'install, uninstall, or status', 'status')
261
+ .option('-y, --yes', 'Skip confirmation prompts')
262
+ .action(async (subcommand, options) => {
263
+ try {
264
+ if (subcommand === 'install') await hooksInstallCommand(options);
265
+ else if (subcommand === 'uninstall') await hooksUninstallCommand(options);
266
+ else await hooksStatusCommand();
267
+ } catch (err) { console.error(chalk.red('\n✖ Error:'), err.message); process.exit(1); }
268
+ });
269
+
156
270
  program
157
271
  .command('doctor')
158
272
  .alias('diagnose')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "3.6.1",
3
+ "version": "3.8.0",
4
4
  "mcpName": "io.github.camgitt/memoir",
5
5
  "description": "MCP server that gives Claude, Cursor, and Gemini long-term memory across sessions. Your AI remembers your codebase, decisions, and preferences — across tools and machines.",
6
6
  "main": "src/index.js",
@@ -9,9 +9,18 @@
9
9
  "memoir": "bin/memoir.js",
10
10
  "memoir-mcp": "src/mcp.js"
11
11
  },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
12
21
  "repository": {
13
22
  "type": "git",
14
- "url": "https://github.com/camgitt/memoir.git"
23
+ "url": "git+https://github.com/camgitt/memoir.git"
15
24
  },
16
25
  "homepage": "https://memoir.sh",
17
26
  "bugs": {
@@ -19,8 +28,9 @@
19
28
  },
20
29
  "scripts": {
21
30
  "start": "node bin/memoir.js",
22
- "test": "node test-cross-machine.mjs && bash test-cross-machine-e2e.sh",
31
+ "test": "node run-tests.mjs",
23
32
  "test:legacy": "bash test-local.sh",
33
+ "prepublishOnly": "npm test",
24
34
  "postinstall": "node -e \"try{const c='\\x1b[36m',r='\\x1b[0m',g='\\x1b[90m';console.log('\\n '+c+'memoir'+r+' installed.\\n Run '+c+'memoir activate'+r+' in any project to give your AI long-term memory.\\n '+g+'https://memoir.sh'+r+'\\n')}catch{}\""
25
35
  },
26
36
  "keywords": [
@@ -0,0 +1,31 @@
1
+ // Auto-refresh — called by the Claude Code SessionStart hook.
2
+ // Reads current session.json (local) and re-injects the pinned block into
3
+ // CLAUDE.md. Instant, no network, no side effects beyond that file.
4
+ //
5
+ // For cross-machine pull-on-start, that's a separate concern — handled by
6
+ // periodic auto-restore or explicit `memoir restore`. This hook only ensures
7
+ // the pinned block matches the current local session state.
8
+
9
+ import { readSession } from '../session/state.js';
10
+ import { renderSession } from '../session/render.js';
11
+ import { injectInto, detectAvailableTargets } from '../session/inject.js';
12
+
13
+ export async function autoRefreshCommand(options = {}) {
14
+ const verbose = !!options.verbose;
15
+ try {
16
+ const state = await readSession();
17
+ const rendered = renderSession(state);
18
+ const targets = detectAvailableTargets();
19
+ for (const [tool, target] of Object.entries(targets)) {
20
+ try {
21
+ const res = await injectInto(target, rendered);
22
+ if (verbose) console.log(`memoir auto-refresh: ${res.replaced ? 'updated' : 'created'} ${tool} → ${res.path}`);
23
+ } catch (err) {
24
+ if (verbose) console.error(`memoir auto-refresh: ${tool} failed: ${err.message}`);
25
+ }
26
+ }
27
+ } catch (err) {
28
+ if (verbose) console.error(`memoir auto-refresh: ${err.message}`);
29
+ // Never fail the hook — session start must proceed.
30
+ }
31
+ }
@@ -0,0 +1,57 @@
1
+ // Auto-push command — called by the Claude Code Stop hook after every response.
2
+ //
3
+ // Rules of engagement:
4
+ // - Debounced: won't run more than once per DEBOUNCE_SECONDS.
5
+ // - Non-blocking: detaches a background subprocess and exits immediately
6
+ // so Claude's response pipeline is never held up.
7
+ // - Silent: no stdout unless --verbose is passed.
8
+ //
9
+ // The actual push work happens in a fully detached child — the hook process
10
+ // itself just records a timestamp and returns.
11
+
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ import os from 'os';
15
+ import { spawn } from 'child_process';
16
+
17
+ const home = os.homedir();
18
+ const STAMP_FILE = path.join(home, '.config', 'memoir', 'last-autopush.timestamp');
19
+ const DEBOUNCE_SECONDS_DEFAULT = 30;
20
+
21
+ export async function autopushCommand(options = {}) {
22
+ const debounce = parseInt(options.debounce || DEBOUNCE_SECONDS_DEFAULT, 10);
23
+ const verbose = !!options.verbose;
24
+
25
+ try {
26
+ await fs.ensureDir(path.dirname(STAMP_FILE));
27
+ } catch {}
28
+
29
+ const now = Date.now();
30
+ let last = 0;
31
+ try {
32
+ const raw = await fs.readFile(STAMP_FILE, 'utf8');
33
+ last = parseInt(raw.trim(), 10) || 0;
34
+ } catch {}
35
+
36
+ const elapsed = (now - last) / 1000;
37
+ if (last && elapsed < debounce) {
38
+ if (verbose) console.log(`memoir autopush: skipped (${Math.floor(elapsed)}s since last, debounce=${debounce}s)`);
39
+ return;
40
+ }
41
+
42
+ // Stamp BEFORE spawning so rapid repeat calls don't all race through.
43
+ try {
44
+ await fs.writeFile(STAMP_FILE, String(now));
45
+ } catch {}
46
+
47
+ // Detach a background push. Parent exits immediately so Claude isn't blocked.
48
+ const memoirBin = process.argv[1]; // path to this same memoir CLI
49
+ const child = spawn(process.execPath, [memoirBin, 'push'], {
50
+ detached: true,
51
+ stdio: verbose ? 'inherit' : 'ignore',
52
+ env: { ...process.env, MEMOIR_AUTOPUSH: '1' },
53
+ });
54
+ child.unref();
55
+
56
+ if (verbose) console.log('memoir autopush: triggered (background)');
57
+ }
@@ -8,15 +8,7 @@ import os from 'os';
8
8
  import { execSync } from 'child_process';
9
9
  import { getConfig } from '../config.js';
10
10
  import { adapters } from '../adapters/index.js';
11
-
12
- const SECRET_PATTERNS = [
13
- { pattern: /sk-[a-zA-Z0-9]{20,}/, label: 'OpenAI/Stripe secret key' },
14
- { pattern: /key-[a-zA-Z0-9]{20,}/, label: 'API key' },
15
- { pattern: /ghp_[a-zA-Z0-9]{36,}/, label: 'GitHub personal access token' },
16
- { pattern: /gho_[a-zA-Z0-9]{36,}/, label: 'GitHub OAuth token' },
17
- { pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS access key' },
18
- { pattern: /Bearer\s+[a-zA-Z0-9._\-]{20,}/, label: 'Bearer token' },
19
- ];
11
+ import { scanForSecrets as scanTextForSecrets } from '../security/scanner.js';
20
12
 
21
13
  const SENSITIVE_FILENAMES = ['.env', 'credentials', 'token.json'];
22
14
 
@@ -61,11 +53,9 @@ async function scanForSecrets(files) {
61
53
  // Skip files larger than 1MB
62
54
  if (stat.size > 1024 * 1024) continue;
63
55
  const content = await fs.readFile(filePath, 'utf-8');
64
- for (const { pattern, label } of SECRET_PATTERNS) {
65
- if (pattern.test(content)) {
66
- warnings.push({ file: filePath, reason: label });
67
- break;
68
- }
56
+ const { found } = scanTextForSecrets(content);
57
+ if (found.length > 0) {
58
+ warnings.push({ file: filePath, reason: found[0].label });
69
59
  }
70
60
  } catch {
71
61
  // Skip unreadable files
@@ -0,0 +1,171 @@
1
+ // Install / uninstall / status for Claude Code hooks.
2
+ //
3
+ // Hooks are configured in ~/.claude/settings.json under the `hooks` object:
4
+ // {
5
+ // "hooks": {
6
+ // "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "..." }] }],
7
+ // "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "..." }] }]
8
+ // }
9
+ // }
10
+ //
11
+ // We merge into any existing hooks the user has — never clobber. Our entries
12
+ // are identified by a marker in the command string so we can find and remove
13
+ // them on uninstall.
14
+
15
+ import fs from 'fs-extra';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import chalk from 'chalk';
19
+ import boxen from 'boxen';
20
+ import inquirer from 'inquirer';
21
+
22
+ const home = os.homedir();
23
+ const CLAUDE_SETTINGS = path.join(home, '.claude', 'settings.json');
24
+ const MARKER = 'memoir'; // any command containing this is ours
25
+
26
+ const OUR_HOOKS = {
27
+ Stop: {
28
+ type: 'command',
29
+ command: 'memoir autopush --debounce 30',
30
+ },
31
+ SessionStart: {
32
+ type: 'command',
33
+ command: 'memoir auto-refresh',
34
+ },
35
+ };
36
+
37
+ async function readSettings() {
38
+ if (!await fs.pathExists(CLAUDE_SETTINGS)) return {};
39
+ try {
40
+ return JSON.parse(await fs.readFile(CLAUDE_SETTINGS, 'utf8'));
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+
46
+ async function writeSettings(settings) {
47
+ await fs.ensureDir(path.dirname(CLAUDE_SETTINGS));
48
+ const tmp = `${CLAUDE_SETTINGS}.tmp-${process.pid}`;
49
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2));
50
+ await fs.move(tmp, CLAUDE_SETTINGS, { overwrite: true });
51
+ }
52
+
53
+ function findOurEntry(hookList) {
54
+ // hookList is array of { matcher, hooks: [{type, command}] }
55
+ return (hookList || []).findIndex(entry =>
56
+ (entry.hooks || []).some(h => h.type === 'command' && (h.command || '').includes(MARKER))
57
+ );
58
+ }
59
+
60
+ function ensureOurHook(settings, eventName, hook) {
61
+ settings.hooks = settings.hooks || {};
62
+ settings.hooks[eventName] = settings.hooks[eventName] || [];
63
+ const list = settings.hooks[eventName];
64
+
65
+ const existing = findOurEntry(list);
66
+ if (existing >= 0) {
67
+ // Replace (idempotent — same shape every time)
68
+ list[existing] = { matcher: '', hooks: [hook] };
69
+ } else {
70
+ list.push({ matcher: '', hooks: [hook] });
71
+ }
72
+ }
73
+
74
+ function removeOurHook(settings, eventName) {
75
+ if (!settings?.hooks?.[eventName]) return false;
76
+ const list = settings.hooks[eventName];
77
+ const idx = findOurEntry(list);
78
+ if (idx < 0) return false;
79
+ list.splice(idx, 1);
80
+ if (list.length === 0) delete settings.hooks[eventName];
81
+ return true;
82
+ }
83
+
84
+ export async function hooksInstallCommand(options = {}) {
85
+ const settings = await readSettings();
86
+ const fresh = JSON.parse(JSON.stringify(settings)); // snapshot for diff
87
+
88
+ ensureOurHook(fresh, 'Stop', OUR_HOOKS.Stop);
89
+ ensureOurHook(fresh, 'SessionStart', OUR_HOOKS.SessionStart);
90
+
91
+ const isNoop = JSON.stringify(settings) === JSON.stringify(fresh);
92
+ if (isNoop) {
93
+ console.log('\n' + chalk.green(' ✓ Hooks already installed — nothing to do.\n'));
94
+ return;
95
+ }
96
+
97
+ // Show what will change
98
+ console.log('\n' + boxen(
99
+ chalk.cyan('memoir will add these hooks to ') + chalk.white.bold('~/.claude/settings.json') + chalk.cyan(':') + '\n\n' +
100
+ chalk.gray(' Stop: ') + chalk.white(OUR_HOOKS.Stop.command) + '\n' +
101
+ chalk.gray(' fires after every response; auto-pushes (debounced 30s)') + '\n\n' +
102
+ chalk.gray(' SessionStart: ') + chalk.white(OUR_HOOKS.SessionStart.command) + '\n' +
103
+ chalk.gray(' fires at session open; refreshes pinned block from session.json') + '\n\n' +
104
+ chalk.gray(' Your existing settings (including other hooks) will be preserved.'),
105
+ { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
106
+ ) + '\n');
107
+
108
+ if (!options.yes) {
109
+ const { confirm } = await inquirer.prompt([{
110
+ type: 'confirm',
111
+ name: 'confirm',
112
+ message: 'Install hooks?',
113
+ default: true,
114
+ }]);
115
+ if (!confirm) {
116
+ console.log(chalk.yellow('\n Cancelled.\n'));
117
+ return;
118
+ }
119
+ }
120
+
121
+ await writeSettings(fresh);
122
+ console.log('\n' + chalk.green(' ✓ Hooks installed. Restart Claude Code to activate.\n'));
123
+ }
124
+
125
+ export async function hooksUninstallCommand(options = {}) {
126
+ const settings = await readSettings();
127
+ const fresh = JSON.parse(JSON.stringify(settings));
128
+ const removedStop = removeOurHook(fresh, 'Stop');
129
+ const removedStart = removeOurHook(fresh, 'SessionStart');
130
+
131
+ if (!removedStop && !removedStart) {
132
+ console.log('\n' + chalk.gray(' No memoir hooks installed.\n'));
133
+ return;
134
+ }
135
+
136
+ if (!options.yes) {
137
+ const { confirm } = await inquirer.prompt([{
138
+ type: 'confirm',
139
+ name: 'confirm',
140
+ message: 'Remove memoir hooks from ~/.claude/settings.json?',
141
+ default: true,
142
+ }]);
143
+ if (!confirm) {
144
+ console.log(chalk.yellow('\n Cancelled.\n'));
145
+ return;
146
+ }
147
+ }
148
+
149
+ await writeSettings(fresh);
150
+ console.log('\n' + chalk.green(' ✓ memoir hooks removed.\n'));
151
+ }
152
+
153
+ export async function hooksStatusCommand() {
154
+ const settings = await readSettings();
155
+ const hasStop = (settings.hooks?.Stop || []).some(entry =>
156
+ (entry.hooks || []).some(h => (h.command || '').includes(MARKER))
157
+ );
158
+ const hasStart = (settings.hooks?.SessionStart || []).some(entry =>
159
+ (entry.hooks || []).some(h => (h.command || '').includes(MARKER))
160
+ );
161
+
162
+ const mark = (b) => b ? chalk.green(' ✓ ') : chalk.gray(' ✗ ');
163
+ console.log('\n' + boxen(
164
+ chalk.cyan.bold('memoir hooks status') + '\n\n' +
165
+ mark(hasStop) + chalk.white('Stop hook') + chalk.gray(' (auto-push)') + '\n' +
166
+ mark(hasStart) + chalk.white('SessionStart hook') + chalk.gray(' (auto-refresh)') + '\n\n' +
167
+ chalk.gray(' Settings: ') + chalk.white(CLAUDE_SETTINGS) + '\n' +
168
+ chalk.gray(' Run ') + chalk.cyan('memoir hooks install') + chalk.gray(' to add missing hooks.'),
169
+ { padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
170
+ ) + '\n');
171
+ }