memento-mcp 0.3.12 → 0.3.14
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/package.json +2 -1
- package/scripts/memento-userprompt-recall.sh +85 -1
- package/src/cli.js +54 -124
- package/src/index.js +55 -5
- package/src/storage/hosted.js +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memento-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
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": [
|
|
@@ -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,7 @@ 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
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
63
66
|
|
|
64
67
|
if [ -z "$USER_MESSAGE" ] || [ ${#USER_MESSAGE} -lt 10 ]; then
|
|
65
68
|
exit 0
|
|
@@ -67,16 +70,97 @@ fi
|
|
|
67
70
|
|
|
68
71
|
QUERY="${USER_MESSAGE:0:500}"
|
|
69
72
|
|
|
73
|
+
# --- Image detection ---
|
|
74
|
+
# Collect up to 3 images from two sources:
|
|
75
|
+
# 1. Claude Code image cache (pasted/dropped images, recent only)
|
|
76
|
+
# 2. File paths mentioned in the message text
|
|
77
|
+
IMAGE_PATHS=()
|
|
78
|
+
|
|
79
|
+
# Prong 1: Claude Code image cache — pasted images cached in ~/.claude/image-cache/{session_id}/
|
|
80
|
+
if [ -n "$SESSION_ID" ]; then
|
|
81
|
+
IMAGE_CACHE="$HOME/.claude/image-cache/$SESSION_ID"
|
|
82
|
+
if [ -d "$IMAGE_CACHE" ]; then
|
|
83
|
+
while IFS= read -r img; do
|
|
84
|
+
IMAGE_PATHS+=("$img")
|
|
85
|
+
done < <(find "$IMAGE_CACHE" -maxdepth 1 -name "*.png" -newermt '5 seconds ago' 2>/dev/null | head -3)
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Prong 2: File paths in message text (explicit image references)
|
|
90
|
+
if [ ${#IMAGE_PATHS[@]} -lt 3 ]; then
|
|
91
|
+
REMAINING_SLOTS=$(( 3 - ${#IMAGE_PATHS[@]} ))
|
|
92
|
+
while IFS= read -r img; do
|
|
93
|
+
if [ -f "$img" ]; then
|
|
94
|
+
IMAGE_PATHS+=("$img")
|
|
95
|
+
fi
|
|
96
|
+
done < <(echo "$USER_MESSAGE" | grep -oE '(/[^ ]+\.(jpg|jpeg|png|gif|webp))' | head -"$REMAINING_SLOTS")
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Build images JSON array (base64-encoded, downscaled to 224x224)
|
|
100
|
+
IMAGES_JSON=""
|
|
101
|
+
if [ ${#IMAGE_PATHS[@]} -gt 0 ] && command -v convert &>/dev/null; then
|
|
102
|
+
IMAGES_JSON=$(python3 -c "
|
|
103
|
+
import subprocess, base64, json, sys, os
|
|
104
|
+
|
|
105
|
+
paths = sys.argv[1:]
|
|
106
|
+
images = []
|
|
107
|
+
for p in paths:
|
|
108
|
+
if not os.path.isfile(p):
|
|
109
|
+
continue
|
|
110
|
+
ext = os.path.splitext(p)[1].lower()
|
|
111
|
+
mime_map = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp'}
|
|
112
|
+
mimetype = mime_map.get(ext, 'image/jpeg')
|
|
113
|
+
try:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
['convert', p, '-resize', '224x224^', '-gravity', 'center', '-extent', '224x224', '-quality', '80', 'jpeg:-'],
|
|
116
|
+
capture_output=True, timeout=5
|
|
117
|
+
)
|
|
118
|
+
if result.returncode == 0 and len(result.stdout) > 0:
|
|
119
|
+
b64 = base64.b64encode(result.stdout).decode()
|
|
120
|
+
images.append({'data': b64, 'mimetype': 'image/jpeg'})
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
if images:
|
|
125
|
+
print(json.dumps(images))
|
|
126
|
+
else:
|
|
127
|
+
print('')
|
|
128
|
+
" "${IMAGE_PATHS[@]}" 2>/dev/null)
|
|
129
|
+
fi
|
|
130
|
+
|
|
70
131
|
# Toast: start retrieving
|
|
71
132
|
"$TOAST" memento "⏳ Retrieving memories..." &>/dev/null
|
|
72
133
|
|
|
134
|
+
# Build request body with optional images
|
|
135
|
+
REQUEST_BODY=$(python3 -c "
|
|
136
|
+
import json, sys
|
|
137
|
+
|
|
138
|
+
message = sys.argv[1]
|
|
139
|
+
images_json = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
140
|
+
|
|
141
|
+
body = {
|
|
142
|
+
'message': message,
|
|
143
|
+
'include': ['memories', 'skip_list']
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if images_json:
|
|
147
|
+
try:
|
|
148
|
+
images = json.loads(images_json)
|
|
149
|
+
if images:
|
|
150
|
+
body['images'] = images
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
print(json.dumps(body))
|
|
155
|
+
" "$QUERY" "$IMAGES_JSON")
|
|
156
|
+
|
|
73
157
|
# Call Memento SaaS /v1/context
|
|
74
158
|
SAAS_OUTPUT=$(curl -s --max-time 8 \
|
|
75
159
|
-X POST \
|
|
76
160
|
-H "Authorization: Bearer $MEMENTO_KEY" \
|
|
77
161
|
-H "X-Memento-Workspace: $MEMENTO_WS" \
|
|
78
162
|
-H "Content-Type: application/json" \
|
|
79
|
-
-d "
|
|
163
|
+
-d "$REQUEST_BODY" \
|
|
80
164
|
"$MEMENTO_API/v1/context" 2>/dev/null \
|
|
81
165
|
| python3 -c "
|
|
82
166
|
import json, sys
|
package/src/cli.js
CHANGED
|
@@ -120,9 +120,7 @@ before compaction. Trust the hooks. Focus on writing good memories.`;
|
|
|
120
120
|
|
|
121
121
|
const HEADLESS_CMDS = {
|
|
122
122
|
"claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
123
|
-
"codex": (prompt) => ["codex", "exec", prompt],
|
|
124
123
|
"gemini": (prompt) => ["gemini", prompt],
|
|
125
|
-
"opencode": (prompt) => ["opencode", "run", prompt],
|
|
126
124
|
};
|
|
127
125
|
|
|
128
126
|
function buildIntegrationPrompt(blob) {
|
|
@@ -162,13 +160,16 @@ function runAgentHeadless(agentKey, prompt) {
|
|
|
162
160
|
// ---------------------------------------------------------------------------
|
|
163
161
|
|
|
164
162
|
function parseFlags(argv) {
|
|
165
|
-
const flags = { nonInteractive: false, apiKey: null };
|
|
163
|
+
const flags = { nonInteractive: false, apiKey: null, agent: null };
|
|
166
164
|
for (let i = 0; i < argv.length; i++) {
|
|
167
165
|
if (argv[i] === "-y" || argv[i] === "--yes") {
|
|
168
166
|
flags.nonInteractive = true;
|
|
169
167
|
} else if (argv[i] === "--api-key" && argv[i + 1]) {
|
|
170
168
|
flags.apiKey = argv[i + 1];
|
|
171
169
|
i++;
|
|
170
|
+
} else if (argv[i] === "--agent" && argv[i + 1]) {
|
|
171
|
+
flags.agent = argv[i + 1];
|
|
172
|
+
i++;
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
// Also check environment variable
|
|
@@ -244,29 +245,6 @@ function writeMcpJson(cwd) {
|
|
|
244
245
|
return ".mcp.json";
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
function writeCodexToml(cwd) {
|
|
248
|
-
const dir = path.join(cwd, ".codex");
|
|
249
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
250
|
-
const filePath = path.join(dir, "config.toml");
|
|
251
|
-
|
|
252
|
-
let content = "";
|
|
253
|
-
try {
|
|
254
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
255
|
-
} catch {
|
|
256
|
-
/* file doesn't exist */
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Skip if memento section already exists
|
|
260
|
-
if (/\[mcp_servers\.memento\]/.test(content)) {
|
|
261
|
-
return ".codex/config.toml (already configured)";
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const section = `\n[mcp_servers.memento]\ncommand = "npx"\nargs = ["-y", "memento-mcp"]\n`;
|
|
265
|
-
const separator = content && !content.endsWith("\n") ? "\n" : "";
|
|
266
|
-
fs.writeFileSync(filePath, content + separator + section);
|
|
267
|
-
return ".codex/config.toml";
|
|
268
|
-
}
|
|
269
|
-
|
|
270
248
|
function writeGeminiJson(cwd) {
|
|
271
249
|
const dir = path.join(cwd, ".gemini");
|
|
272
250
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -277,22 +255,6 @@ function writeGeminiJson(cwd) {
|
|
|
277
255
|
return ".gemini/settings.json";
|
|
278
256
|
}
|
|
279
257
|
|
|
280
|
-
function writeOpencodeJson(cwd) {
|
|
281
|
-
const filePath = path.join(cwd, "opencode.json");
|
|
282
|
-
const existing = readJsonFile(filePath) || {};
|
|
283
|
-
deepMerge(existing, {
|
|
284
|
-
mcp: {
|
|
285
|
-
memento: {
|
|
286
|
-
type: "local",
|
|
287
|
-
command: ["npx", "-y", "memento-mcp"],
|
|
288
|
-
enabled: true,
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
writeJsonFile(filePath, existing);
|
|
293
|
-
return "opencode.json";
|
|
294
|
-
}
|
|
295
|
-
|
|
296
258
|
const AGENTS = {
|
|
297
259
|
"claude-code": {
|
|
298
260
|
name: "Claude Code",
|
|
@@ -301,38 +263,31 @@ const AGENTS = {
|
|
|
301
263
|
hasHooks: true,
|
|
302
264
|
nextSteps: "Restart Claude Code to activate.",
|
|
303
265
|
},
|
|
304
|
-
codex: {
|
|
305
|
-
name: "OpenAI Codex",
|
|
306
|
-
detect: (cwd) => fs.existsSync(path.join(cwd, ".codex")),
|
|
307
|
-
configWriter: writeCodexToml,
|
|
308
|
-
hasHooks: false,
|
|
309
|
-
nextSteps: "Run `codex` in this directory — memento tools load automatically.",
|
|
310
|
-
},
|
|
311
266
|
gemini: {
|
|
312
267
|
name: "Gemini CLI",
|
|
313
268
|
detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
|
|
314
269
|
configWriter: writeGeminiJson,
|
|
315
|
-
hasHooks:
|
|
270
|
+
hasHooks: true,
|
|
316
271
|
nextSteps: "Run `gemini` in this directory — memento tools load automatically.",
|
|
317
272
|
},
|
|
318
|
-
|
|
319
|
-
name: "
|
|
320
|
-
detect: (
|
|
321
|
-
configWriter:
|
|
273
|
+
manual: {
|
|
274
|
+
name: "I'll set up my agent myself",
|
|
275
|
+
detect: () => false,
|
|
276
|
+
configWriter: () => "(skipped — manual setup)",
|
|
322
277
|
hasHooks: false,
|
|
323
|
-
nextSteps: "
|
|
278
|
+
nextSteps: "Point your agent's MCP config at: npx -y memento-mcp",
|
|
324
279
|
},
|
|
325
280
|
};
|
|
326
281
|
|
|
327
282
|
// Exported for testing
|
|
328
|
-
export { AGENTS, writeMcpJson,
|
|
283
|
+
export { AGENTS, writeMcpJson, writeGeminiJson };
|
|
329
284
|
|
|
330
285
|
// ---------------------------------------------------------------------------
|
|
331
286
|
// CLI
|
|
332
287
|
// ---------------------------------------------------------------------------
|
|
333
288
|
|
|
334
289
|
async function runInit(flags = {}) {
|
|
335
|
-
const { nonInteractive = false, apiKey: flagApiKey = null } = flags;
|
|
290
|
+
const { nonInteractive = false, apiKey: flagApiKey = null, agent: flagAgent = null } = flags;
|
|
336
291
|
const cwd = process.cwd();
|
|
337
292
|
const projectName = path.basename(cwd);
|
|
338
293
|
|
|
@@ -420,16 +375,25 @@ async function runInit(flags = {}) {
|
|
|
420
375
|
|
|
421
376
|
let selectedAgents;
|
|
422
377
|
if (nonInteractive) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
378
|
+
if (flagAgent) {
|
|
379
|
+
// Validate --agent value
|
|
380
|
+
if (!AGENTS[flagAgent]) {
|
|
381
|
+
const valid = Object.keys(AGENTS).join(", ");
|
|
382
|
+
console.error(` Error: unknown agent "${flagAgent}". Valid agents: ${valid}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
selectedAgents = [flagAgent];
|
|
386
|
+
console.log(` Agent: ${AGENTS[flagAgent].name} (--agent flag)`);
|
|
387
|
+
} else {
|
|
388
|
+
// Auto-detect: use first detected agent, or default to claude-code
|
|
389
|
+
selectedAgents = detected.length > 0 ? [detected[0]] : ["claude-code"];
|
|
390
|
+
console.log(` Agent: ${AGENTS[selectedAgents[0]].name} (auto-detected)`);
|
|
391
|
+
}
|
|
426
392
|
} else {
|
|
427
393
|
console.log("\nDetected agents:");
|
|
428
394
|
const markers = {
|
|
429
395
|
"claude-code": ".claude/",
|
|
430
|
-
codex: ".codex/",
|
|
431
396
|
gemini: ".gemini/",
|
|
432
|
-
opencode: "opencode.json",
|
|
433
397
|
};
|
|
434
398
|
for (const key of agentKeys) {
|
|
435
399
|
const agent = AGENTS[key];
|
|
@@ -464,8 +428,6 @@ async function runInit(flags = {}) {
|
|
|
464
428
|
}
|
|
465
429
|
|
|
466
430
|
const hasClaude = selectedAgents.includes("claude-code");
|
|
467
|
-
|
|
468
|
-
// 5. Hooks — if any hook-supporting agent is selected (Claude Code, Gemini CLI)
|
|
469
431
|
const hasGemini = selectedAgents.includes("gemini");
|
|
470
432
|
const hasHookAgent = hasClaude || hasGemini;
|
|
471
433
|
|
|
@@ -531,7 +493,6 @@ async function runInit(flags = {}) {
|
|
|
531
493
|
created.push(".memento.json");
|
|
532
494
|
|
|
533
495
|
// 7. Copy hook scripts — gated on any hook-supporting agent
|
|
534
|
-
const hasCodex = selectedAgents.includes("codex");
|
|
535
496
|
const anyHookEnabled =
|
|
536
497
|
enableUserPrompt || enableStop || enablePreCompact || enableSessionStart;
|
|
537
498
|
|
|
@@ -549,9 +510,6 @@ async function runInit(flags = {}) {
|
|
|
549
510
|
enableSessionStart && "memento-sessionstart-identity.sh",
|
|
550
511
|
].filter(Boolean);
|
|
551
512
|
|
|
552
|
-
// Also copy Codex notify script if Codex is selected
|
|
553
|
-
if (hasCodex) scriptFiles.push("memento-codex-notify.sh");
|
|
554
|
-
|
|
555
513
|
for (const name of scriptFiles) {
|
|
556
514
|
const src = path.join(pkgScriptsDir, name);
|
|
557
515
|
if (!fs.existsSync(src)) continue; // skip if script doesn't exist yet
|
|
@@ -604,31 +562,7 @@ async function runInit(flags = {}) {
|
|
|
604
562
|
}
|
|
605
563
|
}
|
|
606
564
|
|
|
607
|
-
// 8c.
|
|
608
|
-
if (hasCodex) {
|
|
609
|
-
const pkgScriptsDir = path.resolve(__dirname, "..", "scripts");
|
|
610
|
-
const localScriptsDir = path.join(cwd, ".memento", "scripts");
|
|
611
|
-
// Ensure the notify script is copied (may not have been copied above if no hook agent)
|
|
612
|
-
const notifySrc = path.join(pkgScriptsDir, "memento-codex-notify.sh");
|
|
613
|
-
const notifyDest = path.join(localScriptsDir, "memento-codex-notify.sh");
|
|
614
|
-
if (fs.existsSync(notifySrc) && !fs.existsSync(notifyDest)) {
|
|
615
|
-
fs.mkdirSync(localScriptsDir, { recursive: true });
|
|
616
|
-
fs.copyFileSync(notifySrc, notifyDest);
|
|
617
|
-
fs.chmodSync(notifyDest, 0o755);
|
|
618
|
-
}
|
|
619
|
-
const notifyScript = notifyDest;
|
|
620
|
-
const codexTomlPath = path.join(cwd, ".codex", "config.toml");
|
|
621
|
-
let tomlContent = "";
|
|
622
|
-
try { tomlContent = fs.readFileSync(codexTomlPath, "utf-8"); } catch { /* doesn't exist yet */ }
|
|
623
|
-
if (!tomlContent.includes("notify")) {
|
|
624
|
-
const notifyLine = `\nnotify = ["bash", "${notifyScript}"]\n`;
|
|
625
|
-
const separator = tomlContent && !tomlContent.endsWith("\n") ? "\n" : "";
|
|
626
|
-
fs.writeFileSync(codexTomlPath, tomlContent + separator + notifyLine);
|
|
627
|
-
created.push(".codex/config.toml (notify)");
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// 9. Per-agent config files
|
|
565
|
+
// 8c. Per-agent config files
|
|
632
566
|
for (const agentKey of selectedAgents) {
|
|
633
567
|
const agent = AGENTS[agentKey];
|
|
634
568
|
const result = agent.configWriter(cwd);
|
|
@@ -641,16 +575,15 @@ async function runInit(flags = {}) {
|
|
|
641
575
|
if (appendToGitignore(cwd, ".memento/scripts/")) gitignoreUpdated = true;
|
|
642
576
|
if (gitignoreUpdated) created.push(".gitignore (updated)");
|
|
643
577
|
|
|
644
|
-
//
|
|
578
|
+
// 10. Summary
|
|
645
579
|
const labels = {
|
|
646
580
|
".memento.json": "workspace config + credentials",
|
|
647
581
|
".memento/scripts/": "hook scripts (recall + distillation)",
|
|
648
582
|
".claude/settings.local.json": "hooks registered with Claude Code",
|
|
649
583
|
".mcp.json": "MCP server registered (Claude Code)",
|
|
650
|
-
".codex/config.toml": "MCP server registered (Codex)",
|
|
651
|
-
".codex/config.toml (already configured)": "MCP server (Codex, skipped)",
|
|
652
584
|
".gemini/settings.json": "MCP server registered (Gemini CLI)",
|
|
653
|
-
"
|
|
585
|
+
".gemini/settings.json (hooks)": "hooks registered with Gemini CLI",
|
|
586
|
+
"(skipped — manual setup)": "MCP config skipped (manual setup)",
|
|
654
587
|
".gitignore (updated)": "credentials excluded from git",
|
|
655
588
|
};
|
|
656
589
|
const colWidth = Math.max(...created.map((f) => f.length)) + 2;
|
|
@@ -669,6 +602,14 @@ async function runInit(flags = {}) {
|
|
|
669
602
|
}
|
|
670
603
|
console.log(" Your agent will wake up remembering.\n");
|
|
671
604
|
|
|
605
|
+
// Show non-interactive equivalent
|
|
606
|
+
if (!nonInteractive) {
|
|
607
|
+
const parts = ["npx memento-mcp init -y"];
|
|
608
|
+
if (apiKey) parts.push(`--api-key ${apiKey}`);
|
|
609
|
+
parts.push(`--agent ${selectedAgents[0]}`);
|
|
610
|
+
console.log(` Non-interactive equivalent:\n ${parts.join(" ")}\n`);
|
|
611
|
+
}
|
|
612
|
+
|
|
672
613
|
// 12. Auto-integrate agent instructions
|
|
673
614
|
const primaryAgent = selectedAgents[0];
|
|
674
615
|
const prompt = buildIntegrationPrompt(INSTRUCTIONS_BLOB);
|
|
@@ -797,23 +738,6 @@ async function runUpdate() {
|
|
|
797
738
|
}
|
|
798
739
|
}
|
|
799
740
|
|
|
800
|
-
// Codex CLI — ensure notify is configured
|
|
801
|
-
const hasCodex = agents.includes("codex")
|
|
802
|
-
|| fs.existsSync(path.join(cwd, ".codex"));
|
|
803
|
-
if (hasCodex) {
|
|
804
|
-
const notifyScript = path.join(localScriptsDir, "memento-codex-notify.sh");
|
|
805
|
-
const codexTomlPath = path.join(cwd, ".codex", "config.toml");
|
|
806
|
-
let tomlContent = "";
|
|
807
|
-
try { tomlContent = fs.readFileSync(codexTomlPath, "utf-8"); } catch { /* doesn't exist yet */ }
|
|
808
|
-
if (!tomlContent.includes("notify") && fs.existsSync(notifyScript)) {
|
|
809
|
-
const notifyLine = `\nnotify = ["bash", "${notifyScript}"]\n`;
|
|
810
|
-
const separator = tomlContent && !tomlContent.endsWith("\n") ? "\n" : "";
|
|
811
|
-
fs.mkdirSync(path.dirname(codexTomlPath), { recursive: true });
|
|
812
|
-
fs.writeFileSync(codexTomlPath, tomlContent + separator + notifyLine);
|
|
813
|
-
registeredHooks.push("Codex CLI → .codex/config.toml (notify)");
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
741
|
console.log(`\n ✓ Memento hooks updated to v${pkgVersion}\n`);
|
|
818
742
|
console.log(" Updated scripts:");
|
|
819
743
|
for (const name of updated) {
|
|
@@ -882,16 +806,22 @@ if (isMain) {
|
|
|
882
806
|
Memento Protocol CLI
|
|
883
807
|
|
|
884
808
|
Usage:
|
|
885
|
-
npx memento-mcp init
|
|
886
|
-
npx memento-mcp init -y
|
|
887
|
-
npx memento-mcp init
|
|
888
|
-
npx memento-mcp
|
|
889
|
-
npx memento-mcp
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
809
|
+
npx memento-mcp init Set up Memento in the current project
|
|
810
|
+
npx memento-mcp init -y Non-interactive setup (uses defaults)
|
|
811
|
+
npx memento-mcp init -y --agent gemini Non-interactive with specific agent
|
|
812
|
+
npx memento-mcp init --api-key KEY Provide API key (skips signup)
|
|
813
|
+
npx memento-mcp update Update hook scripts to latest version
|
|
814
|
+
npx memento-mcp Start the MCP server (used by .mcp.json)
|
|
815
|
+
|
|
816
|
+
Flags:
|
|
817
|
+
-y, --yes Non-interactive mode (uses defaults, for CI/scripting)
|
|
818
|
+
--api-key KEY Provide API key (skips signup prompt)
|
|
819
|
+
--agent AGENT Select agent: claude-code, gemini, or manual
|
|
820
|
+
|
|
821
|
+
The -y flag enables fully non-interactive setup. Combine with --agent
|
|
822
|
+
to select a specific agent (defaults to auto-detect, then claude-code).
|
|
823
|
+
|
|
824
|
+
Supports Claude Code, Gemini CLI, and manual setup for any agent.
|
|
895
825
|
`);
|
|
896
826
|
process.exit(1);
|
|
897
827
|
}
|
package/src/index.js
CHANGED
|
@@ -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);
|