oh-memos-mcp 3.0.0 → 3.1.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/CHANGELOG.md +21 -0
- package/README.md +2 -2
- package/dist/access-tracker.d.ts +22 -0
- package/dist/access-tracker.js +187 -0
- package/dist/formatters.d.ts +18 -0
- package/dist/formatters.js +52 -4
- package/dist/handlers/graph.d.ts +31 -0
- package/dist/handlers/graph.js +173 -90
- package/dist/handlers/memory.d.ts +16 -0
- package/dist/handlers/memory.js +128 -22
- package/dist/handlers/search.js +129 -12
- package/dist/memory-quality.d.ts +15 -0
- package/dist/memory-quality.js +314 -13
- package/dist/memory-tier.d.ts +56 -0
- package/dist/memory-tier.js +68 -0
- package/dist/models.js +10 -3
- package/dist/spreading-activation.d.ts +157 -0
- package/dist/spreading-activation.js +205 -0
- package/dist/types.d.ts +7 -0
- package/package.json +67 -68
package/dist/handlers/graph.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_graph(mode="path"), memos_graph(mode="related"), memos_graph(mode="schema"), memos_graph(mode="impact")
|
|
5
5
|
*/
|
|
6
|
-
import { MEMOS_URL, MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, logger, registeredCubes } from "../config.js";
|
|
6
|
+
import { MEMOS_URL, MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, logger, registeredCubes, } from "../config.js";
|
|
7
7
|
import { fetchWithTimeout } from "../api-client.js";
|
|
8
8
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
9
|
import { formatProvenance } from "../graph-provenance.js";
|
|
10
|
-
import { buildGraphImportPlan, renderGraphImportPlan } from "../graphify-import.js";
|
|
10
|
+
import { buildGraphImportPlan, renderGraphImportPlan, } from "../graphify-import.js";
|
|
11
11
|
import { detectQueryIntent, extractMemoriesFromResponse, getGraphsForIntent, getIntentDescription, } from "../query-processing.js";
|
|
12
12
|
import { ERR_NEO4J_CONFIG, ERR_PARAM_MISSING, apiErrorResponse, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
13
13
|
// ============================================================================
|
|
@@ -22,8 +22,13 @@ import { ERR_NEO4J_CONFIG, ERR_PARAM_MISSING, apiErrorResponse, cubeRegistration
|
|
|
22
22
|
*/
|
|
23
23
|
export function handleMemosGraphifyImport(arguments_) {
|
|
24
24
|
const raw = arguments_.graph_json;
|
|
25
|
-
if (raw === undefined ||
|
|
26
|
-
|
|
25
|
+
if (raw === undefined ||
|
|
26
|
+
raw === null ||
|
|
27
|
+
(typeof raw === "string" && !raw.trim())) {
|
|
28
|
+
return errorResponse("graph_json is required for Graphify import dry-run", ERR_PARAM_MISSING, [
|
|
29
|
+
"Pass the contents of Graphify graph.json as graph_json",
|
|
30
|
+
"No database write occurs in this mode",
|
|
31
|
+
]);
|
|
27
32
|
}
|
|
28
33
|
let parsed = raw;
|
|
29
34
|
if (typeof raw === "string") {
|
|
@@ -39,7 +44,9 @@ export function handleMemosGraphifyImport(arguments_) {
|
|
|
39
44
|
}
|
|
40
45
|
try {
|
|
41
46
|
const plan = buildGraphImportPlan(parsed, {
|
|
42
|
-
projectKey: typeof arguments_.project_key === "string"
|
|
47
|
+
projectKey: typeof arguments_.project_key === "string"
|
|
48
|
+
? arguments_.project_key
|
|
49
|
+
: undefined,
|
|
43
50
|
});
|
|
44
51
|
return [{ type: "text", text: renderGraphImportPlan(plan) }];
|
|
45
52
|
}
|
|
@@ -66,7 +73,7 @@ async function neo4jQuery(cypher, parameters) {
|
|
|
66
73
|
}),
|
|
67
74
|
timeoutMs: 15,
|
|
68
75
|
});
|
|
69
|
-
const data = await response.json();
|
|
76
|
+
const data = (await response.json());
|
|
70
77
|
return { ok: response.ok, data, status: response.status };
|
|
71
78
|
}
|
|
72
79
|
catch (err) {
|
|
@@ -105,7 +112,7 @@ export async function handleMemosTracePath(arguments_) {
|
|
|
105
112
|
}),
|
|
106
113
|
});
|
|
107
114
|
if (response.ok) {
|
|
108
|
-
const data = await response.json();
|
|
115
|
+
const data = (await response.json());
|
|
109
116
|
if (data.code === 200) {
|
|
110
117
|
const traceData = data.data ?? {};
|
|
111
118
|
const found = traceData.path_found ?? traceData.found ?? false;
|
|
@@ -136,7 +143,10 @@ export async function handleMemosTracePath(arguments_) {
|
|
|
136
143
|
const nodes = path.nodes ?? [];
|
|
137
144
|
const edges = path.edges ?? [];
|
|
138
145
|
// If API returns empty nodes, fall back to Neo4j
|
|
139
|
-
if (nodes.length === 0 &&
|
|
146
|
+
if (nodes.length === 0 &&
|
|
147
|
+
NEO4J_HTTP_URL &&
|
|
148
|
+
NEO4J_USER &&
|
|
149
|
+
NEO4J_PASSWORD) {
|
|
140
150
|
throw new Error("API returned empty path nodes, falling back to Neo4j");
|
|
141
151
|
}
|
|
142
152
|
results.push(`### Path ${i + 1} (Length: ${length})`, "", "```");
|
|
@@ -175,12 +185,15 @@ export async function handleMemosTracePath(arguments_) {
|
|
|
175
185
|
const cypher = `
|
|
176
186
|
MATCH (source:Memory), (target:Memory)
|
|
177
187
|
WHERE source.id = $source_id AND target.id = $target_id
|
|
178
|
-
MATCH path = shortestPath((source)-[*1..${maxDepth}]-(target))
|
|
179
|
-
RETURN [n IN nodes(path) | {id: n.id, memory: n.memory}] AS nodes,
|
|
180
|
-
[r IN relationships(path) | {type: type(r), provenance: properties(r)}] AS rels
|
|
188
|
+
MATCH path = shortestPath((source)-[*1..${maxDepth}]-(target))
|
|
189
|
+
RETURN [n IN nodes(path) | {id: n.id, memory: n.memory}] AS nodes,
|
|
190
|
+
[r IN relationships(path) | {type: type(r), provenance: properties(r)}] AS rels
|
|
181
191
|
LIMIT 1
|
|
182
192
|
`;
|
|
183
|
-
const { ok, data, status } = await neo4jQuery(cypher, {
|
|
193
|
+
const { ok, data, status } = await neo4jQuery(cypher, {
|
|
194
|
+
source_id: sourceId,
|
|
195
|
+
target_id: targetId,
|
|
196
|
+
});
|
|
184
197
|
const results = ["## 🔗 Path Trace (Direct Query)", ""];
|
|
185
198
|
if (ok && data) {
|
|
186
199
|
const rows = data.results?.[0]?.data ?? [];
|
|
@@ -240,7 +253,7 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
240
253
|
}),
|
|
241
254
|
});
|
|
242
255
|
if (searchResponse.ok) {
|
|
243
|
-
const data = await searchResponse.json();
|
|
256
|
+
const data = (await searchResponse.json());
|
|
244
257
|
if (data.code === 200) {
|
|
245
258
|
memories = extractMemoriesFromResponse(data.data ?? {});
|
|
246
259
|
}
|
|
@@ -252,10 +265,14 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
252
265
|
const retrySearch = await fetchWithTimeout(`${MEMOS_URL}/search`, {
|
|
253
266
|
method: "POST",
|
|
254
267
|
headers: { "Content-Type": "application/json" },
|
|
255
|
-
body: JSON.stringify({
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
user_id: MEMOS_USER,
|
|
270
|
+
query,
|
|
271
|
+
install_cube_ids: [cubeId],
|
|
272
|
+
}),
|
|
256
273
|
});
|
|
257
274
|
if (retrySearch.ok) {
|
|
258
|
-
const retryData = await retrySearch.json();
|
|
275
|
+
const retryData = (await retrySearch.json());
|
|
259
276
|
if (retryData.code === 200) {
|
|
260
277
|
memories = extractMemoriesFromResponse(retryData.data ?? {});
|
|
261
278
|
}
|
|
@@ -280,10 +297,10 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
280
297
|
cypher = `
|
|
281
298
|
MATCH (a)-[r:${edgeTypesCypher}]->(b)
|
|
282
299
|
WHERE a.id IN [${idList}] OR b.id IN [${idList}]
|
|
283
|
-
RETURN a.id as source_id, a.memory as source_memory,
|
|
284
|
-
type(r) as relation_type,
|
|
285
|
-
b.id as target_id, b.memory as target_memory,
|
|
286
|
-
properties(r) as relation_provenance
|
|
300
|
+
RETURN a.id as source_id, a.memory as source_memory,
|
|
301
|
+
type(r) as relation_type,
|
|
302
|
+
b.id as target_id, b.memory as target_memory,
|
|
303
|
+
properties(r) as relation_provenance
|
|
287
304
|
LIMIT 20
|
|
288
305
|
`;
|
|
289
306
|
params = {};
|
|
@@ -293,10 +310,10 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
293
310
|
cypher = `
|
|
294
311
|
MATCH (a)-[r:${edgeTypesCypher}]->(b)
|
|
295
312
|
WHERE a.memory CONTAINS $keyword OR b.memory CONTAINS $keyword
|
|
296
|
-
RETURN a.id as source_id, a.memory as source_memory,
|
|
297
|
-
type(r) as relation_type,
|
|
298
|
-
b.id as target_id, b.memory as target_memory,
|
|
299
|
-
properties(r) as relation_provenance
|
|
313
|
+
RETURN a.id as source_id, a.memory as source_memory,
|
|
314
|
+
type(r) as relation_type,
|
|
315
|
+
b.id as target_id, b.memory as target_memory,
|
|
316
|
+
properties(r) as relation_provenance
|
|
300
317
|
LIMIT 20
|
|
301
318
|
`;
|
|
302
319
|
params = { keyword: firstWord };
|
|
@@ -358,6 +375,123 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
358
375
|
// ============================================================================
|
|
359
376
|
// memos_graph(mode="schema")
|
|
360
377
|
// ============================================================================
|
|
378
|
+
/**
|
|
379
|
+
* 把 /graph/schema 的响应渲染成报告。
|
|
380
|
+
*
|
|
381
|
+
* ## 为什么是导出的纯函数
|
|
382
|
+
*
|
|
383
|
+
* 这段逻辑原本内联在 handler 里,单测触达不到 —— 于是**五个字段名读错了
|
|
384
|
+
* 却长期无人发现**。抽出来才能钉住字段契约。同一类漏洞本轮已出现过两次
|
|
385
|
+
* (list 路径的层级过滤、access-tracker 的接线)。
|
|
386
|
+
*
|
|
387
|
+
* ## 修掉的字段名不匹配
|
|
388
|
+
*
|
|
389
|
+
* API(SchemaData)返回的键与此前读取的键对不上,读不到就 `?? 0` 静默兜成零:
|
|
390
|
+
*
|
|
391
|
+
* | 此前读 | API 实际返回 |
|
|
392
|
+
* |----------------------------|--------------------|
|
|
393
|
+
* | `avg_connections_per_node` | `avg_connections` |
|
|
394
|
+
* | `orphan_node_count` | `orphan_nodes` |
|
|
395
|
+
* | `edge_type_distribution` | `edge_types` |
|
|
396
|
+
* | `memory_type_distribution` | `memory_types` |
|
|
397
|
+
* | `tag_frequency` | `top_tags` |
|
|
398
|
+
*
|
|
399
|
+
* `max_connections` / `total_nodes` / `total_edges` / `time_range` 恰好同名,
|
|
400
|
+
* 所以只有上面五项失效 —— 部分正确正是它难被发现的原因。
|
|
401
|
+
*
|
|
402
|
+
* 后果不只是少显示:健康评估读的也是错字段,于是 orphan 比例恒为 0(永远
|
|
403
|
+
* 报「连接良好」),avg 恒为 0(永远报「平均连接过低」)。两句结论同时出现、
|
|
404
|
+
* 自相矛盾,而它们**都是基于零值**得出的。
|
|
405
|
+
*
|
|
406
|
+
* 兼容两种键名:后端字段若将来改回长名,读取仍然成立。
|
|
407
|
+
*/
|
|
408
|
+
export function formatSchemaReport(schema) {
|
|
409
|
+
/** 依次尝试多个键名,取第一个存在的。 */
|
|
410
|
+
const pick = (...keys) => {
|
|
411
|
+
for (const key of keys) {
|
|
412
|
+
if (schema[key] !== undefined && schema[key] !== null)
|
|
413
|
+
return schema[key];
|
|
414
|
+
}
|
|
415
|
+
return undefined;
|
|
416
|
+
};
|
|
417
|
+
const num = (...keys) => {
|
|
418
|
+
const raw = Number(pick(...keys));
|
|
419
|
+
return Number.isFinite(raw) ? raw : 0;
|
|
420
|
+
};
|
|
421
|
+
const dict = (...keys) => {
|
|
422
|
+
const raw = pick(...keys);
|
|
423
|
+
return raw && typeof raw === "object"
|
|
424
|
+
? raw
|
|
425
|
+
: {};
|
|
426
|
+
};
|
|
427
|
+
const totalNodes = num("total_nodes");
|
|
428
|
+
const avgConn = num("avg_connections", "avg_connections_per_node");
|
|
429
|
+
const orphanCount = num("orphan_nodes", "orphan_node_count");
|
|
430
|
+
const results = ["## 📊 Knowledge Graph Schema", ""];
|
|
431
|
+
results.push("### Overview");
|
|
432
|
+
results.push(`- **Total Nodes**: ${totalNodes}`);
|
|
433
|
+
results.push(`- **Total Edges**: ${num("total_edges")}`);
|
|
434
|
+
results.push(`- **Avg Connections/Node**: ${avgConn.toFixed(2)}`);
|
|
435
|
+
results.push(`- **Max Connections**: ${num("max_connections")}`);
|
|
436
|
+
results.push(`- **Orphan Nodes**: ${orphanCount}`);
|
|
437
|
+
results.push("");
|
|
438
|
+
const timeRange = dict("time_range");
|
|
439
|
+
if (timeRange.earliest || timeRange.latest) {
|
|
440
|
+
results.push("### Time Range");
|
|
441
|
+
if (timeRange.earliest)
|
|
442
|
+
results.push(`- Earliest: ${timeRange.earliest}`);
|
|
443
|
+
if (timeRange.latest)
|
|
444
|
+
results.push(`- Latest: ${timeRange.latest}`);
|
|
445
|
+
results.push("");
|
|
446
|
+
}
|
|
447
|
+
const edgeDist = dict("edge_types", "edge_type_distribution");
|
|
448
|
+
if (Object.keys(edgeDist).length > 0) {
|
|
449
|
+
results.push("### Relationship Types");
|
|
450
|
+
for (const [t, c] of Object.entries(edgeDist).sort((a, b) => b[1] - a[1])) {
|
|
451
|
+
results.push(`- **${t}**: ${c}`);
|
|
452
|
+
}
|
|
453
|
+
results.push("");
|
|
454
|
+
}
|
|
455
|
+
const memDist = dict("memory_types", "memory_type_distribution");
|
|
456
|
+
if (Object.keys(memDist).length > 0) {
|
|
457
|
+
results.push("### Memory Types");
|
|
458
|
+
for (const [t, c] of Object.entries(memDist).sort((a, b) => b[1] - a[1])) {
|
|
459
|
+
results.push(`- ${t}: ${c}`);
|
|
460
|
+
}
|
|
461
|
+
results.push("");
|
|
462
|
+
}
|
|
463
|
+
// top_tags 在 SchemaData 里是 list;旧读法当成 {tag: count} 字典。两种都收。
|
|
464
|
+
const rawTags = pick("top_tags", "tag_frequency");
|
|
465
|
+
const tagItems = Array.isArray(rawTags)
|
|
466
|
+
? rawTags
|
|
467
|
+
.slice(0, 10)
|
|
468
|
+
.map((entry) => Array.isArray(entry)
|
|
469
|
+
? [String(entry[0]), entry[1]]
|
|
470
|
+
: [String(entry), ""])
|
|
471
|
+
: Object.entries(dict("top_tags", "tag_frequency")).slice(0, 10);
|
|
472
|
+
if (tagItems.length > 0) {
|
|
473
|
+
results.push("### Top Tags");
|
|
474
|
+
for (const [tag, count] of tagItems) {
|
|
475
|
+
results.push(count === "" ? `- \`${tag}\`` : `- \`${tag}\`: ${count}`);
|
|
476
|
+
}
|
|
477
|
+
results.push("");
|
|
478
|
+
}
|
|
479
|
+
results.push("### Health Assessment");
|
|
480
|
+
if (totalNodes > 0) {
|
|
481
|
+
const orphanRatio = orphanCount / totalNodes;
|
|
482
|
+
if (orphanRatio > 0.5)
|
|
483
|
+
results.push("⚠️ High orphan ratio - many memories are not connected");
|
|
484
|
+
else if (orphanRatio > 0.2)
|
|
485
|
+
results.push("📋 Moderate orphan ratio");
|
|
486
|
+
else
|
|
487
|
+
results.push("✅ Good connectivity - memories are well connected");
|
|
488
|
+
}
|
|
489
|
+
if (avgConn < 1)
|
|
490
|
+
results.push("⚠️ Low average connections - consider enriching relationships");
|
|
491
|
+
else if (avgConn > 5)
|
|
492
|
+
results.push("✅ Rich relationships - good knowledge graph density");
|
|
493
|
+
return results.join("\n");
|
|
494
|
+
}
|
|
361
495
|
export async function handleMemosExportSchema(arguments_) {
|
|
362
496
|
const cubeId = getCubeIdFromArgs(arguments_);
|
|
363
497
|
const sampleSize = Math.min(Math.max(Number(arguments_.sample_size ?? 100), 10), 1000);
|
|
@@ -375,70 +509,10 @@ export async function handleMemosExportSchema(arguments_) {
|
|
|
375
509
|
}),
|
|
376
510
|
});
|
|
377
511
|
if (response.ok) {
|
|
378
|
-
const data = await response.json();
|
|
512
|
+
const data = (await response.json());
|
|
379
513
|
if (data.code === 200) {
|
|
380
514
|
const schema = data.data ?? {};
|
|
381
|
-
|
|
382
|
-
results.push("## 📊 Knowledge Graph Schema", "");
|
|
383
|
-
results.push("### Overview");
|
|
384
|
-
results.push(`- **Total Nodes**: ${schema.total_nodes ?? 0}`);
|
|
385
|
-
results.push(`- **Total Edges**: ${schema.total_edges ?? 0}`);
|
|
386
|
-
results.push(`- **Avg Connections/Node**: ${Number(schema.avg_connections_per_node ?? 0).toFixed(2)}`);
|
|
387
|
-
results.push(`- **Max Connections**: ${schema.max_connections ?? 0}`);
|
|
388
|
-
results.push(`- **Orphan Nodes**: ${schema.orphan_node_count ?? 0}`);
|
|
389
|
-
results.push("");
|
|
390
|
-
const timeRange = schema.time_range ?? {};
|
|
391
|
-
if (timeRange.earliest || timeRange.latest) {
|
|
392
|
-
results.push("### Time Range");
|
|
393
|
-
if (timeRange.earliest)
|
|
394
|
-
results.push(`- Earliest: ${timeRange.earliest}`);
|
|
395
|
-
if (timeRange.latest)
|
|
396
|
-
results.push(`- Latest: ${timeRange.latest}`);
|
|
397
|
-
results.push("");
|
|
398
|
-
}
|
|
399
|
-
const edgeDist = schema.edge_type_distribution ?? {};
|
|
400
|
-
if (Object.keys(edgeDist).length > 0) {
|
|
401
|
-
results.push("### Relationship Types");
|
|
402
|
-
for (const [t, c] of Object.entries(edgeDist).sort((a, b) => b[1] - a[1])) {
|
|
403
|
-
results.push(`- **${t}**: ${c}`);
|
|
404
|
-
}
|
|
405
|
-
results.push("");
|
|
406
|
-
}
|
|
407
|
-
const memDist = schema.memory_type_distribution ?? {};
|
|
408
|
-
if (Object.keys(memDist).length > 0) {
|
|
409
|
-
results.push("### Memory Types");
|
|
410
|
-
for (const [t, c] of Object.entries(memDist).sort((a, b) => b[1] - a[1])) {
|
|
411
|
-
results.push(`- ${t}: ${c}`);
|
|
412
|
-
}
|
|
413
|
-
results.push("");
|
|
414
|
-
}
|
|
415
|
-
const tagFreq = schema.tag_frequency ?? {};
|
|
416
|
-
const tagItems = Object.entries(tagFreq).slice(0, 10);
|
|
417
|
-
if (tagItems.length > 0) {
|
|
418
|
-
results.push("### Top Tags");
|
|
419
|
-
for (const [tag, count] of tagItems) {
|
|
420
|
-
results.push(`- \`${tag}\`: ${count}`);
|
|
421
|
-
}
|
|
422
|
-
results.push("");
|
|
423
|
-
}
|
|
424
|
-
results.push("### Health Assessment");
|
|
425
|
-
const totalNodes = Number(schema.total_nodes ?? 0);
|
|
426
|
-
const orphanCount = Number(schema.orphan_node_count ?? 0);
|
|
427
|
-
if (totalNodes > 0) {
|
|
428
|
-
const orphanRatio = orphanCount / totalNodes;
|
|
429
|
-
if (orphanRatio > 0.5)
|
|
430
|
-
results.push("⚠️ High orphan ratio - many memories are not connected");
|
|
431
|
-
else if (orphanRatio > 0.2)
|
|
432
|
-
results.push("📋 Moderate orphan ratio");
|
|
433
|
-
else
|
|
434
|
-
results.push("✅ Good connectivity - memories are well connected");
|
|
435
|
-
}
|
|
436
|
-
const avgConn = Number(schema.avg_connections_per_node ?? 0);
|
|
437
|
-
if (avgConn < 1)
|
|
438
|
-
results.push("⚠️ Low average connections - consider enriching relationships");
|
|
439
|
-
else if (avgConn > 5)
|
|
440
|
-
results.push("✅ Rich relationships - good knowledge graph density");
|
|
441
|
-
return [{ type: "text", text: results.join("\n") }];
|
|
515
|
+
return [{ type: "text", text: formatSchemaReport(schema) }];
|
|
442
516
|
}
|
|
443
517
|
else {
|
|
444
518
|
return apiErrorResponse("Schema export", String(data.message ?? "Unknown error"));
|
|
@@ -475,13 +549,15 @@ export async function handleMemosImpact(arguments_) {
|
|
|
475
549
|
MATCH (source:Memory {id: $source_id})-[:CAUSE|FOLLOWS*1..${maxDepth}]->(node:Memory)
|
|
476
550
|
WITH DISTINCT source, node
|
|
477
551
|
MATCH path = shortestPath((source)-[:CAUSE|FOLLOWS*1..${maxDepth}]->(node))
|
|
478
|
-
RETURN node.id AS id, node.key AS key, node.memory AS memory,
|
|
479
|
-
length(path) AS depth, properties(node) AS node_provenance
|
|
552
|
+
RETURN node.id AS id, node.key AS key, node.memory AS memory,
|
|
553
|
+
length(path) AS depth, properties(node) AS node_provenance
|
|
480
554
|
ORDER BY depth ASC
|
|
481
555
|
LIMIT 30
|
|
482
556
|
`;
|
|
483
557
|
try {
|
|
484
|
-
const { ok, data, status } = await neo4jQuery(cypher, {
|
|
558
|
+
const { ok, data, status } = await neo4jQuery(cypher, {
|
|
559
|
+
source_id: memoryId,
|
|
560
|
+
});
|
|
485
561
|
if (!ok) {
|
|
486
562
|
return apiErrorResponse("Impact analysis", `Neo4j HTTP ${status}`);
|
|
487
563
|
}
|
|
@@ -495,7 +571,12 @@ export async function handleMemosImpact(arguments_) {
|
|
|
495
571
|
}
|
|
496
572
|
const rows = data.results?.[0]?.data ?? [];
|
|
497
573
|
if (rows.length === 0) {
|
|
498
|
-
return [
|
|
574
|
+
return [
|
|
575
|
+
{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: "No forward impact found — this memory has no CAUSE or FOLLOWS successors.",
|
|
578
|
+
},
|
|
579
|
+
];
|
|
499
580
|
}
|
|
500
581
|
// Group by depth
|
|
501
582
|
const depthGroups = {};
|
|
@@ -525,7 +606,9 @@ export async function handleMemosImpact(arguments_) {
|
|
|
525
606
|
1: "Direct Impact",
|
|
526
607
|
2: "Indirect Impact",
|
|
527
608
|
};
|
|
528
|
-
for (const depth of Object.keys(depthGroups)
|
|
609
|
+
for (const depth of Object.keys(depthGroups)
|
|
610
|
+
.map(Number)
|
|
611
|
+
.sort((a, b) => a - b)) {
|
|
529
612
|
const items = depthGroups[depth];
|
|
530
613
|
const label = depthLabels[depth] ?? `Downstream (hop ${depth})`;
|
|
531
614
|
results.push(`### ${label} (${items.length} node${items.length !== 1 ? "s" : ""})`, "");
|
|
@@ -9,4 +9,20 @@ export declare function handleMemosList(arguments_: Record<string, unknown>): Pr
|
|
|
9
9
|
export declare function handleMemosGet(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
10
10
|
export declare function handleMemosGetStats(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
11
11
|
export declare function extractMemoriesFromData(data: SearchData): MemoryNode[];
|
|
12
|
+
/**
|
|
13
|
+
* memos_list_v2(Full 路径)的取数序列:提取 → 滤层级 → 滤业务类型 → 截断。
|
|
14
|
+
*
|
|
15
|
+
* 抽成导出的纯函数**只为让顺序可测**。这三步的次序不是随意的:
|
|
16
|
+
*
|
|
17
|
+
* - 滤层级必须在 slice **之前**。放到之后,`limit` 会被随即隐藏的
|
|
18
|
+
* WorkingMemory 副本吃掉 —— 要 20 条只拿到 10 条。
|
|
19
|
+
* - slice 必须最后。它是对外承诺的输出上界。
|
|
20
|
+
*
|
|
21
|
+
* 之前这段逻辑内联在 handler 里,单测触达不到:实测切断层级过滤后
|
|
22
|
+
* 全部 319 项 vitest 依然通过。这与 P1.5 的 W3/W4 是同一类漏洞
|
|
23
|
+
* (见 docs/design/memory-retrieval-optimization.md 第 11 节)。
|
|
24
|
+
* 在测试里自行组合这几步只能证明测试文件本身,证明不了 handler,
|
|
25
|
+
* 所以必须让 handler 和测试调用同一个函数。
|
|
26
|
+
*/
|
|
27
|
+
export declare function prepareListMemories(data: SearchData, memoryType: string | undefined, limit: number): MemoryNode[];
|
|
12
28
|
//# sourceMappingURL=memory.d.ts.map
|
package/dist/handlers/memory.js
CHANGED
|
@@ -9,6 +9,8 @@ import { getMemoryProvider } from "../providers/provider-factory.js";
|
|
|
9
9
|
import { apiCallWithRetry } from "../api-client.js";
|
|
10
10
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
11
11
|
import { parseMemoryWriteResponse } from "../memory-write-response.js";
|
|
12
|
+
import { recordAccess } from "../access-tracker.js";
|
|
13
|
+
import { filterEphemeralTier } from "../memory-tier.js";
|
|
12
14
|
// formatMemoriesForDisplay intentionally not imported here — list output is built
|
|
13
15
|
// from the truncated allMemories in handleMemosList (avoids printing the full cube).
|
|
14
16
|
import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, toFull, } from "../models.js";
|
|
@@ -66,13 +68,28 @@ export async function handleMemosSave(arguments_) {
|
|
|
66
68
|
content = `[${memoryType}] ${content}`;
|
|
67
69
|
}
|
|
68
70
|
if (isDuplicateSave(content, cubeId)) {
|
|
69
|
-
return [
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: `⏭️ Skipped: Same content was saved within ${DEDUP_TTL_SECONDS}s (dedup protection)`,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
70
77
|
}
|
|
71
78
|
const provider = getMemoryProvider(MEMOS_CUBES_DIR);
|
|
72
79
|
if (provider) {
|
|
73
|
-
const saved = await provider.save({
|
|
80
|
+
const saved = await provider.save({
|
|
81
|
+
cubeId,
|
|
82
|
+
content,
|
|
83
|
+
memoryType,
|
|
84
|
+
tags: [memoryType],
|
|
85
|
+
});
|
|
74
86
|
markSaved(content, cubeId);
|
|
75
|
-
return [
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: `Memory saved as [${memoryType}] in local cube '${cubeId}' · ID: ${saved.id}`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
76
93
|
}
|
|
77
94
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
78
95
|
if (!regSuccess)
|
|
@@ -88,8 +105,15 @@ export async function handleMemosSave(arguments_) {
|
|
|
88
105
|
markSaved(content, cubeId);
|
|
89
106
|
const write = parseMemoryWriteResponse(result.data);
|
|
90
107
|
const idText = write.memoryIds.length > 0 ? ` · IDs: ${write.memoryIds.join(", ")}` : "";
|
|
91
|
-
const warningText = write.warnings.length > 0
|
|
92
|
-
|
|
108
|
+
const warningText = write.warnings.length > 0
|
|
109
|
+
? ` · warnings: ${write.warnings.join("; ")}`
|
|
110
|
+
: "";
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `Memory saved as [${memoryType}] in cube '${cubeId}'${idText}${warningText}`,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
93
117
|
}
|
|
94
118
|
else if (result.data) {
|
|
95
119
|
return apiErrorResponse("Save", String(result.data.message ?? "Unknown error"));
|
|
@@ -112,11 +136,34 @@ export async function handleMemosList(arguments_) {
|
|
|
112
136
|
allMemories = allMemories.slice(0, limit);
|
|
113
137
|
const totalCount = allMemories.length;
|
|
114
138
|
if (compact && shouldCompact(totalCount)) {
|
|
115
|
-
return [
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: compactedResultToText({
|
|
143
|
+
preview: allMemories.slice(0, PREVIEW_COUNT).map(toMinimal),
|
|
144
|
+
totalCount,
|
|
145
|
+
omittedCount: totalCount - Math.min(PREVIEW_COUNT, totalCount),
|
|
146
|
+
message: 'Use memos_get(memory_id="<id>") for full details',
|
|
147
|
+
query: "local list",
|
|
148
|
+
cubeId,
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
];
|
|
116
152
|
}
|
|
117
153
|
if (!allMemories.length)
|
|
118
|
-
return [
|
|
119
|
-
|
|
154
|
+
return [
|
|
155
|
+
{ type: "text", text: `## Cube: ${cubeId}\n\nNo memories found.` },
|
|
156
|
+
];
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: [
|
|
161
|
+
`## Cube: ${cubeId} (${allMemories.length})`,
|
|
162
|
+
"",
|
|
163
|
+
...allMemories.map((m, i) => `${i + 1}. [${extractMcpType(m)}] ${m.memory}\n ID: \`${m.id}\``),
|
|
164
|
+
].join("\n\n"),
|
|
165
|
+
},
|
|
166
|
+
];
|
|
120
167
|
}
|
|
121
168
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
122
169
|
if (!regSuccess)
|
|
@@ -132,11 +179,7 @@ export async function handleMemosList(arguments_) {
|
|
|
132
179
|
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params }, ensureCubeRegistered);
|
|
133
180
|
if (result.success && result.data) {
|
|
134
181
|
const resultData = result.data.data ?? {};
|
|
135
|
-
|
|
136
|
-
if (memoryType) {
|
|
137
|
-
allMemories = allMemories.filter((m) => extractMcpType(m) === memoryType);
|
|
138
|
-
}
|
|
139
|
-
allMemories = allMemories.slice(0, limit); // always bound output to the requested limit
|
|
182
|
+
const allMemories = prepareListMemories(resultData, memoryType, limit);
|
|
140
183
|
const totalCount = allMemories.length;
|
|
141
184
|
if (compact && shouldCompact(totalCount)) {
|
|
142
185
|
const preview = allMemories.slice(0, PREVIEW_COUNT).map(toMinimal);
|
|
@@ -154,7 +197,12 @@ export async function handleMemosList(arguments_) {
|
|
|
154
197
|
// (Previously this fell back to formatMemoriesForDisplay(resultData) = the full
|
|
155
198
|
// untrimmed pull, which is how a limit=5 request printed the entire cube.)
|
|
156
199
|
if (totalCount === 0) {
|
|
157
|
-
return [
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
type: "text",
|
|
203
|
+
text: `## Cube: ${cubeId}\n\nNo memories${memoryType ? ` of type ${memoryType}` : ""} found.`,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
158
206
|
}
|
|
159
207
|
const header = memoryType
|
|
160
208
|
? `## Cube: ${cubeId} — type=${memoryType} (${totalCount})`
|
|
@@ -189,16 +237,40 @@ export async function handleMemosGet(arguments_) {
|
|
|
189
237
|
const node = await localProvider.get(cubeId, memoryId);
|
|
190
238
|
if (!node)
|
|
191
239
|
return [{ type: "text", text: notFoundText(memoryId) }];
|
|
240
|
+
// 只在确实取到记忆后记账 —— 未命中的 id 不构成使用度信号。
|
|
241
|
+
recordAccess(MEMOS_CUBES_DIR, cubeId, [memoryId]);
|
|
192
242
|
const full = toFull(node, cubeId, MEMOS_USER);
|
|
193
|
-
return [
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: [
|
|
247
|
+
"## 📝 Memory Details",
|
|
248
|
+
"",
|
|
249
|
+
`**ID**: \`${full.id}\``,
|
|
250
|
+
`**Type**: ${full.memoryType}`,
|
|
251
|
+
`**Cube**: ${full.cubeId} (local)`,
|
|
252
|
+
full.tags.length ? `**Tags**: ${full.tags.join(", ")}` : "",
|
|
253
|
+
full.createdAt ? `**Created**: ${full.createdAt}` : "",
|
|
254
|
+
"",
|
|
255
|
+
"### Content",
|
|
256
|
+
"",
|
|
257
|
+
full.content,
|
|
258
|
+
]
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.join("\n"),
|
|
261
|
+
},
|
|
262
|
+
];
|
|
194
263
|
}
|
|
195
264
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
196
265
|
if (!regSuccess)
|
|
197
266
|
return cubeRegistrationError(cubeId, regError);
|
|
198
267
|
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId, {}, ensureCubeRegistered);
|
|
199
268
|
if (result.success && result.data) {
|
|
200
|
-
const resultData = result.data
|
|
269
|
+
const resultData = result.data
|
|
270
|
+
.data;
|
|
201
271
|
if (resultData) {
|
|
272
|
+
// Full 模式同样记在本机侧车 —— 使用度是本机数据,不需要后端端点。
|
|
273
|
+
recordAccess(MEMOS_CUBES_DIR, cubeId, [memoryId]);
|
|
202
274
|
const fullMem = toFull(resultData, cubeId, MEMOS_USER);
|
|
203
275
|
const lines = [
|
|
204
276
|
"## 📝 Memory Details",
|
|
@@ -265,14 +337,25 @@ export async function handleMemosGetStats(arguments_) {
|
|
|
265
337
|
return [{ type: "text", text: `No memories found in cube '${cubeId}'.` }];
|
|
266
338
|
}
|
|
267
339
|
const typeIcons = {
|
|
268
|
-
BUGFIX: "🐛",
|
|
269
|
-
|
|
270
|
-
|
|
340
|
+
BUGFIX: "🐛",
|
|
341
|
+
ERROR_PATTERN: "🔴",
|
|
342
|
+
DECISION: "📋",
|
|
343
|
+
GOTCHA: "⚠️",
|
|
344
|
+
CODE_PATTERN: "📝",
|
|
345
|
+
CONFIG: "⚙️",
|
|
346
|
+
FEATURE: "✨",
|
|
347
|
+
MILESTONE: "🎯",
|
|
348
|
+
PROGRESS: "📊",
|
|
349
|
+
INFERRED: "🔗",
|
|
271
350
|
SYNTHESIS: "🧠",
|
|
272
351
|
};
|
|
273
|
-
const result_lines = [
|
|
352
|
+
const result_lines = [
|
|
353
|
+
`## 📊 Memory Stats: ${cubeId}`,
|
|
354
|
+
`Total Memories: **${total}**`,
|
|
355
|
+
"",
|
|
356
|
+
];
|
|
274
357
|
for (const [mtype, count] of Object.entries(stats).sort((a, b) => b[1] - a[1])) {
|
|
275
|
-
const percentage = (count / total * 100).toFixed(1);
|
|
358
|
+
const percentage = ((count / total) * 100).toFixed(1);
|
|
276
359
|
const icon = typeIcons[mtype] ?? "📌";
|
|
277
360
|
result_lines.push(`- ${icon} **${mtype}**: ${count} (${percentage}%)`);
|
|
278
361
|
}
|
|
@@ -287,7 +370,7 @@ export async function handleMemosGetStats(arguments_) {
|
|
|
287
370
|
result_lines.push(" `BUGFIX` · `DECISION` · `MILESTONE` · `FEATURE` · `GOTCHA` · `CONFIG`");
|
|
288
371
|
}
|
|
289
372
|
if (userTyped > 0) {
|
|
290
|
-
result_lines.push("", `✅ **用户标注记忆**: ${userTyped} 条 (${(userTyped / total * 100).toFixed(0)}%)`);
|
|
373
|
+
result_lines.push("", `✅ **用户标注记忆**: ${userTyped} 条 (${((userTyped / total) * 100).toFixed(0)}%)`);
|
|
291
374
|
}
|
|
292
375
|
return [{ type: "text", text: result_lines.join("\n") }];
|
|
293
376
|
}
|
|
@@ -315,4 +398,27 @@ export function extractMemoriesFromData(data) {
|
|
|
315
398
|
}
|
|
316
399
|
return memories;
|
|
317
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* memos_list_v2(Full 路径)的取数序列:提取 → 滤层级 → 滤业务类型 → 截断。
|
|
403
|
+
*
|
|
404
|
+
* 抽成导出的纯函数**只为让顺序可测**。这三步的次序不是随意的:
|
|
405
|
+
*
|
|
406
|
+
* - 滤层级必须在 slice **之前**。放到之后,`limit` 会被随即隐藏的
|
|
407
|
+
* WorkingMemory 副本吃掉 —— 要 20 条只拿到 10 条。
|
|
408
|
+
* - slice 必须最后。它是对外承诺的输出上界。
|
|
409
|
+
*
|
|
410
|
+
* 之前这段逻辑内联在 handler 里,单测触达不到:实测切断层级过滤后
|
|
411
|
+
* 全部 319 项 vitest 依然通过。这与 P1.5 的 W3/W4 是同一类漏洞
|
|
412
|
+
* (见 docs/design/memory-retrieval-optimization.md 第 11 节)。
|
|
413
|
+
* 在测试里自行组合这几步只能证明测试文件本身,证明不了 handler,
|
|
414
|
+
* 所以必须让 handler 和测试调用同一个函数。
|
|
415
|
+
*/
|
|
416
|
+
export function prepareListMemories(data, memoryType, limit) {
|
|
417
|
+
const extracted = extractMemoriesFromData(data);
|
|
418
|
+
const visible = [...filterEphemeralTier(extracted, (m) => m.metadata)];
|
|
419
|
+
const typed = memoryType
|
|
420
|
+
? visible.filter((m) => extractMcpType(m) === memoryType)
|
|
421
|
+
: visible;
|
|
422
|
+
return typed.slice(0, limit);
|
|
423
|
+
}
|
|
318
424
|
//# sourceMappingURL=memory.js.map
|