morpheus-cli 0.5.0 → 0.5.2

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 (38) hide show
  1. package/README.md +26 -7
  2. package/dist/channels/telegram.js +173 -0
  3. package/dist/cli/commands/restart.js +15 -14
  4. package/dist/cli/commands/start.js +17 -12
  5. package/dist/config/manager.js +31 -0
  6. package/dist/config/mcp-manager.js +19 -1
  7. package/dist/config/schemas.js +2 -0
  8. package/dist/http/api.js +222 -0
  9. package/dist/runtime/memory/session-embedding-worker.js +3 -3
  10. package/dist/runtime/memory/trinity-db.js +203 -0
  11. package/dist/runtime/neo.js +16 -26
  12. package/dist/runtime/oracle.js +16 -8
  13. package/dist/runtime/session-embedding-scheduler.js +1 -1
  14. package/dist/runtime/tasks/dispatcher.js +21 -0
  15. package/dist/runtime/tasks/repository.js +4 -0
  16. package/dist/runtime/tasks/worker.js +4 -1
  17. package/dist/runtime/tools/__tests__/tools.test.js +1 -3
  18. package/dist/runtime/tools/factory.js +1 -1
  19. package/dist/runtime/tools/index.js +1 -3
  20. package/dist/runtime/tools/morpheus-tools.js +742 -0
  21. package/dist/runtime/tools/neo-tool.js +19 -9
  22. package/dist/runtime/tools/trinity-tool.js +98 -0
  23. package/dist/runtime/trinity-connector.js +611 -0
  24. package/dist/runtime/trinity-crypto.js +52 -0
  25. package/dist/runtime/trinity.js +246 -0
  26. package/dist/runtime/webhooks/dispatcher.js +73 -2
  27. package/dist/runtime/webhooks/repository.js +7 -0
  28. package/dist/ui/assets/index-DP2V4kRd.js +112 -0
  29. package/dist/ui/assets/index-mglRG5Zw.css +1 -0
  30. package/dist/ui/index.html +2 -2
  31. package/dist/ui/sw.js +1 -1
  32. package/package.json +6 -1
  33. package/dist/runtime/tools/analytics-tools.js +0 -139
  34. package/dist/runtime/tools/config-tools.js +0 -64
  35. package/dist/runtime/tools/diagnostic-tools.js +0 -153
  36. package/dist/runtime/tools/task-query-tool.js +0 -76
  37. package/dist/ui/assets/index-20lLB1sM.js +0 -112
  38. package/dist/ui/assets/index-BJ56bRfs.css +0 -1
package/README.md CHANGED
@@ -18,6 +18,7 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
18
18
  - `Neo`: MCP and internal operational tools (config, diagnostics, analytics).
19
19
  - `Apoc`: DevTools/browser execution (filesystem, shell, git, network, packages, processes, system, browser automation).
20
20
  - `Sati`: long-term memory retrieval/evaluation.
21
+ - `Trinity`: database specialist. Executes queries, introspects schemas, and manages registered databases (PostgreSQL, MySQL, SQLite, MongoDB).
21
22
 
22
23
  ## Installation
23
24
 
@@ -68,10 +69,10 @@ morpheus session status
68
69
  Morpheus uses asynchronous delegation by default:
69
70
 
70
71
  1. Oracle receives user request.
71
- 2. If execution is needed, Oracle calls `neo_delegate` or `apoc_delegate`.
72
+ 2. If execution is needed, Oracle calls `neo_delegate`, `apoc_delegate`, or `trinity_delegate`.
72
73
  3. Delegate tool creates a row in `tasks` table with origin metadata (`channel`, `session`, `message`, `user`).
73
74
  4. Oracle immediately acknowledges task creation.
74
- 5. `TaskWorker` executes pending tasks.
75
+ 5. `TaskWorker` executes pending tasks (routes `trinit` tasks to Trinity agent).
75
76
  6. `TaskNotifier` sends completion/failure through `TaskDispatcher`.
76
77
 
77
78
  Important behavior:
@@ -94,11 +95,13 @@ Task results are delivered proactively with metadata (task id, agent, status) an
94
95
  The dashboard includes:
95
96
  - Chat with session management
96
97
  - Tasks page (stats, filters, details, retry)
97
- - Agent settings (Oracle/Sati/Neo/Apoc)
98
- - MCP manager
99
- - Sati memories
98
+ - Agent settings (Oracle/Sati/Neo/Apoc/Trinity)
99
+ - MCP manager (add/edit/delete/toggle/reload)
100
+ - Sati memories (search, bulk delete)
100
101
  - Usage stats and model pricing
102
+ - Trinity databases (register/test/refresh schema)
101
103
  - Webhooks and notification inbox
104
+ - Logs viewer
102
105
 
103
106
  Chat-specific rendering:
104
107
  - AI messages rendered as markdown
@@ -140,6 +143,11 @@ apoc:
140
143
  working_dir: /home/user/projects
141
144
  timeout_ms: 30000
142
145
 
146
+ trinity:
147
+ provider: openai
148
+ model: gpt-4o-mini
149
+ temperature: 0.2
150
+
143
151
  runtime:
144
152
  async_tasks:
145
153
  enabled: true
@@ -178,6 +186,9 @@ Provider-specific keys:
178
186
  - `TELEGRAM_BOT_TOKEN`
179
187
  - `THE_ARCHITECT_PASS`
180
188
 
189
+ Security:
190
+ - `MORPHEUS_SECRET` — AES-256-GCM key for encrypting Trinity database passwords (required when using Trinity)
191
+
181
192
  Generic Morpheus overrides (selected):
182
193
 
183
194
  | Variable | Target |
@@ -213,6 +224,10 @@ Generic Morpheus overrides (selected):
213
224
  | `MORPHEUS_APOC_API_KEY` | `apoc.api_key` |
214
225
  | `MORPHEUS_APOC_WORKING_DIR` | `apoc.working_dir` |
215
226
  | `MORPHEUS_APOC_TIMEOUT_MS` | `apoc.timeout_ms` |
227
+ | `MORPHEUS_TRINITY_PROVIDER` | `trinity.provider` |
228
+ | `MORPHEUS_TRINITY_MODEL` | `trinity.model` |
229
+ | `MORPHEUS_TRINITY_TEMPERATURE` | `trinity.temperature` |
230
+ | `MORPHEUS_TRINITY_API_KEY` | `trinity.api_key` |
216
231
  | `MORPHEUS_AUDIO_PROVIDER` | `audio.provider` |
217
232
  | `MORPHEUS_AUDIO_MODEL` | `audio.model` |
218
233
  | `MORPHEUS_AUDIO_ENABLED` | `audio.enabled` |
@@ -262,9 +277,10 @@ Authenticated endpoints (`x-architect-pass`):
262
277
  - Sessions: `/api/sessions*`
263
278
  - Chat: `POST /api/chat`
264
279
  - Tasks: `GET /api/tasks`, `GET /api/tasks/stats`, `GET /api/tasks/:id`, `POST /api/tasks/:id/retry`
265
- - Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`
266
- - MCP: `/api/mcp/*`
280
+ - Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`
281
+ - MCP: `/api/mcp/*` (servers CRUD + reload + status)
267
282
  - Sati memories: `/api/sati/memories*`
283
+ - Trinity databases: `GET/POST/PUT/DELETE /api/trinity/databases`, `POST /api/trinity/databases/:id/test`, `POST /api/trinity/databases/:id/refresh-schema`
268
284
  - Usage/model pricing/logs/restart
269
285
  - Webhook management and webhook notifications
270
286
 
@@ -368,6 +384,9 @@ src/
368
384
  apoc.ts
369
385
  neo.ts
370
386
  oracle.ts
387
+ trinity.ts
388
+ trinity-connector.ts # PostgreSQL/MySQL/SQLite/MongoDB drivers
389
+ trinity-crypto.ts # AES-256-GCM encryption for DB passwords
371
390
  memory/
372
391
  tasks/
373
392
  tools/
@@ -150,6 +150,7 @@ export class TelegramAdapter {
150
150
  /help \\- Show available commands
151
151
  /zaion \\- Show system configurations
152
152
  /sati qnt \\- Show specific memories
153
+ /trinity \\- List registered Trinity databases
153
154
  /newsession \\- Archive current session and start fresh
154
155
  /sessions \\- List all sessions with titles and switch between them
155
156
  /restart \\- Restart the Morpheus agent
@@ -466,6 +467,125 @@ export class TelegramAdapter {
466
467
  await ctx.reply(`❌ Failed to ${enable ? 'enable' : 'disable'} MCP '${serverName}': ${error.message}`);
467
468
  }
468
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
+ });
469
589
  this.bot.launch().catch((err) => {
470
590
  if (this.isConnected) {
471
591
  this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
@@ -596,6 +716,9 @@ export class TelegramAdapter {
596
716
  case '/sati':
597
717
  await this.handleSatiCommand(ctx, user, args);
598
718
  break;
719
+ case '/trinity':
720
+ await this.handleTrinityCommand(ctx, user);
721
+ break;
599
722
  case '/restart':
600
723
  await this.handleRestartCommand(ctx, user);
601
724
  break;
@@ -954,6 +1077,56 @@ How can I assist you today?`;
954
1077
  response += `\\- Max Duration: ${escMd(config.audio.maxDurationSeconds)}s\n`;
955
1078
  await ctx.reply(response, { parse_mode: 'MarkdownV2' });
956
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
+ }
957
1130
  async handleSatiCommand(ctx, user, args) {
958
1131
  let limit = null;
959
1132
  if (args.length > 0) {
@@ -66,10 +66,10 @@ export const restartCommand = new Command('restart')
66
66
  const config = await configManager.load();
67
67
  // Initialize persistent logging
68
68
  await display.initialize(config.logging);
69
- display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
70
- display.log(chalk.gray(`PID: ${process.pid}`));
69
+ display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`), { source: 'Zaion' });
70
+ display.log(chalk.gray(`PID: ${process.pid}`), { source: 'Zaion' });
71
71
  if (options.ui) {
72
- display.log(chalk.blue(`Web UI enabled to port ${options.port}`));
72
+ display.log(chalk.blue(`Web UI enabled to port ${options.port}`), { source: 'Zaion' });
73
73
  }
74
74
  // Initialize Oracle
75
75
  const oracle = new Oracle(config);
@@ -82,17 +82,17 @@ export const restartCommand = new Command('restart')
82
82
  catch (err) {
83
83
  display.stopSpinner();
84
84
  if (err instanceof ProviderError) {
85
- display.log(chalk.red(`\nProvider Error (${err.provider}):`));
86
- display.log(chalk.white(err.message));
85
+ display.log(chalk.red(`\nProvider Error (${err.provider}):`), { source: 'Oracle' });
86
+ display.log(chalk.white(err.message), { source: 'Oracle' });
87
87
  if (err.suggestion) {
88
- display.log(chalk.yellow(`Tip: ${err.suggestion}`));
88
+ display.log(chalk.yellow(`Tip: ${err.suggestion}`), { source: 'Oracle' });
89
89
  }
90
90
  }
91
91
  else {
92
- display.log(chalk.red('\nOracle initialization failed:'));
93
- display.log(chalk.white(err.message));
92
+ display.log(chalk.red('\nOracle initialization failed:'), { source: 'Oracle' });
93
+ display.log(chalk.white(err.message), { source: 'Oracle' });
94
94
  if (err.message.includes('API Key')) {
95
- display.log(chalk.yellow('Tip: Check your API key in configuration or environment variables.'));
95
+ display.log(chalk.yellow('Tip: Check your API key in configuration or environment variables.'), { source: 'Oracle' });
96
96
  }
97
97
  }
98
98
  await clearPid();
@@ -112,7 +112,7 @@ export const restartCommand = new Command('restart')
112
112
  httpServer.start(port);
113
113
  }
114
114
  catch (e) {
115
- display.log(chalk.red(`Failed to start Web UI: ${e.message}`));
115
+ display.log(chalk.red(`Failed to start Web UI: ${e.message}`), { source: 'Zaion' });
116
116
  }
117
117
  }
118
118
  // Initialize Telegram
@@ -126,11 +126,11 @@ export const restartCommand = new Command('restart')
126
126
  adapters.push(telegram);
127
127
  }
128
128
  catch (e) {
129
- display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'));
129
+ display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'), { source: 'Zaion' });
130
130
  }
131
131
  }
132
132
  else {
133
- display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'));
133
+ display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'), { source: 'Zaion' });
134
134
  }
135
135
  }
136
136
  if (asyncTasksEnabled) {
@@ -140,7 +140,7 @@ export const restartCommand = new Command('restart')
140
140
  // Handle graceful shutdown
141
141
  const shutdown = async (signal) => {
142
142
  display.stopSpinner();
143
- display.log(`\n${signal} received. Shutting down...`);
143
+ display.log(`\n${signal} received. Shutting down...`, { source: 'Zaion' });
144
144
  if (httpServer) {
145
145
  httpServer.stop();
146
146
  }
@@ -177,7 +177,8 @@ export const restartCommand = new Command('restart')
177
177
  }
178
178
  catch (error) {
179
179
  display.stopSpinner();
180
- console.error(chalk.red('Failed to restart Morpheus:'), error.message);
180
+ display.log(chalk.red('Failed to restart Morpheus:'), { source: 'Zaion' });
181
+ display.log(chalk.white(error.message), { source: 'Zaion' });
181
182
  await clearPid();
182
183
  process.exit(1);
183
184
  }
@@ -91,7 +91,7 @@ export const startCommand = new Command('start')
91
91
  display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
92
92
  display.log(chalk.gray(`PID: ${process.pid}`));
93
93
  if (options.ui) {
94
- display.log(chalk.blue(`Web UI enabled to port ${options.port}`));
94
+ display.log(chalk.blue(`Web UI enabled to port ${options.port}`), { source: 'Zaion' });
95
95
  }
96
96
  // Initialize Oracle
97
97
  const oracle = new Oracle(config);
@@ -104,17 +104,17 @@ export const startCommand = new Command('start')
104
104
  catch (err) {
105
105
  display.stopSpinner();
106
106
  if (err instanceof ProviderError) {
107
- display.log(chalk.red(`\nProvider Error (${err.provider}):`));
108
- display.log(chalk.white(err.message));
107
+ display.log(chalk.red(`\nProvider Error (${err.provider}):`), { source: 'Oracle' });
108
+ display.log(chalk.white(err.message), { source: 'Oracle' });
109
109
  if (err.suggestion) {
110
- display.log(chalk.yellow(`Tip: ${err.suggestion}`));
110
+ display.log(chalk.yellow(`Tip: ${err.suggestion}`), { source: 'Oracle' });
111
111
  }
112
112
  }
113
113
  else {
114
- display.log(chalk.red('\nOracle initialization failed:'));
115
- display.log(chalk.white(err.message));
114
+ display.log(chalk.red('\nOracle initialization failed:'), { source: 'Oracle' });
115
+ display.log(chalk.white(err.message), { source: 'Oracle' });
116
116
  if (err.message.includes('API Key')) {
117
- display.log(chalk.yellow('Tip: Check your API key in configuration or environment variables.'));
117
+ display.log(chalk.yellow('Tip: Check your API key in configuration or environment variables.'), { source: 'Oracle' });
118
118
  }
119
119
  }
120
120
  await clearPid();
@@ -134,7 +134,7 @@ export const startCommand = new Command('start')
134
134
  httpServer.start(port);
135
135
  }
136
136
  catch (e) {
137
- display.log(chalk.red(`Failed to start Web UI: ${e.message}`));
137
+ display.log(chalk.red(`Failed to start Web UI: ${e.message}`), { source: 'Zaion' });
138
138
  }
139
139
  }
140
140
  // Initialize Telegram
@@ -149,11 +149,11 @@ export const startCommand = new Command('start')
149
149
  adapters.push(telegram);
150
150
  }
151
151
  catch (e) {
152
- display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'));
152
+ display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'), { source: 'Zaion' });
153
153
  }
154
154
  }
155
155
  else {
156
- display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'));
156
+ display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'), { source: 'Zaion' });
157
157
  }
158
158
  }
159
159
  // Start Background Services
@@ -162,10 +162,14 @@ export const startCommand = new Command('start')
162
162
  taskWorker.start();
163
163
  taskNotifier.start();
164
164
  }
165
+ // Recover webhook notifications stuck in 'pending' from previous runs
166
+ WebhookDispatcher.recoverStale().catch((err) => {
167
+ display.log(`Webhook recovery error: ${err.message}`, { source: 'Webhooks', level: 'error' });
168
+ });
165
169
  // Handle graceful shutdown
166
170
  const shutdown = async (signal) => {
167
171
  display.stopSpinner();
168
- display.log(`\n${signal} received. Shutting down...`);
172
+ display.log(`\n${signal} received. Shutting down...`, { source: 'Zaion' });
169
173
  if (httpServer) {
170
174
  httpServer.stop();
171
175
  }
@@ -202,7 +206,8 @@ export const startCommand = new Command('start')
202
206
  }
203
207
  catch (error) {
204
208
  display.stopSpinner();
205
- console.error(chalk.red('Failed to start Morpheus:'), error.message);
209
+ display.log(chalk.red('Failed to start Morpheus:'), { source: 'Zaion' });
210
+ display.log(chalk.white(error.message), { source: 'Zaion' });
206
211
  await clearPid();
207
212
  process.exit(1);
208
213
  }
@@ -124,6 +124,29 @@ export class ConfigManager {
124
124
  context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
125
125
  };
126
126
  }
127
+ // Apply precedence to Trinity config
128
+ const trinityEnvVars = [
129
+ 'MORPHEUS_TRINITY_PROVIDER',
130
+ 'MORPHEUS_TRINITY_MODEL',
131
+ 'MORPHEUS_TRINITY_TEMPERATURE',
132
+ 'MORPHEUS_TRINITY_API_KEY',
133
+ ];
134
+ const hasTrinityEnvOverrides = trinityEnvVars.some((envVar) => process.env[envVar] !== undefined);
135
+ let trinityConfig;
136
+ if (config.trinity || hasTrinityEnvOverrides) {
137
+ const trinityProvider = resolveProvider('MORPHEUS_TRINITY_PROVIDER', config.trinity?.provider, llmConfig.provider);
138
+ const trinityMaxTokensFallback = config.trinity?.max_tokens ?? llmConfig.max_tokens;
139
+ const trinityContextWindowFallback = config.trinity?.context_window ?? llmConfig.context_window;
140
+ trinityConfig = {
141
+ provider: trinityProvider,
142
+ model: resolveModel(trinityProvider, 'MORPHEUS_TRINITY_MODEL', config.trinity?.model || llmConfig.model),
143
+ temperature: resolveNumeric('MORPHEUS_TRINITY_TEMPERATURE', config.trinity?.temperature, llmConfig.temperature),
144
+ max_tokens: resolveOptionalNumeric('MORPHEUS_TRINITY_MAX_TOKENS', config.trinity?.max_tokens, trinityMaxTokensFallback),
145
+ api_key: resolveApiKey(trinityProvider, 'MORPHEUS_TRINITY_API_KEY', config.trinity?.api_key || llmConfig.api_key),
146
+ base_url: config.trinity?.base_url || config.llm.base_url,
147
+ context_window: resolveOptionalNumeric('MORPHEUS_TRINITY_CONTEXT_WINDOW', config.trinity?.context_window, trinityContextWindowFallback),
148
+ };
149
+ }
127
150
  // Apply precedence to audio config
128
151
  const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
129
152
  // AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
@@ -169,6 +192,7 @@ export class ConfigManager {
169
192
  sati: satiConfig,
170
193
  neo: neoConfig,
171
194
  apoc: apocConfig,
195
+ trinity: trinityConfig,
172
196
  audio: audioConfig,
173
197
  channels: channelsConfig,
174
198
  ui: uiConfig,
@@ -236,4 +260,11 @@ export class ConfigManager {
236
260
  ...this.config.llm,
237
261
  };
238
262
  }
263
+ getTrinityConfig() {
264
+ if (this.config.trinity) {
265
+ return { ...this.config.trinity };
266
+ }
267
+ // Fallback to main LLM config
268
+ return { ...this.config.llm };
269
+ }
239
270
  }
@@ -21,8 +21,11 @@ const readConfigFile = async () => {
21
21
  };
22
22
  const writeConfigFile = async (config) => {
23
23
  const configPath = path.join(MORPHEUS_ROOT, MCP_FILE_NAME);
24
+ const tmpPath = configPath + '.tmp';
24
25
  const serialized = JSON.stringify(config, null, 2) + '\n';
25
- await fs.writeFile(configPath, serialized, 'utf-8');
26
+ // Atomic write: write to temp file first, then rename — prevents partial writes from corrupting the live file
27
+ await fs.writeFile(tmpPath, serialized, 'utf-8');
28
+ await fs.rename(tmpPath, configPath);
26
29
  };
27
30
  const isMetadataKey = (key) => key.startsWith('_') || RESERVED_KEYS.has(key);
28
31
  const normalizeName = (rawName) => rawName.replace(/^\$/, '');
@@ -44,6 +47,21 @@ const ensureValidName = (name) => {
44
47
  }
45
48
  };
46
49
  export class MCPManager {
50
+ static reloadCallback = null;
51
+ /** Called by Oracle after initialization so MCPManager can trigger a full agent reload. */
52
+ static registerReloadCallback(fn) {
53
+ MCPManager.reloadCallback = fn;
54
+ }
55
+ /**
56
+ * Reloads MCP tools across all agents (Oracle provider, Neo catalog, Trinity catalog).
57
+ * Requires Oracle to have been initialized (and thus have registered its callback).
58
+ */
59
+ static async reloadAgents() {
60
+ if (!MCPManager.reloadCallback) {
61
+ throw new Error('Reload callback not registered — Oracle must be initialized before calling reloadAgents().');
62
+ }
63
+ await MCPManager.reloadCallback();
64
+ }
47
65
  static async listServers() {
48
66
  const config = await readConfigFile();
49
67
  const servers = [];
@@ -27,6 +27,7 @@ export const ApocConfigSchema = LLMConfigSchema.extend({
27
27
  timeout_ms: z.number().int().positive().optional(),
28
28
  });
29
29
  export const NeoConfigSchema = LLMConfigSchema;
30
+ export const TrinityConfigSchema = LLMConfigSchema;
30
31
  export const WebhookConfigSchema = z.object({
31
32
  telegram_notify_all: z.boolean().optional(),
32
33
  }).optional();
@@ -40,6 +41,7 @@ export const ConfigSchema = z.object({
40
41
  sati: SatiConfigSchema.optional(),
41
42
  neo: NeoConfigSchema.optional(),
42
43
  apoc: ApocConfigSchema.optional(),
44
+ trinity: TrinityConfigSchema.optional(),
43
45
  webhooks: WebhookConfigSchema,
44
46
  audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
45
47
  memory: z.object({