neohive 6.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.
package/cli.js ADDED
@@ -0,0 +1,931 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ const command = process.argv[2];
9
+
10
+ function printUsage() {
11
+ console.log(`
12
+ Neohive — Neohive v5.3.0
13
+ MCP message broker for inter-agent communication
14
+ Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
+
16
+ Usage:
17
+ npx neohive init Auto-detect CLI and configure MCP
18
+ npx neohive init --claude Configure for Claude Code
19
+ npx neohive init --gemini Configure for Gemini CLI
20
+ npx neohive init --codex Configure for Codex CLI
21
+ npx neohive init --all Configure for all supported CLIs
22
+ npx neohive init --ollama Setup Ollama agent bridge (local LLM)
23
+ npx neohive init --template T Initialize with a team template (pair, team, review, debate, ollama)
24
+ npx neohive templates List available agent templates
25
+ npx neohive dashboard Launch the web dashboard (http://localhost:3000)
26
+ npx neohive dashboard --lan Launch dashboard accessible on LAN (phone/tablet)
27
+ npx neohive reset Clear all conversation data
28
+ npx neohive msg <agent> <text> Send a message to an agent
29
+ npx neohive status Show active agents and message count
30
+ npx neohive uninstall Remove neohive from all CLI configs
31
+ npx neohive help Show this help message
32
+
33
+ v5.0 — True Autonomy Engine (61 tools):
34
+ New tools: get_work, verify_and_advance, start_plan, retry_with_improvement
35
+ Proactive work loop: get_work → do work → verify_and_advance → get_work
36
+ Parallel workflow steps with dependency graphs (depends_on)
37
+ Auto-retry with skill accumulation (3 attempts then team escalation)
38
+ Watchdog engine: idle nudge, stuck detection, auto-reassign
39
+ 100ms handoff cooldowns in autonomous mode
40
+ Plan dashboard: live progress, pause/stop/skip/reassign controls
41
+ `);
42
+ }
43
+
44
+ // Detect which CLIs are installed
45
+ function detectCLIs() {
46
+ const detected = [];
47
+ const home = os.homedir();
48
+
49
+ // Claude Code: ~/.claude/ directory exists
50
+ if (fs.existsSync(path.join(home, '.claude'))) {
51
+ detected.push('claude');
52
+ }
53
+
54
+ // Gemini CLI: ~/.gemini/ or GEMINI_API_KEY set
55
+ if (fs.existsSync(path.join(home, '.gemini')) || process.env.GEMINI_API_KEY) {
56
+ detected.push('gemini');
57
+ }
58
+
59
+ // Codex CLI: ~/.codex/ directory exists
60
+ if (fs.existsSync(path.join(home, '.codex'))) {
61
+ detected.push('codex');
62
+ }
63
+
64
+ return detected;
65
+ }
66
+
67
+ // Detect Ollama installation
68
+ function detectOllama() {
69
+ try {
70
+ const version = execSync('ollama --version', { encoding: 'utf8', timeout: 5000 }).trim();
71
+ return { installed: true, version };
72
+ } catch {
73
+ return { installed: false };
74
+ }
75
+ }
76
+
77
+ // The data directory where all agents read/write — must be the same for server + dashboard
78
+ function dataDir(cwd) {
79
+ return path.join(cwd, '.neohive');
80
+ }
81
+
82
+ // Configure for Claude Code (.mcp.json in project root)
83
+ function setupClaude(serverPath, cwd) {
84
+ const mcpConfigPath = path.join(cwd, '.mcp.json');
85
+ let mcpConfig = { mcpServers: {} };
86
+ if (fs.existsSync(mcpConfigPath)) {
87
+ try {
88
+ mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
89
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
90
+ } catch {
91
+ // Backup corrupted file before overwriting
92
+ const backup = mcpConfigPath + '.backup';
93
+ fs.copyFileSync(mcpConfigPath, backup);
94
+ console.log(' [warn] Existing .mcp.json was invalid — backed up to .mcp.json.backup');
95
+ }
96
+ }
97
+
98
+ mcpConfig.mcpServers['neohive'] = {
99
+ command: 'node',
100
+ args: [serverPath],
101
+ timeout: 300,
102
+ };
103
+
104
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
105
+ console.log(' [ok] Claude Code: .mcp.json updated');
106
+ }
107
+
108
+ // Configure for Gemini CLI (.gemini/settings.json or GEMINI.md with MCP config)
109
+ function setupGemini(serverPath, cwd) {
110
+ // Gemini CLI uses .gemini/settings.json for MCP configuration
111
+ const geminiDir = path.join(cwd, '.gemini');
112
+ const settingsPath = path.join(geminiDir, 'settings.json');
113
+
114
+ if (!fs.existsSync(geminiDir)) {
115
+ fs.mkdirSync(geminiDir, { recursive: true });
116
+ }
117
+
118
+ let settings = { mcpServers: {} };
119
+ if (fs.existsSync(settingsPath)) {
120
+ try {
121
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
122
+ if (!settings.mcpServers) settings.mcpServers = {};
123
+ } catch {
124
+ const backup = settingsPath + '.backup';
125
+ fs.copyFileSync(settingsPath, backup);
126
+ console.log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
127
+ }
128
+ }
129
+
130
+ settings.mcpServers['neohive'] = {
131
+ command: 'node',
132
+ args: [serverPath],
133
+ timeout: 300,
134
+ trust: true,
135
+ };
136
+
137
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
138
+ console.log(' [ok] Gemini CLI: .gemini/settings.json updated');
139
+ }
140
+
141
+ // Configure for Codex CLI (uses .codex/config.toml)
142
+ function setupCodex(serverPath, cwd) {
143
+ const codexDir = path.join(cwd, '.codex');
144
+ const configPath = path.join(codexDir, 'config.toml');
145
+
146
+ if (!fs.existsSync(codexDir)) {
147
+ fs.mkdirSync(codexDir, { recursive: true });
148
+ }
149
+
150
+ // Read existing config or start fresh
151
+ let config = '';
152
+ if (fs.existsSync(configPath)) {
153
+ config = fs.readFileSync(configPath, 'utf8');
154
+ }
155
+
156
+ // Backup existing config before modifying
157
+ if (fs.existsSync(configPath)) {
158
+ fs.copyFileSync(configPath, configPath + '.backup');
159
+ }
160
+
161
+ // Only add if not already present
162
+ if (!config.includes('[mcp_servers.neohive]')) {
163
+ const tomlBlock = `
164
+ [mcp_servers.neohive]
165
+ command = "node"
166
+ args = [${JSON.stringify(serverPath)}]
167
+ timeout = 300
168
+ `;
169
+ config += tomlBlock;
170
+ fs.writeFileSync(configPath, config);
171
+ }
172
+
173
+ console.log(' [ok] Codex CLI: .codex/config.toml updated');
174
+ }
175
+
176
+ // Setup Ollama agent bridge script
177
+ function setupOllama(serverPath, cwd) {
178
+ const dir = dataDir(cwd);
179
+ const scriptPath = path.join(cwd, '.neohive', 'ollama-agent.js');
180
+
181
+ if (!fs.existsSync(path.join(cwd, '.neohive'))) {
182
+ fs.mkdirSync(path.join(cwd, '.neohive'), { recursive: true });
183
+ }
184
+
185
+ const script = `#!/usr/bin/env node
186
+ // ollama-agent.js - bridges Ollama to Neohive
187
+ // Usage: node .neohive/ollama-agent.js [agent-name] [model]
188
+ const fs = require('fs'), path = require('path'), http = require('http');
189
+ const DATA_DIR = path.join(__dirname);
190
+ const name = process.argv[2] || 'Ollama';
191
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(name)) throw new Error('Invalid agent name');
192
+ const model = process.argv[3] || 'llama3';
193
+ const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
194
+
195
+ function readJson(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
196
+ function readJsonl(f) { if (!fs.existsSync(f)) return []; return fs.readFileSync(f, 'utf8').split(/\\r?\\n/).filter(l => l.trim()).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
197
+
198
+ // Register agent
199
+ function register() {
200
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
201
+ const agents = readJson(agentsFile);
202
+ agents[name] = { pid: process.pid, timestamp: new Date().toISOString(), last_activity: new Date().toISOString(), provider: 'Ollama (' + model + ')' };
203
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
204
+ console.log('[' + name + '] Registered (PID ' + process.pid + ', model: ' + model + ')');
205
+ }
206
+
207
+ // Update heartbeat
208
+ function heartbeat() {
209
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
210
+ const agents = readJson(agentsFile);
211
+ if (agents[name]) {
212
+ agents[name].last_activity = new Date().toISOString();
213
+ agents[name].pid = process.pid;
214
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
215
+ }
216
+ }
217
+
218
+ // Call Ollama API
219
+ function callOllama(prompt) {
220
+ return new Promise(function(resolve, reject) {
221
+ const url = new URL(OLLAMA_URL + '/api/chat');
222
+ const body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], stream: false });
223
+ const req = http.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, function(res) {
224
+ let data = '';
225
+ res.on('data', function(c) { data += c; });
226
+ res.on('end', function() {
227
+ try { const j = JSON.parse(data); resolve(j.message ? j.message.content : data); }
228
+ catch { resolve(data); }
229
+ });
230
+ });
231
+ req.on('error', reject);
232
+ req.write(body);
233
+ req.end();
234
+ });
235
+ }
236
+
237
+ // Send a message
238
+ function sendMessage(to, content) {
239
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
240
+ const msg = { id: msgId, from: name, to: to, content: content, timestamp: new Date().toISOString() };
241
+ fs.appendFileSync(path.join(DATA_DIR, 'messages.jsonl'), JSON.stringify(msg) + '\\n');
242
+ fs.appendFileSync(path.join(DATA_DIR, 'history.jsonl'), JSON.stringify(msg) + '\\n');
243
+ console.log('[' + name + '] -> ' + to + ': ' + content.substring(0, 80) + (content.length > 80 ? '...' : ''));
244
+ }
245
+
246
+ // Listen for messages
247
+ let lastOffset = 0;
248
+ function checkMessages() {
249
+ const consumedFile = path.join(DATA_DIR, 'consumed-' + name + '.json');
250
+ const consumed = readJson(consumedFile);
251
+ lastOffset = consumed.offset || 0;
252
+
253
+ const messages = readJsonl(path.join(DATA_DIR, 'messages.jsonl'));
254
+ const newMsgs = messages.slice(lastOffset).filter(function(m) {
255
+ return m.to === name || (m.to === 'all' && m.from !== name);
256
+ });
257
+
258
+ if (newMsgs.length > 0) {
259
+ consumed.offset = messages.length;
260
+ fs.writeFileSync(consumedFile, JSON.stringify(consumed));
261
+ }
262
+
263
+ return newMsgs;
264
+ }
265
+
266
+ async function processMessages() {
267
+ const msgs = checkMessages();
268
+ for (const m of msgs) {
269
+ console.log('[' + name + '] <- ' + m.from + ': ' + m.content.substring(0, 80));
270
+ try {
271
+ const response = await callOllama(m.content);
272
+ sendMessage(m.from, response);
273
+ } catch (e) {
274
+ sendMessage(m.from, 'Error calling Ollama: ' + e.message);
275
+ }
276
+ }
277
+ }
278
+
279
+ // Main loop
280
+ register();
281
+ const hb = setInterval(heartbeat, 10000);
282
+ hb.unref();
283
+ console.log('[' + name + '] Listening for messages... (Ctrl+C to stop)');
284
+ setInterval(processMessages, 2000);
285
+
286
+ // Cleanup on exit
287
+ process.on('SIGINT', function() { console.log('\\n[' + name + '] Shutting down.'); process.exit(0); });
288
+ `;
289
+
290
+ const tmpPath = scriptPath + '.tmp.' + process.pid;
291
+ fs.writeFileSync(tmpPath, script);
292
+ fs.renameSync(tmpPath, scriptPath);
293
+ console.log(' [ok] Ollama agent script created: .neohive/ollama-agent.js');
294
+ console.log('');
295
+ console.log(' Launch an Ollama agent with:');
296
+ console.log(' node .neohive/ollama-agent.js <name> <model>');
297
+ console.log('');
298
+ console.log(' Examples:');
299
+ console.log(' node .neohive/ollama-agent.js Ollama llama3');
300
+ console.log(' node .neohive/ollama-agent.js Coder codellama');
301
+ console.log(' node .neohive/ollama-agent.js Writer mistral');
302
+ }
303
+
304
+ function init() {
305
+ const cwd = process.cwd();
306
+ const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
307
+ const gitignorePath = path.join(cwd, '.gitignore');
308
+ const flag = process.argv[3];
309
+
310
+ console.log('');
311
+ console.log(' Neohive — Initializing Neohive');
312
+ console.log(' ==========================================');
313
+ console.log('');
314
+
315
+ let targets = [];
316
+
317
+ if (flag === '--claude') {
318
+ targets = ['claude'];
319
+ } else if (flag === '--gemini') {
320
+ targets = ['gemini'];
321
+ } else if (flag === '--codex') {
322
+ targets = ['codex'];
323
+ } else if (flag === '--all') {
324
+ targets = ['claude', 'gemini', 'codex'];
325
+ } else if (flag === '--ollama') {
326
+ const ollama = detectOllama();
327
+ if (!ollama.installed) {
328
+ console.log(' Ollama not found. Install it from: https://ollama.com/download');
329
+ console.log(' After installing, run: ollama pull llama3');
330
+ console.log('');
331
+ } else {
332
+ console.log(' Ollama detected: ' + ollama.version);
333
+ setupOllama(serverPath, cwd);
334
+ }
335
+ targets = detectCLIs();
336
+ if (targets.length === 0) targets = ['claude'];
337
+ } else {
338
+ // Auto-detect
339
+ targets = detectCLIs();
340
+ if (targets.length === 0) {
341
+ // Default to claude if nothing detected
342
+ targets = ['claude'];
343
+ console.log(' No CLI detected, defaulting to Claude Code config.');
344
+ } else {
345
+ console.log(` Detected CLI(s): ${targets.join(', ')}`);
346
+ }
347
+ }
348
+
349
+ console.log('');
350
+
351
+ for (const target of targets) {
352
+ switch (target) {
353
+ case 'claude': setupClaude(serverPath, cwd); break;
354
+ case 'gemini': setupGemini(serverPath, cwd); break;
355
+ case 'codex': setupCodex(serverPath, cwd); break;
356
+ }
357
+ }
358
+
359
+ // Add .neohive/ and MCP config files to .gitignore
360
+ const gitignoreEntries = ['.neohive/', '.mcp.json', '.codex/', '.gemini/'];
361
+ if (fs.existsSync(gitignorePath)) {
362
+ let content = fs.readFileSync(gitignorePath, 'utf8');
363
+ const missing = gitignoreEntries.filter(e => !content.includes(e));
364
+ if (missing.length) {
365
+ content += '\n# Neohive (auto-added by neohive init)\n' + missing.join('\n') + '\n';
366
+ fs.writeFileSync(gitignorePath, content);
367
+ console.log(' [ok] Added to .gitignore: ' + missing.join(', '));
368
+ } else {
369
+ console.log(' [ok] .gitignore already configured');
370
+ }
371
+ } else {
372
+ fs.writeFileSync(gitignorePath, '# Neohive (auto-added by neohive init)\n' + gitignoreEntries.join('\n') + '\n');
373
+ console.log(' [ok] .gitignore created');
374
+ }
375
+
376
+ console.log('');
377
+ console.log(' Neohive is ready! Restart your CLI to pick up the MCP tools.');
378
+ console.log('');
379
+
380
+ // Show template if --template was provided
381
+ var templateFlag = null;
382
+ for (var i = 3; i < process.argv.length; i++) {
383
+ if (process.argv[i] === '--template' && process.argv[i + 1]) {
384
+ templateFlag = process.argv[i + 1];
385
+ break;
386
+ }
387
+ }
388
+
389
+ if (templateFlag) {
390
+ showTemplate(templateFlag);
391
+ } else {
392
+ console.log(' Open two terminals and start a conversation between agents.');
393
+ console.log(' Tip: Use "npx neohive init --template pair" for ready-made prompts.');
394
+ console.log('');
395
+ console.log(' \x1b[1m Monitor:\x1b[0m');
396
+ console.log(' npx neohive dashboard');
397
+ console.log(' npx neohive status');
398
+ console.log(' npx neohive doctor');
399
+ console.log('');
400
+ }
401
+ }
402
+
403
+ function reset() {
404
+ const targetDir = process.env.NEOHIVE_DATA_DIR || path.join(process.cwd(), '.neohive');
405
+
406
+ if (!fs.existsSync(targetDir)) {
407
+ console.log(' No .neohive/ directory found. Nothing to reset.');
408
+ return;
409
+ }
410
+
411
+ // Safety: count messages to show user what they're about to delete
412
+ const historyFile = path.join(targetDir, 'history.jsonl');
413
+ let msgCount = 0;
414
+ if (fs.existsSync(historyFile)) {
415
+ msgCount = fs.readFileSync(historyFile, 'utf8').split(/\r?\n/).filter(l => l.trim()).length;
416
+ }
417
+
418
+ // Require --force flag, otherwise warn and exit
419
+ if (!process.argv.includes('--force')) {
420
+ console.log('');
421
+ console.log(' ⚠ This will permanently delete all conversation data in:');
422
+ console.log(' ' + targetDir);
423
+ if (msgCount > 0) console.log(' (' + msgCount + ' messages in history)');
424
+ console.log('');
425
+ console.log(' To confirm, run: npx neohive reset --force');
426
+ console.log('');
427
+ return;
428
+ }
429
+
430
+ // Auto-archive before deleting
431
+ const archiveDir = path.join(targetDir, '..', '.neohive-archive');
432
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
433
+ const archivePath = path.join(archiveDir, timestamp);
434
+ try {
435
+ fs.mkdirSync(archivePath, { recursive: true });
436
+ const filesToArchive = ['history.jsonl', 'messages.jsonl', 'agents.json', 'decisions.json', 'tasks.json'];
437
+ let archived = 0;
438
+ for (const f of filesToArchive) {
439
+ const src = path.join(targetDir, f);
440
+ if (fs.existsSync(src)) {
441
+ fs.copyFileSync(src, path.join(archivePath, f));
442
+ archived++;
443
+ }
444
+ }
445
+ if (archived > 0) {
446
+ console.log(' [ok] Archived ' + archived + ' files to .neohive-archive/' + timestamp + '/');
447
+ }
448
+ } catch (e) {
449
+ console.log(' [warn] Could not archive: ' + e.message + ' — proceeding with reset anyway.');
450
+ }
451
+
452
+ fs.rmSync(targetDir, { recursive: true, force: true });
453
+ fs.mkdirSync(targetDir, { recursive: true });
454
+ console.log(' Cleared all data from ' + targetDir);
455
+ }
456
+
457
+ function getTemplates() {
458
+ const templatesDir = path.join(__dirname, 'templates');
459
+ if (!fs.existsSync(templatesDir)) return [];
460
+ return fs.readdirSync(templatesDir)
461
+ .filter(f => f.endsWith('.json'))
462
+ .map(f => {
463
+ try { return JSON.parse(fs.readFileSync(path.join(templatesDir, f), 'utf8')); }
464
+ catch { return null; }
465
+ })
466
+ .filter(Boolean);
467
+ }
468
+
469
+ function listTemplates() {
470
+ const templates = getTemplates();
471
+ console.log('');
472
+ console.log(' Available Agent Templates');
473
+ console.log(' ========================');
474
+ console.log('');
475
+ for (const t of templates) {
476
+ const agentNames = t.agents.map(a => a.name).join(', ');
477
+ console.log(' ' + t.name.padEnd(12) + ' ' + t.description);
478
+ console.log(' ' + ''.padEnd(12) + ' Agents: ' + agentNames);
479
+ console.log('');
480
+ }
481
+ console.log(' Usage: npx neohive init --template <name>');
482
+ console.log('');
483
+ }
484
+
485
+ function showTemplate(templateName) {
486
+ const templates = getTemplates();
487
+ const template = templates.find(t => t.name === templateName);
488
+ if (!template) {
489
+ console.error(' Unknown template: ' + templateName);
490
+ console.error(' Available: ' + templates.map(t => t.name).join(', '));
491
+ process.exit(1);
492
+ }
493
+
494
+ console.log('');
495
+ console.log(' Template: ' + template.name);
496
+ console.log(' ' + template.description);
497
+ console.log('');
498
+ console.log(' Copy these prompts into each terminal:');
499
+ console.log(' ======================================');
500
+
501
+ for (var i = 0; i < template.agents.length; i++) {
502
+ var a = template.agents[i];
503
+ console.log('');
504
+ console.log(' --- Terminal ' + (i + 1) + ': ' + a.name + ' (' + a.role + ') ---');
505
+ console.log('');
506
+ console.log(' ' + a.prompt.replace(/\n/g, '\n '));
507
+ console.log('');
508
+ }
509
+ }
510
+
511
+ function dashboard() {
512
+ if (process.argv.includes('--lan')) {
513
+ process.env.NEOHIVE_LAN = 'true';
514
+ }
515
+ require('./dashboard.js');
516
+ }
517
+
518
+ function resolveDataDirCli() {
519
+ return process.env.NEOHIVE_DATA_DIR || path.join(process.cwd(), '.neohive');
520
+ }
521
+
522
+ function readJsonl(filePath) {
523
+ if (!fs.existsSync(filePath)) return [];
524
+ return fs.readFileSync(filePath, 'utf8')
525
+ .split(/\r?\n/)
526
+ .filter(l => l.trim())
527
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
528
+ .filter(Boolean);
529
+ }
530
+
531
+ function readJson(filePath) {
532
+ if (!fs.existsSync(filePath)) return {};
533
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
534
+ }
535
+
536
+ function isPidAlive(pid) {
537
+ if (!pid) return false;
538
+ try { process.kill(pid, 0); return true; } catch { return false; }
539
+ }
540
+
541
+ function cliMsg() {
542
+ const recipient = process.argv[3];
543
+ const textParts = process.argv.slice(4);
544
+ if (!recipient || !textParts.length) {
545
+ console.error(' Usage: npx neohive msg <agent> <text>');
546
+ process.exit(1);
547
+ }
548
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(recipient)) {
549
+ console.error(' Agent name must be 1-20 alphanumeric characters (with _ or -).');
550
+ process.exit(1);
551
+ }
552
+ const text = textParts.join(' ');
553
+ const dir = resolveDataDirCli();
554
+ if (!fs.existsSync(dir)) {
555
+ console.error(' No .neohive/ directory found. Run "npx neohive init" first.');
556
+ process.exit(1);
557
+ }
558
+
559
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
560
+ const msg = {
561
+ id: msgId,
562
+ from: 'CLI',
563
+ to: recipient,
564
+ content: text,
565
+ timestamp: new Date().toISOString(),
566
+ };
567
+
568
+ const messagesFile = path.join(dir, 'messages.jsonl');
569
+ const historyFile = path.join(dir, 'history.jsonl');
570
+ fs.appendFileSync(messagesFile, JSON.stringify(msg) + '\n');
571
+ fs.appendFileSync(historyFile, JSON.stringify(msg) + '\n');
572
+
573
+ console.log(' Message sent to ' + recipient + ': ' + text);
574
+ }
575
+
576
+ function cliStatus() {
577
+ const dir = resolveDataDirCli();
578
+ if (!fs.existsSync(dir)) {
579
+ console.error(' No .neohive/ directory found. Run "npx neohive init" first.');
580
+ process.exit(1);
581
+ }
582
+
583
+ const agents = readJson(path.join(dir, 'agents.json'));
584
+ const history = readJsonl(path.join(dir, 'history.jsonl'));
585
+ const profiles = readJson(path.join(dir, 'profiles.json'));
586
+ const workflows = readJson(path.join(dir, 'workflows.json'));
587
+ const tasks = readJson(path.join(dir, 'tasks.json'));
588
+
589
+ // Merge heartbeat files for live activity data
590
+ try {
591
+ const files = fs.readdirSync(dir).filter(f => f.startsWith('heartbeat-') && f.endsWith('.json'));
592
+ for (const f of files) {
593
+ const name = f.slice(10, -5);
594
+ if (agents[name]) {
595
+ try {
596
+ const hb = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
597
+ if (hb.last_activity) agents[name].last_activity = hb.last_activity;
598
+ if (hb.pid) agents[name].pid = hb.pid;
599
+ } catch {}
600
+ }
601
+ }
602
+ } catch {}
603
+
604
+ const onlineCount = Object.values(agents).filter(a => isPidAlive(a.pid)).length;
605
+
606
+ console.log('');
607
+ console.log(' Neohive — Status');
608
+ console.log(' =======================');
609
+ console.log(' Messages: ' + history.length + ' | Agents: ' + onlineCount + '/' + Object.keys(agents).length + ' online');
610
+ console.log('');
611
+
612
+ // Agents with roles
613
+ const names = Object.keys(agents);
614
+ if (!names.length) {
615
+ console.log(' No agents registered.');
616
+ } else {
617
+ console.log(' Agents:');
618
+ for (const name of names) {
619
+ const info = agents[name];
620
+ const alive = isPidAlive(info.pid);
621
+ const status = alive ? '\x1b[32monline\x1b[0m' : '\x1b[31moffline\x1b[0m';
622
+ const lastActivity = info.last_activity || info.timestamp || '';
623
+ const role = (profiles && profiles[name] && profiles[name].role) ? ' [' + profiles[name].role + ']' : '';
624
+ const msgCount = history.filter(m => m.from === name).length;
625
+ console.log(' ' + name.padEnd(16) + ' ' + status + role.padEnd(16) + ' msgs: ' + msgCount + ' last: ' + (lastActivity ? new Date(lastActivity).toLocaleTimeString() : '-'));
626
+ }
627
+ }
628
+
629
+ // Active workflows
630
+ const activeWfs = Array.isArray(workflows) ? workflows.filter(w => w.status === 'active') : [];
631
+ if (activeWfs.length > 0) {
632
+ console.log('');
633
+ console.log(' Workflows:');
634
+ for (const wf of activeWfs) {
635
+ const done = wf.steps.filter(s => s.status === 'done').length;
636
+ const total = wf.steps.length;
637
+ const pct = Math.round((done / total) * 100);
638
+ const mode = wf.autonomous ? ' (autonomous)' : '';
639
+ console.log(' ' + wf.name.padEnd(24) + ' ' + done + '/' + total + ' (' + pct + '%)' + mode);
640
+ }
641
+ }
642
+
643
+ // Active tasks
644
+ const activeTasks = Array.isArray(tasks) ? tasks.filter(t => t.status === 'in_progress') : [];
645
+ if (activeTasks.length > 0) {
646
+ console.log('');
647
+ console.log(' Tasks in progress:');
648
+ for (const t of activeTasks.slice(0, 5)) {
649
+ console.log(' ' + (t.title || 'Untitled').padEnd(30) + ' -> ' + (t.assignee || 'unassigned'));
650
+ }
651
+ if (activeTasks.length > 5) console.log(' ... and ' + (activeTasks.length - 5) + ' more');
652
+ }
653
+
654
+ console.log('');
655
+ }
656
+
657
+ // v5.0: Diagnostic health check
658
+ function cliDoctor() {
659
+ console.log('');
660
+ console.log(' \x1b[1mNeohive — Doctor\x1b[0m');
661
+ console.log(' ======================');
662
+ let issues = 0;
663
+
664
+ // Check data directory
665
+ const dir = path.join(process.cwd(), '.neohive');
666
+ if (fs.existsSync(dir)) {
667
+ console.log(' \x1b[32m✓\x1b[0m .neohive/ directory exists');
668
+ try { fs.accessSync(dir, fs.constants.W_OK); console.log(' \x1b[32m✓\x1b[0m .neohive/ is writable'); }
669
+ catch { console.log(' \x1b[31m✗\x1b[0m .neohive/ is NOT writable'); issues++; }
670
+ } else {
671
+ console.log(' \x1b[33m!\x1b[0m .neohive/ not found. Run "npx neohive init" first.');
672
+ issues++;
673
+ }
674
+
675
+ // Check server.js
676
+ const serverPath = path.join(__dirname, 'server.js');
677
+ if (fs.existsSync(serverPath)) {
678
+ console.log(' \x1b[32m✓\x1b[0m server.js found');
679
+ } else {
680
+ console.log(' \x1b[31m✗\x1b[0m server.js MISSING'); issues++;
681
+ }
682
+
683
+ // Check agents online
684
+ if (fs.existsSync(dir)) {
685
+ const agentsFile = path.join(dir, 'agents.json');
686
+ if (fs.existsSync(agentsFile)) {
687
+ const agents = readJson(agentsFile);
688
+ const online = Object.entries(agents).filter(([, a]) => isPidAlive(a.pid)).length;
689
+ const total = Object.keys(agents).length;
690
+ if (online > 0) {
691
+ console.log(' \x1b[32m✓\x1b[0m ' + online + '/' + total + ' agents online');
692
+ } else if (total > 0) {
693
+ console.log(' \x1b[33m!\x1b[0m ' + total + ' agents registered but none online');
694
+ } else {
695
+ console.log(' \x1b[33m!\x1b[0m No agents registered yet');
696
+ }
697
+ }
698
+
699
+ // Check config
700
+ const configFile = path.join(dir, 'config.json');
701
+ if (fs.existsSync(configFile)) {
702
+ const config = readJson(configFile);
703
+ console.log(' \x1b[32m✓\x1b[0m Conversation mode: ' + (config.conversation_mode || 'direct'));
704
+ }
705
+
706
+ // Check guide file
707
+ const guideFile = path.join(dir, 'guide.md');
708
+ if (fs.existsSync(guideFile)) {
709
+ console.log(' \x1b[32m✓\x1b[0m Custom guide.md found');
710
+ }
711
+ }
712
+
713
+ // Check Node version
714
+ const nodeVersion = process.version;
715
+ const major = parseInt(nodeVersion.slice(1));
716
+ if (major >= 18) {
717
+ console.log(' \x1b[32m✓\x1b[0m Node.js ' + nodeVersion + ' (OK)');
718
+ } else {
719
+ console.log(' \x1b[31m✗\x1b[0m Node.js ' + nodeVersion + ' — v18+ recommended'); issues++;
720
+ }
721
+
722
+ console.log('');
723
+ if (issues === 0) {
724
+ console.log(' \x1b[32mAll checks passed. System is healthy.\x1b[0m');
725
+ } else {
726
+ console.log(' \x1b[31m' + issues + ' issue(s) found. Fix them and run doctor again.\x1b[0m');
727
+ }
728
+ console.log('');
729
+ }
730
+
731
+ // Uninstall neohive from all CLI configs
732
+ function uninstall() {
733
+ const cwd = process.cwd();
734
+ const home = os.homedir();
735
+ const removed = [];
736
+ const notFound = [];
737
+
738
+ console.log('');
739
+ console.log(' Neohive — Uninstall');
740
+ console.log(' =========================');
741
+ console.log('');
742
+
743
+ // 1. Remove from Claude Code project config (.mcp.json in cwd)
744
+ const mcpLocalPath = path.join(cwd, '.mcp.json');
745
+ if (fs.existsSync(mcpLocalPath)) {
746
+ try {
747
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpLocalPath, 'utf8'));
748
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers['neohive']) {
749
+ delete mcpConfig.mcpServers['neohive'];
750
+ fs.writeFileSync(mcpLocalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
751
+ removed.push('Claude Code (project): ' + mcpLocalPath);
752
+ } else {
753
+ notFound.push('Claude Code (project): no neohive entry in .mcp.json');
754
+ }
755
+ } catch (e) {
756
+ console.log(' [warn] Could not parse ' + mcpLocalPath + ': ' + e.message);
757
+ }
758
+ } else {
759
+ notFound.push('Claude Code (project): .mcp.json not found');
760
+ }
761
+
762
+ // 2. Remove from Claude Code global config (~/.claude/mcp.json)
763
+ const mcpGlobalPath = path.join(home, '.claude', 'mcp.json');
764
+ if (fs.existsSync(mcpGlobalPath)) {
765
+ try {
766
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpGlobalPath, 'utf8'));
767
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers['neohive']) {
768
+ delete mcpConfig.mcpServers['neohive'];
769
+ fs.writeFileSync(mcpGlobalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
770
+ removed.push('Claude Code (global): ' + mcpGlobalPath);
771
+ } else {
772
+ notFound.push('Claude Code (global): no neohive entry');
773
+ }
774
+ } catch (e) {
775
+ console.log(' [warn] Could not parse ' + mcpGlobalPath + ': ' + e.message);
776
+ }
777
+ } else {
778
+ notFound.push('Claude Code (global): ~/.claude/mcp.json not found');
779
+ }
780
+
781
+ // 3. Remove from Gemini CLI config (~/.gemini/settings.json)
782
+ const geminiSettingsPath = path.join(home, '.gemini', 'settings.json');
783
+ if (fs.existsSync(geminiSettingsPath)) {
784
+ try {
785
+ const settings = JSON.parse(fs.readFileSync(geminiSettingsPath, 'utf8'));
786
+ if (settings.mcpServers && settings.mcpServers['neohive']) {
787
+ delete settings.mcpServers['neohive'];
788
+ fs.writeFileSync(geminiSettingsPath, JSON.stringify(settings, null, 2) + '\n');
789
+ removed.push('Gemini CLI: ' + geminiSettingsPath);
790
+ } else {
791
+ notFound.push('Gemini CLI: no neohive entry');
792
+ }
793
+ } catch (e) {
794
+ console.log(' [warn] Could not parse ' + geminiSettingsPath + ': ' + e.message);
795
+ }
796
+ } else {
797
+ notFound.push('Gemini CLI: ~/.gemini/settings.json not found');
798
+ }
799
+
800
+ // 4. Remove from Gemini CLI project config (.gemini/settings.json in cwd)
801
+ const geminiLocalPath = path.join(cwd, '.gemini', 'settings.json');
802
+ if (fs.existsSync(geminiLocalPath)) {
803
+ try {
804
+ const settings = JSON.parse(fs.readFileSync(geminiLocalPath, 'utf8'));
805
+ if (settings.mcpServers && settings.mcpServers['neohive']) {
806
+ delete settings.mcpServers['neohive'];
807
+ fs.writeFileSync(geminiLocalPath, JSON.stringify(settings, null, 2) + '\n');
808
+ removed.push('Gemini CLI (project): ' + geminiLocalPath);
809
+ } else {
810
+ notFound.push('Gemini CLI (project): no neohive entry');
811
+ }
812
+ } catch (e) {
813
+ console.log(' [warn] Could not parse ' + geminiLocalPath + ': ' + e.message);
814
+ }
815
+ }
816
+
817
+ // 5. Remove from Codex CLI config (~/.codex/config.toml)
818
+ const codexConfigPath = path.join(home, '.codex', 'config.toml');
819
+ if (fs.existsSync(codexConfigPath)) {
820
+ try {
821
+ let config = fs.readFileSync(codexConfigPath, 'utf8');
822
+ if (config.includes('[mcp_servers.neohive]')) {
823
+ // Remove from [mcp_servers.neohive] to the next [section] or end of file
824
+ // This covers both [mcp_servers.neohive] and [mcp_servers.neohive.env]
825
+ config = config.replace(/\n?\[mcp_servers\.neohive[^\]]*\][^\[]*(?=\[|$)/g, '');
826
+ // Clean up multiple blank lines left behind
827
+ config = config.replace(/\n{3,}/g, '\n\n');
828
+ fs.writeFileSync(codexConfigPath, config);
829
+ removed.push('Codex CLI: ' + codexConfigPath);
830
+ } else {
831
+ notFound.push('Codex CLI: no neohive section in config.toml');
832
+ }
833
+ } catch (e) {
834
+ console.log(' [warn] Could not process ' + codexConfigPath + ': ' + e.message);
835
+ }
836
+ } else {
837
+ notFound.push('Codex CLI: ~/.codex/config.toml not found');
838
+ }
839
+
840
+ // 6. Remove from Codex CLI project config (.codex/config.toml in cwd)
841
+ const codexLocalPath = path.join(cwd, '.codex', 'config.toml');
842
+ if (fs.existsSync(codexLocalPath)) {
843
+ try {
844
+ let config = fs.readFileSync(codexLocalPath, 'utf8');
845
+ if (config.includes('[mcp_servers.neohive]')) {
846
+ config = config.replace(/\n?\[mcp_servers\.neohive[^\]]*\][^\[]*(?=\[|$)/g, '');
847
+ config = config.replace(/\n{3,}/g, '\n\n');
848
+ fs.writeFileSync(codexLocalPath, config);
849
+ removed.push('Codex CLI (project): ' + codexLocalPath);
850
+ }
851
+ } catch (e) {
852
+ console.log(' [warn] Could not process ' + codexLocalPath + ': ' + e.message);
853
+ }
854
+ }
855
+
856
+ // Print summary
857
+ if (removed.length > 0) {
858
+ console.log(' Removed neohive from:');
859
+ for (const r of removed) {
860
+ console.log(' [ok] ' + r);
861
+ }
862
+ } else {
863
+ console.log(' No neohive configurations found to remove.');
864
+ }
865
+
866
+ if (notFound.length > 0) {
867
+ console.log('');
868
+ console.log(' Skipped (not found):');
869
+ for (const n of notFound) {
870
+ console.log(' [-] ' + n);
871
+ }
872
+ }
873
+
874
+ // 7. Check for data directory
875
+ const dataPath = path.join(cwd, '.neohive');
876
+ if (fs.existsSync(dataPath)) {
877
+ console.log('');
878
+ console.log(' Found .neohive/ directory with conversation data.');
879
+ console.log(' To remove it, manually delete: ' + dataPath);
880
+ }
881
+
882
+ console.log('');
883
+ if (removed.length > 0) {
884
+ console.log(' Restart your CLI terminals for changes to take effect.');
885
+ }
886
+ console.log('');
887
+ }
888
+
889
+ switch (command) {
890
+ case 'init':
891
+ init();
892
+ break;
893
+ case 'templates':
894
+ listTemplates();
895
+ break;
896
+ case 'dashboard':
897
+ dashboard();
898
+ break;
899
+ case 'reset':
900
+ reset();
901
+ break;
902
+ case 'doctor':
903
+ cliDoctor();
904
+ break;
905
+ case 'msg':
906
+ case 'message':
907
+ case 'send':
908
+ cliMsg();
909
+ break;
910
+ case 'status':
911
+ cliStatus();
912
+ break;
913
+ case 'uninstall':
914
+ case 'remove':
915
+ uninstall();
916
+ break;
917
+ case 'plugin':
918
+ case 'plugins':
919
+ console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
920
+ break;
921
+ case 'help':
922
+ case '--help':
923
+ case '-h':
924
+ case undefined:
925
+ printUsage();
926
+ break;
927
+ default:
928
+ console.error(` Unknown command: ${command}`);
929
+ printUsage();
930
+ process.exit(1);
931
+ }