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.
Files changed (41) hide show
  1. package/.env.example +27 -0
  2. package/README.md +71 -0
  3. package/dist/api-client.d.ts +22 -0
  4. package/dist/api-client.js +127 -0
  5. package/dist/config.d.ts +30 -0
  6. package/dist/config.js +163 -0
  7. package/dist/cube-manager.d.ts +16 -0
  8. package/dist/cube-manager.js +357 -0
  9. package/dist/formatters.d.ts +16 -0
  10. package/dist/formatters.js +174 -0
  11. package/dist/handlers/admin.d.ts +13 -0
  12. package/dist/handlers/admin.js +331 -0
  13. package/dist/handlers/calendar.d.ts +8 -0
  14. package/dist/handlers/calendar.js +281 -0
  15. package/dist/handlers/graph.d.ts +11 -0
  16. package/dist/handlers/graph.js +502 -0
  17. package/dist/handlers/index.d.ts +7 -0
  18. package/dist/handlers/index.js +71 -0
  19. package/dist/handlers/memory.d.ts +12 -0
  20. package/dist/handlers/memory.js +270 -0
  21. package/dist/handlers/search.d.ts +11 -0
  22. package/dist/handlers/search.js +334 -0
  23. package/dist/handlers/utils.d.ts +22 -0
  24. package/dist/handlers/utils.js +75 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.js +18 -0
  27. package/dist/keyword-enhancer.d.ts +18 -0
  28. package/dist/keyword-enhancer.js +335 -0
  29. package/dist/memory-analysis.d.ts +7 -0
  30. package/dist/memory-analysis.js +45 -0
  31. package/dist/models.d.ts +21 -0
  32. package/dist/models.js +101 -0
  33. package/dist/query-processing.d.ts +20 -0
  34. package/dist/query-processing.js +265 -0
  35. package/dist/server.d.ts +5 -0
  36. package/dist/server.js +114 -0
  37. package/dist/tools-registry.d.ts +303 -0
  38. package/dist/tools-registry.js +369 -0
  39. package/dist/types.d.ts +118 -0
  40. package/dist/types.js +5 -0
  41. 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