neohive 6.1.0 → 6.1.2

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/cli.js CHANGED
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const { execSync } = require('child_process');
7
7
  const { upsertNeohiveMcpInToml } = require('./lib/codex-neohive-toml');
8
+ const pkg = require('./package.json');
8
9
 
9
10
  // ─────────────────────────────────────────────────────────────────────────────
10
11
  // CLI_CONFIG — centralized constants for the Neohive CLI
@@ -21,7 +22,7 @@ const command = process.argv[2];
21
22
 
22
23
  function printUsage() {
23
24
  console.log(`
24
- Neohive v6.1.0
25
+ Neohive v${pkg.version}
25
26
  The MCP collaboration layer for AI CLI tools.
26
27
 
27
28
  Usage:
@@ -717,6 +718,22 @@ function reset() {
717
718
  console.log(' [warn] Could not archive: ' + e.message + ' — proceeding with reset anyway.');
718
719
  }
719
720
 
721
+ // Kill any running MCP server processes before wiping data.
722
+ // Otherwise orphaned heartbeat intervals keep writing into the fresh directory.
723
+ try {
724
+ const agentsFile = path.join(targetDir, 'agents.json');
725
+ if (fs.existsSync(agentsFile)) {
726
+ const agents = JSON.parse(fs.readFileSync(agentsFile, 'utf8'));
727
+ let killed = 0;
728
+ for (const [name, info] of Object.entries(agents)) {
729
+ if (info.pid) {
730
+ try { process.kill(info.pid, 'SIGTERM'); killed++; } catch {}
731
+ }
732
+ }
733
+ if (killed > 0) console.log(' [ok] Terminated ' + killed + ' running agent process(es)');
734
+ }
735
+ } catch {}
736
+
720
737
  fs.rmSync(targetDir, { recursive: true, force: true });
721
738
  fs.mkdirSync(targetDir, { recursive: true });
722
739
  console.log(' Cleared all data from ' + targetDir);
package/dashboard.js CHANGED
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const { spawn } = require('child_process');
7
7
  const { upsertNeohiveMcpInToml } = require('./lib/codex-neohive-toml');
8
+ const pkg = require(path.join(__dirname, 'package.json'));
8
9
  const { readIdeActivity, applyIdeActivityHint } = require('./lib/ide-activity');
9
10
  const _audit = require('./lib/audit');
10
11
 
@@ -312,22 +313,23 @@ function readJson(file) {
312
313
  }
313
314
 
314
315
  function isPidAlive(pid, lastActivity) {
315
- const STALE_THRESHOLD = 30000; // 30s — 3x heartbeat interval, catches dead agents faster
316
+ const STALE_THRESHOLD = 30000; // 30s — 3x heartbeat interval
317
+ const PID_TRUST_WINDOW = 60000; // 60s — beyond this, PID check is unreliable (OS reuses PIDs)
316
318
 
317
- // PRIORITY 1: Trust heartbeat freshness over PID status
318
- // Heartbeats are written by the actual running process — if fresh, agent is alive
319
- // regardless of whether process.kill can see the PID
320
319
  if (lastActivity) {
321
320
  const stale = Date.now() - new Date(lastActivity).getTime();
322
321
  if (stale < STALE_THRESHOLD) return true;
322
+ // A real neohive agent writes heartbeat every 10s. If 60s have passed
323
+ // without one, the PID belongs to a different process (OS recycled it).
324
+ if (stale > PID_TRUST_WINDOW) return false;
323
325
  }
324
326
 
325
- // PRIORITY 2: If heartbeat is stale, check PID as fallback
327
+ // Heartbeat is stale but within the trust window — verify PID as fallback
326
328
  try {
327
329
  process.kill(pid, 0);
328
- return true; // PID exists — alive even with stale heartbeat
330
+ return true;
329
331
  } catch {
330
- return false; // PID dead AND heartbeat stale — truly dead
332
+ return false;
331
333
  }
332
334
  }
333
335
 
@@ -3958,12 +3960,27 @@ function startFileWatcher() {
3958
3960
  sseNotifyAll(changeType);
3959
3961
  }, 2000);
3960
3962
  });
3961
- fsWatcher.on('error', () => {}); // ignore watch errors
3963
+ fsWatcher.on('error', () => {
3964
+ setTimeout(startFileWatcher, 1000);
3965
+ });
3962
3966
  } catch {}
3963
3967
  }
3964
3968
 
3965
3969
  startFileWatcher();
3966
3970
 
3971
+ // macOS fs.watch() silently stops emitting events when the watched directory is
3972
+ // deleted and recreated (e.g. reset --force). The watcher object stays non-null
3973
+ // but is dead. Force-restart it every 30s to guarantee the dashboard stays live.
3974
+ let _lastWatcherRestart = Date.now();
3975
+ setInterval(() => {
3976
+ const dataDir = resolveDataDir();
3977
+ if (!fs.existsSync(dataDir)) return;
3978
+ if (!fsWatcher || Date.now() - _lastWatcherRestart > 30000) {
3979
+ startFileWatcher();
3980
+ _lastWatcherRestart = Date.now();
3981
+ }
3982
+ }, 5000).unref();
3983
+
3967
3984
  server.on('error', (err) => {
3968
3985
  if (err.code === 'EADDRINUSE') {
3969
3986
  console.error(`\n Error: Port ${PORT} is already in use.`);
@@ -3979,7 +3996,7 @@ server.listen(PORT, LAN_MODE ? '0.0.0.0' : '127.0.0.1', () => {
3979
3996
  const dataDir = resolveDataDir();
3980
3997
  const lanIP = getLanIP();
3981
3998
  console.log('');
3982
- console.log(' Neohive Dashboard v6.0.0');
3999
+ console.log(` Neohive Dashboard v${pkg.version}`);
3983
4000
  console.log(' ============================================');
3984
4001
  console.log(' Dashboard: http://localhost:' + PORT);
3985
4002
  if (LAN_MODE && lanIP) {
package/lib/agents.js CHANGED
@@ -12,6 +12,7 @@ const _pidAliveCache = {};
12
12
  let _isAutonomousMode = () => false;
13
13
  function setAutonomousModeCheck(fn) { _isAutonomousMode = fn; }
14
14
 
15
+ const PID_TRUST_WINDOW_MS = 60000;
15
16
  function isPidAlive(pid, lastActivity) {
16
17
  const cacheKey = `${pid}_${lastActivity}`;
17
18
  const cached = _pidAliveCache[cacheKey];
@@ -22,9 +23,14 @@ function isPidAlive(pid, lastActivity) {
22
23
 
23
24
  if (lastActivity) {
24
25
  const stale = Date.now() - new Date(lastActivity).getTime();
25
- if (stale < STALE_THRESHOLD) alive = true;
26
- }
27
- if (!alive) {
26
+ if (stale < STALE_THRESHOLD) {
27
+ alive = true;
28
+ } else if (stale > PID_TRUST_WINDOW_MS) {
29
+ alive = false;
30
+ } else {
31
+ try { process.kill(pid, 0); alive = true; } catch { alive = false; }
32
+ }
33
+ } else {
28
34
  try { process.kill(pid, 0); alive = true; } catch { alive = false; }
29
35
  }
30
36
  _pidAliveCache[cacheKey] = { alive, ts: Date.now() };
package/logo.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neohive",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
4
4
  "description": "The MCP collaboration layer for AI CLI tools. Turn Claude Code, Gemini CLI, and Codex CLI into a team.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -6,6 +6,7 @@ const {
6
6
  } = require('@modelcontextprotocol/sdk/types.js');
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const pkg = require('./package.json');
9
10
 
10
11
  // --- Modular infrastructure (lib/) ---
11
12
  // These modules are the canonical implementations. The inline code below
@@ -453,40 +454,33 @@ function getAcks() {
453
454
  }
454
455
  }
455
456
 
456
- // Cache for isPidAlive results — avoids redundant process.kill calls at 100-agent scale
457
457
  const _pidAliveCache = {};
458
+ const PID_TRUST_WINDOW_MS = 60000; // Beyond 60s without heartbeat, PID is unreliable (OS reuses PIDs)
458
459
  function isPidAlive(pid, lastActivity) {
459
- // Cache with 5s TTL — PID status doesn't change faster than heartbeats
460
460
  const cacheKey = `${pid}_${lastActivity}`;
461
461
  const cached = _pidAliveCache[cacheKey];
462
462
  if (cached && Date.now() - cached.ts < SERVER_CONFIG.AGENT_CACHE_TTL_MS) return cached.alive;
463
463
 
464
- // 30s stale threshold — 3x the 10s heartbeat interval, catches dead agents faster
465
464
  const STALE_THRESHOLD = SERVER_CONFIG.AGENT_STALE_THRESHOLD_MS;
466
465
  let alive = false;
467
466
 
468
- // PRIORITY 1: Trust heartbeat freshness over PID status
469
- // Heartbeat files are written by the actual running process — if fresh, agent is alive
470
- // regardless of whether process.kill can see the PID (cross-process PID visibility issues)
471
467
  if (lastActivity) {
472
468
  const stale = Date.now() - new Date(lastActivity).getTime();
473
469
  if (stale < STALE_THRESHOLD) {
474
470
  alive = true;
475
- }
476
- }
477
-
478
- // PRIORITY 2: If heartbeat is stale, verify PID is actually dead
479
- if (!alive) {
480
- try {
481
- process.kill(pid, 0);
482
- alive = true; // PID exists — agent is alive even with stale heartbeat
483
- } catch {
484
- // PID dead AND heartbeat stale — agent is truly dead
471
+ } else if (stale > PID_TRUST_WINDOW_MS) {
472
+ // A real neohive agent writes heartbeat every 10s. If 60s have passed
473
+ // without one, the PID belongs to a different process (OS recycled it).
485
474
  alive = false;
475
+ } else {
476
+ // Within trust window — verify PID as fallback
477
+ try { process.kill(pid, 0); alive = true; } catch { alive = false; }
486
478
  }
479
+ } else {
480
+ try { process.kill(pid, 0); alive = true; } catch { alive = false; }
487
481
  }
482
+
488
483
  _pidAliveCache[cacheKey] = { alive, ts: Date.now() };
489
- // Evict old entries (keep cache small)
490
484
  const keys = Object.keys(_pidAliveCache);
491
485
  if (keys.length > 200) {
492
486
  const cutoff = Date.now() - SERVER_CONFIG.POLL_INTERVAL_MS * 5;
@@ -7242,7 +7236,7 @@ const messaging = require('./tools/messaging')(_messagingCtx);
7242
7236
  // --- MCP Server setup ---
7243
7237
 
7244
7238
  const server = new Server(
7245
- { name: 'neohive', version: '6.1.0' },
7239
+ { name: 'neohive', version: pkg.version },
7246
7240
  { capabilities: { tools: { listChanged: true } } }
7247
7241
  );
7248
7242