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.
- package/package.json +5 -2
- package/server.mjs +194 -12
- package/watcher.mjs +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mevoric",
|
|
3
|
-
"version": "2.
|
|
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
|
-
"@
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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)
|