mevoric 2.1.0 → 2.3.0

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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/server.mjs +223 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevoric",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Unified memory + agent bridge for Claude Code. Semantic recall, cross-tab messaging, session checkpoints — one MCP server.",
5
5
  "type": "module",
6
6
  "main": "server.mjs",
@@ -34,6 +34,7 @@
34
34
  "LICENSE"
35
35
  ],
36
36
  "dependencies": {
37
+ "@anthropic-ai/claude-agent-sdk": "^0.2.54",
37
38
  "@modelcontextprotocol/sdk": "^1.0.0",
38
39
  "node-notifier": "^10.0.1"
39
40
  }
package/server.mjs CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  } from '@modelcontextprotocol/sdk/types.js';
25
25
  import {
26
26
  existsSync, mkdirSync, writeFileSync, readFileSync,
27
- readdirSync, unlinkSync, renameSync
27
+ readdirSync, unlinkSync, renameSync, appendFileSync
28
28
  } from 'fs';
29
29
  import { resolve, dirname } from 'path';
30
30
  import { randomBytes, randomUUID } from 'crypto';
@@ -67,6 +67,10 @@ const MEMORY_SERVER_URL = process.env.MEVORIC_SERVER_URL
67
67
  // Session-level conversation ID for memory tools
68
68
  const sessionConversationId = randomUUID();
69
69
 
70
+ // Cache retrieved memories so judge_memories can evaluate them locally
71
+ // Map<conversationId, [{mem0_id, memory, score}]>
72
+ const retrievalCache = new Map();
73
+
70
74
  // Write conversation ID to temp file so external tools can reference it
71
75
  const CONVID_FILE = resolve(tmpdir(), 'mevoric-convid');
72
76
  try { writeFileSync(CONVID_FILE, sessionConversationId); } catch {}
@@ -683,13 +687,19 @@ async function handleRetrieveMemories(args) {
683
687
  .filter(m => (m.score || 0) >= SCORE_THRESHOLD)
684
688
  .slice(0, MAX_RESULTS)
685
689
  .map((m, i) => ({
690
+ mem0_id: m.mem0_id,
686
691
  memory: m.memory,
687
692
  score: Math.round((m.score || 0) * 1000) / 1000,
688
693
  rank: i + 1
689
694
  }));
690
695
 
696
+ // Cache for judge_memories (includes mem0_id for verdict posting)
697
+ if (filtered.length > 0) {
698
+ retrievalCache.set(sessionConversationId, filtered);
699
+ }
700
+
691
701
  return {
692
- memories: filtered,
702
+ memories: filtered.map(m => ({ memory: m.memory, score: m.score, rank: m.rank })),
693
703
  conversation_id: sessionConversationId,
694
704
  ...(filtered.length === 0 && raw.length > 0
695
705
  ? { note: `${raw.length} memories found but none above relevance threshold (${SCORE_THRESHOLD})` }
@@ -727,6 +737,134 @@ async function handleStoreConversation(args) {
727
737
  }
728
738
  }
729
739
 
740
+ const JUDGE_PROMPT = `You are evaluating whether a retrieved memory helped answer a user's question.
741
+
742
+ USER QUERY:
743
+ {query}
744
+
745
+ ASSISTANT RESPONSE:
746
+ {response}
747
+
748
+ RETRIEVED MEMORY:
749
+ {memory}
750
+
751
+ EVALUATION — walk through these steps:
752
+
753
+ Step 1: Find evidence. Quote any part of the response that uses information from this memory.
754
+ Did you find evidence? Answer YES or NO.
755
+
756
+ Step 2:
757
+ If YES (memory was used): Is the information in the memory correct based on the response?
758
+ - If correct → verdict: "strengthen"
759
+ - If incorrect → verdict: "correct", and provide the corrected text
760
+
761
+ If NO (memory was NOT used): Why wasn't it used?
762
+ - If the memory is irrelevant to the query → verdict: "drop"
763
+ - If the memory is related but wasn't needed → verdict: "weaken"
764
+
765
+ Return JSON only:
766
+ {
767
+ "evidence": "quote from response, or 'none'",
768
+ "reasoning": "your step-by-step reasoning",
769
+ "verdict": "strengthen" | "weaken" | "correct" | "drop",
770
+ "confidence": 0.0 to 1.0,
771
+ "corrected_content": "only if verdict is correct, otherwise null"
772
+ }`;
773
+
774
+ const CONFIDENCE_THRESHOLD = 0.85;
775
+
776
+ function getCleanEnv() {
777
+ const env = { ...process.env };
778
+ delete env.ANTHROPIC_API_KEY;
779
+ delete env.CLAUDECODE;
780
+ return env;
781
+ }
782
+
783
+ async function judgeOneMemory(queryText, responseText, memoryContent) {
784
+ const prompt = JUDGE_PROMPT
785
+ .replace('{query}', queryText)
786
+ .replace('{response}', responseText)
787
+ .replace('{memory}', memoryContent);
788
+
789
+ let claudeQuery;
790
+ try {
791
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
792
+ claudeQuery = sdk.query;
793
+ } catch {
794
+ throw new Error('Claude Agent SDK not available');
795
+ }
796
+
797
+ let fullText = '';
798
+ for await (const ev of claudeQuery({
799
+ prompt,
800
+ options: {
801
+ maxTurns: 1,
802
+ allowedTools: [],
803
+ model: 'haiku',
804
+ permissionMode: 'bypassPermissions',
805
+ allowDangerouslySkipPermissions: true,
806
+ persistSession: false,
807
+ env: getCleanEnv(),
808
+ }
809
+ })) {
810
+ if (ev?.type === 'assistant' && ev.message?.content) {
811
+ for (const block of ev.message.content) {
812
+ if (block.type === 'text' && block.text) fullText += block.text;
813
+ }
814
+ }
815
+ if (ev?.type === 'result' && ev.text) fullText = ev.text;
816
+ }
817
+
818
+ let cleaned = fullText.trim();
819
+ if (cleaned.startsWith('```')) cleaned = cleaned.split('\n', 2)[1] ? cleaned.slice(cleaned.indexOf('\n') + 1) : cleaned.slice(3);
820
+ if (cleaned.endsWith('```')) cleaned = cleaned.slice(0, -3);
821
+ cleaned = cleaned.trim();
822
+
823
+ return JSON.parse(cleaned);
824
+ }
825
+
826
+ async function runJudgeInBackground(memories, queryText, responseText, convId, userId) {
827
+ let judged = 0;
828
+ let failed = 0;
829
+
830
+ for (const mem of memories) {
831
+ try {
832
+ const judgment = await judgeOneMemory(queryText, responseText, mem.memory);
833
+ const verdict = judgment.verdict || 'weaken';
834
+ const confidence = parseFloat(judgment.confidence) || 0;
835
+ const note = judgment.reasoning || '';
836
+ const corrected = judgment.corrected_content || null;
837
+
838
+ // Confidence guard: strengthen always passes, everything else needs >= 85%
839
+ const actionTaken = (verdict === 'strengthen' || confidence >= CONFIDENCE_THRESHOLD)
840
+ ? 'logged' : `blocked_low_confidence (${Math.round(confidence * 100)}%)`;
841
+
842
+ // POST verdict to Newcode for storage
843
+ try {
844
+ await memoryFetch('/api/verdict', {
845
+ mem0_id: mem.mem0_id,
846
+ conversation_id: convId,
847
+ user_id: userId,
848
+ verdict,
849
+ judge_note: note,
850
+ corrected_content: corrected,
851
+ action_taken: actionTaken,
852
+ }, 10000);
853
+ } catch {
854
+ // Storage failed but judgment succeeded — log locally
855
+ console.error(`[Mevoric] Failed to store verdict for ${mem.mem0_id}`);
856
+ }
857
+
858
+ judged++;
859
+ } catch (err) {
860
+ failed++;
861
+ console.error(`[Mevoric] Judge failed for memory: ${err.message}`);
862
+ }
863
+ }
864
+
865
+ console.error(`[Mevoric] Judge complete: ${judged} judged, ${failed} failed out of ${memories.length}`);
866
+ }
867
+
730
868
  async function handleJudgeMemories(args) {
731
869
  const convId = args.conversation_id || sessionConversationId;
732
870
  const queryText = args.query_text;
@@ -736,18 +874,22 @@ async function handleJudgeMemories(args) {
736
874
  }
737
875
  const userId = args.user_id || 'lloyd';
738
876
 
739
- try {
740
- const data = await memoryFetch('/feedback', {
741
- conversation_id: convId,
742
- user_id: userId,
743
- query_text: queryText,
744
- response_text: responseText
745
- }, 30000);
746
-
747
- return { status: data.status || 'judging', conversation_id: convId };
748
- } catch (err) {
749
- return { error: err.message, conversation_id: convId };
877
+ // Get cached memories from this conversation's retrieve call
878
+ const memories = retrievalCache.get(convId);
879
+ if (!memories || memories.length === 0) {
880
+ return { status: 'skipped', reason: 'No memories retrieved in this conversation to judge', conversation_id: convId };
750
881
  }
882
+
883
+ // Run judging in background — don't block the tool response
884
+ runJudgeInBackground(memories, queryText, responseText, convId, userId)
885
+ .catch(err => console.error(`[Mevoric] Background judge error: ${err.message}`));
886
+
887
+ return {
888
+ status: 'judging',
889
+ count: memories.length,
890
+ conversation_id: convId,
891
+ note: 'Evaluating locally via Claude SDK. Verdicts will be posted to Newcode.'
892
+ };
751
893
  }
752
894
 
753
895
  // ============================================================
@@ -1095,8 +1237,34 @@ async function runCapturePrompt() {
1095
1237
  const clean = stripSystemTags(prompt);
1096
1238
  if (clean.length < 5) process.exit(0);
1097
1239
 
1240
+ // Append to JSONL file (one line per prompt, accumulates across the session)
1098
1241
  const tmp = tmpdir();
1099
- writeFileSync(resolve(tmp, `mevoric-prompt-${sessionId}`), clean, 'utf8');
1242
+ const entry = JSON.stringify({ ts: Date.now(), prompt: clean });
1243
+ appendFileSync(resolve(tmp, `mevoric-prompt-${sessionId}`), entry + '\n', 'utf8');
1244
+
1245
+ // Fire-and-forget POST to /ingest so this prompt is saved even if session crashes
1246
+ try {
1247
+ let convId = '';
1248
+ try { convId = readFileSync(resolve(tmp, 'mevoric-convid'), 'utf8').trim(); } catch {}
1249
+ if (!convId) convId = sessionId;
1250
+
1251
+ const project = process.cwd().split(/[\\/]/).pop();
1252
+ const controller = new AbortController();
1253
+ const timer = setTimeout(() => controller.abort(), 3000);
1254
+ await fetch(`${MEMORY_SERVER_URL}/ingest`, {
1255
+ method: 'POST',
1256
+ headers: { 'Content-Type': 'application/json' },
1257
+ body: JSON.stringify({
1258
+ messages: [{ role: 'user', content: clean.slice(0, 10000) }],
1259
+ user_id: 'lloyd',
1260
+ conversation_id: convId,
1261
+ project
1262
+ }),
1263
+ signal: controller.signal
1264
+ });
1265
+ clearTimeout(timer);
1266
+ } catch {} // Best-effort — prompt is still in JSONL file for Stop hook fallback
1267
+
1100
1268
  process.exit(0);
1101
1269
  }
1102
1270
 
@@ -1119,13 +1287,26 @@ async function runIngest() {
1119
1287
  const assistantMsg = data.last_assistant_message || '';
1120
1288
  if (!sessionId || !assistantMsg) process.exit(0);
1121
1289
 
1122
- // Read user prompt saved by --capture-prompt
1290
+ // Read ALL user prompts saved by --capture-prompt (JSONL format, one per line)
1123
1291
  const tmp = tmpdir();
1124
1292
  const promptPath = resolve(tmp, `mevoric-prompt-${sessionId}`);
1125
- let userMsg = '';
1293
+ let allPrompts = [];
1126
1294
  try {
1127
- userMsg = readFileSync(promptPath, 'utf8');
1295
+ const raw = readFileSync(promptPath, 'utf8');
1296
+ allPrompts = raw.split('\n').filter(Boolean).map(line => {
1297
+ try { return JSON.parse(line); } catch { return null; }
1298
+ }).filter(Boolean);
1128
1299
  } catch {}
1300
+ // Fallback for old plain-text format (pre-JSONL)
1301
+ if (allPrompts.length === 0) {
1302
+ try {
1303
+ const plain = readFileSync(promptPath, 'utf8');
1304
+ if (plain && plain.length >= 5) allPrompts = [{ ts: Date.now(), prompt: plain }];
1305
+ } catch {}
1306
+ }
1307
+ const userMsg = allPrompts.length > 0 ? allPrompts[allPrompts.length - 1].prompt : '';
1308
+ // Clean up temp file
1309
+ try { unlinkSync(promptPath); } catch {}
1129
1310
 
1130
1311
  const cleanAssistant = stripSystemTags(assistantMsg);
1131
1312
  if (!cleanAssistant || cleanAssistant.length < 50) process.exit(0);
@@ -1144,6 +1325,17 @@ async function runIngest() {
1144
1325
  else if (prev.content) existing = { exchanges: [{ role: 'context', content: prev.content }] };
1145
1326
  } catch {}
1146
1327
 
1328
+ // Store all user prompts from this session, not just the last one
1329
+ if (allPrompts.length > 1) {
1330
+ for (let i = 0; i < allPrompts.length - 1; i++) {
1331
+ existing.exchanges.push({
1332
+ timestamp: new Date(allPrompts[i].ts).toISOString(),
1333
+ user: allPrompts[i].prompt.slice(0, 2000),
1334
+ assistant: ''
1335
+ });
1336
+ }
1337
+ }
1338
+ // Final exchange has the actual assistant response
1147
1339
  existing.exchanges.push({
1148
1340
  timestamp: new Date().toISOString(),
1149
1341
  user: userMsg.slice(0, 2000),
@@ -1194,8 +1386,8 @@ async function runIngest() {
1194
1386
  renameSync(cpTmp, cpPath);
1195
1387
  } catch {}
1196
1388
 
1197
- // --- 3. POST to memory server /ingest (ported from Python auto-ingest.py) ---
1198
- if (userMsg && cleanAssistant) {
1389
+ // --- 3. POST to memory server /ingest — full conversation (all prompts + final response) ---
1390
+ if ((allPrompts.length > 0 || userMsg) && cleanAssistant) {
1199
1391
  // Read conversation ID from temp file (written by MCP server process)
1200
1392
  let convId = '';
1201
1393
  try {
@@ -1204,6 +1396,17 @@ async function runIngest() {
1204
1396
  if (!convId) convId = sessionId; // fallback
1205
1397
 
1206
1398
  try {
1399
+ // Build messages array: all user prompts + final assistant response
1400
+ const messages = [];
1401
+ if (allPrompts.length > 0) {
1402
+ for (const entry of allPrompts) {
1403
+ messages.push({ role: 'user', content: entry.prompt.slice(0, 10000) });
1404
+ }
1405
+ } else if (userMsg) {
1406
+ messages.push({ role: 'user', content: userMsg.slice(0, 10000) });
1407
+ }
1408
+ messages.push({ role: 'assistant', content: cleanAssistant.slice(0, 10000) });
1409
+
1207
1410
  const project = process.cwd().split(/[\\/]/).pop();
1208
1411
  const controller = new AbortController();
1209
1412
  const timer = setTimeout(() => controller.abort(), 15000);
@@ -1211,10 +1414,7 @@ async function runIngest() {
1211
1414
  method: 'POST',
1212
1415
  headers: { 'Content-Type': 'application/json' },
1213
1416
  body: JSON.stringify({
1214
- messages: [
1215
- { role: 'user', content: userMsg.slice(0, 10000) },
1216
- { role: 'assistant', content: cleanAssistant.slice(0, 10000) }
1217
- ],
1417
+ messages,
1218
1418
  user_id: 'lloyd',
1219
1419
  conversation_id: convId,
1220
1420
  project