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 +16 -0
- package/package.json +2 -1
- package/scripts/README.md +15 -14
- package/scripts/memento-stop-cache.sh +45 -0
- package/scripts/memento-userprompt-recall.sh +20 -2
- package/src/cli.js +132 -42
- package/src/config.js +2 -2
- package/templates/CLAUDE-SECTION.md +23 -0
- package/scripts/memento-stop-recall.sh +0 -148
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
|
+
"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
|
|
29
|
-
| `memento-stop-
|
|
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-
|
|
109
|
-
"timeout":
|
|
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-
|
|
156
|
-
"timeout":
|
|
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-
|
|
206
|
+
### `memento-stop-cache.sh` — Stop / SessionEnd
|
|
207
207
|
|
|
208
|
-
|
|
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:**
|
|
211
|
-
- **
|
|
212
|
-
- **
|
|
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:**
|
|
217
|
+
**Output format:** None — exits cleanly with no stdout.
|
|
217
218
|
|
|
218
|
-
**
|
|
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
|
-
|
|
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:-
|
|
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
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"
|
|
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
|
-
|
|
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 —
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|
|
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,
|
|
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-
|
|
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
|
|
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,
|
|
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,
|
|
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:
|
|
21
|
-
"stop-
|
|
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"
|