claude-mem-lite 2.20.0 → 2.21.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.20.0",
13
+ "version": "2.21.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/install.mjs CHANGED
@@ -353,6 +353,21 @@ async function install() {
353
353
  }
354
354
  }
355
355
  } catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
356
+
357
+ // Sync launch.mjs to plugin cache — ensures MCP server loads dev code via symlink detection
358
+ try {
359
+ const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
360
+ if (existsSync(cacheBase)) {
361
+ const srcLaunch = join(PROJECT_DIR, 'scripts', 'launch.mjs');
362
+ for (const ver of readdirSync(cacheBase)) {
363
+ const dest = join(cacheBase, ver, 'scripts', 'launch.mjs');
364
+ if (existsSync(join(cacheBase, ver, 'scripts'))) {
365
+ copyFileSync(srcLaunch, dest);
366
+ }
367
+ }
368
+ ok('Plugin cache: launch.mjs synced (dev mode MCP routing)');
369
+ }
370
+ } catch (e) { warn(`Plugin cache sync: ${e.message}`); }
356
371
  }
357
372
 
358
373
  // 4. Configure hooks (merge: preserve user's existing hooks, replace ours)
package/nlp.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // nlp.mjs -- FTS5 query building, synonym expansion, CJK tokenization.
2
2
  // Extracted from utils.mjs for focused module boundaries.
3
3
 
4
- import { BASE_STOP_WORDS } from './stop-words.mjs';
4
+ import { BASE_STOP_WORDS, CJK_STOP_WORDS } from './stop-words.mjs';
5
5
  import { SYNONYM_MAP, CJK_COMPOUNDS } from './synonyms.mjs';
6
6
 
7
7
  // Re-export for backward compatibility (consumers import from nlp.mjs or utils.mjs)
@@ -51,7 +51,7 @@ export function cjkBigrams(text) {
51
51
  return [...new Set(tokens)].join(' ');
52
52
  }
53
53
 
54
- // ─── CJK Synonym Extraction ─────────────────────────────────────────────────
54
+ // ─── CJK Keyword Extraction ─────────────────────────────────────────────────
55
55
 
56
56
  // Extract known CJK words (from SYNONYM_MAP) out of unsegmented CJK text.
57
57
  // Greedy longest-match: "数据库的全文搜索" → ["数据库", "搜索"] (skips particles/unknown).
@@ -77,6 +77,37 @@ export function extractCjkSynonymTokens(text) {
77
77
  return found;
78
78
  }
79
79
 
80
+ // Merged CJK dictionary: CJK_COMPOUNDS + CJK keys from SYNONYM_MAP — sorted longest first.
81
+ // Gives broadest coverage: "搜索" from SYNONYM_MAP + "函数" from CJK_COMPOUNDS.
82
+ const _cjkMergedKeys = [...new Set([...CJK_COMPOUNDS, ..._cjkSynonymKeys])]
83
+ .sort((a, b) => b.length - a.length);
84
+
85
+ /**
86
+ * Extract CJK keywords using merged dictionary (CJK_COMPOUNDS + SYNONYM_MAP keys).
87
+ * Broader than either source alone. Filters CJK stop words.
88
+ * "这个函数是做什么的" → ["函数"] (not noisy bigrams)
89
+ * "修复数据库性能优化" → ["修复", "数据库", "性能", "优化"]
90
+ * "之前修复的FTS搜索排序" → ["修复", "搜索", "排序"]
91
+ */
92
+ export function extractCjkKeywords(text) {
93
+ const found = [];
94
+ let i = 0;
95
+ while (i < text.length) {
96
+ if (!/[\u4e00-\u9fff\u3400-\u4dbf]/.test(text[i])) { i++; continue; }
97
+ let matched = false;
98
+ for (const word of _cjkMergedKeys) {
99
+ if (text.startsWith(word, i) && !CJK_STOP_WORDS.has(word)) {
100
+ found.push(word);
101
+ i += word.length;
102
+ matched = true;
103
+ break;
104
+ }
105
+ }
106
+ if (!matched) i++;
107
+ }
108
+ return found;
109
+ }
110
+
80
111
  // ─── FTS5 Token Formatting ──────────────────────────────────────────────────
81
112
 
82
113
  // Format a term for FTS5: quote if it contains spaces, hyphens, or special chars
@@ -124,13 +155,14 @@ export function sanitizeFtsQuery(query) {
124
155
  // Filter stop words (but keep all if filtering would empty the query)
125
156
  const filtered = tokens.filter(t => !FTS_STOP_WORDS.has(t.toLowerCase()));
126
157
  if (filtered.length > 0) tokens = filtered;
127
- // Split unsegmented CJK tokens into known vocabulary words for synonym expansion.
128
- // e.g. "数据库的全文搜索" ["数据库", "搜索"] (both have EN synonyms in SYNONYM_MAP)
158
+ // Split unsegmented CJK tokens into known vocabulary words using CJK_COMPOUNDS dictionary.
159
+ // Uses broader dictionary than synonym-only extraction for better recall.
160
+ // e.g. "这个函数是做什么的" → ["函数"] (not noisy bigrams)
129
161
  const expandedTokens = [];
130
162
  let cjkExtracted = false;
131
163
  for (const t of tokens) {
132
164
  if (/[\u4e00-\u9fff\u3400-\u4dbf]/.test(t) && t.length > 2) {
133
- const cjkWords = extractCjkSynonymTokens(t);
165
+ const cjkWords = extractCjkKeywords(t);
134
166
  if (cjkWords.length > 0) {
135
167
  expandedTokens.push(...cjkWords);
136
168
  cjkExtracted = true;
@@ -146,7 +178,7 @@ export function sanitizeFtsQuery(query) {
146
178
  // Skip bigrams when CJK synonym extraction already produced meaningful tokens —
147
179
  // bigrams joined with AND would make the query too restrictive.
148
180
  const bigrams = cjkExtracted ? null : cjkBigrams(cleaned);
149
- const bigramSet = new Set(bigrams ? bigrams.split(' ').filter(Boolean) : []);
181
+ const bigramSet = new Set(bigrams ? bigrams.split(' ').filter(b => b && !CJK_STOP_WORDS.has(b)) : []);
150
182
  const hasBigrams = bigramSet.size > 0;
151
183
  const finalTokens = [];
152
184
  const seen = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -2,9 +2,10 @@
2
2
  // launch.mjs — Auto-installs dependencies then starts MCP server
3
3
  // Uses only Node built-ins so it works before npm install
4
4
  import { execSync } from 'node:child_process';
5
- import { existsSync } from 'node:fs';
5
+ import { existsSync, lstatSync } from 'node:fs';
6
6
  import { dirname, join } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import { homedir } from 'node:os';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(__dirname, '..');
@@ -19,4 +20,15 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
19
20
  process.stderr.write('[claude-mem-lite] Dependencies installed\n');
20
21
  }
21
22
 
22
- await import(new URL('../server.mjs', import.meta.url).href);
23
+ // Dev mode: prefer ~/.claude-mem-lite/server.mjs (symlinked to source) over
24
+ // CLAUDE_PLUGIN_ROOT (potentially stale plugin cache). This ensures the MCP
25
+ // server always runs the latest code when installed with `install --dev`.
26
+ const devServer = join(homedir(), '.claude-mem-lite', 'server.mjs');
27
+ let useDevServer = false;
28
+ try { useDevServer = existsSync(devServer) && lstatSync(devServer).isSymbolicLink(); } catch {}
29
+
30
+ if (useDevServer) {
31
+ await import(pathToFileURL(devServer).href);
32
+ } else {
33
+ await import(new URL('../server.mjs', import.meta.url).href);
34
+ }
@@ -74,7 +74,7 @@ export function reRankWithContext(db, results, project) {
74
74
  * Mutates result objects in-place by adding superseded=true flag.
75
75
  * @param {object[]} results Array of search result objects with source, files_modified, date, importance
76
76
  */
77
- export function markSuperseded(db, results) {
77
+ export function markSuperseded(results) {
78
78
  if (!results || results.length === 0) return;
79
79
  // Build map: file → [result objects], only for obs with files
80
80
  const fileMap = new Map();
package/server.mjs CHANGED
@@ -569,7 +569,7 @@ server.registerTool(
569
569
  if (ftsQuery && results.some(r => r.source === 'obs')) {
570
570
  const obsResults = results.filter(r => r.source === 'obs');
571
571
  reRankWithContext(db, obsResults, currentProject);
572
- markSuperseded(db, obsResults);
572
+ markSuperseded(obsResults);
573
573
  results.sort((a, b) => (a.score ?? 0) - (b.score ?? 0));
574
574
  }
575
575
 
package/stop-words.mjs CHANGED
@@ -14,3 +14,22 @@ export const BASE_STOP_WORDS = new Set([
14
14
  'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some',
15
15
  'such', 'than', 'too', 'very', 'just', 'also', 'then', 'so', 'if',
16
16
  ]);
17
+
18
+ /** CJK stop words — particles, function words, and fillers that add noise to FTS queries. */
19
+ export const CJK_STOP_WORDS = new Set([
20
+ // Particles & structural
21
+ '的', '了', '是', '在', '有', '和', '与', '被', '把', '给',
22
+ '个', '这', '那', '也', '就', '都', '而', '及', '或', '但',
23
+ '还', '到', '让', '很', '得', '着', '过', '会', '要',
24
+ '能', '不', '没',
25
+ // Question words (intent detection handles these separately)
26
+ '什么', '怎么', '如何', '为何', '哪个',
27
+ // Quantifiers & demonstratives
28
+ '一下', '一些', '一个', '一种',
29
+ // Directional/auxiliary
30
+ '来', '去', '做', '从', '向', '比', '对',
31
+ // Sentence-final particles
32
+ '吗', '吧', '呢', '呀', '啊', '嗯', '哦',
33
+ // Common filler phrases
34
+ '看看', '帮我', '没有', '可以', '怎么样',
35
+ ]);
package/synonyms.mjs CHANGED
@@ -120,11 +120,26 @@ export const SYNONYM_PAIRS = [
120
120
  ['打包', 'bundle'], ['类型', 'type'], ['类型', 'typescript'],
121
121
  // Errors
122
122
  ['错误', 'error'], ['异常', 'exception'],
123
+ ['报错', 'error'], ['崩溃', 'crash'],
123
124
  // Infrastructure
124
125
  ['容器', 'container'], ['容器', 'docker'],
125
126
  ['集群', 'cluster'], ['集群', 'kubernetes'],
126
127
  ['网关', 'gateway'], ['负载', 'load balancing'],
127
128
  ['队列', 'queue'], ['序列化', 'serialize'],
129
+ // Code structure (missing from earlier CJK pairs)
130
+ ['函数', 'function'], ['变量', 'variable'],
131
+ ['模块', 'module'], ['框架', 'framework'],
132
+ ['编译', 'compile'], ['服务器', 'server'],
133
+ ['前端', 'frontend'], ['后端', 'backend'],
134
+ ['优化', 'optimize'], ['优化', 'optimization'],
135
+ ['架构', 'architecture'], ['设计', 'design'],
136
+ ['文档', 'documentation'], ['文档', 'docs'],
137
+ ['版本', 'version'], ['分支', 'branch'],
138
+ ['提交', 'commit'], ['推送', 'push'],
139
+ ['合并', 'merge'], ['升级', 'upgrade'],
140
+ ['安装', 'install'], ['导入', 'import'],
141
+ ['导出', 'export'], ['状态', 'state'],
142
+ ['系统', 'system'], ['算法', 'algorithm'],
128
143
  ];
129
144
 
130
145
  // ─── Bidirectional SYNONYM_MAP (case-insensitive) ──────────────────────────────
@@ -160,7 +175,7 @@ export const CJK_COMPOUNDS = new Set([
160
175
  '报错', '崩溃', '泄露', '溢出', '死锁', '超时', '中断', '异常', '故障',
161
176
  // architecture
162
177
  '架构', '设计', '方案', '规划', '文档', '注释', '版本', '分支', '依赖',
163
- '性能', '安全', '漏洞', '补丁',
178
+ '性能', '安全', '漏洞', '补丁', '系统', '算法',
164
179
  ]);
165
180
 
166
181
  // ─── Dispatch Synonyms (unidirectional, broader groupings) ──────────────────
package/utils.mjs CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
9
9
  // Backward compatibility: all consumers import from utils.mjs
10
10
 
11
11
  export { DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, OBS_FTS_COLUMNS } from './scoring-sql.mjs';
12
- export { cjkBigrams, extractCjkSynonymTokens, SYNONYM_MAP, expandToken, sanitizeFtsQuery, relaxFtsQueryToOr, FTS_STOP_WORDS, CJK_COMPOUNDS } from './nlp.mjs';
12
+ export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, SYNONYM_MAP, expandToken, sanitizeFtsQuery, relaxFtsQueryToOr, FTS_STOP_WORDS, CJK_COMPOUNDS } from './nlp.mjs';
13
13
  export { resolveProject, _resetProjectCache } from './project-utils.mjs';
14
14
  export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
15
15
  export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey } from './format-utils.mjs';