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.
Files changed (3) hide show
  1. package/package.json +4 -2
  2. package/server.mjs +40 -0
  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.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)