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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Handlers
|
|
3
|
+
*
|
|
4
|
+
* memos_save, memos_list_v2, memos_get, memos_get_stats
|
|
5
|
+
*/
|
|
6
|
+
import * as crypto from "crypto";
|
|
7
|
+
import { MEMOS_URL, MEMOS_USER, logger } from "../config.js";
|
|
8
|
+
import { apiCallWithRetry } from "../api-client.js";
|
|
9
|
+
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
10
|
+
import { formatMemoriesForDisplay } from "../formatters.js";
|
|
11
|
+
import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, toFull, } from "../models.js";
|
|
12
|
+
import { computeMemoryStats, extractMcpType } from "../query-processing.js";
|
|
13
|
+
import { ERR_PARAM_MISSING, apiErrorResponse, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Deduplication Cache
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const saveDedupCache = new Map();
|
|
18
|
+
const DEDUP_TTL_SECONDS = 60;
|
|
19
|
+
function contentHash(content, cubeId) {
|
|
20
|
+
return crypto.createHash("md5").update(`${cubeId}:${content}`).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
function isDuplicateSave(content, cubeId) {
|
|
23
|
+
const key = contentHash(content, cubeId);
|
|
24
|
+
const now = Date.now() / 1000;
|
|
25
|
+
// Clean expired
|
|
26
|
+
for (const [k, [, ts]] of saveDedupCache) {
|
|
27
|
+
if (now - ts > DEDUP_TTL_SECONDS)
|
|
28
|
+
saveDedupCache.delete(k);
|
|
29
|
+
}
|
|
30
|
+
const entry = saveDedupCache.get(key);
|
|
31
|
+
if (entry && now - entry[1] < DEDUP_TTL_SECONDS) {
|
|
32
|
+
logger.debug(`Duplicate save detected (within ${DEDUP_TTL_SECONDS}s), skipping`);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function markSaved(content, cubeId) {
|
|
38
|
+
const key = contentHash(content, cubeId);
|
|
39
|
+
saveDedupCache.set(key, [cubeId, Date.now() / 1000]);
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// memos_save
|
|
43
|
+
// ============================================================================
|
|
44
|
+
export async function handleMemosSave(arguments_) {
|
|
45
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
46
|
+
let content = String(arguments_.content ?? "");
|
|
47
|
+
const memoryType = arguments_.memory_type;
|
|
48
|
+
if (!memoryType) {
|
|
49
|
+
return errorResponse("memory_type parameter is required", ERR_PARAM_MISSING, [
|
|
50
|
+
"Bug fix -> `BUGFIX` or `ERROR_PATTERN`",
|
|
51
|
+
"Technical decision -> `DECISION`",
|
|
52
|
+
"Gotcha/trap -> `GOTCHA`",
|
|
53
|
+
"Code template -> `CODE_PATTERN`",
|
|
54
|
+
"Config change -> `CONFIG`",
|
|
55
|
+
"New feature -> `FEATURE`",
|
|
56
|
+
"Major achievement -> `MILESTONE`",
|
|
57
|
+
"Pure progress update -> `PROGRESS`",
|
|
58
|
+
'Example: `memos_save(content="...", memory_type="BUGFIX")`',
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
if (!content.startsWith(`[${memoryType}]`)) {
|
|
62
|
+
content = `[${memoryType}] ${content}`;
|
|
63
|
+
}
|
|
64
|
+
if (isDuplicateSave(content, cubeId)) {
|
|
65
|
+
return [{ type: "text", text: `⏭️ Skipped: Same content was saved within ${DEDUP_TTL_SECONDS}s (dedup protection)` }];
|
|
66
|
+
}
|
|
67
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
68
|
+
if (!regSuccess)
|
|
69
|
+
return cubeRegistrationError(cubeId, regError);
|
|
70
|
+
const result = await apiCallWithRetry("POST", `${MEMOS_URL}/memories`, cubeId, {
|
|
71
|
+
body: {
|
|
72
|
+
user_id: MEMOS_USER,
|
|
73
|
+
mem_cube_id: cubeId,
|
|
74
|
+
memory_content: content,
|
|
75
|
+
},
|
|
76
|
+
}, ensureCubeRegistered);
|
|
77
|
+
if (result.success) {
|
|
78
|
+
markSaved(content, cubeId);
|
|
79
|
+
return [{ type: "text", text: `Memory saved as [${memoryType}] in cube '${cubeId}'` }];
|
|
80
|
+
}
|
|
81
|
+
else if (result.data) {
|
|
82
|
+
return apiErrorResponse("Save", String(result.data.message ?? "Unknown error"));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
return apiErrorResponse("Save", `HTTP ${result.status}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// memos_list_v2
|
|
90
|
+
// ============================================================================
|
|
91
|
+
export async function handleMemosList(arguments_) {
|
|
92
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
93
|
+
const limit = Number(arguments_.limit ?? 20);
|
|
94
|
+
const memoryType = arguments_.memory_type;
|
|
95
|
+
const compact = arguments_.compact !== false; // Default true
|
|
96
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
97
|
+
if (!regSuccess)
|
|
98
|
+
return cubeRegistrationError(cubeId, regError);
|
|
99
|
+
const params = {
|
|
100
|
+
user_id: MEMOS_USER,
|
|
101
|
+
mem_cube_id: cubeId,
|
|
102
|
+
};
|
|
103
|
+
if (!memoryType) {
|
|
104
|
+
params.limit = limit;
|
|
105
|
+
}
|
|
106
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params }, ensureCubeRegistered);
|
|
107
|
+
if (result.success && result.data) {
|
|
108
|
+
const resultData = result.data.data ?? {};
|
|
109
|
+
let allMemories = extractMemoriesFromData(resultData);
|
|
110
|
+
if (memoryType) {
|
|
111
|
+
allMemories = allMemories.filter((m) => extractMcpType(m) === memoryType).slice(0, limit);
|
|
112
|
+
}
|
|
113
|
+
const totalCount = allMemories.length;
|
|
114
|
+
if (compact && shouldCompact(totalCount)) {
|
|
115
|
+
const preview = allMemories.slice(0, PREVIEW_COUNT).map(toMinimal);
|
|
116
|
+
const text = compactedResultToText({
|
|
117
|
+
preview,
|
|
118
|
+
totalCount,
|
|
119
|
+
omittedCount: totalCount - preview.length,
|
|
120
|
+
message: 'Use memos_get(memory_id="<id>") for full details',
|
|
121
|
+
query: memoryType ? `list (type=${memoryType})` : "list all",
|
|
122
|
+
cubeId,
|
|
123
|
+
});
|
|
124
|
+
return [{ type: "text", text }];
|
|
125
|
+
}
|
|
126
|
+
const formatted = formatMemoriesForDisplay(resultData);
|
|
127
|
+
return [{ type: "text", text: formatted }];
|
|
128
|
+
}
|
|
129
|
+
else if (result.data) {
|
|
130
|
+
return apiErrorResponse("List", String(result.data.message ?? "Unknown error"));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
return apiErrorResponse("List", `HTTP ${result.status}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// memos_get
|
|
138
|
+
// ============================================================================
|
|
139
|
+
export async function handleMemosGet(arguments_) {
|
|
140
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
141
|
+
const memoryId = String(arguments_.memory_id ?? "");
|
|
142
|
+
if (!memoryId) {
|
|
143
|
+
return errorResponse("memory_id parameter is required", ERR_PARAM_MISSING, [
|
|
144
|
+
"Get memory_id from memos_search or memos_list_v2 results",
|
|
145
|
+
'Example: `memos_get(memory_id="abc123-...")`',
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
149
|
+
if (!regSuccess)
|
|
150
|
+
return cubeRegistrationError(cubeId, regError);
|
|
151
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId, {}, ensureCubeRegistered);
|
|
152
|
+
if (result.success && result.data) {
|
|
153
|
+
const resultData = result.data.data;
|
|
154
|
+
if (resultData) {
|
|
155
|
+
const fullMem = toFull(resultData, cubeId, MEMOS_USER);
|
|
156
|
+
const lines = [
|
|
157
|
+
"## 📝 Memory Details",
|
|
158
|
+
"",
|
|
159
|
+
`**ID**: \`${fullMem.id}\``,
|
|
160
|
+
`**Type**: ${fullMem.memoryType}`,
|
|
161
|
+
`**Cube**: ${fullMem.cubeId}`,
|
|
162
|
+
];
|
|
163
|
+
if (fullMem.key)
|
|
164
|
+
lines.push(`**Key**: ${fullMem.key}`);
|
|
165
|
+
if (fullMem.tags.length > 0)
|
|
166
|
+
lines.push(`**Tags**: ${fullMem.tags.join(", ")}`);
|
|
167
|
+
if (fullMem.createdAt)
|
|
168
|
+
lines.push(`**Created**: ${fullMem.createdAt}`);
|
|
169
|
+
lines.push("", "### Content", "", fullMem.content);
|
|
170
|
+
if (fullMem.background) {
|
|
171
|
+
lines.push("", "### Background", "", fullMem.background);
|
|
172
|
+
}
|
|
173
|
+
if (fullMem.relations && fullMem.relations.length > 0) {
|
|
174
|
+
lines.push("", "### Relations", "");
|
|
175
|
+
for (const rel of fullMem.relations.slice(0, 5)) {
|
|
176
|
+
lines.push(`- ${JSON.stringify(rel)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
return [{ type: "text", text: notFoundText(memoryId) }];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (result.data) {
|
|
186
|
+
const errMsg = String(result.data.message ?? "Unknown error");
|
|
187
|
+
if (errMsg.toLowerCase().includes("not found") || result.status === 404) {
|
|
188
|
+
return [{ type: "text", text: notFoundText(memoryId) }];
|
|
189
|
+
}
|
|
190
|
+
return apiErrorResponse("Get", errMsg);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
return apiErrorResponse("Get", `HTTP ${result.status}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function notFoundText(memoryId) {
|
|
197
|
+
return [
|
|
198
|
+
`❌ Memory not found: \`${memoryId}\``,
|
|
199
|
+
"",
|
|
200
|
+
"💡 **Tips**:",
|
|
201
|
+
"- Verify the ID is correct (copy from memos_search results)",
|
|
202
|
+
"- The memory may have been deleted",
|
|
203
|
+
"- Try `memos_search` to find the memory again",
|
|
204
|
+
].join("\n");
|
|
205
|
+
}
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// memos_get_stats
|
|
208
|
+
// ============================================================================
|
|
209
|
+
export async function handleMemosGetStats(arguments_) {
|
|
210
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
211
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
212
|
+
if (!regSuccess)
|
|
213
|
+
return cubeRegistrationError(cubeId, regError);
|
|
214
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId } }, ensureCubeRegistered);
|
|
215
|
+
if (result.success && result.data) {
|
|
216
|
+
const [stats, total] = computeMemoryStats(result.data.data ?? {});
|
|
217
|
+
if (total === 0) {
|
|
218
|
+
return [{ type: "text", text: `No memories found in cube '${cubeId}'.` }];
|
|
219
|
+
}
|
|
220
|
+
const typeIcons = {
|
|
221
|
+
BUGFIX: "🐛", ERROR_PATTERN: "🔴", DECISION: "📋",
|
|
222
|
+
GOTCHA: "⚠️", CODE_PATTERN: "📝", CONFIG: "⚙️",
|
|
223
|
+
FEATURE: "✨", MILESTONE: "🎯", PROGRESS: "📊", INFERRED: "🔗",
|
|
224
|
+
};
|
|
225
|
+
const result_lines = [`## 📊 Memory Stats: ${cubeId}`, `Total Memories: **${total}**`, ""];
|
|
226
|
+
for (const [mtype, count] of Object.entries(stats).sort((a, b) => b[1] - a[1])) {
|
|
227
|
+
const percentage = (count / total * 100).toFixed(1);
|
|
228
|
+
const icon = typeIcons[mtype] ?? "📌";
|
|
229
|
+
result_lines.push(`- ${icon} **${mtype}**: ${count} (${percentage}%)`);
|
|
230
|
+
}
|
|
231
|
+
const inferredCount = stats.INFERRED ?? 0;
|
|
232
|
+
const progressCount = stats.PROGRESS ?? 0;
|
|
233
|
+
const userTyped = total - inferredCount - progressCount;
|
|
234
|
+
if (inferredCount > 0) {
|
|
235
|
+
result_lines.push("", "---", "", `ℹ️ **INFERRED** (${inferredCount} 条): 图数据库自动生成的因果推断节点,非用户保存,属正常现象。`);
|
|
236
|
+
}
|
|
237
|
+
if (total > 0 && progressCount / total > 0.5) {
|
|
238
|
+
result_lines.push("", "---", "", `⚠️ **PROGRESS 占比偏高** (${progressCount}/${total}): 保存时建议显式指定类型:`);
|
|
239
|
+
result_lines.push(" `BUGFIX` · `DECISION` · `MILESTONE` · `FEATURE` · `GOTCHA` · `CONFIG`");
|
|
240
|
+
}
|
|
241
|
+
if (userTyped > 0) {
|
|
242
|
+
result_lines.push("", `✅ **用户标注记忆**: ${userTyped} 条 (${(userTyped / total * 100).toFixed(0)}%)`);
|
|
243
|
+
}
|
|
244
|
+
return [{ type: "text", text: result_lines.join("\n") }];
|
|
245
|
+
}
|
|
246
|
+
else if (result.data) {
|
|
247
|
+
return apiErrorResponse("Stats", String(result.data.message ?? "Unknown error"));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
return apiErrorResponse("Stats", `HTTP ${result.status}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Helpers
|
|
255
|
+
// ============================================================================
|
|
256
|
+
export function extractMemoriesFromData(data) {
|
|
257
|
+
const memories = [];
|
|
258
|
+
const textMems = data.text_mem ?? [];
|
|
259
|
+
for (const cubeData of textMems) {
|
|
260
|
+
const memData = cubeData.memories;
|
|
261
|
+
if (memData && !Array.isArray(memData) && memData.nodes) {
|
|
262
|
+
memories.push(...memData.nodes);
|
|
263
|
+
}
|
|
264
|
+
else if (Array.isArray(memData)) {
|
|
265
|
+
memories.push(...memData);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return memories;
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Handlers
|
|
3
|
+
*
|
|
4
|
+
* memos_search, memos_search_context, memos_suggest, memos_context_resume
|
|
5
|
+
*/
|
|
6
|
+
import type { TextContent } from "../types.js";
|
|
7
|
+
export declare function handleMemosSearch(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
8
|
+
export declare function handleMemosSearchContext(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
9
|
+
export declare function handleMemosSuggest(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
10
|
+
export declare function handleMemosContextResume(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
11
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Handlers
|
|
3
|
+
*
|
|
4
|
+
* memos_search, memos_search_context, memos_suggest, memos_context_resume
|
|
5
|
+
*/
|
|
6
|
+
import { MEMOS_URL, MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, logger } from "../config.js";
|
|
7
|
+
import { apiCallWithRetry, fetchWithTimeout } from "../api-client.js";
|
|
8
|
+
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
|
+
import { formatMemoriesForDisplay } from "../formatters.js";
|
|
10
|
+
import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, } from "../models.js";
|
|
11
|
+
import { applyKeywordRerank, detectQueryIntent, filterEdgesByIntent, filterMemoriesByType, getIntentDescription, parseMemoryTypePrefix, } from "../query-processing.js";
|
|
12
|
+
import { suggestSearchQueries } from "../memory-analysis.js";
|
|
13
|
+
import { apiErrorResponse, cubeRegistrationError, getCubeIdFromArgs, } from "./utils.js";
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Temporal Graph Query (Neo4j)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
async function getTemporalMemories(cubeId, topK = 10, timeWindowHours) {
|
|
18
|
+
if (!NEO4J_HTTP_URL || !NEO4J_USER || !NEO4J_PASSWORD) {
|
|
19
|
+
logger.debug("Neo4j config missing, skipping temporal query");
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const auth = Buffer.from(`${NEO4J_USER}:${NEO4J_PASSWORD}`).toString("base64");
|
|
23
|
+
const timeFilter = timeWindowHours
|
|
24
|
+
? `AND n.updated_at >= datetime() - duration({hours: ${timeWindowHours}})`
|
|
25
|
+
: "";
|
|
26
|
+
const cypher = `
|
|
27
|
+
MATCH (n:Memory)
|
|
28
|
+
WHERE n.user_name = $user_name
|
|
29
|
+
AND n.status = 'activated'
|
|
30
|
+
${timeFilter}
|
|
31
|
+
RETURN n.id AS id, n.memory AS memory, n.key AS key,
|
|
32
|
+
n.updated_at AS updated_at, n.background AS background,
|
|
33
|
+
n.tags AS tags
|
|
34
|
+
ORDER BY n.updated_at DESC
|
|
35
|
+
LIMIT $top_k
|
|
36
|
+
`;
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetchWithTimeout(NEO4J_HTTP_URL, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Basic ${auth}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
statements: [
|
|
46
|
+
{
|
|
47
|
+
statement: cypher,
|
|
48
|
+
parameters: { user_name: MEMOS_USER, top_k: topK },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
timeoutMs: 10,
|
|
53
|
+
});
|
|
54
|
+
if (response.ok) {
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
const errors = data.errors ?? [];
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
logger.warning(`Neo4j temporal query errors: ${JSON.stringify(errors)}`);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const rows = (data.results?.[0]?.data ?? []);
|
|
62
|
+
const memories = [];
|
|
63
|
+
for (let i = 0; i < rows.length; i++) {
|
|
64
|
+
const row = rows[i];
|
|
65
|
+
const r = row.row ?? [];
|
|
66
|
+
if (r.length >= 4) {
|
|
67
|
+
memories.push({
|
|
68
|
+
id: String(r[0] ?? ""),
|
|
69
|
+
memory: String(r[1] ?? ""),
|
|
70
|
+
key: String(r[2] ?? ""),
|
|
71
|
+
updated_at: String(r[3] ?? ""),
|
|
72
|
+
background: r.length > 4 ? String(r[4] ?? "") : "",
|
|
73
|
+
tags: Array.isArray(r[5]) ? r[5] : [],
|
|
74
|
+
metadata: {
|
|
75
|
+
relativity: 0.8 - i * 0.05,
|
|
76
|
+
temporal_rank: i + 1,
|
|
77
|
+
source: "temporal_query",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
logger.info(`Temporal query returned ${memories.length} memories`);
|
|
83
|
+
return memories;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.error(`Temporal query error: ${err}`);
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
function mergeTemporalResults(searchData, temporalMemories, intent) {
|
|
92
|
+
if (temporalMemories.length === 0)
|
|
93
|
+
return searchData;
|
|
94
|
+
const existingIds = new Set();
|
|
95
|
+
const textMem = searchData.text_mem ?? [];
|
|
96
|
+
for (const bucket of textMem) {
|
|
97
|
+
const memData = bucket.memories;
|
|
98
|
+
const memories = Array.isArray(memData)
|
|
99
|
+
? memData
|
|
100
|
+
: (memData?.nodes ?? []);
|
|
101
|
+
for (const mem of memories) {
|
|
102
|
+
const id = mem.id ?? mem.metadata?.id;
|
|
103
|
+
if (id)
|
|
104
|
+
existingIds.add(id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const newTemporal = temporalMemories.filter((m) => !existingIds.has(m.id));
|
|
108
|
+
if (newTemporal.length === 0)
|
|
109
|
+
return searchData;
|
|
110
|
+
if (intent === "temporal") {
|
|
111
|
+
const temporalBucket = {
|
|
112
|
+
cube_id: "temporal",
|
|
113
|
+
memories: { nodes: newTemporal, edges: [] },
|
|
114
|
+
_source: "temporal_graph_query",
|
|
115
|
+
};
|
|
116
|
+
searchData.text_mem = [temporalBucket, ...textMem];
|
|
117
|
+
}
|
|
118
|
+
return searchData;
|
|
119
|
+
}
|
|
120
|
+
function extractSearchMemories(data) {
|
|
121
|
+
const memories = [];
|
|
122
|
+
for (const cubeData of data.text_mem ?? []) {
|
|
123
|
+
const memData = cubeData.memories;
|
|
124
|
+
if (memData && !Array.isArray(memData) && memData.nodes) {
|
|
125
|
+
memories.push(...memData.nodes);
|
|
126
|
+
}
|
|
127
|
+
else if (Array.isArray(memData)) {
|
|
128
|
+
memories.push(...memData);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return memories;
|
|
132
|
+
}
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// memos_search
|
|
135
|
+
// ============================================================================
|
|
136
|
+
export async function handleMemosSearch(arguments_) {
|
|
137
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
138
|
+
const rawQuery = String(arguments_.query ?? "");
|
|
139
|
+
const topK = Number(arguments_.top_k ?? 10);
|
|
140
|
+
const compact = arguments_.compact !== false;
|
|
141
|
+
const [memType, cleanedQuery] = parseMemoryTypePrefix(rawQuery);
|
|
142
|
+
const query = cleanedQuery || rawQuery;
|
|
143
|
+
const intent = detectQueryIntent(query);
|
|
144
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
145
|
+
if (!regSuccess)
|
|
146
|
+
return cubeRegistrationError(cubeId, regError);
|
|
147
|
+
const apiResult = await apiCallWithRetry("POST", `${MEMOS_URL}/search`, cubeId, {
|
|
148
|
+
body: {
|
|
149
|
+
user_id: MEMOS_USER,
|
|
150
|
+
query,
|
|
151
|
+
install_cube_ids: [cubeId],
|
|
152
|
+
top_k: topK,
|
|
153
|
+
},
|
|
154
|
+
}, ensureCubeRegistered);
|
|
155
|
+
if (apiResult.success && apiResult.data) {
|
|
156
|
+
let resultData = apiResult.data.data ?? {};
|
|
157
|
+
// Temporal enhancement
|
|
158
|
+
if (intent === "temporal") {
|
|
159
|
+
let timeWindow;
|
|
160
|
+
const timeMatch = query.match(/(\d+)\s*(?:小时|hour|h)/i);
|
|
161
|
+
if (timeMatch)
|
|
162
|
+
timeWindow = parseInt(timeMatch[1]);
|
|
163
|
+
else if (/今天|today/i.test(query))
|
|
164
|
+
timeWindow = 24;
|
|
165
|
+
else if (/本周|this\s*week|week/i.test(query))
|
|
166
|
+
timeWindow = 168;
|
|
167
|
+
const temporalMemories = await getTemporalMemories(cubeId, topK, timeWindow);
|
|
168
|
+
resultData = mergeTemporalResults(resultData, temporalMemories, intent);
|
|
169
|
+
}
|
|
170
|
+
resultData = filterEdgesByIntent(resultData, intent);
|
|
171
|
+
resultData = filterMemoriesByType(resultData, memType);
|
|
172
|
+
const keywordQuery = memType ? cleanedQuery : query;
|
|
173
|
+
resultData = applyKeywordRerank(resultData, keywordQuery);
|
|
174
|
+
const allMemories = extractSearchMemories(resultData);
|
|
175
|
+
const totalCount = allMemories.length;
|
|
176
|
+
if (compact && shouldCompact(totalCount)) {
|
|
177
|
+
const preview = allMemories.slice(0, PREVIEW_COUNT).map(toMinimal);
|
|
178
|
+
let text = compactedResultToText({
|
|
179
|
+
preview,
|
|
180
|
+
totalCount,
|
|
181
|
+
omittedCount: totalCount - preview.length,
|
|
182
|
+
message: 'Use memos_get(memory_id="<id>") for full details',
|
|
183
|
+
query: rawQuery,
|
|
184
|
+
cubeId,
|
|
185
|
+
});
|
|
186
|
+
if (intent !== "default") {
|
|
187
|
+
const intentDesc = getIntentDescription(intent);
|
|
188
|
+
text = `*${intentDesc}*\n\n${text}`;
|
|
189
|
+
}
|
|
190
|
+
return [{ type: "text", text }];
|
|
191
|
+
}
|
|
192
|
+
let formatted = formatMemoriesForDisplay(resultData);
|
|
193
|
+
if (intent !== "default") {
|
|
194
|
+
const intentDesc = getIntentDescription(intent);
|
|
195
|
+
formatted = `*${intentDesc}*\n\n${formatted}`;
|
|
196
|
+
}
|
|
197
|
+
return [{ type: "text", text: formatted }];
|
|
198
|
+
}
|
|
199
|
+
else if (apiResult.data) {
|
|
200
|
+
return apiErrorResponse("Search", String(apiResult.data.message ?? "Unknown error"));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
return apiErrorResponse("Search", `HTTP ${apiResult.status}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// memos_search_context
|
|
208
|
+
// ============================================================================
|
|
209
|
+
export async function handleMemosSearchContext(arguments_) {
|
|
210
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
211
|
+
const rawQuery = String(arguments_.query ?? "");
|
|
212
|
+
const context = arguments_.context ?? [];
|
|
213
|
+
const [memType, cleanedQuery] = parseMemoryTypePrefix(rawQuery);
|
|
214
|
+
const query = cleanedQuery || rawQuery;
|
|
215
|
+
const contextText = context.slice(-5).map((m) => m.content ?? "").join(" ");
|
|
216
|
+
const intent = detectQueryIntent(`${query} ${contextText}`);
|
|
217
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
218
|
+
if (!regSuccess)
|
|
219
|
+
return cubeRegistrationError(cubeId, regError);
|
|
220
|
+
const chatHistory = context.slice(-10)
|
|
221
|
+
.filter((m) => m.content)
|
|
222
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetchWithTimeout(`${MEMOS_URL}/search`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
user_id: MEMOS_USER,
|
|
229
|
+
query,
|
|
230
|
+
readable_cube_ids: [cubeId],
|
|
231
|
+
enable_context_analysis: true,
|
|
232
|
+
chat_history: chatHistory,
|
|
233
|
+
top_k: 15,
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
if (response.ok) {
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
if (data.code === 200) {
|
|
239
|
+
const results = [];
|
|
240
|
+
const intentDesc = getIntentDescription(intent);
|
|
241
|
+
results.push(`## ${intentDesc}`, "");
|
|
242
|
+
let resultData = data.data ?? {};
|
|
243
|
+
if (intent === "temporal") {
|
|
244
|
+
const temporalMemories = await getTemporalMemories(cubeId, 15);
|
|
245
|
+
resultData = mergeTemporalResults(resultData, temporalMemories, intent);
|
|
246
|
+
}
|
|
247
|
+
resultData = filterEdgesByIntent(resultData, intent);
|
|
248
|
+
resultData = filterMemoriesByType(resultData, memType);
|
|
249
|
+
const keywordQuery = memType ? cleanedQuery : query;
|
|
250
|
+
resultData = applyKeywordRerank(resultData, keywordQuery);
|
|
251
|
+
const formatted = formatMemoriesForDisplay(resultData);
|
|
252
|
+
if (context.length > 0) {
|
|
253
|
+
results.push(`*Analyzed with ${context.length} context messages*`, "");
|
|
254
|
+
}
|
|
255
|
+
results.push(formatted);
|
|
256
|
+
return [{ type: "text", text: results.join("\n") }];
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Fallback to standard search
|
|
260
|
+
const fallbackResult = await apiCallWithRetry("POST", `${MEMOS_URL}/search`, cubeId, { body: { user_id: MEMOS_USER, query, install_cube_ids: [cubeId] } }, ensureCubeRegistered);
|
|
261
|
+
if (fallbackResult.success && fallbackResult.data) {
|
|
262
|
+
let resultData = fallbackResult.data.data ?? {};
|
|
263
|
+
resultData = filterEdgesByIntent(resultData, intent);
|
|
264
|
+
resultData = filterMemoriesByType(resultData, memType);
|
|
265
|
+
const keywordQuery = memType ? cleanedQuery : query;
|
|
266
|
+
resultData = applyKeywordRerank(resultData, keywordQuery);
|
|
267
|
+
const formatted = formatMemoriesForDisplay(resultData);
|
|
268
|
+
return [{ type: "text", text: `## Search Results (fallback)\n\n${formatted}` }];
|
|
269
|
+
}
|
|
270
|
+
return apiErrorResponse("Context search", String(data.message ?? "Unknown error"));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
return apiErrorResponse("Context search", `HTTP ${response.status}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
return apiErrorResponse("Context search", String(err));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// memos_suggest
|
|
283
|
+
// ============================================================================
|
|
284
|
+
export async function handleMemosSuggest(arguments_) {
|
|
285
|
+
const context = String(arguments_.context ?? "");
|
|
286
|
+
const suggestions = suggestSearchQueries(context);
|
|
287
|
+
if (suggestions.length > 0) {
|
|
288
|
+
const result = ["## 🔍 Suggested Searches\n", "Based on your context, try these searches:\n"];
|
|
289
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
290
|
+
result.push(`${i + 1}. \`${suggestions[i]}\``);
|
|
291
|
+
}
|
|
292
|
+
return [{ type: "text", text: result.join("\n") }];
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
return [{ type: "text", text: "No specific suggestions. Try searching with keywords from your context." }];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// memos_context_resume
|
|
300
|
+
// ============================================================================
|
|
301
|
+
export async function handleMemosContextResume(arguments_) {
|
|
302
|
+
const cubeId = getCubeIdFromArgs(arguments_);
|
|
303
|
+
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
304
|
+
if (!regSuccess)
|
|
305
|
+
return cubeRegistrationError(cubeId, regError);
|
|
306
|
+
// Try temporal query first
|
|
307
|
+
let recentMemories = await getTemporalMemories(cubeId, 10, 24);
|
|
308
|
+
// Fallback to API list
|
|
309
|
+
if (recentMemories.length === 0) {
|
|
310
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 10 } }, ensureCubeRegistered);
|
|
311
|
+
if (result.success && result.data) {
|
|
312
|
+
recentMemories = extractSearchMemories(result.data.data ?? {});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const lines = ["## Context Resumed", ""];
|
|
316
|
+
if (recentMemories.length > 0) {
|
|
317
|
+
lines.push(`**Recent memories** (${recentMemories.length} items, last 24h):`, "");
|
|
318
|
+
for (let i = 0; i < Math.min(recentMemories.length, 10); i++) {
|
|
319
|
+
const mem = recentMemories[i];
|
|
320
|
+
const content = mem.memory ?? "";
|
|
321
|
+
const summary = content.slice(0, 120).split("\n")[0];
|
|
322
|
+
lines.push(`${i + 1}. ${summary}`);
|
|
323
|
+
}
|
|
324
|
+
lines.push("");
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
lines.push("No recent memories found in this cube.", "");
|
|
328
|
+
}
|
|
329
|
+
lines.push("---");
|
|
330
|
+
lines.push("**REMINDER**: Use MCP memos tools (`memos_save`, `memos_search`) for ALL memory operations.");
|
|
331
|
+
lines.push("NEVER use `mkdir` or `Write` to create memory files.");
|
|
332
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
333
|
+
}
|
|
334
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared error formatting and cube ID resolution.
|
|
5
|
+
*/
|
|
6
|
+
import type { TextContent } from "../types.js";
|
|
7
|
+
export declare const ERR_API_UNREACHABLE = "API_UNREACHABLE";
|
|
8
|
+
export declare const ERR_API_ERROR = "API_ERROR";
|
|
9
|
+
export declare const ERR_CUBE_NOT_FOUND = "CUBE_NOT_FOUND";
|
|
10
|
+
export declare const ERR_CUBE_REGISTRATION = "CUBE_REGISTRATION_FAILED";
|
|
11
|
+
export declare const ERR_PARAM_MISSING = "PARAM_MISSING";
|
|
12
|
+
export declare const ERR_PARAM_INVALID = "PARAM_INVALID";
|
|
13
|
+
export declare const ERR_NEO4J_CONFIG = "NEO4J_CONFIG_MISSING";
|
|
14
|
+
export declare const ERR_OPERATION_FAILED = "OPERATION_FAILED";
|
|
15
|
+
export declare const ERR_DELETE_DISABLED = "DELETE_DISABLED";
|
|
16
|
+
export declare const ERR_USER_ERROR = "USER_ERROR";
|
|
17
|
+
export declare function errorResponse(message: string, errorCode?: string, suggestions?: string[]): TextContent[];
|
|
18
|
+
export declare function successResponse(message: string): TextContent[];
|
|
19
|
+
export declare function cubeRegistrationError(cubeId: string, detail: string | null): TextContent[];
|
|
20
|
+
export declare function apiErrorResponse(operation: string, statusOrMsg: string | number): TextContent[];
|
|
21
|
+
export declare function getCubeIdFromArgs(arguments_: Record<string, unknown>): string;
|
|
22
|
+
//# sourceMappingURL=utils.d.ts.map
|