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/CHANGELOG.md +640 -0
- package/LICENSE +75 -0
- package/README.md +342 -0
- package/SECURITY.md +58 -0
- package/cli.js +931 -0
- package/conversation-templates/autonomous-feature.json +22 -0
- package/conversation-templates/code-review.json +21 -0
- package/conversation-templates/debug-squad.json +21 -0
- package/conversation-templates/feature-build.json +21 -0
- package/conversation-templates/research-write.json +21 -0
- package/dashboard.html +8571 -0
- package/dashboard.js +2962 -0
- package/lib/agents.js +107 -0
- package/lib/compact.js +124 -0
- package/lib/config.js +127 -0
- package/lib/file-io.js +166 -0
- package/lib/logger.js +13 -0
- package/lib/messaging.js +137 -0
- package/lib/state.js +23 -0
- package/logo.png +0 -0
- package/package.json +57 -0
- package/server.js +7179 -0
- package/templates/debate.json +16 -0
- package/templates/managed.json +26 -0
- package/templates/pair.json +16 -0
- package/templates/review.json +16 -0
- package/templates/team.json +21 -0
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
|
+
}
|