memento-mcp 0.3.4 → 0.3.5
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/package.json +1 -1
- package/scripts/memento-codex-notify.sh +79 -0
- package/src/cli.js +131 -87
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Codex CLI notify hook — post-turn memory storage to Memento.
|
|
3
|
+
# Receives JSON payload as argv[1] on agent-turn-complete events.
|
|
4
|
+
#
|
|
5
|
+
# Extracts the assistant's response and stores it as a Memento observation.
|
|
6
|
+
# Best-effort — failures are silent. This is fire-and-forget.
|
|
7
|
+
|
|
8
|
+
set -o pipefail
|
|
9
|
+
|
|
10
|
+
PAYLOAD="$1"
|
|
11
|
+
[ -z "$PAYLOAD" ] && exit 0
|
|
12
|
+
|
|
13
|
+
# Only handle agent-turn-complete events
|
|
14
|
+
EVENT_TYPE=$(echo "$PAYLOAD" | python3 -c "import json,sys; print(json.load(sys.stdin).get('type',''))" 2>/dev/null)
|
|
15
|
+
[ "$EVENT_TYPE" != "agent-turn-complete" ] && exit 0
|
|
16
|
+
|
|
17
|
+
# Find .memento.json by walking up
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
CONFIG_JSON=""
|
|
20
|
+
_d="$(pwd)"
|
|
21
|
+
while true; do
|
|
22
|
+
if [ -f "$_d/.memento.json" ]; then
|
|
23
|
+
CONFIG_JSON=$(cat "$_d/.memento.json" 2>/dev/null)
|
|
24
|
+
break
|
|
25
|
+
fi
|
|
26
|
+
_p="$(dirname "$_d")"
|
|
27
|
+
[ "$_p" = "$_d" ] && break
|
|
28
|
+
_d="$_p"
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
[ -z "$CONFIG_JSON" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Extract config
|
|
34
|
+
MEMENTO_API_KEY=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('apiKey',''))" 2>/dev/null)
|
|
35
|
+
MEMENTO_API_URL=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('apiUrl',''))" 2>/dev/null)
|
|
36
|
+
MEMENTO_WORKSPACE=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('workspace',''))" 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
MEMENTO_API="${MEMENTO_API_URL:-https://memento-api.myrakrusemark.workers.dev}"
|
|
39
|
+
[ -z "$MEMENTO_API_KEY" ] && exit 0
|
|
40
|
+
|
|
41
|
+
# Extract turn content and store to Memento API
|
|
42
|
+
python3 -c "
|
|
43
|
+
import json, sys, urllib.request
|
|
44
|
+
|
|
45
|
+
payload = json.loads(sys.argv[1])
|
|
46
|
+
api_url = sys.argv[2]
|
|
47
|
+
api_key = sys.argv[3]
|
|
48
|
+
workspace = sys.argv[4]
|
|
49
|
+
|
|
50
|
+
assistant_msg = payload.get('last-assistant-message', '')
|
|
51
|
+
|
|
52
|
+
# Only store if there's meaningful content
|
|
53
|
+
if not assistant_msg or len(assistant_msg) < 50:
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
|
|
56
|
+
# Truncate for storage
|
|
57
|
+
summary = assistant_msg[:500]
|
|
58
|
+
if len(assistant_msg) > 500:
|
|
59
|
+
summary += '...'
|
|
60
|
+
|
|
61
|
+
data = json.dumps({
|
|
62
|
+
'content': summary,
|
|
63
|
+
'type': 'observation',
|
|
64
|
+
'tags': ['codex', 'turn-summary', 'auto-capture']
|
|
65
|
+
}).encode()
|
|
66
|
+
|
|
67
|
+
req = urllib.request.Request(
|
|
68
|
+
f'{api_url}/v1/memories',
|
|
69
|
+
data=data,
|
|
70
|
+
headers={
|
|
71
|
+
'Authorization': f'Bearer {api_key}',
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'X-Memento-Workspace': workspace
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
urllib.request.urlopen(req, timeout=3)
|
|
77
|
+
" "$PAYLOAD" "$MEMENTO_API" "$MEMENTO_API_KEY" "$MEMENTO_WORKSPACE" 2>/dev/null || true
|
|
78
|
+
|
|
79
|
+
exit 0
|
package/src/cli.js
CHANGED
|
@@ -196,10 +196,23 @@ function writeJsonFile(filePath, data) {
|
|
|
196
196
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Idempotently register a hook in a settings object.
|
|
201
|
+
* Works for both Claude Code and Gemini CLI (same JSON structure).
|
|
202
|
+
* Returns true if a new hook was added, false if already present.
|
|
203
|
+
*/
|
|
204
|
+
function ensureHook(settings, eventName, command, timeout) {
|
|
205
|
+
const existing = settings.hooks?.[eventName] || [];
|
|
206
|
+
const alreadyRegistered = existing.some((entry) =>
|
|
207
|
+
entry.hooks?.some((h) => h.command === command)
|
|
208
|
+
);
|
|
209
|
+
if (alreadyRegistered) return false;
|
|
210
|
+
if (!settings.hooks) settings.hooks = {};
|
|
211
|
+
settings.hooks[eventName] = [
|
|
212
|
+
...existing,
|
|
213
|
+
{ hooks: [{ type: "command", command, timeout }] },
|
|
214
|
+
];
|
|
215
|
+
return true;
|
|
203
216
|
}
|
|
204
217
|
|
|
205
218
|
function appendToGitignore(cwd, line) {
|
|
@@ -452,13 +465,16 @@ async function runInit(flags = {}) {
|
|
|
452
465
|
|
|
453
466
|
const hasClaude = selectedAgents.includes("claude-code");
|
|
454
467
|
|
|
455
|
-
// 5. Hooks —
|
|
468
|
+
// 5. Hooks — if any hook-supporting agent is selected (Claude Code, Gemini CLI)
|
|
469
|
+
const hasGemini = selectedAgents.includes("gemini");
|
|
470
|
+
const hasHookAgent = hasClaude || hasGemini;
|
|
471
|
+
|
|
456
472
|
let enableUserPrompt = false;
|
|
457
473
|
let enableStop = false;
|
|
458
474
|
let enablePreCompact = false;
|
|
459
475
|
let enableSessionStart = false;
|
|
460
476
|
|
|
461
|
-
if (
|
|
477
|
+
if (hasHookAgent) {
|
|
462
478
|
if (nonInteractive) {
|
|
463
479
|
// All hooks on by default in non-interactive mode
|
|
464
480
|
enableUserPrompt = true;
|
|
@@ -466,10 +482,10 @@ async function runInit(flags = {}) {
|
|
|
466
482
|
enablePreCompact = true;
|
|
467
483
|
enableSessionStart = false; // identity not enabled in -y mode
|
|
468
484
|
} else {
|
|
469
|
-
console.log("\
|
|
485
|
+
console.log("\nAgent hooks (automate recall + distillation):");
|
|
470
486
|
enableUserPrompt = await askYesNo(
|
|
471
487
|
rl,
|
|
472
|
-
"
|
|
488
|
+
" Prompt recall — recall on every message?",
|
|
473
489
|
true,
|
|
474
490
|
);
|
|
475
491
|
enableStop = await askYesNo(rl, " Stop — autonomous recall after responses?", true);
|
|
@@ -514,11 +530,12 @@ async function runInit(flags = {}) {
|
|
|
514
530
|
writeJsonFile(configPath, config);
|
|
515
531
|
created.push(".memento.json");
|
|
516
532
|
|
|
517
|
-
// 7. Copy hook scripts
|
|
533
|
+
// 7. Copy hook scripts — gated on any hook-supporting agent
|
|
534
|
+
const hasCodex = selectedAgents.includes("codex");
|
|
518
535
|
const anyHookEnabled =
|
|
519
536
|
enableUserPrompt || enableStop || enablePreCompact || enableSessionStart;
|
|
520
537
|
|
|
521
|
-
if (
|
|
538
|
+
if (hasHookAgent && anyHookEnabled) {
|
|
522
539
|
const pkgScriptsDir = path.resolve(__dirname, "..", "scripts");
|
|
523
540
|
const localScriptsDir = path.join(cwd, ".memento", "scripts");
|
|
524
541
|
if (!fs.existsSync(localScriptsDir))
|
|
@@ -532,8 +549,12 @@ async function runInit(flags = {}) {
|
|
|
532
549
|
enableSessionStart && "memento-sessionstart-identity.sh",
|
|
533
550
|
].filter(Boolean);
|
|
534
551
|
|
|
552
|
+
// Also copy Codex notify script if Codex is selected
|
|
553
|
+
if (hasCodex) scriptFiles.push("memento-codex-notify.sh");
|
|
554
|
+
|
|
535
555
|
for (const name of scriptFiles) {
|
|
536
556
|
const src = path.join(pkgScriptsDir, name);
|
|
557
|
+
if (!fs.existsSync(src)) continue; // skip if script doesn't exist yet
|
|
537
558
|
const dest = path.join(localScriptsDir, name);
|
|
538
559
|
fs.copyFileSync(src, dest);
|
|
539
560
|
fs.chmodSync(dest, 0o755);
|
|
@@ -546,67 +567,65 @@ async function runInit(flags = {}) {
|
|
|
546
567
|
const versionPath = path.join(cwd, ".memento", "version");
|
|
547
568
|
fs.writeFileSync(versionPath, pkgVersion + "\n");
|
|
548
569
|
|
|
549
|
-
//
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
570
|
+
// Hook script commands (absolute paths)
|
|
571
|
+
const recallCmd = path.join(localScriptsDir, "memento-userprompt-recall.sh");
|
|
572
|
+
const stopCmd = path.join(localScriptsDir, "memento-stop-recall.sh");
|
|
573
|
+
const precompactCmd = path.join(localScriptsDir, "memento-precompact-distill.sh");
|
|
574
|
+
const sessionStartCmd = path.join(localScriptsDir, "memento-sessionstart-identity.sh");
|
|
575
|
+
|
|
576
|
+
// 8a. Claude Code hooks — .claude/settings.local.json
|
|
577
|
+
if (hasClaude) {
|
|
578
|
+
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
579
|
+
const settings = readJsonFile(settingsPath) || {};
|
|
580
|
+
let changed = false;
|
|
581
|
+
if (enableUserPrompt) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 5000) || changed;
|
|
582
|
+
if (enableStop) changed = ensureHook(settings, "Stop", stopCmd, 5000) || changed;
|
|
583
|
+
if (enablePreCompact) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
|
|
584
|
+
if (enableSessionStart) changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
585
|
+
if (changed) {
|
|
586
|
+
writeJsonFile(settingsPath, settings);
|
|
587
|
+
created.push(".claude/settings.local.json");
|
|
588
|
+
}
|
|
563
589
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
590
|
+
|
|
591
|
+
// 8b. Gemini CLI hooks — .gemini/settings.json
|
|
592
|
+
if (hasGemini) {
|
|
593
|
+
const settingsPath = path.join(cwd, ".gemini", "settings.json");
|
|
594
|
+
const settings = readJsonFile(settingsPath) || {};
|
|
595
|
+
let changed = false;
|
|
596
|
+
if (enableUserPrompt) changed = ensureHook(settings, "BeforeAgent", recallCmd, 5000) || changed;
|
|
597
|
+
if (enableStop) changed = ensureHook(settings, "SessionEnd", stopCmd, 5000) || changed;
|
|
598
|
+
if (enablePreCompact) changed = ensureHook(settings, "PreCompress", precompactCmd, 30000) || changed;
|
|
599
|
+
if (enableSessionStart) changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
600
|
+
if (changed) {
|
|
601
|
+
writeJsonFile(settingsPath, settings);
|
|
602
|
+
created.push(".gemini/settings.json (hooks)");
|
|
603
|
+
}
|
|
576
604
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 8c. Codex CLI notify — .codex/config.toml (fire-and-forget, post-turn memory storage)
|
|
608
|
+
if (hasCodex) {
|
|
609
|
+
const pkgScriptsDir = path.resolve(__dirname, "..", "scripts");
|
|
610
|
+
const localScriptsDir = path.join(cwd, ".memento", "scripts");
|
|
611
|
+
// Ensure the notify script is copied (may not have been copied above if no hook agent)
|
|
612
|
+
const notifySrc = path.join(pkgScriptsDir, "memento-codex-notify.sh");
|
|
613
|
+
const notifyDest = path.join(localScriptsDir, "memento-codex-notify.sh");
|
|
614
|
+
if (fs.existsSync(notifySrc) && !fs.existsSync(notifyDest)) {
|
|
615
|
+
fs.mkdirSync(localScriptsDir, { recursive: true });
|
|
616
|
+
fs.copyFileSync(notifySrc, notifyDest);
|
|
617
|
+
fs.chmodSync(notifyDest, 0o755);
|
|
589
618
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
),
|
|
600
|
-
timeout: 10000,
|
|
601
|
-
},
|
|
602
|
-
],
|
|
603
|
-
},
|
|
604
|
-
];
|
|
619
|
+
const notifyScript = notifyDest;
|
|
620
|
+
const codexTomlPath = path.join(cwd, ".codex", "config.toml");
|
|
621
|
+
let tomlContent = "";
|
|
622
|
+
try { tomlContent = fs.readFileSync(codexTomlPath, "utf-8"); } catch { /* doesn't exist yet */ }
|
|
623
|
+
if (!tomlContent.includes("notify")) {
|
|
624
|
+
const notifyLine = `\nnotify = ["bash", "${notifyScript}"]\n`;
|
|
625
|
+
const separator = tomlContent && !tomlContent.endsWith("\n") ? "\n" : "";
|
|
626
|
+
fs.writeFileSync(codexTomlPath, tomlContent + separator + notifyLine);
|
|
627
|
+
created.push(".codex/config.toml (notify)");
|
|
605
628
|
}
|
|
606
|
-
|
|
607
|
-
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
608
|
-
mergeJsonFile(settingsPath, { hooks });
|
|
609
|
-
created.push(".claude/settings.local.json");
|
|
610
629
|
}
|
|
611
630
|
|
|
612
631
|
// 9. Per-agent config files
|
|
@@ -746,28 +765,51 @@ async function runUpdate() {
|
|
|
746
765
|
const versionPath = path.join(cwd, ".memento", "version");
|
|
747
766
|
fs.writeFileSync(versionPath, pkgVersion + "\n");
|
|
748
767
|
|
|
749
|
-
// Ensure SessionStart hook is registered for
|
|
750
|
-
// Detect by
|
|
768
|
+
// Ensure SessionStart hook is registered for agents that support hooks
|
|
769
|
+
// Detect by config agents field or directory presence (older configs may lack agents)
|
|
751
770
|
const config = readJsonFile(configPath) || {};
|
|
752
|
-
const
|
|
771
|
+
const agents = config.agents || [];
|
|
772
|
+
const sessionStartCmd = path.join(localScriptsDir, "memento-sessionstart-identity.sh");
|
|
773
|
+
const registeredHooks = [];
|
|
774
|
+
|
|
775
|
+
// Claude Code
|
|
776
|
+
const hasClaude = agents.includes("claude-code")
|
|
753
777
|
|| fs.existsSync(path.join(cwd, ".claude"));
|
|
754
|
-
let hooksUpdated = false;
|
|
755
778
|
if (hasClaude) {
|
|
756
779
|
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
780
|
+
const settings = readJsonFile(settingsPath) || {};
|
|
781
|
+
if (ensureHook(settings, "SessionStart", sessionStartCmd, 10000)) {
|
|
782
|
+
writeJsonFile(settingsPath, settings);
|
|
783
|
+
registeredHooks.push("Claude Code → .claude/settings.local.json");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Gemini CLI
|
|
788
|
+
const hasGemini = agents.includes("gemini")
|
|
789
|
+
|| fs.existsSync(path.join(cwd, ".gemini"));
|
|
790
|
+
if (hasGemini) {
|
|
791
|
+
const settingsPath = path.join(cwd, ".gemini", "settings.json");
|
|
792
|
+
const settings = readJsonFile(settingsPath) || {};
|
|
793
|
+
if (ensureHook(settings, "SessionStart", sessionStartCmd, 10000)) {
|
|
794
|
+
writeJsonFile(settingsPath, settings);
|
|
795
|
+
registeredHooks.push("Gemini CLI → .gemini/settings.json");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Codex CLI — ensure notify is configured
|
|
800
|
+
const hasCodex = agents.includes("codex")
|
|
801
|
+
|| fs.existsSync(path.join(cwd, ".codex"));
|
|
802
|
+
if (hasCodex) {
|
|
803
|
+
const notifyScript = path.join(localScriptsDir, "memento-codex-notify.sh");
|
|
804
|
+
const codexTomlPath = path.join(cwd, ".codex", "config.toml");
|
|
805
|
+
let tomlContent = "";
|
|
806
|
+
try { tomlContent = fs.readFileSync(codexTomlPath, "utf-8"); } catch { /* doesn't exist yet */ }
|
|
807
|
+
if (!tomlContent.includes("notify") && fs.existsSync(notifyScript)) {
|
|
808
|
+
const notifyLine = `\nnotify = ["bash", "${notifyScript}"]\n`;
|
|
809
|
+
const separator = tomlContent && !tomlContent.endsWith("\n") ? "\n" : "";
|
|
810
|
+
fs.mkdirSync(path.dirname(codexTomlPath), { recursive: true });
|
|
811
|
+
fs.writeFileSync(codexTomlPath, tomlContent + separator + notifyLine);
|
|
812
|
+
registeredHooks.push("Codex CLI → .codex/config.toml (notify)");
|
|
771
813
|
}
|
|
772
814
|
}
|
|
773
815
|
|
|
@@ -776,9 +818,11 @@ async function runUpdate() {
|
|
|
776
818
|
for (const name of updated) {
|
|
777
819
|
console.log(` ${name}`);
|
|
778
820
|
}
|
|
779
|
-
if (
|
|
821
|
+
if (registeredHooks.length > 0) {
|
|
780
822
|
console.log("\n Registered hooks:");
|
|
781
|
-
|
|
823
|
+
for (const hook of registeredHooks) {
|
|
824
|
+
console.log(` ${hook}`);
|
|
825
|
+
}
|
|
782
826
|
}
|
|
783
827
|
console.log(`\n Version written to .memento/version`);
|
|
784
828
|
console.log(" Restart your agent session to pick up changes.\n");
|