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 +35 -39
- package/package.json +1 -1
- package/scripts/README.md +144 -0
- package/scripts/memento-stop-recall.sh +104 -0
- package/src/index.js +113 -3
- package/src/storage/hosted.js +23 -5
- /package/scripts/{memento-memory-recall.sh → memento-userprompt-recall.sh} +0 -0
package/README.md
CHANGED
|
@@ -104,48 +104,53 @@ Before session ends:
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
## Hooks
|
|
121
114
|
|
|
122
|
-
|
|
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
|
-
|
|
117
|
+
**Quick setup:**
|
|
129
118
|
|
|
130
119
|
```bash
|
|
131
|
-
|
|
120
|
+
cp .env.example .env # add your API key and workspace
|
|
121
|
+
chmod +x scripts/*.sh # make scripts executable
|
|
132
122
|
```
|
|
133
123
|
|
|
134
|
-
|
|
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
|
-
"
|
|
142
|
-
|
|
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
|
-
"
|
|
148
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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])) ===
|
package/src/storage/hosted.js
CHANGED
|
@@ -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
|
|
117
|
+
const json = await this._fetchJson(
|
|
117
118
|
"GET",
|
|
118
119
|
`/v1/memories/recall?${params}`
|
|
119
120
|
);
|
|
120
|
-
if (
|
|
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 }) {
|
|
File without changes
|