morpheus-cli 0.4.15 → 0.5.1
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 +293 -1115
- package/dist/channels/telegram.js +379 -74
- package/dist/cli/commands/doctor.js +34 -0
- package/dist/cli/commands/init.js +128 -0
- package/dist/cli/commands/restart.js +32 -14
- package/dist/cli/commands/start.js +28 -12
- package/dist/config/manager.js +82 -0
- package/dist/config/mcp-manager.js +19 -1
- package/dist/config/schemas.js +9 -0
- package/dist/devkit/tools/network.js +1 -1
- package/dist/http/api.js +399 -10
- package/dist/runtime/apoc.js +25 -17
- 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/session-embedding-worker.js +3 -3
- package/dist/runtime/memory/sqlite.js +24 -0
- package/dist/runtime/memory/trinity-db.js +203 -0
- package/dist/runtime/neo.js +124 -0
- package/dist/runtime/oracle.js +252 -205
- package/dist/runtime/providers/factory.js +1 -12
- package/dist/runtime/session-embedding-scheduler.js +1 -1
- package/dist/runtime/tasks/context.js +53 -0
- package/dist/runtime/tasks/dispatcher.js +91 -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 +99 -0
- package/dist/runtime/tools/__tests__/tools.test.js +1 -3
- package/dist/runtime/tools/apoc-tool.js +61 -8
- package/dist/runtime/tools/delegation-guard.js +29 -0
- package/dist/runtime/tools/factory.js +1 -1
- package/dist/runtime/tools/index.js +2 -3
- package/dist/runtime/tools/morpheus-tools.js +742 -0
- package/dist/runtime/tools/neo-tool.js +109 -0
- package/dist/runtime/tools/trinity-tool.js +98 -0
- package/dist/runtime/trinity-connector.js +611 -0
- package/dist/runtime/trinity-crypto.js +52 -0
- package/dist/runtime/trinity.js +246 -0
- package/dist/runtime/webhooks/dispatcher.js +10 -19
- package/dist/types/config.js +10 -0
- package/dist/ui/assets/index-DP2V4kRd.js +112 -0
- package/dist/ui/assets/index-mglRG5Zw.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +6 -1
- package/dist/runtime/tools/analytics-tools.js +0 -139
- package/dist/runtime/tools/config-tools.js +0 -64
- package/dist/runtime/tools/diagnostic-tools.js +0 -153
- 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,8 @@ 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
|
|
153
|
+
/trinity \\- List registered Trinity databases
|
|
82
154
|
/newsession \\- Archive current session and start fresh
|
|
83
155
|
/sessions \\- List all sessions with titles and switch between them
|
|
84
156
|
/restart \\- Restart the Morpheus agent
|
|
@@ -123,18 +195,25 @@ export class TelegramAdapter {
|
|
|
123
195
|
try {
|
|
124
196
|
// Send "typing" status
|
|
125
197
|
await ctx.sendChatAction('typing');
|
|
198
|
+
const sessionId = await this.history.getCurrentSessionOrCreate();
|
|
199
|
+
await this.oracle.setSessionId(sessionId);
|
|
126
200
|
// Process with Agent
|
|
127
|
-
const response = await this.oracle.chat(text
|
|
201
|
+
const response = await this.oracle.chat(text, undefined, false, {
|
|
202
|
+
origin_channel: 'telegram',
|
|
203
|
+
session_id: sessionId,
|
|
204
|
+
origin_message_id: String(ctx.message.message_id),
|
|
205
|
+
origin_user_id: userId,
|
|
206
|
+
});
|
|
128
207
|
if (response) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
208
|
+
const rich = await toTelegramRichText(response);
|
|
209
|
+
for (const chunk of rich.chunks) {
|
|
133
210
|
try {
|
|
134
|
-
ctx.reply(
|
|
211
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
135
212
|
}
|
|
136
213
|
catch {
|
|
137
|
-
|
|
214
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
215
|
+
if (plain)
|
|
216
|
+
await ctx.reply(plain);
|
|
138
217
|
}
|
|
139
218
|
}
|
|
140
219
|
this.display.log(`Responded to @${user}: ${response}`, { source: 'Telegram' });
|
|
@@ -205,8 +284,15 @@ export class TelegramAdapter {
|
|
|
205
284
|
// So I should treat 'text' as if it was a text message.
|
|
206
285
|
await ctx.reply(`🎤 Transcription: "${text}"`);
|
|
207
286
|
await ctx.sendChatAction('typing');
|
|
287
|
+
const sessionId = await this.history.getCurrentSessionOrCreate();
|
|
288
|
+
await this.oracle.setSessionId(sessionId);
|
|
208
289
|
// Process with Agent
|
|
209
|
-
const response = await this.oracle.chat(text, usage, true
|
|
290
|
+
const response = await this.oracle.chat(text, usage, true, {
|
|
291
|
+
origin_channel: 'telegram',
|
|
292
|
+
session_id: sessionId,
|
|
293
|
+
origin_message_id: String(ctx.message.message_id),
|
|
294
|
+
origin_user_id: userId,
|
|
295
|
+
});
|
|
210
296
|
// if (listeningMsg) {
|
|
211
297
|
// try {
|
|
212
298
|
// await ctx.telegram.deleteMessage(ctx.chat.id, listeningMsg.message_id);
|
|
@@ -215,11 +301,16 @@ export class TelegramAdapter {
|
|
|
215
301
|
// }
|
|
216
302
|
// }
|
|
217
303
|
if (response) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
304
|
+
const rich = await toTelegramRichText(response);
|
|
305
|
+
for (const chunk of rich.chunks) {
|
|
306
|
+
try {
|
|
307
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
311
|
+
if (plain)
|
|
312
|
+
await ctx.reply(plain);
|
|
313
|
+
}
|
|
223
314
|
}
|
|
224
315
|
this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
|
|
225
316
|
}
|
|
@@ -376,6 +467,125 @@ export class TelegramAdapter {
|
|
|
376
467
|
await ctx.reply(`❌ Failed to ${enable ? 'enable' : 'disable'} MCP '${serverName}': ${error.message}`);
|
|
377
468
|
}
|
|
378
469
|
});
|
|
470
|
+
// --- Trinity DB Test Connection ---
|
|
471
|
+
this.bot.action(/^test_trinity_db_/, async (ctx) => {
|
|
472
|
+
const data = ctx.callbackQuery.data;
|
|
473
|
+
const id = parseInt(data.replace('test_trinity_db_', ''), 10);
|
|
474
|
+
if (isNaN(id)) {
|
|
475
|
+
await ctx.answerCbQuery('Invalid ID');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
await ctx.answerCbQuery('Testing connection…');
|
|
479
|
+
try {
|
|
480
|
+
const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
|
|
481
|
+
const { testConnection } = await import('../runtime/trinity-connector.js');
|
|
482
|
+
const db = DatabaseRegistry.getInstance().getDatabase(id);
|
|
483
|
+
if (!db) {
|
|
484
|
+
await ctx.reply('❌ Database not found.');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const ok = await testConnection(db);
|
|
488
|
+
await ctx.reply(ok
|
|
489
|
+
? `✅ <b>${escapeHtml(db.name)}</b>: connection successful.`
|
|
490
|
+
: `❌ <b>${escapeHtml(db.name)}</b>: connection failed.`, { parse_mode: 'HTML' });
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
await ctx.reply(`❌ Error testing connection: ${escapeHtml(e.message)}`, { parse_mode: 'HTML' });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
// --- Trinity DB Refresh Schema ---
|
|
497
|
+
this.bot.action(/^refresh_trinity_db_schema_/, async (ctx) => {
|
|
498
|
+
const data = ctx.callbackQuery.data;
|
|
499
|
+
const id = parseInt(data.replace('refresh_trinity_db_schema_', ''), 10);
|
|
500
|
+
if (isNaN(id)) {
|
|
501
|
+
await ctx.answerCbQuery('Invalid ID');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
await ctx.answerCbQuery('Refreshing schema…');
|
|
505
|
+
try {
|
|
506
|
+
const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
|
|
507
|
+
const { introspectSchema } = await import('../runtime/trinity-connector.js');
|
|
508
|
+
const { Trinity } = await import('../runtime/trinity.js');
|
|
509
|
+
const registry = DatabaseRegistry.getInstance();
|
|
510
|
+
const db = registry.getDatabase(id);
|
|
511
|
+
if (!db) {
|
|
512
|
+
await ctx.reply('❌ Database not found.');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const schema = await introspectSchema(db);
|
|
516
|
+
registry.updateSchema(id, JSON.stringify(schema, null, 2));
|
|
517
|
+
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
518
|
+
const tableNames = schema.databases
|
|
519
|
+
? schema.databases.flatMap((d) => d.tables.map((t) => `${d.name}.${t.name}`))
|
|
520
|
+
: schema.tables.map((t) => t.name);
|
|
521
|
+
const count = tableNames.length;
|
|
522
|
+
await ctx.reply(`🔄 <b>${escapeHtml(db.name)}</b>: schema refreshed — ${count} ${count === 1 ? 'table' : 'tables'}.`, { parse_mode: 'HTML' });
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
await ctx.reply(`❌ Error refreshing schema: ${escapeHtml(e.message)}`, { parse_mode: 'HTML' });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// --- Trinity DB Delete Flow ---
|
|
529
|
+
this.bot.action(/^ask_trinity_db_delete_/, async (ctx) => {
|
|
530
|
+
const data = ctx.callbackQuery.data;
|
|
531
|
+
const id = parseInt(data.replace('ask_trinity_db_delete_', ''), 10);
|
|
532
|
+
if (isNaN(id)) {
|
|
533
|
+
await ctx.answerCbQuery('Invalid ID');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
|
|
538
|
+
const db = DatabaseRegistry.getInstance().getDatabase(id);
|
|
539
|
+
if (!db) {
|
|
540
|
+
await ctx.answerCbQuery('Database not found');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
await ctx.answerCbQuery();
|
|
544
|
+
await ctx.reply(`⚠️ Delete <b>${escapeHtml(db.name)}</b> (${escapeHtml(db.type)}) from Trinity?\n\nThe actual database won't be affected — only this registration will be removed.`, {
|
|
545
|
+
parse_mode: 'HTML',
|
|
546
|
+
reply_markup: {
|
|
547
|
+
inline_keyboard: [
|
|
548
|
+
[
|
|
549
|
+
{ text: '🗑️ Yes, delete', callback_data: `confirm_trinity_db_delete_${id}` },
|
|
550
|
+
{ text: 'Cancel', callback_data: 'cancel_trinity_db_delete' },
|
|
551
|
+
],
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
await ctx.answerCbQuery(`Error: ${e.message}`, { show_alert: true });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
this.bot.action(/^confirm_trinity_db_delete_/, async (ctx) => {
|
|
561
|
+
const data = ctx.callbackQuery.data;
|
|
562
|
+
const id = parseInt(data.replace('confirm_trinity_db_delete_', ''), 10);
|
|
563
|
+
if (isNaN(id)) {
|
|
564
|
+
await ctx.answerCbQuery('Invalid ID');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
|
|
569
|
+
const registry = DatabaseRegistry.getInstance();
|
|
570
|
+
const db = registry.getDatabase(id);
|
|
571
|
+
const name = db?.name ?? `#${id}`;
|
|
572
|
+
const deleted = registry.deleteDatabase(id);
|
|
573
|
+
await ctx.answerCbQuery(deleted ? '🗑️ Deleted' : 'Not found');
|
|
574
|
+
if (ctx.updateType === 'callback_query')
|
|
575
|
+
ctx.deleteMessage().catch(() => { });
|
|
576
|
+
const user = ctx.from?.username || ctx.from?.first_name || 'unknown';
|
|
577
|
+
this.display.log(`Trinity DB '${name}' deleted by @${user}`, { source: 'Telegram', level: 'info' });
|
|
578
|
+
await ctx.reply(deleted ? `🗑️ <b>${escapeHtml(name)}</b> removed from Trinity.` : `❌ Database #${id} not found.`, { parse_mode: 'HTML' });
|
|
579
|
+
}
|
|
580
|
+
catch (e) {
|
|
581
|
+
await ctx.answerCbQuery(`Error: ${e.message}`, { show_alert: true });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
this.bot.action('cancel_trinity_db_delete', async (ctx) => {
|
|
585
|
+
await ctx.answerCbQuery('Cancelled');
|
|
586
|
+
if (ctx.updateType === 'callback_query')
|
|
587
|
+
ctx.deleteMessage().catch(() => { });
|
|
588
|
+
});
|
|
379
589
|
this.bot.launch().catch((err) => {
|
|
380
590
|
if (this.isConnected) {
|
|
381
591
|
this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
|
|
@@ -410,18 +620,10 @@ export class TelegramAdapter {
|
|
|
410
620
|
await fs.writeFile(filePath, buffer);
|
|
411
621
|
return filePath;
|
|
412
622
|
}
|
|
413
|
-
/**
|
|
414
|
-
* Escapes a string for Telegram MarkdownV2 format.
|
|
415
|
-
* All special characters outside code spans must be escaped with a backslash.
|
|
416
|
-
*/
|
|
417
|
-
escapeMarkdownV2(text) {
|
|
418
|
-
// Characters that must be escaped in MarkdownV2 outside of code/pre blocks
|
|
419
|
-
return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, '\\$1');
|
|
420
|
-
}
|
|
421
623
|
/**
|
|
422
624
|
* Sends a proactive message to all allowed Telegram users.
|
|
423
625
|
* Used by the webhook notification system to push results.
|
|
424
|
-
*
|
|
626
|
+
* Uses Telegram HTML parse mode for richer formatting, with plain-text fallback.
|
|
425
627
|
*/
|
|
426
628
|
async sendMessage(text) {
|
|
427
629
|
if (!this.isConnected || !this.bot) {
|
|
@@ -433,22 +635,39 @@ export class TelegramAdapter {
|
|
|
433
635
|
this.display.log('No allowed Telegram users configured — skipping notification.', { source: 'Telegram', level: 'warning' });
|
|
434
636
|
return;
|
|
435
637
|
}
|
|
436
|
-
|
|
437
|
-
const { text: mdText, parse_mode } = await toMd(text);
|
|
638
|
+
const rich = await toTelegramRichText(text);
|
|
438
639
|
for (const userId of allowedUsers) {
|
|
640
|
+
for (const chunk of rich.chunks) {
|
|
641
|
+
try {
|
|
642
|
+
await this.bot.telegram.sendMessage(userId, chunk, { parse_mode: rich.parse_mode });
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
try {
|
|
646
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
647
|
+
if (plain)
|
|
648
|
+
await this.bot.telegram.sendMessage(userId, plain);
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
this.display.log(`Failed to send message chunk to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async sendMessageToUser(userId, text) {
|
|
658
|
+
if (!this.isConnected || !this.bot) {
|
|
659
|
+
this.display.log('Cannot send direct message: Telegram bot not connected.', { source: 'Telegram', level: 'warning' });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const rich = await toTelegramRichText(text);
|
|
663
|
+
for (const chunk of rich.chunks) {
|
|
439
664
|
try {
|
|
440
|
-
await this.bot.telegram.sendMessage(userId,
|
|
665
|
+
await this.bot.telegram.sendMessage(userId, chunk, { parse_mode: rich.parse_mode });
|
|
441
666
|
}
|
|
442
667
|
catch {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const MAX_LEN = 4096;
|
|
446
|
-
const plain = text.length > MAX_LEN ? text.slice(0, MAX_LEN - 3) + '...' : text;
|
|
668
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
669
|
+
if (plain)
|
|
447
670
|
await this.bot.telegram.sendMessage(userId, plain);
|
|
448
|
-
}
|
|
449
|
-
catch (err) {
|
|
450
|
-
this.display.log(`Failed to send message to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
|
|
451
|
-
}
|
|
452
671
|
}
|
|
453
672
|
}
|
|
454
673
|
}
|
|
@@ -497,6 +716,9 @@ export class TelegramAdapter {
|
|
|
497
716
|
case '/sati':
|
|
498
717
|
await this.handleSatiCommand(ctx, user, args);
|
|
499
718
|
break;
|
|
719
|
+
case '/trinity':
|
|
720
|
+
await this.handleTrinityCommand(ctx, user);
|
|
721
|
+
break;
|
|
500
722
|
case '/restart':
|
|
501
723
|
await this.handleRestartCommand(ctx, user);
|
|
502
724
|
break;
|
|
@@ -627,7 +849,7 @@ How can I assist you today?`;
|
|
|
627
849
|
response += `✅ Node\\.js: ${escMd(nodeVersion)}\n`;
|
|
628
850
|
}
|
|
629
851
|
else {
|
|
630
|
-
response += `❌ Node\\.js: ${escMd(nodeVersion)} \\(Required:
|
|
852
|
+
response += `❌ Node\\.js: ${escMd(nodeVersion)} \\(Required: \\>\\=18\\)\n`;
|
|
631
853
|
}
|
|
632
854
|
if (config) {
|
|
633
855
|
response += '✅ Configuration: Valid\n';
|
|
@@ -677,6 +899,17 @@ How can I assist you today?`;
|
|
|
677
899
|
response += `❌ Apoc API key missing \\(${escMd(apocProvider)}\\)\n`;
|
|
678
900
|
}
|
|
679
901
|
}
|
|
902
|
+
// Neo
|
|
903
|
+
const neo = config.neo;
|
|
904
|
+
const neoProvider = neo?.provider || llmProvider;
|
|
905
|
+
if (neoProvider && neoProvider !== 'ollama') {
|
|
906
|
+
if (hasApiKey(neoProvider, neo?.api_key ?? config.llm?.api_key)) {
|
|
907
|
+
response += `✅ Neo API key \\(${escMd(neoProvider)}\\)\n`;
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
response += `❌ Neo API key missing \\(${escMd(neoProvider)}\\)\n`;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
680
913
|
// Telegram token
|
|
681
914
|
if (config.channels?.telegram?.enabled) {
|
|
682
915
|
const hasTelegramToken = config.channels.telegram?.token || process.env.TELEGRAM_BOT_TOKEN;
|
|
@@ -760,11 +993,16 @@ How can I assist you today?`;
|
|
|
760
993
|
Só faça isso agora.`;
|
|
761
994
|
let response = await this.oracle.chat(prompt);
|
|
762
995
|
if (response) {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
996
|
+
const rich = await toTelegramRichText(response);
|
|
997
|
+
for (const chunk of rich.chunks) {
|
|
998
|
+
try {
|
|
999
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
1003
|
+
if (plain)
|
|
1004
|
+
await ctx.reply(plain);
|
|
1005
|
+
}
|
|
768
1006
|
}
|
|
769
1007
|
}
|
|
770
1008
|
// await ctx.reply(`Command not recognized. Type /help to see available commands.`);
|
|
@@ -812,6 +1050,22 @@ How can I assist you today?`;
|
|
|
812
1050
|
response += `\\- Inherits Oracle config\n`;
|
|
813
1051
|
}
|
|
814
1052
|
response += '\n';
|
|
1053
|
+
// Neo config (falls back to llm if not set)
|
|
1054
|
+
const neo = config.neo;
|
|
1055
|
+
response += `*Neo \\(MCP \\+ Internal Tools\\):*\n`;
|
|
1056
|
+
if (neo?.provider) {
|
|
1057
|
+
response += `\\- Provider: ${escMd(neo.provider)}\n`;
|
|
1058
|
+
response += `\\- Model: ${escMd(neo.model || config.llm.model)}\n`;
|
|
1059
|
+
response += `\\- Temperature: ${escMd(neo.temperature ?? 0.2)}\n`;
|
|
1060
|
+
response += `\\- Context Window: ${escMd(neo.context_window ?? config.llm.context_window ?? 100)}\n`;
|
|
1061
|
+
if (neo.max_tokens !== undefined) {
|
|
1062
|
+
response += `\\- Max Tokens: ${escMd(neo.max_tokens)}\n`;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
response += `\\- Inherits Oracle config\n`;
|
|
1067
|
+
}
|
|
1068
|
+
response += '\n';
|
|
815
1069
|
response += `*Channels:*\n`;
|
|
816
1070
|
response += `\\- Telegram Enabled: ${escMd(config.channels.telegram.enabled)}\n`;
|
|
817
1071
|
response += `\\- Discord Enabled: ${escMd(config.channels.discord.enabled)}\n\n`;
|
|
@@ -823,6 +1077,56 @@ How can I assist you today?`;
|
|
|
823
1077
|
response += `\\- Max Duration: ${escMd(config.audio.maxDurationSeconds)}s\n`;
|
|
824
1078
|
await ctx.reply(response, { parse_mode: 'MarkdownV2' });
|
|
825
1079
|
}
|
|
1080
|
+
async handleTrinityCommand(ctx, user) {
|
|
1081
|
+
try {
|
|
1082
|
+
const { DatabaseRegistry } = await import('../runtime/memory/trinity-db.js');
|
|
1083
|
+
const registry = DatabaseRegistry.getInstance();
|
|
1084
|
+
const databases = registry.listDatabases();
|
|
1085
|
+
if (databases.length === 0) {
|
|
1086
|
+
await ctx.reply('No databases registered in Trinity. Use the web UI to register databases.');
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
let html = `<b>Trinity Databases (${databases.length}):</b>\n\n`;
|
|
1090
|
+
const keyboard = [];
|
|
1091
|
+
for (const db of databases) {
|
|
1092
|
+
const schema = db.schema_json ? JSON.parse(db.schema_json) : null;
|
|
1093
|
+
const tables = schema?.tables?.map((t) => t.name).filter(Boolean) ?? [];
|
|
1094
|
+
const updatedAt = db.schema_updated_at
|
|
1095
|
+
? new Date(db.schema_updated_at).toLocaleDateString()
|
|
1096
|
+
: 'never';
|
|
1097
|
+
html += `🗄️ <b>${escapeHtml(db.name)}</b> (${escapeHtml(db.type)})\n`;
|
|
1098
|
+
if (db.host)
|
|
1099
|
+
html += ` Host: ${escapeHtml(db.host)}:${db.port}\n`;
|
|
1100
|
+
if (db.database_name && !db.host)
|
|
1101
|
+
html += ` File: ${escapeHtml(db.database_name)}\n`;
|
|
1102
|
+
if (tables.length > 0) {
|
|
1103
|
+
const tableList = tables.slice(0, 20).join(', ');
|
|
1104
|
+
const extra = tables.length > 20 ? ` (+${tables.length - 20} more)` : '';
|
|
1105
|
+
html += ` Tables: ${escapeHtml(tableList)}${escapeHtml(extra)}\n`;
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
html += ` Tables: (schema not loaded)\n`;
|
|
1109
|
+
}
|
|
1110
|
+
html += ` Schema updated: ${escapeHtml(updatedAt)}\n\n`;
|
|
1111
|
+
keyboard.push([
|
|
1112
|
+
{ text: `🔌 Test ${db.name}`, callback_data: `test_trinity_db_${db.id}` },
|
|
1113
|
+
{ text: `🔄 Schema`, callback_data: `refresh_trinity_db_schema_${db.id}` },
|
|
1114
|
+
{ text: `🗑️ Delete`, callback_data: `ask_trinity_db_delete_${db.id}` },
|
|
1115
|
+
]);
|
|
1116
|
+
}
|
|
1117
|
+
const chunks = splitHtmlChunks(html.trim());
|
|
1118
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1119
|
+
const isLast = i === chunks.length - 1;
|
|
1120
|
+
await ctx.reply(chunks[i], {
|
|
1121
|
+
parse_mode: 'HTML',
|
|
1122
|
+
...(isLast && keyboard.length > 0 ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
catch (e) {
|
|
1127
|
+
await ctx.reply(`Error listing Trinity databases: ${e.message}`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
826
1130
|
async handleSatiCommand(ctx, user, args) {
|
|
827
1131
|
let limit = null;
|
|
828
1132
|
if (args.length > 0) {
|
|
@@ -833,28 +1137,29 @@ How can I assist you today?`;
|
|
|
833
1137
|
}
|
|
834
1138
|
}
|
|
835
1139
|
try {
|
|
836
|
-
// Usar o repositório SATI para obter memórias de longo prazo
|
|
837
1140
|
const repository = SatiRepository.getInstance();
|
|
838
1141
|
const memories = repository.getAllMemories();
|
|
839
1142
|
if (memories.length === 0) {
|
|
840
|
-
await ctx.reply(
|
|
1143
|
+
await ctx.reply('No memories found.');
|
|
841
1144
|
return;
|
|
842
1145
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
selectedMemories = memories.slice(0, Math.min(limit, memories.length));
|
|
847
|
-
}
|
|
1146
|
+
const selectedMemories = limit !== null
|
|
1147
|
+
? memories.slice(0, Math.min(limit, memories.length))
|
|
1148
|
+
: memories;
|
|
848
1149
|
const countLabel = limit !== null
|
|
849
|
-
? `${
|
|
850
|
-
: `${
|
|
851
|
-
let
|
|
1150
|
+
? `${selectedMemories.length} SATI Memories (showing first ${selectedMemories.length})`
|
|
1151
|
+
: `${selectedMemories.length} SATI Memories`;
|
|
1152
|
+
let html = `<b>${escapeHtml(countLabel)}:</b>\n\n`;
|
|
852
1153
|
for (const memory of selectedMemories) {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1154
|
+
const summary = memory.summary.length > 200
|
|
1155
|
+
? memory.summary.substring(0, 200) + '...'
|
|
1156
|
+
: memory.summary;
|
|
1157
|
+
html += `<b>${escapeHtml(memory.category)} (${escapeHtml(memory.importance)}):</b> ${escapeHtml(summary)}\n\n`;
|
|
1158
|
+
}
|
|
1159
|
+
const chunks = splitHtmlChunks(html.trim());
|
|
1160
|
+
for (const chunk of chunks) {
|
|
1161
|
+
await ctx.reply(chunk, { parse_mode: 'HTML' });
|
|
856
1162
|
}
|
|
857
|
-
await ctx.reply(response, { parse_mode: 'MarkdownV2' });
|
|
858
1163
|
}
|
|
859
1164
|
catch (error) {
|
|
860
1165
|
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;
|