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
@@ -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)
@@ -62,10 +66,10 @@ export const restartCommand = new Command('restart')
62
66
  const config = await configManager.load();
63
67
  // Initialize persistent logging
64
68
  await display.initialize(config.logging);
65
- display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
66
- 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' });
67
71
  if (options.ui) {
68
- 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' });
69
73
  }
70
74
  // Initialize Oracle
71
75
  const oracle = new Oracle(config);
@@ -78,17 +82,17 @@ export const restartCommand = new Command('restart')
78
82
  catch (err) {
79
83
  display.stopSpinner();
80
84
  if (err instanceof ProviderError) {
81
- display.log(chalk.red(`\nProvider Error (${err.provider}):`));
82
- 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' });
83
87
  if (err.suggestion) {
84
- display.log(chalk.yellow(`Tip: ${err.suggestion}`));
88
+ display.log(chalk.yellow(`Tip: ${err.suggestion}`), { source: 'Oracle' });
85
89
  }
86
90
  }
87
91
  else {
88
- display.log(chalk.red('\nOracle initialization failed:'));
89
- 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' });
90
94
  if (err.message.includes('API Key')) {
91
- 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' });
92
96
  }
93
97
  }
94
98
  await clearPid();
@@ -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 {
@@ -105,7 +112,7 @@ export const restartCommand = new Command('restart')
105
112
  httpServer.start(port);
106
113
  }
107
114
  catch (e) {
108
- 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' });
109
116
  }
110
117
  }
111
118
  // Initialize Telegram
@@ -114,26 +121,36 @@ 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) {
120
- display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'));
129
+ display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'), { source: 'Zaion' });
121
130
  }
122
131
  }
123
132
  else {
124
- 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' });
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();
130
- display.log(`\n${signal} received. Shutting down...`);
143
+ display.log(`\n${signal} received. Shutting down...`, { source: 'Zaion' });
131
144
  if (httpServer) {
132
145
  httpServer.stop();
133
146
  }
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
  };
@@ -160,7 +177,8 @@ export const restartCommand = new Command('restart')
160
177
  }
161
178
  catch (error) {
162
179
  display.stopSpinner();
163
- 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' });
164
182
  await clearPid();
165
183
  process.exit(1);
166
184
  }
@@ -15,6 +15,9 @@ import { ProviderError } from '../../runtime/errors.js';
15
15
  import { HttpServer } from '../../http/server.js';
16
16
  import { getVersion } from '../utils/version.js';
17
17
  import { startSessionEmbeddingScheduler } from '../../runtime/session-embedding-scheduler.js';
18
+ import { TaskWorker } from '../../runtime/tasks/worker.js';
19
+ import { TaskNotifier } from '../../runtime/tasks/notifier.js';
20
+ import { TaskDispatcher } from '../../runtime/tasks/dispatcher.js';
18
21
  export const startCommand = new Command('start')
19
22
  .description('Start the Morpheus agent')
20
23
  .option('--ui', 'Enable web UI', true)
@@ -88,7 +91,7 @@ export const startCommand = new Command('start')
88
91
  display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
89
92
  display.log(chalk.gray(`PID: ${process.pid}`));
90
93
  if (options.ui) {
91
- 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' });
92
95
  }
93
96
  // Initialize Oracle
94
97
  const oracle = new Oracle(config);
@@ -101,17 +104,17 @@ export const startCommand = new Command('start')
101
104
  catch (err) {
102
105
  display.stopSpinner();
103
106
  if (err instanceof ProviderError) {
104
- display.log(chalk.red(`\nProvider Error (${err.provider}):`));
105
- 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' });
106
109
  if (err.suggestion) {
107
- display.log(chalk.yellow(`Tip: ${err.suggestion}`));
110
+ display.log(chalk.yellow(`Tip: ${err.suggestion}`), { source: 'Oracle' });
108
111
  }
109
112
  }
110
113
  else {
111
- display.log(chalk.red('\nOracle initialization failed:'));
112
- 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' });
113
116
  if (err.message.includes('API Key')) {
114
- 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' });
115
118
  }
116
119
  }
117
120
  await clearPid();
@@ -119,6 +122,9 @@ export const startCommand = new Command('start')
119
122
  }
120
123
  const adapters = [];
121
124
  let httpServer;
125
+ const taskWorker = new TaskWorker();
126
+ const taskNotifier = new TaskNotifier();
127
+ const asyncTasksEnabled = config.runtime?.async_tasks?.enabled !== false;
122
128
  // Initialize Web UI
123
129
  if (options.ui && config.ui.enabled) {
124
130
  try {
@@ -128,7 +134,7 @@ export const startCommand = new Command('start')
128
134
  httpServer.start(port);
129
135
  }
130
136
  catch (e) {
131
- 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' });
132
138
  }
133
139
  }
134
140
  // Initialize Telegram
@@ -139,28 +145,37 @@ export const startCommand = new Command('start')
139
145
  await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
140
146
  // Wire Telegram adapter to webhook dispatcher for proactive notifications
141
147
  WebhookDispatcher.setTelegramAdapter(telegram);
148
+ TaskDispatcher.setTelegramAdapter(telegram);
142
149
  adapters.push(telegram);
143
150
  }
144
151
  catch (e) {
145
- display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'));
152
+ display.log(chalk.red('Failed to initialize Telegram adapter. Continuing...'), { source: 'Zaion' });
146
153
  }
147
154
  }
148
155
  else {
149
- 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' });
150
157
  }
151
158
  }
152
159
  // Start Background Services
153
160
  startSessionEmbeddingScheduler();
161
+ if (asyncTasksEnabled) {
162
+ taskWorker.start();
163
+ taskNotifier.start();
164
+ }
154
165
  // Handle graceful shutdown
155
166
  const shutdown = async (signal) => {
156
167
  display.stopSpinner();
157
- display.log(`\n${signal} received. Shutting down...`);
168
+ display.log(`\n${signal} received. Shutting down...`, { source: 'Zaion' });
158
169
  if (httpServer) {
159
170
  httpServer.stop();
160
171
  }
161
172
  for (const adapter of adapters) {
162
173
  await adapter.disconnect();
163
174
  }
175
+ if (asyncTasksEnabled) {
176
+ taskWorker.stop();
177
+ taskNotifier.stop();
178
+ }
164
179
  await clearPid();
165
180
  process.exit(0);
166
181
  };
@@ -187,7 +202,8 @@ export const startCommand = new Command('start')
187
202
  }
188
203
  catch (error) {
189
204
  display.stopSpinner();
190
- console.error(chalk.red('Failed to start Morpheus:'), error.message);
205
+ display.log(chalk.red('Failed to start Morpheus:'), { source: 'Zaion' });
206
+ display.log(chalk.white(error.message), { source: 'Zaion' });
191
207
  await clearPid();
192
208
  process.exit(1);
193
209
  }
@@ -85,6 +85,68 @@ export class ConfigManager {
85
85
  timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000
86
86
  };
87
87
  }
88
+ // Apply precedence to Neo config
89
+ const neoEnvVars = [
90
+ 'MORPHEUS_NEO_PROVIDER',
91
+ 'MORPHEUS_NEO_MODEL',
92
+ 'MORPHEUS_NEO_TEMPERATURE',
93
+ 'MORPHEUS_NEO_MAX_TOKENS',
94
+ 'MORPHEUS_NEO_API_KEY',
95
+ 'MORPHEUS_NEO_BASE_URL',
96
+ 'MORPHEUS_NEO_CONTEXT_WINDOW',
97
+ ];
98
+ const hasNeoEnvOverrides = neoEnvVars.some((envVar) => process.env[envVar] !== undefined);
99
+ const resolveOptionalNumeric = (envVar, configValue, fallbackValue) => {
100
+ if (fallbackValue !== undefined) {
101
+ return resolveNumeric(envVar, configValue, fallbackValue);
102
+ }
103
+ if (process.env[envVar] !== undefined && process.env[envVar] !== '') {
104
+ const parsed = Number(process.env[envVar]);
105
+ if (!Number.isNaN(parsed)) {
106
+ return parsed;
107
+ }
108
+ }
109
+ return configValue;
110
+ };
111
+ let neoConfig;
112
+ if (config.neo || hasNeoEnvOverrides) {
113
+ const neoProvider = resolveProvider('MORPHEUS_NEO_PROVIDER', config.neo?.provider, llmConfig.provider);
114
+ const neoBaseUrl = resolveString('MORPHEUS_NEO_BASE_URL', config.neo?.base_url, llmConfig.base_url || '');
115
+ const neoMaxTokensFallback = config.neo?.max_tokens ?? llmConfig.max_tokens;
116
+ const neoContextWindowFallback = config.neo?.context_window ?? llmConfig.context_window;
117
+ neoConfig = {
118
+ provider: neoProvider,
119
+ model: resolveModel(neoProvider, 'MORPHEUS_NEO_MODEL', config.neo?.model || llmConfig.model),
120
+ temperature: resolveNumeric('MORPHEUS_NEO_TEMPERATURE', config.neo?.temperature, llmConfig.temperature),
121
+ max_tokens: resolveOptionalNumeric('MORPHEUS_NEO_MAX_TOKENS', config.neo?.max_tokens, neoMaxTokensFallback),
122
+ api_key: resolveApiKey(neoProvider, 'MORPHEUS_NEO_API_KEY', config.neo?.api_key || llmConfig.api_key),
123
+ base_url: neoBaseUrl || undefined,
124
+ context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
125
+ };
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
+ }
88
150
  // Apply precedence to audio config
89
151
  const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
90
152
  // AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
@@ -128,7 +190,9 @@ export class ConfigManager {
128
190
  agent: agentConfig,
129
191
  llm: llmConfig,
130
192
  sati: satiConfig,
193
+ neo: neoConfig,
131
194
  apoc: apocConfig,
195
+ trinity: trinityConfig,
132
196
  audio: audioConfig,
133
197
  channels: channelsConfig,
134
198
  ui: uiConfig,
@@ -185,4 +249,22 @@ export class ConfigManager {
185
249
  timeout_ms: 30_000
186
250
  };
187
251
  }
252
+ getNeoConfig() {
253
+ if (this.config.neo) {
254
+ return {
255
+ ...this.config.neo
256
+ };
257
+ }
258
+ // Fallback to main LLM config
259
+ return {
260
+ ...this.config.llm,
261
+ };
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
+ }
188
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 = [];
@@ -26,6 +26,8 @@ export const ApocConfigSchema = LLMConfigSchema.extend({
26
26
  working_dir: z.string().optional(),
27
27
  timeout_ms: z.number().int().positive().optional(),
28
28
  });
29
+ export const NeoConfigSchema = LLMConfigSchema;
30
+ export const TrinityConfigSchema = LLMConfigSchema;
29
31
  export const WebhookConfigSchema = z.object({
30
32
  telegram_notify_all: z.boolean().optional(),
31
33
  }).optional();
@@ -37,12 +39,19 @@ export const ConfigSchema = z.object({
37
39
  }).default(DEFAULT_CONFIG.agent),
38
40
  llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
39
41
  sati: SatiConfigSchema.optional(),
42
+ neo: NeoConfigSchema.optional(),
40
43
  apoc: ApocConfigSchema.optional(),
44
+ trinity: TrinityConfigSchema.optional(),
41
45
  webhooks: WebhookConfigSchema,
42
46
  audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
43
47
  memory: z.object({
44
48
  limit: z.number().int().positive().optional(),
45
49
  }).default(DEFAULT_CONFIG.memory),
50
+ runtime: z.object({
51
+ async_tasks: z.object({
52
+ enabled: z.boolean().default(DEFAULT_CONFIG.runtime?.async_tasks.enabled ?? true),
53
+ }).default(DEFAULT_CONFIG.runtime?.async_tasks ?? { enabled: true }),
54
+ }).optional(),
46
55
  channels: z.object({
47
56
  telegram: z.object({
48
57
  enabled: z.boolean().default(false),
@@ -64,7 +64,7 @@ export function createNetworkTools(ctx) {
64
64
  });
65
65
  }, {
66
66
  name: 'ping',
67
- description: 'Check if a host is reachable on a given port (TCP connect check).',
67
+ description: 'Preferred connectivity check tool. Verify if a host is reachable on a given port (TCP connect check). Use this instead of shell ping for routine reachability checks.',
68
68
  schema: z.object({
69
69
  host: z.string().describe('Hostname or IP'),
70
70
  port: z.number().int().optional().describe('Port to check, default 80'),