memento-mcp 0.3.6 → 0.3.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memento-mcp",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "mcpName": "io.github.myrakrusemark/memento-protocol",
5
5
  "description": "The Memento Protocol — persistent memory for AI agents",
6
6
  "type": "module",
@@ -42,7 +42,7 @@ print('true' if hook.get('enabled', True) else 'false')
42
42
  MEMENTO_WORKSPACE="${MEMENTO_WORKSPACE:-$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('workspace',''))" 2>/dev/null)}"
43
43
 
44
44
  RECALL_LIMIT=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('hooks',{}).get('$HOOK_NAME',{}).get('limit',5))" 2>/dev/null)
45
- RECALL_MAX_LENGTH=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('hooks',{}).get('$HOOK_NAME',{}).get('maxLength',200))" 2>/dev/null)
45
+ RECALL_MAX_LENGTH=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('hooks',{}).get('$HOOK_NAME',{}).get('maxLength',120))" 2>/dev/null)
46
46
  fi
47
47
  # --- End config block ---
48
48
 
@@ -92,34 +92,24 @@ try:
92
92
  data = json.load(sys.stdin)
93
93
  lines = []
94
94
  count = 0
95
+ abbrev = {'instruction':'instr','observation':'obs','decision':'dec','preference':'pref'}
95
96
 
96
97
  memories = data.get('memories', {}).get('matches', [])
97
98
  if memories:
98
99
  for m in memories[:${RECALL_LIMIT:-5}]:
99
- tags = m.get('tags', [])
100
- tag_str = f' [{\", \".join(tags)}]' if tags else ''
101
- content = m['content'][:${RECALL_MAX_LENGTH:-200}]
102
- score = m.get('score', '?')
103
- date_str = f' {m[\"created_at\"][:10]}' if m.get('created_at') else ''
104
- lines.append(f' {m[\"id\"]} ({m[\"type\"]}, {score}{date_str}){tag_str} — {content}')
100
+ content = m['content'][:${RECALL_MAX_LENGTH:-120}]
101
+ t = abbrev.get(m['type'], m['type'])
102
+ lines.append(f' · {content} [{m[\"id\"]} {t}]')
105
103
  count += 1
106
104
 
107
105
  skip_matches = data.get('skip_matches', [])
108
106
  if skip_matches:
109
- lines.append('')
110
- lines.append('SKIP LIST WARNINGS:')
111
107
  for s in skip_matches:
112
- lines.append(f' ⚠ SKIP: {s[\"item\"]} — {s[\"reason\"]} (expires: {s[\"expires\"]})')
108
+ lines.append(f' Skip: {s[\"item\"]} — {s[\"reason\"]} (expires {s[\"expires\"]})')
113
109
 
114
- # Auto-extracted memories
115
110
  extracted = data.get('extracted', [])
116
111
  if extracted:
117
- lines.append('')
118
- lines.append('MEMORIES STORED:')
119
- for e in extracted:
120
- etags = [t for t in e.get('tags', []) if t != 'source:auto-extract']
121
- etag_str = f' [{\", \".join(etags)}]' if etags else ''
122
- lines.append(f' {e[\"id\"]} ({e.get(\"type\", \"observation\")}){etag_str} — {e[\"content\"]}')
112
+ lines.append(f' Stored: {len(extracted)} new memories')
123
113
 
124
114
  # Output: recall_count \t extracted_count \t detail
125
115
  extracted_count = len(extracted)
@@ -143,9 +133,11 @@ if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
143
133
  "$TOAST" memento "✓ ${EXTRACTED_COUNT} memories stored" &>/dev/null
144
134
  else
145
135
  "$TOAST" memento "✓ No memories matched" &>/dev/null
146
- exit 0
147
136
  fi
148
- else
137
+ exit 0
138
+ fi
139
+
140
+ if [ "$SAAS_COUNT" != "0" ]; then
149
141
  if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
150
142
  "$TOAST" memento "✓ ${SAAS_COUNT} recalled, ${EXTRACTED_COUNT} stored" &>/dev/null
151
143
  else
@@ -154,16 +146,16 @@ else
154
146
  fi
155
147
 
156
148
  # Build summary line
157
- SUMMARY="Autonomous Recall: ${SAAS_COUNT} memories"
149
+ SUMMARY="Recall (${SAAS_COUNT})"
158
150
  if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
159
- SUMMARY="${SUMMARY}, ${EXTRACTED_COUNT} memories stored"
151
+ SUMMARY="Recall (${SAAS_COUNT}), stored ${EXTRACTED_COUNT}"
160
152
  fi
161
153
 
162
- # Block the Stop so Claude continues the reason becomes Claude's next instruction.
163
- REASON="${SUMMARY} surfaced from your last response.
154
+ # Block the Stop so recalled memories are injected into context.
155
+ # If no memories are relevant, respond with <...> to signal active silence.
156
+ REASON="${SUMMARY}:
164
157
  ${SAAS_DETAIL}
165
-
166
- You have absorbed these memories into context. If any recalled memory is stale, wrong, or overlaps with others — update, delete, or consolidate it now. Otherwise continue naturally."
158
+ Stale or wrong? Update or consolidate."
167
159
 
168
160
  python3 -c "
169
161
  import json, sys
package/src/index.js CHANGED
@@ -433,6 +433,72 @@ This is reconsolidation — like how the brain rebuilds memories on recall. Freq
433
433
  }
434
434
  );
435
435
 
436
+ // ---------------------------------------------------------------------------
437
+ // Tool: memento_extract
438
+ // ---------------------------------------------------------------------------
439
+
440
+ server.tool(
441
+ "memento_extract",
442
+ `Extract memories from a conversation transcript or the conversation buffer using LLM. Use this to manually trigger memory extraction — e.g., to flush the buffer before ending a session, or to extract from a pasted transcript.
443
+
444
+ Two modes:
445
+ - "distill" (default): Full extraction — facts, decisions, instructions, observations. Up to 20 memories.
446
+ - "seeds": Relational/experiential texture — preferences, emotions, sensory details. Up to 3 memories.
447
+
448
+ If no transcript is provided, reads from the conversation buffer (auto-populated by the context hook). Buffer is cleared after extraction unless keep_buffer is true.`,
449
+ {
450
+ transcript: z.string().optional().describe("Raw conversation text. If omitted, reads from the conversation buffer."),
451
+ mode: z.enum(["distill", "seeds"]).optional().describe("Extraction mode (default: distill)"),
452
+ max_memories: z.number().optional().describe("Override max memories to extract"),
453
+ keep_buffer: z.boolean().optional().describe("If true, don't clear the buffer after reading it"),
454
+ },
455
+ async ({ transcript, mode, max_memories, keep_buffer }) => {
456
+ const result = await storage.extractMemories(null, {
457
+ transcript,
458
+ mode,
459
+ max_memories,
460
+ keep_buffer,
461
+ });
462
+
463
+ if (result.error && !result.stored) {
464
+ return {
465
+ content: [{ type: "text", text: `Error: ${result.error}` }],
466
+ isError: true,
467
+ };
468
+ }
469
+
470
+ if (result.error) {
471
+ // Partial success — extraction ran but had issues
472
+ const summary = result.stored?.length > 0
473
+ ? result.stored.map((m) => `- **${m.id}** (${m.type}): ${m.content}`).join("\n")
474
+ : "No memories extracted.";
475
+ return {
476
+ content: [{
477
+ type: "text",
478
+ text: `Extraction completed with warning: ${result.error}\n\n${summary}`,
479
+ }],
480
+ };
481
+ }
482
+
483
+ if (!result.stored || result.stored.length === 0) {
484
+ return {
485
+ content: [{
486
+ type: "text",
487
+ text: `No novel memories found (mode: ${result.mode || mode || "distill"}, source: ${result.source || "unknown"}).`,
488
+ }],
489
+ };
490
+ }
491
+
492
+ const summary = result.stored.map((m) => `- **${m.id}** (${m.type}): ${m.content}`).join("\n");
493
+ return {
494
+ content: [{
495
+ type: "text",
496
+ text: `Extracted ${result.count} memor${result.count === 1 ? "y" : "ies"} (mode: ${result.mode}, source: ${result.source}):\n\n${summary}`,
497
+ }],
498
+ };
499
+ }
500
+ );
501
+
436
502
  // ---------------------------------------------------------------------------
437
503
  // Tool: memento_skip_add
438
504
  // ---------------------------------------------------------------------------
@@ -261,6 +261,19 @@ export class HostedStorageAdapter extends StorageInterface {
261
261
  return res;
262
262
  }
263
263
 
264
+ async extractMemories(_wsPath, { transcript, mode, max_memories, source_tag, keep_buffer }) {
265
+ const body = {};
266
+ if (transcript) body.transcript = transcript;
267
+ if (mode) body.mode = mode;
268
+ if (max_memories) body.max_memories = max_memories;
269
+ if (source_tag) body.source_tag = source_tag;
270
+ if (keep_buffer !== undefined) body.keep_buffer = keep_buffer;
271
+
272
+ const res = await this._fetchJson("POST", "/v1/extract", body);
273
+ if (res.error && !res.stored) return { error: res.error };
274
+ return res;
275
+ }
276
+
264
277
  async getIdentity(_wsPath) {
265
278
  const { text, isError } = await this._fetch("GET", "/v1/identity");
266
279
  if (isError) return { error: text };