morpheus-cli 0.4.9 → 0.4.11

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,18 @@ 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
+ */
23
+ function toMd(text) {
24
+ const MAX = 4096;
25
+ const converted = convert(text, 'escape');
26
+ const safe = converted.length > MAX ? converted.slice(0, MAX - 3) + '\\.\\.\\.' : converted;
27
+ return { text: safe, parse_mode: 'MarkdownV2' };
28
+ }
16
29
  export class TelegramAdapter {
17
30
  bot = null;
18
31
  isConnected = false;
@@ -87,7 +100,12 @@ export class TelegramAdapter {
87
100
  // Process with Agent
88
101
  const response = await this.oracle.chat(text);
89
102
  if (response) {
90
- await ctx.reply(response);
103
+ try {
104
+ await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
105
+ }
106
+ catch {
107
+ await ctx.reply(response);
108
+ }
91
109
  this.display.log(`Responded to @${user}: ${response}`, { source: 'Telegram' });
92
110
  }
93
111
  }
@@ -154,7 +172,7 @@ export class TelegramAdapter {
154
172
  // The prompt says "reply with the answer".
155
173
  // "Transcribe them... and process the resulting text as a standard user prompt."
156
174
  // So I should treat 'text' as if it was a text message.
157
- await ctx.reply(`🎤 *Transcription*: _"${text}"_`, { parse_mode: 'Markdown' });
175
+ await ctx.reply(`🎤 Transcription: "${text}"`);
158
176
  await ctx.sendChatAction('typing');
159
177
  // Process with Agent
160
178
  const response = await this.oracle.chat(text, usage, true);
@@ -166,7 +184,12 @@ export class TelegramAdapter {
166
184
  // }
167
185
  // }
168
186
  if (response) {
169
- await ctx.reply(response);
187
+ try {
188
+ await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
189
+ }
190
+ catch {
191
+ await ctx.reply(response);
192
+ }
170
193
  this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
171
194
  }
172
195
  }
@@ -227,7 +250,7 @@ export class TelegramAdapter {
227
250
  const sessionId = data.replace('ask_archive_session_', '');
228
251
  // Fetch session title for better UX (optional, but nice) - for now just use ID
229
252
  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',
253
+ parse_mode: 'MarkdownV2',
231
254
  reply_markup: {
232
255
  inline_keyboard: [
233
256
  [
@@ -249,7 +272,7 @@ export class TelegramAdapter {
249
272
  if (ctx.updateType === 'callback_query') {
250
273
  ctx.deleteMessage().catch(() => { });
251
274
  }
252
- await ctx.reply(`✅ Session \`${sessionId}\` has been archived and moved to long-term memory.`, { parse_mode: 'Markdown' });
275
+ await ctx.reply(`✅ Session \`${sessionId}\` has been archived and moved to long-term memory.`, { parse_mode: 'MarkdownV2' });
253
276
  }
254
277
  catch (error) {
255
278
  await ctx.answerCbQuery(`Error archiving: ${error.message}`, { show_alert: true });
@@ -260,7 +283,7 @@ export class TelegramAdapter {
260
283
  const data = ctx.callbackQuery.data;
261
284
  const sessionId = data.replace('ask_delete_session_', '');
262
285
  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',
286
+ parse_mode: 'MarkdownV2',
264
287
  reply_markup: {
265
288
  inline_keyboard: [
266
289
  [
@@ -282,7 +305,7 @@ export class TelegramAdapter {
282
305
  if (ctx.updateType === 'callback_query') {
283
306
  ctx.deleteMessage().catch(() => { });
284
307
  }
285
- await ctx.reply(`🗑️ Session \`${sessionId}\` has been permanently deleted.`, { parse_mode: 'Markdown' });
308
+ await ctx.reply(`🗑️ Session \`${sessionId}\` has been permanently deleted.`, { parse_mode: 'MarkdownV2' });
286
309
  }
287
310
  catch (error) {
288
311
  await ctx.answerCbQuery(`Error deleting: ${error.message}`, { show_alert: true });
@@ -385,7 +408,7 @@ export class TelegramAdapter {
385
408
  for (const userId of allowedUsers) {
386
409
  try {
387
410
  // Send as plain text — LLM output often has unbalanced markdown that
388
- // causes "Can't find end of entity" errors with parse_mode: 'Markdown'.
411
+ // causes "Can't find end of entity" errors with parse_mode: 'MarkdownV2'.
389
412
  await this.bot.telegram.sendMessage(userId, safeText);
390
413
  }
391
414
  catch (err) {
@@ -464,7 +487,7 @@ export class TelegramAdapter {
464
487
  async handleNewSessionCommand(ctx, user) {
465
488
  try {
466
489
  await ctx.reply("Are you ready to start a new session? Please confirm.", {
467
- parse_mode: 'Markdown', reply_markup: {
490
+ parse_mode: 'MarkdownV2', reply_markup: {
468
491
  inline_keyboard: [
469
492
  [{ text: 'Yes, start new session', callback_data: 'confirm_new_session' }, { text: 'No, cancel', callback_data: 'cancel_new_session' }]
470
493
  ]
@@ -490,7 +513,7 @@ export class TelegramAdapter {
490
513
  const history = new SQLiteChatMessageHistory({ sessionId: "" });
491
514
  const sessions = await history.listSessions();
492
515
  if (sessions.length === 0) {
493
- await ctx.reply('No active or paused sessions found.', { parse_mode: 'Markdown' });
516
+ await ctx.reply('No active or paused sessions found.', { parse_mode: 'MarkdownV2' });
494
517
  return;
495
518
  }
496
519
  let response = '*Sessions:*\n\n';
@@ -521,7 +544,7 @@ export class TelegramAdapter {
521
544
  keyboard.push(sessionButtons);
522
545
  }
523
546
  await ctx.reply(response, {
524
- parse_mode: 'Markdown',
547
+ parse_mode: 'MarkdownV2',
525
548
  reply_markup: {
526
549
  inline_keyboard: keyboard
527
550
  }
@@ -642,7 +665,7 @@ How can I assist you today?`;
642
665
  else {
643
666
  response += '⚠️ Configuration: Missing\n';
644
667
  }
645
- await ctx.reply(response, { parse_mode: 'Markdown' });
668
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
646
669
  }
647
670
  async handleStatsCommand(ctx, user) {
648
671
  try {
@@ -685,7 +708,7 @@ How can I assist you today?`;
685
708
  else {
686
709
  response += 'No detailed usage statistics available.';
687
710
  }
688
- await ctx.reply(response, { parse_mode: 'Markdown' });
711
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
689
712
  history.close();
690
713
  }
691
714
  catch (error) {
@@ -702,7 +725,7 @@ How can I assist you today?`;
702
725
  let response = await this.oracle.chat(prompt);
703
726
  if (response) {
704
727
  try {
705
- await ctx.reply(response, { parse_mode: 'Markdown' });
728
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
706
729
  }
707
730
  catch {
708
731
  await ctx.reply(response);
@@ -717,7 +740,7 @@ How can I assist you today?`;
717
740
  ${this.HELP_MESSAGE}
718
741
 
719
742
  How can I assist you today?`;
720
- await ctx.reply(helpMessage, { parse_mode: 'Markdown' });
743
+ await ctx.reply(helpMessage, { parse_mode: 'MarkdownV2' });
721
744
  }
722
745
  async handleZaionCommand(ctx, user) {
723
746
  const config = this.config.get();
@@ -767,7 +790,7 @@ How can I assist you today?`;
767
790
  response += `*Audio:*\n`;
768
791
  response += `- Enabled: ${config.audio.enabled}\n`;
769
792
  response += `- Max Duration: ${config.audio.maxDurationSeconds}s\n`;
770
- await ctx.reply(response, { parse_mode: 'Markdown' });
793
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
771
794
  }
772
795
  async handleSatiCommand(ctx, user, args) {
773
796
  let limit = null;
@@ -797,7 +820,7 @@ How can I assist you today?`;
797
820
  const truncatedSummary = memory.summary.length > 200 ? memory.summary.substring(0, 200) + '...' : memory.summary;
798
821
  response += `*${memory.category} (${memory.importance}):* ${truncatedSummary}\n\n`;
799
822
  }
800
- await ctx.reply(response, { parse_mode: 'Markdown' });
823
+ await ctx.reply(response, { parse_mode: 'MarkdownV2' });
801
824
  }
802
825
  catch (error) {
803
826
  await ctx.reply(`Failed to retrieve memories: ${error.message}`);
@@ -891,7 +914,7 @@ How can I assist you today?`;
891
914
  Construtor.probe(),
892
915
  ]);
893
916
  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' });
917
+ await ctx.reply('*No MCP Servers Configured*\n\nThere are currently no MCP servers configured in the system.', { parse_mode: 'MarkdownV2' });
895
918
  return;
896
919
  }
897
920
  const probeMap = new Map(probeResults.map(r => [r.name, r]));
@@ -932,13 +955,13 @@ How can I assist you today?`;
932
955
  }
933
956
  });
934
957
  await ctx.reply(response, {
935
- parse_mode: 'Markdown',
958
+ parse_mode: 'MarkdownV2',
936
959
  reply_markup: { inline_keyboard: keyboard },
937
960
  });
938
961
  }
939
962
  catch (error) {
940
963
  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' });
964
+ await ctx.reply('An error occurred while retrieving the list of MCP servers. Please check the logs for more details.', { parse_mode: 'MarkdownV2' });
942
965
  }
943
966
  }
944
967
  }
@@ -4,6 +4,7 @@ import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
6
  import { buildDevKit } from "../devkit/index.js";
7
+ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
7
8
  /**
8
9
  * Apoc is a subagent of Oracle specialized in devtools operations.
9
10
  * It receives delegated tasks from Oracle and executes them using DevKit tools
@@ -15,12 +16,20 @@ import { buildDevKit } from "../devkit/index.js";
15
16
  */
16
17
  export class Apoc {
17
18
  static instance = null;
19
+ static currentSessionId = undefined;
18
20
  agent;
19
21
  config;
20
22
  display = DisplayManager.getInstance();
21
23
  constructor(config) {
22
24
  this.config = config || ConfigManager.getInstance().get();
23
25
  }
26
+ /**
27
+ * Called by Oracle before each chat() so Apoc knows which session to
28
+ * attribute its token usage to.
29
+ */
30
+ static setSessionId(sessionId) {
31
+ Apoc.currentSessionId = sessionId;
32
+ }
24
33
  static getInstance(config) {
25
34
  if (!Apoc.instance) {
26
35
  Apoc.instance = new Apoc(config);
@@ -54,8 +63,9 @@ export class Apoc {
54
63
  * Execute a devtools task delegated by Oracle.
55
64
  * @param task Natural language task description
56
65
  * @param context Optional additional context from the ongoing conversation
66
+ * @param sessionId Session to attribute token usage to (defaults to 'apoc')
57
67
  */
58
- async execute(task, context) {
68
+ async execute(task, context, sessionId) {
59
69
  if (!this.agent) {
60
70
  await this.initialize();
61
71
  }
@@ -90,6 +100,23 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
90
100
  const messages = [systemMessage, userMessage];
91
101
  try {
92
102
  const response = await this.agent.invoke({ messages });
103
+ // Persist Apoc-generated messages so token usage is tracked in short-memory.db.
104
+ // Use the caller's session when provided, then the static session set by Oracle,
105
+ // otherwise fall back to 'apoc'.
106
+ const apocConfig = this.config.apoc || this.config.llm;
107
+ const newMessages = response.messages.slice(messages.length);
108
+ if (newMessages.length > 0) {
109
+ const targetSession = sessionId ?? Apoc.currentSessionId ?? 'apoc';
110
+ const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
111
+ for (const msg of newMessages) {
112
+ msg.provider_metadata = {
113
+ provider: apocConfig.provider,
114
+ model: apocConfig.model,
115
+ };
116
+ }
117
+ await history.addMessages(newMessages);
118
+ history.close();
119
+ }
93
120
  const lastMessage = response.messages[response.messages.length - 1];
94
121
  const content = typeof lastMessage.content === "string"
95
122
  ? lastMessage.content
@@ -6,6 +6,7 @@ import { ProviderError } from "./errors.js";
6
6
  import { DisplayManager } from "./display.js";
7
7
  import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
8
8
  import { SatiMemoryMiddleware } from "./memory/sati/index.js";
9
+ import { Apoc } from "./apoc.js";
9
10
  export class Oracle {
10
11
  provider;
11
12
  config;
@@ -208,6 +209,11 @@ You maintain intent until resolution.
208
209
  }
209
210
  messages.push(...previousMessages);
210
211
  messages.push(userMessage);
212
+ // Propagate current session to Apoc so its token usage lands in the right session
213
+ const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
214
+ ? this.history.currentSessionId
215
+ : undefined;
216
+ Apoc.setSessionId(currentSessionId);
211
217
  const response = await this.provider.invoke({ messages });
212
218
  // Identify new messages generated during the interaction
213
219
  // The `messages` array passed to invoke had length `messages.length`
@@ -229,9 +235,6 @@ You maintain intent until resolution.
229
235
  const lastMessage = response.messages[response.messages.length - 1];
230
236
  const responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
231
237
  // Sati Middleware: Evaluation (Fire and forget)
232
- const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
233
- ? this.history.currentSessionId
234
- : undefined;
235
238
  this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
236
239
  .catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
237
240
  return responseContent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morpheus-cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
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",