morpheus-cli 0.4.14 → 0.5.0
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 +275 -1116
- package/dist/channels/telegram.js +210 -73
- package/dist/cli/commands/doctor.js +34 -0
- package/dist/cli/commands/init.js +128 -0
- package/dist/cli/commands/restart.js +17 -0
- package/dist/cli/commands/start.js +15 -0
- package/dist/config/manager.js +51 -0
- package/dist/config/schemas.js +7 -0
- package/dist/devkit/tools/network.js +1 -1
- package/dist/http/api.js +177 -10
- package/dist/runtime/apoc.js +139 -32
- package/dist/runtime/memory/sati/repository.js +30 -2
- package/dist/runtime/memory/sati/service.js +46 -15
- package/dist/runtime/memory/sati/system-prompts.js +71 -29
- package/dist/runtime/memory/sqlite.js +24 -0
- package/dist/runtime/neo.js +134 -0
- package/dist/runtime/oracle.js +244 -133
- package/dist/runtime/providers/factory.js +1 -12
- package/dist/runtime/tasks/context.js +53 -0
- package/dist/runtime/tasks/dispatcher.js +70 -0
- package/dist/runtime/tasks/notifier.js +68 -0
- package/dist/runtime/tasks/repository.js +370 -0
- package/dist/runtime/tasks/types.js +1 -0
- package/dist/runtime/tasks/worker.js +96 -0
- package/dist/runtime/tools/apoc-tool.js +61 -8
- package/dist/runtime/tools/delegation-guard.js +29 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/runtime/tools/neo-tool.js +99 -0
- package/dist/runtime/tools/task-query-tool.js +76 -0
- package/dist/runtime/webhooks/dispatcher.js +10 -19
- package/dist/types/config.js +10 -0
- package/dist/ui/assets/index-20lLB1sM.js +112 -0
- package/dist/ui/assets/index-BJ56bRfs.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-LemKVRjC.js +0 -112
- package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
|
@@ -13,30 +13,101 @@ import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
|
|
|
13
13
|
import { SatiRepository } from '../runtime/memory/sati/repository.js';
|
|
14
14
|
import { MCPManager } from '../config/mcp-manager.js';
|
|
15
15
|
import { Construtor } from '../runtime/tools/factory.js';
|
|
16
|
+
function escapeHtml(text) {
|
|
17
|
+
return text
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>');
|
|
21
|
+
}
|
|
22
|
+
/** Strips HTML tags and unescapes entities for plain-text Telegram fallback. */
|
|
23
|
+
function stripHtmlTags(html) {
|
|
24
|
+
return html
|
|
25
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
26
|
+
.replace(/<\/?(p|div|li|tr)[^>]*>/gi, '\n')
|
|
27
|
+
.replace(/<[^>]+>/g, '')
|
|
28
|
+
.replace(/&/g, '&')
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"')
|
|
32
|
+
.replace(/'/g, "'")
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
16
35
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* render as plain text instead of breaking the parse.
|
|
20
|
-
* Truncates to Telegram's 4096-char hard limit.
|
|
21
|
-
* Use for dynamic LLM/Oracle output.
|
|
36
|
+
* Splits an HTML string into chunks ≤ maxLen that never break inside an HTML tag.
|
|
37
|
+
* Prefers splitting at paragraph (double newline) or line boundaries.
|
|
22
38
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
function splitHtmlChunks(html, maxLen = 4096) {
|
|
40
|
+
if (html.length <= maxLen)
|
|
41
|
+
return [html];
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let remaining = html.trim();
|
|
44
|
+
while (remaining.length > maxLen) {
|
|
45
|
+
let splitAt = -1;
|
|
46
|
+
for (const sep of ['\n\n', '\n', ' ']) {
|
|
47
|
+
const pos = remaining.lastIndexOf(sep, maxLen - 1);
|
|
48
|
+
if (pos < maxLen / 4)
|
|
49
|
+
continue; // avoid tiny first chunks
|
|
50
|
+
// Confirm position is not inside an HTML tag
|
|
51
|
+
const before = remaining.slice(0, pos);
|
|
52
|
+
const lastOpen = before.lastIndexOf('<');
|
|
53
|
+
const lastClose = before.lastIndexOf('>');
|
|
54
|
+
if (lastOpen > lastClose)
|
|
55
|
+
continue; // inside a tag — try next separator
|
|
56
|
+
splitAt = pos + sep.length;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (splitAt <= 0) {
|
|
60
|
+
// Fallback: split right after the last closing '>' before maxLen
|
|
61
|
+
const closing = remaining.lastIndexOf('>', maxLen - 1);
|
|
62
|
+
splitAt = closing > 0 ? closing + 1 : maxLen;
|
|
63
|
+
}
|
|
64
|
+
const chunk = remaining.slice(0, splitAt).trim();
|
|
65
|
+
if (chunk)
|
|
66
|
+
chunks.push(chunk);
|
|
67
|
+
remaining = remaining.slice(splitAt).trim();
|
|
31
68
|
}
|
|
32
|
-
|
|
69
|
+
if (remaining)
|
|
70
|
+
chunks.push(remaining);
|
|
71
|
+
return chunks.filter(Boolean);
|
|
33
72
|
}
|
|
34
|
-
async function
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
async function toTelegramRichText(text) {
|
|
74
|
+
let source = String(text ?? '').replace(/\r\n/g, '\n');
|
|
75
|
+
const uuidRegex = /\b([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})\b/g;
|
|
76
|
+
const codeBlocks = [];
|
|
77
|
+
source = source.replace(/```([a-zA-Z0-9_-]+)?\n?([\s\S]*?)```/g, (_m, _lang, code) => {
|
|
78
|
+
const idx = codeBlocks.push(`<pre><code>${escapeHtml(String(code).trimEnd())}</code></pre>`) - 1;
|
|
79
|
+
return `@@CODEBLOCK_${idx}@@`;
|
|
80
|
+
});
|
|
81
|
+
const inlineCodes = [];
|
|
82
|
+
source = source.replace(/`([^`\n]+)`/g, (_m, code) => {
|
|
83
|
+
const idx = inlineCodes.push(`<code>${escapeHtml(code)}</code>`) - 1;
|
|
84
|
+
return `@@INLINECODE_${idx}@@`;
|
|
85
|
+
});
|
|
86
|
+
const links = [];
|
|
87
|
+
source = source.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => {
|
|
88
|
+
const safeUrl = String(url).replace(/"/g, '"');
|
|
89
|
+
const idx = links.push(`<a href="${safeUrl}">${escapeHtml(label)}</a>`) - 1;
|
|
90
|
+
return `@@LINK_${idx}@@`;
|
|
91
|
+
});
|
|
92
|
+
// Markdown bullets become visible bullets in Telegram HTML mode.
|
|
93
|
+
source = source.replace(/^(\s*)[-*]\s+/gm, '$1• ');
|
|
94
|
+
// Escape user/model content before reinserting HTML tags.
|
|
95
|
+
source = escapeHtml(source);
|
|
96
|
+
// Headings -> bold lines
|
|
97
|
+
source = source.replace(/^#{1,6}\s+(.+)$/gm, (_m, title) => `<b>${title.trim()}</b>`);
|
|
98
|
+
// Bold
|
|
99
|
+
source = source.replace(/\*\*([\s\S]+?)\*\*/g, '<b>$1</b>');
|
|
100
|
+
source = source.replace(/__([\s\S]+?)__/g, '<b>$1</b>');
|
|
101
|
+
// Italic (conservative)
|
|
102
|
+
source = source.replace(/(^|[\s(])\*([^*\n]+)\*(?=[$\s).,!?:;])/gm, '$1<i>$2</i>');
|
|
103
|
+
source = source.replace(/(^|[\s(])_([^_\n]+)_(?=[$\s).,!?:;])/gm, '$1<i>$2</i>');
|
|
104
|
+
// Make task/session IDs easier to copy in Telegram.
|
|
105
|
+
source = source.replace(uuidRegex, '<code>$1</code>');
|
|
106
|
+
// Restore placeholders
|
|
107
|
+
source = source.replace(/@@CODEBLOCK_(\d+)@@/g, (_m, idx) => codeBlocks[Number(idx)] || '');
|
|
108
|
+
source = source.replace(/@@INLINECODE_(\d+)@@/g, (_m, idx) => inlineCodes[Number(idx)] || '');
|
|
109
|
+
source = source.replace(/@@LINK_(\d+)@@/g, (_m, idx) => links[Number(idx)] || '');
|
|
110
|
+
return { chunks: splitHtmlChunks(source.trim()), parse_mode: 'HTML' };
|
|
40
111
|
}
|
|
41
112
|
/**
|
|
42
113
|
* Escapes special characters in a plain string segment so it's safe to embed
|
|
@@ -78,7 +149,7 @@ export class TelegramAdapter {
|
|
|
78
149
|
/stats \\- Show token usage statistics
|
|
79
150
|
/help \\- Show available commands
|
|
80
151
|
/zaion \\- Show system configurations
|
|
81
|
-
/sati
|
|
152
|
+
/sati qnt \\- Show specific memories
|
|
82
153
|
/newsession \\- Archive current session and start fresh
|
|
83
154
|
/sessions \\- List all sessions with titles and switch between them
|
|
84
155
|
/restart \\- Restart the Morpheus agent
|
|
@@ -123,14 +194,26 @@ export class TelegramAdapter {
|
|
|
123
194
|
try {
|
|
124
195
|
// Send "typing" status
|
|
125
196
|
await ctx.sendChatAction('typing');
|
|
197
|
+
const sessionId = await this.history.getCurrentSessionOrCreate();
|
|
198
|
+
await this.oracle.setSessionId(sessionId);
|
|
126
199
|
// Process with Agent
|
|
127
|
-
const response = await this.oracle.chat(text
|
|
200
|
+
const response = await this.oracle.chat(text, undefined, false, {
|
|
201
|
+
origin_channel: 'telegram',
|
|
202
|
+
session_id: sessionId,
|
|
203
|
+
origin_message_id: String(ctx.message.message_id),
|
|
204
|
+
origin_user_id: userId,
|
|
205
|
+
});
|
|
128
206
|
if (response) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
207
|
+
const rich = await toTelegramRichText(response);
|
|
208
|
+
for (const chunk of rich.chunks) {
|
|
209
|
+
try {
|
|
210
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
214
|
+
if (plain)
|
|
215
|
+
await ctx.reply(plain);
|
|
216
|
+
}
|
|
134
217
|
}
|
|
135
218
|
this.display.log(`Responded to @${user}: ${response}`, { source: 'Telegram' });
|
|
136
219
|
}
|
|
@@ -200,8 +283,15 @@ export class TelegramAdapter {
|
|
|
200
283
|
// So I should treat 'text' as if it was a text message.
|
|
201
284
|
await ctx.reply(`🎤 Transcription: "${text}"`);
|
|
202
285
|
await ctx.sendChatAction('typing');
|
|
286
|
+
const sessionId = await this.history.getCurrentSessionOrCreate();
|
|
287
|
+
await this.oracle.setSessionId(sessionId);
|
|
203
288
|
// Process with Agent
|
|
204
|
-
const response = await this.oracle.chat(text, usage, true
|
|
289
|
+
const response = await this.oracle.chat(text, usage, true, {
|
|
290
|
+
origin_channel: 'telegram',
|
|
291
|
+
session_id: sessionId,
|
|
292
|
+
origin_message_id: String(ctx.message.message_id),
|
|
293
|
+
origin_user_id: userId,
|
|
294
|
+
});
|
|
205
295
|
// if (listeningMsg) {
|
|
206
296
|
// try {
|
|
207
297
|
// await ctx.telegram.deleteMessage(ctx.chat.id, listeningMsg.message_id);
|
|
@@ -210,11 +300,16 @@ export class TelegramAdapter {
|
|
|
210
300
|
// }
|
|
211
301
|
// }
|
|
212
302
|
if (response) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
303
|
+
const rich = await toTelegramRichText(response);
|
|
304
|
+
for (const chunk of rich.chunks) {
|
|
305
|
+
try {
|
|
306
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
310
|
+
if (plain)
|
|
311
|
+
await ctx.reply(plain);
|
|
312
|
+
}
|
|
218
313
|
}
|
|
219
314
|
this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
|
|
220
315
|
}
|
|
@@ -405,18 +500,10 @@ export class TelegramAdapter {
|
|
|
405
500
|
await fs.writeFile(filePath, buffer);
|
|
406
501
|
return filePath;
|
|
407
502
|
}
|
|
408
|
-
/**
|
|
409
|
-
* Escapes a string for Telegram MarkdownV2 format.
|
|
410
|
-
* All special characters outside code spans must be escaped with a backslash.
|
|
411
|
-
*/
|
|
412
|
-
escapeMarkdownV2(text) {
|
|
413
|
-
// Characters that must be escaped in MarkdownV2 outside of code/pre blocks
|
|
414
|
-
return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, '\\$1');
|
|
415
|
-
}
|
|
416
503
|
/**
|
|
417
504
|
* Sends a proactive message to all allowed Telegram users.
|
|
418
505
|
* Used by the webhook notification system to push results.
|
|
419
|
-
*
|
|
506
|
+
* Uses Telegram HTML parse mode for richer formatting, with plain-text fallback.
|
|
420
507
|
*/
|
|
421
508
|
async sendMessage(text) {
|
|
422
509
|
if (!this.isConnected || !this.bot) {
|
|
@@ -428,22 +515,39 @@ export class TelegramAdapter {
|
|
|
428
515
|
this.display.log('No allowed Telegram users configured — skipping notification.', { source: 'Telegram', level: 'warning' });
|
|
429
516
|
return;
|
|
430
517
|
}
|
|
431
|
-
|
|
432
|
-
const { text: mdText, parse_mode } = await toMd(text);
|
|
518
|
+
const rich = await toTelegramRichText(text);
|
|
433
519
|
for (const userId of allowedUsers) {
|
|
520
|
+
for (const chunk of rich.chunks) {
|
|
521
|
+
try {
|
|
522
|
+
await this.bot.telegram.sendMessage(userId, chunk, { parse_mode: rich.parse_mode });
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
try {
|
|
526
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
527
|
+
if (plain)
|
|
528
|
+
await this.bot.telegram.sendMessage(userId, plain);
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
this.display.log(`Failed to send message chunk to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async sendMessageToUser(userId, text) {
|
|
538
|
+
if (!this.isConnected || !this.bot) {
|
|
539
|
+
this.display.log('Cannot send direct message: Telegram bot not connected.', { source: 'Telegram', level: 'warning' });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const rich = await toTelegramRichText(text);
|
|
543
|
+
for (const chunk of rich.chunks) {
|
|
434
544
|
try {
|
|
435
|
-
await this.bot.telegram.sendMessage(userId,
|
|
545
|
+
await this.bot.telegram.sendMessage(userId, chunk, { parse_mode: rich.parse_mode });
|
|
436
546
|
}
|
|
437
547
|
catch {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const MAX_LEN = 4096;
|
|
441
|
-
const plain = text.length > MAX_LEN ? text.slice(0, MAX_LEN - 3) + '...' : text;
|
|
548
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
549
|
+
if (plain)
|
|
442
550
|
await this.bot.telegram.sendMessage(userId, plain);
|
|
443
|
-
}
|
|
444
|
-
catch (err) {
|
|
445
|
-
this.display.log(`Failed to send message to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
|
|
446
|
-
}
|
|
447
551
|
}
|
|
448
552
|
}
|
|
449
553
|
}
|
|
@@ -622,7 +726,7 @@ How can I assist you today?`;
|
|
|
622
726
|
response += `✅ Node\\.js: ${escMd(nodeVersion)}\n`;
|
|
623
727
|
}
|
|
624
728
|
else {
|
|
625
|
-
response += `❌ Node\\.js: ${escMd(nodeVersion)} \\(Required:
|
|
729
|
+
response += `❌ Node\\.js: ${escMd(nodeVersion)} \\(Required: \\>\\=18\\)\n`;
|
|
626
730
|
}
|
|
627
731
|
if (config) {
|
|
628
732
|
response += '✅ Configuration: Valid\n';
|
|
@@ -672,6 +776,17 @@ How can I assist you today?`;
|
|
|
672
776
|
response += `❌ Apoc API key missing \\(${escMd(apocProvider)}\\)\n`;
|
|
673
777
|
}
|
|
674
778
|
}
|
|
779
|
+
// Neo
|
|
780
|
+
const neo = config.neo;
|
|
781
|
+
const neoProvider = neo?.provider || llmProvider;
|
|
782
|
+
if (neoProvider && neoProvider !== 'ollama') {
|
|
783
|
+
if (hasApiKey(neoProvider, neo?.api_key ?? config.llm?.api_key)) {
|
|
784
|
+
response += `✅ Neo API key \\(${escMd(neoProvider)}\\)\n`;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
response += `❌ Neo API key missing \\(${escMd(neoProvider)}\\)\n`;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
675
790
|
// Telegram token
|
|
676
791
|
if (config.channels?.telegram?.enabled) {
|
|
677
792
|
const hasTelegramToken = config.channels.telegram?.token || process.env.TELEGRAM_BOT_TOKEN;
|
|
@@ -755,11 +870,16 @@ How can I assist you today?`;
|
|
|
755
870
|
Só faça isso agora.`;
|
|
756
871
|
let response = await this.oracle.chat(prompt);
|
|
757
872
|
if (response) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
873
|
+
const rich = await toTelegramRichText(response);
|
|
874
|
+
for (const chunk of rich.chunks) {
|
|
875
|
+
try {
|
|
876
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
880
|
+
if (plain)
|
|
881
|
+
await ctx.reply(plain);
|
|
882
|
+
}
|
|
763
883
|
}
|
|
764
884
|
}
|
|
765
885
|
// await ctx.reply(`Command not recognized. Type /help to see available commands.`);
|
|
@@ -807,6 +927,22 @@ How can I assist you today?`;
|
|
|
807
927
|
response += `\\- Inherits Oracle config\n`;
|
|
808
928
|
}
|
|
809
929
|
response += '\n';
|
|
930
|
+
// Neo config (falls back to llm if not set)
|
|
931
|
+
const neo = config.neo;
|
|
932
|
+
response += `*Neo \\(MCP \\+ Internal Tools\\):*\n`;
|
|
933
|
+
if (neo?.provider) {
|
|
934
|
+
response += `\\- Provider: ${escMd(neo.provider)}\n`;
|
|
935
|
+
response += `\\- Model: ${escMd(neo.model || config.llm.model)}\n`;
|
|
936
|
+
response += `\\- Temperature: ${escMd(neo.temperature ?? 0.2)}\n`;
|
|
937
|
+
response += `\\- Context Window: ${escMd(neo.context_window ?? config.llm.context_window ?? 100)}\n`;
|
|
938
|
+
if (neo.max_tokens !== undefined) {
|
|
939
|
+
response += `\\- Max Tokens: ${escMd(neo.max_tokens)}\n`;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
response += `\\- Inherits Oracle config\n`;
|
|
944
|
+
}
|
|
945
|
+
response += '\n';
|
|
810
946
|
response += `*Channels:*\n`;
|
|
811
947
|
response += `\\- Telegram Enabled: ${escMd(config.channels.telegram.enabled)}\n`;
|
|
812
948
|
response += `\\- Discord Enabled: ${escMd(config.channels.discord.enabled)}\n\n`;
|
|
@@ -828,28 +964,29 @@ How can I assist you today?`;
|
|
|
828
964
|
}
|
|
829
965
|
}
|
|
830
966
|
try {
|
|
831
|
-
// Usar o repositório SATI para obter memórias de longo prazo
|
|
832
967
|
const repository = SatiRepository.getInstance();
|
|
833
968
|
const memories = repository.getAllMemories();
|
|
834
969
|
if (memories.length === 0) {
|
|
835
|
-
await ctx.reply(
|
|
970
|
+
await ctx.reply('No memories found.');
|
|
836
971
|
return;
|
|
837
972
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
selectedMemories = memories.slice(0, Math.min(limit, memories.length));
|
|
842
|
-
}
|
|
973
|
+
const selectedMemories = limit !== null
|
|
974
|
+
? memories.slice(0, Math.min(limit, memories.length))
|
|
975
|
+
: memories;
|
|
843
976
|
const countLabel = limit !== null
|
|
844
|
-
? `${
|
|
845
|
-
: `${
|
|
846
|
-
let
|
|
977
|
+
? `${selectedMemories.length} SATI Memories (showing first ${selectedMemories.length})`
|
|
978
|
+
: `${selectedMemories.length} SATI Memories`;
|
|
979
|
+
let html = `<b>${escapeHtml(countLabel)}:</b>\n\n`;
|
|
847
980
|
for (const memory of selectedMemories) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
981
|
+
const summary = memory.summary.length > 200
|
|
982
|
+
? memory.summary.substring(0, 200) + '...'
|
|
983
|
+
: memory.summary;
|
|
984
|
+
html += `<b>${escapeHtml(memory.category)} (${escapeHtml(memory.importance)}):</b> ${escapeHtml(summary)}\n\n`;
|
|
985
|
+
}
|
|
986
|
+
const chunks = splitHtmlChunks(html.trim());
|
|
987
|
+
for (const chunk of chunks) {
|
|
988
|
+
await ctx.reply(chunk, { parse_mode: 'HTML' });
|
|
851
989
|
}
|
|
852
|
-
await ctx.reply(response, { parse_mode: 'MarkdownV2' });
|
|
853
990
|
}
|
|
854
991
|
catch (error) {
|
|
855
992
|
await ctx.reply(`Failed to retrieve memories: ${error.message}`);
|
|
@@ -50,6 +50,8 @@ export const doctorCommand = new Command('doctor')
|
|
|
50
50
|
// Check API keys availability for active providers
|
|
51
51
|
const llmProvider = config.llm?.provider;
|
|
52
52
|
const satiProvider = config.sati?.provider;
|
|
53
|
+
const apocProvider = config.apoc?.provider || llmProvider;
|
|
54
|
+
const neoProvider = config.neo?.provider || llmProvider;
|
|
53
55
|
// Check LLM provider API key
|
|
54
56
|
if (llmProvider && llmProvider !== 'ollama') {
|
|
55
57
|
const hasLlmApiKey = config.llm?.api_key ||
|
|
@@ -80,6 +82,38 @@ export const doctorCommand = new Command('doctor')
|
|
|
80
82
|
allPassed = false;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
85
|
+
// Check Apoc provider API key
|
|
86
|
+
if (apocProvider && apocProvider !== 'ollama') {
|
|
87
|
+
const hasApocApiKey = config.apoc?.api_key ||
|
|
88
|
+
config.llm?.api_key ||
|
|
89
|
+
(apocProvider === 'openai' && process.env.OPENAI_API_KEY) ||
|
|
90
|
+
(apocProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
|
|
91
|
+
(apocProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
|
|
92
|
+
(apocProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
|
|
93
|
+
if (hasApocApiKey) {
|
|
94
|
+
console.log(chalk.green('✓') + ` Apoc API key available for ${apocProvider}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(chalk.red('✗') + ` Apoc API key missing for ${apocProvider}. Either set in config or define environment variable.`);
|
|
98
|
+
allPassed = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Check Neo provider API key
|
|
102
|
+
if (neoProvider && neoProvider !== 'ollama') {
|
|
103
|
+
const hasNeoApiKey = config.neo?.api_key ||
|
|
104
|
+
config.llm?.api_key ||
|
|
105
|
+
(neoProvider === 'openai' && process.env.OPENAI_API_KEY) ||
|
|
106
|
+
(neoProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
|
|
107
|
+
(neoProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
|
|
108
|
+
(neoProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
|
|
109
|
+
if (hasNeoApiKey) {
|
|
110
|
+
console.log(chalk.green('✓') + ` Neo API key available for ${neoProvider}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(chalk.red('✗') + ` Neo API key missing for ${neoProvider}. Either set in config or define environment variable.`);
|
|
114
|
+
allPassed = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
83
117
|
// Check audio API key if enabled
|
|
84
118
|
if (config.audio?.enabled && config.llm?.provider !== 'gemini') {
|
|
85
119
|
const hasAudioApiKey = config.audio?.apiKey || process.env.GOOGLE_API_KEY;
|
|
@@ -208,6 +208,134 @@ export const initCommand = new Command('init')
|
|
|
208
208
|
if (satiApiKey) {
|
|
209
209
|
await configManager.set('sati.api_key', satiApiKey);
|
|
210
210
|
}
|
|
211
|
+
// Neo (MCP + Internal Tools Agent) Configuration
|
|
212
|
+
display.log(chalk.blue('\nNeo (MCP + Internal Tools Agent) Configuration'));
|
|
213
|
+
const configureNeo = await select({
|
|
214
|
+
message: 'Configure Neo separately?',
|
|
215
|
+
choices: [
|
|
216
|
+
{ name: 'No (Use Oracle provider/model defaults)', value: 'no' },
|
|
217
|
+
{ name: 'Yes', value: 'yes' },
|
|
218
|
+
],
|
|
219
|
+
default: currentConfig.neo ? 'yes' : 'no',
|
|
220
|
+
});
|
|
221
|
+
let neoProvider = provider;
|
|
222
|
+
let neoModel = model;
|
|
223
|
+
let neoTemperature = currentConfig.neo?.temperature ?? 0.2;
|
|
224
|
+
let neoContextWindow = currentConfig.neo?.context_window ?? Number(contextWindow);
|
|
225
|
+
let neoMaxTokens = currentConfig.neo?.max_tokens;
|
|
226
|
+
let neoApiKey = currentConfig.neo?.api_key || apiKey || currentConfig.llm.api_key;
|
|
227
|
+
let neoBaseUrl = provider === 'openrouter'
|
|
228
|
+
? (currentConfig.neo?.base_url || currentConfig.llm.base_url || 'https://openrouter.ai/api/v1')
|
|
229
|
+
: undefined;
|
|
230
|
+
if (configureNeo === 'yes') {
|
|
231
|
+
neoProvider = await select({
|
|
232
|
+
message: 'Select Neo LLM Provider:',
|
|
233
|
+
choices: [
|
|
234
|
+
{ name: 'OpenAI', value: 'openai' },
|
|
235
|
+
{ name: 'Anthropic', value: 'anthropic' },
|
|
236
|
+
{ name: 'OpenRouter', value: 'openrouter' },
|
|
237
|
+
{ name: 'Ollama', value: 'ollama' },
|
|
238
|
+
{ name: 'Google Gemini', value: 'gemini' },
|
|
239
|
+
],
|
|
240
|
+
default: currentConfig.neo?.provider || provider,
|
|
241
|
+
});
|
|
242
|
+
let defaultNeoModel = 'gpt-3.5-turbo';
|
|
243
|
+
switch (neoProvider) {
|
|
244
|
+
case 'openai':
|
|
245
|
+
defaultNeoModel = 'gpt-4o';
|
|
246
|
+
break;
|
|
247
|
+
case 'anthropic':
|
|
248
|
+
defaultNeoModel = 'claude-3-5-sonnet-20240620';
|
|
249
|
+
break;
|
|
250
|
+
case 'openrouter':
|
|
251
|
+
defaultNeoModel = 'openrouter/auto';
|
|
252
|
+
break;
|
|
253
|
+
case 'ollama':
|
|
254
|
+
defaultNeoModel = 'llama3';
|
|
255
|
+
break;
|
|
256
|
+
case 'gemini':
|
|
257
|
+
defaultNeoModel = 'gemini-pro';
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
if (neoProvider === currentConfig.neo?.provider) {
|
|
261
|
+
defaultNeoModel = currentConfig.neo?.model || defaultNeoModel;
|
|
262
|
+
}
|
|
263
|
+
neoModel = await input({
|
|
264
|
+
message: 'Enter Neo Model Name:',
|
|
265
|
+
default: defaultNeoModel,
|
|
266
|
+
});
|
|
267
|
+
const neoTemperatureInput = await input({
|
|
268
|
+
message: 'Neo Temperature (0-1):',
|
|
269
|
+
default: (currentConfig.neo?.temperature ?? 0.2).toString(),
|
|
270
|
+
validate: (val) => {
|
|
271
|
+
const n = Number(val);
|
|
272
|
+
return (!isNaN(n) && n >= 0 && n <= 1) || 'Must be a number between 0 and 1';
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
neoTemperature = Number(neoTemperatureInput);
|
|
276
|
+
const neoContextWindowInput = await input({
|
|
277
|
+
message: 'Neo Context Window (messages):',
|
|
278
|
+
default: (currentConfig.neo?.context_window ?? Number(contextWindow)).toString(),
|
|
279
|
+
validate: (val) => (!isNaN(Number(val)) && Number(val) > 0) || 'Must be a positive number',
|
|
280
|
+
});
|
|
281
|
+
neoContextWindow = Number(neoContextWindowInput);
|
|
282
|
+
const neoMaxTokensInput = await input({
|
|
283
|
+
message: 'Neo Max Tokens (optional, leave empty for model default):',
|
|
284
|
+
default: currentConfig.neo?.max_tokens?.toString() || '',
|
|
285
|
+
validate: (val) => {
|
|
286
|
+
if (val.trim() === '')
|
|
287
|
+
return true;
|
|
288
|
+
return (!isNaN(Number(val)) && Number(val) > 0) || 'Must be a positive number';
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
neoMaxTokens = neoMaxTokensInput.trim() === '' ? undefined : Number(neoMaxTokensInput);
|
|
292
|
+
if (neoProvider !== 'ollama') {
|
|
293
|
+
const hasExistingNeoKey = !!currentConfig.neo?.api_key || !!currentConfig.llm?.api_key;
|
|
294
|
+
let neoKeyMsg = hasExistingNeoKey
|
|
295
|
+
? 'Enter Neo API Key (leave empty to preserve existing, or if using env vars):'
|
|
296
|
+
: 'Enter Neo API Key (leave empty if using env vars):';
|
|
297
|
+
if (neoProvider === 'openai') {
|
|
298
|
+
neoKeyMsg = `${neoKeyMsg} (Env vars: MORPHEUS_NEO_API_KEY / OPENAI_API_KEY)`;
|
|
299
|
+
}
|
|
300
|
+
else if (neoProvider === 'anthropic') {
|
|
301
|
+
neoKeyMsg = `${neoKeyMsg} (Env vars: MORPHEUS_NEO_API_KEY / ANTHROPIC_API_KEY)`;
|
|
302
|
+
}
|
|
303
|
+
else if (neoProvider === 'gemini') {
|
|
304
|
+
neoKeyMsg = `${neoKeyMsg} (Env vars: MORPHEUS_NEO_API_KEY / GOOGLE_API_KEY)`;
|
|
305
|
+
}
|
|
306
|
+
else if (neoProvider === 'openrouter') {
|
|
307
|
+
neoKeyMsg = `${neoKeyMsg} (Env vars: MORPHEUS_NEO_API_KEY / OPENROUTER_API_KEY)`;
|
|
308
|
+
}
|
|
309
|
+
const neoKeyInput = await password({ message: neoKeyMsg });
|
|
310
|
+
if (neoKeyInput) {
|
|
311
|
+
neoApiKey = neoKeyInput;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
neoApiKey = currentConfig.neo?.api_key || currentConfig.llm?.api_key;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (neoProvider === 'openrouter') {
|
|
318
|
+
neoBaseUrl = await input({
|
|
319
|
+
message: 'Enter Neo OpenRouter Base URL:',
|
|
320
|
+
default: currentConfig.neo?.base_url || currentConfig.llm.base_url || 'https://openrouter.ai/api/v1',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
neoBaseUrl = undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
await configManager.save({
|
|
328
|
+
...configManager.get(),
|
|
329
|
+
neo: {
|
|
330
|
+
provider: neoProvider,
|
|
331
|
+
model: neoModel,
|
|
332
|
+
temperature: neoTemperature,
|
|
333
|
+
context_window: neoContextWindow,
|
|
334
|
+
max_tokens: neoMaxTokens,
|
|
335
|
+
api_key: neoApiKey,
|
|
336
|
+
base_url: neoBaseUrl,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
211
339
|
// Audio Configuration
|
|
212
340
|
const audioEnabled = await confirm({
|
|
213
341
|
message: 'Enable Audio Transcription? (Requires Gemini)',
|
|
@@ -12,6 +12,10 @@ import { Oracle } from '../../runtime/oracle.js';
|
|
|
12
12
|
import { ProviderError } from '../../runtime/errors.js';
|
|
13
13
|
import { HttpServer } from '../../http/server.js';
|
|
14
14
|
import { getVersion } from '../utils/version.js';
|
|
15
|
+
import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
16
|
+
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
17
|
+
import { TaskDispatcher } from '../../runtime/tasks/dispatcher.js';
|
|
18
|
+
import { WebhookDispatcher } from '../../runtime/webhooks/dispatcher.js';
|
|
15
19
|
export const restartCommand = new Command('restart')
|
|
16
20
|
.description('Restart the Morpheus agent')
|
|
17
21
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -96,6 +100,9 @@ export const restartCommand = new Command('restart')
|
|
|
96
100
|
}
|
|
97
101
|
const adapters = [];
|
|
98
102
|
let httpServer;
|
|
103
|
+
const taskWorker = new TaskWorker();
|
|
104
|
+
const taskNotifier = new TaskNotifier();
|
|
105
|
+
const asyncTasksEnabled = config.runtime?.async_tasks?.enabled !== false;
|
|
99
106
|
// Initialize Web UI
|
|
100
107
|
if (options.ui && config.ui.enabled) {
|
|
101
108
|
try {
|
|
@@ -114,6 +121,8 @@ export const restartCommand = new Command('restart')
|
|
|
114
121
|
const telegram = new TelegramAdapter(oracle);
|
|
115
122
|
try {
|
|
116
123
|
await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
|
|
124
|
+
WebhookDispatcher.setTelegramAdapter(telegram);
|
|
125
|
+
TaskDispatcher.setTelegramAdapter(telegram);
|
|
117
126
|
adapters.push(telegram);
|
|
118
127
|
}
|
|
119
128
|
catch (e) {
|
|
@@ -124,6 +133,10 @@ export const restartCommand = new Command('restart')
|
|
|
124
133
|
display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'));
|
|
125
134
|
}
|
|
126
135
|
}
|
|
136
|
+
if (asyncTasksEnabled) {
|
|
137
|
+
taskWorker.start();
|
|
138
|
+
taskNotifier.start();
|
|
139
|
+
}
|
|
127
140
|
// Handle graceful shutdown
|
|
128
141
|
const shutdown = async (signal) => {
|
|
129
142
|
display.stopSpinner();
|
|
@@ -134,6 +147,10 @@ export const restartCommand = new Command('restart')
|
|
|
134
147
|
for (const adapter of adapters) {
|
|
135
148
|
await adapter.disconnect();
|
|
136
149
|
}
|
|
150
|
+
if (asyncTasksEnabled) {
|
|
151
|
+
taskWorker.stop();
|
|
152
|
+
taskNotifier.stop();
|
|
153
|
+
}
|
|
137
154
|
await clearPid();
|
|
138
155
|
process.exit(0);
|
|
139
156
|
};
|