morpheus-cli 0.2.5 → 0.2.6

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.
@@ -4,9 +4,13 @@ import chalk from 'chalk';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
+ import { spawn } from 'child_process';
7
8
  import { ConfigManager } from '../config/manager.js';
8
9
  import { DisplayManager } from '../runtime/display.js';
9
10
  import { Telephonist } from '../runtime/telephonist.js';
11
+ import { readPid, isProcessRunning, checkStalePid } from '../runtime/lifecycle.js';
12
+ import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
13
+ import { SatiRepository } from '../runtime/memory/sati/repository.js';
10
14
  export class TelegramAdapter {
11
15
  bot = null;
12
16
  isConnected = false;
@@ -40,13 +44,18 @@ export class TelegramAdapter {
40
44
  return; // Silent fail for security
41
45
  }
42
46
  this.display.log(`@${user}: ${text}`, { source: 'Telegram' });
47
+ // Handle system commands
48
+ if (text.startsWith('/')) {
49
+ await this.handleSystemCommand(ctx, text, user);
50
+ return;
51
+ }
43
52
  try {
44
53
  // Send "typing" status
45
54
  await ctx.sendChatAction('typing');
46
55
  // Process with Agent
47
56
  const response = await this.oracle.chat(text);
48
57
  if (response) {
49
- await ctx.reply(response);
58
+ await ctx.reply(response, { parse_mode: 'Markdown' });
50
59
  this.display.log(`Responded to @${user}`, { source: 'Telegram' });
51
60
  }
52
61
  }
@@ -135,6 +144,10 @@ export class TelegramAdapter {
135
144
  }
136
145
  });
137
146
  this.isConnected = true;
147
+ // Check if there's a restart notification to send
148
+ this.checkAndSendRestartNotification().catch((err) => {
149
+ this.display.log(`Failed to send restart notification: ${err.message}`, { source: 'Telegram', level: 'error' });
150
+ });
138
151
  process.once('SIGINT', () => this.disconnect());
139
152
  process.once('SIGTERM', () => this.disconnect());
140
153
  }
@@ -174,4 +187,290 @@ export class TelegramAdapter {
174
187
  this.bot = null;
175
188
  this.display.log(chalk.gray('Telegram disconnected.'), { source: 'Telegram' });
176
189
  }
190
+ async handleSystemCommand(ctx, text, user) {
191
+ const command = text.split(' ')[0];
192
+ const args = text.split(' ').slice(1);
193
+ switch (command) {
194
+ case '/start':
195
+ await this.handleStartCommand(ctx, user);
196
+ break;
197
+ case '/status':
198
+ await this.handleStatusCommand(ctx, user);
199
+ break;
200
+ case '/doctor':
201
+ await this.handleDoctorCommand(ctx, user);
202
+ break;
203
+ case '/stats':
204
+ await this.handleStatsCommand(ctx, user);
205
+ break;
206
+ case '/help':
207
+ await this.handleHelpCommand(ctx, user);
208
+ break;
209
+ case '/zaion':
210
+ await this.handleZaionCommand(ctx, user);
211
+ break;
212
+ case '/sati':
213
+ await this.handleSatiCommand(ctx, user, args);
214
+ break;
215
+ case '/restart':
216
+ await this.handleRestartCommand(ctx, user);
217
+ break;
218
+ default:
219
+ await this.handleDefaultCommand(ctx, user, command);
220
+ }
221
+ }
222
+ async handleStartCommand(ctx, user) {
223
+ const welcomeMessage = `
224
+ Hello, @${user}! I am ${this.config.get().agent.name}, ${this.config.get().agent.personality}.
225
+
226
+ I am your local AI operator/agent. Here are the commands you can use:
227
+
228
+ /start - Show this welcome message and available commands
229
+ /status - Check the status of the Morpheus agent
230
+ /doctor - Diagnose environment and configuration issues
231
+ /stats - Show token usage statistics
232
+ /help - Show available commands
233
+ /zaion - Show system configurations
234
+ /sati <qnt> - Show specific memories
235
+ /restart - Restart the Morpheus agent
236
+
237
+ How can I assist you today?`;
238
+ await ctx.reply(welcomeMessage);
239
+ }
240
+ async handleStatusCommand(ctx, user) {
241
+ try {
242
+ await checkStalePid();
243
+ const pid = await readPid();
244
+ if (pid && isProcessRunning(pid)) {
245
+ await ctx.reply(`Morpheus is running (PID: ${pid})`);
246
+ }
247
+ else {
248
+ await ctx.reply('Morpheus is stopped.');
249
+ }
250
+ }
251
+ catch (error) {
252
+ await ctx.reply(`Failed to check status: ${error.message}`);
253
+ }
254
+ }
255
+ async handleDoctorCommand(ctx, user) {
256
+ // Implementação simplificada do diagnóstico
257
+ const config = this.config.get();
258
+ let response = '*Morpheus Doctor*\n\n';
259
+ // Verificar versão do Node.js
260
+ const nodeVersion = process.version;
261
+ const majorVersion = parseInt(nodeVersion.replace('v', '').split('.')[0], 10);
262
+ if (majorVersion >= 18) {
263
+ response += '✅ Node.js Version: ' + nodeVersion + ' (Satisfied)\n';
264
+ }
265
+ else {
266
+ response += '❌ Node.js Version: ' + nodeVersion + ' (Required: >=18)\n';
267
+ }
268
+ // Verificar configuração
269
+ if (config) {
270
+ response += '✅ Configuration: Valid\n';
271
+ // Verificar se há chave de API disponível para o provedor ativo
272
+ const llmProvider = config.llm?.provider;
273
+ if (llmProvider && llmProvider !== 'ollama') {
274
+ const hasLlmApiKey = config.llm?.api_key ||
275
+ (llmProvider === 'openai' && process.env.OPENAI_API_KEY) ||
276
+ (llmProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
277
+ (llmProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
278
+ (llmProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
279
+ if (hasLlmApiKey) {
280
+ response += `✅ LLM API key available for ${llmProvider}\n`;
281
+ }
282
+ else {
283
+ response += `❌ LLM API key missing for ${llmProvider}. Either set in config or define environment variable.\n`;
284
+ }
285
+ }
286
+ // Verificar token do Telegram se ativado
287
+ if (config.channels?.telegram?.enabled) {
288
+ const hasTelegramToken = config.channels.telegram?.token || process.env.TELEGRAM_BOT_TOKEN;
289
+ if (hasTelegramToken) {
290
+ response += '✅ Telegram bot token available\n';
291
+ }
292
+ else {
293
+ response += '❌ Telegram bot token missing. Either set in config or define TELEGRAM_BOT_TOKEN environment variable.\n';
294
+ }
295
+ }
296
+ }
297
+ else {
298
+ response += '⚠️ Configuration: Missing\n';
299
+ }
300
+ await ctx.reply(response, { parse_mode: 'Markdown' });
301
+ }
302
+ async handleStatsCommand(ctx, user) {
303
+ try {
304
+ // Criar instância temporária do histórico para obter estatísticas
305
+ const history = new SQLiteChatMessageHistory({
306
+ sessionId: "default",
307
+ databasePath: undefined, // Usará o caminho padrão
308
+ limit: 100, // Limite arbitrário para esta operação
309
+ });
310
+ const stats = await history.getGlobalUsageStats();
311
+ const groupedStats = await history.getUsageStatsByProviderAndModel();
312
+ let response = '*Token Usage Statistics*\n\n';
313
+ response += `Total Input Tokens: ${stats.totalInputTokens}\n`;
314
+ response += `Total Output Tokens: ${stats.totalOutputTokens}\n`;
315
+ response += `Total Tokens: ${stats.totalInputTokens + stats.totalOutputTokens}\n\n`;
316
+ if (groupedStats.length > 0) {
317
+ response += '*Breakdown by Provider and Model:*\n';
318
+ for (const stat of groupedStats) {
319
+ response += `- ${stat.provider}/${stat.model}: ${stat.totalTokens} tokens (${stat.messageCount} messages)\n`;
320
+ }
321
+ }
322
+ else {
323
+ response += 'No detailed usage statistics available.';
324
+ }
325
+ await ctx.reply(response, { parse_mode: 'Markdown' });
326
+ // Fechar conexão com o banco de dados
327
+ history.close();
328
+ }
329
+ catch (error) {
330
+ await ctx.reply(`Failed to retrieve statistics: ${error.message}`);
331
+ }
332
+ }
333
+ async handleDefaultCommand(ctx, user, command) {
334
+ const prompt = `O usuário envio o comando: ${command},
335
+ Não entendemos o comando
336
+ temos os seguintes comandos disponíveis: /start, /status, /doctor, /stats, /help, /zaion, /sati <qnt>, /restart
337
+ Identifique se ele talvez tenha errado o comando e pergunte se ele não quis executar outro comando.
338
+ Só faça isso agora.`;
339
+ let response = await this.oracle.chat(prompt);
340
+ if (response) {
341
+ await ctx.reply(response, { parse_mode: 'Markdown' });
342
+ }
343
+ // await ctx.reply(`Command not recognized. Type /help to see available commands.`);
344
+ }
345
+ async handleHelpCommand(ctx, user) {
346
+ const helpMessage = `
347
+ *Available Commands:*
348
+
349
+ /start - Show welcome message and available commands
350
+ /status - Check the status of the Morpheus agent
351
+ /doctor - Diagnose environment and configuration issues
352
+ /stats - Show token usage statistics
353
+ /help - Show this help message
354
+ /zaion - Show system configurations
355
+ /sati <qnt> - Show specific memories
356
+ /restart - Restart the Morpheus agent
357
+
358
+ How can I assist you today?`;
359
+ await ctx.reply(helpMessage, { parse_mode: 'Markdown' });
360
+ }
361
+ async handleZaionCommand(ctx, user) {
362
+ const config = this.config.get();
363
+ let response = '*System Configuration*\n\n';
364
+ response += `*Agent:*\n`;
365
+ response += `- Name: ${config.agent.name}\n`;
366
+ response += `- Personality: ${config.agent.personality}\n\n`;
367
+ response += `*LLM:*\n`;
368
+ response += `- Provider: ${config.llm.provider}\n`;
369
+ response += `- Model: ${config.llm.model}\n`;
370
+ response += `- Temperature: ${config.llm.temperature}\n`;
371
+ response += `- Context Window: ${config.llm.context_window || 100}\n\n`;
372
+ response += `*Channels:*\n`;
373
+ response += `- Telegram Enabled: ${config.channels.telegram.enabled}\n`;
374
+ response += `- Discord Enabled: ${config.channels.discord.enabled}\n\n`;
375
+ response += `*UI:*\n`;
376
+ response += `- Enabled: ${config.ui.enabled}\n`;
377
+ response += `- Port: ${config.ui.port}\n\n`;
378
+ response += `*Audio:*\n`;
379
+ response += `- Enabled: ${config.audio.enabled}\n`;
380
+ response += `- Max Duration: ${config.audio.maxDurationSeconds}s\n`;
381
+ await ctx.reply(response, { parse_mode: 'Markdown' });
382
+ }
383
+ async handleSatiCommand(ctx, user, args) {
384
+ let limit = null;
385
+ if (args.length > 0) {
386
+ limit = parseInt(args[0], 10);
387
+ if (isNaN(limit) || limit <= 0) {
388
+ await ctx.reply('Invalid quantity. Please specify a positive number. Usage: /sati <qnt>');
389
+ return;
390
+ }
391
+ }
392
+ try {
393
+ // Usar o repositório SATI para obter memórias de longo prazo
394
+ const repository = SatiRepository.getInstance();
395
+ const memories = repository.getAllMemories();
396
+ if (memories.length === 0) {
397
+ await ctx.reply(`No memories found.`);
398
+ return;
399
+ }
400
+ // Se nenhum limite for especificado, usar todas as memórias
401
+ let selectedMemories = memories;
402
+ if (limit !== null) {
403
+ selectedMemories = memories.slice(0, Math.min(limit, memories.length));
404
+ }
405
+ let response = `*${selectedMemories.length} SATI Memories${limit !== null ? ` (Showing first ${selectedMemories.length})` : ''}:*\n\n`;
406
+ for (const memory of selectedMemories) {
407
+ // Limitar o tamanho do resumo para evitar mensagens muito longas
408
+ const truncatedSummary = memory.summary.length > 200 ? memory.summary.substring(0, 200) + '...' : memory.summary;
409
+ response += `*${memory.category} (${memory.importance}):* ${truncatedSummary}\n\n`;
410
+ }
411
+ await ctx.reply(response, { parse_mode: 'Markdown' });
412
+ }
413
+ catch (error) {
414
+ await ctx.reply(`Failed to retrieve memories: ${error.message}`);
415
+ }
416
+ }
417
+ async handleRestartCommand(ctx, user) {
418
+ // Store the user ID who requested the restart
419
+ const userId = ctx.from.id;
420
+ // Save the user ID to a temporary file so the restarted process can notify them
421
+ const restartNotificationFile = path.join(os.tmpdir(), 'morpheus_restart_notification.json');
422
+ try {
423
+ await fs.writeJson(restartNotificationFile, { userId: userId, username: user }, { encoding: 'utf8' });
424
+ }
425
+ catch (error) {
426
+ this.display.log(`Failed to save restart notification info: ${error.message}`, { source: 'Telegram', level: 'error' });
427
+ }
428
+ // Respond to the user first
429
+ await ctx.reply('🔄 Restart initiated. The Morpheus agent will restart shortly.');
430
+ // Schedule the restart after a short delay to ensure the response is sent
431
+ setTimeout(() => {
432
+ // Stop the bot to prevent processing more messages
433
+ if (this.bot) {
434
+ try {
435
+ this.bot.stop();
436
+ }
437
+ catch (e) {
438
+ // Ignore stop errors
439
+ }
440
+ }
441
+ // Execute the restart command using the CLI
442
+ const restartProcess = spawn(process.execPath, [process.argv[1], 'restart'], {
443
+ detached: true,
444
+ stdio: 'ignore'
445
+ });
446
+ restartProcess.unref();
447
+ // Exit the current process
448
+ process.exit(0);
449
+ }, 500); // Shorter delay to minimize chance of processing more messages
450
+ }
451
+ async checkAndSendRestartNotification() {
452
+ const restartNotificationFile = path.join(os.tmpdir(), 'morpheus_restart_notification.json');
453
+ try {
454
+ // Check if the notification file exists
455
+ if (await fs.pathExists(restartNotificationFile)) {
456
+ const notificationData = await fs.readJson(restartNotificationFile);
457
+ // Send a message to the user who requested the restart
458
+ if (this.bot && notificationData.userId) {
459
+ try {
460
+ await this.bot.telegram.sendMessage(notificationData.userId, '✅ Morpheus agent has been successfully restarted!');
461
+ // Optionally, also send to the display
462
+ this.display.log(`Restart notification sent to user ${notificationData.username} (ID: ${notificationData.userId})`, { source: 'Telegram', level: 'info' });
463
+ }
464
+ catch (error) {
465
+ this.display.log(`Failed to send restart notification to user ${notificationData.username}: ${error.message}`, { source: 'Telegram', level: 'error' });
466
+ }
467
+ }
468
+ // Remove the notification file after sending the message
469
+ await fs.remove(restartNotificationFile);
470
+ }
471
+ }
472
+ catch (error) {
473
+ this.display.log(`Error checking restart notification: ${error.message}`, { source: 'Telegram', level: 'error' });
474
+ }
475
+ }
177
476
  }
@@ -0,0 +1,167 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import { scaffold } from '../../runtime/scaffold.js';
5
+ import { DisplayManager } from '../../runtime/display.js';
6
+ import { writePid, readPid, isProcessRunning, clearPid, checkStalePid } from '../../runtime/lifecycle.js';
7
+ import { ConfigManager } from '../../config/manager.js';
8
+ import { renderBanner } from '../utils/render.js';
9
+ import { TelegramAdapter } from '../../channels/telegram.js';
10
+ import { PATHS } from '../../config/paths.js';
11
+ import { Oracle } from '../../runtime/oracle.js';
12
+ import { ProviderError } from '../../runtime/errors.js';
13
+ import { HttpServer } from '../../http/server.js';
14
+ import { getVersion } from '../utils/version.js';
15
+ export const restartCommand = new Command('restart')
16
+ .description('Restart the Morpheus agent')
17
+ .option('--ui', 'Enable web UI', true)
18
+ .option('--no-ui', 'Disable web UI')
19
+ .option('-p, --port <number>', 'Port for web UI', '3333')
20
+ .action(async (options) => {
21
+ const display = DisplayManager.getInstance();
22
+ try {
23
+ // First, try to stop the current process
24
+ display.log(chalk.blue('Stopping current Morpheus process...'));
25
+ await checkStalePid();
26
+ const pid = await readPid();
27
+ if (pid) {
28
+ if (isProcessRunning(pid)) {
29
+ process.kill(pid, 'SIGTERM');
30
+ display.log(chalk.green(`Sent stop signal to Morpheus (PID: ${pid}).`));
31
+ // Wait a bit for the process to terminate
32
+ await new Promise(resolve => setTimeout(resolve, 2000));
33
+ }
34
+ else {
35
+ display.log(chalk.yellow('Current process not running, continuing with restart...'));
36
+ await clearPid();
37
+ }
38
+ }
39
+ else {
40
+ display.log(chalk.yellow('No running process found, continuing with restart...'));
41
+ }
42
+ // Now start a new process
43
+ display.log(chalk.blue('Starting new Morpheus process...'));
44
+ renderBanner(getVersion());
45
+ await scaffold(); // Ensure env exists
46
+ // Cleanup stale PID first
47
+ await checkStalePid();
48
+ const existingPid = await readPid();
49
+ if (existingPid !== null && isProcessRunning(existingPid)) {
50
+ display.log(chalk.red(`Morpheus is already running (PID: ${existingPid})`));
51
+ process.exit(1);
52
+ }
53
+ // Check config existence
54
+ if (!await fs.pathExists(PATHS.config)) {
55
+ display.log(chalk.yellow("Configuration not found."));
56
+ display.log(chalk.cyan("Please run 'morpheus init' first to set up your agent."));
57
+ process.exit(1);
58
+ }
59
+ // Write current PID
60
+ await writePid(process.pid);
61
+ const configManager = ConfigManager.getInstance();
62
+ const config = await configManager.load();
63
+ // Initialize persistent logging
64
+ await display.initialize(config.logging);
65
+ display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
66
+ display.log(chalk.gray(`PID: ${process.pid}`));
67
+ if (options.ui) {
68
+ display.log(chalk.blue(`Web UI enabled to port ${options.port}`));
69
+ }
70
+ // Initialize Oracle
71
+ const oracle = new Oracle(config);
72
+ try {
73
+ display.startSpinner(`Initializing ${config.llm.provider} oracle...`);
74
+ await oracle.initialize();
75
+ display.stopSpinner();
76
+ display.log(chalk.green('✓ Oracle initialized'), { source: 'Oracle' });
77
+ }
78
+ catch (err) {
79
+ display.stopSpinner();
80
+ if (err instanceof ProviderError) {
81
+ display.log(chalk.red(`\nProvider Error (${err.provider}):`));
82
+ display.log(chalk.white(err.message));
83
+ if (err.suggestion) {
84
+ display.log(chalk.yellow(`Tip: ${err.suggestion}`));
85
+ }
86
+ }
87
+ else {
88
+ display.log(chalk.red('\nOracle initialization failed:'));
89
+ display.log(chalk.white(err.message));
90
+ if (err.message.includes('API Key')) {
91
+ display.log(chalk.yellow('Tip: Check your API key in configuration or environment variables.'));
92
+ }
93
+ }
94
+ await clearPid();
95
+ process.exit(1);
96
+ }
97
+ const adapters = [];
98
+ let httpServer;
99
+ // Initialize Web UI
100
+ if (options.ui && config.ui.enabled) {
101
+ try {
102
+ httpServer = new HttpServer();
103
+ // Use CLI port if provided and valid, otherwise fallback to config or default
104
+ const port = parseInt(options.port) || config.ui.port || 3333;
105
+ httpServer.start(port);
106
+ }
107
+ catch (e) {
108
+ display.log(chalk.red(`Failed to start Web UI: ${e.message}`));
109
+ }
110
+ }
111
+ // Initialize Telegram
112
+ if (config.channels.telegram.enabled) {
113
+ if (config.channels.telegram.token) {
114
+ const telegram = new TelegramAdapter(oracle);
115
+ try {
116
+ await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
117
+ adapters.push(telegram);
118
+ }
119
+ catch (e) {
120
+ display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'));
121
+ }
122
+ }
123
+ else {
124
+ display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'));
125
+ }
126
+ }
127
+ // Handle graceful shutdown
128
+ const shutdown = async (signal) => {
129
+ display.stopSpinner();
130
+ display.log(`\n${signal} received. Shutting down...`);
131
+ if (httpServer) {
132
+ httpServer.stop();
133
+ }
134
+ for (const adapter of adapters) {
135
+ await adapter.disconnect();
136
+ }
137
+ await clearPid();
138
+ process.exit(0);
139
+ };
140
+ process.on('SIGINT', () => shutdown('SIGINT'));
141
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
142
+ // Allow ESC to exit
143
+ if (process.stdin.isTTY) {
144
+ process.stdin.setRawMode(true);
145
+ process.stdin.resume();
146
+ process.stdin.setEncoding('utf8');
147
+ process.stdin.on('data', (key) => {
148
+ // ESC or Ctrl+C
149
+ if (key === '\u001B' || key === '\u0003') {
150
+ shutdown('User Quit');
151
+ }
152
+ });
153
+ }
154
+ // Keep process alive (Mock Agent Loop)
155
+ display.startSpinner('Agent active and listening... (Press ctrl+c to stop)');
156
+ // Prevent node from exiting
157
+ setInterval(() => {
158
+ // Heartbeat or background tasks would go here
159
+ }, 5000);
160
+ }
161
+ catch (error) {
162
+ display.stopSpinner();
163
+ console.error(chalk.red('Failed to restart Morpheus:'), error.message);
164
+ await clearPid();
165
+ process.exit(1);
166
+ }
167
+ });
package/dist/cli/index.js CHANGED
@@ -5,6 +5,7 @@ import { statusCommand } from './commands/status.js';
5
5
  import { configCommand } from './commands/config.js';
6
6
  import { doctorCommand } from './commands/doctor.js';
7
7
  import { initCommand } from './commands/init.js';
8
+ import { restartCommand } from './commands/restart.js';
8
9
  import { scaffold } from '../runtime/scaffold.js';
9
10
  import { getVersion } from './utils/version.js';
10
11
  export async function cli() {
@@ -19,6 +20,7 @@ export async function cli() {
19
20
  program.addCommand(initCommand);
20
21
  program.addCommand(startCommand);
21
22
  program.addCommand(stopCommand);
23
+ program.addCommand(restartCommand);
22
24
  program.addCommand(statusCommand);
23
25
  program.addCommand(configCommand);
24
26
  program.addCommand(doctorCommand);
@@ -0,0 +1,140 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { MCPConfigFileSchema, MCPServerConfigSchema } from './schemas.js';
4
+ import { DEFAULT_MCP_TEMPLATE } from '../types/mcp.js';
5
+ import { MORPHEUS_ROOT } from './paths.js';
6
+ const MCP_FILE_NAME = 'mcps.json';
7
+ const RESERVED_KEYS = new Set(['$schema']);
8
+ const readConfigFile = async () => {
9
+ const configPath = path.join(MORPHEUS_ROOT, MCP_FILE_NAME);
10
+ try {
11
+ const raw = await fs.readFile(configPath, 'utf-8');
12
+ const parsed = JSON.parse(raw);
13
+ return MCPConfigFileSchema.parse(parsed);
14
+ }
15
+ catch (error) {
16
+ if (error.code === 'ENOENT') {
17
+ return DEFAULT_MCP_TEMPLATE;
18
+ }
19
+ throw error;
20
+ }
21
+ };
22
+ const writeConfigFile = async (config) => {
23
+ const configPath = path.join(MORPHEUS_ROOT, MCP_FILE_NAME);
24
+ const serialized = JSON.stringify(config, null, 2) + '\n';
25
+ await fs.writeFile(configPath, serialized, 'utf-8');
26
+ };
27
+ const isMetadataKey = (key) => key.startsWith('_') || RESERVED_KEYS.has(key);
28
+ const normalizeName = (rawName) => rawName.replace(/^\$/, '');
29
+ const findRawKey = (config, name) => {
30
+ const direct = name in config ? name : null;
31
+ if (direct)
32
+ return direct;
33
+ const prefixed = `$${name}`;
34
+ if (prefixed in config)
35
+ return prefixed;
36
+ return null;
37
+ };
38
+ const ensureValidName = (name) => {
39
+ if (!name || name.trim().length === 0) {
40
+ throw new Error('Name is required.');
41
+ }
42
+ if (name.startsWith('_') || name === '$schema') {
43
+ throw new Error('Reserved names cannot be used for MCP servers.');
44
+ }
45
+ };
46
+ export class MCPManager {
47
+ static async listServers() {
48
+ const config = await readConfigFile();
49
+ const servers = [];
50
+ for (const [rawName, value] of Object.entries(config)) {
51
+ if (isMetadataKey(rawName))
52
+ continue;
53
+ if (rawName === '$schema')
54
+ continue;
55
+ if (!value || typeof value !== 'object')
56
+ continue;
57
+ try {
58
+ const parsed = MCPServerConfigSchema.parse(value);
59
+ const enabled = !rawName.startsWith('$');
60
+ servers.push({
61
+ name: normalizeName(rawName),
62
+ enabled,
63
+ config: parsed,
64
+ });
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ }
70
+ return servers;
71
+ }
72
+ static async addServer(name, config) {
73
+ ensureValidName(name);
74
+ const parsedConfig = MCPServerConfigSchema.parse(config);
75
+ const file = await readConfigFile();
76
+ const existing = findRawKey(file, name);
77
+ if (existing) {
78
+ throw new Error(`Server "${name}" already exists.`);
79
+ }
80
+ const next = {};
81
+ for (const [key, value] of Object.entries(file)) {
82
+ next[key] = value;
83
+ }
84
+ next[name] = parsedConfig;
85
+ await writeConfigFile(next);
86
+ }
87
+ static async updateServer(name, config) {
88
+ ensureValidName(name);
89
+ const parsedConfig = MCPServerConfigSchema.parse(config);
90
+ const file = await readConfigFile();
91
+ const rawKey = findRawKey(file, name);
92
+ if (!rawKey) {
93
+ throw new Error(`Server "${name}" not found.`);
94
+ }
95
+ const next = {};
96
+ for (const [key, value] of Object.entries(file)) {
97
+ if (key === rawKey) {
98
+ next[key] = parsedConfig;
99
+ }
100
+ else {
101
+ next[key] = value;
102
+ }
103
+ }
104
+ await writeConfigFile(next);
105
+ }
106
+ static async deleteServer(name) {
107
+ ensureValidName(name);
108
+ const file = await readConfigFile();
109
+ const rawKey = findRawKey(file, name);
110
+ if (!rawKey) {
111
+ throw new Error(`Server "${name}" not found.`);
112
+ }
113
+ const next = {};
114
+ for (const [key, value] of Object.entries(file)) {
115
+ if (key === rawKey)
116
+ continue;
117
+ next[key] = value;
118
+ }
119
+ await writeConfigFile(next);
120
+ }
121
+ static async setServerEnabled(name, enabled) {
122
+ ensureValidName(name);
123
+ const file = await readConfigFile();
124
+ const rawKey = findRawKey(file, name);
125
+ if (!rawKey) {
126
+ throw new Error(`Server "${name}" not found.`);
127
+ }
128
+ const targetKey = enabled ? normalizeName(rawKey) : `$${normalizeName(rawKey)}`;
129
+ const next = {};
130
+ for (const [key, value] of Object.entries(file)) {
131
+ if (key === rawKey) {
132
+ next[targetKey] = value;
133
+ }
134
+ else {
135
+ next[key] = value;
136
+ }
137
+ }
138
+ await writeConfigFile(next);
139
+ }
140
+ }