mevoric 2.0.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 (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/init.mjs +445 -0
  4. package/package.json +38 -0
  5. package/server.mjs +1469 -0
package/server.mjs ADDED
@@ -0,0 +1,1469 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Mevoric — Unified memory + agent bridge for Claude Code.
5
+ *
6
+ * 12 tools:
7
+ * Memory: retrieve_memories, store_conversation, judge_memories
8
+ * Bridge: register_agent, list_agents, send_message, read_messages, broadcast
9
+ * Context: share_context, get_context
10
+ * Checkpoints: save_checkpoint, load_checkpoint
11
+ *
12
+ * 4 hook modes:
13
+ * --bootstrap-context (SessionStart)
14
+ * --capture-prompt (UserPromptSubmit)
15
+ * --check-messages (UserPromptSubmit)
16
+ * --ingest (Stop) — saves context + checkpoint + POSTs to memory server
17
+ */
18
+
19
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
20
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
+ import {
22
+ CallToolRequestSchema,
23
+ ListToolsRequestSchema
24
+ } from '@modelcontextprotocol/sdk/types.js';
25
+ import {
26
+ existsSync, mkdirSync, writeFileSync, readFileSync,
27
+ readdirSync, unlinkSync, renameSync
28
+ } from 'fs';
29
+ import { resolve, dirname } from 'path';
30
+ import { randomBytes, randomUUID } from 'crypto';
31
+ import { homedir, tmpdir, platform } from 'os';
32
+
33
+ // ============================================================
34
+ // Constants (configurable via environment variables)
35
+ // ============================================================
36
+
37
+ function getDefaultDataDir() {
38
+ const p = platform();
39
+ if (p === 'win32') {
40
+ return resolve(process.env.LOCALAPPDATA || resolve(homedir(), 'AppData', 'Local'), 'mevoric');
41
+ }
42
+ if (p === 'darwin') {
43
+ return resolve(homedir(), 'Library', 'Application Support', 'mevoric');
44
+ }
45
+ return resolve(process.env.XDG_DATA_HOME || resolve(homedir(), '.local', 'share'), 'mevoric');
46
+ }
47
+
48
+ // Support legacy AGENT_BRIDGE_DATA_DIR for backwards compat during migration
49
+ const DATA_DIR = process.env.MEVORIC_DATA_DIR || process.env.AGENT_BRIDGE_DATA_DIR || getDefaultDataDir();
50
+ const AGENTS_DIR = resolve(DATA_DIR, 'agents');
51
+ const MESSAGES_DIR = resolve(DATA_DIR, 'messages');
52
+ const CONTEXT_DIR = resolve(DATA_DIR, 'context');
53
+ const CURSORS_DIR = resolve(DATA_DIR, 'cursors');
54
+ const CHECKPOINTS_DIR = resolve(DATA_DIR, 'checkpoints');
55
+ const CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
56
+ const HEARTBEAT_INTERVAL_MS = parseInt(process.env.MEVORIC_HEARTBEAT_MS || '15000', 10);
57
+ const STALE_THRESHOLD_MS = HEARTBEAT_INTERVAL_MS * 3;
58
+ const DEAD_THRESHOLD_MS = parseInt(process.env.MEVORIC_DEAD_MS || '300000', 10);
59
+ const MESSAGE_TTL_MS = parseInt(process.env.MEVORIC_MESSAGE_TTL_MS || '3600000', 10);
60
+
61
+ // Memory server (newcode backend)
62
+ const MEMORY_SERVER_URL = process.env.MEVORIC_SERVER_URL
63
+ || process.env.NEWCODE_SERVER_URL
64
+ || 'http://192.168.2.100:4000';
65
+
66
+ // Session-level conversation ID for memory tools
67
+ const sessionConversationId = randomUUID();
68
+
69
+ // Write conversation ID to temp file so external tools can reference it
70
+ const CONVID_FILE = resolve(tmpdir(), 'mevoric-convid');
71
+ try { writeFileSync(CONVID_FILE, sessionConversationId); } catch {}
72
+
73
+ // ============================================================
74
+ // Agent State (in-memory, per-process)
75
+ // ============================================================
76
+
77
+ const agentId = `agent-${randomBytes(3).toString('hex')}`;
78
+ let agentName = process.env.MEVORIC_AGENT_NAME || process.env.AGENT_BRIDGE_NAME || null;
79
+ let agentBaseName = agentName;
80
+ const startedAt = new Date().toISOString();
81
+ let lastReadTimestamp = Date.now();
82
+ let heartbeatTimer = null;
83
+
84
+ // ============================================================
85
+ // Directory Setup
86
+ // ============================================================
87
+
88
+ function ensureDirs() {
89
+ mkdirSync(AGENTS_DIR, { recursive: true });
90
+ mkdirSync(MESSAGES_DIR, { recursive: true });
91
+ mkdirSync(CONTEXT_DIR, { recursive: true });
92
+ mkdirSync(CURSORS_DIR, { recursive: true });
93
+ mkdirSync(CHECKPOINTS_DIR, { recursive: true });
94
+ }
95
+
96
+ // ============================================================
97
+ // Process Liveness Check
98
+ // ============================================================
99
+
100
+ function isProcessAlive(pid) {
101
+ try {
102
+ process.kill(pid, 0);
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // ============================================================
110
+ // Agent File Operations
111
+ // ============================================================
112
+
113
+ function getAgentData() {
114
+ return {
115
+ id: agentId,
116
+ name: agentName,
117
+ baseName: agentBaseName || agentName,
118
+ project: process.cwd().split(/[\\/]/).pop(),
119
+ cwd: process.cwd(),
120
+ pid: process.pid,
121
+ startedAt,
122
+ lastHeartbeat: new Date().toISOString()
123
+ };
124
+ }
125
+
126
+ function writeAgentFile() {
127
+ const data = JSON.stringify(getAgentData(), null, 2);
128
+ const targetPath = resolve(AGENTS_DIR, `${agentId}.json`);
129
+ const tmpPath = targetPath + '.tmp';
130
+ try {
131
+ writeFileSync(tmpPath, data);
132
+ renameSync(tmpPath, targetPath);
133
+ } catch (err) {
134
+ try { writeFileSync(targetPath, data); } catch {}
135
+ }
136
+ }
137
+
138
+ function removeAgentFile() {
139
+ try { unlinkSync(resolve(AGENTS_DIR, `${agentId}.json`)); } catch {}
140
+ try { unlinkSync(resolve(AGENTS_DIR, `${agentId}.json.tmp`)); } catch {}
141
+ }
142
+
143
+ function readAgentFile(filePath) {
144
+ try {
145
+ return JSON.parse(readFileSync(filePath, 'utf8'));
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ // ============================================================
152
+ // Get All Agents (with staleness detection + cleanup)
153
+ // ============================================================
154
+
155
+ function getAllAgents() {
156
+ const agents = [];
157
+ let files;
158
+ try {
159
+ files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
160
+ } catch {
161
+ return agents;
162
+ }
163
+
164
+ const now = Date.now();
165
+
166
+ for (const file of files) {
167
+ const filePath = resolve(AGENTS_DIR, file);
168
+ const agent = readAgentFile(filePath);
169
+ if (!agent) continue;
170
+
171
+ const heartbeatAge = now - new Date(agent.lastHeartbeat).getTime();
172
+ const pidAlive = isProcessAlive(agent.pid);
173
+
174
+ if (!pidAlive && agent.id !== agentId) {
175
+ try { unlinkSync(filePath); } catch {}
176
+ continue;
177
+ }
178
+
179
+ if (heartbeatAge > DEAD_THRESHOLD_MS && agent.id !== agentId) {
180
+ try { unlinkSync(filePath); } catch {}
181
+ continue;
182
+ }
183
+
184
+ agents.push({
185
+ ...agent,
186
+ status: heartbeatAge > STALE_THRESHOLD_MS ? 'stale' : 'active',
187
+ isMe: agent.id === agentId
188
+ });
189
+ }
190
+
191
+ return agents;
192
+ }
193
+
194
+ // ============================================================
195
+ // Cleanup
196
+ // ============================================================
197
+
198
+ function cleanOldMessages() {
199
+ let files;
200
+ try {
201
+ files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json'));
202
+ } catch {
203
+ return;
204
+ }
205
+
206
+ const cutoff = Date.now() - MESSAGE_TTL_MS;
207
+
208
+ for (const file of files) {
209
+ const timestamp = parseInt(file.split('-')[0], 10);
210
+ if (!isNaN(timestamp) && timestamp < cutoff) {
211
+ try { unlinkSync(resolve(MESSAGES_DIR, file)); } catch {}
212
+ }
213
+ }
214
+ }
215
+
216
+ // ============================================================
217
+ // Heartbeat
218
+ // ============================================================
219
+
220
+ function startHeartbeat() {
221
+ heartbeatTimer = setInterval(() => {
222
+ writeAgentFile();
223
+ cleanOldMessages();
224
+ }, HEARTBEAT_INTERVAL_MS);
225
+ heartbeatTimer.unref();
226
+ }
227
+
228
+ // ============================================================
229
+ // Message Operations
230
+ // ============================================================
231
+
232
+ function writeMessage(to, toName, content, isBroadcast) {
233
+ const now = Date.now();
234
+ const rand = randomBytes(3).toString('hex');
235
+ const msgId = `msg-${now}-${rand}`;
236
+ const filename = `${now}-${rand}.json`;
237
+
238
+ const message = {
239
+ id: msgId,
240
+ from: agentId,
241
+ fromName: agentName,
242
+ to: isBroadcast ? '*' : to,
243
+ toName: isBroadcast ? null : toName,
244
+ broadcast: isBroadcast,
245
+ content,
246
+ project: process.cwd().split(/[\\/]/).pop(),
247
+ timestamp: new Date(now).toISOString()
248
+ };
249
+
250
+ const filePath = resolve(MESSAGES_DIR, filename);
251
+ writeFileSync(filePath, JSON.stringify(message, null, 2), { flag: 'wx' });
252
+ return message;
253
+ }
254
+
255
+ function readNewMessages(includeBroadcasts = true) {
256
+ let files;
257
+ try {
258
+ files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json')).sort();
259
+ } catch {
260
+ return [];
261
+ }
262
+
263
+ const messages = [];
264
+
265
+ for (const file of files) {
266
+ const timestamp = parseInt(file.split('-')[0], 10);
267
+ if (isNaN(timestamp) || timestamp <= lastReadTimestamp) continue;
268
+
269
+ const filePath = resolve(MESSAGES_DIR, file);
270
+ let msg;
271
+ try {
272
+ msg = JSON.parse(readFileSync(filePath, 'utf8'));
273
+ } catch {
274
+ continue;
275
+ }
276
+
277
+ if (msg.from === agentId) continue;
278
+
279
+ const isForMe = msg.to === agentId || msg.toName === agentName;
280
+ const isBroadcast = msg.broadcast && msg.to === '*';
281
+
282
+ if (isForMe || (isBroadcast && includeBroadcasts)) {
283
+ messages.push({
284
+ ...msg,
285
+ ageSeconds: Math.round((Date.now() - new Date(msg.timestamp).getTime()) / 1000)
286
+ });
287
+ }
288
+ }
289
+
290
+ if (files.length > 0) {
291
+ const lastFile = files[files.length - 1];
292
+ const lastTs = parseInt(lastFile.split('-')[0], 10);
293
+ if (!isNaN(lastTs) && lastTs > lastReadTimestamp) {
294
+ lastReadTimestamp = lastTs;
295
+ }
296
+ }
297
+
298
+ return messages;
299
+ }
300
+
301
+ // ============================================================
302
+ // Resolve Agent Target (name or ID)
303
+ // ============================================================
304
+
305
+ function resolveAgent(nameOrId) {
306
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
307
+
308
+ const byId = agents.find(a => a.id === nameOrId);
309
+ if (byId) return byId;
310
+
311
+ const nameL = nameOrId.toLowerCase();
312
+ const byName = agents
313
+ .filter(a => a.name && a.name.toLowerCase() === nameL)
314
+ .sort((a, b) => new Date(b.lastHeartbeat) - new Date(a.lastHeartbeat));
315
+ if (byName.length > 0) return byName[0];
316
+
317
+ const byBase = agents
318
+ .filter(a => a.baseName && a.baseName.toLowerCase() === nameL)
319
+ .sort((a, b) => new Date(b.lastHeartbeat) - new Date(a.lastHeartbeat));
320
+ if (byBase.length > 0) return byBase[0];
321
+
322
+ const partial = agents.filter(a => a.name && a.name.toLowerCase().includes(nameL));
323
+ if (partial.length === 1) return partial[0];
324
+
325
+ return null;
326
+ }
327
+
328
+ function resolveAllAgents(nameOrId) {
329
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
330
+ const nameL = nameOrId.toLowerCase();
331
+
332
+ const byId = agents.find(a => a.id === nameOrId);
333
+ if (byId) return [byId];
334
+
335
+ const matches = agents.filter(a =>
336
+ (a.name && a.name.toLowerCase() === nameL) ||
337
+ (a.baseName && a.baseName.toLowerCase() === nameL)
338
+ );
339
+ if (matches.length > 0) return matches;
340
+
341
+ const partial = agents.filter(a => a.name && a.name.toLowerCase().includes(nameL));
342
+ return partial;
343
+ }
344
+
345
+ // ============================================================
346
+ // HTTP Helper (for memory server calls)
347
+ // ============================================================
348
+
349
+ async function memoryFetch(endpoint, body, timeoutMs = 30000) {
350
+ const controller = new AbortController();
351
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
352
+
353
+ try {
354
+ const res = await fetch(`${MEMORY_SERVER_URL}${endpoint}`, {
355
+ method: 'POST',
356
+ headers: { 'Content-Type': 'application/json' },
357
+ body: JSON.stringify(body),
358
+ signal: controller.signal
359
+ });
360
+ clearTimeout(timer);
361
+ return await res.json();
362
+ } catch (err) {
363
+ clearTimeout(timer);
364
+ if (err.name === 'AbortError') {
365
+ throw new Error(`Memory server timeout after ${timeoutMs}ms on ${endpoint}`);
366
+ }
367
+ throw new Error(`Memory server unreachable (${MEMORY_SERVER_URL}${endpoint}): ${err.message}`);
368
+ }
369
+ }
370
+
371
+ // ============================================================
372
+ // Tool Handlers — Bridge
373
+ // ============================================================
374
+
375
+ async function handleRegister(args) {
376
+ const name = args.name?.trim();
377
+ if (!name) return { error: 'Name is required' };
378
+
379
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
380
+ let finalName = name;
381
+ const nameLower = name.toLowerCase();
382
+ const conflicts = agents.filter(a => a.name && a.name.toLowerCase() === nameLower);
383
+ if (conflicts.length > 0) {
384
+ const allNames = agents.map(a => a.name?.toLowerCase()).filter(Boolean);
385
+ let suffix = 2;
386
+ while (allNames.includes(`${nameLower}-${suffix}`)) suffix++;
387
+ finalName = `${name}-${suffix}`;
388
+ }
389
+
390
+ agentName = finalName;
391
+ agentBaseName = name;
392
+ writeAgentFile();
393
+
394
+ return {
395
+ registered: true,
396
+ id: agentId,
397
+ name: agentName,
398
+ baseName: name,
399
+ project: process.cwd().split(/[\\/]/).pop(),
400
+ cwd: process.cwd(),
401
+ pid: process.pid,
402
+ ...(finalName !== name ? { note: `Name "${name}" was taken, registered as "${finalName}"` } : {})
403
+ };
404
+ }
405
+
406
+ async function handleListAgents() {
407
+ const agents = getAllAgents();
408
+ return {
409
+ agents,
410
+ totalActive: agents.filter(a => a.status === 'active').length,
411
+ myId: agentId,
412
+ myName: agentName
413
+ };
414
+ }
415
+
416
+ async function handleSendMessage(args) {
417
+ const { to, content } = args;
418
+ if (!to) return { error: 'Target agent (to) is required' };
419
+ if (!content) return { error: 'Message content is required' };
420
+
421
+ const target = resolveAgent(to);
422
+ if (!target) {
423
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
424
+ return {
425
+ error: `No active agent found matching "${to}"`,
426
+ availableAgents: agents.map(a => ({ id: a.id, name: a.name, project: a.project }))
427
+ };
428
+ }
429
+
430
+ const msg = writeMessage(target.id, target.name, content, false);
431
+ return {
432
+ sent: true,
433
+ messageId: msg.id,
434
+ to: { id: target.id, name: target.name },
435
+ timestamp: msg.timestamp
436
+ };
437
+ }
438
+
439
+ async function handleReadMessages(args) {
440
+ const includeBroadcasts = args.include_broadcasts !== false;
441
+ const messages = readNewMessages(includeBroadcasts);
442
+ return {
443
+ messages,
444
+ count: messages.length,
445
+ myId: agentId,
446
+ myName: agentName
447
+ };
448
+ }
449
+
450
+ async function handleBroadcast(args) {
451
+ const { content } = args;
452
+ if (!content) return { error: 'Message content is required' };
453
+
454
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
455
+ const msg = writeMessage('*', null, content, true);
456
+
457
+ return {
458
+ broadcast: true,
459
+ messageId: msg.id,
460
+ activeRecipients: agents.length,
461
+ timestamp: msg.timestamp
462
+ };
463
+ }
464
+
465
+ // ============================================================
466
+ // Tool Handlers — Context
467
+ // ============================================================
468
+
469
+ function writeContextFile(name, content, uniqueId) {
470
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
471
+ const suffix = uniqueId || agentId;
472
+ const data = JSON.stringify({
473
+ agentName: name,
474
+ agentId: uniqueId || agentId,
475
+ baseName: name,
476
+ project: process.cwd().split(/[\\/]/).pop(),
477
+ updatedAt: new Date().toISOString(),
478
+ content
479
+ }, null, 2);
480
+ const targetPath = resolve(CONTEXT_DIR, `${safeName}--${suffix}.json`);
481
+ const tmpPath = targetPath + '.tmp';
482
+ try {
483
+ writeFileSync(tmpPath, data);
484
+ renameSync(tmpPath, targetPath);
485
+ } catch (err) {
486
+ try { writeFileSync(targetPath, data); } catch {}
487
+ }
488
+ }
489
+
490
+ function readAllContextFiles() {
491
+ let files;
492
+ try {
493
+ files = readdirSync(CONTEXT_DIR).filter(f => f.endsWith('.json'));
494
+ } catch {
495
+ return [];
496
+ }
497
+ const contexts = [];
498
+ for (const file of files) {
499
+ try {
500
+ const ctx = JSON.parse(readFileSync(resolve(CONTEXT_DIR, file), 'utf8'));
501
+ contexts.push(ctx);
502
+ } catch {
503
+ continue;
504
+ }
505
+ }
506
+ return contexts;
507
+ }
508
+
509
+ async function handleShareContext(args) {
510
+ const { content } = args;
511
+ if (!content) return { error: 'Content is required — dump what you know' };
512
+ if (!agentName) return { error: 'Register with a name first (register_agent) before sharing context' };
513
+
514
+ writeContextFile(agentName, content);
515
+
516
+ return {
517
+ shared: true,
518
+ agentName,
519
+ project: process.cwd().split(/[\\/]/).pop(),
520
+ updatedAt: new Date().toISOString(),
521
+ contentLength: content.length
522
+ };
523
+ }
524
+
525
+ async function handleGetContext(args) {
526
+ const { from } = args;
527
+
528
+ if (from) {
529
+ const safeName = from.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
530
+ let files;
531
+ try {
532
+ files = readdirSync(CONTEXT_DIR).filter(f => f.endsWith('.json'));
533
+ } catch {
534
+ return { found: false, error: `No shared context found for "${from}"` };
535
+ }
536
+
537
+ const matches = [];
538
+ for (const file of files) {
539
+ const basePart = file.replace(/\.json$/, '').split('--')[0];
540
+ if (basePart === safeName) {
541
+ try {
542
+ const ctx = JSON.parse(readFileSync(resolve(CONTEXT_DIR, file), 'utf8'));
543
+ matches.push(ctx);
544
+ } catch { continue; }
545
+ }
546
+ }
547
+
548
+ if (matches.length === 0) {
549
+ return { found: false, error: `No shared context found for "${from}"` };
550
+ }
551
+ if (matches.length === 1) {
552
+ return { found: true, context: matches[0] };
553
+ }
554
+ return { found: true, contexts: matches, count: matches.length };
555
+ }
556
+
557
+ const contexts = readAllContextFiles();
558
+ return {
559
+ contexts,
560
+ count: contexts.length
561
+ };
562
+ }
563
+
564
+ // ============================================================
565
+ // Tool Handlers — Checkpoints
566
+ // ============================================================
567
+
568
+ function writeCheckpointFile(name, sessionId, checkpointData) {
569
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
570
+ const suffix = sessionId || agentId;
571
+ const data = JSON.stringify({
572
+ version: 1,
573
+ agentName: name,
574
+ project: process.cwd().split(/[\\/]/).pop(),
575
+ sessionId: suffix,
576
+ createdAt: new Date().toISOString(),
577
+ ...checkpointData
578
+ }, null, 2);
579
+ const targetPath = resolve(CHECKPOINTS_DIR, `${safeName}--${suffix}.json`);
580
+ const tmpPath = targetPath + '.tmp';
581
+ try {
582
+ writeFileSync(tmpPath, data);
583
+ renameSync(tmpPath, targetPath);
584
+ } catch (err) {
585
+ try { writeFileSync(targetPath, data); } catch {}
586
+ }
587
+ return targetPath;
588
+ }
589
+
590
+ function readLatestCheckpoint(name) {
591
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
592
+ let files;
593
+ try {
594
+ files = readdirSync(CHECKPOINTS_DIR).filter(f => f.endsWith('.json'));
595
+ } catch {
596
+ return null;
597
+ }
598
+
599
+ const matches = [];
600
+ for (const file of files) {
601
+ const basePart = file.replace(/\.json$/, '').split('--')[0];
602
+ if (basePart === safeName) {
603
+ try {
604
+ const data = JSON.parse(readFileSync(resolve(CHECKPOINTS_DIR, file), 'utf8'));
605
+ matches.push(data);
606
+ } catch { continue; }
607
+ }
608
+ }
609
+
610
+ if (matches.length === 0) return null;
611
+
612
+ matches.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
613
+
614
+ const age = Date.now() - new Date(matches[0].createdAt).getTime();
615
+ if (age > CHECKPOINT_MAX_AGE_MS) return null;
616
+
617
+ return matches[0];
618
+ }
619
+
620
+ async function handleSaveCheckpoint(args) {
621
+ if (!agentName) return { error: 'Register with a name first (register_agent) before saving checkpoints' };
622
+
623
+ const checkpoint = {
624
+ task: args.task || null,
625
+ files_touched: args.files_touched || [],
626
+ key_decisions: args.key_decisions || [],
627
+ notes: args.notes || ''
628
+ };
629
+
630
+ const path = writeCheckpointFile(agentName, agentId, checkpoint);
631
+
632
+ return {
633
+ saved: true,
634
+ agentName,
635
+ project: process.cwd().split(/[\\/]/).pop(),
636
+ createdAt: new Date().toISOString(),
637
+ path
638
+ };
639
+ }
640
+
641
+ async function handleLoadCheckpoint(args) {
642
+ const name = args.from || agentName;
643
+ if (!name) return { error: 'Provide agent name via "from" parameter, or register first' };
644
+
645
+ const checkpoint = readLatestCheckpoint(name);
646
+ if (!checkpoint) {
647
+ return { found: false, error: `No recent checkpoint found for "${name}" (max age: 24h)` };
648
+ }
649
+
650
+ const ageMin = Math.round((Date.now() - new Date(checkpoint.createdAt).getTime()) / 1000 / 60);
651
+ return {
652
+ found: true,
653
+ ageMinutes: ageMin,
654
+ checkpoint
655
+ };
656
+ }
657
+
658
+ // ============================================================
659
+ // Tool Handlers — Memory (calls newcode HTTP server)
660
+ // ============================================================
661
+
662
+ async function handleRetrieveMemories(args) {
663
+ const query = args.query;
664
+ if (!query) return { error: 'query is required' };
665
+ const userId = args.user_id || 'lloyd';
666
+ const project = process.cwd().split(/[\\/]/).pop();
667
+
668
+ const SCORE_THRESHOLD = 0.25;
669
+ const MAX_RESULTS = 10;
670
+
671
+ try {
672
+ const data = await memoryFetch('/retrieve', {
673
+ query,
674
+ user_id: userId,
675
+ conversation_id: sessionConversationId,
676
+ project,
677
+ limit: MAX_RESULTS
678
+ }, 30000);
679
+
680
+ const raw = data.memories || [];
681
+ const filtered = raw
682
+ .filter(m => (m.score || 0) >= SCORE_THRESHOLD)
683
+ .slice(0, MAX_RESULTS)
684
+ .map((m, i) => ({
685
+ memory: m.memory,
686
+ score: Math.round((m.score || 0) * 1000) / 1000,
687
+ rank: i + 1
688
+ }));
689
+
690
+ return {
691
+ memories: filtered,
692
+ conversation_id: sessionConversationId,
693
+ ...(filtered.length === 0 && raw.length > 0
694
+ ? { note: `${raw.length} memories found but none above relevance threshold (${SCORE_THRESHOLD})` }
695
+ : {})
696
+ };
697
+ } catch (err) {
698
+ return { error: err.message, conversation_id: sessionConversationId };
699
+ }
700
+ }
701
+
702
+ async function handleStoreConversation(args) {
703
+ const userMessage = args.user_message;
704
+ const assistantResponse = args.assistant_response;
705
+ if (!userMessage || !assistantResponse) {
706
+ return { error: 'Both user_message and assistant_response are required' };
707
+ }
708
+ const userId = args.user_id || 'lloyd';
709
+ const convId = args.conversation_id || sessionConversationId;
710
+ const project = process.cwd().split(/[\\/]/).pop();
711
+
712
+ try {
713
+ const data = await memoryFetch('/ingest', {
714
+ messages: [
715
+ { role: 'user', content: userMessage },
716
+ { role: 'assistant', content: assistantResponse }
717
+ ],
718
+ user_id: userId,
719
+ conversation_id: convId,
720
+ project
721
+ }, 60000);
722
+
723
+ return { status: data.status || 'stored', conversation_id: convId };
724
+ } catch (err) {
725
+ return { error: err.message, conversation_id: convId };
726
+ }
727
+ }
728
+
729
+ async function handleJudgeMemories(args) {
730
+ const convId = args.conversation_id || sessionConversationId;
731
+ const queryText = args.query_text;
732
+ const responseText = args.response_text;
733
+ if (!queryText || !responseText) {
734
+ return { error: 'Both query_text and response_text are required' };
735
+ }
736
+ const userId = args.user_id || 'lloyd';
737
+
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 };
749
+ }
750
+ }
751
+
752
+ // ============================================================
753
+ // Hook Helpers
754
+ // ============================================================
755
+
756
+ function resolveAgentName(cwd) {
757
+ if (process.env.MEVORIC_AGENT_NAME) return process.env.MEVORIC_AGENT_NAME;
758
+ if (process.env.AGENT_BRIDGE_NAME) return process.env.AGENT_BRIDGE_NAME;
759
+
760
+ if (cwd) {
761
+ const mcpPath = resolve(cwd, '.mcp.json');
762
+ try {
763
+ const mcp = JSON.parse(readFileSync(mcpPath, 'utf8'));
764
+ // Check both mevoric and legacy agent-bridge entries
765
+ const entry = mcp.mcpServers?.['mevoric'] || mcp.mcpServers?.['agent-bridge'];
766
+ const name = entry?.env?.MEVORIC_AGENT_NAME || entry?.env?.AGENT_BRIDGE_NAME;
767
+ if (name) return name;
768
+ } catch {}
769
+ }
770
+
771
+ try {
772
+ const files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
773
+ for (const file of files) {
774
+ const agent = readAgentFile(resolve(AGENTS_DIR, file));
775
+ if (agent?.name && agent.cwd === cwd) return agent.name;
776
+ }
777
+ } catch {}
778
+
779
+ return null;
780
+ }
781
+
782
+ function readMessagesForAgent(name, cursorTimestamp) {
783
+ let files;
784
+ try {
785
+ files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json')).sort();
786
+ } catch {
787
+ return { messages: [], newCursor: cursorTimestamp };
788
+ }
789
+
790
+ const pending = [];
791
+ let newCursor = cursorTimestamp;
792
+ const nameLower = name.toLowerCase();
793
+
794
+ for (const file of files) {
795
+ const timestamp = parseInt(file.split('-')[0], 10);
796
+ if (isNaN(timestamp) || timestamp <= cursorTimestamp) continue;
797
+
798
+ const filePath = resolve(MESSAGES_DIR, file);
799
+ let msg;
800
+ try { msg = JSON.parse(readFileSync(filePath, 'utf8')); } catch { continue; }
801
+
802
+ if (msg.fromName?.toLowerCase() === nameLower) continue;
803
+
804
+ const isForMe = msg.toName?.toLowerCase() === nameLower || msg.to?.toLowerCase() === nameLower;
805
+ const isBroadcast = msg.broadcast && msg.to === '*';
806
+
807
+ if (isForMe || isBroadcast) {
808
+ pending.push(msg);
809
+ }
810
+
811
+ if (timestamp > newCursor) newCursor = timestamp;
812
+ }
813
+
814
+ return { messages: pending, newCursor };
815
+ }
816
+
817
+ function readCursor(name) {
818
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
819
+ const cursorPath = resolve(CURSORS_DIR, `${safeName}.cursor`);
820
+ try {
821
+ return parseInt(readFileSync(cursorPath, 'utf8').trim(), 10) || 0;
822
+ } catch {
823
+ return 0;
824
+ }
825
+ }
826
+
827
+ function writeCursor(name, timestamp) {
828
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
829
+ const cursorPath = resolve(CURSORS_DIR, `${safeName}.cursor`);
830
+ const tmpPath = cursorPath + '.tmp';
831
+ try {
832
+ writeFileSync(tmpPath, String(timestamp));
833
+ renameSync(tmpPath, cursorPath);
834
+ } catch {
835
+ try { writeFileSync(cursorPath, String(timestamp)); } catch {}
836
+ }
837
+ }
838
+
839
+ // ============================================================
840
+ // System tag stripping (shared by hooks)
841
+ // ============================================================
842
+
843
+ const TAG_PATTERNS = [
844
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
845
+ /<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g,
846
+ /<ide_selection>[\s\S]*?<\/ide_selection>/g,
847
+ /<task-notification>[\s\S]*?<\/task-notification>/g,
848
+ ];
849
+
850
+ function stripSystemTags(text) {
851
+ let clean = text;
852
+ for (const pat of TAG_PATTERNS) clean = clean.replace(pat, '');
853
+ return clean.replace(/\n\s*\n/g, '\n').trim();
854
+ }
855
+
856
+ // ============================================================
857
+ // Tool Definitions
858
+ // ============================================================
859
+
860
+ const TOOLS = [
861
+ // --- Memory tools ---
862
+ {
863
+ name: 'retrieve_memories',
864
+ description: 'Search for memories relevant to a query.\nReturns memories ranked by relevance with feedback adjustments.\nCall this before responding to get context from past conversations.',
865
+ inputSchema: {
866
+ type: 'object',
867
+ properties: {
868
+ query: { type: 'string', description: 'The search query' },
869
+ user_id: { type: 'string', default: 'lloyd', description: 'User ID (default: lloyd)' }
870
+ },
871
+ required: ['query']
872
+ }
873
+ },
874
+ {
875
+ name: 'store_conversation',
876
+ description: 'Store memories from a conversation exchange.\nExtracts facts, preferences, and rules from both the user message and assistant response.\nCall this after responding to save what was learned.\n\nIMPORTANT: Pass the COMPLETE user message and COMPLETE assistant response.\nDo NOT summarize or truncate. The full text is needed for accurate memory extraction.',
877
+ inputSchema: {
878
+ type: 'object',
879
+ properties: {
880
+ user_message: { type: 'string', description: 'The complete user message' },
881
+ assistant_response: { type: 'string', description: 'The complete assistant response' },
882
+ user_id: { type: 'string', default: 'lloyd', description: 'User ID (default: lloyd)' },
883
+ conversation_id: { type: 'string', default: '', description: 'Conversation ID (uses session ID if blank)' }
884
+ },
885
+ required: ['user_message', 'assistant_response']
886
+ }
887
+ },
888
+ {
889
+ name: 'judge_memories',
890
+ description: 'Run the feedback judge on memories retrieved during a conversation.\nEvaluates whether each retrieved memory was useful, correct, or irrelevant.\nCall this after responding to improve future retrieval.',
891
+ inputSchema: {
892
+ type: 'object',
893
+ properties: {
894
+ conversation_id: { type: 'string', description: 'Conversation ID from the retrieval session' },
895
+ query_text: { type: 'string', description: 'The original query text' },
896
+ response_text: { type: 'string', description: 'The response text that used the memories' },
897
+ user_id: { type: 'string', default: 'lloyd', description: 'User ID (default: lloyd)' }
898
+ },
899
+ required: ['conversation_id', 'query_text', 'response_text']
900
+ }
901
+ },
902
+ // --- Bridge tools ---
903
+ {
904
+ name: 'register_agent',
905
+ description: 'Register this agent with a human-readable name so other agents can find and message you. Call this at the start of a session.',
906
+ inputSchema: {
907
+ type: 'object',
908
+ properties: {
909
+ name: {
910
+ type: 'string',
911
+ description: 'A short human-readable name for this agent (e.g., "frontend-dev", "reviewer", "planner")'
912
+ }
913
+ },
914
+ required: ['name']
915
+ }
916
+ },
917
+ {
918
+ name: 'list_agents',
919
+ description: 'List all active agents across all tabs. Shows agent ID, name, project, and status.',
920
+ inputSchema: {
921
+ type: 'object',
922
+ properties: {}
923
+ }
924
+ },
925
+ {
926
+ name: 'send_message',
927
+ description: 'Send a message to a specific agent by name or ID. Use list_agents first to see who is available.',
928
+ inputSchema: {
929
+ type: 'object',
930
+ properties: {
931
+ to: { type: 'string', description: 'The name or ID of the target agent' },
932
+ content: { type: 'string', description: 'The message content to send' }
933
+ },
934
+ required: ['to', 'content']
935
+ }
936
+ },
937
+ {
938
+ name: 'read_messages',
939
+ description: 'Check for new messages sent to you since your last read. Returns unread messages in chronological order.',
940
+ inputSchema: {
941
+ type: 'object',
942
+ properties: {
943
+ include_broadcasts: { type: 'boolean', description: 'Whether to include broadcast messages (default: true)' }
944
+ }
945
+ }
946
+ },
947
+ {
948
+ name: 'broadcast',
949
+ description: 'Send a message to ALL active agents. Use sparingly — prefer direct messages when you know the recipient.',
950
+ inputSchema: {
951
+ type: 'object',
952
+ properties: {
953
+ content: { type: 'string', description: 'The message content to broadcast to all agents' }
954
+ },
955
+ required: ['content']
956
+ }
957
+ },
958
+ {
959
+ name: 'share_context',
960
+ description: 'Share your accumulated working knowledge so other agents (even in other projects) can read it. Write a comprehensive dump of everything you know — files read, decisions made, key facts, current state. Context persists after your session ends.',
961
+ inputSchema: {
962
+ type: 'object',
963
+ properties: {
964
+ content: { type: 'string', description: 'Freeform text dump of everything you know that would be useful to another agent picking up where you left off' }
965
+ },
966
+ required: ['content']
967
+ }
968
+ },
969
+ {
970
+ name: 'get_context',
971
+ description: 'Read shared context from another agent. Works even if that agent is no longer active — context persists across sessions. Call with no arguments to see all available contexts.',
972
+ inputSchema: {
973
+ type: 'object',
974
+ properties: {
975
+ from: { type: 'string', description: 'Name of the agent whose context you want to read (e.g., "nova-main"). Omit to get all shared contexts.' }
976
+ }
977
+ }
978
+ },
979
+ {
980
+ name: 'save_checkpoint',
981
+ description: 'Save a structured checkpoint of your current working state. Use this before context gets compressed, when switching tasks, or periodically during long sessions. The checkpoint will be auto-loaded when a new session starts with the same agent name.',
982
+ inputSchema: {
983
+ type: 'object',
984
+ properties: {
985
+ task: {
986
+ type: 'object',
987
+ description: 'Current task state',
988
+ properties: {
989
+ description: { type: 'string', description: 'What you are working on' },
990
+ status: { type: 'string', description: 'in_progress, blocked, or completed' },
991
+ steps_completed: { type: 'array', items: { type: 'string' }, description: 'Steps already done' },
992
+ steps_remaining: { type: 'array', items: { type: 'string' }, description: 'Steps left to do' }
993
+ }
994
+ },
995
+ files_touched: { type: 'array', items: { type: 'string' }, description: 'File paths you have read or modified' },
996
+ key_decisions: { type: 'array', items: { type: 'string' }, description: 'Important decisions made during this session' },
997
+ notes: { type: 'string', description: 'Freeform notes — anything that does not fit the structured fields' }
998
+ }
999
+ }
1000
+ },
1001
+ {
1002
+ name: 'load_checkpoint',
1003
+ description: 'Load the most recent checkpoint for an agent. Defaults to your own checkpoints. Returns null if no checkpoint exists or if the most recent one is older than 24 hours.',
1004
+ inputSchema: {
1005
+ type: 'object',
1006
+ properties: {
1007
+ from: { type: 'string', description: 'Agent name to load checkpoint for (defaults to your own name)' }
1008
+ }
1009
+ }
1010
+ }
1011
+ ];
1012
+
1013
+ // ============================================================
1014
+ // Tool Dispatcher
1015
+ // ============================================================
1016
+
1017
+ async function handleToolCall(name, args) {
1018
+ switch (name) {
1019
+ // Memory
1020
+ case 'retrieve_memories': return handleRetrieveMemories(args);
1021
+ case 'store_conversation': return handleStoreConversation(args);
1022
+ case 'judge_memories': return handleJudgeMemories(args);
1023
+ // Bridge
1024
+ case 'register_agent': return handleRegister(args);
1025
+ case 'list_agents': return handleListAgents();
1026
+ case 'send_message': return handleSendMessage(args);
1027
+ case 'read_messages': return handleReadMessages(args);
1028
+ case 'broadcast': return handleBroadcast(args);
1029
+ // Context
1030
+ case 'share_context': return handleShareContext(args);
1031
+ case 'get_context': return handleGetContext(args);
1032
+ // Checkpoints
1033
+ case 'save_checkpoint': return handleSaveCheckpoint(args);
1034
+ case 'load_checkpoint': return handleLoadCheckpoint(args);
1035
+ default: return { error: `Unknown tool: ${name}` };
1036
+ }
1037
+ }
1038
+
1039
+ // ============================================================
1040
+ // MCP Server
1041
+ // ============================================================
1042
+
1043
+ const server = new Server(
1044
+ { name: 'mevoric', version: '2.0.0' },
1045
+ { capabilities: { tools: {} } }
1046
+ );
1047
+
1048
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1049
+ tools: TOOLS
1050
+ }));
1051
+
1052
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1053
+ const { name, arguments: args } = request.params;
1054
+
1055
+ try {
1056
+ const result = await handleToolCall(name, args || {});
1057
+ return {
1058
+ content: [{
1059
+ type: 'text',
1060
+ text: JSON.stringify(result, null, 2)
1061
+ }]
1062
+ };
1063
+ } catch (err) {
1064
+ return {
1065
+ content: [{
1066
+ type: 'text',
1067
+ text: JSON.stringify({
1068
+ error: `Tool execution failed: ${err.message}`,
1069
+ tool: name,
1070
+ args
1071
+ }, null, 2)
1072
+ }],
1073
+ isError: true
1074
+ };
1075
+ }
1076
+ });
1077
+
1078
+ // ============================================================
1079
+ // CLI: --capture-prompt (UserPromptSubmit hook mode)
1080
+ // ============================================================
1081
+
1082
+ async function runCapturePrompt() {
1083
+ const chunks = [];
1084
+ for await (const chunk of process.stdin) chunks.push(chunk);
1085
+ const raw = Buffer.concat(chunks).toString('utf8');
1086
+
1087
+ let data;
1088
+ try { data = JSON.parse(raw); } catch { process.exit(0); }
1089
+
1090
+ const sessionId = data.session_id || '';
1091
+ const prompt = data.prompt || '';
1092
+ if (!sessionId || !prompt) process.exit(0);
1093
+
1094
+ const clean = stripSystemTags(prompt);
1095
+ if (clean.length < 5) process.exit(0);
1096
+
1097
+ const tmp = tmpdir();
1098
+ writeFileSync(resolve(tmp, `mevoric-prompt-${sessionId}`), clean, 'utf8');
1099
+ process.exit(0);
1100
+ }
1101
+
1102
+ // ============================================================
1103
+ // CLI: --ingest (Stop hook mode)
1104
+ // ============================================================
1105
+ // Unified: saves context + auto-checkpoint + POSTs to memory server
1106
+
1107
+ async function runIngest() {
1108
+ ensureDirs();
1109
+
1110
+ const chunks = [];
1111
+ for await (const chunk of process.stdin) chunks.push(chunk);
1112
+ const raw = Buffer.concat(chunks).toString('utf8');
1113
+
1114
+ let data;
1115
+ try { data = JSON.parse(raw); } catch { process.exit(0); }
1116
+
1117
+ const sessionId = data.session_id || '';
1118
+ const assistantMsg = data.last_assistant_message || '';
1119
+ if (!sessionId || !assistantMsg) process.exit(0);
1120
+
1121
+ // Read user prompt saved by --capture-prompt
1122
+ const tmp = tmpdir();
1123
+ const promptPath = resolve(tmp, `mevoric-prompt-${sessionId}`);
1124
+ let userMsg = '';
1125
+ try {
1126
+ userMsg = readFileSync(promptPath, 'utf8');
1127
+ } catch {}
1128
+
1129
+ const cleanAssistant = stripSystemTags(assistantMsg);
1130
+ if (!cleanAssistant || cleanAssistant.length < 50) process.exit(0);
1131
+
1132
+ const name = process.env.MEVORIC_AGENT_NAME || process.env.AGENT_BRIDGE_NAME || findAgentNameForSession(sessionId);
1133
+ if (!name) process.exit(0);
1134
+
1135
+ // --- 1. Save context file (agent-bridge behavior) ---
1136
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
1137
+ const ctxPath = resolve(CONTEXT_DIR, `${safeName}--${sessionId}.json`);
1138
+
1139
+ let existing = { exchanges: [] };
1140
+ try {
1141
+ const prev = JSON.parse(readFileSync(ctxPath, 'utf8'));
1142
+ if (prev.exchanges) existing = prev;
1143
+ else if (prev.content) existing = { exchanges: [{ role: 'context', content: prev.content }] };
1144
+ } catch {}
1145
+
1146
+ existing.exchanges.push({
1147
+ timestamp: new Date().toISOString(),
1148
+ user: userMsg.slice(0, 2000),
1149
+ assistant: cleanAssistant.slice(0, 5000)
1150
+ });
1151
+ if (existing.exchanges.length > 20) {
1152
+ existing.exchanges = existing.exchanges.slice(-20);
1153
+ }
1154
+
1155
+ const ctxData = JSON.stringify({
1156
+ agentName: name,
1157
+ baseName: name,
1158
+ project: process.cwd().split(/[\\/]/).pop(),
1159
+ updatedAt: new Date().toISOString(),
1160
+ sessionId,
1161
+ exchanges: existing.exchanges
1162
+ }, null, 2);
1163
+
1164
+ const tmpCtx = ctxPath + '.tmp';
1165
+ try {
1166
+ writeFileSync(tmpCtx, ctxData);
1167
+ renameSync(tmpCtx, ctxPath);
1168
+ } catch {
1169
+ try { writeFileSync(ctxPath, ctxData); } catch {}
1170
+ }
1171
+
1172
+ // --- 2. Auto-save checkpoint ---
1173
+ try {
1174
+ mkdirSync(CHECKPOINTS_DIR, { recursive: true });
1175
+ const cpData = JSON.stringify({
1176
+ version: 1,
1177
+ agentName: name,
1178
+ project: process.cwd().split(/[\\/]/).pop(),
1179
+ sessionId,
1180
+ createdAt: new Date().toISOString(),
1181
+ auto: true,
1182
+ task: {
1183
+ description: userMsg.slice(0, 200) || 'Session ended',
1184
+ status: 'interrupted'
1185
+ },
1186
+ files_touched: [],
1187
+ key_decisions: [],
1188
+ notes: cleanAssistant.slice(0, 500)
1189
+ }, null, 2);
1190
+ const cpPath = resolve(CHECKPOINTS_DIR, `${safeName}--${sessionId}.json`);
1191
+ const cpTmp = cpPath + '.tmp';
1192
+ writeFileSync(cpTmp, cpData);
1193
+ renameSync(cpTmp, cpPath);
1194
+ } catch {}
1195
+
1196
+ // --- 3. POST to memory server /ingest (ported from Python auto-ingest.py) ---
1197
+ if (userMsg && cleanAssistant) {
1198
+ // Read conversation ID from temp file (written by MCP server process)
1199
+ let convId = '';
1200
+ try {
1201
+ convId = readFileSync(resolve(tmp, 'mevoric-convid'), 'utf8').trim();
1202
+ } catch {}
1203
+ if (!convId) convId = sessionId; // fallback
1204
+
1205
+ try {
1206
+ const project = process.cwd().split(/[\\/]/).pop();
1207
+ const controller = new AbortController();
1208
+ const timer = setTimeout(() => controller.abort(), 15000);
1209
+ await fetch(`${MEMORY_SERVER_URL}/ingest`, {
1210
+ method: 'POST',
1211
+ headers: { 'Content-Type': 'application/json' },
1212
+ body: JSON.stringify({
1213
+ messages: [
1214
+ { role: 'user', content: userMsg.slice(0, 10000) },
1215
+ { role: 'assistant', content: cleanAssistant.slice(0, 10000) }
1216
+ ],
1217
+ user_id: 'lloyd',
1218
+ conversation_id: convId,
1219
+ project
1220
+ }),
1221
+ signal: controller.signal
1222
+ });
1223
+ clearTimeout(timer);
1224
+ } catch {} // Best-effort — don't block session exit
1225
+
1226
+ // --- 4. POST to memory server /feedback (fire-and-forget) ---
1227
+ try {
1228
+ const controller2 = new AbortController();
1229
+ const timer2 = setTimeout(() => controller2.abort(), 5000);
1230
+ await fetch(`${MEMORY_SERVER_URL}/feedback`, {
1231
+ method: 'POST',
1232
+ headers: { 'Content-Type': 'application/json' },
1233
+ body: JSON.stringify({
1234
+ conversation_id: convId,
1235
+ user_id: 'lloyd',
1236
+ query_text: userMsg.slice(0, 5000),
1237
+ response_text: cleanAssistant.slice(0, 5000)
1238
+ }),
1239
+ signal: controller2.signal
1240
+ });
1241
+ clearTimeout(timer2);
1242
+ } catch {} // Best-effort
1243
+ }
1244
+
1245
+ process.exit(0);
1246
+ }
1247
+
1248
+ function findAgentNameForSession(sessionId) {
1249
+ try {
1250
+ const files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
1251
+ const cwd = process.cwd();
1252
+ for (const file of files) {
1253
+ const agent = readAgentFile(resolve(AGENTS_DIR, file));
1254
+ if (agent && agent.name && agent.cwd === cwd) return agent.name;
1255
+ }
1256
+ } catch {}
1257
+ return null;
1258
+ }
1259
+
1260
+ // ============================================================
1261
+ // CLI: --check-messages (UserPromptSubmit hook mode)
1262
+ // ============================================================
1263
+
1264
+ async function runCheckMessages() {
1265
+ ensureDirs();
1266
+
1267
+ const chunks = [];
1268
+ for await (const chunk of process.stdin) chunks.push(chunk);
1269
+ const raw = Buffer.concat(chunks).toString('utf8');
1270
+
1271
+ let data;
1272
+ try { data = JSON.parse(raw); } catch { process.exit(0); }
1273
+
1274
+ const cwd = data.cwd || process.cwd();
1275
+ const name = resolveAgentName(cwd);
1276
+ if (!name) process.exit(0);
1277
+
1278
+ const cursor = readCursor(name);
1279
+ const { messages, newCursor } = readMessagesForAgent(name, cursor);
1280
+
1281
+ if (newCursor > cursor) {
1282
+ writeCursor(name, newCursor);
1283
+ }
1284
+
1285
+ if (messages.length === 0) {
1286
+ process.exit(0);
1287
+ }
1288
+
1289
+ const formatted = messages.map(m => {
1290
+ const from = m.fromName || m.from;
1291
+ const age = Math.round((Date.now() - new Date(m.timestamp).getTime()) / 1000);
1292
+ const ageStr = age < 60 ? `${age}s ago` : `${Math.round(age / 60)}min ago`;
1293
+ return `[MESSAGE FROM ${from}]: ${m.content} (${ageStr})`;
1294
+ }).join('\n\n');
1295
+
1296
+ const output = {
1297
+ hookSpecificOutput: {
1298
+ hookEventName: data.hook_event_name || 'UserPromptSubmit',
1299
+ additionalContext: `--- INCOMING AGENT MESSAGES (${messages.length}) ---\n${formatted}\n--- END AGENT MESSAGES ---\nYou received ${messages.length} message(s) from other agents. Read and respond to them. Use send_message to reply if needed.`
1300
+ }
1301
+ };
1302
+
1303
+ process.stdout.write(JSON.stringify(output));
1304
+ process.exit(0);
1305
+ }
1306
+
1307
+ // ============================================================
1308
+ // CLI: --bootstrap-context (SessionStart hook mode)
1309
+ // ============================================================
1310
+
1311
+ async function runBootstrapContext() {
1312
+ ensureDirs();
1313
+
1314
+ const chunks = [];
1315
+ for await (const chunk of process.stdin) chunks.push(chunk);
1316
+ const raw = Buffer.concat(chunks).toString('utf8');
1317
+
1318
+ let data;
1319
+ try { data = JSON.parse(raw); } catch { process.exit(0); }
1320
+
1321
+ const cwd = data.cwd || process.cwd();
1322
+ const myName = resolveAgentName(cwd);
1323
+ if (!myName) process.exit(0);
1324
+
1325
+ const myNameLower = myName.toLowerCase();
1326
+
1327
+ let activeAgents = [];
1328
+ try {
1329
+ const agentFiles = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
1330
+ for (const file of agentFiles) {
1331
+ try {
1332
+ const agent = JSON.parse(readFileSync(resolve(AGENTS_DIR, file), 'utf8'));
1333
+ if (agent.name && agent.name.toLowerCase() !== myNameLower && isProcessAlive(agent.pid)) {
1334
+ activeAgents.push(agent);
1335
+ }
1336
+ } catch {}
1337
+ }
1338
+ } catch {}
1339
+
1340
+ const mySessionId = data.session_id || '';
1341
+ const contexts = readAllContextFiles().filter(c => {
1342
+ if (c.sessionId && mySessionId && c.sessionId === mySessionId) return false;
1343
+ if (c.agentId && c.agentId === myName) return false;
1344
+ return true;
1345
+ });
1346
+
1347
+ const cursor = readCursor(myName);
1348
+ const { messages, newCursor } = readMessagesForAgent(myName, cursor);
1349
+ if (newCursor > cursor) {
1350
+ writeCursor(myName, newCursor);
1351
+ }
1352
+
1353
+ const checkpoint = readLatestCheckpoint(myName);
1354
+
1355
+ if (activeAgents.length === 0 && contexts.length === 0 && messages.length === 0 && !checkpoint) {
1356
+ process.exit(0);
1357
+ }
1358
+
1359
+ const parts = [];
1360
+
1361
+ if (checkpoint) {
1362
+ const ageMin = Math.round((Date.now() - new Date(checkpoint.createdAt).getTime()) / 1000 / 60);
1363
+ const cpParts = [`--- CHECKPOINT (from previous session, ${ageMin}min ago) ---`];
1364
+ if (checkpoint.task) {
1365
+ if (checkpoint.task.description) cpParts.push(`Task: ${checkpoint.task.description}`);
1366
+ if (checkpoint.task.status) cpParts.push(`Status: ${checkpoint.task.status}`);
1367
+ if (checkpoint.task.steps_completed?.length) cpParts.push(`Completed: ${checkpoint.task.steps_completed.join(', ')}`);
1368
+ if (checkpoint.task.steps_remaining?.length) cpParts.push(`Remaining: ${checkpoint.task.steps_remaining.join(', ')}`);
1369
+ }
1370
+ if (checkpoint.files_touched?.length) cpParts.push(`Files: ${checkpoint.files_touched.join(', ')}`);
1371
+ if (checkpoint.key_decisions?.length) cpParts.push(`Decisions: ${checkpoint.key_decisions.join('; ')}`);
1372
+ if (checkpoint.notes) cpParts.push(`Notes: ${checkpoint.notes.slice(0, 1000)}`);
1373
+ cpParts.push('--- END CHECKPOINT ---');
1374
+ parts.push(cpParts.join('\n'));
1375
+ }
1376
+
1377
+ if (activeAgents.length > 0) {
1378
+ parts.push(`ACTIVE AGENTS: ${activeAgents.map(a => `${a.name} (${a.project})`).join(', ')}`);
1379
+ }
1380
+
1381
+ for (const ctx of contexts) {
1382
+ const ageMin = Math.round((Date.now() - new Date(ctx.updatedAt).getTime()) / 1000 / 60);
1383
+ let summary = '';
1384
+ if (ctx.content) {
1385
+ summary = ctx.content.slice(0, 3000);
1386
+ } else if (ctx.exchanges && ctx.exchanges.length > 0) {
1387
+ const recent = ctx.exchanges.slice(-3);
1388
+ summary = recent.map(e =>
1389
+ `User: ${(e.user || '').slice(0, 200)}\nAssistant: ${(e.assistant || '').slice(0, 500)}`
1390
+ ).join('\n---\n');
1391
+ }
1392
+ if (summary) {
1393
+ parts.push(`--- CONTEXT FROM ${ctx.agentName} (${ctx.project || 'unknown'}, updated ${ageMin}min ago) ---\n${summary}`);
1394
+ }
1395
+ }
1396
+
1397
+ if (messages.length > 0) {
1398
+ const formatted = messages.map(m => {
1399
+ const from = m.fromName || m.from;
1400
+ const age = Math.round((Date.now() - new Date(m.timestamp).getTime()) / 1000);
1401
+ const ageStr = age < 60 ? `${age}s ago` : `${Math.round(age / 60)}min ago`;
1402
+ return `[MESSAGE FROM ${from}]: ${m.content} (${ageStr})`;
1403
+ }).join('\n\n');
1404
+ parts.push(`--- PENDING MESSAGES (${messages.length}) ---\n${formatted}`);
1405
+ }
1406
+
1407
+ const output = {
1408
+ hookSpecificOutput: {
1409
+ hookEventName: data.hook_event_name || 'SessionStart',
1410
+ additionalContext: `--- MEVORIC BOOTSTRAP ---\nYou are "${myName}". Mevoric connects you with other Claude Code sessions and provides persistent memory. Messages from other agents are delivered automatically before each prompt.\n\n${parts.join('\n\n')}\n--- END BOOTSTRAP ---`
1411
+ }
1412
+ };
1413
+
1414
+ process.stdout.write(JSON.stringify(output));
1415
+ process.exit(0);
1416
+ }
1417
+
1418
+ // ============================================================
1419
+ // Main
1420
+ // ============================================================
1421
+
1422
+ if (process.argv.includes('--capture-prompt')) {
1423
+ runCapturePrompt().catch(() => process.exit(0));
1424
+ } else if (process.argv.includes('--ingest')) {
1425
+ runIngest().catch(() => process.exit(0));
1426
+ } else if (process.argv.includes('--check-messages')) {
1427
+ runCheckMessages().catch(() => process.exit(0));
1428
+ } else if (process.argv.includes('--bootstrap-context')) {
1429
+ runBootstrapContext().catch(() => process.exit(0));
1430
+ } else {
1431
+ async function main() {
1432
+ ensureDirs();
1433
+ cleanOldMessages();
1434
+
1435
+ if (agentName) {
1436
+ const existing = getAllAgents().filter(a => a.status === 'active' && a.name?.toLowerCase() === agentName.toLowerCase());
1437
+ if (existing.length > 0) {
1438
+ const allNames = getAllAgents().map(a => a.name?.toLowerCase()).filter(Boolean);
1439
+ let suffix = 2;
1440
+ while (allNames.includes(`${agentName.toLowerCase()}-${suffix}`)) suffix++;
1441
+ agentName = `${agentBaseName}-${suffix}`;
1442
+ }
1443
+ }
1444
+
1445
+ writeAgentFile();
1446
+ startHeartbeat();
1447
+
1448
+ const cleanup = () => {
1449
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
1450
+ removeAgentFile();
1451
+ };
1452
+ process.on('exit', cleanup);
1453
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
1454
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
1455
+
1456
+ const transport = new StdioServerTransport();
1457
+ await server.connect(transport);
1458
+
1459
+ console.error(`[Mevoric] Server running`);
1460
+ console.error(`[Mevoric] Agent ID: ${agentId}`);
1461
+ console.error(`[Mevoric] Data dir: ${DATA_DIR}`);
1462
+ console.error(`[Mevoric] Memory server: ${MEMORY_SERVER_URL}`);
1463
+ }
1464
+
1465
+ main().catch(err => {
1466
+ console.error(`[Mevoric] Fatal: ${err.message}`);
1467
+ process.exit(1);
1468
+ });
1469
+ }