neohive 6.0.3 → 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 +262 -77
- package/README.md +66 -63
- package/SECURITY.md +8 -6
- package/cli.js +268 -33
- package/dashboard.html +2269 -546
- package/dashboard.js +492 -105
- 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/github-sync.js +291 -0
- package/lib/hooks.js +173 -0
- package/lib/ide-activity.js +121 -0
- package/logo.svg +1 -0
- package/package.json +11 -2
- package/scripts/check-portable-paths.mjs +74 -0
- package/server.js +1148 -743
- 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 };
|