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.
- package/README.md +549 -493
- package/dist/channels/telegram.js +300 -1
- package/dist/cli/commands/restart.js +167 -0
- package/dist/cli/index.js +2 -0
- package/dist/config/mcp-manager.js +140 -0
- package/dist/http/__tests__/status_api.test.js +55 -0
- package/dist/http/__tests__/status_with_server_api.test.js +60 -0
- package/dist/http/api.js +85 -0
- package/dist/ui/assets/index-BiXkm8Yr.css +1 -0
- package/dist/ui/assets/index-BrbyUtJ5.js +96 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-3USYAgWN.css +0 -1
- package/dist/ui/assets/index-DKCPYzx2.js +0 -58
|
@@ -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
|
+
}
|