morpheus-cli 0.4.11 → 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.
@@ -19,6 +19,7 @@ import { Construtor } from '../runtime/tools/factory.js';
19
19
  * Unsupported tags (e.g. tables) have their special chars escaped so they
20
20
  * render as plain text instead of breaking the parse.
21
21
  * Truncates to Telegram's 4096-char hard limit.
22
+ * Use for dynamic LLM/Oracle output.
22
23
  */
23
24
  function toMd(text) {
24
25
  const MAX = 4096;
@@ -26,6 +27,20 @@ function toMd(text) {
26
27
  const safe = converted.length > MAX ? converted.slice(0, MAX - 3) + '\\.\\.\\.' : converted;
27
28
  return { text: safe, parse_mode: 'MarkdownV2' };
28
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
+ }
29
44
  export class TelegramAdapter {
30
45
  bot = null;
31
46
  isConnected = false;
@@ -46,18 +61,18 @@ export class TelegramAdapter {
46
61
  this.rateLimiter.set(userId, now);
47
62
  return false;
48
63
  }
49
- HELP_MESSAGE = `/start - Show this welcome message and available commands
50
- /status - Check the status of the Morpheus agent
51
- /doctor - Diagnose environment and configuration issues
52
- /stats - Show token usage statistics
53
- /help - Show available commands
54
- /zaion - Show system configurations
55
- /sati <qnt> - Show specific memories
56
- /newsession - Archive current session and start fresh
57
- /sessions - List all sessions with titles and switch between them
58
- /restart - Restart the Morpheus agent
59
- /mcpreload - Reload MCP servers without restarting
60
- /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`;
61
76
  constructor(oracle) {
62
77
  this.oracle = oracle;
63
78
  }
@@ -402,17 +417,22 @@ export class TelegramAdapter {
402
417
  this.display.log('No allowed Telegram users configured — skipping notification.', { source: 'Telegram', level: 'warning' });
403
418
  return;
404
419
  }
405
- // Truncate to Telegram's 4096 char limit
406
- const MAX_LEN = 4096;
407
- 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);
408
422
  for (const userId of allowedUsers) {
409
423
  try {
410
- // Send as plain text — LLM output often has unbalanced markdown that
411
- // causes "Can't find end of entity" errors with parse_mode: 'MarkdownV2'.
412
- await this.bot.telegram.sendMessage(userId, safeText);
424
+ await this.bot.telegram.sendMessage(userId, mdText, { parse_mode });
413
425
  }
414
- catch (err) {
415
- 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
+ }
416
436
  }
417
437
  }
418
438
  }
@@ -521,10 +541,10 @@ export class TelegramAdapter {
521
541
  for (const session of sessions) {
522
542
  const title = session.title || 'Untitled Session';
523
543
  const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
524
- response += `${statusEmoji} *${title}*\n`;
525
- response += `- ID: ${session.id}\n`;
526
- response += `- Status: ${session.status}\n`;
527
- 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`;
528
548
  // Adicionar botão inline para alternar para esta sessão
529
549
  const sessionButtons = [];
530
550
  if (session.status !== 'active') {
@@ -682,31 +702,31 @@ How can I assist you today?`;
682
702
  const totalAudioSeconds = groupedStats.reduce((sum, s) => sum + (s.totalAudioSeconds || 0), 0);
683
703
  const totalCost = stats.totalEstimatedCostUsd;
684
704
  let response = '*Token Usage Statistics*\n\n';
685
- response += `Input Tokens: ${stats.totalInputTokens.toLocaleString()}\n`;
686
- response += `Output Tokens: ${stats.totalOutputTokens.toLocaleString()}\n`;
687
- 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`;
688
708
  if (totalAudioSeconds > 0) {
689
- response += `Audio Processed: ${totalAudioSeconds.toFixed(1)}s\n`;
709
+ response += `Audio Processed: ${escMd(totalAudioSeconds.toFixed(1))}s\n`;
690
710
  }
691
711
  if (totalCost != null) {
692
- response += `Estimated Cost: $${totalCost.toFixed(4)}\n`;
712
+ response += `Estimated Cost: \\$${escMd(totalCost.toFixed(4))}\n`;
693
713
  }
694
714
  response += '\n';
695
715
  if (groupedStats.length > 0) {
696
716
  response += '*By Provider/Model:*\n';
697
717
  for (const stat of groupedStats) {
698
- response += `\n*${stat.provider}/${stat.model}*\n`;
699
- 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`;
700
720
  if (stat.totalAudioSeconds > 0) {
701
- response += ` Audio: ${stat.totalAudioSeconds.toFixed(1)}s\n`;
721
+ response += ` Audio: ${escMd(stat.totalAudioSeconds.toFixed(1))}s\n`;
702
722
  }
703
723
  if (stat.estimatedCostUsd != null) {
704
- response += ` Cost: $${stat.estimatedCostUsd.toFixed(4)}\n`;
724
+ response += ` Cost: \\$${escMd(stat.estimatedCostUsd.toFixed(4))}\n`;
705
725
  }
706
726
  }
707
727
  }
708
728
  else {
709
- response += 'No detailed usage statistics available.';
729
+ response += 'No detailed usage statistics available\\.';
710
730
  }
711
731
  await ctx.reply(response, { parse_mode: 'MarkdownV2' });
712
732
  history.close();
@@ -725,7 +745,7 @@ How can I assist you today?`;
725
745
  let response = await this.oracle.chat(prompt);
726
746
  if (response) {
727
747
  try {
728
- await ctx.reply(response, { parse_mode: 'MarkdownV2' });
748
+ await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
729
749
  }
730
750
  catch {
731
751
  await ctx.reply(response);
@@ -746,50 +766,50 @@ How can I assist you today?`;
746
766
  const config = this.config.get();
747
767
  let response = '*System Configuration*\n\n';
748
768
  response += `*Agent:*\n`;
749
- response += `- Name: ${config.agent.name}\n`;
750
- response += `- Personality: ${config.agent.personality}\n\n`;
751
- response += `*Oracle (LLM):*\n`;
752
- response += `- Provider: ${config.llm.provider}\n`;
753
- response += `- Model: ${config.llm.model}\n`;
754
- response += `- Temperature: ${config.llm.temperature}\n`;
755
- 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`;
756
776
  // Sati config (falls back to llm if not set)
757
777
  const sati = config.sati;
758
- response += `*Sati (Memory):*\n`;
778
+ response += `*Sati \\(Memory\\):*\n`;
759
779
  if (sati?.provider) {
760
- response += `- Provider: ${sati.provider}\n`;
761
- response += `- Model: ${sati.model || config.llm.model}\n`;
762
- response += `- Temperature: ${sati.temperature ?? config.llm.temperature}\n`;
763
- 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`;
764
784
  }
765
785
  else {
766
- response += `- Inherits Oracle config\n`;
786
+ response += `\\- Inherits Oracle config\n`;
767
787
  }
768
788
  response += '\n';
769
789
  // Apoc config (falls back to llm if not set)
770
790
  const apoc = config.apoc;
771
- response += `*Apoc (DevTools):*\n`;
791
+ response += `*Apoc \\(DevTools\\):*\n`;
772
792
  if (apoc?.provider) {
773
- response += `- Provider: ${apoc.provider}\n`;
774
- response += `- Model: ${apoc.model || config.llm.model}\n`;
775
- 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`;
776
796
  if (apoc.working_dir)
777
- response += `- Working Dir: ${apoc.working_dir}\n`;
778
- 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`;
779
799
  }
780
800
  else {
781
- response += `- Inherits Oracle config\n`;
801
+ response += `\\- Inherits Oracle config\n`;
782
802
  }
783
803
  response += '\n';
784
804
  response += `*Channels:*\n`;
785
- response += `- Telegram Enabled: ${config.channels.telegram.enabled}\n`;
786
- 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`;
787
807
  response += `*UI:*\n`;
788
- response += `- Enabled: ${config.ui.enabled}\n`;
789
- response += `- Port: ${config.ui.port}\n\n`;
808
+ response += `\\- Enabled: ${escMd(config.ui.enabled)}\n`;
809
+ response += `\\- Port: ${escMd(config.ui.port)}\n\n`;
790
810
  response += `*Audio:*\n`;
791
- response += `- Enabled: ${config.audio.enabled}\n`;
792
- response += `- Max Duration: ${config.audio.maxDurationSeconds}s\n`;
811
+ response += `\\- Enabled: ${escMd(config.audio.enabled)}\n`;
812
+ response += `\\- Max Duration: ${escMd(config.audio.maxDurationSeconds)}s\n`;
793
813
  await ctx.reply(response, { parse_mode: 'MarkdownV2' });
794
814
  }
795
815
  async handleSatiCommand(ctx, user, args) {
@@ -814,11 +834,14 @@ How can I assist you today?`;
814
834
  if (limit !== null) {
815
835
  selectedMemories = memories.slice(0, Math.min(limit, memories.length));
816
836
  }
817
- 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`;
818
841
  for (const memory of selectedMemories) {
819
842
  // Limitar o tamanho do resumo para evitar mensagens muito longas
820
843
  const truncatedSummary = memory.summary.length > 200 ? memory.summary.substring(0, 200) + '...' : memory.summary;
821
- response += `*${memory.category} (${memory.importance}):* ${truncatedSummary}\n\n`;
844
+ response += `*${escMd(memory.category)} \\(${escMd(memory.importance)}\\):* ${escMd(truncatedSummary)}\n\n`;
822
845
  }
823
846
  await ctx.reply(response, { parse_mode: 'MarkdownV2' });
824
847
  }
@@ -918,7 +941,7 @@ How can I assist you today?`;
918
941
  return;
919
942
  }
920
943
  const probeMap = new Map(probeResults.map(r => [r.name, r]));
921
- let response = `*MCP Servers (${servers.length})*\n\n`;
944
+ let response = `*MCP Servers \\(${escMd(servers.length)}\\)*\n\n`;
922
945
  const keyboard = [];
923
946
  servers.forEach((server, index) => {
924
947
  const enabledStatus = server.enabled ? '✅ Enabled' : '❌ Disabled';
@@ -926,25 +949,25 @@ How can I assist you today?`;
926
949
  const probe = probeMap.get(server.name);
927
950
  const connectionStatus = probe
928
951
  ? probe.ok
929
- ? `🟢 Connected (${probe.toolCount} tools)`
952
+ ? `🟢 Connected \\(${escMd(probe.toolCount)} tools\\)`
930
953
  : `🔴 Failed`
931
954
  : '⚪ Unknown';
932
- response += `*${index + 1}. ${server.name}*\n`;
955
+ response += `*${escMd(index + 1)}\\. ${escMd(server.name)}*\n`;
933
956
  response += `Status: ${enabledStatus}\n`;
934
957
  response += `Connection: ${connectionStatus}\n`;
935
- response += `Transport: ${transport}\n`;
958
+ response += `Transport: ${escMd(transport)}\n`;
936
959
  if (server.config.transport === 'stdio') {
937
- response += `Command: \`${server.config.command}\`\n`;
960
+ response += `Command: \`${escMd(server.config.command)}\`\n`;
938
961
  if (server.config.args && server.config.args.length > 0) {
939
- response += `Args: \`${server.config.args.join(' ')}\`\n`;
962
+ response += `Args: \`${escMd(server.config.args.join(' '))}\`\n`;
940
963
  }
941
964
  }
942
965
  else if (server.config.transport === 'http') {
943
- response += `URL: \`${server.config.url}\`\n`;
966
+ response += `URL: \`${escMd(server.config.url)}\`\n`;
944
967
  }
945
968
  if (probe && !probe.ok && probe.error) {
946
969
  const shortError = probe.error.length > 80 ? probe.error.slice(0, 80) + '…' : probe.error;
947
- response += `Error: \`${shortError}\`\n`;
970
+ response += `Error: \`${escMd(shortError)}\`\n`;
948
971
  }
949
972
  response += '\n';
950
973
  if (server.enabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morpheus-cli",
3
- "version": "0.4.11",
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"