memento-mcp 0.3.13 → 0.3.15
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 +9 -2
- package/README.md +2 -2
- package/package.json +2 -1
- package/scripts/memento-stop-recall.sh +1 -1
- package/scripts/memento-userprompt-recall.sh +115 -1
- package/src/cli.js +3 -3
- package/src/index.js +57 -7
- package/src/storage/hosted.js +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.3.15] - 2026-03-13
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Renamed `memento_store` → `memento_remember` for cognitive language consistency — pairs with `memento_recall`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
9
16
|
## [Unreleased]
|
|
10
17
|
|
|
11
18
|
### Added
|
|
@@ -16,7 +23,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
16
23
|
- `source:distill` tag renamed to `source:distill:llama-3.1-8b` to encode model provenance. The `claude-code` path tags memories `source:distill:claude-code`.
|
|
17
24
|
|
|
18
25
|
### Removed
|
|
19
|
-
- **Passive memory extraction (auto_extract)** — removed conversation buffer, auto-extraction from `/v1/context`, and buffer fallback from `/v1/extract`. Smart models know when to store via `
|
|
26
|
+
- **Passive memory extraction (auto_extract)** — removed conversation buffer, auto-extraction from `/v1/context`, and buffer fallback from `/v1/extract`. Smart models know when to store via `memento_remember`; passive extraction produced noisy, low-signal memories. The `memento_extract` tool and `/v1/extract` route still work with explicit transcripts. The `conversation_buffer` table is no longer created for new workspaces.
|
|
20
27
|
|
|
21
28
|
---
|
|
22
29
|
|
|
@@ -85,7 +92,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
85
92
|
|
|
86
93
|
### Added
|
|
87
94
|
- Initial release
|
|
88
|
-
- MCP server with `memento_init`, `memento_read`, `memento_update`, `
|
|
95
|
+
- MCP server with `memento_init`, `memento_read`, `memento_update`, `memento_remember`, `memento_recall`, `memento_skip_add`, `memento_skip_check`, `memento_health`
|
|
89
96
|
- SaaS API on Cloudflare Workers + Turso edge database
|
|
90
97
|
- Semantic search via `bge-small-en-v1.5` embeddings in Cloudflare Vectorize
|
|
91
98
|
- Free tier: 100 memories, 20 items, 1 workspace
|
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ The MCP server connects at startup. Restart so it picks up the new config.
|
|
|
101
101
|
|
|
102
102
|
```text
|
|
103
103
|
> memento_health() # verify connection
|
|
104
|
-
>
|
|
104
|
+
> memento_remember( # store your first memory
|
|
105
105
|
content: "API uses /v2 endpoints. Auth is Bearer token in header.",
|
|
106
106
|
type: "instruction",
|
|
107
107
|
tags: ["api", "auth"]
|
|
@@ -126,7 +126,7 @@ On session start:
|
|
|
126
126
|
3. `memento_recall` with current task context — find relevant past memories
|
|
127
127
|
|
|
128
128
|
During work — actively manage your own memories:
|
|
129
|
-
- `
|
|
129
|
+
- `memento_remember` when you learn something, make a decision, or discover a pattern
|
|
130
130
|
- `memento_recall` before starting any subtask — someone may have already figured it out
|
|
131
131
|
- `memento_item_update` as you make progress — don't wait until the end
|
|
132
132
|
- `memento_item_create` when new work emerges
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memento-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"mcpName": "io.github.myrakrusemark/memento-protocol",
|
|
5
5
|
"description": "The Memento Protocol — persistent memory for AI agents",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
24
24
|
"dotenv": "^16.6.1",
|
|
25
|
+
"sharp": "^0.34.5",
|
|
25
26
|
"zod": "^3.24.2"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
@@ -135,7 +135,7 @@ SUMMARY="Memento Recall (${SAAS_COUNT})"
|
|
|
135
135
|
# If no memories are relevant, respond with <...> to signal active silence.
|
|
136
136
|
REASON="${SUMMARY}:
|
|
137
137
|
${SAAS_DETAIL}
|
|
138
|
-
Stale or wrong? memento_memory_delete · memento_consolidate ·
|
|
138
|
+
Stale or wrong? memento_memory_delete · memento_consolidate · memento_remember. Otherwise <...>."
|
|
139
139
|
|
|
140
140
|
python3 -c "
|
|
141
141
|
import json, sys
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# JSON output: systemMessage (user sees count) + additionalContext (model sees details).
|
|
4
4
|
#
|
|
5
5
|
# Calls /v1/context endpoint for memories + skip list matches.
|
|
6
|
+
# Supports image search: detects pasted images (Claude Code image cache) and
|
|
7
|
+
# file paths in the message, downscales to 224x224, sends to /v1/context.
|
|
6
8
|
|
|
7
9
|
set -o pipefail
|
|
8
10
|
|
|
@@ -60,6 +62,8 @@ MEMENTO_WS="${MEMENTO_WORKSPACE:-default}"
|
|
|
60
62
|
|
|
61
63
|
INPUT=$(cat)
|
|
62
64
|
USER_MESSAGE=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
|
|
65
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
66
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
63
67
|
|
|
64
68
|
if [ -z "$USER_MESSAGE" ] || [ ${#USER_MESSAGE} -lt 10 ]; then
|
|
65
69
|
exit 0
|
|
@@ -67,16 +71,126 @@ fi
|
|
|
67
71
|
|
|
68
72
|
QUERY="${USER_MESSAGE:0:500}"
|
|
69
73
|
|
|
74
|
+
# --- Image detection ---
|
|
75
|
+
# Collect up to 3 images from two sources:
|
|
76
|
+
# 1. Claude Code image cache (pasted/dropped images, detected via marker file)
|
|
77
|
+
# 2. File paths mentioned in the message text
|
|
78
|
+
IMAGE_PATHS=()
|
|
79
|
+
|
|
80
|
+
# Prong 1: Claude Code image cache — pasted images cached in ~/.claude/image-cache/{conversation_id}/
|
|
81
|
+
# The conversation UUID comes from transcript_path (basename minus .jsonl), NOT session_id.
|
|
82
|
+
CONV_ID=""
|
|
83
|
+
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
84
|
+
CONV_ID=$(basename "$TRANSCRIPT_PATH" .jsonl)
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Try conversation ID first, fall back to session_id
|
|
88
|
+
IMAGE_CACHE=""
|
|
89
|
+
if [ -n "$CONV_ID" ] && [ -d "$HOME/.claude/image-cache/$CONV_ID" ]; then
|
|
90
|
+
IMAGE_CACHE="$HOME/.claude/image-cache/$CONV_ID"
|
|
91
|
+
elif [ -n "$SESSION_ID" ] && [ -d "$HOME/.claude/image-cache/$SESSION_ID" ]; then
|
|
92
|
+
IMAGE_CACHE="$HOME/.claude/image-cache/$SESSION_ID"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if [ -n "$IMAGE_CACHE" ]; then
|
|
96
|
+
# Track seen images via marker file to detect newly pasted ones.
|
|
97
|
+
# Each image is numbered sequentially (1.png, 2.png, ...).
|
|
98
|
+
MARKER="/tmp/memento-img-seen-$(echo "$IMAGE_CACHE" | md5sum | cut -c1-16)"
|
|
99
|
+
LAST_SEEN=0
|
|
100
|
+
if [ -f "$MARKER" ]; then
|
|
101
|
+
LAST_SEEN=$(cat "$MARKER" 2>/dev/null || echo 0)
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
CURRENT_COUNT=$(ls "$IMAGE_CACHE"/*.png 2>/dev/null | wc -l)
|
|
105
|
+
|
|
106
|
+
if [ "$CURRENT_COUNT" -gt "$LAST_SEEN" ]; then
|
|
107
|
+
# New images detected — grab the ones we haven't seen
|
|
108
|
+
for i in $(seq $((LAST_SEEN + 1)) "$CURRENT_COUNT"); do
|
|
109
|
+
if [ -f "$IMAGE_CACHE/$i.png" ] && [ ${#IMAGE_PATHS[@]} -lt 3 ]; then
|
|
110
|
+
IMAGE_PATHS+=("$IMAGE_CACHE/$i.png")
|
|
111
|
+
fi
|
|
112
|
+
done
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Update marker to current count
|
|
116
|
+
echo "$CURRENT_COUNT" > "$MARKER"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Prong 2: File paths in message text (explicit image references)
|
|
120
|
+
if [ ${#IMAGE_PATHS[@]} -lt 3 ]; then
|
|
121
|
+
REMAINING_SLOTS=$(( 3 - ${#IMAGE_PATHS[@]} ))
|
|
122
|
+
while IFS= read -r img; do
|
|
123
|
+
if [ -f "$img" ]; then
|
|
124
|
+
IMAGE_PATHS+=("$img")
|
|
125
|
+
fi
|
|
126
|
+
done < <(echo "$USER_MESSAGE" | grep -oE '(/[^ ]+\.(jpg|jpeg|png|gif|webp))' | head -"$REMAINING_SLOTS")
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Build images JSON array (base64-encoded, downscaled to 224x224)
|
|
130
|
+
IMAGES_JSON=""
|
|
131
|
+
if [ ${#IMAGE_PATHS[@]} -gt 0 ] && command -v convert &>/dev/null; then
|
|
132
|
+
IMAGES_JSON=$(python3 -c "
|
|
133
|
+
import subprocess, base64, json, sys, os
|
|
134
|
+
|
|
135
|
+
paths = sys.argv[1:]
|
|
136
|
+
images = []
|
|
137
|
+
for p in paths:
|
|
138
|
+
if not os.path.isfile(p):
|
|
139
|
+
continue
|
|
140
|
+
ext = os.path.splitext(p)[1].lower()
|
|
141
|
+
mime_map = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp'}
|
|
142
|
+
mimetype = mime_map.get(ext, 'image/jpeg')
|
|
143
|
+
try:
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
['convert', p, '-resize', '224x224^', '-gravity', 'center', '-extent', '224x224', '-quality', '80', 'jpeg:-'],
|
|
146
|
+
capture_output=True, timeout=5
|
|
147
|
+
)
|
|
148
|
+
if result.returncode == 0 and len(result.stdout) > 0:
|
|
149
|
+
b64 = base64.b64encode(result.stdout).decode()
|
|
150
|
+
images.append({'data': b64, 'mimetype': 'image/jpeg'})
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
if images:
|
|
155
|
+
print(json.dumps(images))
|
|
156
|
+
else:
|
|
157
|
+
print('')
|
|
158
|
+
" "${IMAGE_PATHS[@]}" 2>/dev/null)
|
|
159
|
+
fi
|
|
160
|
+
|
|
70
161
|
# Toast: start retrieving
|
|
71
162
|
"$TOAST" memento "⏳ Retrieving memories..." &>/dev/null
|
|
72
163
|
|
|
164
|
+
# Build request body with optional images
|
|
165
|
+
REQUEST_BODY=$(python3 -c "
|
|
166
|
+
import json, sys
|
|
167
|
+
|
|
168
|
+
message = sys.argv[1]
|
|
169
|
+
images_json = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
170
|
+
|
|
171
|
+
body = {
|
|
172
|
+
'message': message,
|
|
173
|
+
'include': ['memories', 'skip_list']
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if images_json:
|
|
177
|
+
try:
|
|
178
|
+
images = json.loads(images_json)
|
|
179
|
+
if images:
|
|
180
|
+
body['images'] = images
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
print(json.dumps(body))
|
|
185
|
+
" "$QUERY" "$IMAGES_JSON")
|
|
186
|
+
|
|
73
187
|
# Call Memento SaaS /v1/context
|
|
74
188
|
SAAS_OUTPUT=$(curl -s --max-time 8 \
|
|
75
189
|
-X POST \
|
|
76
190
|
-H "Authorization: Bearer $MEMENTO_KEY" \
|
|
77
191
|
-H "X-Memento-Workspace: $MEMENTO_WS" \
|
|
78
192
|
-H "Content-Type: application/json" \
|
|
79
|
-
-d "
|
|
193
|
+
-d "$REQUEST_BODY" \
|
|
80
194
|
"$MEMENTO_API/v1/context" 2>/dev/null \
|
|
81
195
|
| python3 -c "
|
|
82
196
|
import json, sys
|
package/src/cli.js
CHANGED
|
@@ -100,7 +100,7 @@ function httpsPost(url, body) {
|
|
|
100
100
|
const INSTRUCTIONS_BLOB = `## Memento Protocol
|
|
101
101
|
|
|
102
102
|
Working memory is managed by Memento. MCP tools available:
|
|
103
|
-
\`
|
|
103
|
+
\`memento_remember\`, \`memento_recall\`, \`memento_item_list\`,
|
|
104
104
|
\`memento_skip_add\`, \`memento_skip_check\`.
|
|
105
105
|
|
|
106
106
|
**Memory discipline — notes are instructions, not logs.**
|
|
@@ -108,7 +108,7 @@ Write: "Skip X until condition Y" — not "checked X, it was quiet."
|
|
|
108
108
|
Every memory must answer: could a future agent with zero context
|
|
109
109
|
read this and know exactly what to do?
|
|
110
110
|
|
|
111
|
-
Use \`
|
|
111
|
+
Use \`memento_remember\` when you learn something worth keeping.
|
|
112
112
|
Use \`memento_skip_add\` for things to explicitly not re-investigate.
|
|
113
113
|
Use \`memento_recall\` to search memories by keyword or tag.
|
|
114
114
|
Hooks run automatically — recall before responses, distillation
|
|
@@ -359,7 +359,7 @@ async function runInit(flags = {}) {
|
|
|
359
359
|
console.log("\nOptional features:");
|
|
360
360
|
enableImages = await askYesNo(
|
|
361
361
|
rl,
|
|
362
|
-
" Enable image attachments? (attach images to memories via
|
|
362
|
+
" Enable image attachments? (attach images to memories via memento_remember)",
|
|
363
363
|
false,
|
|
364
364
|
);
|
|
365
365
|
enableIdentity = await askYesNo(
|
package/src/index.js
CHANGED
|
@@ -169,11 +169,11 @@ Sections: active_work, standing_decisions, skip_list, session_notes.`,
|
|
|
169
169
|
);
|
|
170
170
|
|
|
171
171
|
// ---------------------------------------------------------------------------
|
|
172
|
-
// Tool:
|
|
172
|
+
// Tool: memento_remember
|
|
173
173
|
// ---------------------------------------------------------------------------
|
|
174
174
|
|
|
175
175
|
server.tool(
|
|
176
|
-
"
|
|
176
|
+
"memento_remember",
|
|
177
177
|
`Store a discrete memory — a fact, decision, observation, or instruction — with tags and optional expiration.
|
|
178
178
|
|
|
179
179
|
IMPORTANT: Write memories as instructions, not logs.
|
|
@@ -257,11 +257,13 @@ Use tags generously — they power recall. Set expiration for time-sensitive fac
|
|
|
257
257
|
|
|
258
258
|
server.tool(
|
|
259
259
|
"memento_recall",
|
|
260
|
-
`Search stored memories by keyword, tag, or
|
|
260
|
+
`Search stored memories by keyword, tag, type, or image similarity. Use this before starting work on any topic — someone may have already figured it out.
|
|
261
261
|
|
|
262
|
-
Results are ranked by relevance (keyword match + recency + access frequency). Each recall increments the memory's access count, reinforcing important memories and letting unused ones decay naturally
|
|
262
|
+
Results are ranked by relevance (keyword match + semantic similarity + recency + access frequency). Each recall increments the memory's access count, reinforcing important memories and letting unused ones decay naturally.
|
|
263
|
+
|
|
264
|
+
Supports image search: provide image_path to find visually similar memories. Can combine text query + image for multi-modal search.`,
|
|
263
265
|
{
|
|
264
|
-
query: z.string().describe("Search query (matched against memory content)"),
|
|
266
|
+
query: z.string().optional().describe("Search query (matched against memory content). Required unless image_path is provided."),
|
|
265
267
|
tags: z.array(z.string()).optional().describe("Filter by tags (matches any)"),
|
|
266
268
|
type: z
|
|
267
269
|
.string()
|
|
@@ -269,9 +271,57 @@ Results are ranked by relevance (keyword match + recency + access frequency). Ea
|
|
|
269
271
|
.describe("Filter by type: fact, decision, observation, instruction"),
|
|
270
272
|
limit: z.number().optional().describe("Max results (default: 10)"),
|
|
271
273
|
workspace: z.string().optional().describe('Omit to search your own workspace. Set to a workspace name (e.g. "fathom") to search that workspace instead.'),
|
|
274
|
+
image_path: z.string().optional().describe(
|
|
275
|
+
"Path to an image file to search by visual similarity. Can combine with query for multi-modal search."
|
|
276
|
+
),
|
|
272
277
|
},
|
|
273
|
-
async ({ query, tags, type, limit, workspace }) => {
|
|
274
|
-
|
|
278
|
+
async ({ query, tags, type, limit, workspace, image_path }) => {
|
|
279
|
+
if (!query && !image_path) {
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: "text", text: 'At least one of "query" or "image_path" is required.' }],
|
|
282
|
+
isError: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Process image if provided
|
|
287
|
+
let images;
|
|
288
|
+
if (image_path) {
|
|
289
|
+
const MIME_MAP = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp" };
|
|
290
|
+
const ext = path.extname(image_path).toLowerCase();
|
|
291
|
+
const mimetype = MIME_MAP[ext];
|
|
292
|
+
if (!mimetype) {
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: "text", text: `Unsupported image format: ${ext}. Allowed: .jpg, .jpeg, .png, .gif, .webp` }],
|
|
295
|
+
isError: true,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let buffer;
|
|
300
|
+
try {
|
|
301
|
+
buffer = fs.readFileSync(image_path);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: `Cannot read image: ${err.message}` }],
|
|
305
|
+
isError: true,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Downscale to 224x224 via sharp for efficient embedding
|
|
310
|
+
try {
|
|
311
|
+
const sharp = (await import("sharp")).default;
|
|
312
|
+
buffer = await sharp(buffer)
|
|
313
|
+
.resize(224, 224, { fit: "cover" })
|
|
314
|
+
.jpeg({ quality: 80 })
|
|
315
|
+
.toBuffer();
|
|
316
|
+
} catch {
|
|
317
|
+
// If sharp fails, send the original — Nomic resizes internally anyway
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const data = buffer.toString("base64");
|
|
321
|
+
images = [{ data, mimetype: "image/jpeg" }];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = await storage.recallMemories(null, { query, tags, type, limit, workspace, images });
|
|
275
325
|
|
|
276
326
|
if (result._raw) {
|
|
277
327
|
return {
|
package/src/storage/hosted.js
CHANGED
|
@@ -108,7 +108,22 @@ export class HostedStorageAdapter extends StorageInterface {
|
|
|
108
108
|
return { _raw: true, text, isError: false };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
async recallMemories(_wsPath, { query, tags, type, limit, workspace }) {
|
|
111
|
+
async recallMemories(_wsPath, { query, tags, type, limit, workspace, images }) {
|
|
112
|
+
// Use POST when images are present (GET can't carry binary data)
|
|
113
|
+
if (images?.length > 0) {
|
|
114
|
+
const body = { images };
|
|
115
|
+
if (query) body.query = query;
|
|
116
|
+
if (tags?.length) body.tags = tags;
|
|
117
|
+
if (type) body.type = type;
|
|
118
|
+
if (limit) body.limit = limit;
|
|
119
|
+
// Note: cross-workspace peek not supported for image search
|
|
120
|
+
|
|
121
|
+
const json = await this._fetchJson("POST", "/v1/memories/recall", body);
|
|
122
|
+
if (json.error) return { error: json.error };
|
|
123
|
+
return { _raw: true, text: json.text, memories: json.memories || [], isError: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Existing GET path for text-only recall
|
|
112
127
|
const params = new URLSearchParams({ query, format: "json" });
|
|
113
128
|
if (tags?.length) params.set("tags", tags.join(","));
|
|
114
129
|
if (type) params.set("type", type);
|