memento-mcp 0.3.19 → 0.4.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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.4.0] - 2026-03-22
10
+
11
+ ### Changed
12
+ - **Combined recall**: UserPromptSubmit hook now reads cached assistant message and makes one `/v1/context` call with both user + assistant context (was two separate API calls per turn).
13
+ - **Stop hook replaced**: `memento-stop-recall.sh` (blocking API call with `decision: 'block'`) replaced by `memento-stop-cache.sh` (fast file write, no API call, no blocking).
14
+ - Default recall limit increased from 5 to 10.
15
+ - Config key `stop-recall` renamed to `stop-cache` in `.memento.json`.
16
+
17
+ ### Migration
18
+ - Run `npx memento-mcp update` to migrate existing installations automatically. The update command removes old stop-recall hooks from agent settings, registers the new stop-cache hook, renames the config key, and deletes the orphaned script.
19
+
20
+ ### Removed
21
+ - `memento-stop-recall.sh` — no longer distributed.
22
+
23
+ ---
24
+
9
25
  ## [0.3.15] - 2026-03-13
10
26
 
11
27
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memento-mcp",
3
- "version": "0.3.19",
3
+ "version": "0.4.0",
4
4
  "mcpName": "io.github.myrakrusemark/memento-protocol",
5
5
  "description": "The Memento Protocol — persistent memory for AI agents",
6
6
  "type": "module",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "src/",
30
30
  "scripts/",
31
+ "templates/",
31
32
  "docs/",
32
33
  "LICENSE",
33
34
  "README.md",
package/scripts/README.md CHANGED
@@ -25,8 +25,8 @@ Memento hooks work with three CLI agents. Each has a different hook system, but
25
25
  | Script | What it does |
26
26
  |--------|-------------|
27
27
  | `memento-sessionstart-identity.sh` | Injects identity crystal + version check at session start |
28
- | `memento-userprompt-recall.sh` | Recalls memories relevant to the user's message |
29
- | `memento-stop-recall.sh` | Recalls memories from the assistant's own output |
28
+ | `memento-userprompt-recall.sh` | Recalls memories from user message + cached assistant context |
29
+ | `memento-stop-cache.sh` | Caches the assistant's last message for combined recall |
30
30
  | `memento-precompact-distill.sh` | Extracts memories from the conversation before context compression |
31
31
  | `memento-codex-notify.sh` | Stores post-turn summaries from Codex CLI as memory observations |
32
32
 
@@ -105,8 +105,8 @@ Add to `.claude/settings.local.json` (project-level) or `~/.claude/settings.json
105
105
  {
106
106
  "hooks": [{
107
107
  "type": "command",
108
- "command": "bash .memento/scripts/memento-stop-recall.sh",
109
- "timeout": 5000
108
+ "command": "bash .memento/scripts/memento-stop-cache.sh",
109
+ "timeout": 2000
110
110
  }]
111
111
  }
112
112
  ],
@@ -152,8 +152,8 @@ Add to `.gemini/settings.json`:
152
152
  {
153
153
  "hooks": [{
154
154
  "type": "command",
155
- "command": "bash .memento/scripts/memento-stop-recall.sh",
156
- "timeout": 5000
155
+ "command": "bash .memento/scripts/memento-stop-cache.sh",
156
+ "timeout": 2000
157
157
  }]
158
158
  }
159
159
  ],
@@ -203,19 +203,20 @@ Fires before every agent response. Sends the user's message to `/v1/context`, wh
203
203
 
204
204
  **Output format:** JSON with `systemMessage` (user display) + `hookSpecificOutput.additionalContext` (model context).
205
205
 
206
- ### `memento-stop-recall.sh` — Stop / SessionEnd
206
+ ### `memento-stop-cache.sh` — Stop / SessionEnd
207
207
 
208
- Fires after every assistant response. Uses the assistant's own output as the recall query so memories surface during autonomous work, not just on user messages.
208
+ Lightweight caching shim that fires after every assistant response. Writes the assistant's last message to a temp file (`/tmp/memento-last-assistant-{workspace}`) so the UserPromptSubmit hook can build a combined recall query from both messages.
209
209
 
210
- - **Timeout:** 5 seconds
211
- - **User sees:** "Autonomous Recall: N memories"
212
- - **Model sees:** Full memory content via the `decision: "block"` mechanism the `reason` field becomes the model's next instruction
213
- - **Loop prevention:** Checks `stop_hook_active` flag to prevent infinite recall loops
210
+ - **Timeout:** 2 seconds
211
+ - **No API calls, no blocking, no output**
212
+ - **Loop prevention:** Checks `stop_hook_active` flag (preserved for safety)
214
213
  - **Empty responses:** Skipped when the assistant message is empty
214
+ - **Cache format:** Line 1 = epoch timestamp, lines 2+ = raw message text
215
+ - **Cache TTL:** 10 minutes (the userprompt hook ignores stale cache files)
215
216
 
216
- **Output format:** JSON with `decision: "block"`, `reason` (model context), and `systemMessage` (user display). The block mechanism is the only way to inject content into model context from a Stop hook `additionalContext` is not supported for Stop events.
217
+ **Output format:** Noneexits cleanly with no stdout.
217
218
 
218
- **Why this matters:** Without the Stop hook, memories only surface when a human sends a message. For autonomous agents that work independently running ping routines, doing research, monitoring news their own memories never get recalled. The Stop hook closes that gap.
219
+ **How it works with userprompt-recall:** The UserPromptSubmit hook reads the cached assistant message, combines it with the current user message (`{user:500} --- {assistant:300}`), and makes a single `/v1/context` call. This replaces the old two-API-call pattern where stop-recall and userprompt-recall each made independent calls.
219
220
 
220
221
  ### `memento-precompact-distill.sh` — PreCompact / PreCompress
221
222
 
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # Memento stop-cache — lightweight shim that caches the assistant's last message.
3
+ # The UserPromptSubmit hook reads this cache to build a combined recall query.
4
+ # No API calls, no blocking — just a fast file write.
5
+
6
+ INPUT=$(cat)
7
+
8
+ # Prevent infinite loops (preserved from stop-recall for safety)
9
+ STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
10
+ if [ "$STOP_ACTIVE" = "true" ]; then
11
+ exit 0
12
+ fi
13
+
14
+ ASSISTANT_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null)
15
+ if [ -z "$ASSISTANT_MSG" ]; then
16
+ exit 0
17
+ fi
18
+
19
+ # --- Resolve workspace name from .memento.json or env ---
20
+ MEMENTO_WS="${MEMENTO_WORKSPACE:-}"
21
+ if [ -z "$MEMENTO_WS" ]; then
22
+ MEMENTO_WS=$(python3 -c "
23
+ import json, os
24
+ d = os.getcwd()
25
+ while True:
26
+ p = os.path.join(d, '.memento.json')
27
+ if os.path.isfile(p):
28
+ with open(p) as f:
29
+ print(json.load(f).get('workspace', 'default'))
30
+ break
31
+ parent = os.path.dirname(d)
32
+ if parent == d:
33
+ print('default')
34
+ break
35
+ d = parent
36
+ " 2>/dev/null)
37
+ fi
38
+ MEMENTO_WS="${MEMENTO_WS:-default}"
39
+
40
+ # Write cache: epoch timestamp on line 1, message on lines 2+
41
+ CACHE_FILE="/tmp/memento-last-assistant-${MEMENTO_WS}"
42
+ echo "$(date +%s)" > "$CACHE_FILE"
43
+ echo "$ASSISTANT_MSG" >> "$CACHE_FILE"
44
+
45
+ exit 0
@@ -69,7 +69,25 @@ if [ -z "$USER_MESSAGE" ] || [ ${#USER_MESSAGE} -lt 10 ]; then
69
69
  exit 0
70
70
  fi
71
71
 
72
- QUERY="${USER_MESSAGE:0:500}"
72
+ # --- Read cached assistant message (written by stop-cache hook) ---
73
+ CACHE_FILE="/tmp/memento-last-assistant-${MEMENTO_WS}"
74
+ CACHED_ASSISTANT=""
75
+ if [ -f "$CACHE_FILE" ]; then
76
+ CACHE_TS=$(head -1 "$CACHE_FILE")
77
+ NOW=$(date +%s)
78
+ AGE=$(( NOW - CACHE_TS ))
79
+ if [ "$AGE" -lt 600 ]; then
80
+ CACHED_ASSISTANT=$(tail -n +2 "$CACHE_FILE")
81
+ fi
82
+ rm -f "$CACHE_FILE"
83
+ fi
84
+
85
+ # Build query: user message + cached assistant context (if available)
86
+ if [ -n "$CACHED_ASSISTANT" ]; then
87
+ QUERY="${USER_MESSAGE:0:500} --- ${CACHED_ASSISTANT:0:300}"
88
+ else
89
+ QUERY="${USER_MESSAGE:0:500}"
90
+ fi
73
91
 
74
92
  # --- Image detection ---
75
93
  # Collect up to 3 images from two sources:
@@ -202,7 +220,7 @@ try:
202
220
 
203
221
  memories = data.get('memories', {}).get('matches', [])
204
222
  if memories:
205
- for m in memories[:${RECALL_LIMIT:-7}]:
223
+ for m in memories[:${RECALL_LIMIT:-10}]:
206
224
  content = m['content']
207
225
  t = abbrev.get(m['type'], m['type'])
208
226
  lines.append(f' 🔹 {content} [{m[\"id\"]} {t}]')
package/src/cli.js CHANGED
@@ -97,7 +97,7 @@ function httpsPost(url, body) {
97
97
  // ---------------------------------------------------------------------------
98
98
 
99
99
  function parseFlags(argv) {
100
- const flags = { nonInteractive: false, apiKey: null, agent: null };
100
+ const flags = { nonInteractive: false, apiKey: null, agent: null, provision: false };
101
101
  for (let i = 0; i < argv.length; i++) {
102
102
  if (argv[i] === "-y" || argv[i] === "--yes") {
103
103
  flags.nonInteractive = true;
@@ -107,6 +107,8 @@ function parseFlags(argv) {
107
107
  } else if (argv[i] === "--agent" && argv[i + 1]) {
108
108
  flags.agent = argv[i + 1];
109
109
  i++;
110
+ } else if (argv[i] === "--provision") {
111
+ flags.provision = true;
110
112
  }
111
113
  }
112
114
  // Also check environment variable
@@ -153,6 +155,24 @@ function ensureHook(settings, eventName, command, timeout) {
153
155
  return true;
154
156
  }
155
157
 
158
+ /**
159
+ * Remove a hook entry whose command contains the given substring.
160
+ * Returns true if something was removed.
161
+ */
162
+ function removeHook(settings, eventName, commandSubstring) {
163
+ const existing = settings.hooks?.[eventName] || [];
164
+ const filtered = existing.filter(
165
+ (entry) => !entry.hooks?.some((h) => h.command?.includes(commandSubstring))
166
+ );
167
+ if (filtered.length === existing.length) return false;
168
+ if (filtered.length > 0) {
169
+ settings.hooks[eventName] = filtered;
170
+ } else {
171
+ delete settings.hooks[eventName];
172
+ }
173
+ return true;
174
+ }
175
+
156
176
  function appendToGitignore(cwd, line) {
157
177
  const gitignorePath = path.join(cwd, ".gitignore");
158
178
  if (fs.existsSync(gitignorePath)) {
@@ -224,7 +244,9 @@ export { AGENTS, writeMcpJson, writeGeminiJson };
224
244
  // ---------------------------------------------------------------------------
225
245
 
226
246
  async function runInit(flags = {}) {
227
- const { nonInteractive = false, apiKey: flagApiKey = null, agent: flagAgent = null } = flags;
247
+ const { nonInteractive = false, apiKey: flagApiKey = null, agent: flagAgent = null, provision = false } = flags;
248
+ // Re-attach provision to flags so the signup block can check it
249
+ flags.provision = provision;
228
250
  const cwd = process.cwd();
229
251
  const projectName = path.basename(cwd);
230
252
 
@@ -253,39 +275,56 @@ async function runInit(flags = {}) {
253
275
  }
254
276
  if (!apiKey) {
255
277
  if (nonInteractive) {
256
- // 5-second countdown warning, then auto-signup
257
- process.stderr.write(
258
- "\n No API key provided — a new one will be generated.\n" +
259
- " Press Ctrl+C to cancel.\n "
260
- );
261
- for (let i = 5; i > 0; i--) {
262
- process.stderr.write(`${i}... `);
263
- await new Promise((r) => setTimeout(r, 1000));
264
- }
265
- process.stderr.write("\n\n");
266
- }
267
- const email = nonInteractive ? "" : await ask(rl, "Email for account recovery (optional)");
268
- console.log("\nSigning up...");
269
- try {
270
- const body = { workspace };
271
- if (email) body.email = email;
272
- const resp = await httpsPost(`${DEFAULTS.apiUrl}/v1/auth/signup`, body);
273
- if (resp.api_key) {
274
- apiKey = resp.api_key;
275
- console.log(` API key: ${apiKey}`);
276
- } else if (resp.error) {
277
- console.error(` Signup failed: ${resp.error}`);
278
- rl?.close();
279
- process.exit(1);
278
+ // In non-interactive mode, skip signup unless --provision is passed
279
+ // The expected path for container workspaces is MEMENTO_API_KEY env var
280
+ if (!flags.provision) {
281
+ console.log("\n No API key provided (--api-key or MEMENTO_API_KEY env var).");
282
+ console.log(" Skipping signup. Pass --provision to auto-provision a new key.\n");
280
283
  } else {
281
- console.error(" Unexpected response:", JSON.stringify(resp));
284
+ // --provision explicitly passed: auto-signup
285
+ console.log("\nSigning up...");
286
+ try {
287
+ const body = { workspace };
288
+ const resp = await httpsPost(`${DEFAULTS.apiUrl}/v1/auth/signup`, body);
289
+ if (resp.api_key) {
290
+ apiKey = resp.api_key;
291
+ console.log(` API key: ${apiKey}`);
292
+ } else if (resp.error) {
293
+ console.error(` Signup failed: ${resp.error}`);
294
+ process.exit(1);
295
+ } else {
296
+ console.error(" Unexpected response:", JSON.stringify(resp));
297
+ process.exit(1);
298
+ }
299
+ } catch (err) {
300
+ console.error(` Signup failed: ${err.message}`);
301
+ process.exit(1);
302
+ }
303
+ }
304
+ } else {
305
+ const email = await ask(rl, "Email for account recovery (optional)");
306
+ console.log("\nSigning up...");
307
+ try {
308
+ const body = { workspace };
309
+ if (email) body.email = email;
310
+ const resp = await httpsPost(`${DEFAULTS.apiUrl}/v1/auth/signup`, body);
311
+ if (resp.api_key) {
312
+ apiKey = resp.api_key;
313
+ console.log(` API key: ${apiKey}`);
314
+ } else if (resp.error) {
315
+ console.error(` Signup failed: ${resp.error}`);
316
+ rl?.close();
317
+ process.exit(1);
318
+ } else {
319
+ console.error(" Unexpected response:", JSON.stringify(resp));
320
+ rl?.close();
321
+ process.exit(1);
322
+ }
323
+ } catch (err) {
324
+ console.error(` Signup failed: ${err.message}`);
282
325
  rl?.close();
283
326
  process.exit(1);
284
327
  }
285
- } catch (err) {
286
- console.error(` Signup failed: ${err.message}`);
287
- rl?.close();
288
- process.exit(1);
289
328
  }
290
329
  }
291
330
 
@@ -387,7 +426,7 @@ async function runInit(flags = {}) {
387
426
  " Prompt recall — recall on every message?",
388
427
  true,
389
428
  );
390
- enableStop = await askYesNo(rl, " Stop — autonomous recall after responses?", true);
429
+ enableStop = await askYesNo(rl, " Stop — cache assistant message for combined recall?", true);
391
430
  enablePreCompact = await askYesNo(
392
431
  rl,
393
432
  " PreCompact — distill memories before context compression?",
@@ -416,7 +455,7 @@ async function runInit(flags = {}) {
416
455
  },
417
456
  hooks: {
418
457
  "userprompt-recall": { enabled: enableUserPrompt },
419
- "stop-recall": { enabled: enableStop },
458
+ "stop-cache": { enabled: enableStop },
420
459
  "precompact-distill": { enabled: enablePreCompact },
421
460
  "sessionstart-identity": { enabled: enableSessionStart },
422
461
  },
@@ -441,7 +480,7 @@ async function runInit(flags = {}) {
441
480
  "hook-toast.sh",
442
481
  "memento-instructions.sh",
443
482
  enableUserPrompt && "memento-userprompt-recall.sh",
444
- enableStop && "memento-stop-recall.sh",
483
+ enableStop && "memento-stop-cache.sh",
445
484
  enablePreCompact && "memento-precompact-distill.sh",
446
485
  enableSessionStart && "memento-sessionstart-identity.sh",
447
486
  ].filter(Boolean);
@@ -464,7 +503,7 @@ async function runInit(flags = {}) {
464
503
  // Hook script commands (absolute paths)
465
504
  const instructionsCmd = path.join(localScriptsDir, "memento-instructions.sh");
466
505
  const recallCmd = path.join(localScriptsDir, "memento-userprompt-recall.sh");
467
- const stopCmd = path.join(localScriptsDir, "memento-stop-recall.sh");
506
+ const stopCmd = path.join(localScriptsDir, "memento-stop-cache.sh");
468
507
  const precompactCmd = path.join(localScriptsDir, "memento-precompact-distill.sh");
469
508
  const sessionStartCmd = path.join(localScriptsDir, "memento-sessionstart-identity.sh");
470
509
 
@@ -476,7 +515,7 @@ async function runInit(flags = {}) {
476
515
  // Instructions hook always registered (not gated by enableSessionStart)
477
516
  changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000) || changed;
478
517
  if (enableUserPrompt) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 5000) || changed;
479
- if (enableStop) changed = ensureHook(settings, "Stop", stopCmd, 5000) || changed;
518
+ if (enableStop) changed = ensureHook(settings, "Stop", stopCmd, 2000) || changed;
480
519
  if (enablePreCompact) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
481
520
  if (enableSessionStart) changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
482
521
  if (changed) {
@@ -493,7 +532,7 @@ async function runInit(flags = {}) {
493
532
  // Instructions hook always registered
494
533
  changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000) || changed;
495
534
  if (enableUserPrompt) changed = ensureHook(settings, "BeforeAgent", recallCmd, 5000) || changed;
496
- if (enableStop) changed = ensureHook(settings, "SessionEnd", stopCmd, 5000) || changed;
535
+ if (enableStop) changed = ensureHook(settings, "SessionEnd", stopCmd, 2000) || changed;
497
536
  if (enablePreCompact) changed = ensureHook(settings, "PreCompress", precompactCmd, 30000) || changed;
498
537
  if (enableSessionStart) changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
499
538
  if (changed) {
@@ -510,6 +549,25 @@ async function runInit(flags = {}) {
510
549
  created.push(result);
511
550
  }
512
551
 
552
+ // 9b. CLAUDE.md — append Memento portable section
553
+ const claudeMdPath = path.join(cwd, "CLAUDE.md");
554
+ const mementoTemplatePath = path.resolve(__dirname, "..", "templates", "CLAUDE-SECTION.md");
555
+ try {
556
+ const section = fs.readFileSync(mementoTemplatePath, "utf-8");
557
+ if (fs.existsSync(claudeMdPath)) {
558
+ const existing = fs.readFileSync(claudeMdPath, "utf-8");
559
+ if (existing.includes("Memento MCP")) {
560
+ console.log(" · CLAUDE.md (already has memento section)");
561
+ } else {
562
+ fs.appendFileSync(claudeMdPath, "\n" + section);
563
+ created.push("CLAUDE.md (memento section appended)");
564
+ }
565
+ } else {
566
+ fs.writeFileSync(claudeMdPath, section);
567
+ created.push("CLAUDE.md (created with memento section)");
568
+ }
569
+ } catch { /* template not found — skip silently */ }
570
+
513
571
  // 10. Add .memento.json and .memento/scripts/ to .gitignore
514
572
  let gitignoreUpdated = false;
515
573
  if (appendToGitignore(cwd, ".memento.json")) gitignoreUpdated = true;
@@ -524,6 +582,8 @@ async function runInit(flags = {}) {
524
582
  ".mcp.json": "MCP server registered (Claude Code)",
525
583
  ".gemini/settings.json": "MCP server registered (Gemini CLI)",
526
584
  ".gemini/settings.json (hooks)": "hooks registered with Gemini CLI",
585
+ "CLAUDE.md (memento section appended)": "portable Memento instructions",
586
+ "CLAUDE.md (created with memento section)": "portable Memento instructions",
527
587
  "(skipped — manual setup)": "MCP config skipped (manual setup)",
528
588
  ".gitignore (updated)": "credentials excluded from git",
529
589
  };
@@ -606,16 +666,34 @@ async function runUpdate() {
606
666
  // Hook script paths
607
667
  const instructionsCmd = path.join(localScriptsDir, "memento-instructions.sh");
608
668
  const recallCmd = path.join(localScriptsDir, "memento-userprompt-recall.sh");
609
- const stopCmd = path.join(localScriptsDir, "memento-stop-recall.sh");
669
+ const stopCmd = path.join(localScriptsDir, "memento-stop-cache.sh");
610
670
  const precompactCmd = path.join(localScriptsDir, "memento-precompact-distill.sh");
611
671
  const sessionStartCmd = path.join(localScriptsDir, "memento-sessionstart-identity.sh");
612
672
 
613
- // Hook enabled flags (default to true for recall/stop/precompact if not specified)
673
+ // Hook enabled flags check both old "stop-recall" and new "stop-cache" keys
614
674
  const enableUserPrompt = hooks["userprompt-recall"]?.enabled !== false;
615
- const enableStop = hooks["stop-recall"]?.enabled !== false;
675
+ const enableStop = (hooks["stop-cache"]?.enabled ?? hooks["stop-recall"]?.enabled) !== false;
616
676
  const enablePreCompact = hooks["precompact-distill"]?.enabled !== false;
617
677
  const enableSessionStart = hooks["sessionstart-identity"]?.enabled && features.identity;
618
678
 
679
+ // --- Migration: stop-recall → stop-cache ---
680
+ const migrations = [];
681
+
682
+ // Migrate .memento.json config key
683
+ if (hooks["stop-recall"] !== undefined) {
684
+ config.hooks["stop-cache"] = config.hooks["stop-recall"];
685
+ delete config.hooks["stop-recall"];
686
+ writeJsonFile(configPath, config);
687
+ migrations.push(".memento.json: stop-recall → stop-cache");
688
+ }
689
+
690
+ // Delete orphaned stop-recall script
691
+ const oldStopPath = path.join(localScriptsDir, "memento-stop-recall.sh");
692
+ if (fs.existsSync(oldStopPath)) {
693
+ fs.unlinkSync(oldStopPath);
694
+ migrations.push("Deleted .memento/scripts/memento-stop-recall.sh");
695
+ }
696
+
619
697
  const registeredHooks = [];
620
698
 
621
699
  // Claude Code
@@ -625,9 +703,11 @@ async function runUpdate() {
625
703
  const settingsPath = path.join(cwd, ".claude", "settings.local.json");
626
704
  const settings = readJsonFile(settingsPath) || {};
627
705
  let changed = false;
706
+ // Remove old stop-recall hook
707
+ changed = removeHook(settings, "Stop", "memento-stop-recall.sh") || changed;
628
708
  changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000) || changed;
629
709
  if (enableUserPrompt) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 5000) || changed;
630
- if (enableStop) changed = ensureHook(settings, "Stop", stopCmd, 5000) || changed;
710
+ if (enableStop) changed = ensureHook(settings, "Stop", stopCmd, 2000) || changed;
631
711
  if (enablePreCompact) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
632
712
  if (enableSessionStart) changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
633
713
  if (changed) {
@@ -643,9 +723,11 @@ async function runUpdate() {
643
723
  const settingsPath = path.join(cwd, ".gemini", "settings.json");
644
724
  const settings = readJsonFile(settingsPath) || {};
645
725
  let changed = false;
726
+ // Remove old stop-recall hook
727
+ changed = removeHook(settings, "SessionEnd", "memento-stop-recall.sh") || changed;
646
728
  changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000) || changed;
647
729
  if (enableUserPrompt) changed = ensureHook(settings, "BeforeAgent", recallCmd, 5000) || changed;
648
- if (enableStop) changed = ensureHook(settings, "SessionEnd", stopCmd, 5000) || changed;
730
+ if (enableStop) changed = ensureHook(settings, "SessionEnd", stopCmd, 2000) || changed;
649
731
  if (enablePreCompact) changed = ensureHook(settings, "PreCompress", precompactCmd, 30000) || changed;
650
732
  if (enableSessionStart) changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
651
733
  if (changed) {
@@ -655,6 +737,13 @@ async function runUpdate() {
655
737
  }
656
738
 
657
739
  console.log(`\n ✓ Memento hooks updated to v${pkgVersion}\n`);
740
+ if (migrations.length > 0) {
741
+ console.log(" Migrations:");
742
+ for (const m of migrations) {
743
+ console.log(` ↳ ${m}`);
744
+ }
745
+ console.log();
746
+ }
658
747
  console.log(" Updated scripts:");
659
748
  for (const name of updated) {
660
749
  console.log(` ${name}`);
@@ -713,6 +802,7 @@ if (isMain) {
713
802
  -y, --yes Non-interactive mode (uses defaults, for CI/scripting)
714
803
  --api-key KEY Provide API key (skips signup prompt)
715
804
  --agent AGENT Select agent: claude-code, gemini, or manual
805
+ --provision Auto-provision a new API key in non-interactive mode
716
806
 
717
807
  The -y flag enables fully non-interactive setup. Combine with --agent
718
808
  to select a specific agent (defaults to auto-detect, then claude-code).
package/src/config.js CHANGED
@@ -17,8 +17,8 @@ export const DEFAULTS = {
17
17
  agents: [],
18
18
  features: { images: false, identity: false },
19
19
  hooks: {
20
- "userprompt-recall": { enabled: true, limit: 5, maxLength: 200 },
21
- "stop-recall": { enabled: true, limit: 5, maxLength: 200 },
20
+ "userprompt-recall": { enabled: true, limit: 10, maxLength: 200 },
21
+ "stop-cache": { enabled: true },
22
22
  "precompact-distill": { enabled: true },
23
23
  "sessionstart-identity": { enabled: true },
24
24
  },
@@ -0,0 +1,23 @@
1
+ ---
2
+
3
+ # Memento MCP (`mcp__memento__*`)
4
+
5
+ **Load tools:** `ToolSearch query="+memento" max_results=20` — then READ the tool descriptions.
6
+
7
+ **Memory discipline — notes are instructions, not logs.**
8
+ Write: "Skip X until condition Y" — not "checked X, it was quiet."
9
+ Every memory must answer: could a future agent with zero context read this and know exactly what to do?
10
+
11
+ | Tool | What it does |
12
+ |------|-------------|
13
+ | `memento_health` | System health — item/memory/skip counts, last updated |
14
+ | `memento_remember` | Store a memory (fact/decision/observation/instruction) with tags + expiration |
15
+ | `memento_recall` | Search memories by keyword/tag/type — ranked by relevance |
16
+ | `memento_consolidate` | Merge 3+ overlapping memories into one sharper representation |
17
+ | `memento_skip_add` / `memento_skip_check` | Anti-memory: things to NOT investigate right now (with expiration) |
18
+ | `memento_item_create` | Create structured item (active_work/standing_decision/skip_list/waiting_for/session_note) |
19
+ | `memento_item_update` | Update item fields (status, next_action, priority, category, tags) |
20
+ | `memento_item_delete` | Delete item (prefer archiving via status=archived) |
21
+ | `memento_item_list` | List items with filters (category, status, query) |
22
+ | `memento_identity` | Read identity crystal |
23
+ | `memento_identity_update` | Write/replace identity crystal |
@@ -1,148 +0,0 @@
1
- #!/bin/bash
2
- # Memento autonomous recall — fires on Stop (after assistant response).
3
- # Uses the assistant's own output as the recall query, so memories surface
4
- # during autonomous work, not just when the user sends a message.
5
-
6
- set -o pipefail
7
-
8
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
- TOAST="$SCRIPT_DIR/hook-toast.sh"
10
-
11
- # --- Config from .memento.json (if present) ---
12
- CONFIG_JSON=$(python3 -c "
13
- import json, os
14
- d = os.getcwd()
15
- while True:
16
- p = os.path.join(d, '.memento.json')
17
- if os.path.isfile(p):
18
- with open(p) as f:
19
- print(f.read())
20
- break
21
- parent = os.path.dirname(d)
22
- if parent == d:
23
- break
24
- d = parent
25
- " 2>/dev/null)
26
-
27
- if [ -n "$CONFIG_JSON" ]; then
28
- HOOK_NAME="stop-recall"
29
- HOOK_ENABLED=$(echo "$CONFIG_JSON" | python3 -c "
30
- import json, sys
31
- cfg = json.load(sys.stdin)
32
- hook = cfg.get('hooks', {}).get('$HOOK_NAME', {})
33
- print('true' if hook.get('enabled', True) else 'false')
34
- " 2>/dev/null)
35
-
36
- if [ "$HOOK_ENABLED" = "false" ]; then
37
- exit 0
38
- fi
39
-
40
- MEMENTO_API_KEY="${MEMENTO_API_KEY:-$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('apiKey',''))" 2>/dev/null)}"
41
- MEMENTO_API_URL="${MEMENTO_API_URL:-$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('apiUrl',''))" 2>/dev/null)}"
42
- MEMENTO_WORKSPACE="${MEMENTO_WORKSPACE:-$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('workspace',''))" 2>/dev/null)}"
43
-
44
- RECALL_LIMIT=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('hooks',{}).get('$HOOK_NAME',{}).get('limit',5))" 2>/dev/null)
45
- RECALL_MAX_LENGTH=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('hooks',{}).get('$HOOK_NAME',{}).get('maxLength',120))" 2>/dev/null)
46
- fi
47
- # --- End config block ---
48
-
49
- # Source credentials from .env (gitignored) — fallback if no .memento.json
50
- if [ -f "$SCRIPT_DIR/../.env" ]; then
51
- set -a
52
- source "$SCRIPT_DIR/../.env"
53
- set +a
54
- fi
55
-
56
- MEMENTO_API="${MEMENTO_API_URL:-https://memento-api.myrakrusemark.workers.dev}"
57
- MEMENTO_KEY="${MEMENTO_API_KEY:?MEMENTO_API_KEY not set — check memento-mcp/.env or .memento.json}"
58
- MEMENTO_WS="${MEMENTO_WORKSPACE:-default}"
59
-
60
- INPUT=$(cat)
61
-
62
- # Prevent infinite loops — if this Stop was triggered by a previous Stop hook, bail
63
- STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
64
- if [ "$STOP_ACTIVE" = "true" ]; then
65
- exit 0
66
- fi
67
-
68
- # Get the assistant's last message
69
- ASSISTANT_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null)
70
-
71
- if [ -z "$ASSISTANT_MSG" ]; then
72
- exit 0
73
- fi
74
-
75
- # Truncate to first 500 chars for the query
76
- QUERY="${ASSISTANT_MSG:0:500}"
77
-
78
- # Toast: start retrieving
79
- "$TOAST" memento "⏳ Autonomous recall..." &>/dev/null
80
-
81
- # Call Memento /v1/context
82
- RESULT=$(curl -s --max-time 8 \
83
- -X POST \
84
- -H "Authorization: Bearer $MEMENTO_KEY" \
85
- -H "X-Memento-Workspace: $MEMENTO_WS" \
86
- -H "Content-Type: application/json" \
87
- -d "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"]}" \
88
- "$MEMENTO_API/v1/context" 2>/dev/null \
89
- | python3 -c "
90
- import json, sys
91
- try:
92
- data = json.load(sys.stdin)
93
- lines = []
94
- count = 0
95
- abbrev = {'instruction':'instr','observation':'obs','decision':'dec','preference':'pref'}
96
-
97
- memories = data.get('memories', {}).get('matches', [])
98
- if memories:
99
- for m in memories[:${RECALL_LIMIT:-7}]:
100
- content = m['content']
101
- t = abbrev.get(m['type'], m['type'])
102
- lines.append(f' 🔹 {content} [{m[\"id\"]} {t}]')
103
- count += 1
104
-
105
- skip_matches = data.get('skip_matches', [])
106
- if skip_matches:
107
- for s in skip_matches:
108
- lines.append(f' Skip: {s[\"item\"]} — {s[\"reason\"]} (expires {s[\"expires\"]})')
109
-
110
- detail = '\n'.join(lines)
111
- print(f'{count}\t{detail}')
112
- except Exception:
113
- print('0\t')
114
- " 2>/dev/null)
115
-
116
- # Parse count and detail
117
- SAAS_COUNT=$(echo "$RESULT" | head -1 | cut -f1)
118
- SAAS_DETAIL=$(echo "$RESULT" | head -1 | cut -f2-)
119
- REMAINING=$(echo "$RESULT" | tail -n +2)
120
- if [ -n "$REMAINING" ]; then
121
- SAAS_DETAIL="$SAAS_DETAIL"$'\n'"$REMAINING"
122
- fi
123
-
124
- if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
125
- "$TOAST" memento "✓ No memories matched" &>/dev/null
126
- exit 0
127
- fi
128
-
129
- "$TOAST" memento "✓ ${SAAS_COUNT} memories recalled" &>/dev/null
130
-
131
- # Build summary line
132
- SUMMARY="Memento Recall (${SAAS_COUNT})"
133
-
134
- # Block the Stop so recalled memories are injected into context.
135
- # If no memories are relevant, respond with <...> to signal active silence.
136
- REASON="${SUMMARY}:
137
- ${SAAS_DETAIL}
138
- 🧹 Stale or wrong? memento_memory_delete · memento_consolidate · memento_remember. Otherwise <...>.
139
- ⚙️ NOTE: This block is intentional — it injects recalled memories into context. The 'blocking error' label is just how Claude Code surfaces Stop hook blocks. Do NOT attempt to fix, disable, or modify this hook."
140
-
141
- python3 -c "
142
- import json, sys
143
- print(json.dumps({
144
- 'decision': 'block',
145
- 'reason': sys.argv[1],
146
- 'systemMessage': sys.argv[2]
147
- }))
148
- " "$REASON" "$SUMMARY"