morpheus-cli 0.5.0 → 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 (35) 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 +13 -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/worker.js +4 -1
  16. package/dist/runtime/tools/__tests__/tools.test.js +1 -3
  17. package/dist/runtime/tools/factory.js +1 -1
  18. package/dist/runtime/tools/index.js +1 -3
  19. package/dist/runtime/tools/morpheus-tools.js +742 -0
  20. package/dist/runtime/tools/neo-tool.js +19 -9
  21. package/dist/runtime/tools/trinity-tool.js +98 -0
  22. package/dist/runtime/trinity-connector.js +611 -0
  23. package/dist/runtime/trinity-crypto.js +52 -0
  24. package/dist/runtime/trinity.js +246 -0
  25. package/dist/ui/assets/index-DP2V4kRd.js +112 -0
  26. package/dist/ui/assets/index-mglRG5Zw.css +1 -0
  27. package/dist/ui/index.html +2 -2
  28. package/dist/ui/sw.js +1 -1
  29. package/package.json +6 -1
  30. package/dist/runtime/tools/analytics-tools.js +0 -139
  31. package/dist/runtime/tools/config-tools.js +0 -64
  32. package/dist/runtime/tools/diagnostic-tools.js +0 -153
  33. package/dist/runtime/tools/task-query-tool.js +0 -76
  34. package/dist/ui/assets/index-20lLB1sM.js +0 -112
  35. package/dist/ui/assets/index-BJ56bRfs.css +0 -1
@@ -0,0 +1,742 @@
1
+ import { tool } from "@langchain/core/tools";
2
+ import { z } from "zod";
3
+ import { ConfigManager } from "../../config/manager.js";
4
+ import { promises as fsPromises } from "fs";
5
+ import path from "path";
6
+ import { homedir } from "os";
7
+ import Database from "better-sqlite3";
8
+ import { TaskRepository } from "../tasks/repository.js";
9
+ import { TaskRequestContext } from "../tasks/context.js";
10
+ // ─── Shared ───────────────────────────────────────────────────────────────────
11
+ const shortMemoryDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
12
+ // ─── Config ───────────────────────────────────────────────────────────────────
13
+ function setNestedValue(obj, dotPath, value) {
14
+ const keys = dotPath.split(".");
15
+ let curr = obj;
16
+ for (let i = 0; i < keys.length - 1; i++) {
17
+ if (!curr[keys[i]] || typeof curr[keys[i]] !== "object") {
18
+ curr[keys[i]] = {};
19
+ }
20
+ curr = curr[keys[i]];
21
+ }
22
+ curr[keys[keys.length - 1]] = value;
23
+ }
24
+ export const ConfigQueryTool = tool(async ({ key }) => {
25
+ try {
26
+ const configManager = ConfigManager.getInstance();
27
+ await configManager.load();
28
+ const config = configManager.get();
29
+ if (key) {
30
+ const value = key.split(".").reduce((obj, k) => (obj ? obj[k] : undefined), config);
31
+ return JSON.stringify({ [key]: value });
32
+ }
33
+ return JSON.stringify(config);
34
+ }
35
+ catch {
36
+ return JSON.stringify({ error: "Failed to query configuration" });
37
+ }
38
+ }, {
39
+ name: "morpheus_config_query",
40
+ description: "Queries current configuration values. Accepts an optional 'key' parameter (dot notation supported, e.g. 'llm.model') to get a specific configuration value, or no parameter to get all configuration values.",
41
+ schema: z.object({
42
+ key: z.string().optional(),
43
+ }),
44
+ });
45
+ export const ConfigUpdateTool = tool(async ({ updates }) => {
46
+ try {
47
+ const configManager = ConfigManager.getInstance();
48
+ await configManager.load();
49
+ const currentConfig = configManager.get();
50
+ const newConfig = { ...currentConfig };
51
+ for (const key in updates) {
52
+ setNestedValue(newConfig, key, updates[key]);
53
+ }
54
+ await configManager.save(newConfig);
55
+ return JSON.stringify({ success: true, message: "Configuration updated successfully" });
56
+ }
57
+ catch (error) {
58
+ return JSON.stringify({ error: `Failed to update configuration: ${error.message}` });
59
+ }
60
+ }, {
61
+ name: "morpheus_config_update",
62
+ description: "Updates configuration values with validation. Accepts an 'updates' object containing key-value pairs to update. Supports dot notation for nested fields (e.g. 'llm.model').",
63
+ schema: z.object({
64
+ updates: z.object({}).passthrough(),
65
+ }),
66
+ });
67
+ // ─── Diagnostics ──────────────────────────────────────────────────────────────
68
+ export const DiagnosticTool = tool(async () => {
69
+ try {
70
+ const timestamp = new Date().toISOString();
71
+ const components = {};
72
+ const morpheusRoot = path.join(homedir(), ".morpheus");
73
+ // Configuration
74
+ try {
75
+ const configManager = ConfigManager.getInstance();
76
+ await configManager.load();
77
+ const config = configManager.get();
78
+ const requiredFields = ["llm", "logging", "ui"];
79
+ const missingFields = requiredFields.filter((field) => !(field in config));
80
+ if (missingFields.length === 0) {
81
+ const sati = config.sati;
82
+ const apoc = config.apoc;
83
+ components.config = {
84
+ status: "healthy",
85
+ message: "Configuration is valid and complete",
86
+ details: {
87
+ oracleProvider: config.llm?.provider,
88
+ oracleModel: config.llm?.model,
89
+ satiProvider: sati?.provider ?? `${config.llm?.provider} (inherited)`,
90
+ satiModel: sati?.model ?? `${config.llm?.model} (inherited)`,
91
+ apocProvider: apoc?.provider ?? `${config.llm?.provider} (inherited)`,
92
+ apocModel: apoc?.model ?? `${config.llm?.model} (inherited)`,
93
+ apocWorkingDir: apoc?.working_dir ?? "not set",
94
+ uiEnabled: config.ui?.enabled,
95
+ uiPort: config.ui?.port,
96
+ },
97
+ };
98
+ }
99
+ else {
100
+ components.config = {
101
+ status: "warning",
102
+ message: `Missing required configuration fields: ${missingFields.join(", ")}`,
103
+ details: { missingFields },
104
+ };
105
+ }
106
+ }
107
+ catch (error) {
108
+ components.config = {
109
+ status: "error",
110
+ message: `Configuration error: ${error.message}`,
111
+ details: {},
112
+ };
113
+ }
114
+ // Short-term memory DB
115
+ try {
116
+ const dbPath = path.join(morpheusRoot, "memory", "short-memory.db");
117
+ await fsPromises.access(dbPath);
118
+ const stat = await fsPromises.stat(dbPath);
119
+ components.shortMemoryDb = {
120
+ status: "healthy",
121
+ message: "Short-memory database is accessible",
122
+ details: { path: dbPath, sizeBytes: stat.size },
123
+ };
124
+ }
125
+ catch (error) {
126
+ components.shortMemoryDb = {
127
+ status: "error",
128
+ message: `Short-memory DB not accessible: ${error.message}`,
129
+ details: {},
130
+ };
131
+ }
132
+ // Sati long-term memory DB
133
+ try {
134
+ const satiDbPath = path.join(morpheusRoot, "memory", "sati-memory.db");
135
+ await fsPromises.access(satiDbPath);
136
+ const stat = await fsPromises.stat(satiDbPath);
137
+ components.satiMemoryDb = {
138
+ status: "healthy",
139
+ message: "Sati memory database is accessible",
140
+ details: { path: satiDbPath, sizeBytes: stat.size },
141
+ };
142
+ }
143
+ catch {
144
+ components.satiMemoryDb = {
145
+ status: "warning",
146
+ message: "Sati memory database does not exist yet (no memories stored yet)",
147
+ details: {},
148
+ };
149
+ }
150
+ // LLM provider configured
151
+ try {
152
+ const configManager = ConfigManager.getInstance();
153
+ const config = configManager.get();
154
+ if (config.llm?.provider) {
155
+ components.network = {
156
+ status: "healthy",
157
+ message: `Oracle LLM provider configured: ${config.llm.provider}`,
158
+ details: { provider: config.llm.provider, model: config.llm.model },
159
+ };
160
+ }
161
+ else {
162
+ components.network = {
163
+ status: "warning",
164
+ message: "No Oracle LLM provider configured",
165
+ details: {},
166
+ };
167
+ }
168
+ }
169
+ catch (error) {
170
+ components.network = {
171
+ status: "error",
172
+ message: `Network check error: ${error.message}`,
173
+ details: {},
174
+ };
175
+ }
176
+ // Agent process
177
+ components.agent = {
178
+ status: "healthy",
179
+ message: "Agent is running (this tool is executing inside the agent process)",
180
+ details: { pid: process.pid, uptime: `${Math.floor(process.uptime())}s` },
181
+ };
182
+ // Logs directory
183
+ try {
184
+ const logsDir = path.join(morpheusRoot, "logs");
185
+ await fsPromises.access(logsDir);
186
+ components.logs = {
187
+ status: "healthy",
188
+ message: "Logs directory is accessible",
189
+ details: { path: logsDir },
190
+ };
191
+ }
192
+ catch {
193
+ components.logs = {
194
+ status: "warning",
195
+ message: "Logs directory not found (will be created on first log write)",
196
+ details: {},
197
+ };
198
+ }
199
+ return JSON.stringify({ timestamp, components });
200
+ }
201
+ catch (error) {
202
+ console.error("Error in DiagnosticTool:", error);
203
+ return JSON.stringify({ timestamp: new Date().toISOString(), error: "Failed to run diagnostics" });
204
+ }
205
+ }, {
206
+ name: "diagnostic_check",
207
+ description: "Performs system health diagnostics and returns a comprehensive report covering configuration (Oracle/Sati/Apoc), short-memory DB, Sati long-term memory DB, LLM provider, agent process, and logs directory.",
208
+ schema: z.object({}),
209
+ });
210
+ // ─── Analytics ────────────────────────────────────────────────────────────────
211
+ export const MessageCountTool = tool(async ({ timeRange }) => {
212
+ try {
213
+ const db = new Database(shortMemoryDbPath);
214
+ let query = "SELECT COUNT(*) as count FROM messages";
215
+ const params = [];
216
+ if (timeRange) {
217
+ query += " WHERE created_at BETWEEN ? AND ?";
218
+ params.push(new Date(timeRange.start).getTime());
219
+ params.push(new Date(timeRange.end).getTime());
220
+ }
221
+ const result = db.prepare(query).get(...params);
222
+ db.close();
223
+ return JSON.stringify(result.count);
224
+ }
225
+ catch (error) {
226
+ console.error("Error in MessageCountTool:", error);
227
+ return JSON.stringify({ error: `Failed to count messages: ${error.message}` });
228
+ }
229
+ }, {
230
+ name: "message_count",
231
+ description: "Returns count of stored messages. Accepts an optional 'timeRange' parameter with ISO date strings (start/end) for filtering.",
232
+ schema: z.object({
233
+ timeRange: z
234
+ .object({
235
+ start: z.string().describe("ISO date string, e.g. 2026-01-01T00:00:00Z"),
236
+ end: z.string().describe("ISO date string, e.g. 2026-12-31T23:59:59Z"),
237
+ })
238
+ .optional(),
239
+ }),
240
+ });
241
+ export const TokenUsageTool = tool(async ({ timeRange }) => {
242
+ try {
243
+ const db = new Database(shortMemoryDbPath);
244
+ let whereClause = "";
245
+ const params = [];
246
+ if (timeRange) {
247
+ whereClause = " WHERE created_at BETWEEN ? AND ?";
248
+ params.push(new Date(timeRange.start).getTime());
249
+ params.push(new Date(timeRange.end).getTime());
250
+ }
251
+ const row = db
252
+ .prepare(`SELECT
253
+ SUM(input_tokens) as inputTokens,
254
+ SUM(output_tokens) as outputTokens,
255
+ SUM(total_tokens) as totalTokens,
256
+ COALESCE(SUM(audio_duration_seconds), 0) as totalAudioSeconds
257
+ FROM messages${whereClause}`)
258
+ .get(...params);
259
+ const costRow = db
260
+ .prepare(`SELECT
261
+ SUM((COALESCE(m.input_tokens, 0) / 1000000.0) * p.input_price_per_1m
262
+ + (COALESCE(m.output_tokens, 0) / 1000000.0) * p.output_price_per_1m) as totalCost
263
+ FROM messages m
264
+ INNER JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
265
+ WHERE m.provider IS NOT NULL${whereClause ? whereClause.replace("WHERE", "AND") : ""}`)
266
+ .get(...params);
267
+ db.close();
268
+ return JSON.stringify({
269
+ inputTokens: row.inputTokens || 0,
270
+ outputTokens: row.outputTokens || 0,
271
+ totalTokens: row.totalTokens || 0,
272
+ totalAudioSeconds: row.totalAudioSeconds || 0,
273
+ estimatedCostUsd: costRow.totalCost ?? null,
274
+ timestamp: new Date().toISOString(),
275
+ });
276
+ }
277
+ catch (error) {
278
+ console.error("Error in TokenUsageTool:", error);
279
+ return JSON.stringify({ error: `Failed to get token usage: ${error.message}` });
280
+ }
281
+ }, {
282
+ name: "token_usage",
283
+ description: "Returns global token usage statistics including input/output tokens, total tokens, audio duration in seconds, and estimated cost in USD (when pricing is configured). Accepts an optional 'timeRange' parameter with ISO date strings for filtering.",
284
+ schema: z.object({
285
+ timeRange: z
286
+ .object({
287
+ start: z.string().describe("ISO date string, e.g. 2026-01-01T00:00:00Z"),
288
+ end: z.string().describe("ISO date string, e.g. 2026-12-31T23:59:59Z"),
289
+ })
290
+ .optional(),
291
+ }),
292
+ });
293
+ export const ProviderModelUsageTool = tool(async () => {
294
+ try {
295
+ const db = new Database(shortMemoryDbPath);
296
+ const query = `
297
+ SELECT
298
+ m.provider,
299
+ COALESCE(m.model, 'unknown') as model,
300
+ SUM(m.input_tokens) as totalInputTokens,
301
+ SUM(m.output_tokens) as totalOutputTokens,
302
+ SUM(m.total_tokens) as totalTokens,
303
+ COUNT(*) as messageCount,
304
+ COALESCE(SUM(m.audio_duration_seconds), 0) as totalAudioSeconds,
305
+ p.input_price_per_1m,
306
+ p.output_price_per_1m
307
+ FROM messages m
308
+ LEFT JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
309
+ WHERE m.provider IS NOT NULL
310
+ GROUP BY m.provider, COALESCE(m.model, 'unknown')
311
+ ORDER BY m.provider, m.model
312
+ `;
313
+ const rows = db.prepare(query).all();
314
+ db.close();
315
+ const results = rows.map((row) => {
316
+ const inputTokens = row.totalInputTokens || 0;
317
+ const outputTokens = row.totalOutputTokens || 0;
318
+ let estimatedCostUsd = null;
319
+ if (row.input_price_per_1m != null && row.output_price_per_1m != null) {
320
+ estimatedCostUsd =
321
+ (inputTokens / 1_000_000) * row.input_price_per_1m +
322
+ (outputTokens / 1_000_000) * row.output_price_per_1m;
323
+ }
324
+ return {
325
+ provider: row.provider,
326
+ model: row.model,
327
+ totalInputTokens: inputTokens,
328
+ totalOutputTokens: outputTokens,
329
+ totalTokens: row.totalTokens || 0,
330
+ messageCount: row.messageCount || 0,
331
+ totalAudioSeconds: row.totalAudioSeconds || 0,
332
+ estimatedCostUsd,
333
+ };
334
+ });
335
+ return JSON.stringify(results);
336
+ }
337
+ catch (error) {
338
+ console.error("Error in ProviderModelUsageTool:", error);
339
+ return JSON.stringify({ error: `Failed to get provider usage stats: ${error.message}` });
340
+ }
341
+ }, {
342
+ name: "provider_model_usage",
343
+ description: "Returns token usage statistics grouped by provider and model, including audio duration and estimated cost in USD (when pricing is configured).",
344
+ schema: z.object({}),
345
+ });
346
+ // ─── Tasks ────────────────────────────────────────────────────────────────────
347
+ function toTaskView(task) {
348
+ return {
349
+ id: task.id,
350
+ agent: task.agent,
351
+ status: task.status,
352
+ input: task.input,
353
+ output: task.output,
354
+ error: task.error,
355
+ session_id: task.session_id,
356
+ origin_channel: task.origin_channel,
357
+ created_at: task.created_at,
358
+ started_at: task.started_at,
359
+ finished_at: task.finished_at,
360
+ updated_at: task.updated_at,
361
+ };
362
+ }
363
+ export const TaskQueryTool = tool(async ({ task_id, limit, session_id, include_completed }) => {
364
+ try {
365
+ const repository = TaskRepository.getInstance();
366
+ if (task_id) {
367
+ const task = repository.getTaskById(task_id);
368
+ if (!task) {
369
+ return JSON.stringify({ found: false, query: { task_id }, message: "Task not found" });
370
+ }
371
+ return JSON.stringify({ found: true, query: { task_id }, task: toTaskView(task) });
372
+ }
373
+ const ctx = TaskRequestContext.get();
374
+ const targetSessionId = session_id ?? ctx?.session_id;
375
+ const requestedLimit = Math.max(1, Math.min(50, limit ?? 10));
376
+ const baseLimit = Math.max(requestedLimit * 5, 50);
377
+ const tasks = repository.listTasks({ session_id: targetSessionId, limit: baseLimit });
378
+ const filtered = tasks.filter((task) => (include_completed ? true : task.status !== "completed"));
379
+ const latest = filtered.slice(0, requestedLimit);
380
+ return JSON.stringify({
381
+ found: latest.length > 0,
382
+ query: {
383
+ task_id: null,
384
+ limit: requestedLimit,
385
+ session_id: targetSessionId ?? null,
386
+ include_completed: include_completed ?? false,
387
+ },
388
+ count: latest.length,
389
+ tasks: latest.map(toTaskView),
390
+ });
391
+ }
392
+ catch (error) {
393
+ return JSON.stringify({ found: false, error: error?.message ?? String(error) });
394
+ }
395
+ }, {
396
+ name: "task_query",
397
+ description: "Query task status directly from database without delegation. Supports lookup by task id, or latest tasks (default: only non-completed) for current session.",
398
+ schema: z.object({
399
+ task_id: z.string().uuid().optional().describe("Specific task id to fetch"),
400
+ limit: z
401
+ .number()
402
+ .int()
403
+ .min(1)
404
+ .max(50)
405
+ .optional()
406
+ .describe("Max number of tasks to return when task_id is not provided (default: 10)"),
407
+ session_id: z
408
+ .string()
409
+ .optional()
410
+ .describe("Optional session id filter; if omitted, uses current request session"),
411
+ include_completed: z
412
+ .boolean()
413
+ .optional()
414
+ .describe("Include completed tasks when listing latest tasks (default: false)"),
415
+ }),
416
+ });
417
+ // ─── MCP Management ───────────────────────────────────────────────────────────
418
+ export const McpListTool = tool(async () => {
419
+ try {
420
+ const { MCPManager } = await import("../../config/mcp-manager.js");
421
+ const servers = await MCPManager.listServers();
422
+ const result = servers.map((s) => ({
423
+ name: s.name,
424
+ enabled: s.enabled,
425
+ transport: s.config.transport,
426
+ ...(s.config.transport === "stdio"
427
+ ? { command: s.config.command, args: s.config.args }
428
+ : { url: s.config.url }),
429
+ }));
430
+ return JSON.stringify(result);
431
+ }
432
+ catch (error) {
433
+ return JSON.stringify({ error: `Failed to list MCP servers: ${error.message}` });
434
+ }
435
+ }, {
436
+ name: "mcp_list",
437
+ description: "Lists all registered MCP servers with their name, enabled status, transport type, and connection details.",
438
+ schema: z.object({}),
439
+ });
440
+ export const McpManageTool = tool(async ({ action, name, config }) => {
441
+ try {
442
+ const { MCPManager } = await import("../../config/mcp-manager.js");
443
+ const requireName = () => {
444
+ if (!name)
445
+ throw new Error(`"name" is required for action "${action}"`);
446
+ return name;
447
+ };
448
+ switch (action) {
449
+ case "add":
450
+ if (!config)
451
+ return JSON.stringify({ error: "config is required for add action" });
452
+ await MCPManager.addServer(requireName(), config);
453
+ return JSON.stringify({ success: true, message: `MCP server "${name}" added` });
454
+ case "update":
455
+ if (!config)
456
+ return JSON.stringify({ error: "config is required for update action" });
457
+ await MCPManager.updateServer(requireName(), config);
458
+ return JSON.stringify({ success: true, message: `MCP server "${name}" updated` });
459
+ case "delete":
460
+ await MCPManager.deleteServer(requireName());
461
+ return JSON.stringify({ success: true, message: `MCP server "${name}" deleted` });
462
+ case "enable":
463
+ await MCPManager.setServerEnabled(requireName(), true);
464
+ return JSON.stringify({ success: true, message: `MCP server "${name}" enabled` });
465
+ case "disable":
466
+ await MCPManager.setServerEnabled(requireName(), false);
467
+ return JSON.stringify({ success: true, message: `MCP server "${name}" disabled` });
468
+ case "reload":
469
+ await MCPManager.reloadAgents();
470
+ return JSON.stringify({ success: true, message: "MCP tools reloaded across Oracle, Neo, and Trinity" });
471
+ default:
472
+ return JSON.stringify({ error: `Unknown action: ${action}` });
473
+ }
474
+ }
475
+ catch (error) {
476
+ return JSON.stringify({ error: `MCP manage failed: ${error.message}` });
477
+ }
478
+ }, {
479
+ name: "mcp_manage",
480
+ description: "Manage MCP servers: add, update, delete, enable, disable, or reload (triggers a full tool reload across Oracle, Neo, and Trinity).",
481
+ schema: z.object({
482
+ action: z.enum(["add", "update", "delete", "enable", "disable", "reload"]),
483
+ name: z.string().optional().describe("MCP server name (required for all actions except reload)"),
484
+ config: z
485
+ .object({
486
+ transport: z.enum(["stdio", "http"]),
487
+ command: z.string().optional().describe("Required for stdio transport"),
488
+ args: z.array(z.string()).optional(),
489
+ env: z.record(z.string(), z.string()).optional(),
490
+ url: z.string().optional().describe("Required for http transport"),
491
+ headers: z.record(z.string(), z.string()).optional(),
492
+ })
493
+ .optional()
494
+ .describe("Server configuration (required for add/update)"),
495
+ }),
496
+ });
497
+ // ─── Webhook Management ───────────────────────────────────────────────────────
498
+ export const WebhookListTool = tool(async () => {
499
+ try {
500
+ const { WebhookRepository } = await import("../webhooks/repository.js");
501
+ const webhooks = WebhookRepository.getInstance().listWebhooks();
502
+ const result = webhooks.map((w) => ({
503
+ id: w.id,
504
+ name: w.name,
505
+ enabled: w.enabled,
506
+ notification_channels: w.notification_channels,
507
+ prompt: w.prompt.length > 100 ? w.prompt.slice(0, 100) + "…" : w.prompt,
508
+ trigger_count: w.trigger_count,
509
+ created_at: w.created_at,
510
+ last_triggered_at: w.last_triggered_at,
511
+ }));
512
+ return JSON.stringify(result);
513
+ }
514
+ catch (error) {
515
+ return JSON.stringify({ error: `Failed to list webhooks: ${error.message}` });
516
+ }
517
+ }, {
518
+ name: "webhook_list",
519
+ description: "Lists all registered webhooks with their name, enabled status, notification channels, and prompt (truncated to 100 chars). Does not include api_key.",
520
+ schema: z.object({}),
521
+ });
522
+ export const WebhookManageTool = tool(async ({ action, name, id, prompt, enabled, notification_channels }) => {
523
+ try {
524
+ const { WebhookRepository } = await import("../webhooks/repository.js");
525
+ const repo = WebhookRepository.getInstance();
526
+ const resolveId = () => {
527
+ if (id)
528
+ return id;
529
+ const wh = repo.getWebhookByName(name);
530
+ if (!wh)
531
+ throw new Error(`Webhook "${name}" not found`);
532
+ return wh.id;
533
+ };
534
+ switch (action) {
535
+ case "create": {
536
+ if (!prompt)
537
+ return JSON.stringify({ error: "prompt is required for create action" });
538
+ const wh = repo.createWebhook({
539
+ name,
540
+ prompt,
541
+ notification_channels: (notification_channels ?? ["ui"]),
542
+ });
543
+ return JSON.stringify({ success: true, id: wh.id, name: wh.name, api_key: wh.api_key });
544
+ }
545
+ case "update": {
546
+ const whId = resolveId();
547
+ const updated = repo.updateWebhook(whId, { name, prompt, enabled, notification_channels: notification_channels });
548
+ if (!updated)
549
+ return JSON.stringify({ error: "Webhook not found" });
550
+ return JSON.stringify({ success: true, id: updated.id, name: updated.name });
551
+ }
552
+ case "delete": {
553
+ const whId = resolveId();
554
+ const deleted = repo.deleteWebhook(whId);
555
+ return JSON.stringify({
556
+ success: deleted,
557
+ message: deleted ? `Webhook "${name}" deleted` : "Webhook not found",
558
+ });
559
+ }
560
+ default:
561
+ return JSON.stringify({ error: `Unknown action: ${action}` });
562
+ }
563
+ }
564
+ catch (error) {
565
+ return JSON.stringify({ error: `Webhook manage failed: ${error.message}` });
566
+ }
567
+ }, {
568
+ name: "webhook_manage",
569
+ description: "Manage webhooks: create, update, or delete a webhook. Create returns the api_key.",
570
+ schema: z.object({
571
+ action: z.enum(["create", "update", "delete"]),
572
+ name: z.string().describe("Webhook name"),
573
+ id: z.string().optional().describe("Webhook id (optional, resolved from name if omitted)"),
574
+ prompt: z.string().optional().describe("Instruction prompt for the webhook (required for create)"),
575
+ enabled: z.boolean().optional().describe("Enable or disable the webhook (for update)"),
576
+ notification_channels: z
577
+ .array(z.string())
578
+ .optional()
579
+ .describe("Notification channels, e.g. ['ui', 'telegram']"),
580
+ }),
581
+ });
582
+ // ─── Trinity Database Management ──────────────────────────────────────────────
583
+ export const TrinityDbListTool = tool(async () => {
584
+ try {
585
+ const { DatabaseRegistry } = await import("../memory/trinity-db.js");
586
+ const databases = DatabaseRegistry.getInstance().listDatabases();
587
+ const result = databases.map((db) => ({
588
+ id: db.id,
589
+ name: db.name,
590
+ type: db.type,
591
+ host: db.host,
592
+ port: db.port,
593
+ database_name: db.database_name,
594
+ username: db.username,
595
+ allow_read: db.allow_read,
596
+ allow_insert: db.allow_insert,
597
+ allow_update: db.allow_update,
598
+ allow_delete: db.allow_delete,
599
+ allow_ddl: db.allow_ddl,
600
+ schema_updated_at: db.schema_updated_at,
601
+ created_at: db.created_at,
602
+ }));
603
+ return JSON.stringify(result);
604
+ }
605
+ catch (error) {
606
+ return JSON.stringify({ error: `Failed to list Trinity databases: ${error.message}` });
607
+ }
608
+ }, {
609
+ name: "trinity_db_list",
610
+ description: "Lists all registered Trinity databases with their metadata (without passwords or connection strings).",
611
+ schema: z.object({}),
612
+ });
613
+ export const TrinityDbManageTool = tool(async ({ action, name, id, type, host, port, database_name, username, password, connection_string, allow_read, allow_insert, allow_update, allow_delete, allow_ddl, }) => {
614
+ try {
615
+ const { DatabaseRegistry } = await import("../memory/trinity-db.js");
616
+ const registry = DatabaseRegistry.getInstance();
617
+ const resolveId = () => {
618
+ if (id !== undefined)
619
+ return id;
620
+ const db = registry.getDatabaseByName(name);
621
+ if (!db)
622
+ throw new Error(`Database "${name}" not found`);
623
+ return db.id;
624
+ };
625
+ switch (action) {
626
+ case "register": {
627
+ if (!type)
628
+ return JSON.stringify({ error: "type is required for register action" });
629
+ const db = registry.createDatabase({
630
+ name,
631
+ type: type,
632
+ host,
633
+ port,
634
+ database_name,
635
+ username,
636
+ password,
637
+ connection_string,
638
+ allow_read,
639
+ allow_insert,
640
+ allow_update,
641
+ allow_delete,
642
+ allow_ddl,
643
+ });
644
+ return JSON.stringify({ success: true, id: db.id, name: db.name, type: db.type });
645
+ }
646
+ case "update": {
647
+ const dbId = resolveId();
648
+ const updated = registry.updateDatabase(dbId, {
649
+ name,
650
+ type: type,
651
+ host,
652
+ port,
653
+ database_name,
654
+ username,
655
+ password,
656
+ connection_string,
657
+ allow_read,
658
+ allow_insert,
659
+ allow_update,
660
+ allow_delete,
661
+ allow_ddl,
662
+ });
663
+ if (!updated)
664
+ return JSON.stringify({ error: "Database not found" });
665
+ return JSON.stringify({ success: true, id: updated.id, name: updated.name });
666
+ }
667
+ case "delete": {
668
+ const dbId = resolveId();
669
+ const deleted = registry.deleteDatabase(dbId);
670
+ return JSON.stringify({
671
+ success: deleted,
672
+ message: deleted ? `Database "${name}" deleted` : "Database not found",
673
+ });
674
+ }
675
+ case "test": {
676
+ const dbId = resolveId();
677
+ const db = registry.getDatabase(dbId);
678
+ if (!db)
679
+ return JSON.stringify({ error: `Database "${name}" not found` });
680
+ const { testConnection } = await import("../trinity-connector.js");
681
+ const ok = await testConnection(db);
682
+ return JSON.stringify({ status: ok ? "connected" : "failed", database: db.name });
683
+ }
684
+ case "refresh_schema": {
685
+ const dbId = resolveId();
686
+ const db = registry.getDatabase(dbId);
687
+ if (!db)
688
+ return JSON.stringify({ error: `Database "${name}" not found` });
689
+ const { introspectSchema } = await import("../trinity-connector.js");
690
+ const schema = await introspectSchema(db);
691
+ registry.updateSchema(dbId, JSON.stringify(schema));
692
+ const { Trinity } = await import("../trinity.js");
693
+ await Trinity.refreshDelegateCatalog();
694
+ return JSON.stringify({ success: true, message: `Schema refreshed for "${db.name}"` });
695
+ }
696
+ default:
697
+ return JSON.stringify({ error: `Unknown action: ${action}` });
698
+ }
699
+ }
700
+ catch (error) {
701
+ return JSON.stringify({ error: `Trinity DB manage failed: ${error.message}` });
702
+ }
703
+ }, {
704
+ name: "trinity_db_manage",
705
+ description: "Manage Trinity database registrations: register, update, delete, test connection, or refresh schema.",
706
+ schema: z.object({
707
+ action: z.enum(["register", "update", "delete", "test", "refresh_schema"]),
708
+ name: z.string().describe("Database registration name"),
709
+ id: z.number().int().optional().describe("Database id (optional, resolved from name if omitted)"),
710
+ type: z
711
+ .enum(["postgresql", "mysql", "sqlite", "mongodb"])
712
+ .optional()
713
+ .describe("Database type (required for register)"),
714
+ host: z.string().optional(),
715
+ port: z.number().int().optional(),
716
+ database_name: z.string().optional(),
717
+ username: z.string().optional(),
718
+ password: z.string().optional().describe("Password (stored encrypted)"),
719
+ connection_string: z.string().optional().describe("Full connection string (stored encrypted)"),
720
+ allow_read: z.boolean().optional(),
721
+ allow_insert: z.boolean().optional(),
722
+ allow_update: z.boolean().optional(),
723
+ allow_delete: z.boolean().optional(),
724
+ allow_ddl: z.boolean().optional(),
725
+ }),
726
+ });
727
+ // ─── Unified export ───────────────────────────────────────────────────────────
728
+ export const morpheusTools = [
729
+ ConfigQueryTool,
730
+ ConfigUpdateTool,
731
+ DiagnosticTool,
732
+ MessageCountTool,
733
+ TokenUsageTool,
734
+ ProviderModelUsageTool,
735
+ TaskQueryTool,
736
+ McpListTool,
737
+ McpManageTool,
738
+ WebhookListTool,
739
+ WebhookManageTool,
740
+ TrinityDbListTool,
741
+ TrinityDbManageTool,
742
+ ];