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.
- package/README.md +26 -7
- package/dist/channels/telegram.js +173 -0
- package/dist/cli/commands/restart.js +15 -14
- package/dist/cli/commands/start.js +13 -12
- package/dist/config/manager.js +31 -0
- package/dist/config/mcp-manager.js +19 -1
- package/dist/config/schemas.js +2 -0
- package/dist/http/api.js +222 -0
- package/dist/runtime/memory/session-embedding-worker.js +3 -3
- package/dist/runtime/memory/trinity-db.js +203 -0
- package/dist/runtime/neo.js +16 -26
- package/dist/runtime/oracle.js +16 -8
- package/dist/runtime/session-embedding-scheduler.js +1 -1
- package/dist/runtime/tasks/dispatcher.js +21 -0
- package/dist/runtime/tasks/worker.js +4 -1
- package/dist/runtime/tools/__tests__/tools.test.js +1 -3
- package/dist/runtime/tools/factory.js +1 -1
- package/dist/runtime/tools/index.js +1 -3
- package/dist/runtime/tools/morpheus-tools.js +742 -0
- package/dist/runtime/tools/neo-tool.js +19 -9
- package/dist/runtime/tools/trinity-tool.js +98 -0
- package/dist/runtime/trinity-connector.js +611 -0
- package/dist/runtime/trinity-crypto.js +52 -0
- package/dist/runtime/trinity.js +246 -0
- package/dist/ui/assets/index-DP2V4kRd.js +112 -0
- package/dist/ui/assets/index-mglRG5Zw.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +6 -1
- package/dist/runtime/tools/analytics-tools.js +0 -139
- package/dist/runtime/tools/config-tools.js +0 -64
- package/dist/runtime/tools/diagnostic-tools.js +0 -153
- package/dist/runtime/tools/task-query-tool.js +0 -76
- package/dist/ui/assets/index-20lLB1sM.js +0 -112
- 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
|
+
];
|