mevoric 2.0.0 → 2.1.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 +4 -2
- package/server.mjs +40 -0
- 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.1.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,12 @@
|
|
|
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
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
38
|
+
"node-notifier": "^10.0.1"
|
|
37
39
|
}
|
|
38
40
|
}
|
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
|
// ============================================================
|
|
@@ -1242,6 +1243,22 @@ async function runIngest() {
|
|
|
1242
1243
|
} catch {} // Best-effort
|
|
1243
1244
|
}
|
|
1244
1245
|
|
|
1246
|
+
// --- 5. Broadcast session-end notification (picked up by watcher) ---
|
|
1247
|
+
try {
|
|
1248
|
+
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
1249
|
+
const summary = userMsg.slice(0, 100) || 'session ended';
|
|
1250
|
+
const msgData = JSON.stringify({
|
|
1251
|
+
fromName: name,
|
|
1252
|
+
toName: '*',
|
|
1253
|
+
broadcast: true,
|
|
1254
|
+
to: '*',
|
|
1255
|
+
content: `${name} finished: ${summary}`,
|
|
1256
|
+
timestamp: new Date().toISOString()
|
|
1257
|
+
});
|
|
1258
|
+
const msgFile = resolve(MESSAGES_DIR, `${Date.now()}-${randomBytes(4).toString('hex')}.json`);
|
|
1259
|
+
writeFileSync(msgFile, msgData);
|
|
1260
|
+
} catch {}
|
|
1261
|
+
|
|
1245
1262
|
process.exit(0);
|
|
1246
1263
|
}
|
|
1247
1264
|
|
|
@@ -1308,8 +1325,31 @@ async function runCheckMessages() {
|
|
|
1308
1325
|
// CLI: --bootstrap-context (SessionStart hook mode)
|
|
1309
1326
|
// ============================================================
|
|
1310
1327
|
|
|
1328
|
+
function ensureWatcherRunning() {
|
|
1329
|
+
const pidFile = resolve(DATA_DIR, 'watcher.pid');
|
|
1330
|
+
try {
|
|
1331
|
+
const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1332
|
+
if (pid && isProcessAlive(pid)) return; // already running
|
|
1333
|
+
} catch {}
|
|
1334
|
+
|
|
1335
|
+
// Spawn watcher as detached background process
|
|
1336
|
+
const watcherPath = resolve(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), 'watcher.mjs');
|
|
1337
|
+
if (!existsSync(watcherPath)) return;
|
|
1338
|
+
|
|
1339
|
+
const child = spawn(process.execPath, [watcherPath], {
|
|
1340
|
+
detached: true,
|
|
1341
|
+
stdio: 'ignore',
|
|
1342
|
+
windowsHide: true,
|
|
1343
|
+
});
|
|
1344
|
+
child.unref();
|
|
1345
|
+
|
|
1346
|
+
// Save PID so we can check next time
|
|
1347
|
+
try { writeFileSync(pidFile, String(child.pid), 'utf8'); } catch {}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1311
1350
|
async function runBootstrapContext() {
|
|
1312
1351
|
ensureDirs();
|
|
1352
|
+
ensureWatcherRunning();
|
|
1313
1353
|
|
|
1314
1354
|
const chunks = [];
|
|
1315
1355
|
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)
|