morpheus-cli 0.4.10 → 0.4.12

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.
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
7
  import { spawn } from 'child_process';
8
+ import { convert } from 'telegram-markdown-v2';
8
9
  import { ConfigManager } from '../config/manager.js';
9
10
  import { DisplayManager } from '../runtime/display.js';
10
11
  import { createTelephonist } from '../runtime/telephonist.js';
@@ -13,6 +14,33 @@ import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
13
14
  import { SatiRepository } from '../runtime/memory/sati/repository.js';
14
15
  import { MCPManager } from '../config/mcp-manager.js';
15
16
  import { Construtor } from '../runtime/tools/factory.js';
17
+ /**
18
+ * Converts standard Markdown (as produced by LLMs) to Telegram MarkdownV2.
19
+ * Unsupported tags (e.g. tables) have their special chars escaped so they
20
+ * render as plain text instead of breaking the parse.
21
+ * Truncates to Telegram's 4096-char hard limit.
22
+ * Use for dynamic LLM/Oracle output.
23
+ */
24
+ function toMd(text) {
25
+ const MAX = 4096;
26
+ const converted = convert(text, 'escape');
27
+ const safe = converted.length > MAX ? converted.slice(0, MAX - 3) + '\\.\\.\\.' : converted;
28
+ return { text: safe, parse_mode: 'MarkdownV2' };
29
+ }
30
+ /**
31
+ * Escapes special characters in a plain string segment so it's safe to embed
32
+ * inside a manually-built MarkdownV2 message. Does NOT touch * _ ` [ ] chars
33
+ * (those are intentional MarkdownV2 formatting from our own code).
34
+ * Use for dynamic values (usernames, numbers, paths) interpolated into fixed templates.
35
+ */
36
+ function escMd(value) {
37
+ // Escape all MarkdownV2 special characters that are NOT used as intentional
38
+ // formatters in our static templates (*bold*, _italic_, `code`, [link]).
39
+ // Per Telegram docs: _ * [ ] ( ) ~ ` # + - = | { } . ! must be escaped.
40
+ // We skip * _ ` [ ] here because those are our intentional formatters.
41
+ // The - must be at end of character class to avoid being treated as a range.
42
+ return String(value).replace(/([.!?(){}#+~|=>$@\\-])/g, '\\$1');
43
+ }
16
44
  export class TelegramAdapter {
17
45
  bot = null;
18
46
  isConnected = false;
@@ -33,18 +61,18 @@ export class TelegramAdapter {
33
61
  this.rateLimiter.set(userId, now);
34
62
  return false;
35
63
  }
36
- HELP_MESSAGE = `/start - Show this welcome message and available commands
37
- /status - Check the status of the Morpheus agent
38
- /doctor - Diagnose environment and configuration issues
39
- /stats - Show token usage statistics
40
- /help - Show available commands
41
- /zaion - Show system configurations
42
- /sati <qnt> - Show specific memories
43
- /newsession - Archive current session and start fresh
44
- /sessions - List all sessions with titles and switch between them
45
- /restart - Restart the Morpheus agent
46
- /mcpreload - Reload MCP servers without restarting
47
- /mcp or /mcps - List registered MCP servers`;
64
+ HELP_MESSAGE = `/start \\- Show this welcome message and available commands
65
+ /status \\- Check the status of the Morpheus agent
66
+ /doctor \\- Diagnose environment and configuration issues
67
+ /stats \\- Show token usage statistics
68
+ /help \\- Show available commands
69
+ /zaion \\- Show system configurations
70
+ /sati <qnt> \\- Show specific memories
71
+ /newsession \\- Archive current session and start fresh
72
+ /sessions \\- List all sessions with titles and switch between them
73
+ /restart \\- Restart the Morpheus agent
74
+ /mcpreload \\- Reload MCP servers without restarting
75
+ /mcp or /mcps \\- List registered MCP servers`;
48
76
  constructor(oracle) {
49
77
  this.oracle = oracle;
50
78
  }
@@ -87,7 +115,12 @@ export class TelegramAdapter {
87
115
  // Process with Agent
88
116
  const response = await this.oracle.chat(text);
89
117
  if (response) {
90
- await ctx.reply(response);
118
+ try {
119
+ await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
120
+ }
121
+ catch {
122
+ await ctx.reply(response);
123
+ }
91
124
  this.display.log(`Responded to @${user}: ${response}`, { source: 'Telegram' });
92
125
  }
93
126
  }
@@ -154,7 +187,7 @@ export class TelegramAdapter {
154
187
  // The prompt says "reply with the answer".
155
188
  // "Transcribe them... and process the resulting text as a standard user prompt."
156
189
  // So I should treat 'text' as if it was a text message.
157
- await ctx.reply(`🎤 *Transcription*: _"${text}"_`, { parse_mode: 'Markdown' });
190
+ await ctx.reply(`🎤 Transcription: "${text}"`);
158
191
  await ctx.sendChatAction('typing');
159
192
  // Process with Agent
160
193
  const response = await this.oracle.chat(text, usage, true);
@@ -166,7 +199,12 @@ export class TelegramAdapter {
166
199
  // }
167
200
  // }
168
201
  if (response) {
169
- await ctx.reply(response);
202
+ try {
203
+ await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
204
+ }
205
+ catch {
206
+ await ctx.reply(response);
207
+ }
170
208
  this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
171
209
  }
172
210
  }
@@ -227,7 +265,7 @@ export class TelegramAdapter {
227
265
  const sessionId = data.replace('ask_archive_session_', '');
228
266
  // Fetch session title for better UX (optional, but nice) - for now just use ID
229
267
  await ctx.reply(`⚠️ **ARCHIVE SESSION?**\n\nAre you sure you want to archive session \`${sessionId}\`?\n\nIt will be moved to long-term memory (SATI) and removed from the active list. This action cannot be easily undone via Telegram.`, {
230
- parse_mode: 'Markdown',
268
+ parse_mode: 'MarkdownV2',
231
269
  reply_markup: {
232
270
  inline_keyboard: [
233
271
  [
@@ -249,7 +287,7 @@ export class TelegramAdapter {
249
287
  if (ctx.updateType === 'callback_query') {
250
288
  ctx.deleteMessage().catch(() => { });
251
289
  }
252
- await ctx.reply(`✅ Session \`${sessionId}\` has been archived and moved to long-term memory.`, { parse_mode: 'Markdown' });
290
+ await ctx.reply(`✅ Session \`${sessionId}\` has been archived and moved to long-term memory.`, { parse_mode: 'MarkdownV2' });
253
291
  }
254
292
  catch (error) {
255
293
  await ctx.answerCbQuery(`Error archiving: ${error.message}`, { show_alert: true });
@@ -260,7 +298,7 @@ export class TelegramAdapter {
260
298
  const data = ctx.callbackQuery.data;
261
299
  const sessionId = data.replace('ask_delete_session_', '');
262
300
  await ctx.reply(`🚫 **DELETE SESSION?**\n\nAre you sure you want to PERMANENTLY DELETE session \`${sessionId}\`?\n\nThis action is **IRREVERSIBLE**. All data will be lost.`, {
263
- parse_mode: 'Markdown',
301
+ parse_mode: 'MarkdownV2',
264
302
  reply_markup: {
265
303
  inline_keyboard: [
266
304
  [
@@ -282,7 +320,7 @@ export class TelegramAdapter {
282
320
  if (ctx.updateType === 'callback_query') {
283
321
  ctx.deleteMessage().catch(() => { });
284
322
  }
285
- await ctx.reply(`🗑️ Session \`${sessionId}\` has been permanently deleted.`, { parse_mode: 'Markdown' });
323
+ await ctx.reply(`🗑️ Session \`${sessionId}\` has been permanently deleted.`, { parse_mode: 'MarkdownV2' });
286
324
  }
287
325
  catch (error) {
288
326
  await ctx.answerCbQuery(`Error deleting: ${error.message}`, { show_alert: true });
@@ -379,17 +417,22 @@ export class TelegramAdapter {
379
417
  this.display.log('No allowed Telegram users configured — skipping notification.', { source: 'Telegram', level: 'warning' });
380
418
  return;
381
419
  }
382
- // Truncate to Telegram's 4096 char limit
383
- const MAX_LEN = 4096;
384
- const safeText = text.length > MAX_LEN ? text.slice(0, MAX_LEN - 3) + '...' : text;
420
+ // toMd() already truncates to 4096 chars (Telegram's hard limit)
421
+ const { text: mdText, parse_mode } = toMd(text);
385
422
  for (const userId of allowedUsers) {
386
423
  try {
387
- // Send as plain text — LLM output often has unbalanced markdown that
388
- // causes "Can't find end of entity" errors with parse_mode: 'Markdown'.
389
- await this.bot.telegram.sendMessage(userId, safeText);
424
+ await this.bot.telegram.sendMessage(userId, mdText, { parse_mode });
390
425
  }
391
- catch (err) {
392
- this.display.log(`Failed to send message to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
426
+ catch {
427
+ // Fallback to plain text if MarkdownV2 conversion still fails
428
+ try {
429
+ const MAX_LEN = 4096;
430
+ const plain = text.length > MAX_LEN ? text.slice(0, MAX_LEN - 3) + '...' : text;
431
+ await this.bot.telegram.sendMessage(userId, plain);
432
+ }
433
+ catch (err) {
434
+ this.display.log(`Failed to send message to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
435
+ }
393
436
  }
394
437
  }
395
438
  }
@@ -464,7 +507,7 @@ export class TelegramAdapter {
464
507
  async handleNewSessionCommand(ctx, user) {
465
508
  try {
466
509
  await ctx.reply("Are you ready to start a new session? Please confirm.", {
467
- parse_mode: 'Markdown', reply_markup: {
510
+ parse_mode: 'MarkdownV2', reply_markup: {
468
511
  inline_keyboard: [
469
512
  [{ text: 'Yes, start new session', callback_data: 'confirm_new_session' }, { text: 'No, cancel', callback_data: 'cancel_new_session' }]
470
513
  ]
@@ -490,7 +533,7 @@ export class TelegramAdapter {
490
533
  const history = new SQLiteChatMessageHistory({ sessionId: "" });
491
534
  const sessions = await history.listSessions();
492
535
  if (sessions.length === 0) {
493
- await ctx.reply('No active or paused sessions found.', { parse_mode: 'Markdown' });
536
+ await ctx.reply('No active or paused sessions found.', { parse_mode: 'MarkdownV2' });
494
537
  return;
495
538
  }
496
539
  let response = '*Sessions:*\n\n';
@@ -498,10 +541,10 @@ export class TelegramAdapter {
498
541
  for (const session of sessions) {
499
542
  const title = session.title || 'Untitled Session';
500
543
  const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
501
- response += `${statusEmoji} *${title}*\n`;
502
- response += `- ID: ${session.id}\n`;
503
- response += `- Status: ${session.status}\n`;
504
- response += `- Started: ${new Date(session.started_at).toLocaleString()}\n\n`;
544
+ response += `${statusEmoji} *${escMd(title)}*\n`;
545
+ response += `\\- ID: ${escMd(session.id)}\n`;
546
+ response += `\\- Status: ${escMd(session.status)}\n`;
547
+ response += `\\- Started: ${escMd(new Date(session.started_at).toLocaleString())}\n\n`;
505
548
  // Adicionar botão inline para alternar para esta sessão
506
549
  const sessionButtons = [];
507
550
  if (session.status !== 'active') {
@@ -521,7 +564,7 @@ export class TelegramAdapter {
521
564
  keyboard.push(sessionButtons);
522
565
  }
523
566
  await ctx.reply(response, {
524
- parse_mode: 'Markdown',
567
+ parse_mode: 'MarkdownV2',
525
568
  reply_markup: {
526
569
  inline_keyboard: keyboard
527
570
  }
@@ -642,7 +685,7 @@ How can I assist you today?`;
642
685
  else {
643
686
  response += '⚠️ Configuration: Missing\n';
644
687
  }
645
- await ctx.reply(response, { parse_mode: 'Markdown' });
688
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
646
689
  }
647
690
  async handleStatsCommand(ctx, user) {
648
691
  try {
@@ -659,33 +702,33 @@ How can I assist you today?`;
659
702
  const totalAudioSeconds = groupedStats.reduce((sum, s) => sum + (s.totalAudioSeconds || 0), 0);
660
703
  const totalCost = stats.totalEstimatedCostUsd;
661
704
  let response = '*Token Usage Statistics*\n\n';
662
- response += `Input Tokens: ${stats.totalInputTokens.toLocaleString()}\n`;
663
- response += `Output Tokens: ${stats.totalOutputTokens.toLocaleString()}\n`;
664
- response += `Total Tokens: ${totalTokens.toLocaleString()}\n`;
705
+ response += `Input Tokens: ${escMd(stats.totalInputTokens.toLocaleString())}\n`;
706
+ response += `Output Tokens: ${escMd(stats.totalOutputTokens.toLocaleString())}\n`;
707
+ response += `Total Tokens: ${escMd(totalTokens.toLocaleString())}\n`;
665
708
  if (totalAudioSeconds > 0) {
666
- response += `Audio Processed: ${totalAudioSeconds.toFixed(1)}s\n`;
709
+ response += `Audio Processed: ${escMd(totalAudioSeconds.toFixed(1))}s\n`;
667
710
  }
668
711
  if (totalCost != null) {
669
- response += `Estimated Cost: $${totalCost.toFixed(4)}\n`;
712
+ response += `Estimated Cost: \\$${escMd(totalCost.toFixed(4))}\n`;
670
713
  }
671
714
  response += '\n';
672
715
  if (groupedStats.length > 0) {
673
716
  response += '*By Provider/Model:*\n';
674
717
  for (const stat of groupedStats) {
675
- response += `\n*${stat.provider}/${stat.model}*\n`;
676
- response += ` Tokens: ${stat.totalTokens.toLocaleString()} (${stat.messageCount} msgs)\n`;
718
+ response += `\n*${escMd(stat.provider)}/${escMd(stat.model)}*\n`;
719
+ response += ` Tokens: ${escMd(stat.totalTokens.toLocaleString())} \\(${escMd(stat.messageCount)} msgs\\)\n`;
677
720
  if (stat.totalAudioSeconds > 0) {
678
- response += ` Audio: ${stat.totalAudioSeconds.toFixed(1)}s\n`;
721
+ response += ` Audio: ${escMd(stat.totalAudioSeconds.toFixed(1))}s\n`;
679
722
  }
680
723
  if (stat.estimatedCostUsd != null) {
681
- response += ` Cost: $${stat.estimatedCostUsd.toFixed(4)}\n`;
724
+ response += ` Cost: \\$${escMd(stat.estimatedCostUsd.toFixed(4))}\n`;
682
725
  }
683
726
  }
684
727
  }
685
728
  else {
686
- response += 'No detailed usage statistics available.';
729
+ response += 'No detailed usage statistics available\\.';
687
730
  }
688
- await ctx.reply(response, { parse_mode: 'Markdown' });
731
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
689
732
  history.close();
690
733
  }
691
734
  catch (error) {
@@ -702,7 +745,7 @@ How can I assist you today?`;
702
745
  let response = await this.oracle.chat(prompt);
703
746
  if (response) {
704
747
  try {
705
- await ctx.reply(response, { parse_mode: 'Markdown' });
748
+ await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
706
749
  }
707
750
  catch {
708
751
  await ctx.reply(response);
@@ -717,57 +760,57 @@ How can I assist you today?`;
717
760
  ${this.HELP_MESSAGE}
718
761
 
719
762
  How can I assist you today?`;
720
- await ctx.reply(helpMessage, { parse_mode: 'Markdown' });
763
+ await ctx.reply(helpMessage, { parse_mode: 'MarkdownV2' });
721
764
  }
722
765
  async handleZaionCommand(ctx, user) {
723
766
  const config = this.config.get();
724
767
  let response = '*System Configuration*\n\n';
725
768
  response += `*Agent:*\n`;
726
- response += `- Name: ${config.agent.name}\n`;
727
- response += `- Personality: ${config.agent.personality}\n\n`;
728
- response += `*Oracle (LLM):*\n`;
729
- response += `- Provider: ${config.llm.provider}\n`;
730
- response += `- Model: ${config.llm.model}\n`;
731
- response += `- Temperature: ${config.llm.temperature}\n`;
732
- response += `- Context Window: ${config.llm.context_window || 100}\n\n`;
769
+ response += `\\- Name: ${escMd(config.agent.name)}\n`;
770
+ response += `\\- Personality: ${escMd(config.agent.personality)}\n\n`;
771
+ response += `*Oracle \\(LLM\\):*\n`;
772
+ response += `\\- Provider: ${escMd(config.llm.provider)}\n`;
773
+ response += `\\- Model: ${escMd(config.llm.model)}\n`;
774
+ response += `\\- Temperature: ${escMd(config.llm.temperature)}\n`;
775
+ response += `\\- Context Window: ${escMd(config.llm.context_window || 100)}\n\n`;
733
776
  // Sati config (falls back to llm if not set)
734
777
  const sati = config.sati;
735
- response += `*Sati (Memory):*\n`;
778
+ response += `*Sati \\(Memory\\):*\n`;
736
779
  if (sati?.provider) {
737
- response += `- Provider: ${sati.provider}\n`;
738
- response += `- Model: ${sati.model || config.llm.model}\n`;
739
- response += `- Temperature: ${sati.temperature ?? config.llm.temperature}\n`;
740
- response += `- Memory Limit: ${sati.memory_limit ?? 1000}\n`;
780
+ response += `\\- Provider: ${escMd(sati.provider)}\n`;
781
+ response += `\\- Model: ${escMd(sati.model || config.llm.model)}\n`;
782
+ response += `\\- Temperature: ${escMd(sati.temperature ?? config.llm.temperature)}\n`;
783
+ response += `\\- Memory Limit: ${escMd(sati.memory_limit ?? 1000)}\n`;
741
784
  }
742
785
  else {
743
- response += `- Inherits Oracle config\n`;
786
+ response += `\\- Inherits Oracle config\n`;
744
787
  }
745
788
  response += '\n';
746
789
  // Apoc config (falls back to llm if not set)
747
790
  const apoc = config.apoc;
748
- response += `*Apoc (DevTools):*\n`;
791
+ response += `*Apoc \\(DevTools\\):*\n`;
749
792
  if (apoc?.provider) {
750
- response += `- Provider: ${apoc.provider}\n`;
751
- response += `- Model: ${apoc.model || config.llm.model}\n`;
752
- response += `- Temperature: ${apoc.temperature ?? 0.2}\n`;
793
+ response += `\\- Provider: ${escMd(apoc.provider)}\n`;
794
+ response += `\\- Model: ${escMd(apoc.model || config.llm.model)}\n`;
795
+ response += `\\- Temperature: ${escMd(apoc.temperature ?? 0.2)}\n`;
753
796
  if (apoc.working_dir)
754
- response += `- Working Dir: ${apoc.working_dir}\n`;
755
- response += `- Timeout: ${apoc.timeout_ms ?? 30000}ms\n`;
797
+ response += `\\- Working Dir: ${escMd(apoc.working_dir)}\n`;
798
+ response += `\\- Timeout: ${escMd(apoc.timeout_ms ?? 30000)}ms\n`;
756
799
  }
757
800
  else {
758
- response += `- Inherits Oracle config\n`;
801
+ response += `\\- Inherits Oracle config\n`;
759
802
  }
760
803
  response += '\n';
761
804
  response += `*Channels:*\n`;
762
- response += `- Telegram Enabled: ${config.channels.telegram.enabled}\n`;
763
- response += `- Discord Enabled: ${config.channels.discord.enabled}\n\n`;
805
+ response += `\\- Telegram Enabled: ${escMd(config.channels.telegram.enabled)}\n`;
806
+ response += `\\- Discord Enabled: ${escMd(config.channels.discord.enabled)}\n\n`;
764
807
  response += `*UI:*\n`;
765
- response += `- Enabled: ${config.ui.enabled}\n`;
766
- response += `- Port: ${config.ui.port}\n\n`;
808
+ response += `\\- Enabled: ${escMd(config.ui.enabled)}\n`;
809
+ response += `\\- Port: ${escMd(config.ui.port)}\n\n`;
767
810
  response += `*Audio:*\n`;
768
- response += `- Enabled: ${config.audio.enabled}\n`;
769
- response += `- Max Duration: ${config.audio.maxDurationSeconds}s\n`;
770
- await ctx.reply(response, { parse_mode: 'Markdown' });
811
+ response += `\\- Enabled: ${escMd(config.audio.enabled)}\n`;
812
+ response += `\\- Max Duration: ${escMd(config.audio.maxDurationSeconds)}s\n`;
813
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
771
814
  }
772
815
  async handleSatiCommand(ctx, user, args) {
773
816
  let limit = null;
@@ -791,13 +834,16 @@ How can I assist you today?`;
791
834
  if (limit !== null) {
792
835
  selectedMemories = memories.slice(0, Math.min(limit, memories.length));
793
836
  }
794
- let response = `*${selectedMemories.length} SATI Memories${limit !== null ? ` (Showing first ${selectedMemories.length})` : ''}:*\n\n`;
837
+ const countLabel = limit !== null
838
+ ? `${escMd(selectedMemories.length)} SATI Memories \\(Showing first ${escMd(selectedMemories.length)}\\)`
839
+ : `${escMd(selectedMemories.length)} SATI Memories`;
840
+ let response = `*${countLabel}:*\n\n`;
795
841
  for (const memory of selectedMemories) {
796
842
  // Limitar o tamanho do resumo para evitar mensagens muito longas
797
843
  const truncatedSummary = memory.summary.length > 200 ? memory.summary.substring(0, 200) + '...' : memory.summary;
798
- response += `*${memory.category} (${memory.importance}):* ${truncatedSummary}\n\n`;
844
+ response += `*${escMd(memory.category)} \\(${escMd(memory.importance)}\\):* ${escMd(truncatedSummary)}\n\n`;
799
845
  }
800
- await ctx.reply(response, { parse_mode: 'Markdown' });
846
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
801
847
  }
802
848
  catch (error) {
803
849
  await ctx.reply(`Failed to retrieve memories: ${error.message}`);
@@ -891,11 +937,11 @@ How can I assist you today?`;
891
937
  Construtor.probe(),
892
938
  ]);
893
939
  if (servers.length === 0) {
894
- await ctx.reply('*No MCP Servers Configured*\n\nThere are currently no MCP servers configured in the system.', { parse_mode: 'Markdown' });
940
+ await ctx.reply('*No MCP Servers Configured*\n\nThere are currently no MCP servers configured in the system.', { parse_mode: 'MarkdownV2' });
895
941
  return;
896
942
  }
897
943
  const probeMap = new Map(probeResults.map(r => [r.name, r]));
898
- let response = `*MCP Servers (${servers.length})*\n\n`;
944
+ let response = `*MCP Servers \\(${escMd(servers.length)}\\)*\n\n`;
899
945
  const keyboard = [];
900
946
  servers.forEach((server, index) => {
901
947
  const enabledStatus = server.enabled ? '✅ Enabled' : '❌ Disabled';
@@ -903,25 +949,25 @@ How can I assist you today?`;
903
949
  const probe = probeMap.get(server.name);
904
950
  const connectionStatus = probe
905
951
  ? probe.ok
906
- ? `🟢 Connected (${probe.toolCount} tools)`
952
+ ? `🟢 Connected \\(${escMd(probe.toolCount)} tools\\)`
907
953
  : `🔴 Failed`
908
954
  : '⚪ Unknown';
909
- response += `*${index + 1}. ${server.name}*\n`;
955
+ response += `*${escMd(index + 1)}\\. ${escMd(server.name)}*\n`;
910
956
  response += `Status: ${enabledStatus}\n`;
911
957
  response += `Connection: ${connectionStatus}\n`;
912
- response += `Transport: ${transport}\n`;
958
+ response += `Transport: ${escMd(transport)}\n`;
913
959
  if (server.config.transport === 'stdio') {
914
- response += `Command: \`${server.config.command}\`\n`;
960
+ response += `Command: \`${escMd(server.config.command)}\`\n`;
915
961
  if (server.config.args && server.config.args.length > 0) {
916
- response += `Args: \`${server.config.args.join(' ')}\`\n`;
962
+ response += `Args: \`${escMd(server.config.args.join(' '))}\`\n`;
917
963
  }
918
964
  }
919
965
  else if (server.config.transport === 'http') {
920
- response += `URL: \`${server.config.url}\`\n`;
966
+ response += `URL: \`${escMd(server.config.url)}\`\n`;
921
967
  }
922
968
  if (probe && !probe.ok && probe.error) {
923
969
  const shortError = probe.error.length > 80 ? probe.error.slice(0, 80) + '…' : probe.error;
924
- response += `Error: \`${shortError}\`\n`;
970
+ response += `Error: \`${escMd(shortError)}\`\n`;
925
971
  }
926
972
  response += '\n';
927
973
  if (server.enabled) {
@@ -932,13 +978,13 @@ How can I assist you today?`;
932
978
  }
933
979
  });
934
980
  await ctx.reply(response, {
935
- parse_mode: 'Markdown',
981
+ parse_mode: 'MarkdownV2',
936
982
  reply_markup: { inline_keyboard: keyboard },
937
983
  });
938
984
  }
939
985
  catch (error) {
940
986
  this.display.log('Error listing MCP servers: ' + (error instanceof Error ? error.message : String(error)), { source: 'Telegram', level: 'error' });
941
- await ctx.reply('An error occurred while retrieving the list of MCP servers. Please check the logs for more details.', { parse_mode: 'Markdown' });
987
+ await ctx.reply('An error occurred while retrieving the list of MCP servers. Please check the logs for more details.', { parse_mode: 'MarkdownV2' });
942
988
  }
943
989
  }
944
990
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morpheus-cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
5
5
  "bin": {
6
6
  "morpheus": "./bin/morpheus.js"
@@ -47,14 +47,15 @@
47
47
  "fs-extra": "^11.3.3",
48
48
  "js-yaml": "^4.1.1",
49
49
  "langchain": "^1.2.16",
50
+ "mcp-remote": "^0.1.38",
50
51
  "open": "^11.0.0",
51
52
  "ora": "^9.1.0",
52
53
  "sqlite-vec": "^0.1.7-alpha.2",
53
54
  "telegraf": "^4.16.3",
55
+ "telegram-markdown-v2": "^0.0.4",
54
56
  "winston": "^3.19.0",
55
57
  "winston-daily-rotate-file": "^5.0.0",
56
- "zod": "^4.3.6",
57
- "mcp-remote": "^0.1.38"
58
+ "zod": "^4.3.6"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@types/body-parser": "^1.19.6",