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.
- package/dist/channels/telegram.js +43 -20
- package/dist/runtime/apoc.js +28 -1
- package/dist/runtime/oracle.js +6 -3
- package/package.json +4 -3
|
@@ -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
|
-
|
|
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(`🎤
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
}
|
package/dist/runtime/apoc.js
CHANGED
|
@@ -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
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -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.
|
|
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",
|