oh-memos-mcp 1.0.0
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/.env.example +27 -0
- package/README.md +71 -0
- package/dist/api-client.d.ts +22 -0
- package/dist/api-client.js +127 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +163 -0
- package/dist/cube-manager.d.ts +16 -0
- package/dist/cube-manager.js +357 -0
- package/dist/formatters.d.ts +16 -0
- package/dist/formatters.js +174 -0
- package/dist/handlers/admin.d.ts +13 -0
- package/dist/handlers/admin.js +331 -0
- package/dist/handlers/calendar.d.ts +8 -0
- package/dist/handlers/calendar.js +281 -0
- package/dist/handlers/graph.d.ts +11 -0
- package/dist/handlers/graph.js +502 -0
- package/dist/handlers/index.d.ts +7 -0
- package/dist/handlers/index.js +71 -0
- package/dist/handlers/memory.d.ts +12 -0
- package/dist/handlers/memory.js +270 -0
- package/dist/handlers/search.d.ts +11 -0
- package/dist/handlers/search.js +334 -0
- package/dist/handlers/utils.d.ts +22 -0
- package/dist/handlers/utils.js +75 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +18 -0
- package/dist/keyword-enhancer.d.ts +18 -0
- package/dist/keyword-enhancer.js +335 -0
- package/dist/memory-analysis.d.ts +7 -0
- package/dist/memory-analysis.js +45 -0
- package/dist/models.d.ts +21 -0
- package/dist/models.js +101 -0
- package/dist/query-processing.d.ts +20 -0
- package/dist/query-processing.js +265 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +114 -0
- package/dist/tools-registry.d.ts +303 -0
- package/dist/tools-registry.js +369 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +5 -0
- package/package.json +53 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Handlers
|
|
3
|
+
*
|
|
4
|
+
* memos_trace_path, memos_get_graph, memos_export_schema, memos_impact
|
|
5
|
+
*/
|
|
6
|
+
import { MEMOS_URL, MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, logger, registeredCubes } from "../config.js";
|
|
7
|
+
import { fetchWithTimeout } from "../api-client.js";
|
|
8
|
+
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
|
+
import { detectQueryIntent, extractMemoriesFromResponse, getGraphsForIntent, getIntentDescription, } from "../query-processing.js";
|
|
10
|
+
import { ERR_NEO4J_CONFIG, ERR_PARAM_MISSING, apiErrorResponse, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Neo4j Helper
|
|
13
|
+
// ============================================================================
|
|
14
|
+
function neo4jAuthHeader() {
|
|
15
|
+
return `Basic ${Buffer.from(`${NEO4J_USER}:${NEO4J_PASSWORD}`).toString("base64")}`;
|
|
16
|
+
}
|
|
17
|
+
async function neo4jQuery(cypher, parameters) {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetchWithTimeout(NEO4J_HTTP_URL, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
Authorization: neo4jAuthHeader(),
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
statements: [{ statement: cypher, parameters }],
|
|
27
|
+
}),
|
|
28
|
+
timeoutMs: 15,
|
|
29
|
+
});
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
return { ok: response.ok, data, status: response.status };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.error(`Neo4j query error: ${err}`);
|
|
35
|
+
return { ok: false, data: null, status: 0 };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// memos_trace_path
|
|
40
|
+
// ============================================================================
|
|
41
|
+
export async function handleMemosTracePath(arguments_) {
|
|
42
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
43
|
+
const sourceId = String(arguments_.source_id ?? "");
|
|
44
|
+
const targetId = String(arguments_.target_id ?? "");
|
|
45
|
+
const maxDepth = Math.min(Number(arguments_.max_depth ?? 3), 10);
|
|
46
|
+
if (!sourceId || !targetId) {
|
|
47
|
+
return errorResponse("Both source_id and target_id are required", ERR_PARAM_MISSING, [
|
|
48
|
+
"Get node IDs from memos_search or memos_get_graph",
|
|
49
|
+
'`memos_trace_path(source_id="uuid-1", target_id="uuid-2")`',
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
53
|
+
if (!regSuccess)
|
|
54
|
+
return cubeRegistrationError(cubeId, regError);
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetchWithTimeout(`${MEMOS_URL}/product/graph/trace_path`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
user_id: MEMOS_USER,
|
|
61
|
+
source_id: sourceId,
|
|
62
|
+
target_id: targetId,
|
|
63
|
+
max_depth: maxDepth,
|
|
64
|
+
include_all_paths: false,
|
|
65
|
+
mem_cube_id: cubeId,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
if (response.ok) {
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
if (data.code === 200) {
|
|
71
|
+
const traceData = data.data ?? {};
|
|
72
|
+
const found = traceData.path_found ?? traceData.found ?? false;
|
|
73
|
+
const paths = traceData.paths ?? [];
|
|
74
|
+
const sourceNode = traceData.source ?? {};
|
|
75
|
+
const targetNode = traceData.target ?? {};
|
|
76
|
+
const results = ["## 🔗 Path Trace Results", ""];
|
|
77
|
+
if (sourceNode.memory)
|
|
78
|
+
results.push(`**Source**: ${String(sourceNode.memory).slice(0, 80)}...`);
|
|
79
|
+
if (targetNode.memory)
|
|
80
|
+
results.push(`**Target**: ${String(targetNode.memory).slice(0, 80)}...`);
|
|
81
|
+
results.push("");
|
|
82
|
+
if (!found) {
|
|
83
|
+
results.push(`*No path found within ${maxDepth} hops.*`, "");
|
|
84
|
+
results.push("Suggestions:");
|
|
85
|
+
results.push("- Try increasing max_depth (up to 10)");
|
|
86
|
+
results.push("- Verify the node IDs are correct");
|
|
87
|
+
results.push("- The nodes may not be connected in the graph");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
for (let i = 0; i < paths.length; i++) {
|
|
91
|
+
const path = paths[i];
|
|
92
|
+
const length = path.length ?? 0;
|
|
93
|
+
const nodes = path.nodes ?? [];
|
|
94
|
+
const edges = path.edges ?? [];
|
|
95
|
+
// If API returns empty nodes, fall back to Neo4j
|
|
96
|
+
if (nodes.length === 0 && NEO4J_HTTP_URL && NEO4J_USER && NEO4J_PASSWORD) {
|
|
97
|
+
throw new Error("API returned empty path nodes, falling back to Neo4j");
|
|
98
|
+
}
|
|
99
|
+
results.push(`### Path ${i + 1} (Length: ${length})`, "", "```");
|
|
100
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
101
|
+
const node = nodes[j];
|
|
102
|
+
const nodeMem = String(node.memory ?? "").slice(0, 60);
|
|
103
|
+
results.push(`[${j + 1}] ${nodeMem}...`);
|
|
104
|
+
if (j < edges.length) {
|
|
105
|
+
const edge = edges[j];
|
|
106
|
+
results.push(" │");
|
|
107
|
+
results.push(` └── ${edge.type ?? "UNKNOWN"} ──>`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
results.push("```", "");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [{ type: "text", text: results.join("\n") }];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
return apiErrorResponse("Trace path", String(data.message ?? "Unknown error"));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
throw new Error(`HTTP ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.warning(`Falling back to direct Neo4j query: ${err}`);
|
|
125
|
+
if (!NEO4J_HTTP_URL || !NEO4J_USER || !NEO4J_PASSWORD) {
|
|
126
|
+
return errorResponse("Neo4j configuration missing", ERR_NEO4J_CONFIG, [
|
|
127
|
+
"Set NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD in .env",
|
|
128
|
+
"Example: NEO4J_HTTP_URL=http://localhost:7474/db/neo4j/tx/commit",
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
const cypher = `
|
|
132
|
+
MATCH (source:Memory), (target:Memory)
|
|
133
|
+
WHERE source.id = $source_id AND target.id = $target_id
|
|
134
|
+
MATCH path = shortestPath((source)-[*1..${maxDepth}]-(target))
|
|
135
|
+
RETURN [n IN nodes(path) | {id: n.id, memory: n.memory}] AS nodes,
|
|
136
|
+
[r IN relationships(path) | {type: type(r)}] AS rels
|
|
137
|
+
LIMIT 1
|
|
138
|
+
`;
|
|
139
|
+
const { ok, data, status } = await neo4jQuery(cypher, { source_id: sourceId, target_id: targetId });
|
|
140
|
+
const results = ["## 🔗 Path Trace (Direct Query)", ""];
|
|
141
|
+
if (ok && data) {
|
|
142
|
+
const rows = data.results?.[0]?.data ?? [];
|
|
143
|
+
if (rows.length > 0) {
|
|
144
|
+
const row = rows[0].row ?? [[], []];
|
|
145
|
+
const nodes = row[0] ?? [];
|
|
146
|
+
const rels = row[1] ?? [];
|
|
147
|
+
results.push("```");
|
|
148
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
149
|
+
const nodeMem = String(nodes[j].memory ?? "").slice(0, 60);
|
|
150
|
+
results.push(`[${j + 1}] ${nodeMem}...`);
|
|
151
|
+
if (j < rels.length) {
|
|
152
|
+
results.push(` └── ${rels[j].type ?? "?"} ──>`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
results.push("```");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
results.push(`*No path found within ${maxDepth} hops.*`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
results.push(`*Neo4j query error: ${status}*`);
|
|
163
|
+
}
|
|
164
|
+
return [{ type: "text", text: results.join("\n") }];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// memos_get_graph
|
|
169
|
+
// ============================================================================
|
|
170
|
+
export async function handleMemosGetGraph(arguments_) {
|
|
171
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
172
|
+
const query = String(arguments_.query ?? "");
|
|
173
|
+
const intent = detectQueryIntent(query);
|
|
174
|
+
const targetEdgeTypes = getGraphsForIntent(intent);
|
|
175
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
176
|
+
if (!regSuccess)
|
|
177
|
+
return cubeRegistrationError(cubeId, regError);
|
|
178
|
+
if (!NEO4J_HTTP_URL || !NEO4J_USER || !NEO4J_PASSWORD) {
|
|
179
|
+
return errorResponse("Neo4j configuration missing", ERR_NEO4J_CONFIG, [
|
|
180
|
+
"Set NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD in .env",
|
|
181
|
+
"Example: NEO4J_HTTP_URL=http://localhost:7474/db/neo4j/tx/commit",
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
// Search for relevant memories first
|
|
185
|
+
let memories = [];
|
|
186
|
+
try {
|
|
187
|
+
const searchResponse = await fetchWithTimeout(`${MEMOS_URL}/search`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
user_id: MEMOS_USER,
|
|
192
|
+
query,
|
|
193
|
+
install_cube_ids: [cubeId],
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
if (searchResponse.ok) {
|
|
197
|
+
const data = await searchResponse.json();
|
|
198
|
+
if (data.code === 200) {
|
|
199
|
+
memories = extractMemoriesFromResponse(data.data ?? {});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Re-register and retry
|
|
203
|
+
registeredCubes.delete(cubeId);
|
|
204
|
+
const [retrySuccess] = await ensureCubeRegistered(cubeId, true);
|
|
205
|
+
if (retrySuccess) {
|
|
206
|
+
const retrySearch = await fetchWithTimeout(`${MEMOS_URL}/search`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({ user_id: MEMOS_USER, query, install_cube_ids: [cubeId] }),
|
|
210
|
+
});
|
|
211
|
+
if (retrySearch.ok) {
|
|
212
|
+
const retryData = await retrySearch.json();
|
|
213
|
+
if (retryData.code === 200) {
|
|
214
|
+
memories = extractMemoriesFromResponse(retryData.data ?? {});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
logger.warning(`Graph search failed: ${err}`);
|
|
223
|
+
}
|
|
224
|
+
// Build Cypher query
|
|
225
|
+
const edgeTypesCypher = targetEdgeTypes.join("|");
|
|
226
|
+
const memIds = memories
|
|
227
|
+
.slice(0, 10)
|
|
228
|
+
.map((m) => m.id ?? m.metadata?.id)
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
let cypher;
|
|
231
|
+
let params;
|
|
232
|
+
if (memIds.length > 0) {
|
|
233
|
+
const idList = memIds.map((id) => `"${id}"`).join(", ");
|
|
234
|
+
cypher = `
|
|
235
|
+
MATCH (a)-[r:${edgeTypesCypher}]->(b)
|
|
236
|
+
WHERE a.id IN [${idList}] OR b.id IN [${idList}]
|
|
237
|
+
RETURN a.id as source_id, a.memory as source_memory,
|
|
238
|
+
type(r) as relation_type,
|
|
239
|
+
b.id as target_id, b.memory as target_memory
|
|
240
|
+
LIMIT 20
|
|
241
|
+
`;
|
|
242
|
+
params = {};
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const firstWord = query.split(/\s+/)[0] ?? query;
|
|
246
|
+
cypher = `
|
|
247
|
+
MATCH (a)-[r:${edgeTypesCypher}]->(b)
|
|
248
|
+
WHERE a.memory CONTAINS $keyword OR b.memory CONTAINS $keyword
|
|
249
|
+
RETURN a.id as source_id, a.memory as source_memory,
|
|
250
|
+
type(r) as relation_type,
|
|
251
|
+
b.id as target_id, b.memory as target_memory
|
|
252
|
+
LIMIT 20
|
|
253
|
+
`;
|
|
254
|
+
params = { keyword: firstWord };
|
|
255
|
+
}
|
|
256
|
+
const { ok, data, status } = await neo4jQuery(cypher, params);
|
|
257
|
+
const results = [];
|
|
258
|
+
const intentDesc = getIntentDescription(intent);
|
|
259
|
+
results.push(`## 🧠 Knowledge Graph: ${cubeId}`);
|
|
260
|
+
results.push(`Query: \`${query}\` | ${intentDesc}`);
|
|
261
|
+
results.push(`*Filtering edges: ${targetEdgeTypes.join(", ")}*`);
|
|
262
|
+
results.push("");
|
|
263
|
+
if (memories.length > 0) {
|
|
264
|
+
results.push("### 📝 Related Memories", "");
|
|
265
|
+
for (let i = 0; i < Math.min(memories.length, 5); i++) {
|
|
266
|
+
const memory = memories[i].memory ?? "";
|
|
267
|
+
const firstLine = memory.split("\n")[0].slice(0, 100);
|
|
268
|
+
results.push(`${i + 1}. ${firstLine}`);
|
|
269
|
+
}
|
|
270
|
+
results.push("");
|
|
271
|
+
}
|
|
272
|
+
if (ok && data) {
|
|
273
|
+
const rows = data.results?.[0]?.data ?? [];
|
|
274
|
+
if (rows.length > 0) {
|
|
275
|
+
results.push("### 🔗 Relationships", "```");
|
|
276
|
+
for (const row of rows) {
|
|
277
|
+
const r = row.row ?? [];
|
|
278
|
+
if (r.length >= 5) {
|
|
279
|
+
const sourceMem = String(r[1] ?? "").slice(0, 50);
|
|
280
|
+
const relType = String(r[2] ?? "UNKNOWN");
|
|
281
|
+
const targetMem = String(r[4] ?? "").slice(0, 50);
|
|
282
|
+
let arrow;
|
|
283
|
+
if (relType === "CAUSE")
|
|
284
|
+
arrow = "──CAUSE──>";
|
|
285
|
+
else if (relType === "RELATE")
|
|
286
|
+
arrow = "──RELATE──";
|
|
287
|
+
else if (relType === "CONFLICT")
|
|
288
|
+
arrow = "══CONFLICT══";
|
|
289
|
+
else
|
|
290
|
+
arrow = `──${relType}──>`;
|
|
291
|
+
results.push(`[${sourceMem}...]`);
|
|
292
|
+
results.push(` ${arrow}`);
|
|
293
|
+
results.push(`[${targetMem}...]`);
|
|
294
|
+
results.push("");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
results.push("```");
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
results.push("*No relationships found for this query.*");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
results.push(`*Neo4j query error: ${status}*`);
|
|
305
|
+
}
|
|
306
|
+
return [{ type: "text", text: results.join("\n") }];
|
|
307
|
+
}
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// memos_export_schema
|
|
310
|
+
// ============================================================================
|
|
311
|
+
export async function handleMemosExportSchema(arguments_) {
|
|
312
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
313
|
+
const sampleSize = Math.min(Math.max(Number(arguments_.sample_size ?? 100), 10), 1000);
|
|
314
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
315
|
+
if (!regSuccess)
|
|
316
|
+
return cubeRegistrationError(cubeId, regError);
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetchWithTimeout(`${MEMOS_URL}/graph/schema`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: { "Content-Type": "application/json" },
|
|
321
|
+
body: JSON.stringify({
|
|
322
|
+
user_id: MEMOS_USER,
|
|
323
|
+
mem_cube_id: cubeId,
|
|
324
|
+
sample_size: sampleSize,
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
if (response.ok) {
|
|
328
|
+
const data = await response.json();
|
|
329
|
+
if (data.code === 200) {
|
|
330
|
+
const schema = data.data ?? {};
|
|
331
|
+
const results = [];
|
|
332
|
+
results.push("## 📊 Knowledge Graph Schema", "");
|
|
333
|
+
results.push("### Overview");
|
|
334
|
+
results.push(`- **Total Nodes**: ${schema.total_nodes ?? 0}`);
|
|
335
|
+
results.push(`- **Total Edges**: ${schema.total_edges ?? 0}`);
|
|
336
|
+
results.push(`- **Avg Connections/Node**: ${Number(schema.avg_connections_per_node ?? 0).toFixed(2)}`);
|
|
337
|
+
results.push(`- **Max Connections**: ${schema.max_connections ?? 0}`);
|
|
338
|
+
results.push(`- **Orphan Nodes**: ${schema.orphan_node_count ?? 0}`);
|
|
339
|
+
results.push("");
|
|
340
|
+
const timeRange = schema.time_range ?? {};
|
|
341
|
+
if (timeRange.earliest || timeRange.latest) {
|
|
342
|
+
results.push("### Time Range");
|
|
343
|
+
if (timeRange.earliest)
|
|
344
|
+
results.push(`- Earliest: ${timeRange.earliest}`);
|
|
345
|
+
if (timeRange.latest)
|
|
346
|
+
results.push(`- Latest: ${timeRange.latest}`);
|
|
347
|
+
results.push("");
|
|
348
|
+
}
|
|
349
|
+
const edgeDist = schema.edge_type_distribution ?? {};
|
|
350
|
+
if (Object.keys(edgeDist).length > 0) {
|
|
351
|
+
results.push("### Relationship Types");
|
|
352
|
+
for (const [t, c] of Object.entries(edgeDist).sort((a, b) => b[1] - a[1])) {
|
|
353
|
+
results.push(`- **${t}**: ${c}`);
|
|
354
|
+
}
|
|
355
|
+
results.push("");
|
|
356
|
+
}
|
|
357
|
+
const memDist = schema.memory_type_distribution ?? {};
|
|
358
|
+
if (Object.keys(memDist).length > 0) {
|
|
359
|
+
results.push("### Memory Types");
|
|
360
|
+
for (const [t, c] of Object.entries(memDist).sort((a, b) => b[1] - a[1])) {
|
|
361
|
+
results.push(`- ${t}: ${c}`);
|
|
362
|
+
}
|
|
363
|
+
results.push("");
|
|
364
|
+
}
|
|
365
|
+
const tagFreq = schema.tag_frequency ?? {};
|
|
366
|
+
const tagItems = Object.entries(tagFreq).slice(0, 10);
|
|
367
|
+
if (tagItems.length > 0) {
|
|
368
|
+
results.push("### Top Tags");
|
|
369
|
+
for (const [tag, count] of tagItems) {
|
|
370
|
+
results.push(`- \`${tag}\`: ${count}`);
|
|
371
|
+
}
|
|
372
|
+
results.push("");
|
|
373
|
+
}
|
|
374
|
+
results.push("### Health Assessment");
|
|
375
|
+
const totalNodes = Number(schema.total_nodes ?? 0);
|
|
376
|
+
const orphanCount = Number(schema.orphan_node_count ?? 0);
|
|
377
|
+
if (totalNodes > 0) {
|
|
378
|
+
const orphanRatio = orphanCount / totalNodes;
|
|
379
|
+
if (orphanRatio > 0.5)
|
|
380
|
+
results.push("⚠️ High orphan ratio - many memories are not connected");
|
|
381
|
+
else if (orphanRatio > 0.2)
|
|
382
|
+
results.push("📋 Moderate orphan ratio");
|
|
383
|
+
else
|
|
384
|
+
results.push("✅ Good connectivity - memories are well connected");
|
|
385
|
+
}
|
|
386
|
+
const avgConn = Number(schema.avg_connections_per_node ?? 0);
|
|
387
|
+
if (avgConn < 1)
|
|
388
|
+
results.push("⚠️ Low average connections - consider enriching relationships");
|
|
389
|
+
else if (avgConn > 5)
|
|
390
|
+
results.push("✅ Rich relationships - good knowledge graph density");
|
|
391
|
+
return [{ type: "text", text: results.join("\n") }];
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
return apiErrorResponse("Schema export", String(data.message ?? "Unknown error"));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
return apiErrorResponse("Schema export", `HTTP ${response.status}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
return apiErrorResponse("Schema export", String(err));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// memos_impact
|
|
407
|
+
// ============================================================================
|
|
408
|
+
export async function handleMemosImpact(arguments_) {
|
|
409
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
410
|
+
const memoryId = String(arguments_.memory_id ?? "");
|
|
411
|
+
const maxDepth = Math.min(Math.max(Number(arguments_.max_depth ?? 3), 1), 6);
|
|
412
|
+
if (!memoryId) {
|
|
413
|
+
return errorResponse("memory_id is required", ERR_PARAM_MISSING, [
|
|
414
|
+
"Get a memory_id from memos_search or memos_get_graph first",
|
|
415
|
+
'`memos_impact(memory_id="uuid-here")`',
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
if (!NEO4J_HTTP_URL || !NEO4J_USER || !NEO4J_PASSWORD) {
|
|
419
|
+
return errorResponse("Neo4j configuration missing", ERR_NEO4J_CONFIG, [
|
|
420
|
+
"Set NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD in .env",
|
|
421
|
+
"Example: NEO4J_HTTP_URL=http://localhost:7474/db/neo4j/tx/commit",
|
|
422
|
+
]);
|
|
423
|
+
}
|
|
424
|
+
const cypher = `
|
|
425
|
+
MATCH (source:Memory {id: $source_id})-[:CAUSE|FOLLOWS*1..${maxDepth}]->(node:Memory)
|
|
426
|
+
WITH DISTINCT source, node
|
|
427
|
+
MATCH path = shortestPath((source)-[:CAUSE|FOLLOWS*1..${maxDepth}]->(node))
|
|
428
|
+
RETURN node.id AS id, node.key AS key, node.memory AS memory,
|
|
429
|
+
length(path) AS depth
|
|
430
|
+
ORDER BY depth ASC
|
|
431
|
+
LIMIT 30
|
|
432
|
+
`;
|
|
433
|
+
try {
|
|
434
|
+
const { ok, data, status } = await neo4jQuery(cypher, { source_id: memoryId });
|
|
435
|
+
if (!ok) {
|
|
436
|
+
return apiErrorResponse("Impact analysis", `Neo4j HTTP ${status}`);
|
|
437
|
+
}
|
|
438
|
+
if (!data) {
|
|
439
|
+
return apiErrorResponse("Impact analysis", "No data returned");
|
|
440
|
+
}
|
|
441
|
+
const errors = data.errors ?? [];
|
|
442
|
+
if (errors.length > 0) {
|
|
443
|
+
const errMsg = String(errors[0].message ?? "Unknown Neo4j error");
|
|
444
|
+
return apiErrorResponse("Impact analysis", errMsg);
|
|
445
|
+
}
|
|
446
|
+
const rows = data.results?.[0]?.data ?? [];
|
|
447
|
+
if (rows.length === 0) {
|
|
448
|
+
return [{ type: "text", text: "No forward impact found — this memory has no CAUSE or FOLLOWS successors." }];
|
|
449
|
+
}
|
|
450
|
+
// Group by depth
|
|
451
|
+
const depthGroups = {};
|
|
452
|
+
for (const row of rows) {
|
|
453
|
+
const r = row.row ?? [];
|
|
454
|
+
if (r.length >= 4) {
|
|
455
|
+
const depth = Number(r[3]);
|
|
456
|
+
if (!depthGroups[depth])
|
|
457
|
+
depthGroups[depth] = [];
|
|
458
|
+
depthGroups[depth].push({
|
|
459
|
+
id: String(r[0] ?? ""),
|
|
460
|
+
key: String(r[1] ?? ""),
|
|
461
|
+
memory: String(r[2] ?? ""),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const totalCount = Object.values(depthGroups).reduce((sum, items) => sum + items.length, 0);
|
|
466
|
+
const maxHop = Math.max(...Object.keys(depthGroups).map(Number));
|
|
467
|
+
const results = [
|
|
468
|
+
"## Impact Analysis",
|
|
469
|
+
"",
|
|
470
|
+
`**Blast Radius: ${totalCount} downstream memories across ${maxHop} hop(s)**`,
|
|
471
|
+
"",
|
|
472
|
+
];
|
|
473
|
+
const depthLabels = {
|
|
474
|
+
1: "Direct Impact",
|
|
475
|
+
2: "Indirect Impact",
|
|
476
|
+
};
|
|
477
|
+
for (const depth of Object.keys(depthGroups).map(Number).sort((a, b) => a - b)) {
|
|
478
|
+
const items = depthGroups[depth];
|
|
479
|
+
const label = depthLabels[depth] ?? `Downstream (hop ${depth})`;
|
|
480
|
+
results.push(`### ${label} (${items.length} node${items.length !== 1 ? "s" : ""})`, "");
|
|
481
|
+
for (let i = 0; i < Math.min(items.length, 8); i++) {
|
|
482
|
+
const item = items[i];
|
|
483
|
+
const memPreview = item.memory.split("\n")[0].slice(0, 100);
|
|
484
|
+
if (item.key) {
|
|
485
|
+
results.push(`- **${item.key}**: ${memPreview}`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
results.push(`- ${memPreview}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (items.length > 8) {
|
|
492
|
+
results.push(`- ... and ${items.length - 8} more`);
|
|
493
|
+
}
|
|
494
|
+
results.push("");
|
|
495
|
+
}
|
|
496
|
+
return [{ type: "text", text: results.join("\n") }];
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
return apiErrorResponse("Impact analysis", String(err));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Handler Dispatcher
|
|
3
|
+
*/
|
|
4
|
+
import type { TextContent } from "../types.js";
|
|
5
|
+
export declare function dispatchTool(name: string, arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
6
|
+
export declare function handleApiUnreachable(): Promise<TextContent[]>;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Handler Dispatcher
|
|
3
|
+
*/
|
|
4
|
+
import { handleMemosSave, handleMemosList, handleMemosGet, handleMemosGetStats } from "./memory.js";
|
|
5
|
+
import { handleMemosSearch, handleMemosSearchContext, handleMemosSuggest, handleMemosContextResume } from "./search.js";
|
|
6
|
+
import { handleMemosTracePath, handleMemosGetGraph, handleMemosExportSchema, handleMemosImpact } from "./graph.js";
|
|
7
|
+
import { handleMemosCalendar } from "./calendar.js";
|
|
8
|
+
import { handleMemosListCubes, handleMemosRegisterCube, handleMemosCreateUser, handleMemosValidateCubes, handleMemosDelete, } from "./admin.js";
|
|
9
|
+
import { MEMOS_URL } from "../config.js";
|
|
10
|
+
import { errorResponse } from "./utils.js";
|
|
11
|
+
export async function dispatchTool(name, arguments_) {
|
|
12
|
+
switch (name) {
|
|
13
|
+
// Memory tools
|
|
14
|
+
case "memos_save":
|
|
15
|
+
return handleMemosSave(arguments_);
|
|
16
|
+
case "memos_list_v2":
|
|
17
|
+
return handleMemosList(arguments_);
|
|
18
|
+
case "memos_get":
|
|
19
|
+
return handleMemosGet(arguments_);
|
|
20
|
+
case "memos_get_stats":
|
|
21
|
+
return handleMemosGetStats(arguments_);
|
|
22
|
+
// Search tools
|
|
23
|
+
case "memos_search":
|
|
24
|
+
return handleMemosSearch(arguments_);
|
|
25
|
+
case "memos_search_context":
|
|
26
|
+
return handleMemosSearchContext(arguments_);
|
|
27
|
+
case "memos_suggest":
|
|
28
|
+
return handleMemosSuggest(arguments_);
|
|
29
|
+
case "memos_context_resume":
|
|
30
|
+
return handleMemosContextResume(arguments_);
|
|
31
|
+
// Graph tools
|
|
32
|
+
case "memos_trace_path":
|
|
33
|
+
return handleMemosTracePath(arguments_);
|
|
34
|
+
case "memos_get_graph":
|
|
35
|
+
return handleMemosGetGraph(arguments_);
|
|
36
|
+
case "memos_export_schema":
|
|
37
|
+
return handleMemosExportSchema(arguments_);
|
|
38
|
+
case "memos_impact":
|
|
39
|
+
return handleMemosImpact(arguments_);
|
|
40
|
+
// Calendar
|
|
41
|
+
case "memos_calendar":
|
|
42
|
+
return handleMemosCalendar(arguments_);
|
|
43
|
+
// Admin tools
|
|
44
|
+
case "memos_list_cubes":
|
|
45
|
+
return handleMemosListCubes(arguments_);
|
|
46
|
+
case "memos_register_cube":
|
|
47
|
+
return handleMemosRegisterCube(arguments_);
|
|
48
|
+
case "memos_create_user":
|
|
49
|
+
return handleMemosCreateUser(arguments_);
|
|
50
|
+
case "memos_validate_cubes":
|
|
51
|
+
return handleMemosValidateCubes(arguments_);
|
|
52
|
+
case "memos_delete":
|
|
53
|
+
return handleMemosDelete(arguments_);
|
|
54
|
+
default:
|
|
55
|
+
return errorResponse(`Unknown tool: ${name}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function handleApiUnreachable() {
|
|
59
|
+
return [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: [
|
|
62
|
+
`❌ [API_UNREACHABLE] Cannot connect to MemOS API at ${MEMOS_URL}`,
|
|
63
|
+
"",
|
|
64
|
+
"💡 Suggestions:",
|
|
65
|
+
"- Check if MemOS API is running: `curl http://localhost:18000/health`",
|
|
66
|
+
"- Start with: `scripts/local/start.bat`",
|
|
67
|
+
"- Check port availability",
|
|
68
|
+
].join("\n"),
|
|
69
|
+
}];
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Handlers
|
|
3
|
+
*
|
|
4
|
+
* memos_save, memos_list_v2, memos_get, memos_get_stats
|
|
5
|
+
*/
|
|
6
|
+
import type { TextContent, MemoryNode, SearchData } from "../types.js";
|
|
7
|
+
export declare function handleMemosSave(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
8
|
+
export declare function handleMemosList(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
9
|
+
export declare function handleMemosGet(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
10
|
+
export declare function handleMemosGetStats(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
11
|
+
export declare function extractMemoriesFromData(data: SearchData): MemoryNode[];
|
|
12
|
+
//# sourceMappingURL=memory.d.ts.map
|