neohive 6.0.2 → 6.1.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 +269 -77
- package/README.md +66 -63
- package/SECURITY.md +8 -6
- package/cli.js +377 -35
- package/conversation-templates/autonomous-feature.json +54 -4
- package/conversation-templates/code-review.json +41 -3
- package/conversation-templates/debug-squad.json +41 -3
- package/conversation-templates/feature-build.json +41 -3
- package/conversation-templates/research-write.json +41 -3
- package/dashboard.html +3954 -921
- package/dashboard.js +1192 -153
- package/design-system.css +708 -0
- package/design-system.html +264 -0
- package/lib/agents.js +20 -6
- package/lib/audit.js +417 -0
- package/lib/codex-neohive-toml.js +34 -0
- package/lib/compact.js +5 -2
- package/lib/config.js +4 -3
- package/lib/file-io.js +3 -3
- package/lib/github-sync.js +291 -0
- package/lib/hooks.js +173 -0
- package/lib/ide-activity.js +121 -0
- package/lib/resolve-server-data-dir.js +96 -0
- package/logo.svg +1 -0
- package/package.json +12 -3
- package/scripts/check-portable-paths.mjs +74 -0
- package/server.js +1986 -857
- package/templates/debate.json +24 -5
- package/templates/managed.json +48 -9
- package/templates/pair.json +22 -3
- package/templates/review.json +26 -5
- package/templates/team.json +38 -8
- package/tools/channels.js +116 -0
- package/tools/governance.js +471 -0
- package/tools/hooks.js +65 -0
- package/tools/knowledge.js +301 -0
- package/tools/messaging.js +321 -0
- package/tools/safety.js +144 -0
- package/tools/system.js +198 -0
- package/tools/tasks.js +446 -0
- package/tools/workflows.js +286 -0
package/lib/audit.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Audit logging module for comprehensive MCP tool call tracking
|
|
4
|
+
// Based on research in kb:audit-logging-research
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
// Configuration
|
|
11
|
+
const AUDIT_MAX_SIZE = parseInt(process.env.NEOHIVE_AUDIT_MAX_SIZE) || 10485760; // 10MB
|
|
12
|
+
const AUDIT_RETENTION_DAYS = parseInt(process.env.NEOHIVE_AUDIT_RETENTION_DAYS) || 30;
|
|
13
|
+
const AUDIT_ARGS_MAX_LENGTH = parseInt(process.env.NEOHIVE_AUDIT_ARGS_MAX_LENGTH) || 500;
|
|
14
|
+
const AUDIT_RESULT_MAX_LENGTH = parseInt(process.env.NEOHIVE_AUDIT_RESULT_MAX_LENGTH) || 1000;
|
|
15
|
+
const AUDIT_LEVEL = process.env.NEOHIVE_AUDIT_LEVEL || 'standard';
|
|
16
|
+
|
|
17
|
+
// Tool categories for classification
|
|
18
|
+
const TOOL_CATEGORIES = {
|
|
19
|
+
// Agent Lifecycle
|
|
20
|
+
'register': 'agent-lifecycle',
|
|
21
|
+
'list_agents': 'agent-lifecycle',
|
|
22
|
+
|
|
23
|
+
// Messaging
|
|
24
|
+
'send_message': 'messaging',
|
|
25
|
+
'broadcast': 'messaging',
|
|
26
|
+
'listen': 'messaging',
|
|
27
|
+
'listen_codex': 'messaging',
|
|
28
|
+
'wait_for_reply': 'messaging',
|
|
29
|
+
'check_messages': 'messaging',
|
|
30
|
+
'consume_messages': 'messaging',
|
|
31
|
+
'get_notifications': 'messaging',
|
|
32
|
+
'ack_message': 'messaging',
|
|
33
|
+
'get_history': 'messaging',
|
|
34
|
+
'search_messages': 'messaging',
|
|
35
|
+
|
|
36
|
+
// Task Management
|
|
37
|
+
'create_task': 'tasks',
|
|
38
|
+
'update_task': 'tasks',
|
|
39
|
+
'list_tasks': 'tasks',
|
|
40
|
+
|
|
41
|
+
// Workflows
|
|
42
|
+
'create_workflow': 'workflows',
|
|
43
|
+
'advance_workflow': 'workflows',
|
|
44
|
+
'workflow_status': 'workflows',
|
|
45
|
+
|
|
46
|
+
// Knowledge Base
|
|
47
|
+
'kb_write': 'knowledge',
|
|
48
|
+
'kb_read': 'knowledge',
|
|
49
|
+
'kb_list': 'knowledge',
|
|
50
|
+
'log_decision': 'knowledge',
|
|
51
|
+
'get_decisions': 'knowledge',
|
|
52
|
+
'get_briefing': 'knowledge',
|
|
53
|
+
'get_compressed_history': 'knowledge',
|
|
54
|
+
'update_progress': 'knowledge',
|
|
55
|
+
'get_progress': 'knowledge',
|
|
56
|
+
'get_summary': 'knowledge',
|
|
57
|
+
|
|
58
|
+
// Governance
|
|
59
|
+
'call_vote': 'governance',
|
|
60
|
+
'cast_vote': 'governance',
|
|
61
|
+
'vote_status': 'governance',
|
|
62
|
+
'request_review': 'governance',
|
|
63
|
+
'submit_review': 'governance',
|
|
64
|
+
'request_push_approval': 'governance',
|
|
65
|
+
'ack_push': 'governance',
|
|
66
|
+
'add_rule': 'governance',
|
|
67
|
+
'remove_rule': 'governance',
|
|
68
|
+
'toggle_rule': 'governance',
|
|
69
|
+
'log_violation': 'governance',
|
|
70
|
+
|
|
71
|
+
// File Safety
|
|
72
|
+
'lock_file': 'safety',
|
|
73
|
+
'unlock_file': 'safety',
|
|
74
|
+
'declare_dependency': 'safety',
|
|
75
|
+
'check_dependencies': 'safety',
|
|
76
|
+
|
|
77
|
+
// System
|
|
78
|
+
'workspace_write': 'system',
|
|
79
|
+
'workspace_read': 'system',
|
|
80
|
+
'workspace_list': 'system',
|
|
81
|
+
'update_profile': 'system',
|
|
82
|
+
'list_branches': 'system',
|
|
83
|
+
'fork_conversation': 'system',
|
|
84
|
+
'switch_branch': 'system',
|
|
85
|
+
'set_conversation_mode': 'system',
|
|
86
|
+
|
|
87
|
+
// Channels
|
|
88
|
+
'join_channel': 'channels',
|
|
89
|
+
'leave_channel': 'channels',
|
|
90
|
+
'list_channels': 'channels',
|
|
91
|
+
|
|
92
|
+
// Autonomy
|
|
93
|
+
'get_work': 'autonomy',
|
|
94
|
+
'verify_and_advance': 'autonomy',
|
|
95
|
+
'retry_with_improvement': 'autonomy',
|
|
96
|
+
'start_plan': 'autonomy',
|
|
97
|
+
'distribute_prompt': 'autonomy',
|
|
98
|
+
'claim_manager': 'autonomy',
|
|
99
|
+
'yield_floor': 'autonomy',
|
|
100
|
+
'set_phase': 'autonomy',
|
|
101
|
+
|
|
102
|
+
// Utilities
|
|
103
|
+
'share_file': 'utilities',
|
|
104
|
+
'handoff': 'utilities',
|
|
105
|
+
'reset': 'utilities',
|
|
106
|
+
'get_guide': 'utilities',
|
|
107
|
+
'get_reputation': 'utilities',
|
|
108
|
+
'listen_group': 'utilities'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Audit levels configuration
|
|
112
|
+
const AUDIT_LEVELS = {
|
|
113
|
+
minimal: ['governance'], // Only governance tools (current behavior)
|
|
114
|
+
standard: ['messaging', 'tasks', 'workflows', 'governance', 'safety'], // Core tools
|
|
115
|
+
full: Object.keys(TOOL_CATEGORIES) // All categories
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let auditFile = null;
|
|
119
|
+
let pendingWrites = [];
|
|
120
|
+
let writeTimer = null;
|
|
121
|
+
|
|
122
|
+
function init(dataDir) {
|
|
123
|
+
auditFile = path.join(dataDir, 'audit_log.jsonl');
|
|
124
|
+
|
|
125
|
+
// Ensure audit file exists
|
|
126
|
+
if (!fs.existsSync(auditFile)) {
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(auditFile, '', 'utf8');
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error('[audit] Failed to create audit log:', e.message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Start cleanup timer for old archives
|
|
135
|
+
setInterval(cleanupOldArchives, 24 * 60 * 60 * 1000); // Daily cleanup
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function shouldLogTool(toolName) {
|
|
139
|
+
if (AUDIT_LEVEL === 'disabled') return false;
|
|
140
|
+
|
|
141
|
+
const category = TOOL_CATEGORIES[toolName] || 'unknown';
|
|
142
|
+
const allowedCategories = AUDIT_LEVELS[AUDIT_LEVEL] || AUDIT_LEVELS.standard;
|
|
143
|
+
|
|
144
|
+
return allowedCategories.includes(category);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function truncateContent(content, maxLength) {
|
|
148
|
+
if (typeof content !== 'string') {
|
|
149
|
+
content = JSON.stringify(content);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (content.length <= maxLength) return content;
|
|
153
|
+
return content.substring(0, maxLength - 3) + '...';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function redactSensitiveArgs(toolName, args) {
|
|
157
|
+
if (!args || typeof args !== 'object') return args;
|
|
158
|
+
|
|
159
|
+
const redacted = { ...args };
|
|
160
|
+
|
|
161
|
+
// Redact message content for messaging tools
|
|
162
|
+
if (TOOL_CATEGORIES[toolName] === 'messaging' && redacted.content) {
|
|
163
|
+
redacted.content = truncateContent(redacted.content, 100);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Redact sensitive fields
|
|
167
|
+
const sensitiveFields = ['token', 'password', 'secret', 'key', 'auth'];
|
|
168
|
+
for (const field of sensitiveFields) {
|
|
169
|
+
if (redacted[field]) {
|
|
170
|
+
redacted[field] = '[REDACTED]';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return redacted;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function generateRequestId() {
|
|
178
|
+
return 'req_' + crypto.randomBytes(6).toString('hex');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function logToolCall(agent, toolName, args, result, durationMs, context = {}) {
|
|
182
|
+
if (!shouldLogTool(toolName)) return;
|
|
183
|
+
|
|
184
|
+
const entry = {
|
|
185
|
+
timestamp: new Date().toISOString(),
|
|
186
|
+
request_id: generateRequestId(),
|
|
187
|
+
agent: agent || 'unknown',
|
|
188
|
+
tool: toolName,
|
|
189
|
+
category: TOOL_CATEGORIES[toolName] || 'unknown',
|
|
190
|
+
args: redactSensitiveArgs(toolName, args),
|
|
191
|
+
result: result ? {
|
|
192
|
+
success: !result.error,
|
|
193
|
+
...(result.error ? { error: truncateContent(result.error, 200) } : {}),
|
|
194
|
+
...(result.messageId ? { messageId: result.messageId } : {}),
|
|
195
|
+
...(result.success !== undefined ? { success: result.success } : {})
|
|
196
|
+
} : null,
|
|
197
|
+
duration_ms: Math.round(durationMs || 0),
|
|
198
|
+
context: {
|
|
199
|
+
...context,
|
|
200
|
+
session_id: context.session_id || 'sess_' + crypto.randomBytes(4).toString('hex')
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Truncate large args/results
|
|
205
|
+
if (entry.args) {
|
|
206
|
+
entry.args = JSON.parse(truncateContent(JSON.stringify(entry.args), AUDIT_ARGS_MAX_LENGTH));
|
|
207
|
+
}
|
|
208
|
+
if (entry.result) {
|
|
209
|
+
entry.result = JSON.parse(truncateContent(JSON.stringify(entry.result), AUDIT_RESULT_MAX_LENGTH));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add to pending writes for batch processing
|
|
213
|
+
pendingWrites.push(entry);
|
|
214
|
+
|
|
215
|
+
// Schedule batch write
|
|
216
|
+
if (!writeTimer) {
|
|
217
|
+
writeTimer = setTimeout(flushPendingWrites, 100); // 100ms batch window
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function flushPendingWrites() {
|
|
222
|
+
if (pendingWrites.length === 0) return;
|
|
223
|
+
|
|
224
|
+
const entries = [...pendingWrites];
|
|
225
|
+
pendingWrites = [];
|
|
226
|
+
writeTimer = null;
|
|
227
|
+
|
|
228
|
+
// Async write to avoid blocking MCP calls
|
|
229
|
+
setImmediate(() => {
|
|
230
|
+
try {
|
|
231
|
+
const lines = entries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
|
232
|
+
fs.appendFileSync(auditFile, lines, 'utf8');
|
|
233
|
+
|
|
234
|
+
// Check if rotation is needed
|
|
235
|
+
checkRotation();
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.error('[audit] Failed to write entries:', e.message);
|
|
238
|
+
// Re-queue failed entries
|
|
239
|
+
pendingWrites.unshift(...entries);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function checkRotation() {
|
|
245
|
+
if (!auditFile || !fs.existsSync(auditFile)) return;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const stats = fs.statSync(auditFile);
|
|
249
|
+
if (stats.size >= AUDIT_MAX_SIZE) {
|
|
250
|
+
rotateAuditFile();
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.error('[audit] Failed to check file size:', e.message);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function rotateAuditFile() {
|
|
258
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
259
|
+
const archiveName = `audit_log_${timestamp}.jsonl`;
|
|
260
|
+
const archivePath = path.join(path.dirname(auditFile), archiveName);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Move current file to archive
|
|
264
|
+
fs.renameSync(auditFile, archivePath);
|
|
265
|
+
|
|
266
|
+
// Create new audit file
|
|
267
|
+
fs.writeFileSync(auditFile, '', 'utf8');
|
|
268
|
+
|
|
269
|
+
console.log(`[audit] Rotated audit log to ${archiveName}`);
|
|
270
|
+
|
|
271
|
+
// Compress archive in background
|
|
272
|
+
setImmediate(() => compressArchive(archivePath));
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error('[audit] Failed to rotate audit log:', e.message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function compressArchive(archivePath) {
|
|
279
|
+
try {
|
|
280
|
+
const zlib = require('zlib');
|
|
281
|
+
const readStream = fs.createReadStream(archivePath);
|
|
282
|
+
const writeStream = fs.createWriteStream(archivePath + '.gz');
|
|
283
|
+
const gzip = zlib.createGzip();
|
|
284
|
+
|
|
285
|
+
readStream.pipe(gzip).pipe(writeStream);
|
|
286
|
+
|
|
287
|
+
writeStream.on('finish', () => {
|
|
288
|
+
// Remove uncompressed archive
|
|
289
|
+
fs.unlinkSync(archivePath);
|
|
290
|
+
console.log(`[audit] Compressed archive: ${path.basename(archivePath)}.gz`);
|
|
291
|
+
});
|
|
292
|
+
} catch (e) {
|
|
293
|
+
console.error('[audit] Failed to compress archive:', e.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cleanupOldArchives() {
|
|
298
|
+
if (!auditFile) return;
|
|
299
|
+
|
|
300
|
+
const auditDir = path.dirname(auditFile);
|
|
301
|
+
const cutoffTime = Date.now() - (AUDIT_RETENTION_DAYS * 24 * 60 * 60 * 1000);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const files = fs.readdirSync(auditDir);
|
|
305
|
+
for (const file of files) {
|
|
306
|
+
if (file.startsWith('audit_log_') && (file.endsWith('.jsonl') || file.endsWith('.jsonl.gz'))) {
|
|
307
|
+
const filePath = path.join(auditDir, file);
|
|
308
|
+
const stats = fs.statSync(filePath);
|
|
309
|
+
|
|
310
|
+
if (stats.mtime.getTime() < cutoffTime) {
|
|
311
|
+
fs.unlinkSync(filePath);
|
|
312
|
+
console.log(`[audit] Cleaned up old archive: ${file}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (e) {
|
|
317
|
+
console.error('[audit] Failed to cleanup old archives:', e.message);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readAuditLog(filters = {}) {
|
|
322
|
+
if (!auditFile || !fs.existsSync(auditFile)) return [];
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const content = fs.readFileSync(auditFile, 'utf8');
|
|
326
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
327
|
+
|
|
328
|
+
let entries = lines.map(line => {
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(line);
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}).filter(entry => entry !== null);
|
|
335
|
+
|
|
336
|
+
// Apply filters
|
|
337
|
+
if (filters.agent) {
|
|
338
|
+
entries = entries.filter(e => e.agent === filters.agent);
|
|
339
|
+
}
|
|
340
|
+
if (filters.tool) {
|
|
341
|
+
entries = entries.filter(e => e.tool === filters.tool);
|
|
342
|
+
}
|
|
343
|
+
if (filters.category) {
|
|
344
|
+
entries = entries.filter(e => e.category === filters.category);
|
|
345
|
+
}
|
|
346
|
+
if (filters.since) {
|
|
347
|
+
const sinceTime = new Date(filters.since).getTime();
|
|
348
|
+
entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
|
|
349
|
+
}
|
|
350
|
+
if (filters.until) {
|
|
351
|
+
const untilTime = new Date(filters.until).getTime();
|
|
352
|
+
entries = entries.filter(e => new Date(e.timestamp).getTime() <= untilTime);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Sort by timestamp (newest first)
|
|
356
|
+
entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
357
|
+
|
|
358
|
+
// Apply limit
|
|
359
|
+
const limit = parseInt(filters.limit) || 100;
|
|
360
|
+
return entries.slice(0, limit);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.error('[audit] Failed to read audit log:', e.message);
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getAuditStats(filters = {}) {
|
|
368
|
+
const entries = readAuditLog(filters);
|
|
369
|
+
|
|
370
|
+
const stats = {
|
|
371
|
+
total_calls: entries.length,
|
|
372
|
+
agents: {},
|
|
373
|
+
tools: {},
|
|
374
|
+
categories: {},
|
|
375
|
+
success_rate: 0,
|
|
376
|
+
avg_duration_ms: 0
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
let successCount = 0;
|
|
380
|
+
let totalDuration = 0;
|
|
381
|
+
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
// Agent stats
|
|
384
|
+
stats.agents[entry.agent] = (stats.agents[entry.agent] || 0) + 1;
|
|
385
|
+
|
|
386
|
+
// Tool stats
|
|
387
|
+
stats.tools[entry.tool] = (stats.tools[entry.tool] || 0) + 1;
|
|
388
|
+
|
|
389
|
+
// Category stats
|
|
390
|
+
stats.categories[entry.category] = (stats.categories[entry.category] || 0) + 1;
|
|
391
|
+
|
|
392
|
+
// Success tracking
|
|
393
|
+
if (entry.result && entry.result.success !== false) {
|
|
394
|
+
successCount++;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Duration tracking
|
|
398
|
+
if (entry.duration_ms) {
|
|
399
|
+
totalDuration += entry.duration_ms;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
stats.success_rate = entries.length > 0 ? Math.round((successCount / entries.length) * 100) : 0;
|
|
404
|
+
stats.avg_duration_ms = entries.length > 0 ? Math.round(totalDuration / entries.length) : 0;
|
|
405
|
+
|
|
406
|
+
return stats;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
init,
|
|
411
|
+
logToolCall,
|
|
412
|
+
readAuditLog,
|
|
413
|
+
getAuditStats,
|
|
414
|
+
shouldLogTool,
|
|
415
|
+
TOOL_CATEGORIES,
|
|
416
|
+
AUDIT_LEVELS
|
|
417
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** TOML section header for Neohive MCP in Codex config.toml */
|
|
4
|
+
const HEADER = '[mcp_servers.neohive]';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Insert or replace the [mcp_servers.neohive] table (up to the next [section]).
|
|
8
|
+
* Preserves following sections (e.g. [mcp_servers.neohive.env]).
|
|
9
|
+
* @param {string} config
|
|
10
|
+
* @param {{ command: string, serverPath: string, timeout?: number, envSection?: string }} opts
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function upsertNeohiveMcpInToml(config, opts) {
|
|
14
|
+
const { command, serverPath, timeout = 300, envSection } = opts;
|
|
15
|
+
const blockBody =
|
|
16
|
+
`command = ${JSON.stringify(command)}\n` +
|
|
17
|
+
`args = [${JSON.stringify(serverPath)}]\n` +
|
|
18
|
+
`timeout = ${timeout}\n`;
|
|
19
|
+
|
|
20
|
+
const idx = config.indexOf(HEADER);
|
|
21
|
+
if (idx === -1) {
|
|
22
|
+
const sep = config.length && !config.endsWith('\n') ? '\n' : '';
|
|
23
|
+
let addition = `${sep}\n${HEADER}\n${blockBody}`;
|
|
24
|
+
if (envSection) addition += envSection.endsWith('\n') ? envSection : envSection + '\n';
|
|
25
|
+
return config + addition;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const afterHeader = idx + HEADER.length;
|
|
29
|
+
const nextSecIdx = config.indexOf('\n[', afterHeader);
|
|
30
|
+
const end = nextSecIdx === -1 ? config.length : nextSecIdx;
|
|
31
|
+
return config.slice(0, idx) + HEADER + '\n' + blockBody + config.slice(end);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { HEADER, upsertNeohiveMcpInToml };
|
package/lib/compact.js
CHANGED
|
@@ -62,7 +62,8 @@ function autoCompact() {
|
|
|
62
62
|
const messages = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
63
63
|
|
|
64
64
|
const agents = getAgents();
|
|
65
|
-
const
|
|
65
|
+
const allAgentNames = Object.keys(agents);
|
|
66
|
+
const retentionMs = (parseInt(process.env.NEOHIVE_RETENTION_HOURS) || 24) * 3600000;
|
|
66
67
|
const allConsumed = new Set();
|
|
67
68
|
const perAgentConsumed = {};
|
|
68
69
|
if (fs.existsSync(DATA_DIR)) {
|
|
@@ -80,7 +81,9 @@ function autoCompact() {
|
|
|
80
81
|
|
|
81
82
|
const active = messages.filter(m => {
|
|
82
83
|
if (m.to === '__group__') {
|
|
83
|
-
|
|
84
|
+
const msgTime = new Date(m.timestamp).getTime();
|
|
85
|
+
if (msgTime < Date.now() - retentionMs) return false;
|
|
86
|
+
return !allAgentNames.every(n => n === m.from || (perAgentConsumed[n] && perAgentConsumed[n].has(m.id)));
|
|
84
87
|
}
|
|
85
88
|
if (!allConsumed.has(m.id)) return true;
|
|
86
89
|
return false;
|
package/lib/config.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { resolveDataDirForServer } = require('./resolve-server-data-dir');
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
const DATA_DIR =
|
|
7
|
+
// Same rules as server.js: env, else repo .cursor/mcp.json sibling, else cwd/.neohive
|
|
8
|
+
const DATA_DIR = resolveDataDirForServer(path.join(__dirname, '..'));
|
|
8
9
|
|
|
9
10
|
// File paths for all shared data
|
|
10
11
|
const MESSAGES_FILE = path.join(DATA_DIR, 'messages.jsonl');
|
|
@@ -35,7 +36,7 @@ const CHANNELS_FILE_PATH = path.join(DATA_DIR, 'channels.json');
|
|
|
35
36
|
// Constants
|
|
36
37
|
const MAX_CONTENT_BYTES = 1000000; // 1 MB max message size
|
|
37
38
|
const CURRENT_DATA_VERSION = 1;
|
|
38
|
-
const RESERVED_NAMES = ['__system__', '__all__', '__open__', '__close__', 'system', 'dashboard', 'Dashboard'];
|
|
39
|
+
const RESERVED_NAMES = ['__system__', '__all__', '__open__', '__close__', '__user__', 'system', 'dashboard', 'Dashboard'];
|
|
39
40
|
|
|
40
41
|
// Config helpers
|
|
41
42
|
function getConfig() {
|
package/lib/file-io.js
CHANGED
|
@@ -107,7 +107,7 @@ function lockAgentsFile() {
|
|
|
107
107
|
while (Date.now() - start < maxWait) {
|
|
108
108
|
try { fs.writeFileSync(AGENTS_LOCK, String(process.pid), { flag: 'wx' }); return true; }
|
|
109
109
|
catch {}
|
|
110
|
-
|
|
110
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, backoff); } catch {}
|
|
111
111
|
backoff = Math.min(backoff * 2, 500);
|
|
112
112
|
}
|
|
113
113
|
try { fs.unlinkSync(AGENTS_LOCK); } catch {}
|
|
@@ -123,7 +123,7 @@ function lockConfigFile() {
|
|
|
123
123
|
while (Date.now() - start < maxWait) {
|
|
124
124
|
try { fs.writeFileSync(CONFIG_LOCK, String(process.pid), { flag: 'wx' }); return true; }
|
|
125
125
|
catch {}
|
|
126
|
-
|
|
126
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50); } catch {}
|
|
127
127
|
}
|
|
128
128
|
try { fs.unlinkSync(CONFIG_LOCK); } catch {}
|
|
129
129
|
try { fs.writeFileSync(CONFIG_LOCK, String(process.pid), { flag: 'wx' }); return true; } catch {}
|
|
@@ -139,7 +139,7 @@ function withFileLock(filePath, fn) {
|
|
|
139
139
|
while (Date.now() - start < maxWait) {
|
|
140
140
|
try { fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx' }); break; }
|
|
141
141
|
catch {}
|
|
142
|
-
|
|
142
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, backoff); } catch {}
|
|
143
143
|
backoff = Math.min(backoff * 2, 500);
|
|
144
144
|
if (Date.now() - start >= maxWait) {
|
|
145
145
|
try {
|