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
@@ -0,0 +1,246 @@
1
+ import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
+ import { tool } from "@langchain/core/tools";
3
+ import { z } from "zod";
4
+ import { ConfigManager } from "../config/manager.js";
5
+ import { ProviderFactory } from "./providers/factory.js";
6
+ import { ProviderError } from "./errors.js";
7
+ import { DisplayManager } from "./display.js";
8
+ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
9
+ import { DatabaseRegistry } from "./memory/trinity-db.js";
10
+ import { testConnection, introspectSchema, executeQuery } from "./trinity-connector.js";
11
+ import { updateTrinityDelegateToolDescription } from "./tools/trinity-tool.js";
12
+ /**
13
+ * Trinity is a subagent of Oracle specialized in database operations.
14
+ * It receives delegated tasks from Oracle, interprets them in natural language,
15
+ * generates appropriate queries (SQL or NoSQL), executes them, and returns results.
16
+ */
17
+ export class Trinity {
18
+ static instance = null;
19
+ static currentSessionId = undefined;
20
+ agent;
21
+ config;
22
+ display = DisplayManager.getInstance();
23
+ constructor(config) {
24
+ this.config = config || ConfigManager.getInstance().get();
25
+ }
26
+ static setSessionId(sessionId) {
27
+ Trinity.currentSessionId = sessionId;
28
+ }
29
+ static getInstance(config) {
30
+ if (!Trinity.instance) {
31
+ Trinity.instance = new Trinity(config);
32
+ }
33
+ return Trinity.instance;
34
+ }
35
+ static resetInstance() {
36
+ Trinity.instance = null;
37
+ }
38
+ static async refreshDelegateCatalog() {
39
+ const registry = DatabaseRegistry.getInstance();
40
+ const databases = registry.listDatabases();
41
+ updateTrinityDelegateToolDescription(databases);
42
+ }
43
+ buildTrinityTools() {
44
+ const registry = DatabaseRegistry.getInstance();
45
+ const listDatabases = tool(async () => {
46
+ const dbs = registry.listDatabases();
47
+ if (dbs.length === 0)
48
+ return 'No databases registered.';
49
+ return dbs.map((db) => {
50
+ const schema = db.schema_json
51
+ ? JSON.parse(db.schema_json)
52
+ : null;
53
+ let schemaSummary = 'schema not loaded';
54
+ if (schema) {
55
+ if (schema.databases) {
56
+ const totalTables = schema.databases.reduce((acc, d) => acc + d.tables.length, 0);
57
+ schemaSummary = `${schema.databases.length} databases, ${totalTables} tables total`;
58
+ }
59
+ else {
60
+ schemaSummary = schema.tables?.map((t) => t.name).join(', ') || 'no tables';
61
+ }
62
+ }
63
+ const updatedAt = db.schema_updated_at
64
+ ? new Date(db.schema_updated_at).toISOString()
65
+ : 'never';
66
+ return `[${db.id}] ${db.name} (${db.type}) — ${schemaSummary} — schema updated: ${updatedAt}`;
67
+ }).join('\n');
68
+ }, {
69
+ name: 'trinity_list_databases',
70
+ description: 'List all registered databases with their name, type, and schema summary.',
71
+ schema: z.object({}),
72
+ });
73
+ const getSchema = tool(async ({ database_id }) => {
74
+ const db = registry.getDatabase(database_id);
75
+ if (!db)
76
+ return `Database with id ${database_id} not found.`;
77
+ if (!db.schema_json)
78
+ return `No schema cached for database "${db.name}". Use trinity_refresh_schema first.`;
79
+ return `Schema for "${db.name}" (${db.type}):\n${db.schema_json}`;
80
+ }, {
81
+ name: 'trinity_get_schema',
82
+ description: 'Get the full cached schema of a registered database by its id.',
83
+ schema: z.object({
84
+ database_id: z.number().describe('The id of the database to get schema for'),
85
+ }),
86
+ });
87
+ const refreshSchema = tool(async ({ database_id }) => {
88
+ const db = registry.getDatabase(database_id);
89
+ if (!db)
90
+ return `Database with id ${database_id} not found.`;
91
+ try {
92
+ const schema = await introspectSchema(db);
93
+ registry.updateSchema(database_id, JSON.stringify(schema, null, 2));
94
+ if (schema.databases) {
95
+ const totalTables = schema.databases.reduce((acc, d) => acc + d.tables.length, 0);
96
+ const summary = schema.databases.map((d) => `${d.name}(${d.tables.length}t)`).join(', ');
97
+ return `Schema refreshed for "${db.name}". Found ${schema.databases.length} databases: ${summary}. Total: ${totalTables} tables.`;
98
+ }
99
+ return `Schema refreshed for "${db.name}". Tables: ${schema.tables.map((t) => t.name).join(', ')}`;
100
+ }
101
+ catch (err) {
102
+ return `Failed to refresh schema for "${db.name}": ${err.message}`;
103
+ }
104
+ }, {
105
+ name: 'trinity_refresh_schema',
106
+ description: 'Re-introspect and update the cached schema for a registered database.',
107
+ schema: z.object({
108
+ database_id: z.number().describe('The id of the database to refresh schema for'),
109
+ }),
110
+ });
111
+ const testConnectionTool = tool(async ({ database_id }) => {
112
+ const db = registry.getDatabase(database_id);
113
+ if (!db)
114
+ return `Database with id ${database_id} not found.`;
115
+ try {
116
+ const ok = await testConnection(db);
117
+ return ok
118
+ ? `Connection to "${db.name}" (${db.type}) successful.`
119
+ : `Connection to "${db.name}" (${db.type}) failed.`;
120
+ }
121
+ catch (err) {
122
+ return `Connection test failed: ${err.message}`;
123
+ }
124
+ }, {
125
+ name: 'trinity_test_connection',
126
+ description: 'Test connectivity to a registered database.',
127
+ schema: z.object({
128
+ database_id: z.number().describe('The id of the database to test'),
129
+ }),
130
+ });
131
+ const executeQueryTool = tool(async ({ database_id, query, params, }) => {
132
+ const db = registry.getDatabase(database_id);
133
+ if (!db)
134
+ return `Database with id ${database_id} not found.`;
135
+ try {
136
+ const result = await executeQuery(db, query, params);
137
+ if (result.rows.length === 0)
138
+ return `Query returned 0 rows. (rowCount: ${result.rowCount})`;
139
+ const preview = result.rows.slice(0, 50);
140
+ const json = JSON.stringify(preview, null, 2);
141
+ const note = result.rowCount > 50 ? `\n... (${result.rowCount} total rows, showing first 50)` : '';
142
+ return `Rows (${result.rowCount}):\n${json}${note}`;
143
+ }
144
+ catch (err) {
145
+ return `Query execution failed: ${err.message}`;
146
+ }
147
+ }, {
148
+ name: 'trinity_execute_query',
149
+ description: 'Execute a SQL query (PostgreSQL/MySQL/SQLite) or MongoDB JSON command on a registered database. ' +
150
+ 'For SQL: pass a standard SQL string. ' +
151
+ 'For MongoDB: pass a JSON string with { "collection": "name", "operation": "find|aggregate|countDocuments", "filter": {}, "options": {} }.',
152
+ schema: z.object({
153
+ database_id: z.number().describe('The id of the target database'),
154
+ query: z.string().describe('SQL query string or MongoDB JSON command'),
155
+ params: z.array(z.any()).optional().describe('Optional positional parameters for parameterized SQL queries'),
156
+ }),
157
+ });
158
+ return [listDatabases, getSchema, refreshSchema, testConnectionTool, executeQueryTool];
159
+ }
160
+ async initialize() {
161
+ const trinityConfig = this.config.trinity || this.config.llm;
162
+ const tools = this.buildTrinityTools();
163
+ this.display.log(`Trinity initialized with ${tools.length} tools.`, { source: 'Trinity' });
164
+ try {
165
+ this.agent = await ProviderFactory.createBare(trinityConfig, tools);
166
+ }
167
+ catch (err) {
168
+ throw new ProviderError(trinityConfig.provider, err, 'Trinity subagent initialization failed');
169
+ }
170
+ }
171
+ async execute(task, context, sessionId) {
172
+ if (!this.agent) {
173
+ await this.initialize();
174
+ }
175
+ const trinityConfig = this.config.trinity || this.config.llm;
176
+ this.display.log(`Executing delegated task: ${task.slice(0, 80)}...`, { source: 'Trinity' });
177
+ const registry = DatabaseRegistry.getInstance();
178
+ const databases = registry.listDatabases();
179
+ const dbSummary = databases.length > 0
180
+ ? databases.map((db) => {
181
+ const schema = db.schema_json ? JSON.parse(db.schema_json) : null;
182
+ const tables = schema?.tables?.map((t) => t.name).join(', ') || 'schema not loaded';
183
+ return `- [${db.id}] ${db.name} (${db.type}): ${tables}`;
184
+ }).join('\n')
185
+ : ' (no databases registered)';
186
+ const systemMessage = new SystemMessage(`
187
+ You are Trinity, a specialized database subagent within the Morpheus system.
188
+
189
+ You receive natural-language database tasks from Oracle and execute them using your available tools.
190
+
191
+ Registered databases:
192
+ ${dbSummary}
193
+
194
+ OPERATING RULES:
195
+ 1. Interpret the task in natural language and determine the correct query to execute.
196
+ 2. Always check the schema first using trinity_get_schema before writing queries.
197
+ 3. If the schema is missing, use trinity_refresh_schema to load it.
198
+ 4. Write safe, read-only queries by default. Only execute write operations if explicitly requested.
199
+ 5. Return results in a clear, structured format.
200
+ 6. If a query fails, try to diagnose the issue and suggest a fix.
201
+ 7. Never expose raw credentials or connection strings in your responses.
202
+ 8. Respond in the same language as the task.
203
+ 9. For SQL databases: write standard SQL appropriate to the database type.
204
+ 10. For MongoDB: format queries as JSON with { collection, operation, filter, options }.
205
+
206
+ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ''}
207
+ `);
208
+ const userMessage = new HumanMessage(task);
209
+ const messages = [systemMessage, userMessage];
210
+ try {
211
+ const response = await this.agent.invoke({ messages });
212
+ const lastMessage = response.messages[response.messages.length - 1];
213
+ const content = typeof lastMessage.content === 'string'
214
+ ? lastMessage.content
215
+ : JSON.stringify(lastMessage.content);
216
+ const targetSession = sessionId ?? Trinity.currentSessionId ?? 'trinity';
217
+ const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
218
+ try {
219
+ const persisted = new AIMessage(content);
220
+ persisted.usage_metadata =
221
+ lastMessage.usage_metadata ??
222
+ lastMessage.response_metadata?.usage ??
223
+ lastMessage.response_metadata?.tokenUsage ??
224
+ lastMessage.usage;
225
+ persisted.provider_metadata = {
226
+ provider: trinityConfig.provider,
227
+ model: trinityConfig.model,
228
+ };
229
+ await history.addMessage(persisted);
230
+ }
231
+ finally {
232
+ history.close();
233
+ }
234
+ this.display.log('Trinity task completed.', { source: 'Trinity' });
235
+ return content;
236
+ }
237
+ catch (err) {
238
+ throw new ProviderError(trinityConfig.provider, err, 'Trinity task execution failed');
239
+ }
240
+ }
241
+ async reload() {
242
+ this.config = ConfigManager.getInstance().get();
243
+ this.agent = undefined;
244
+ await this.initialize();
245
+ }
246
+ }
@@ -1,5 +1,7 @@
1
1
  import { WebhookRepository } from './repository.js';
2
+ import { TaskRepository } from '../tasks/repository.js';
2
3
  import { DisplayManager } from '../display.js';
4
+ const STALE_NOTIFICATION_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
3
5
  export class WebhookDispatcher {
4
6
  static telegramAdapter = null;
5
7
  static oracle = null;
@@ -35,17 +37,35 @@ export class WebhookDispatcher {
35
37
  }
36
38
  const message = this.buildPrompt(webhook.prompt, payload);
37
39
  try {
38
- await oracle.chat(message, undefined, false, {
40
+ const response = await oracle.chat(message, undefined, false, {
39
41
  origin_channel: 'webhook',
40
42
  session_id: `webhook-${webhook.id}`,
41
43
  origin_message_id: notificationId,
42
44
  });
43
- this.display.log(`Webhook "${webhook.name}" accepted and queued (notification: ${notificationId})`, { source: 'Webhooks', level: 'success' });
45
+ // Check whether Oracle delegated a task for this notification.
46
+ // If a task exists with this origin_message_id, TaskNotifier will update
47
+ // the notification when the task completes. If not (Oracle answered
48
+ // directly), mark it completed now with the direct response.
49
+ const taskRepo = TaskRepository.getInstance();
50
+ const delegatedTask = taskRepo.findTaskByOriginMessageId(notificationId);
51
+ if (delegatedTask) {
52
+ this.display.log(`Webhook "${webhook.name}" accepted and queued (notification: ${notificationId})`, { source: 'Webhooks', level: 'success' });
53
+ }
54
+ else {
55
+ repo.updateNotificationResult(notificationId, 'completed', response);
56
+ this.display.log(`Webhook "${webhook.name}" completed with direct response (notification: ${notificationId})`, { source: 'Webhooks', level: 'success' });
57
+ if (webhook.notification_channels.includes('telegram')) {
58
+ await this.sendTelegram(webhook.name, response, 'completed');
59
+ }
60
+ }
44
61
  }
45
62
  catch (err) {
46
63
  const result = `Execution error: ${err.message}`;
47
64
  this.display.log(`Webhook "${webhook.name}" failed: ${err.message}`, { source: 'Webhooks', level: 'error' });
48
65
  repo.updateNotificationResult(notificationId, 'failed', result);
66
+ if (webhook.notification_channels.includes('telegram')) {
67
+ await this.sendTelegram(webhook.name, result, 'failed');
68
+ }
49
69
  }
50
70
  }
51
71
  /**
@@ -63,6 +83,57 @@ ${payloadStr}
63
83
 
64
84
  Analyze the payload above and follow the instructions provided. Be concise and actionable in your response.`;
65
85
  }
86
+ /**
87
+ * Called at startup to re-dispatch webhook notifications that got stuck in
88
+ * 'pending' status (e.g. from a previous crash or from the direct-response
89
+ * bug). Skips notifications that already have an active task running.
90
+ */
91
+ static async recoverStale() {
92
+ const display = DisplayManager.getInstance();
93
+ const oracle = WebhookDispatcher.oracle;
94
+ if (!oracle) {
95
+ display.log('Webhook recovery skipped — Oracle not available.', {
96
+ source: 'Webhooks',
97
+ level: 'warning',
98
+ });
99
+ return;
100
+ }
101
+ const repo = WebhookRepository.getInstance();
102
+ const taskRepo = TaskRepository.getInstance();
103
+ const stale = repo.findStaleNotifications(STALE_NOTIFICATION_THRESHOLD_MS);
104
+ if (stale.length === 0)
105
+ return;
106
+ display.log(`Recovering ${stale.length} stale webhook notification(s)...`, {
107
+ source: 'Webhooks',
108
+ level: 'warning',
109
+ });
110
+ for (const notification of stale) {
111
+ // Skip if a task is still active for this notification
112
+ const activeTask = taskRepo.findTaskByOriginMessageId(notification.id);
113
+ if (activeTask && (activeTask.status === 'pending' || activeTask.status === 'running')) {
114
+ display.log(`Webhook notification ${notification.id} has active task ${activeTask.id} — skipping recovery.`, { source: 'Webhooks' });
115
+ continue;
116
+ }
117
+ const webhook = repo.getWebhookById(notification.webhook_id);
118
+ if (!webhook || !webhook.enabled) {
119
+ repo.updateNotificationResult(notification.id, 'failed', webhook ? 'Webhook was disabled.' : 'Webhook no longer exists.');
120
+ continue;
121
+ }
122
+ let payload;
123
+ try {
124
+ payload = JSON.parse(notification.payload);
125
+ }
126
+ catch {
127
+ payload = {};
128
+ }
129
+ display.log(`Re-dispatching stale notification ${notification.id} for webhook "${webhook.name}".`, { source: 'Webhooks' });
130
+ // Fire-and-forget re-dispatch (same pattern as the trigger endpoint)
131
+ const dispatcher = new WebhookDispatcher();
132
+ dispatcher.dispatch(webhook, payload, notification.id).catch((err) => {
133
+ display.log(`Recovery dispatch error for notification ${notification.id}: ${err.message}`, { source: 'Webhooks', level: 'error' });
134
+ });
135
+ }
136
+ }
66
137
  /**
67
138
  * Sends a formatted Telegram message to all allowed users.
68
139
  * Silently skips if the adapter is not connected.
@@ -180,6 +180,13 @@ export class WebhookRepository {
180
180
  const row = this.db.prepare('SELECT COUNT(*) as cnt FROM webhook_notifications WHERE read = 0').get();
181
181
  return row?.cnt ?? 0;
182
182
  }
183
+ findStaleNotifications(olderThanMs) {
184
+ const cutoff = Date.now() - olderThanMs;
185
+ const rows = this.db.prepare(`SELECT * FROM webhook_notifications
186
+ WHERE status = 'pending' AND created_at < ?
187
+ ORDER BY created_at ASC`).all(cutoff);
188
+ return rows.map(this.deserializeNotification);
189
+ }
183
190
  deserializeNotification(row) {
184
191
  return {
185
192
  id: row.id,