bus-agent 2.3.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/lib/bus.js ADDED
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Agent Bus — Universal MCP Agent Communication Hub v2.1
3
+ *
4
+ * Any MCP-compatible agent connects via mcporter and can:
5
+ * - Register with rich profile (model, capabilities, status)
6
+ * - Discover agents by capability/model/tags
7
+ * - Send/receive messages (DM, broadcast, channels)
8
+ * - Long-poll for new messages
9
+ * - Get notified when agents join/leave the bus
10
+ */
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { EventEmitter } = require('events');
14
+
15
+ const DATA_DIR = path.join(__dirname, '..', '.bus');
16
+ const AGENTS_FILE = path.join(DATA_DIR, 'agents.json');
17
+ const MSGS_DIR = path.join(DATA_DIR, 'messages');
18
+ const CHANNELS_DIR = path.join(DATA_DIR, 'channels');
19
+ const EVENTS_DIR = path.join(DATA_DIR, 'events');
20
+
21
+ const OFFLINE_TIMEOUT_MS = 300000; // 5 min
22
+
23
+ class AgentBus extends EventEmitter {
24
+ constructor() {
25
+ super();
26
+ for (const d of [DATA_DIR, MSGS_DIR, CHANNELS_DIR, EVENTS_DIR]) {
27
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
28
+ }
29
+ this._agents = this._loadJson(AGENTS_FILE, {});
30
+ this._waiters = {};
31
+ this._eventWaiters = {};
32
+ this._checkStaleAgents();
33
+ }
34
+
35
+ // ── Profile Schema ────────────────────────────────────
36
+ //
37
+ // Each agent profile includes:
38
+ // name, description,
39
+ // model: { provider, name, context_window },
40
+ // capabilities: [string], // e.g. ["code-review", "web-search", "chat", "filesystem"]
41
+ // tags: [string], // e.g. ["code", "ai", "bot"]
42
+ // status: "idle"|"busy"|"offline",
43
+ // version: string,
44
+ // endpoints: { mcp?, http?, websocket? },
45
+ // tools: [string], // tools this agent exposes
46
+ // last_seen, registered_at
47
+
48
+ registerAgent(name, description = '', metadata = {}) {
49
+ const existing = this._agents[name];
50
+ const profile = {
51
+ name,
52
+ description,
53
+ model: metadata.model || existing?.model || null,
54
+ capabilities: metadata.capabilities || existing?.capabilities || [],
55
+ tags: metadata.tags || existing?.tags || [],
56
+ status: metadata.status || 'idle',
57
+ version: metadata.version || existing?.version || '1.0.0',
58
+ endpoints: metadata.endpoints || existing?.endpoints || {},
59
+ tools: metadata.tools || existing?.tools || [],
60
+ metadata: metadata.metadata || existing?.metadata || {},
61
+ last_seen: new Date().toISOString(),
62
+ registered_at: existing?.registered_at || new Date().toISOString(),
63
+ };
64
+
65
+ const isNew = !existing;
66
+ this._agents[name] = profile;
67
+ this._saveJson(AGENTS_FILE, this._agents);
68
+
69
+ // ── Discovery Broadcast ──
70
+ if (isNew) {
71
+ this._logEvent('system', 'agent_joined', { agent: name, profile });
72
+ this._broadcastSystemMessage(`system:agent_joined`, name, {
73
+ agent: name,
74
+ description,
75
+ capabilities: metadata.capabilities || [],
76
+ timestamp: profile.registered_at,
77
+ });
78
+ // Send welcome DM to the new agent
79
+ this.sendMessage('coco', name,
80
+ `👋 Welcome to CoCo Bus, **${name}**!\n` +
81
+ `You are now connected to ${Object.keys(this._agents).length} agent(s).\n` +
82
+ `Try: agent_list, whoami, or send a message to another agent.`,
83
+ { type: 'system_welcome' }
84
+ );
85
+ this._logSystemWelcome(name);
86
+ }
87
+
88
+ return { registered: true, is_new: isNew, agent: profile };
89
+ }
90
+
91
+ updateProfile(name, updates) {
92
+ if (!this._agents[name]) return { ok: false, error: 'Agent not registered' };
93
+
94
+ const profile = this._agents[name];
95
+ const allowed = ['description', 'model', 'capabilities', 'tags', 'status', 'version', 'endpoints', 'tools', 'metadata'];
96
+ for (const key of allowed) {
97
+ if (updates[key] !== undefined) {
98
+ profile[key] = updates[key];
99
+ }
100
+ }
101
+ profile.last_seen = new Date().toISOString();
102
+ this._saveJson(AGENTS_FILE, this._agents);
103
+ this._logEvent('system', 'profile_updated', { agent: name, changes: Object.keys(updates) });
104
+ return { ok: true, profile };
105
+ }
106
+
107
+ getProfile(name) {
108
+ return this._agents[name] || null;
109
+ }
110
+
111
+ listAgents(filter = {}) {
112
+ let agents = Object.values(this._agents);
113
+
114
+ if (filter.online_only) {
115
+ const cutoff = Date.now() - OFFLINE_TIMEOUT_MS;
116
+ agents = agents.filter(a => new Date(a.last_seen).getTime() > cutoff);
117
+ }
118
+
119
+ if (filter.capability) {
120
+ const cap = filter.capability.toLowerCase();
121
+ agents = agents.filter(a => (a.capabilities || []).some(c => c.toLowerCase().includes(cap)));
122
+ }
123
+
124
+ if (filter.model) {
125
+ const m = filter.model.toLowerCase();
126
+ agents = agents.filter(a => {
127
+ const mdl = a.model?.name || a.model?.provider || '';
128
+ return mdl.toLowerCase().includes(m);
129
+ });
130
+ }
131
+
132
+ if (filter.tag) {
133
+ const tag = filter.tag.toLowerCase();
134
+ agents = agents.filter(a => (a.tags || []).some(t => t.toLowerCase() === tag));
135
+ }
136
+
137
+ if (filter.status) {
138
+ agents = agents.filter(a => a.status === filter.status);
139
+ }
140
+
141
+ if (filter.search) {
142
+ const q = filter.search.toLowerCase();
143
+ agents = agents.filter(a =>
144
+ a.name.toLowerCase().includes(q) ||
145
+ (a.description || '').toLowerCase().includes(q) ||
146
+ (a.capabilities || []).some(c => c.toLowerCase().includes(q)) ||
147
+ (a.tags || []).some(t => t.toLowerCase().includes(q))
148
+ );
149
+ }
150
+
151
+ return agents;
152
+ }
153
+
154
+ getAgent(name) {
155
+ return this._agents[name] || null;
156
+ }
157
+
158
+ heartbeat(name) {
159
+ if (this._agents[name]) {
160
+ const wasOffline = this._agents[name].status === 'offline' ||
161
+ (Date.now() - new Date(this._agents[name].last_seen).getTime()) > OFFLINE_TIMEOUT_MS;
162
+ this._agents[name].last_seen = new Date().toISOString();
163
+ this._agents[name].status = 'idle';
164
+ this._saveJson(AGENTS_FILE, this._agents);
165
+
166
+ if (wasOffline) {
167
+ this._logEvent('system', 'agent_online', { agent: name });
168
+ }
169
+ return { ok: true, online: true };
170
+ }
171
+ return { ok: false, error: 'Agent not registered' };
172
+ }
173
+
174
+ setStatus(name, status) {
175
+ const valid = ['idle', 'busy', 'offline'];
176
+ if (!valid.includes(status)) return { ok: false, error: `Status must be: ${valid.join(', ')}` };
177
+ if (!this._agents[name]) return { ok: false, error: 'Agent not registered' };
178
+
179
+ this._agents[name].status = status;
180
+ this._agents[name].last_seen = new Date().toISOString();
181
+ this._saveJson(AGENTS_FILE, this._agents);
182
+ this._logEvent('system', 'status_change', { agent: name, status });
183
+ return { ok: true, status };
184
+ }
185
+
186
+ // ── Direct Messages ─────────────────────────────────────
187
+
188
+ sendMessage(from, to, message, metadata = {}) {
189
+ const msg = {
190
+ id: this._genId(),
191
+ from,
192
+ to,
193
+ message,
194
+ metadata,
195
+ timestamp: new Date().toISOString(),
196
+ };
197
+
198
+ const inboxDir = path.join(MSGS_DIR, to);
199
+ if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
200
+ fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
201
+
202
+ const outboxDir = path.join(MSGS_DIR, `${from}_outbox`);
203
+ if (!fs.existsSync(outboxDir)) fs.mkdirSync(outboxDir, { recursive: true });
204
+ fs.writeFileSync(path.join(outboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
205
+
206
+ this._wakeWaiters(to, msg);
207
+
208
+ return { sent: true, message_id: msg.id };
209
+ }
210
+
211
+ broadcastMessage(from, message) {
212
+ const results = [];
213
+ for (const agentName of Object.keys(this._agents)) {
214
+ if (agentName !== from) {
215
+ const r = this.sendMessage(from, agentName, message, { broadcast: true });
216
+ results.push({ to: agentName, message_id: r.message_id });
217
+ }
218
+ }
219
+ return { broadcast: true, targets: results.length, results };
220
+ }
221
+
222
+ fetchMessages(agentName, limit = 50, opts = {}) {
223
+ const inboxDir = path.join(MSGS_DIR, agentName);
224
+ if (!fs.existsSync(inboxDir)) return [];
225
+
226
+ let files = fs.readdirSync(inboxDir)
227
+ .filter(f => f.endsWith('.json'))
228
+ .sort();
229
+
230
+ if (opts.after_id) {
231
+ const idx = files.findIndex(f => f.startsWith(opts.after_id));
232
+ if (idx >= 0) files = files.slice(idx + 1);
233
+ }
234
+
235
+ if (opts.from) {
236
+ files = files.filter(f => {
237
+ try {
238
+ const m = JSON.parse(fs.readFileSync(path.join(inboxDir, f), 'utf-8'));
239
+ return m.from === opts.from;
240
+ } catch { return false; }
241
+ });
242
+ }
243
+
244
+ files = files.slice(-limit);
245
+
246
+ return files.map(f => {
247
+ return JSON.parse(fs.readFileSync(path.join(inboxDir, f), 'utf-8'));
248
+ });
249
+ }
250
+
251
+ waitForMessages(agentName, timeoutMs = 30000) {
252
+ return new Promise((resolve, reject) => {
253
+ const timer = setTimeout(() => {
254
+ this._removeWaiter(agentName);
255
+ resolve({ timeout: true, messages: this.fetchMessages(agentName, 10) });
256
+ }, timeoutMs);
257
+
258
+ if (!this._waiters[agentName]) this._waiters[agentName] = [];
259
+ this._waiters[agentName].push({ resolve, timer });
260
+ });
261
+ }
262
+
263
+ deleteMessages(agentName, messageIds) {
264
+ const inboxDir = path.join(MSGS_DIR, agentName);
265
+ if (!fs.existsSync(inboxDir)) return { deleted: 0 };
266
+
267
+ let count = 0;
268
+ for (const id of messageIds) {
269
+ const filePath = path.join(inboxDir, `${id}.json`);
270
+ const filePathAlt = path.join(inboxDir, `${id}`);
271
+ try {
272
+ if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); count++; }
273
+ else if (fs.existsSync(filePathAlt)) { fs.unlinkSync(filePathAlt); count++; }
274
+ } catch {}
275
+ }
276
+ return { deleted: count };
277
+ }
278
+
279
+ // ── Channels ────────────────────────────────────────────
280
+
281
+ createChannel(channelId, topic = '', createdBy = '') {
282
+ if (fs.existsSync(path.join(CHANNELS_DIR, `${channelId}.json`))) {
283
+ return { created: false, error: 'Channel already exists' };
284
+ }
285
+ const channel = {
286
+ id: channelId,
287
+ topic,
288
+ created_by: createdBy,
289
+ created_at: new Date().toISOString(),
290
+ members: createdBy ? [createdBy] : [],
291
+ };
292
+ fs.writeFileSync(path.join(CHANNELS_DIR, `${channelId}.json`), JSON.stringify(channel, null, 2), 'utf-8');
293
+ this._logEvent('system', 'channel_created', { channel: channelId, by: createdBy });
294
+ return { created: true, channel };
295
+ }
296
+
297
+ joinChannel(channelId, agentName) {
298
+ const channelPath = path.join(CHANNELS_DIR, `${channelId}.json`);
299
+ if (!fs.existsSync(channelPath)) return { ok: false, error: 'Channel not found' };
300
+ const channel = JSON.parse(fs.readFileSync(channelPath, 'utf-8'));
301
+ if (!channel.members.includes(agentName)) {
302
+ channel.members.push(agentName);
303
+ this._logEvent('system', 'channel_joined', { channel: channelId, agent: agentName });
304
+ }
305
+ fs.writeFileSync(channelPath, JSON.stringify(channel, null, 2), 'utf-8');
306
+ return { joined: true, channel };
307
+ }
308
+
309
+ leaveChannel(channelId, agentName) {
310
+ const channelPath = path.join(CHANNELS_DIR, `${channelId}.json`);
311
+ if (!fs.existsSync(channelPath)) return { ok: false, error: 'Channel not found' };
312
+ const channel = JSON.parse(fs.readFileSync(channelPath, 'utf-8'));
313
+ channel.members = channel.members.filter(m => m !== agentName);
314
+ fs.writeFileSync(channelPath, JSON.stringify(channel, null, 2), 'utf-8');
315
+ this._logEvent('system', 'channel_left', { channel: channelId, agent: agentName });
316
+ return { left: true, channel };
317
+ }
318
+
319
+ channelSend(channelId, from, message) {
320
+ const channelPath = path.join(CHANNELS_DIR, `${channelId}.json`);
321
+ if (!fs.existsSync(channelPath)) return { ok: false, error: 'Channel not found' };
322
+
323
+ const msg = {
324
+ id: this._genId(),
325
+ channel: channelId,
326
+ from,
327
+ message,
328
+ timestamp: new Date().toISOString(),
329
+ };
330
+
331
+ const logDir = path.join(CHANNELS_DIR, channelId, 'log');
332
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
333
+ fs.writeFileSync(path.join(logDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
334
+
335
+ const channel = JSON.parse(fs.readFileSync(channelPath, 'utf-8'));
336
+ for (const member of channel.members) {
337
+ if (member !== from) {
338
+ this.sendMessage(from, member, `[${channelId}] ${message}`, { channel: channelId });
339
+ }
340
+ }
341
+
342
+ return { sent: true, message_id: msg.id };
343
+ }
344
+
345
+ channelHistory(channelId, limit = 50) {
346
+ const logDir = path.join(CHANNELS_DIR, channelId, 'log');
347
+ if (!fs.existsSync(logDir)) return [];
348
+
349
+ const files = fs.readdirSync(logDir)
350
+ .filter(f => f.endsWith('.json'))
351
+ .sort()
352
+ .slice(-limit);
353
+
354
+ return files.map(f => JSON.parse(fs.readFileSync(path.join(logDir, f), 'utf-8')));
355
+ }
356
+
357
+ // ── System Events / Discovery ────────────────────────
358
+
359
+ _broadcastSystemMessage(type, fromAgent, data) {
360
+ const text = JSON.stringify({ type, data });
361
+ for (const agentName of Object.keys(this._agents)) {
362
+ if (agentName !== fromAgent && agentName !== 'coco') {
363
+ this.sendMessage('coco', agentName, text, { type: 'system_event', event_type: type });
364
+ }
365
+ }
366
+ }
367
+
368
+ _logEvent(category, type, data) {
369
+ const event = {
370
+ id: this._genId(),
371
+ category,
372
+ type,
373
+ data,
374
+ timestamp: new Date().toISOString(),
375
+ };
376
+ const dayFile = path.join(EVENTS_DIR, `${new Date().toISOString().slice(0, 10)}.jsonl`);
377
+ try {
378
+ fs.appendFileSync(dayFile, JSON.stringify(event) + '\n', 'utf-8');
379
+ } catch {}
380
+ this.emit('event', event);
381
+ // Wake event waiters
382
+ this._wakeEventWaiters(event);
383
+ }
384
+
385
+ getEvents(opts = {}) {
386
+ const { since, category, type, limit = 100 } = opts;
387
+
388
+ if (!fs.existsSync(EVENTS_DIR)) return [];
389
+
390
+ // Determine which daily files to read
391
+ const allFiles = fs.readdirSync(EVENTS_DIR).filter(f => f.endsWith('.jsonl')).sort();
392
+ let filesToRead = allFiles;
393
+
394
+ if (since) {
395
+ const sinceDate = new Date(since).toISOString().slice(0, 10);
396
+ filesToRead = allFiles.filter(f => f >= sinceDate);
397
+ } else {
398
+ // Default: only today (backward compatible)
399
+ filesToRead = allFiles.slice(-1);
400
+ }
401
+
402
+ let events = [];
403
+ for (const dayFile of filesToRead) {
404
+ try {
405
+ const lines = fs.readFileSync(path.join(EVENTS_DIR, dayFile), 'utf-8').split('\n').filter(Boolean);
406
+ for (const line of lines) {
407
+ try { events.push(JSON.parse(line)); } catch {}
408
+ }
409
+ } catch {}
410
+ }
411
+
412
+ if (since) {
413
+ const sinceTs = new Date(since).getTime();
414
+ events = events.filter(e => e.timestamp && new Date(e.timestamp).getTime() >= sinceTs);
415
+ }
416
+ if (category) events = events.filter(e => e.category === category);
417
+ if (type) events = events.filter(e => e.type === type);
418
+
419
+ // Sort by timestamp ascending
420
+ events.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
421
+
422
+ return events.slice(-limit);
423
+ }
424
+
425
+ waitForEvents(category, type, timeoutMs = 30000) {
426
+ return new Promise((resolve) => {
427
+ const key = `${category || '*'}:${type || '*'}`;
428
+ const timer = setTimeout(() => {
429
+ this._removeEventWaiter(key);
430
+ resolve({ timeout: true, events: [] });
431
+ }, timeoutMs);
432
+
433
+ if (!this._eventWaiters[key]) this._eventWaiters[key] = [];
434
+ this._eventWaiters[key].push({ resolve, timer, category, type });
435
+ });
436
+ }
437
+
438
+ _wakeEventWaiters(event) {
439
+ const keys = [
440
+ `${event.category}:${event.type}`,
441
+ `${event.category}:*`,
442
+ `*:${event.type}`,
443
+ `*:*`,
444
+ ];
445
+ for (const key of keys) {
446
+ const waiters = this._eventWaiters[key] || [];
447
+ for (const w of waiters) {
448
+ clearTimeout(w.timer);
449
+ w.resolve({ timeout: false, events: [event] });
450
+ }
451
+ if (waiters.length > 0) delete this._eventWaiters[key];
452
+ }
453
+ }
454
+
455
+ _removeEventWaiter(key) {
456
+ delete this._eventWaiters[key];
457
+ }
458
+
459
+ // ── Stale Agent Detection ────────────────────────────
460
+
461
+ _checkStaleAgents() {
462
+ setInterval(() => {
463
+ const now = Date.now();
464
+ for (const [name, agent] of Object.entries(this._agents)) {
465
+ const lastSeen = new Date(agent.last_seen).getTime();
466
+ if (now - lastSeen > OFFLINE_TIMEOUT_MS && agent.status !== 'offline') {
467
+ agent.status = 'offline';
468
+ this._logEvent('system', 'agent_offline', { agent: name });
469
+ this._broadcastSystemMessage('system:agent_left', name, {
470
+ agent: name,
471
+ reason: 'heartbeat timeout',
472
+ });
473
+ }
474
+ }
475
+ this._saveJson(AGENTS_FILE, this._agents);
476
+ }, 60000); // Check every minute
477
+ }
478
+
479
+ _logSystemWelcome(name) {
480
+ this._logEvent('system', 'welcome_sent', { agent: name });
481
+ }
482
+
483
+ // ── Internal ────────────────────────────────────────────
484
+
485
+ _wakeWaiters(agentName, msg) {
486
+ const waiters = this._waiters[agentName] || [];
487
+ delete this._waiters[agentName];
488
+ for (const w of waiters) {
489
+ clearTimeout(w.timer);
490
+ w.resolve({ timeout: false, messages: [msg] });
491
+ }
492
+ }
493
+
494
+ _removeWaiter(agentName) {
495
+ delete this._waiters[agentName];
496
+ }
497
+
498
+ _genId() {
499
+ return `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
500
+ }
501
+
502
+ _loadJson(filePath, defaultVal) {
503
+ try {
504
+ if (fs.existsSync(filePath)) {
505
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
506
+ }
507
+ } catch {}
508
+ return defaultVal;
509
+ }
510
+
511
+ _saveJson(filePath, data) {
512
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
513
+ }
514
+ }
515
+
516
+ module.exports = { AgentBus };
package/lib/daemon.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Daemon Manager — run MCP CoCo as a background process
3
+ * with healthcheck, PID tracking, and auto-recovery for Hermes backend.
4
+ */
5
+ const { spawn } = require('child_process');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+
9
+ const PID_FILE = path.join(__dirname, '..', '.coco-daemon.pid');
10
+ const LOG_FILE = path.join(__dirname, '..', 'coco-daemon.log');
11
+ const HERMES_MCP_PID = path.join(__dirname, '..', '.hermes-mcp.pid');
12
+
13
+ class Daemon {
14
+ /**
15
+ * Start the MCP CoCo daemon in background.
16
+ * Returns immediately; the child process runs independently.
17
+ */
18
+ async start() {
19
+ // Check if already running
20
+ if (fs.existsSync(PID_FILE)) {
21
+ const existingPid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
22
+ try {
23
+ process.kill(existingPid, 0);
24
+ console.error(`MCP CoCo already running (PID: ${existingPid})`);
25
+ process.exit(0);
26
+ return;
27
+ } catch {
28
+ // Stale PID file, remove it
29
+ fs.unlinkSync(PID_FILE);
30
+ }
31
+ }
32
+
33
+ // Fork ourselves as daemon
34
+ const child = spawn(process.execPath, [path.join(__dirname, '..', 'index.js')], {
35
+ detached: true,
36
+ stdio: ['ignore', fs.openSync(LOG_FILE, 'a'), fs.openSync(LOG_FILE, 'a')],
37
+ env: { ...process.env, MCP_COCO_DAEMON: '1' },
38
+ });
39
+
40
+ child.unref();
41
+
42
+ // Write PID
43
+ fs.writeFileSync(PID_FILE, String(child.pid), 'utf-8');
44
+
45
+ console.log(`MCP CoCo daemon started (PID: ${child.pid})`);
46
+ console.log(`Log: ${LOG_FILE}`);
47
+
48
+ // Also start Hermes MCP backend
49
+ await this._startHermesMCP();
50
+
51
+ process.exit(0);
52
+ }
53
+
54
+ /**
55
+ * Start Hermes MCP serve as a subprocess of the daemon.
56
+ */
57
+ async _startHermesMCP() {
58
+ if (fs.existsSync(HERMES_MCP_PID)) {
59
+ try {
60
+ const pid = parseInt(fs.readFileSync(HERMES_MCP_PID, 'utf-8').trim());
61
+ process.kill(pid, 0);
62
+ return; // already running
63
+ } catch {
64
+ fs.unlinkSync(HERMES_MCP_PID);
65
+ }
66
+ }
67
+
68
+ const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });
69
+ const child = spawn('hermes', ['mcp', 'serve', '--accept-hooks'], {
70
+ detached: true,
71
+ stdio: ['ignore', logStream, logStream],
72
+ });
73
+
74
+ child.unref();
75
+ fs.writeFileSync(HERMES_MCP_PID, String(child.pid), 'utf-8');
76
+ }
77
+
78
+ /**
79
+ * Quick health check — used by --health flag and mcporter probe.
80
+ */
81
+ async healthCheck() {
82
+ // Check if daemon PID is alive
83
+ if (!fs.existsSync(PID_FILE)) {
84
+ return false;
85
+ }
86
+ try {
87
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
88
+ process.kill(pid, 0);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ }
95
+
96
+ module.exports = { Daemon };