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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memento-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "mcpName": "io.github.myrakrusemark/memento-protocol",
5
5
  "description": "The Memento Protocol — persistent memory for AI agents",
6
6
  "type": "module",
@@ -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
- function mergeJsonFile(filePath, data) {
200
- const existing = readJsonFile(filePath) || {};
201
- const merged = deepMerge(existing, data);
202
- writeJsonFile(filePath, merged);
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 — only if Claude Code selected
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 (hasClaude) {
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("\nClaude Code hooks (automate recall + distillation):");
485
+ console.log("\nAgent hooks (automate recall + distillation):");
470
486
  enableUserPrompt = await askYesNo(
471
487
  rl,
472
- " UserPromptSubmit — recall on every message?",
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 + write Claude Code settings — gated on hasClaude
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 (hasClaude && anyHookEnabled) {
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
- // 8. Write .claude/settings.local.json (hooks)
550
- const hooks = {};
551
- if (enableUserPrompt) {
552
- hooks.UserPromptSubmit = [
553
- {
554
- hooks: [
555
- {
556
- type: "command",
557
- command: path.join(localScriptsDir, "memento-userprompt-recall.sh"),
558
- timeout: 5000,
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
- if (enableStop) {
565
- hooks.Stop = [
566
- {
567
- hooks: [
568
- {
569
- type: "command",
570
- command: path.join(localScriptsDir, "memento-stop-recall.sh"),
571
- timeout: 5000,
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
- if (enablePreCompact) {
578
- hooks.PreCompact = [
579
- {
580
- hooks: [
581
- {
582
- type: "command",
583
- command: path.join(localScriptsDir, "memento-precompact-distill.sh"),
584
- timeout: 30000,
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
- if (enableSessionStart) {
591
- hooks.SessionStart = [
592
- {
593
- hooks: [
594
- {
595
- type: "command",
596
- command: path.join(
597
- localScriptsDir,
598
- "memento-sessionstart-identity.sh",
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 Claude Code workspaces
750
- // Detect by .claude/ dir (older configs may not have agents field)
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 hasClaude = (config.agents || []).includes("claude-code")
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 claudeSettings = readJsonFile(settingsPath) || {};
758
- const sessionStartCmd = path.join(localScriptsDir, "memento-sessionstart-identity.sh");
759
- const existingHooks = claudeSettings.hooks?.["SessionStart"] || [];
760
- const hasSessionStart = existingHooks.some((entry) =>
761
- entry.hooks?.some((h) => h.command === sessionStartCmd)
762
- );
763
- if (!hasSessionStart) {
764
- claudeSettings.hooks = claudeSettings.hooks || {};
765
- claudeSettings.hooks["SessionStart"] = [
766
- ...existingHooks,
767
- { hooks: [{ type: "command", command: sessionStartCmd, timeout: 10000 }] },
768
- ];
769
- writeJsonFile(settingsPath, claudeSettings);
770
- hooksUpdated = true;
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 (hooksUpdated) {
821
+ if (registeredHooks.length > 0) {
780
822
  console.log("\n Registered hooks:");
781
- console.log(" SessionStart → memento-sessionstart-identity.sh (identity + version check)");
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");