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.
Files changed (51) hide show
  1. package/README.md +293 -1115
  2. package/dist/channels/telegram.js +379 -74
  3. package/dist/cli/commands/doctor.js +34 -0
  4. package/dist/cli/commands/init.js +128 -0
  5. package/dist/cli/commands/restart.js +32 -14
  6. package/dist/cli/commands/start.js +28 -12
  7. package/dist/config/manager.js +82 -0
  8. package/dist/config/mcp-manager.js +19 -1
  9. package/dist/config/schemas.js +9 -0
  10. package/dist/devkit/tools/network.js +1 -1
  11. package/dist/http/api.js +399 -10
  12. package/dist/runtime/apoc.js +25 -17
  13. package/dist/runtime/memory/sati/repository.js +30 -2
  14. package/dist/runtime/memory/sati/service.js +46 -15
  15. package/dist/runtime/memory/sati/system-prompts.js +71 -29
  16. package/dist/runtime/memory/session-embedding-worker.js +3 -3
  17. package/dist/runtime/memory/sqlite.js +24 -0
  18. package/dist/runtime/memory/trinity-db.js +203 -0
  19. package/dist/runtime/neo.js +124 -0
  20. package/dist/runtime/oracle.js +252 -205
  21. package/dist/runtime/providers/factory.js +1 -12
  22. package/dist/runtime/session-embedding-scheduler.js +1 -1
  23. package/dist/runtime/tasks/context.js +53 -0
  24. package/dist/runtime/tasks/dispatcher.js +91 -0
  25. package/dist/runtime/tasks/notifier.js +68 -0
  26. package/dist/runtime/tasks/repository.js +370 -0
  27. package/dist/runtime/tasks/types.js +1 -0
  28. package/dist/runtime/tasks/worker.js +99 -0
  29. package/dist/runtime/tools/__tests__/tools.test.js +1 -3
  30. package/dist/runtime/tools/apoc-tool.js +61 -8
  31. package/dist/runtime/tools/delegation-guard.js +29 -0
  32. package/dist/runtime/tools/factory.js +1 -1
  33. package/dist/runtime/tools/index.js +2 -3
  34. package/dist/runtime/tools/morpheus-tools.js +742 -0
  35. package/dist/runtime/tools/neo-tool.js +109 -0
  36. package/dist/runtime/tools/trinity-tool.js +98 -0
  37. package/dist/runtime/trinity-connector.js +611 -0
  38. package/dist/runtime/trinity-crypto.js +52 -0
  39. package/dist/runtime/trinity.js +246 -0
  40. package/dist/runtime/webhooks/dispatcher.js +10 -19
  41. package/dist/types/config.js +10 -0
  42. package/dist/ui/assets/index-DP2V4kRd.js +112 -0
  43. package/dist/ui/assets/index-mglRG5Zw.css +1 -0
  44. package/dist/ui/index.html +2 -2
  45. package/dist/ui/sw.js +1 -1
  46. package/package.json +6 -1
  47. package/dist/runtime/tools/analytics-tools.js +0 -139
  48. package/dist/runtime/tools/config-tools.js +0 -64
  49. package/dist/runtime/tools/diagnostic-tools.js +0 -153
  50. package/dist/ui/assets/index-LemKVRjC.js +0 -112
  51. 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, '&lt;')
20
+ .replace(/>/g, '&gt;');
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(/&amp;/g, '&')
29
+ .replace(/&lt;/g, '<')
30
+ .replace(/&gt;/g, '>')
31
+ .replace(/&quot;/g, '"')
32
+ .replace(/&#39;/g, "'")
33
+ .trim();
34
+ }
16
35
  /**
17
- * Converts standard Markdown (as produced by LLMs) to Telegram MarkdownV2.
18
- * Unsupported tags (e.g. tables) have their special chars escaped so they
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
- // Cached dynamic import of telegram-markdown-v2 (ESM-safe, loaded once on first use)
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- let _convertFn = null;
26
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
- async function getConvert() {
28
- if (!_convertFn) {
29
- const mod = await import('telegram-markdown-v2');
30
- _convertFn = mod.convert;
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
- return _convertFn;
69
+ if (remaining)
70
+ chunks.push(remaining);
71
+ return chunks.filter(Boolean);
33
72
  }
34
- async function toMd(text) {
35
- const MAX = 4096;
36
- const convert = await getConvert();
37
- const converted = convert(text, 'escape');
38
- const safe = converted.length > MAX ? converted.slice(0, MAX - 3) + '\\.\\.\\.' : converted;
39
- return { text: safe, parse_mode: 'MarkdownV2' };
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, '&quot;');
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 <qnt> \\- Show specific memories
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
- try {
130
- await ctx.reply((await toMd(response)).text, { parse_mode: 'MarkdownV2' });
131
- }
132
- catch {
208
+ const rich = await toTelegramRichText(response);
209
+ for (const chunk of rich.chunks) {
133
210
  try {
134
- ctx.reply(response, { parse_mode: 'MarkdownV2' });
211
+ await ctx.reply(chunk, { parse_mode: rich.parse_mode });
135
212
  }
136
213
  catch {
137
- await ctx.reply(response);
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
- try {
219
- await ctx.reply((await toMd(response)).text, { parse_mode: 'MarkdownV2' });
220
- }
221
- catch {
222
- await ctx.reply(response);
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
- * Tries plain text first to avoid Markdown parse errors from LLM output.
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
- // toMd() already truncates to 4096 chars (Telegram's hard limit)
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, mdText, { parse_mode });
665
+ await this.bot.telegram.sendMessage(userId, chunk, { parse_mode: rich.parse_mode });
441
666
  }
442
667
  catch {
443
- // Fallback to plain text if MarkdownV2 conversion still fails
444
- try {
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: >=18\\)\n`;
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
- try {
764
- await ctx.reply((await toMd(response)).text, { parse_mode: 'MarkdownV2' });
765
- }
766
- catch {
767
- await ctx.reply(response);
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(`No memories found.`);
1143
+ await ctx.reply('No memories found.');
841
1144
  return;
842
1145
  }
843
- // Se nenhum limite for especificado, usar todas as memórias
844
- let selectedMemories = memories;
845
- if (limit !== null) {
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
- ? `${escMd(selectedMemories.length)} SATI Memories \\(Showing first ${escMd(selectedMemories.length)}\\)`
850
- : `${escMd(selectedMemories.length)} SATI Memories`;
851
- let response = `*${countLabel}:*\n\n`;
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
- // Limitar o tamanho do resumo para evitar mensagens muito longas
854
- const truncatedSummary = memory.summary.length > 200 ? memory.summary.substring(0, 200) + '...' : memory.summary;
855
- response += `*${escMd(memory.category)} \\(${escMd(memory.importance)}\\):* ${escMd(truncatedSummary)}\n\n`;
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;