memento-mcp 0.1.2 → 0.1.3

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 CHANGED
@@ -104,48 +104,53 @@ Before session ends:
104
104
 
105
105
  ---
106
106
 
107
- ## Hooks
108
-
109
- Hooks automate memory at session boundaries — the agent doesn't need to remember to recall or save. Two production-ready scripts are included in `scripts/`.
107
+ ## The Protocol
110
108
 
111
- ### Setup
109
+ Installing Memento gives your agent memory. *The Protocol* is the system you build around it — orientation after context loss, automatic recall, writing discipline, distillation before context resets, identity that persists across sessions.
112
110
 
113
- 1. **Create a `.env` file** in the repo root (copy from the example):
114
-
115
- ```bash
116
- cp .env.example .env
117
- # Then edit .env with your actual API key and workspace name
118
- ```
111
+ Full guide: **[The Protocol](https://hifathom.com/projects/memento/protocol)** on hifathom.com.
119
112
 
120
- The `.env` file is gitignored. It needs three variables:
113
+ ## Hooks
121
114
 
122
- ```bash
123
- MEMENTO_API_KEY=mp_live_your_key_here
124
- MEMENTO_API_URL=https://memento-api.myrakrusemark.workers.dev
125
- MEMENTO_WORKSPACE=my-project
126
- ```
115
+ Hooks automate memory at session boundaries — recall on every message, distillation before context loss. Three production-ready scripts are included in `scripts/`.
127
116
 
128
- 2. **Make scripts executable** (they should already be, but just in case):
117
+ **Quick setup:**
129
118
 
130
119
  ```bash
131
- chmod +x scripts/*.sh
120
+ cp .env.example .env # add your API key and workspace
121
+ chmod +x scripts/*.sh # make scripts executable
132
122
  ```
133
123
 
134
- 3. **Register in Claude Code settings** — add to `.claude/settings.json` (project-level) or `~/.claude/settings.json` (global):
124
+ Then register in `.claude/settings.json` (project-level) or `~/.claude/settings.json` (global):
135
125
 
136
126
  ```json
137
127
  {
138
128
  "hooks": {
139
129
  "UserPromptSubmit": [
140
130
  {
141
- "command": "/path/to/memento-protocol/scripts/memento-memory-recall.sh",
142
- "timeout": 5000
131
+ "hooks": [{
132
+ "type": "command",
133
+ "command": "/path/to/memento-protocol/scripts/memento-userprompt-recall.sh",
134
+ "timeout": 5000
135
+ }]
136
+ }
137
+ ],
138
+ "Stop": [
139
+ {
140
+ "hooks": [{
141
+ "type": "command",
142
+ "command": "/path/to/memento-protocol/scripts/memento-stop-recall.sh",
143
+ "timeout": 5000
144
+ }]
143
145
  }
144
146
  ],
145
147
  "PreCompact": [
146
148
  {
147
- "command": "/path/to/memento-protocol/scripts/memento-precompact-distill.sh",
148
- "timeout": 30000
149
+ "hooks": [{
150
+ "type": "command",
151
+ "command": "/path/to/memento-protocol/scripts/memento-precompact-distill.sh",
152
+ "timeout": 30000
153
+ }]
149
154
  }
150
155
  ]
151
156
  }
@@ -154,23 +159,13 @@ chmod +x scripts/*.sh
154
159
 
155
160
  Replace `/path/to/memento-protocol` with the actual absolute path to your clone.
156
161
 
157
- ### `memento-memory-recall.sh` (UserPromptSubmit)
158
-
159
- Fires before every agent response. Sends the user's message to the `/v1/context` endpoint, which returns relevant memories and skip list warnings.
160
-
161
- - **Timeout:** 5 seconds (3s API call + overhead)
162
- - **User sees:** "Memento Recall: N memories" in their terminal
163
- - **Model sees:** Full memory details and skip list warnings as injected context (via `additionalContext`)
164
- - **Short messages:** Messages under 10 characters are skipped (greetings, "yes", etc.)
165
-
166
- ### `memento-precompact-distill.sh` (PreCompact)
167
-
168
- Fires before Claude Code compresses the conversation. Parses the full JSONL transcript into readable text, then sends it to `/v1/distill` which extracts key memories, decisions, and observations — so nothing important is lost to compaction.
162
+ | Script | Event | What it does |
163
+ |--------|-------|-------------|
164
+ | `memento-userprompt-recall.sh` | UserPromptSubmit | Recalls memories relevant to the user's message |
165
+ | `memento-stop-recall.sh` | Stop | Recalls memories from the assistant's own output (autonomous work) |
166
+ | `memento-precompact-distill.sh` | PreCompact | Extracts memories from the conversation before context compression |
169
167
 
170
- - **Timeout:** 30 seconds (transcript processing is heavier)
171
- - **User sees:** "Memento Distill: extracted N memories" in their terminal
172
- - **Transcript parsing:** Uses a dedicated parser script if available at `/data/Dropbox/Work/fathom/infrastructure/fathom-mcp/scripts/parse-transcript.sh`. Falls back to direct JSONL extraction (works everywhere, just less polished formatting).
173
- - **Minimum threshold:** Transcripts under 200 characters are skipped.
168
+ See **[scripts/README.md](scripts/README.md)** for detailed documentation on each script, output formats, and how to write your own hooks.
174
169
 
175
170
  ---
176
171
 
@@ -185,6 +180,7 @@ Browse and manage memories visually at [hifathom.com/dashboard](https://hifathom
185
180
  Full reference docs at [hifathom.com/projects/memento](https://hifathom.com/projects/memento):
186
181
 
187
182
  - **[Quick Start](https://hifathom.com/projects/memento/quick-start)** — 5-minute setup guide
183
+ - **[The Protocol](https://hifathom.com/projects/memento/protocol)** — orientation, recall hooks, writing discipline, distillation, identity
188
184
  - **[Core Concepts](https://hifathom.com/projects/memento/concepts)** — memories, working memory, skip lists, identity crystals
189
185
  - **[MCP Tools](https://hifathom.com/projects/memento/mcp-tools)** — full tool reference with parameters and examples
190
186
  - **[API Reference](https://hifathom.com/projects/memento/api)** — REST endpoints, request/response schemas, authentication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memento-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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,144 @@
1
+ # Memento Protocol — Hook Scripts
2
+
3
+ Automation hooks for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that connect the Memento API to session lifecycle events. These scripts make memory automatic — recall on every message, distillation before context loss.
4
+
5
+ ## Naming Convention
6
+
7
+ Scripts follow the pattern `[system]-[hook]-[verb].sh`:
8
+
9
+ | Script | Event | What it does |
10
+ |--------|-------|-------------|
11
+ | `memento-userprompt-recall.sh` | UserPromptSubmit | Recall memories relevant to the user's message |
12
+ | `memento-stop-recall.sh` | Stop | Recall memories relevant to the assistant's own output |
13
+ | `memento-precompact-distill.sh` | PreCompact | Extract memories from the conversation before context compression |
14
+ | `launch-stats.sh` | (manual) | Quick metrics — GitHub stars, npm downloads, signups |
15
+
16
+ ## Setup
17
+
18
+ ### 1. Create a `.env` file
19
+
20
+ ```bash
21
+ cp .env.example .env
22
+ ```
23
+
24
+ Edit `.env` with your credentials:
25
+
26
+ ```bash
27
+ MEMENTO_API_KEY=mp_live_your_key_here
28
+ MEMENTO_API_URL=https://memento-api.myrakrusemark.workers.dev
29
+ MEMENTO_WORKSPACE=my-project
30
+ ```
31
+
32
+ The `.env` file is gitignored. All scripts source it automatically.
33
+
34
+ ### 2. Make scripts executable
35
+
36
+ ```bash
37
+ chmod +x scripts/*.sh
38
+ ```
39
+
40
+ ### 3. Register hooks in Claude Code
41
+
42
+ Add to `.claude/settings.json` (project-level) or `~/.claude/settings.json` (global):
43
+
44
+ ```json
45
+ {
46
+ "hooks": {
47
+ "UserPromptSubmit": [
48
+ {
49
+ "hooks": [{
50
+ "type": "command",
51
+ "command": "/path/to/memento-protocol/scripts/memento-userprompt-recall.sh",
52
+ "timeout": 5000
53
+ }]
54
+ }
55
+ ],
56
+ "Stop": [
57
+ {
58
+ "hooks": [{
59
+ "type": "command",
60
+ "command": "/path/to/memento-protocol/scripts/memento-stop-recall.sh",
61
+ "timeout": 5000
62
+ }]
63
+ }
64
+ ],
65
+ "PreCompact": [
66
+ {
67
+ "hooks": [{
68
+ "type": "command",
69
+ "command": "/path/to/memento-protocol/scripts/memento-precompact-distill.sh",
70
+ "timeout": 30000
71
+ }]
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ ```
77
+
78
+ Replace `/path/to/memento-protocol` with the actual absolute path to your clone.
79
+
80
+ ---
81
+
82
+ ## Script Details
83
+
84
+ ### `memento-userprompt-recall.sh` — UserPromptSubmit
85
+
86
+ Fires before every agent response. Sends the user's message to `/v1/context`, which returns relevant memories and skip list warnings.
87
+
88
+ - **Timeout:** 5 seconds
89
+ - **User sees:** "Memento Recall: N memories"
90
+ - **Model sees:** Full memory content, scores, tags, and skip list warnings (via `additionalContext`)
91
+ - **Short messages:** Messages under 10 characters are skipped (greetings, "yes", etc.)
92
+
93
+ **Output format:** JSON with `systemMessage` (user display) + `hookSpecificOutput.additionalContext` (model context).
94
+
95
+ ### `memento-stop-recall.sh` — Stop
96
+
97
+ Fires after every assistant response. Uses the assistant's own output as the recall query — so memories surface during autonomous work, not just on user messages.
98
+
99
+ - **Timeout:** 5 seconds
100
+ - **User sees:** "Autonomous Recall: N memories"
101
+ - **Model sees:** Full memory content via the `decision: "block"` mechanism — the `reason` field becomes the model's next instruction
102
+ - **Loop prevention:** Checks `stop_hook_active` flag to prevent infinite recall loops
103
+ - **Empty responses:** Skipped when the assistant message is empty
104
+
105
+ **Output format:** JSON with `decision: "block"`, `reason` (model context), and `systemMessage` (user display). The block mechanism is the only way to inject content into model context from a Stop hook — `additionalContext` is not supported for Stop events.
106
+
107
+ **Why this matters:** Without the Stop hook, memories only surface when a human sends a message. For autonomous agents that work independently — running ping routines, doing research, monitoring news — their own memories never get recalled. The Stop hook closes that gap.
108
+
109
+ ### `memento-precompact-distill.sh` — PreCompact
110
+
111
+ Fires before Claude Code compresses the conversation. Parses the full JSONL transcript and sends it to `/v1/distill`, which extracts novel facts, decisions, and observations as stored memories.
112
+
113
+ - **Timeout:** 30 seconds
114
+ - **User sees:** "Memento Distill: extracted N memories"
115
+ - **Minimum threshold:** Transcripts under 200 characters are skipped
116
+ - **Transcript parsing:** Extracts user and assistant text from the JSONL format
117
+
118
+ **Output format:** JSON with `systemMessage` only (informational).
119
+
120
+ **Why this matters:** Context compaction destroys information. Without distillation, anything discussed but not explicitly saved is lost. This hook captures what's novel — deduplicating against existing memories — so nothing important vanishes.
121
+
122
+ ---
123
+
124
+ ## Hook Output Formats
125
+
126
+ Claude Code hooks can output data in several formats. These scripts use two:
127
+
128
+ | Format | Where it appears | Used by |
129
+ |--------|-----------------|---------|
130
+ | `systemMessage` | User's terminal | All scripts |
131
+ | `hookSpecificOutput.additionalContext` | Model context (system-reminder) | UserPromptSubmit recall |
132
+ | `decision: "block"` with `reason` | Model context (next instruction) | Stop recall |
133
+
134
+ The `additionalContext` approach only works for UserPromptSubmit, PreToolUse, and PostToolUse events. For Stop hooks, the `decision: "block"` pattern is the only mechanism that injects content into model context.
135
+
136
+ ## Adding Your Own Hooks
137
+
138
+ Follow the naming convention: `[system]-[hook]-[verb].sh`. Your script receives JSON on stdin with event-specific fields:
139
+
140
+ - **UserPromptSubmit:** `{ "prompt": "user's message" }`
141
+ - **Stop:** `{ "last_assistant_message": "...", "stop_hook_active": false }`
142
+ - **PreCompact:** `{ "transcript_path": "~/.claude/projects/.../conversation.jsonl" }`
143
+
144
+ Source `.env` for credentials, call the Memento API, and output JSON to stdout. Exit 0 for no-op (nothing to report).
@@ -0,0 +1,104 @@
1
+ #!/bin/bash
2
+ # Memento autonomous recall — fires on Stop (after assistant response).
3
+ # Uses the assistant's own output as the recall query, so memories surface
4
+ # during autonomous work, not just when the user sends a message.
5
+
6
+ set -o pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+
10
+ # Source credentials
11
+ if [ -f "$SCRIPT_DIR/../.env" ]; then
12
+ set -a
13
+ source "$SCRIPT_DIR/../.env"
14
+ set +a
15
+ fi
16
+
17
+ MEMENTO_API="${MEMENTO_API_URL:-https://memento-api.myrakrusemark.workers.dev}"
18
+ MEMENTO_KEY="${MEMENTO_API_KEY:?MEMENTO_API_KEY not set}"
19
+ MEMENTO_WS="${MEMENTO_WORKSPACE:-fathom}"
20
+
21
+ INPUT=$(cat)
22
+
23
+ # Prevent infinite loops — if this Stop was triggered by a previous Stop hook, bail
24
+ STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
25
+ if [ "$STOP_ACTIVE" = "true" ]; then
26
+ exit 0
27
+ fi
28
+
29
+ # Get the assistant's last message
30
+ ASSISTANT_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null)
31
+
32
+ if [ -z "$ASSISTANT_MSG" ]; then
33
+ exit 0
34
+ fi
35
+
36
+ # Truncate to first 500 chars for the query
37
+ QUERY="${ASSISTANT_MSG:0:500}"
38
+
39
+ # Call Memento /v1/context
40
+ RESULT=$(curl -s --max-time 3 \
41
+ -X POST \
42
+ -H "Authorization: Bearer $MEMENTO_KEY" \
43
+ -H "X-Memento-Workspace: $MEMENTO_WS" \
44
+ -H "Content-Type: application/json" \
45
+ -d "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"]}" \
46
+ "$MEMENTO_API/v1/context" 2>/dev/null \
47
+ | python3 -c "
48
+ import json, sys
49
+ try:
50
+ data = json.load(sys.stdin)
51
+ lines = []
52
+ count = 0
53
+
54
+ memories = data.get('memories', {}).get('matches', [])
55
+ if memories:
56
+ for m in memories[:5]:
57
+ tags = m.get('tags', [])
58
+ tag_str = f' [{\", \".join(tags)}]' if tags else ''
59
+ content = m['content'][:120]
60
+ score = m.get('score', '?')
61
+ lines.append(f' {m[\"id\"]} ({m[\"type\"]}, {score}){tag_str} — {content}')
62
+ count += 1
63
+
64
+ skip_matches = data.get('skip_matches', [])
65
+ if skip_matches:
66
+ lines.append('')
67
+ lines.append('SKIP LIST WARNINGS:')
68
+ for s in skip_matches:
69
+ lines.append(f' ⚠ SKIP: {s[\"item\"]} — {s[\"reason\"]} (expires: {s[\"expires\"]})')
70
+
71
+ detail = '\n'.join(lines)
72
+ print(f'{count}\t{detail}')
73
+ except Exception:
74
+ print('0\t')
75
+ " 2>/dev/null)
76
+
77
+ # Parse
78
+ SAAS_COUNT=$(echo "$RESULT" | head -1 | cut -f1)
79
+ SAAS_DETAIL=$(echo "$RESULT" | head -1 | cut -f2-)
80
+ REMAINING=$(echo "$RESULT" | tail -n +2)
81
+ if [ -n "$REMAINING" ]; then
82
+ SAAS_DETAIL="$SAAS_DETAIL"$'\n'"$REMAINING"
83
+ fi
84
+
85
+ if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
86
+ exit 0
87
+ fi
88
+
89
+ # Block the Stop so Claude continues — the reason becomes Claude's next instruction.
90
+ # Claude should absorb the memories and respond with <...> (presence without production).
91
+ REASON="Autonomous Recall: ${SAAS_COUNT} memories surfaced from your last response.
92
+ ${SAAS_DETAIL}
93
+
94
+ You have absorbed these memories into context. Respond with <...> — active silence."
95
+
96
+ SUMMARY="Autonomous Recall: ${SAAS_COUNT} memories"
97
+ python3 -c "
98
+ import json, sys
99
+ print(json.dumps({
100
+ 'decision': 'block',
101
+ 'reason': sys.argv[1],
102
+ 'systemMessage': sys.argv[2]
103
+ }))
104
+ " "$REASON" "$SUMMARY"
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import { z } from "zod";
14
14
  import path from "node:path";
15
+ import fs from "node:fs";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import { HostedStorageAdapter } from "./storage/hosted.js";
17
18
 
@@ -197,9 +198,30 @@ Use tags generously — they power recall. Set expiration for time-sensitive fac
197
198
  )
198
199
  .optional()
199
200
  .describe("Links to other memories, items, or vault files"),
201
+ image_path: z
202
+ .string()
203
+ .optional()
204
+ .describe("Local file path to an image to attach to this memory (jpeg, png, gif, webp)"),
200
205
  },
201
- async ({ content, tags, type, expires, linkages }) => {
202
- const result = await storage.storeMemory(null, { content, tags, type, expires, linkages });
206
+ async ({ content, tags, type, expires, linkages, image_path }) => {
207
+ let images;
208
+ if (image_path) {
209
+ const MIME_MAP = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp" };
210
+ const ext = path.extname(image_path).toLowerCase();
211
+ const mimetype = MIME_MAP[ext];
212
+ if (!mimetype) {
213
+ return {
214
+ content: [{ type: "text", text: `Unsupported image format: ${ext}. Allowed: .jpg, .jpeg, .png, .gif, .webp` }],
215
+ isError: true,
216
+ };
217
+ }
218
+ const buffer = fs.readFileSync(image_path);
219
+ const data = buffer.toString("base64");
220
+ const filename = path.basename(image_path);
221
+ images = [{ data, filename, mimetype }];
222
+ }
223
+
224
+ const result = await storage.storeMemory(null, { content, tags, type, expires, linkages, images });
203
225
 
204
226
  if (result._raw) {
205
227
  return {
@@ -278,6 +300,95 @@ Results are ranked by relevance (keyword match + recency + access frequency). Ea
278
300
  }
279
301
  );
280
302
 
303
+ // ---------------------------------------------------------------------------
304
+ // Tool: memento_view_image
305
+ // ---------------------------------------------------------------------------
306
+
307
+ server.tool(
308
+ "memento_view_image",
309
+ `View an image attached to a memory. Use after memento_recall shows a memory has images (look for "Images: [N image(s)]" in results). Returns the image directly in context.
310
+
311
+ Typical flow: memento_recall → see "Images: [2 images]" on a result → memento_view_image with that memory's ID.`,
312
+ {
313
+ memory_id: z.string().describe("The memory ID (from recall results)"),
314
+ filename: z
315
+ .string()
316
+ .optional()
317
+ .describe(
318
+ "Specific filename if the memory has multiple images. If omitted, returns the first image."
319
+ ),
320
+ },
321
+ async ({ memory_id, filename }) => {
322
+ const memory = await storage.getMemory(memory_id);
323
+
324
+ if (memory.error) {
325
+ return {
326
+ content: [{ type: "text", text: `Error fetching memory ${memory_id}: ${memory.error}` }],
327
+ isError: true,
328
+ };
329
+ }
330
+
331
+ const images = memory.images || [];
332
+ if (images.length === 0) {
333
+ return {
334
+ content: [{ type: "text", text: `Memory ${memory_id} has no images.` }],
335
+ };
336
+ }
337
+
338
+ const img = filename
339
+ ? images.find((i) => i.filename === filename)
340
+ : images[0];
341
+
342
+ if (!img) {
343
+ const available = images.map((i) => i.filename).join(", ");
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: `Image "${filename}" not found on memory ${memory_id}. Available: ${available}`,
349
+ },
350
+ ],
351
+ isError: true,
352
+ };
353
+ }
354
+
355
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
356
+ if (img.size > MAX_IMAGE_BYTES) {
357
+ return {
358
+ content: [
359
+ {
360
+ type: "text",
361
+ text: `Image "${img.filename}" is too large (${(img.size / 1024 / 1024).toFixed(1)}MB, max 5MB).`,
362
+ },
363
+ ],
364
+ isError: true,
365
+ };
366
+ }
367
+
368
+ try {
369
+ const base64 = await storage.fetchImage(img.key);
370
+ if (!base64) {
371
+ return {
372
+ content: [{ type: "text", text: `Failed to fetch image "${img.filename}" from storage.` }],
373
+ isError: true,
374
+ };
375
+ }
376
+
377
+ return {
378
+ content: [
379
+ { type: "text", text: `Image from memory ${memory_id}: ${img.filename}` },
380
+ { type: "image", data: base64, mimeType: img.mimetype },
381
+ ],
382
+ };
383
+ } catch (err) {
384
+ return {
385
+ content: [{ type: "text", text: `Error fetching image: ${err.message}` }],
386
+ isError: true,
387
+ };
388
+ }
389
+ }
390
+ );
391
+
281
392
  // ---------------------------------------------------------------------------
282
393
  // Tool: memento_consolidate
283
394
  // ---------------------------------------------------------------------------
@@ -707,7 +818,6 @@ async function main() {
707
818
 
708
819
  // Start the server when run directly (not when imported for testing)
709
820
  // Use fs.realpathSync to resolve npm bin symlinks
710
- import fs from "node:fs";
711
821
  const isMainModule =
712
822
  process.argv[1] &&
713
823
  fs.realpathSync(path.resolve(process.argv[1])) ===
@@ -99,26 +99,44 @@ export class HostedStorageAdapter extends StorageInterface {
99
99
  return { _raw: true, text, isError: false };
100
100
  }
101
101
 
102
- async storeMemory(_wsPath, { content, tags, type, expires, linkages }) {
102
+ async storeMemory(_wsPath, { content, tags, type, expires, linkages, images }) {
103
103
  const body = { content, tags, type, expires };
104
104
  if (linkages) body.linkages = linkages;
105
+ if (images) body.images = images;
105
106
  const { text, isError } = await this._fetch("POST", "/v1/memories", body);
106
107
  if (isError) return { error: text };
107
108
  return { _raw: true, text, isError: false };
108
109
  }
109
110
 
110
111
  async recallMemories(_wsPath, { query, tags, type, limit }) {
111
- const params = new URLSearchParams({ query });
112
+ const params = new URLSearchParams({ query, format: "json" });
112
113
  if (tags?.length) params.set("tags", tags.join(","));
113
114
  if (type) params.set("type", type);
114
115
  if (limit) params.set("limit", String(limit));
115
116
 
116
- const { text, isError } = await this._fetch(
117
+ const json = await this._fetchJson(
117
118
  "GET",
118
119
  `/v1/memories/recall?${params}`
119
120
  );
120
- if (isError) return { error: text };
121
- return { _raw: true, text, isError: false };
121
+ if (json.error) return { error: json.error };
122
+ return { _raw: true, text: json.text, memories: json.memories || [], isError: false };
123
+ }
124
+
125
+ async getMemory(id) {
126
+ return this._fetchJson("GET", `/v1/memories/${id}`);
127
+ }
128
+
129
+ async fetchImage(imageKey) {
130
+ const url = `${this.apiUrl}/v1/images/${imageKey}`;
131
+ const res = await fetch(url, {
132
+ headers: {
133
+ Authorization: `Bearer ${this.apiKey}`,
134
+ "X-Memento-Workspace": this.workspace,
135
+ },
136
+ });
137
+ if (!res.ok) return null;
138
+ const buffer = await res.arrayBuffer();
139
+ return Buffer.from(buffer).toString("base64");
122
140
  }
123
141
 
124
142
  async addSkip(_wsPath, { item, reason, expires }) {