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 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 `memento_store`; 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.
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`, `memento_store`, `memento_recall`, `memento_skip_add`, `memento_skip_check`, `memento_health`
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
- > memento_store( # store your first memory
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
- - `memento_store` when you learn something, make a decision, or discover a pattern
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.13",
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 · memento_store. Otherwise <...>."
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 "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"]}" \
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
- \`memento_store\`, \`memento_recall\`, \`memento_item_list\`,
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 \`memento_store\` when you learn something worth keeping.
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 memento_store)",
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: memento_store
172
+ // Tool: memento_remember
173
173
  // ---------------------------------------------------------------------------
174
174
 
175
175
  server.tool(
176
- "memento_store",
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 type. Use this before starting work on any topic — someone may have already figured it out.
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
- const result = await storage.recallMemories(null, { query, tags, type, limit, workspace });
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 {
@@ -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);