memento-mcp 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -12
- package/package.json +1 -1
- package/scripts/hook-toast.sh +140 -0
- package/scripts/memento-precompact-distill.sh +27 -0
- package/scripts/memento-sessionstart-identity.sh +7 -0
- package/scripts/memento-stop-recall.sh +8 -0
- package/scripts/memento-userprompt-recall.sh +8 -0
- package/src/cli.js +242 -69
- package/src/config.js +4 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ AI agents have anterograde amnesia — every session starts blank. The Memento P
|
|
|
10
10
|
npx memento-mcp init
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
This creates `.memento.json`,
|
|
13
|
+
This creates `.memento.json`, detects your agent (Claude Code, Codex, Gemini CLI, OpenCode), writes the correct MCP config, and sets up hooks (Claude Code only) — all in one command.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -43,29 +43,55 @@ Save the `api_key` from the response — you'll need it next.
|
|
|
43
43
|
|
|
44
44
|
### Step 2: Configure your MCP client
|
|
45
45
|
|
|
46
|
-
**Claude Code
|
|
46
|
+
**Claude Code** — `.mcp.json` in your project root (or `~/.claude.json` globally):
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"memento": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "memento-mcp"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**OpenAI Codex** — `.codex/config.toml`:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[mcp_servers.memento]
|
|
63
|
+
command = "npx"
|
|
64
|
+
args = ["-y", "memento-mcp"]
|
|
65
|
+
```
|
|
49
66
|
|
|
50
|
-
**
|
|
67
|
+
**Gemini CLI** — `.gemini/settings.json`:
|
|
51
68
|
|
|
52
69
|
```json
|
|
53
70
|
{
|
|
54
71
|
"mcpServers": {
|
|
55
72
|
"memento": {
|
|
56
|
-
"command": "
|
|
57
|
-
"args": ["
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "memento-mcp"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**OpenCode** — `opencode.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcp": {
|
|
85
|
+
"memento": {
|
|
86
|
+
"type": "local",
|
|
87
|
+
"command": ["npx", "-y", "memento-mcp"],
|
|
88
|
+
"enabled": true
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
```
|
|
67
93
|
|
|
68
|
-
|
|
94
|
+
Set credentials via `.memento.json` (created by `npx memento-mcp init`) or environment variables `MEMENTO_API_KEY`, `MEMENTO_API_URL`, `MEMENTO_WORKSPACE`.
|
|
69
95
|
|
|
70
96
|
### Step 3: Restart your client
|
|
71
97
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hook-toast.sh — tmux popup notifications for agent hooks.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# hook-toast.sh <system> <message> # one-shot toast (auto-closes after 2s)
|
|
6
|
+
# hook-toast.sh <system> --status <id> # start a multi-stage toast (poll mode)
|
|
7
|
+
# hook-toast.sh --update <id> <message> # update a running toast
|
|
8
|
+
# hook-toast.sh --close <id> # close a running toast (after delay)
|
|
9
|
+
#
|
|
10
|
+
# Built-in systems: fathom, memento
|
|
11
|
+
# fathom → 📝 purple (colour141)
|
|
12
|
+
# memento → 🧠 teal (colour37)
|
|
13
|
+
#
|
|
14
|
+
# Any other system name works too — gets a default icon and grey border.
|
|
15
|
+
#
|
|
16
|
+
# Multi-stage example (PreCompact):
|
|
17
|
+
# hook-toast.sh memento --status precompact
|
|
18
|
+
# hook-toast.sh --update precompact "⏳ Getting context..."
|
|
19
|
+
# hook-toast.sh --update precompact "⏳ Extracting memories..."
|
|
20
|
+
# hook-toast.sh --update precompact "✓ Stored 7 memories"
|
|
21
|
+
# hook-toast.sh --close precompact
|
|
22
|
+
|
|
23
|
+
TOAST_DIR="/tmp/hook-toast"
|
|
24
|
+
mkdir -p "$TOAST_DIR"
|
|
25
|
+
|
|
26
|
+
# Bail silently if not in tmux
|
|
27
|
+
if ! tmux info &>/dev/null; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
get_style() {
|
|
32
|
+
case "$1" in
|
|
33
|
+
memento) echo "colour37" ;;
|
|
34
|
+
fathom) echo "colour141" ;;
|
|
35
|
+
*) echo "colour245" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get_icon() {
|
|
40
|
+
case "$1" in
|
|
41
|
+
memento) echo "🧠" ;;
|
|
42
|
+
fathom) echo "📝" ;;
|
|
43
|
+
*) echo "📌" ;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get_title() {
|
|
48
|
+
case "$1" in
|
|
49
|
+
memento) echo "Memento" ;;
|
|
50
|
+
fathom) echo "Fathom" ;;
|
|
51
|
+
*) echo "$1" ;;
|
|
52
|
+
esac
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# One-shot toast
|
|
56
|
+
toast_oneshot() {
|
|
57
|
+
local system="$1"
|
|
58
|
+
local message="$2"
|
|
59
|
+
local colour icon title
|
|
60
|
+
colour=$(get_style "$system")
|
|
61
|
+
icon=$(get_icon "$system")
|
|
62
|
+
title=$(get_title "$system")
|
|
63
|
+
|
|
64
|
+
(tmux display-popup -x R -y 0 -w 42 -h 3 \
|
|
65
|
+
-s "fg=$colour" -T " $icon $title " -E \
|
|
66
|
+
"echo ' $message'; sleep 2" &>/dev/null &)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Start a multi-stage toast (polling status file)
|
|
70
|
+
toast_start() {
|
|
71
|
+
local system="$1"
|
|
72
|
+
local id="$2"
|
|
73
|
+
local status_file="$TOAST_DIR/$id"
|
|
74
|
+
local colour icon title
|
|
75
|
+
colour=$(get_style "$system")
|
|
76
|
+
icon=$(get_icon "$system")
|
|
77
|
+
title=$(get_title "$system")
|
|
78
|
+
|
|
79
|
+
echo "⏳ Starting..." > "$status_file"
|
|
80
|
+
|
|
81
|
+
(tmux display-popup -x R -y 0 -w 42 -h 3 \
|
|
82
|
+
-s "fg=$colour" -T " $icon $title " -E "
|
|
83
|
+
LAST=''
|
|
84
|
+
while [ -f '$status_file' ]; do
|
|
85
|
+
MSG=\$(cat '$status_file' 2>/dev/null)
|
|
86
|
+
if [ \"\$MSG\" != \"\$LAST\" ]; then
|
|
87
|
+
clear
|
|
88
|
+
echo \" \$MSG\"
|
|
89
|
+
LAST=\"\$MSG\"
|
|
90
|
+
fi
|
|
91
|
+
case \"\$MSG\" in ✓*|✗*) sleep 2; break ;; esac
|
|
92
|
+
sleep 0.2
|
|
93
|
+
done
|
|
94
|
+
" &>/dev/null &)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Update a running toast
|
|
98
|
+
toast_update() {
|
|
99
|
+
local id="$1"
|
|
100
|
+
local message="$2"
|
|
101
|
+
echo "$message" > "$TOAST_DIR/$id"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Close a running toast (remove status file — popup exits on next poll)
|
|
105
|
+
toast_close() {
|
|
106
|
+
local id="$1"
|
|
107
|
+
# Give the popup time to display the final message
|
|
108
|
+
sleep 2
|
|
109
|
+
rm -f "$TOAST_DIR/$id"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# --- Argument parsing ---
|
|
113
|
+
case "${1:-}" in
|
|
114
|
+
--update)
|
|
115
|
+
toast_update "$2" "$3"
|
|
116
|
+
;;
|
|
117
|
+
--close)
|
|
118
|
+
toast_close "$2"
|
|
119
|
+
;;
|
|
120
|
+
--*)
|
|
121
|
+
echo "Unknown option: $1" >&2
|
|
122
|
+
exit 1
|
|
123
|
+
;;
|
|
124
|
+
"")
|
|
125
|
+
echo "Usage: hook-toast.sh <system> <message>" >&2
|
|
126
|
+
echo " hook-toast.sh <system> --status <id>" >&2
|
|
127
|
+
echo " hook-toast.sh --update <id> <message>" >&2
|
|
128
|
+
echo " hook-toast.sh --close <id>" >&2
|
|
129
|
+
exit 1
|
|
130
|
+
;;
|
|
131
|
+
*)
|
|
132
|
+
SYSTEM="$1"
|
|
133
|
+
shift
|
|
134
|
+
if [ "${1:-}" = "--status" ]; then
|
|
135
|
+
toast_start "$SYSTEM" "$2"
|
|
136
|
+
else
|
|
137
|
+
toast_oneshot "$SYSTEM" "$*"
|
|
138
|
+
fi
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
@@ -8,17 +8,27 @@
|
|
|
8
8
|
set -o pipefail
|
|
9
9
|
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
TOAST="$SCRIPT_DIR/hook-toast.sh"
|
|
12
|
+
|
|
13
|
+
# Toast: start multi-stage
|
|
14
|
+
"$TOAST" memento --status distill &>/dev/null
|
|
15
|
+
"$TOAST" --update distill "⏳ Getting context..." &>/dev/null
|
|
16
|
+
|
|
11
17
|
INPUT=$(cat)
|
|
12
18
|
|
|
13
19
|
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path' | sed "s|~|$HOME|")
|
|
14
20
|
|
|
15
21
|
# Only distill if transcript exists and has content
|
|
16
22
|
if [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
23
|
+
"$TOAST" --update distill "✗ No transcript found" &>/dev/null
|
|
24
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
17
25
|
exit 0
|
|
18
26
|
fi
|
|
19
27
|
|
|
20
28
|
LINE_COUNT=$(wc -l < "$TRANSCRIPT_PATH")
|
|
21
29
|
if [ "$LINE_COUNT" -lt 2 ]; then
|
|
30
|
+
"$TOAST" --update distill "✓ Skipped (tiny conversation)" &>/dev/null
|
|
31
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
22
32
|
exit 0
|
|
23
33
|
fi
|
|
24
34
|
|
|
@@ -79,9 +89,14 @@ else
|
|
|
79
89
|
fi
|
|
80
90
|
|
|
81
91
|
if [ ${#TRANSCRIPT_TEXT} -lt 200 ]; then
|
|
92
|
+
"$TOAST" --update distill "✓ Skipped (too short)" &>/dev/null
|
|
93
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
82
94
|
exit 0 # Too short to distill anything useful
|
|
83
95
|
fi
|
|
84
96
|
|
|
97
|
+
# Toast: extracting
|
|
98
|
+
"$TOAST" --update distill "⏳ Extracting memories..." &>/dev/null
|
|
99
|
+
|
|
85
100
|
DISTILL_MODEL="${DISTILL_MODEL:-llama}"
|
|
86
101
|
|
|
87
102
|
# claude-code path: run extraction locally via claude -p, push to /v1/memories/ingest
|
|
@@ -166,9 +181,15 @@ print(json.dumps({'memories': parsed, 'source': 'distill:claude-code'}))
|
|
|
166
181
|
-d "$INGEST_PAYLOAD" \
|
|
167
182
|
"$MEMENTO_API/v1/memories/ingest" > /dev/null 2>&1
|
|
168
183
|
|
|
184
|
+
"$TOAST" --update distill "✓ Stored ${MEMORY_COUNT} memories" &>/dev/null
|
|
185
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
186
|
+
|
|
169
187
|
python3 -c "import json,sys; print(json.dumps({'systemMessage': sys.argv[1]}))" \
|
|
170
188
|
"Memento Distill (claude-code): ${MEMORY_COUNT} memories — ${TYPE_BREAKDOWN}"
|
|
171
189
|
else
|
|
190
|
+
"$TOAST" --update distill "✓ No memories extracted" &>/dev/null
|
|
191
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
192
|
+
|
|
172
193
|
python3 -c "import json,sys; print(json.dumps({'systemMessage': sys.argv[1]}))" \
|
|
173
194
|
"Memento Distill (claude-code): no memories extracted"
|
|
174
195
|
fi
|
|
@@ -206,9 +227,15 @@ MEMORY_COUNT=$(echo "$DISTILL_SUMMARY" | cut -d'|' -f1)
|
|
|
206
227
|
TYPE_BREAKDOWN=$(echo "$DISTILL_SUMMARY" | cut -d'|' -f2)
|
|
207
228
|
|
|
208
229
|
if [ "${MEMORY_COUNT:-0}" -gt 0 ] 2>/dev/null; then
|
|
230
|
+
"$TOAST" --update distill "✓ Stored ${MEMORY_COUNT} memories" &>/dev/null
|
|
231
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
232
|
+
|
|
209
233
|
python3 -c "import json,sys; print(json.dumps({'systemMessage': sys.argv[1]}))" \
|
|
210
234
|
"Memento Distill: ${MEMORY_COUNT} memories — ${TYPE_BREAKDOWN}"
|
|
211
235
|
else
|
|
236
|
+
"$TOAST" --update distill "✓ No memories extracted" &>/dev/null
|
|
237
|
+
("$TOAST" --close distill &>/dev/null &)
|
|
238
|
+
|
|
212
239
|
python3 -c "import json,sys; print(json.dumps({'systemMessage': sys.argv[1]}))" \
|
|
213
240
|
"Memento Distill: no memories extracted"
|
|
214
241
|
fi
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
set -o pipefail
|
|
11
11
|
|
|
12
12
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
TOAST="$SCRIPT_DIR/hook-toast.sh"
|
|
13
14
|
|
|
14
15
|
# --- Config from .memento.json (if present) ---
|
|
15
16
|
CONFIG_JSON=$(python3 -c "
|
|
@@ -80,6 +81,9 @@ ACTIVE_TMP=$(mktemp)
|
|
|
80
81
|
SKIP_TMP=$(mktemp)
|
|
81
82
|
trap 'rm -f "$IDENTITY_TMP" "$ACTIVE_TMP" "$SKIP_TMP"' EXIT
|
|
82
83
|
|
|
84
|
+
# Toast: loading identity
|
|
85
|
+
"$TOAST" memento "⏳ Loading identity..." &>/dev/null
|
|
86
|
+
|
|
83
87
|
# Fetch all three endpoints in parallel
|
|
84
88
|
curl -s --max-time 3 -H "$AUTH_HEADER" -H "$WS_HEADER" \
|
|
85
89
|
"$MEMENTO_API/v1/identity" > "$IDENTITY_TMP" 2>/dev/null &
|
|
@@ -159,4 +163,7 @@ print(json.dumps({
|
|
|
159
163
|
}))
|
|
160
164
|
" "$IDENTITY_TMP" "$ACTIVE_TMP" "$SKIP_TMP" 2>/dev/null
|
|
161
165
|
|
|
166
|
+
# Toast: done
|
|
167
|
+
"$TOAST" memento "✓ Identity loaded" &>/dev/null
|
|
168
|
+
|
|
162
169
|
exit 0
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
set -o pipefail
|
|
7
7
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
TOAST="$SCRIPT_DIR/hook-toast.sh"
|
|
9
10
|
|
|
10
11
|
# --- Config from .memento.json (if present) ---
|
|
11
12
|
CONFIG_JSON=$(python3 -c "
|
|
@@ -74,6 +75,9 @@ fi
|
|
|
74
75
|
# Truncate to first 500 chars for the query
|
|
75
76
|
QUERY="${ASSISTANT_MSG:0:500}"
|
|
76
77
|
|
|
78
|
+
# Toast: start retrieving
|
|
79
|
+
"$TOAST" memento "⏳ Autonomous recall..." &>/dev/null
|
|
80
|
+
|
|
77
81
|
# Call Memento /v1/context
|
|
78
82
|
RESULT=$(curl -s --max-time 3 \
|
|
79
83
|
-X POST \
|
|
@@ -122,9 +126,13 @@ if [ -n "$REMAINING" ]; then
|
|
|
122
126
|
fi
|
|
123
127
|
|
|
124
128
|
if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
|
|
129
|
+
"$TOAST" memento "✓ No memories matched" &>/dev/null
|
|
125
130
|
exit 0
|
|
126
131
|
fi
|
|
127
132
|
|
|
133
|
+
# Toast: result
|
|
134
|
+
"$TOAST" memento "✓ ${SAAS_COUNT} memories recalled" &>/dev/null
|
|
135
|
+
|
|
128
136
|
# Block the Stop so Claude continues — the reason becomes Claude's next instruction.
|
|
129
137
|
REASON="Autonomous Recall: ${SAAS_COUNT} memories surfaced from your last response.
|
|
130
138
|
${SAAS_DETAIL}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
set -o pipefail
|
|
8
8
|
|
|
9
9
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
TOAST="$SCRIPT_DIR/hook-toast.sh"
|
|
10
11
|
|
|
11
12
|
# --- Config from .memento.json (if present) ---
|
|
12
13
|
CONFIG_JSON=$(python3 -c "
|
|
@@ -66,6 +67,9 @@ fi
|
|
|
66
67
|
|
|
67
68
|
QUERY="${USER_MESSAGE:0:500}"
|
|
68
69
|
|
|
70
|
+
# Toast: start retrieving
|
|
71
|
+
"$TOAST" memento "⏳ Retrieving memories..." &>/dev/null
|
|
72
|
+
|
|
69
73
|
# Call Memento SaaS /v1/context
|
|
70
74
|
SAAS_OUTPUT=$(curl -s --max-time 3 \
|
|
71
75
|
-X POST \
|
|
@@ -118,9 +122,13 @@ if [ -n "$REMAINING" ]; then
|
|
|
118
122
|
fi
|
|
119
123
|
|
|
120
124
|
if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
|
|
125
|
+
"$TOAST" memento "✓ No memories matched" &>/dev/null
|
|
121
126
|
exit 0
|
|
122
127
|
fi
|
|
123
128
|
|
|
129
|
+
# Toast: result
|
|
130
|
+
"$TOAST" memento "✓ ${SAAS_COUNT} memories recalled" &>/dev/null
|
|
131
|
+
|
|
124
132
|
DETAIL_TEXT="Memento Recall: ${SAAS_COUNT} memories"
|
|
125
133
|
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"$SAAS_DETAIL"
|
|
126
134
|
DETAIL_TEXT="$DETAIL_TEXT"$'\n'$'\n'"REMINDER: If Memento MCP tools are not loaded, run: ToolSearch query=\"+memento\" max_results=20"
|
package/src/cli.js
CHANGED
|
@@ -96,6 +96,14 @@ function httpsPost(url, body) {
|
|
|
96
96
|
// File writers
|
|
97
97
|
// ---------------------------------------------------------------------------
|
|
98
98
|
|
|
99
|
+
function readJsonFile(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
function writeJsonFile(filePath, data) {
|
|
100
108
|
const dir = path.dirname(filePath);
|
|
101
109
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -103,14 +111,7 @@ function writeJsonFile(filePath, data) {
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
function mergeJsonFile(filePath, data) {
|
|
106
|
-
|
|
107
|
-
if (fs.existsSync(filePath)) {
|
|
108
|
-
try {
|
|
109
|
-
existing = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
110
|
-
} catch {
|
|
111
|
-
// Corrupt file — overwrite
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
+
const existing = readJsonFile(filePath) || {};
|
|
114
115
|
const merged = deepMerge(existing, data);
|
|
115
116
|
writeJsonFile(filePath, merged);
|
|
116
117
|
}
|
|
@@ -127,6 +128,106 @@ function appendToGitignore(cwd, line) {
|
|
|
127
128
|
return true;
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Agent registry — per-agent MCP config writers
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const MCP_SERVER_ENTRY = {
|
|
136
|
+
command: "npx",
|
|
137
|
+
args: ["-y", "memento-mcp"],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function writeMcpJson(cwd) {
|
|
141
|
+
const filePath = path.join(cwd, ".mcp.json");
|
|
142
|
+
const existing = readJsonFile(filePath) || {};
|
|
143
|
+
deepMerge(existing, { mcpServers: { memento: MCP_SERVER_ENTRY } });
|
|
144
|
+
writeJsonFile(filePath, existing);
|
|
145
|
+
return ".mcp.json";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function writeCodexToml(cwd) {
|
|
149
|
+
const dir = path.join(cwd, ".codex");
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
151
|
+
const filePath = path.join(dir, "config.toml");
|
|
152
|
+
|
|
153
|
+
let content = "";
|
|
154
|
+
try {
|
|
155
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
156
|
+
} catch {
|
|
157
|
+
/* file doesn't exist */
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Skip if memento section already exists
|
|
161
|
+
if (/\[mcp_servers\.memento\]/.test(content)) {
|
|
162
|
+
return ".codex/config.toml (already configured)";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const section = `\n[mcp_servers.memento]\ncommand = "npx"\nargs = ["-y", "memento-mcp"]\n`;
|
|
166
|
+
const separator = content && !content.endsWith("\n") ? "\n" : "";
|
|
167
|
+
fs.writeFileSync(filePath, content + separator + section);
|
|
168
|
+
return ".codex/config.toml";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeGeminiJson(cwd) {
|
|
172
|
+
const dir = path.join(cwd, ".gemini");
|
|
173
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
174
|
+
const filePath = path.join(dir, "settings.json");
|
|
175
|
+
const existing = readJsonFile(filePath) || {};
|
|
176
|
+
deepMerge(existing, { mcpServers: { memento: MCP_SERVER_ENTRY } });
|
|
177
|
+
writeJsonFile(filePath, existing);
|
|
178
|
+
return ".gemini/settings.json";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeOpencodeJson(cwd) {
|
|
182
|
+
const filePath = path.join(cwd, "opencode.json");
|
|
183
|
+
const existing = readJsonFile(filePath) || {};
|
|
184
|
+
deepMerge(existing, {
|
|
185
|
+
mcp: {
|
|
186
|
+
memento: {
|
|
187
|
+
type: "local",
|
|
188
|
+
command: ["npx", "-y", "memento-mcp"],
|
|
189
|
+
enabled: true,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
writeJsonFile(filePath, existing);
|
|
194
|
+
return "opencode.json";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const AGENTS = {
|
|
198
|
+
"claude-code": {
|
|
199
|
+
name: "Claude Code",
|
|
200
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
|
|
201
|
+
configWriter: writeMcpJson,
|
|
202
|
+
hasHooks: true,
|
|
203
|
+
nextSteps: "Restart Claude Code to activate.",
|
|
204
|
+
},
|
|
205
|
+
codex: {
|
|
206
|
+
name: "OpenAI Codex",
|
|
207
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, ".codex")),
|
|
208
|
+
configWriter: writeCodexToml,
|
|
209
|
+
hasHooks: false,
|
|
210
|
+
nextSteps: "Run `codex` in this directory — memento tools load automatically.",
|
|
211
|
+
},
|
|
212
|
+
gemini: {
|
|
213
|
+
name: "Gemini CLI",
|
|
214
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
|
|
215
|
+
configWriter: writeGeminiJson,
|
|
216
|
+
hasHooks: false,
|
|
217
|
+
nextSteps: "Run `gemini` in this directory — memento tools load automatically.",
|
|
218
|
+
},
|
|
219
|
+
opencode: {
|
|
220
|
+
name: "OpenCode",
|
|
221
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, "opencode.json")),
|
|
222
|
+
configWriter: writeOpencodeJson,
|
|
223
|
+
hasHooks: false,
|
|
224
|
+
nextSteps: "Run `opencode` in this directory — memento tools load automatically.",
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Exported for testing
|
|
229
|
+
export { AGENTS, writeMcpJson, writeCodexToml, writeGeminiJson, writeOpencodeJson };
|
|
230
|
+
|
|
130
231
|
// ---------------------------------------------------------------------------
|
|
131
232
|
// CLI
|
|
132
233
|
// ---------------------------------------------------------------------------
|
|
@@ -193,26 +294,76 @@ async function runInit() {
|
|
|
193
294
|
false,
|
|
194
295
|
);
|
|
195
296
|
|
|
196
|
-
// 4.
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
297
|
+
// 4. Agent detection + selection
|
|
298
|
+
const agentKeys = Object.keys(AGENTS);
|
|
299
|
+
const detected = agentKeys.filter((key) => AGENTS[key].detect(cwd));
|
|
300
|
+
|
|
301
|
+
console.log("\nDetected agents:");
|
|
302
|
+
const markers = {
|
|
303
|
+
"claude-code": ".claude/",
|
|
304
|
+
codex: ".codex/",
|
|
305
|
+
gemini: ".gemini/",
|
|
306
|
+
opencode: "opencode.json",
|
|
307
|
+
};
|
|
308
|
+
for (const key of agentKeys) {
|
|
309
|
+
const agent = AGENTS[key];
|
|
310
|
+
const isDetected = detected.includes(key);
|
|
311
|
+
const mark = isDetected ? "✓" : " ";
|
|
312
|
+
const hint = isDetected ? ` (${markers[key]} found)` : "";
|
|
313
|
+
console.log(` ${mark} ${agent.name}${hint}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log("\n Configure for which agents?");
|
|
317
|
+
agentKeys.forEach((key, i) => {
|
|
318
|
+
const mark = detected.includes(key) ? " ✓" : "";
|
|
319
|
+
console.log(` ${i + 1}. ${AGENTS[key].name}${mark}`);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const defaultSelection =
|
|
323
|
+
detected.length > 0
|
|
324
|
+
? detected.map((key) => agentKeys.indexOf(key) + 1).join(",")
|
|
325
|
+
: "1";
|
|
326
|
+
const selectionStr = await ask(rl, "\n Enter numbers, comma-separated", defaultSelection);
|
|
327
|
+
|
|
328
|
+
const selectedIndices = selectionStr
|
|
329
|
+
.split(",")
|
|
330
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
331
|
+
.filter((n) => n >= 1 && n <= agentKeys.length);
|
|
332
|
+
const selectedAgents = [...new Set(selectedIndices.map((i) => agentKeys[i - 1]))];
|
|
333
|
+
|
|
334
|
+
if (selectedAgents.length === 0) {
|
|
335
|
+
console.log(" No agents selected. Defaulting to Claude Code.");
|
|
336
|
+
selectedAgents.push("claude-code");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const hasClaude = selectedAgents.includes("claude-code");
|
|
340
|
+
|
|
341
|
+
// 5. Hooks — only if Claude Code selected
|
|
342
|
+
let enableUserPrompt = false;
|
|
343
|
+
let enableStop = false;
|
|
344
|
+
let enablePreCompact = false;
|
|
209
345
|
let enableSessionStart = false;
|
|
210
|
-
|
|
211
|
-
|
|
346
|
+
|
|
347
|
+
if (hasClaude) {
|
|
348
|
+
console.log("\nClaude Code hooks (automate recall + distillation):");
|
|
349
|
+
enableUserPrompt = await askYesNo(
|
|
350
|
+
rl,
|
|
351
|
+
" UserPromptSubmit — recall on every message?",
|
|
352
|
+
true,
|
|
353
|
+
);
|
|
354
|
+
enableStop = await askYesNo(rl, " Stop — autonomous recall after responses?", true);
|
|
355
|
+
enablePreCompact = await askYesNo(
|
|
212
356
|
rl,
|
|
213
|
-
"
|
|
214
|
-
true
|
|
357
|
+
" PreCompact — distill memories before context compression?",
|
|
358
|
+
true,
|
|
215
359
|
);
|
|
360
|
+
if (enableIdentity) {
|
|
361
|
+
enableSessionStart = await askYesNo(
|
|
362
|
+
rl,
|
|
363
|
+
" SessionStart — inject identity + active items at startup?",
|
|
364
|
+
true,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
216
367
|
}
|
|
217
368
|
|
|
218
369
|
rl.close();
|
|
@@ -221,6 +372,7 @@ async function runInit() {
|
|
|
221
372
|
const config = {
|
|
222
373
|
apiKey,
|
|
223
374
|
workspace,
|
|
375
|
+
agents: selectedAgents,
|
|
224
376
|
features: {
|
|
225
377
|
images: enableImages,
|
|
226
378
|
identity: enableIdentity,
|
|
@@ -235,21 +387,23 @@ async function runInit() {
|
|
|
235
387
|
|
|
236
388
|
const created = [];
|
|
237
389
|
|
|
238
|
-
//
|
|
390
|
+
// 6. Write .memento.json
|
|
239
391
|
const configPath = path.join(cwd, ".memento.json");
|
|
240
392
|
writeJsonFile(configPath, config);
|
|
241
393
|
created.push(".memento.json");
|
|
242
394
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
395
|
+
// 7. Copy hook scripts + write Claude Code settings — gated on hasClaude
|
|
396
|
+
const anyHookEnabled =
|
|
397
|
+
enableUserPrompt || enableStop || enablePreCompact || enableSessionStart;
|
|
398
|
+
|
|
399
|
+
if (hasClaude && anyHookEnabled) {
|
|
247
400
|
const pkgScriptsDir = path.resolve(__dirname, "..", "scripts");
|
|
248
401
|
const localScriptsDir = path.join(cwd, ".memento", "scripts");
|
|
249
402
|
if (!fs.existsSync(localScriptsDir))
|
|
250
403
|
fs.mkdirSync(localScriptsDir, { recursive: true });
|
|
251
404
|
|
|
252
405
|
const scriptFiles = [
|
|
406
|
+
"hook-toast.sh",
|
|
253
407
|
enableUserPrompt && "memento-userprompt-recall.sh",
|
|
254
408
|
enableStop && "memento-stop-recall.sh",
|
|
255
409
|
enablePreCompact && "memento-precompact-distill.sh",
|
|
@@ -264,7 +418,7 @@ async function runInit() {
|
|
|
264
418
|
}
|
|
265
419
|
created.push(".memento/scripts/");
|
|
266
420
|
|
|
267
|
-
//
|
|
421
|
+
// 8. Write .claude/settings.local.json (hooks)
|
|
268
422
|
const hooks = {};
|
|
269
423
|
if (enableUserPrompt) {
|
|
270
424
|
hooks.UserPromptSubmit = [
|
|
@@ -305,14 +459,16 @@ async function runInit() {
|
|
|
305
459
|
},
|
|
306
460
|
];
|
|
307
461
|
}
|
|
308
|
-
|
|
309
462
|
if (enableSessionStart) {
|
|
310
463
|
hooks.SessionStart = [
|
|
311
464
|
{
|
|
312
465
|
hooks: [
|
|
313
466
|
{
|
|
314
467
|
type: "command",
|
|
315
|
-
command: path.join(
|
|
468
|
+
command: path.join(
|
|
469
|
+
localScriptsDir,
|
|
470
|
+
"memento-sessionstart-identity.sh",
|
|
471
|
+
),
|
|
316
472
|
timeout: 10000,
|
|
317
473
|
},
|
|
318
474
|
],
|
|
@@ -325,30 +481,29 @@ async function runInit() {
|
|
|
325
481
|
created.push(".claude/settings.local.json");
|
|
326
482
|
}
|
|
327
483
|
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
args: ["-y", "memento-mcp"],
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
created.push(".mcp.json");
|
|
484
|
+
// 9. Per-agent config files
|
|
485
|
+
for (const agentKey of selectedAgents) {
|
|
486
|
+
const agent = AGENTS[agentKey];
|
|
487
|
+
const result = agent.configWriter(cwd);
|
|
488
|
+
created.push(result);
|
|
489
|
+
}
|
|
339
490
|
|
|
340
|
-
//
|
|
491
|
+
// 10. Add .memento.json and .memento/scripts/ to .gitignore
|
|
341
492
|
let gitignoreUpdated = false;
|
|
342
493
|
if (appendToGitignore(cwd, ".memento.json")) gitignoreUpdated = true;
|
|
343
494
|
if (appendToGitignore(cwd, ".memento/scripts/")) gitignoreUpdated = true;
|
|
344
495
|
if (gitignoreUpdated) created.push(".gitignore (updated)");
|
|
345
496
|
|
|
346
|
-
//
|
|
497
|
+
// 11. Summary
|
|
347
498
|
const labels = {
|
|
348
499
|
".memento.json": "workspace config + credentials",
|
|
349
500
|
".memento/scripts/": "hook scripts (recall + distillation)",
|
|
350
501
|
".claude/settings.local.json": "hooks registered with Claude Code",
|
|
351
|
-
".mcp.json": "MCP server registered",
|
|
502
|
+
".mcp.json": "MCP server registered (Claude Code)",
|
|
503
|
+
".codex/config.toml": "MCP server registered (Codex)",
|
|
504
|
+
".codex/config.toml (already configured)": "MCP server (Codex, skipped)",
|
|
505
|
+
".gemini/settings.json": "MCP server registered (Gemini CLI)",
|
|
506
|
+
"opencode.json": "MCP server registered (OpenCode)",
|
|
352
507
|
".gitignore (updated)": "credentials excluded from git",
|
|
353
508
|
};
|
|
354
509
|
const colWidth = Math.max(...created.map((f) => f.length)) + 2;
|
|
@@ -358,14 +513,25 @@ async function runInit() {
|
|
|
358
513
|
const label = labels[f] || "";
|
|
359
514
|
console.log(` ${f.padEnd(colWidth)}${label}`);
|
|
360
515
|
}
|
|
361
|
-
|
|
516
|
+
|
|
517
|
+
// Per-agent next steps
|
|
518
|
+
console.log("\n Next steps:");
|
|
519
|
+
for (const agentKey of selectedAgents) {
|
|
520
|
+
const agent = AGENTS[agentKey];
|
|
521
|
+
console.log(` · ${agent.name}: ${agent.nextSteps}`);
|
|
522
|
+
}
|
|
362
523
|
console.log(" Your agent will wake up remembering.\n");
|
|
363
524
|
|
|
364
|
-
//
|
|
525
|
+
// 12. CLAUDE.md / AGENTS.md boilerplate
|
|
526
|
+
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
|
|
527
|
+
const docTarget = hasNonClaude
|
|
528
|
+
? "your CLAUDE.md, AGENTS.md, or equivalent"
|
|
529
|
+
: "your CLAUDE.md";
|
|
530
|
+
|
|
365
531
|
console.log("─".repeat(60));
|
|
366
532
|
console.log(`
|
|
367
|
-
One more step: paste the following into
|
|
368
|
-
or hand it to
|
|
533
|
+
One more step: paste the following into ${docTarget},
|
|
534
|
+
or hand it to your agent and ask it to add it. This teaches
|
|
369
535
|
your agent the memory discipline Memento expects.
|
|
370
536
|
|
|
371
537
|
── paste below this line ──────────────────────────────
|
|
@@ -392,32 +558,39 @@ before compaction. Trust the hooks. Focus on writing good memories.
|
|
|
392
558
|
}
|
|
393
559
|
|
|
394
560
|
// ---------------------------------------------------------------------------
|
|
395
|
-
// Entrypoint
|
|
561
|
+
// Entrypoint — only run when this module is the entry point (not imported)
|
|
396
562
|
// ---------------------------------------------------------------------------
|
|
397
563
|
|
|
398
|
-
const
|
|
564
|
+
const isMain =
|
|
565
|
+
process.argv[1] &&
|
|
566
|
+
(process.argv[1].endsWith("/cli.js") || process.argv[1].endsWith("memento-mcp"));
|
|
399
567
|
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
568
|
+
if (isMain) {
|
|
569
|
+
const args = process.argv.slice(2);
|
|
570
|
+
|
|
571
|
+
if (args[0] === "init") {
|
|
572
|
+
runInit().catch((err) => {
|
|
573
|
+
console.error(err);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
});
|
|
576
|
+
} else if (args.length === 0) {
|
|
577
|
+
// No args — start the MCP server (this is what .mcp.json invokes)
|
|
578
|
+
// Must call main() explicitly because the isMainModule guard in index.js
|
|
579
|
+
// checks process.argv[1] which still points to cli.js, not index.js.
|
|
580
|
+
const { main } = await import("./index.js");
|
|
581
|
+
await main();
|
|
582
|
+
} else {
|
|
583
|
+
console.log(`
|
|
413
584
|
Memento Protocol CLI
|
|
414
585
|
|
|
415
586
|
Usage:
|
|
416
587
|
npx memento-mcp init Set up Memento in the current project
|
|
417
588
|
npx memento-mcp Start the MCP server (used by .mcp.json)
|
|
418
589
|
|
|
419
|
-
This creates .memento.json, configures
|
|
420
|
-
and sets up
|
|
590
|
+
This creates .memento.json, configures your agent's MCP client,
|
|
591
|
+
and sets up hooks (Claude Code) — all in one command.
|
|
592
|
+
Supports Claude Code, Codex, Gemini CLI, and OpenCode.
|
|
421
593
|
`);
|
|
422
|
-
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
423
596
|
}
|
package/src/config.js
CHANGED
|
@@ -14,6 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
export const DEFAULTS = {
|
|
15
15
|
apiUrl: "https://memento-api.myrakrusemark.workers.dev",
|
|
16
16
|
workspace: "default",
|
|
17
|
+
agents: [],
|
|
17
18
|
features: { images: false, identity: false },
|
|
18
19
|
hooks: {
|
|
19
20
|
"userprompt-recall": { enabled: true, limit: 5, maxLength: 200 },
|
|
@@ -69,5 +70,7 @@ export function resolveConfig(startDir = process.cwd()) {
|
|
|
69
70
|
hooks[key] = { ...defaults, ...(fileConfig.hooks?.[key] || {}) };
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
const agents = fileConfig.agents || DEFAULTS.agents;
|
|
74
|
+
|
|
75
|
+
return { apiKey, apiUrl, workspace, agents, features, hooks };
|
|
73
76
|
}
|