mevoric 2.0.0 → 2.2.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 (3) hide show
  1. package/package.json +5 -2
  2. package/server.mjs +194 -12
  3. package/watcher.mjs +146 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevoric",
3
- "version": "2.0.0",
3
+ "version": "2.2.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",
@@ -29,10 +29,13 @@
29
29
  "files": [
30
30
  "server.mjs",
31
31
  "init.mjs",
32
+ "watcher.mjs",
32
33
  "README.md",
33
34
  "LICENSE"
34
35
  ],
35
36
  "dependencies": {
36
- "@modelcontextprotocol/sdk": "^1.0.0"
37
+ "@anthropic-ai/claude-agent-sdk": "^0.2.54",
38
+ "@modelcontextprotocol/sdk": "^1.0.0",
39
+ "node-notifier": "^10.0.1"
37
40
  }
38
41
  }
package/server.mjs CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from 'fs';
29
29
  import { resolve, dirname } from 'path';
30
30
  import { randomBytes, randomUUID } from 'crypto';
31
+ import { spawn } from 'child_process';
31
32
  import { homedir, tmpdir, platform } from 'os';
32
33
 
33
34
  // ============================================================
@@ -66,6 +67,10 @@ const MEMORY_SERVER_URL = process.env.MEVORIC_SERVER_URL
66
67
  // Session-level conversation ID for memory tools
67
68
  const sessionConversationId = randomUUID();
68
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
+
69
74
  // Write conversation ID to temp file so external tools can reference it
70
75
  const CONVID_FILE = resolve(tmpdir(), 'mevoric-convid');
71
76
  try { writeFileSync(CONVID_FILE, sessionConversationId); } catch {}
@@ -682,13 +687,19 @@ async function handleRetrieveMemories(args) {
682
687
  .filter(m => (m.score || 0) >= SCORE_THRESHOLD)
683
688
  .slice(0, MAX_RESULTS)
684
689
  .map((m, i) => ({
690
+ mem0_id: m.mem0_id,
685
691
  memory: m.memory,
686
692
  score: Math.round((m.score || 0) * 1000) / 1000,
687
693
  rank: i + 1
688
694
  }));
689
695
 
696
+ // Cache for judge_memories (includes mem0_id for verdict posting)
697
+ if (filtered.length > 0) {
698
+ retrievalCache.set(sessionConversationId, filtered);
699
+ }
700
+
690
701
  return {
691
- memories: filtered,
702
+ memories: filtered.map(m => ({ memory: m.memory, score: m.score, rank: m.rank })),
692
703
  conversation_id: sessionConversationId,
693
704
  ...(filtered.length === 0 && raw.length > 0
694
705
  ? { note: `${raw.length} memories found but none above relevance threshold (${SCORE_THRESHOLD})` }
@@ -726,6 +737,134 @@ async function handleStoreConversation(args) {
726
737
  }
727
738
  }
728
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
+
729
868
  async function handleJudgeMemories(args) {
730
869
  const convId = args.conversation_id || sessionConversationId;
731
870
  const queryText = args.query_text;
@@ -735,18 +874,22 @@ async function handleJudgeMemories(args) {
735
874
  }
736
875
  const userId = args.user_id || 'lloyd';
737
876
 
738
- try {
739
- const data = await memoryFetch('/feedback', {
740
- conversation_id: convId,
741
- user_id: userId,
742
- query_text: queryText,
743
- response_text: responseText
744
- }, 30000);
745
-
746
- return { status: data.status || 'judging', conversation_id: convId };
747
- } catch (err) {
748
- 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 };
749
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
+ };
750
893
  }
751
894
 
752
895
  // ============================================================
@@ -1242,6 +1385,22 @@ async function runIngest() {
1242
1385
  } catch {} // Best-effort
1243
1386
  }
1244
1387
 
1388
+ // --- 5. Broadcast session-end notification (picked up by watcher) ---
1389
+ try {
1390
+ mkdirSync(MESSAGES_DIR, { recursive: true });
1391
+ const summary = userMsg.slice(0, 100) || 'session ended';
1392
+ const msgData = JSON.stringify({
1393
+ fromName: name,
1394
+ toName: '*',
1395
+ broadcast: true,
1396
+ to: '*',
1397
+ content: `${name} finished: ${summary}`,
1398
+ timestamp: new Date().toISOString()
1399
+ });
1400
+ const msgFile = resolve(MESSAGES_DIR, `${Date.now()}-${randomBytes(4).toString('hex')}.json`);
1401
+ writeFileSync(msgFile, msgData);
1402
+ } catch {}
1403
+
1245
1404
  process.exit(0);
1246
1405
  }
1247
1406
 
@@ -1308,8 +1467,31 @@ async function runCheckMessages() {
1308
1467
  // CLI: --bootstrap-context (SessionStart hook mode)
1309
1468
  // ============================================================
1310
1469
 
1470
+ function ensureWatcherRunning() {
1471
+ const pidFile = resolve(DATA_DIR, 'watcher.pid');
1472
+ try {
1473
+ const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
1474
+ if (pid && isProcessAlive(pid)) return; // already running
1475
+ } catch {}
1476
+
1477
+ // Spawn watcher as detached background process
1478
+ const watcherPath = resolve(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), 'watcher.mjs');
1479
+ if (!existsSync(watcherPath)) return;
1480
+
1481
+ const child = spawn(process.execPath, [watcherPath], {
1482
+ detached: true,
1483
+ stdio: 'ignore',
1484
+ windowsHide: true,
1485
+ });
1486
+ child.unref();
1487
+
1488
+ // Save PID so we can check next time
1489
+ try { writeFileSync(pidFile, String(child.pid), 'utf8'); } catch {}
1490
+ }
1491
+
1311
1492
  async function runBootstrapContext() {
1312
1493
  ensureDirs();
1494
+ ensureWatcherRunning();
1313
1495
 
1314
1496
  const chunks = [];
1315
1497
  for await (const chunk of process.stdin) chunks.push(chunk);
package/watcher.mjs ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Mevoric Notification Watcher
5
+ *
6
+ * Background heartbeat that polls for new agent messages every N seconds
7
+ * and pops a Windows toast notification when one arrives.
8
+ *
9
+ * Zero API credits. Just reads files on a timer.
10
+ *
11
+ * Usage:
12
+ * node watcher.mjs # default 5 second poll
13
+ * node watcher.mjs --interval 3 # 3 second poll
14
+ * node watcher.mjs --test # send a test notification then exit
15
+ */
16
+
17
+ import { readdirSync, readFileSync, writeFileSync } from 'fs';
18
+ import { resolve } from 'path';
19
+ import { execSync } from 'child_process';
20
+ import { platform } from 'os';
21
+
22
+ // ── Config ──────────────────────────────────────────────
23
+
24
+ const DATA_DIR = process.env.MEVORIC_DATA_DIR
25
+ || process.env.AGENT_BRIDGE_DATA_DIR
26
+ || (platform() === 'win32'
27
+ ? resolve(process.env.LOCALAPPDATA || '', 'agent-bridge')
28
+ : resolve(process.env.HOME || '', '.local', 'share', 'mevoric'));
29
+
30
+ const MESSAGES_DIR = resolve(DATA_DIR, 'messages');
31
+ const CURSOR_FILE = resolve(DATA_DIR, 'watcher.cursor');
32
+
33
+ const args = process.argv.slice(2);
34
+ const intervalIdx = args.indexOf('--interval');
35
+ const POLL_MS = (intervalIdx !== -1 && args[intervalIdx + 1])
36
+ ? parseInt(args[intervalIdx + 1], 10) * 1000
37
+ : 5000;
38
+
39
+ // ── Cursor (tracks what we've already notified about) ───
40
+
41
+ function loadCursor() {
42
+ try {
43
+ return parseInt(readFileSync(CURSOR_FILE, 'utf8').trim(), 10) || Date.now();
44
+ } catch {
45
+ return Date.now();
46
+ }
47
+ }
48
+
49
+ function saveCursor(ts) {
50
+ try {
51
+ writeFileSync(CURSOR_FILE, String(ts), 'utf8');
52
+ } catch {}
53
+ }
54
+
55
+ // ── Popup Notification ───────────────────────────────────
56
+
57
+ function notify(title, body) {
58
+ if (platform() !== 'win32') {
59
+ console.log(`[NOTIFY] ${title}: ${body}`);
60
+ return;
61
+ }
62
+
63
+ // WScript.Shell Popup — works on all Windows, bypasses DND, auto-closes after 5s
64
+ const safeTitle = title.replace(/'/g, "''");
65
+ const safeBody = body.replace(/'/g, "''");
66
+
67
+ try {
68
+ execSync(
69
+ `powershell -NoProfile -Command "(New-Object -ComObject WScript.Shell).Popup('${safeBody}', 5, 'Mevoric: ${safeTitle}', 0x40)"`,
70
+ { stdio: 'ignore', timeout: 8000 }
71
+ );
72
+ } catch {
73
+ console.log(`[NOTIFY] ${title}: ${body}`);
74
+ }
75
+ }
76
+
77
+ // ── Poll for new messages ───────────────────────────────
78
+
79
+ function checkMessages(lastTs) {
80
+ let files;
81
+ try {
82
+ files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json')).sort();
83
+ } catch {
84
+ return { messages: [], highestTs: lastTs };
85
+ }
86
+
87
+ const newMessages = [];
88
+ let highestTs = lastTs;
89
+
90
+ for (const file of files) {
91
+ const ts = parseInt(file.split('-')[0], 10);
92
+ if (isNaN(ts) || ts <= lastTs) continue;
93
+
94
+ try {
95
+ const msg = JSON.parse(readFileSync(resolve(MESSAGES_DIR, file), 'utf8'));
96
+ newMessages.push(msg);
97
+ if (ts > highestTs) highestTs = ts;
98
+ } catch {
99
+ // malformed, skip
100
+ }
101
+ }
102
+
103
+ return { messages: newMessages, highestTs };
104
+ }
105
+
106
+ // ── Test mode ───────────────────────────────────────────
107
+
108
+ if (args.includes('--test')) {
109
+ console.log('Sending test notification...');
110
+ notify('emergence-main-2', 'Hey Lloyd, I finished restoring the editor. PIE is ready.');
111
+ setTimeout(() => process.exit(0), 3000);
112
+ } else {
113
+
114
+ // ── Main loop ───────────────────────────────────────────
115
+
116
+ let cursor = loadCursor();
117
+
118
+ console.log(`[Mevoric Watcher] Polling every ${POLL_MS / 1000}s`);
119
+ console.log(`[Mevoric Watcher] Messages dir: ${MESSAGES_DIR}`);
120
+ console.log(`[Mevoric Watcher] Cursor: ${new Date(cursor).toISOString()}`);
121
+
122
+ setInterval(() => {
123
+ const { messages, highestTs } = checkMessages(cursor);
124
+
125
+ if (messages.length > 0) {
126
+ for (const msg of messages) {
127
+ const from = msg.fromName || msg.from || 'unknown';
128
+ const preview = (msg.content || '').slice(0, 120);
129
+ const target = msg.broadcast ? 'broadcast' : `→ ${msg.toName || msg.to}`;
130
+
131
+ console.log(`[${new Date().toLocaleTimeString()}] ${from} (${target}): ${preview}`);
132
+ notify(`${from}`, preview);
133
+ }
134
+
135
+ cursor = highestTs;
136
+ saveCursor(cursor);
137
+ }
138
+ }, POLL_MS);
139
+
140
+ // Keep alive
141
+ process.on('SIGINT', () => {
142
+ console.log('\n[Mevoric Watcher] Stopped.');
143
+ process.exit(0);
144
+ });
145
+
146
+ } // end else (not --test)