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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared error formatting and cube ID resolution.
|
|
5
|
+
*/
|
|
6
|
+
import { detectCubeFromPath } from "../keyword-enhancer.js";
|
|
7
|
+
import { getDefaultCubeId } from "../cube-manager.js";
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Error Codes
|
|
10
|
+
// ============================================================================
|
|
11
|
+
export const ERR_API_UNREACHABLE = "API_UNREACHABLE";
|
|
12
|
+
export const ERR_API_ERROR = "API_ERROR";
|
|
13
|
+
export const ERR_CUBE_NOT_FOUND = "CUBE_NOT_FOUND";
|
|
14
|
+
export const ERR_CUBE_REGISTRATION = "CUBE_REGISTRATION_FAILED";
|
|
15
|
+
export const ERR_PARAM_MISSING = "PARAM_MISSING";
|
|
16
|
+
export const ERR_PARAM_INVALID = "PARAM_INVALID";
|
|
17
|
+
export const ERR_NEO4J_CONFIG = "NEO4J_CONFIG_MISSING";
|
|
18
|
+
export const ERR_OPERATION_FAILED = "OPERATION_FAILED";
|
|
19
|
+
export const ERR_DELETE_DISABLED = "DELETE_DISABLED";
|
|
20
|
+
export const ERR_USER_ERROR = "USER_ERROR";
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Response Helpers
|
|
23
|
+
// ============================================================================
|
|
24
|
+
export function errorResponse(message, errorCode, suggestions) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (errorCode) {
|
|
27
|
+
parts.push(`❌ [${errorCode}] ${message}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
parts.push(`❌ ${message}`);
|
|
31
|
+
}
|
|
32
|
+
if (suggestions && suggestions.length > 0) {
|
|
33
|
+
parts.push("");
|
|
34
|
+
parts.push("💡 Suggestions:");
|
|
35
|
+
for (const s of suggestions) {
|
|
36
|
+
parts.push(`- ${s}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [{ type: "text", text: parts.join("\n") }];
|
|
40
|
+
}
|
|
41
|
+
export function successResponse(message) {
|
|
42
|
+
return [{ type: "text", text: message }];
|
|
43
|
+
}
|
|
44
|
+
export function cubeRegistrationError(cubeId, detail) {
|
|
45
|
+
return errorResponse(`Cube '${cubeId}' registration failed: ${detail ?? "unknown error"}`, ERR_CUBE_REGISTRATION, [
|
|
46
|
+
`Check if MemOS API is running: \`curl http://localhost:18000/health\``,
|
|
47
|
+
`Verify cube exists: \`memos_list_cubes(include_status=True)\``,
|
|
48
|
+
`Try manual registration: \`memos_register_cube(cube_id="...")\``,
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
export function apiErrorResponse(operation, statusOrMsg) {
|
|
52
|
+
return errorResponse(`${operation} failed: ${statusOrMsg}`, ERR_API_ERROR, [
|
|
53
|
+
"Check API health: `curl http://localhost:18000/health/detail`",
|
|
54
|
+
"Check API logs for details",
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Cube ID Resolution
|
|
59
|
+
// ============================================================================
|
|
60
|
+
export function getCubeIdFromArgs(arguments_) {
|
|
61
|
+
const argCubeId = arguments_.cube_id;
|
|
62
|
+
const projectPath = arguments_.project_path;
|
|
63
|
+
if (projectPath) {
|
|
64
|
+
try {
|
|
65
|
+
const derived = detectCubeFromPath(projectPath);
|
|
66
|
+
if (derived)
|
|
67
|
+
return derived;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return argCubeId ?? getDefaultCubeId();
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=utils.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* oh-memos-mcp — MCP Server for MemOS Intelligent Memory Management
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx oh-memos-mcp
|
|
7
|
+
* node dist/index.js
|
|
8
|
+
*
|
|
9
|
+
* Configuration via environment variables or CLI args.
|
|
10
|
+
* See README.md for full documentation.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* oh-memos-mcp — MCP Server for MemOS Intelligent Memory Management
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx oh-memos-mcp
|
|
7
|
+
* node dist/index.js
|
|
8
|
+
*
|
|
9
|
+
* Configuration via environment variables or CLI args.
|
|
10
|
+
* See README.md for full documentation.
|
|
11
|
+
*/
|
|
12
|
+
import { runServer } from "./server.js";
|
|
13
|
+
import { logger } from "./config.js";
|
|
14
|
+
runServer().catch((err) => {
|
|
15
|
+
logger.error(`Fatal error: ${err}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyword Enhancer Module
|
|
3
|
+
*
|
|
4
|
+
* - Extended stopwords (programming/technical + Chinese)
|
|
5
|
+
* - Fuzzy matching with Levenshtein distance
|
|
6
|
+
* - Smart cube auto-detection from project path
|
|
7
|
+
* - Structured field weighting
|
|
8
|
+
*/
|
|
9
|
+
export declare const ENGLISH_STOPWORDS: Set<string>;
|
|
10
|
+
export declare const CHINESE_STOPWORDS: Set<string>;
|
|
11
|
+
export declare const ALL_STOPWORDS: Set<string>;
|
|
12
|
+
export declare function levenshteinDistance(s1: string, s2: string): number;
|
|
13
|
+
export declare function fuzzyMatchScore(query: string, text: string, threshold?: number): number;
|
|
14
|
+
export declare function findFuzzyMatches(keyword: string, text: string, threshold?: number, maxWordLenDiff?: number): Array<[string, number]>;
|
|
15
|
+
export declare function detectCubeFromPath(projectPath?: string): string;
|
|
16
|
+
export declare function extractKeywordsEnhanced(query: string, stopwords?: Set<string>, minLength?: number): string[];
|
|
17
|
+
export declare function keywordMatchScoreEnhanced(text: string, keywords: string[], metadata?: Record<string, unknown>, enableFuzzy?: boolean, fuzzyThreshold?: number): number;
|
|
18
|
+
//# sourceMappingURL=keyword-enhancer.d.ts.map
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyword Enhancer Module
|
|
3
|
+
*
|
|
4
|
+
* - Extended stopwords (programming/technical + Chinese)
|
|
5
|
+
* - Fuzzy matching with Levenshtein distance
|
|
6
|
+
* - Smart cube auto-detection from project path
|
|
7
|
+
* - Structured field weighting
|
|
8
|
+
*/
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Extended Stopwords
|
|
11
|
+
// =============================================================================
|
|
12
|
+
export const ENGLISH_STOPWORDS = new Set([
|
|
13
|
+
// General English
|
|
14
|
+
"the", "and", "or", "a", "an", "to", "of", "for", "with", "in", "on",
|
|
15
|
+
"at", "is", "are", "was", "were", "be", "been", "being", "this", "that",
|
|
16
|
+
"it", "as", "from", "by", "about", "into", "over", "after", "before",
|
|
17
|
+
"not", "but", "if", "then", "else", "when", "where", "which", "who",
|
|
18
|
+
"whom", "whose", "why", "how", "all", "each", "every", "both", "few",
|
|
19
|
+
"more", "most", "other", "some", "such", "no", "nor", "only", "own",
|
|
20
|
+
"same", "so", "than", "too", "very", "just", "can", "will", "should",
|
|
21
|
+
"would", "could", "may", "might", "must", "shall", "do", "does", "did",
|
|
22
|
+
"doing", "done", "have", "has", "had", "having", "get", "got", "gets",
|
|
23
|
+
"getting", "make", "makes", "made", "making", "go", "goes", "went",
|
|
24
|
+
"going", "come", "comes", "came", "coming", "take", "takes", "took",
|
|
25
|
+
"taking", "see", "sees", "saw", "seeing", "know", "knows", "knew",
|
|
26
|
+
"knowing", "think", "thinks", "thought", "thinking", "want", "wants",
|
|
27
|
+
"wanted", "wanting", "use", "uses", "used", "using", "find", "finds",
|
|
28
|
+
"found", "finding", "give", "gives", "gave", "giving", "tell", "tells",
|
|
29
|
+
"told", "telling", "say", "says", "said", "saying", "try", "tries",
|
|
30
|
+
"tried", "trying", "need", "needs", "needed", "needing", "let", "lets",
|
|
31
|
+
"put", "puts", "keep", "keeps", "kept", "keeping", "begin", "begins",
|
|
32
|
+
"began", "beginning", "show", "shows", "showed", "showing",
|
|
33
|
+
// Programming common words (usually not meaningful for search)
|
|
34
|
+
"todo", "fixme", "hack", "note", "xxx", "temp", "tmp", "foo", "bar",
|
|
35
|
+
"baz", "qux", "test", "example", "sample", "demo", "main", "index",
|
|
36
|
+
"app", "application", "module", "package", "import", "export", "require",
|
|
37
|
+
"define", "declare", "const", "var", "function", "class", "method",
|
|
38
|
+
"return", "void", "null", "undefined", "true", "false", "new", "delete",
|
|
39
|
+
"typeof", "instanceof", "catch", "finally", "throw", "async",
|
|
40
|
+
"await", "yield", "public", "private", "protected", "static", "final",
|
|
41
|
+
"abstract", "interface", "extends", "implements", "super", "self", "cls",
|
|
42
|
+
"args", "kwargs", "param", "params", "arg", "argument", "arguments",
|
|
43
|
+
"value", "values", "result", "results", "data", "info", "item", "items",
|
|
44
|
+
"list", "array", "object", "dict", "map", "tuple", "string",
|
|
45
|
+
"str", "int", "float", "bool", "boolean", "number", "num", "char",
|
|
46
|
+
"byte", "bytes", "type", "types", "name", "names", "key", "keys",
|
|
47
|
+
"val", "vals", "obj", "objs", "elem", "elems", "element", "elements",
|
|
48
|
+
"node", "nodes", "attr", "attrs", "attribute", "attributes", "prop",
|
|
49
|
+
"props", "property", "properties", "opt", "opts", "option", "options",
|
|
50
|
+
"cfg", "conf", "config", "configuration", "setting", "settings",
|
|
51
|
+
"env", "environment", "ctx", "context", "req", "request", "res",
|
|
52
|
+
"response", "err", "error", "errors", "msg", "message", "messages",
|
|
53
|
+
"log", "logs", "debug", "warn", "warning", "warnings", "input", "output",
|
|
54
|
+
"out", "src", "source", "dest", "destination", "target", "path", "paths",
|
|
55
|
+
"file", "files", "dir", "directory", "directories", "folder", "folders",
|
|
56
|
+
"url", "urls", "uri", "uris", "id", "ids", "uid", "uuid", "guid",
|
|
57
|
+
"ref", "refs", "reference", "references", "ptr", "pointer", "pointers",
|
|
58
|
+
"handle", "handles", "callback", "callbacks", "handler", "handlers",
|
|
59
|
+
"listener", "listeners", "event", "events", "action", "actions",
|
|
60
|
+
"state", "states", "status", "flag", "flags", "mode", "modes",
|
|
61
|
+
"level", "levels", "size", "sizes", "length", "len", "count", "counts",
|
|
62
|
+
"total", "sum", "avg", "average", "min", "max", "first", "last",
|
|
63
|
+
"prev", "previous", "next", "current", "cur", "old", "start", "end",
|
|
64
|
+
"init", "initialize", "initialized", "setup", "teardown", "cleanup",
|
|
65
|
+
"reset", "clear", "load", "save", "post", "update", "remove", "insert",
|
|
66
|
+
"append", "push", "pop", "shift", "unshift", "sub", "mul", "div",
|
|
67
|
+
"mod", "xor", "bit", "bits", "word", "words", "line", "lines",
|
|
68
|
+
"row", "rows", "col", "column", "columns", "cell", "cells", "grid",
|
|
69
|
+
"grids", "table", "tables", "record", "records", "field", "fields",
|
|
70
|
+
"schema", "schemas", "model", "models", "view", "views", "controller",
|
|
71
|
+
"controllers", "service", "services", "repository", "repositories",
|
|
72
|
+
"factory", "factories", "builder", "builders", "adapter", "adapters",
|
|
73
|
+
"wrapper", "wrappers", "helper", "helpers", "util", "utils", "utility",
|
|
74
|
+
"utilities", "tool", "tools", "lib", "library", "libraries", "framework",
|
|
75
|
+
"frameworks", "plugin", "plugins", "extension", "extensions",
|
|
76
|
+
"component", "components", "widget", "widgets", "block", "blocks",
|
|
77
|
+
"section", "sections", "part", "parts", "piece", "pieces", "chunk",
|
|
78
|
+
"chunks", "segment", "segments", "unit", "units", "modules", "system",
|
|
79
|
+
"systems", "layer", "layers", "process", "processes", "thread", "threads",
|
|
80
|
+
"task", "tasks", "job", "jobs", "work", "works", "worker", "workers",
|
|
81
|
+
"queue", "queues", "stack", "stacks", "pool", "pools", "cache", "caches",
|
|
82
|
+
"buffer", "buffers", "stream", "streams", "pipe", "pipes", "channel",
|
|
83
|
+
"channels", "socket", "sockets", "port", "ports", "host", "hosts",
|
|
84
|
+
"server", "servers", "client", "clients", "user", "users", "admin",
|
|
85
|
+
"admins", "root", "guest", "account", "accounts", "profile", "profiles",
|
|
86
|
+
"session", "sessions", "token", "tokens", "auth", "authentication",
|
|
87
|
+
"authorization", "permission", "permissions", "role", "roles", "access",
|
|
88
|
+
"grant", "grants", "deny", "denies", "filter", "filters",
|
|
89
|
+
"validate", "validates", "validation", "check", "checks", "verify",
|
|
90
|
+
"verifies", "verification", "confirm", "confirms", "confirmation",
|
|
91
|
+
"approve", "approves", "approval", "reject", "rejects", "rejection",
|
|
92
|
+
"accept", "accepts", "acceptance", "decline", "declines", "cancel",
|
|
93
|
+
"cancels", "cancellation", "abort", "aborts", "retry", "retries",
|
|
94
|
+
"timeout", "timeouts", "expire", "expires", "expiration",
|
|
95
|
+
"refresh", "refreshes", "renew", "renews", "renewal",
|
|
96
|
+
]);
|
|
97
|
+
export const CHINESE_STOPWORDS = new Set([
|
|
98
|
+
"的", "了", "和", "与", "在", "是", "有", "我", "你", "他", "她", "它",
|
|
99
|
+
"我们", "你们", "他们", "她们", "它们", "这", "那", "这个", "那个",
|
|
100
|
+
"这些", "那些", "这里", "那里", "这儿", "那儿", "什么", "怎么", "怎样",
|
|
101
|
+
"如何", "为什么", "哪", "哪个", "哪些", "哪里", "哪儿", "谁", "多少",
|
|
102
|
+
"几", "多", "少", "大", "小", "好", "坏", "对", "错", "行", "不行",
|
|
103
|
+
"可以", "不可以", "能", "不能", "会", "不会", "要", "不要", "想",
|
|
104
|
+
"不想", "应该", "不应该", "必须", "不必", "可能", "不可能", "一定",
|
|
105
|
+
"也许", "或许", "大概", "肯定", "当然", "确实", "真的", "假的",
|
|
106
|
+
"及", "或", "或者", "还是", "以及", "并", "并且", "而",
|
|
107
|
+
"而且", "但", "但是", "不过", "然而", "可是", "虽然", "尽管", "即使",
|
|
108
|
+
"如果", "假如", "要是", "只要", "除非", "无论", "不管", "不论",
|
|
109
|
+
"因为", "由于", "所以", "因此", "因而", "于是", "那么", "这样",
|
|
110
|
+
"既然", "只有", "才", "就", "便", "却", "倒", "反而", "否则",
|
|
111
|
+
"不然", "要不", "要不然", "要么", "以便", "以免", "以致", "为了",
|
|
112
|
+
"为着", "对于", "关于", "至于", "根据", "按照", "依照", "通过",
|
|
113
|
+
"经过", "随着", "沿着", "顺着", "朝着", "向着", "趁着", "除了",
|
|
114
|
+
"除开", "除去", "除却", "除外",
|
|
115
|
+
"自己", "自身", "本人", "本身", "彼此", "相互", "互相", "大家",
|
|
116
|
+
"各位", "诸位", "各自", "某", "某个", "某些", "其", "其中", "其他",
|
|
117
|
+
"其它", "其余", "另", "另外", "另一", "别", "别的", "任何", "一切",
|
|
118
|
+
"所有", "全部", "整个", "每", "每个", "各", "各个", "同", "同一",
|
|
119
|
+
"同样", "一样", "一般", "一种", "某种", "这种", "那种", "各种",
|
|
120
|
+
"现在", "目前", "当前", "眼前", "此时", "此刻", "刚才", "方才",
|
|
121
|
+
"刚刚", "正在", "正", "将", "将要", "快要", "就要", "已经", "已",
|
|
122
|
+
"曾", "曾经", "过去", "以前", "之前", "以后", "之后", "后来", "然后",
|
|
123
|
+
"接着", "继而", "随后", "最后", "最终", "终于", "始终", "一直",
|
|
124
|
+
"常常", "经常", "往往", "总是", "老是", "有时", "有时候", "偶尔",
|
|
125
|
+
"突然", "忽然", "马上", "立刻", "立即", "赶紧", "赶快", "连忙",
|
|
126
|
+
"从来", "一向", "向来", "素来", "历来", "原来", "本来", "起初",
|
|
127
|
+
"很", "非常", "十分", "极", "极其", "极为", "特别", "尤其", "格外",
|
|
128
|
+
"更", "更加", "越", "越发", "越来越", "最", "顶", "太", "过于",
|
|
129
|
+
"稍", "稍微", "略", "略微", "比较", "相当", "相对", "颇", "挺",
|
|
130
|
+
"不", "没", "没有", "未", "尚未", "并不", "并非", "并没", "并没有",
|
|
131
|
+
"也", "还", "又", "再", "仍", "仍然", "依然", "依旧", "照样",
|
|
132
|
+
"都", "全", "全都", "皆", "均", "统统", "通通", "一概", "一律",
|
|
133
|
+
"只", "只是", "仅", "仅仅", "光", "单", "单独",
|
|
134
|
+
"啊", "呀", "哇", "哦", "噢", "哎", "唉", "嗨", "喂", "嘿",
|
|
135
|
+
"嗯", "哼", "吧", "呢", "吗", "嘛", "啦", "喽", "呗",
|
|
136
|
+
"罢了", "而已", "罢", "地", "得", "着", "过", "们",
|
|
137
|
+
"做", "用", "把", "被", "给", "让", "叫", "使",
|
|
138
|
+
"令", "请", "来", "去", "到", "进", "出", "上", "下", "起", "开",
|
|
139
|
+
"关", "拿", "放", "看", "听", "说", "写", "读",
|
|
140
|
+
]);
|
|
141
|
+
export const ALL_STOPWORDS = new Set([
|
|
142
|
+
...ENGLISH_STOPWORDS,
|
|
143
|
+
...CHINESE_STOPWORDS,
|
|
144
|
+
]);
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Fuzzy Matching
|
|
147
|
+
// =============================================================================
|
|
148
|
+
export function levenshteinDistance(s1, s2) {
|
|
149
|
+
if (s1.length < s2.length) {
|
|
150
|
+
[s1, s2] = [s2, s1];
|
|
151
|
+
}
|
|
152
|
+
if (s2.length === 0)
|
|
153
|
+
return s1.length;
|
|
154
|
+
let previousRow = Array.from({ length: s2.length + 1 }, (_, i) => i);
|
|
155
|
+
for (let i = 0; i < s1.length; i++) {
|
|
156
|
+
const currentRow = [i + 1];
|
|
157
|
+
for (let j = 0; j < s2.length; j++) {
|
|
158
|
+
const insertions = previousRow[j + 1] + 1;
|
|
159
|
+
const deletions = currentRow[j] + 1;
|
|
160
|
+
const substitutions = previousRow[j] + (s1[i] !== s2[j] ? 1 : 0);
|
|
161
|
+
currentRow.push(Math.min(insertions, deletions, substitutions));
|
|
162
|
+
}
|
|
163
|
+
previousRow = currentRow;
|
|
164
|
+
}
|
|
165
|
+
return previousRow[previousRow.length - 1];
|
|
166
|
+
}
|
|
167
|
+
export function fuzzyMatchScore(query, text, threshold = 0.7) {
|
|
168
|
+
if (!query || !text)
|
|
169
|
+
return 0.0;
|
|
170
|
+
const queryLower = query.toLowerCase();
|
|
171
|
+
const textLower = text.toLowerCase();
|
|
172
|
+
if (queryLower === textLower)
|
|
173
|
+
return 1.0;
|
|
174
|
+
if (textLower.includes(queryLower))
|
|
175
|
+
return 0.9;
|
|
176
|
+
const maxLen = Math.max(queryLower.length, textLower.length);
|
|
177
|
+
if (maxLen === 0)
|
|
178
|
+
return 0.0;
|
|
179
|
+
const distance = levenshteinDistance(queryLower, textLower);
|
|
180
|
+
const similarity = 1 - distance / maxLen;
|
|
181
|
+
return similarity >= threshold ? similarity : 0.0;
|
|
182
|
+
}
|
|
183
|
+
export function findFuzzyMatches(keyword, text, threshold = 0.7, maxWordLenDiff = 3) {
|
|
184
|
+
if (!keyword || !text)
|
|
185
|
+
return [];
|
|
186
|
+
const matches = [];
|
|
187
|
+
const keywordLower = keyword.toLowerCase();
|
|
188
|
+
const keywordLen = keywordLower.length;
|
|
189
|
+
const words = text.match(/[A-Za-z0-9_]+|[\u4e00-\u9fff]+/g) ?? [];
|
|
190
|
+
for (const word of words) {
|
|
191
|
+
const wordLower = word.toLowerCase();
|
|
192
|
+
const wordLen = wordLower.length;
|
|
193
|
+
if (Math.abs(wordLen - keywordLen) > maxWordLenDiff)
|
|
194
|
+
continue;
|
|
195
|
+
const score = fuzzyMatchScore(keywordLower, wordLower, threshold);
|
|
196
|
+
if (score > 0) {
|
|
197
|
+
matches.push([word, score]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return matches.sort((a, b) => b[1] - a[1]);
|
|
201
|
+
}
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// Smart Cube Detection
|
|
204
|
+
// =============================================================================
|
|
205
|
+
export function detectCubeFromPath(projectPath) {
|
|
206
|
+
const targetPath = projectPath ?? process.cwd();
|
|
207
|
+
// Normalize: handle Windows backslashes
|
|
208
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
209
|
+
// Extract folder name (last segment)
|
|
210
|
+
const segments = normalized.replace(/\/$/, "").split("/");
|
|
211
|
+
let folderName = segments[segments.length - 1] ?? "";
|
|
212
|
+
// Windows drive handling: "G:" → empty, take next
|
|
213
|
+
if (/^[A-Za-z]:$/.test(folderName)) {
|
|
214
|
+
folderName = segments[segments.length - 2] ?? "default";
|
|
215
|
+
}
|
|
216
|
+
if (!folderName)
|
|
217
|
+
return "default_cube";
|
|
218
|
+
// Normalize: lowercase, replace -, ., space with _
|
|
219
|
+
let cubeId = folderName.toLowerCase();
|
|
220
|
+
cubeId = cubeId.replace(/[-.\s]+/g, "_");
|
|
221
|
+
cubeId = cubeId.replace(/[^a-z0-9_]/g, "");
|
|
222
|
+
// Ensure doesn't start with digit
|
|
223
|
+
if (cubeId && /^\d/.test(cubeId)) {
|
|
224
|
+
cubeId = "_" + cubeId;
|
|
225
|
+
}
|
|
226
|
+
if (!cubeId.endsWith("_cube")) {
|
|
227
|
+
cubeId = cubeId + "_cube";
|
|
228
|
+
}
|
|
229
|
+
return cubeId || "default_cube";
|
|
230
|
+
}
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// Enhanced Keyword Extraction and Scoring
|
|
233
|
+
// =============================================================================
|
|
234
|
+
export function extractKeywordsEnhanced(query, stopwords = ALL_STOPWORDS, minLength = 2) {
|
|
235
|
+
if (!query)
|
|
236
|
+
return [];
|
|
237
|
+
const rawTokens = query.match(/[A-Za-z0-9_]+|[\u4e00-\u9fff]+/g) ?? [];
|
|
238
|
+
const keywords = [];
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const token of rawTokens) {
|
|
241
|
+
if (!token)
|
|
242
|
+
continue;
|
|
243
|
+
// Chinese token
|
|
244
|
+
if (/[\u4e00-\u9fff]/.test(token)) {
|
|
245
|
+
if (token.length < minLength)
|
|
246
|
+
continue;
|
|
247
|
+
if (stopwords.has(token))
|
|
248
|
+
continue;
|
|
249
|
+
if (!seen.has(token)) {
|
|
250
|
+
keywords.push(token);
|
|
251
|
+
seen.add(token);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// English token
|
|
256
|
+
const lowered = token.toLowerCase();
|
|
257
|
+
if (lowered.length < minLength)
|
|
258
|
+
continue;
|
|
259
|
+
if (stopwords.has(lowered))
|
|
260
|
+
continue;
|
|
261
|
+
if (!seen.has(lowered)) {
|
|
262
|
+
keywords.push(lowered);
|
|
263
|
+
seen.add(lowered);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return keywords;
|
|
267
|
+
}
|
|
268
|
+
export function keywordMatchScoreEnhanced(text, keywords, metadata, enableFuzzy = true, fuzzyThreshold = 0.75) {
|
|
269
|
+
if (!text || keywords.length === 0)
|
|
270
|
+
return 0.0;
|
|
271
|
+
const textLower = text.toLowerCase();
|
|
272
|
+
let score = 0.0;
|
|
273
|
+
let matchedCount = 0;
|
|
274
|
+
const keyField = metadata ? String(metadata.key ?? "").toLowerCase() : "";
|
|
275
|
+
const rawTags = metadata?.tags;
|
|
276
|
+
const tags = Array.isArray(rawTags)
|
|
277
|
+
? rawTags.map((t) => String(t).toLowerCase())
|
|
278
|
+
: [];
|
|
279
|
+
for (const kw of keywords) {
|
|
280
|
+
const kwLower = kw.toLowerCase();
|
|
281
|
+
const isChinese = /[\u4e00-\u9fff]/.test(kw);
|
|
282
|
+
let kwMatched = false;
|
|
283
|
+
// 1. Key field match (highest weight)
|
|
284
|
+
if (keyField && (keyField.includes(kwLower) || (isChinese && keyField.includes(kw)))) {
|
|
285
|
+
score += 5.0;
|
|
286
|
+
kwMatched = true;
|
|
287
|
+
}
|
|
288
|
+
// 2. Tags match
|
|
289
|
+
for (const tag of tags) {
|
|
290
|
+
if (tag.includes(kwLower) || (isChinese && tag.includes(kw))) {
|
|
291
|
+
score += 3.0;
|
|
292
|
+
kwMatched = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// 3. Text match
|
|
297
|
+
if (isChinese) {
|
|
298
|
+
if (text.includes(kw)) {
|
|
299
|
+
score += 2.0;
|
|
300
|
+
kwMatched = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const wordBoundaryRegex = new RegExp(`\\b${escapeRegex(kwLower)}\\b`);
|
|
305
|
+
if (wordBoundaryRegex.test(textLower)) {
|
|
306
|
+
score += 2.5;
|
|
307
|
+
kwMatched = true;
|
|
308
|
+
}
|
|
309
|
+
else if (textLower.includes(kwLower)) {
|
|
310
|
+
score += 1.5;
|
|
311
|
+
kwMatched = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// 4. Fuzzy match
|
|
315
|
+
if (enableFuzzy && !kwMatched && !isChinese) {
|
|
316
|
+
const fuzzyMatches = findFuzzyMatches(kwLower, textLower, fuzzyThreshold);
|
|
317
|
+
if (fuzzyMatches.length > 0) {
|
|
318
|
+
score += fuzzyMatches[0][1] * 1.0;
|
|
319
|
+
kwMatched = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (kwMatched)
|
|
323
|
+
matchedCount++;
|
|
324
|
+
}
|
|
325
|
+
// Coverage bonus
|
|
326
|
+
if (matchedCount > 0) {
|
|
327
|
+
const coverage = matchedCount / keywords.length;
|
|
328
|
+
score += coverage * 1.5;
|
|
329
|
+
}
|
|
330
|
+
return score;
|
|
331
|
+
}
|
|
332
|
+
function escapeRegex(str) {
|
|
333
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=keyword-enhancer.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Analysis Module
|
|
3
|
+
*
|
|
4
|
+
* Generates search suggestions based on context.
|
|
5
|
+
*/
|
|
6
|
+
import { extractKeywords } from "./query-processing.js";
|
|
7
|
+
const MEMORY_TYPE_KEYWORDS = {
|
|
8
|
+
ERROR_PATTERN: ["error", "bug", "exception", "fail", "crash", "traceback", "错误", "异常", "失败"],
|
|
9
|
+
DECISION: ["decision", "decided", "choose", "选择", "决定", "方案", "架构"],
|
|
10
|
+
BUGFIX: ["fix", "fixed", "patch", "repair", "修复", "解决"],
|
|
11
|
+
GOTCHA: ["gotcha", "trap", "caveat", "workaround", "注意", "陷阱", "坑"],
|
|
12
|
+
CODE_PATTERN: ["pattern", "template", "snippet", "example", "模板", "示例"],
|
|
13
|
+
CONFIG: ["config", "configuration", "setting", "env", "配置", "环境"],
|
|
14
|
+
MILESTONE: ["milestone", "completed", "released", "shipped", "完成", "里程碑"],
|
|
15
|
+
FEATURE: ["feature", "added", "implemented", "new", "新功能", "实现"],
|
|
16
|
+
};
|
|
17
|
+
export function suggestSearchQueries(context) {
|
|
18
|
+
if (!context)
|
|
19
|
+
return [];
|
|
20
|
+
const suggestions = [];
|
|
21
|
+
const contextLower = context.toLowerCase();
|
|
22
|
+
// Check for memory type keywords
|
|
23
|
+
for (const [memType, keywords] of Object.entries(MEMORY_TYPE_KEYWORDS)) {
|
|
24
|
+
if (keywords.some((kw) => contextLower.includes(kw))) {
|
|
25
|
+
const extracted = extractKeywords(context);
|
|
26
|
+
if (extracted.length > 0) {
|
|
27
|
+
suggestions.push(`${memType} ${extracted.slice(0, 2).join(" ")}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
suggestions.push(memType);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Add generic keyword-based suggestions
|
|
35
|
+
const keywords = extractKeywords(context).slice(0, 3);
|
|
36
|
+
if (keywords.length > 0 && suggestions.length === 0) {
|
|
37
|
+
suggestions.push(keywords.join(" "));
|
|
38
|
+
if (keywords.length > 1) {
|
|
39
|
+
suggestions.push(keywords[0]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Remove duplicates and limit to 5
|
|
43
|
+
return [...new Set(suggestions)].slice(0, 5);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=memory-analysis.js.map
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered Memory Models + Compaction Logic
|
|
3
|
+
*/
|
|
4
|
+
import type { MemoryNode, MemoryMinimal, MemoryBrief, MemoryFull } from "./types.js";
|
|
5
|
+
export declare const COMPACTION_THRESHOLD = 15;
|
|
6
|
+
export declare const PREVIEW_COUNT = 5;
|
|
7
|
+
export declare function shouldCompact(count: number): boolean;
|
|
8
|
+
export declare function getTypeIcon(memoryType: string): string;
|
|
9
|
+
export declare function toMinimal(memory: MemoryNode | Record<string, unknown>): MemoryMinimal;
|
|
10
|
+
export declare function toBrief(memory: MemoryNode | Record<string, unknown>): MemoryBrief;
|
|
11
|
+
export declare function toFull(memory: MemoryNode | Record<string, unknown>, cubeId?: string, userId?: string): MemoryFull;
|
|
12
|
+
export interface CompactedSearchResultData {
|
|
13
|
+
preview: MemoryMinimal[];
|
|
14
|
+
totalCount: number;
|
|
15
|
+
omittedCount: number;
|
|
16
|
+
message: string;
|
|
17
|
+
query: string;
|
|
18
|
+
cubeId: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function compactedResultToText(result: CompactedSearchResultData): string;
|
|
21
|
+
//# sourceMappingURL=models.d.ts.map
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered Memory Models + Compaction Logic
|
|
3
|
+
*/
|
|
4
|
+
import { extractMcpType } from "./query-processing.js";
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Compaction Configuration
|
|
7
|
+
// ============================================================================
|
|
8
|
+
export const COMPACTION_THRESHOLD = 15;
|
|
9
|
+
export const PREVIEW_COUNT = 5;
|
|
10
|
+
export function shouldCompact(count) {
|
|
11
|
+
return count > COMPACTION_THRESHOLD;
|
|
12
|
+
}
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Type Icon
|
|
15
|
+
// ============================================================================
|
|
16
|
+
export function getTypeIcon(memoryType) {
|
|
17
|
+
const icons = {
|
|
18
|
+
ERROR_PATTERN: "🔴",
|
|
19
|
+
BUGFIX: "🐛",
|
|
20
|
+
DECISION: "📋",
|
|
21
|
+
GOTCHA: "⚠️",
|
|
22
|
+
CODE_PATTERN: "📝",
|
|
23
|
+
CONFIG: "⚙️",
|
|
24
|
+
FEATURE: "✨",
|
|
25
|
+
MILESTONE: "🎯",
|
|
26
|
+
PROGRESS: "📊",
|
|
27
|
+
INFERRED: "🔗",
|
|
28
|
+
};
|
|
29
|
+
return icons[memoryType] ?? "📌";
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Conversion Utilities
|
|
33
|
+
// ============================================================================
|
|
34
|
+
export function toMinimal(memory) {
|
|
35
|
+
const memoryType = extractMcpType(memory);
|
|
36
|
+
const content = memory.memory ?? memory.content ?? "";
|
|
37
|
+
const cleanContent = content.replace(/^\[[A-Z_]+\]\s*/, "");
|
|
38
|
+
const lines = cleanContent.split("\n").filter((l) => l.trim());
|
|
39
|
+
const firstLine = lines[0] ?? cleanContent;
|
|
40
|
+
const summary = firstLine.length > 100 ? firstLine.slice(0, 100) + "..." : firstLine;
|
|
41
|
+
const meta = memory.metadata ?? {};
|
|
42
|
+
const created = memory.updated_at ??
|
|
43
|
+
memory.created_at ??
|
|
44
|
+
meta.updated_at ??
|
|
45
|
+
meta.created_at;
|
|
46
|
+
return {
|
|
47
|
+
id: memory.id ?? "",
|
|
48
|
+
memoryType,
|
|
49
|
+
summary,
|
|
50
|
+
createdAt: created,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function toBrief(memory) {
|
|
54
|
+
const minimal = toMinimal(memory);
|
|
55
|
+
const meta = memory.metadata ?? {};
|
|
56
|
+
const rawTags = memory.tags ?? meta.tags ?? [];
|
|
57
|
+
return {
|
|
58
|
+
...minimal,
|
|
59
|
+
key: memory.key ?? meta.key,
|
|
60
|
+
tags: Array.isArray(rawTags) ? rawTags.map(String) : [],
|
|
61
|
+
relevance: meta.relativity ?? 1.0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function toFull(memory, cubeId = "", userId = "") {
|
|
65
|
+
const brief = toBrief(memory);
|
|
66
|
+
const meta = memory.metadata ?? {};
|
|
67
|
+
const content = memory.memory ?? memory.content ?? "";
|
|
68
|
+
const cleanContent = content.replace(/^\[[A-Z_]+\]\s*/, "");
|
|
69
|
+
return {
|
|
70
|
+
...brief,
|
|
71
|
+
content: cleanContent,
|
|
72
|
+
background: memory.background ?? meta.background,
|
|
73
|
+
cubeId,
|
|
74
|
+
userId,
|
|
75
|
+
relations: memory.relations ?? [],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function compactedResultToText(result) {
|
|
79
|
+
const lines = [
|
|
80
|
+
"## 🔍 Search Results (Compacted)",
|
|
81
|
+
"",
|
|
82
|
+
`**Query**: \`${result.query}\``,
|
|
83
|
+
`**Total**: ${result.totalCount} memories found`,
|
|
84
|
+
`**Showing**: Top ${result.preview.length} (omitted ${result.omittedCount})`,
|
|
85
|
+
"",
|
|
86
|
+
"### Preview",
|
|
87
|
+
"",
|
|
88
|
+
];
|
|
89
|
+
for (let i = 0; i < result.preview.length; i++) {
|
|
90
|
+
const mem = result.preview[i];
|
|
91
|
+
const icon = getTypeIcon(mem.memoryType);
|
|
92
|
+
lines.push(`${i + 1}. ${icon} **[${mem.memoryType}]** ${mem.summary}`);
|
|
93
|
+
lines.push(` ID: \`${mem.id}\``);
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
lines.push("---");
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push('💡 **Tip**: Use `memos_get(memory_id="<id>")` to get full details of a specific memory.');
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=models.js.map
|