claude-mem-lite 2.20.0 → 2.23.1

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.23.1",
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.23.1",
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/README.md CHANGED
@@ -154,7 +154,7 @@ Source files stay in the cloned repo. Update via `git pull && node install.mjs i
154
154
  ### What happens during installation
155
155
 
156
156
  1. **Install dependencies** -- `npm install --omit=dev` (compiles native `better-sqlite3`)
157
- 2. **Register MCP server** -- `mem` server with 12 tools (search, timeline, get, save, update, stats, delete, compress, maintain, registry, export, fts_check)
157
+ 2. **Register MCP server** -- `mem` server with 15 tools (search, recent, recall, timeline, get, save, update, stats, delete, compress, maintain, export, fts_check, browse, registry)
158
158
  3. **Configure hooks** -- `PostToolUse`, `SessionStart`, `Stop`, `UserPromptSubmit` lifecycle hooks
159
159
  4. **Create data directory** -- `~/.claude-mem-lite/` (hidden) for database, runtime, and managed resource files
160
160
  5. **Auto-migrate** -- If `~/.claude-mem/` (original claude-mem) or `~/claude-mem-lite/` (pre-v0.5 unhidden) exists, migrates database and runtime files to `~/.claude-mem-lite/`, preserving the original untouched
@@ -207,6 +207,8 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
207
207
  | Tool | Description |
208
208
  |------|-------------|
209
209
  | `mem_search` | FTS5 full-text search with BM25 ranking. Filters by type, project, date range, importance level. |
210
+ | `mem_recent` | Show most recent observations, ordered by time. Quick snapshot of latest activity. |
211
+ | `mem_recall` | Recall observations related to a file. Use before editing to surface past bugfixes and context. |
210
212
  | `mem_timeline` | Browse observations chronologically around an anchor point. |
211
213
  | `mem_get` | Retrieve full details for specific observation IDs (includes importance and related_ids). |
212
214
  | `mem_save` | Manually save a memory/observation. |
@@ -217,6 +219,7 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
217
219
  | `mem_maintain` | Memory maintenance: scan for duplicates/stale/broken items, then execute cleanup/dedup/rebuild_vectors operations. |
218
220
  | `mem_export` | Export observations as JSON or JSONL for backup or migration. Filters by project, type, date range. |
219
221
  | `mem_fts_check` | Check FTS5 index integrity or rebuild indexes. Use when search results seem wrong or after DB recovery. |
222
+ | `mem_browse` | Tier-grouped memory dashboard. Shows observations organized by memory tier (working/active/archive). |
220
223
  | `mem_registry` | Manage resource registry: search for skills/agents by need, list resources, view stats, import/remove tools, reindex. |
221
224
 
222
225
  ### Skill Commands (in Claude Code chat)
@@ -224,9 +227,11 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
224
227
  ```
225
228
  /mem search <query> # Full-text search across all memories
226
229
  /mem recent [n] # Show recent N observations (default 10)
230
+ /mem recall <file> # Show past observations for a file
227
231
  /mem save <text> # Save a manual memory/note
228
232
  /mem stats # Show memory statistics
229
233
  /mem timeline <query> # Browse timeline around a match
234
+ /mem browse # Tier-grouped memory dashboard
230
235
  /mem <query> # Shorthand for search
231
236
  ```
232
237
 
package/README.zh-CN.md CHANGED
@@ -144,7 +144,7 @@ node install.mjs install
144
144
  ### 安装过程
145
145
 
146
146
  1. **安装依赖** -- `npm install --omit=dev`(编译原生 `better-sqlite3`)
147
- 2. **注册 MCP 服务器** -- `mem` 服务器,包含 12 个工具(search、timeline、get、save、update、stats、delete、compress、maintain、registry、export、fts_check)
147
+ 2. **注册 MCP 服务器** -- `mem` 服务器,包含 15 个工具(search、recent、recall、timeline、get、save、update、stats、delete、compress、maintain、export、fts_check、browse、registry
148
148
  3. **配置钩子** -- `PostToolUse`、`PreToolUse`、`SessionStart`、`Stop`、`UserPromptSubmit` 生命周期钩子
149
149
  4. **创建数据目录** -- `~/.claude-mem-lite/`(隐藏目录),存放数据库、运行时和托管资源文件
150
150
  5. **自动迁移** -- 自动检测 `~/.claude-mem/`(原版 claude-mem)或 `~/claude-mem-lite/`(v0.5 前的非隐藏目录),将数据库和运行时文件迁移到 `~/.claude-mem-lite/`,原目录保持不变
@@ -197,6 +197,8 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
197
197
  | 工具 | 描述 |
198
198
  |------|------|
199
199
  | `mem_search` | 基于 BM25 排名的 FTS5 全文搜索。支持按类型、项目、日期范围、重要度过滤。 |
200
+ | `mem_recent` | 显示最近的观察,按时间排序。快速查看最新活动。 |
201
+ | `mem_recall` | 召回与文件相关的观察。编辑文件前使用,回顾过去的修复和上下文。 |
200
202
  | `mem_timeline` | 围绕锚点按时间顺序浏览观察。 |
201
203
  | `mem_get` | 获取指定观察 ID 的完整详情(包含重要度和关联 ID)。 |
202
204
  | `mem_save` | 手动保存记忆/观察。 |
@@ -207,6 +209,7 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
207
209
  | `mem_maintain` | 记忆维护:扫描重复/过期/损坏条目,执行清理/去重/向量重建操作。 |
208
210
  | `mem_export` | 导出观察为 JSON 或 JSONL 格式,支持按项目、类型、日期范围过滤。 |
209
211
  | `mem_fts_check` | 检查 FTS5 索引完整性或重建索引。搜索结果异常或数据库恢复后使用。 |
212
+ | `mem_browse` | 分层记忆仪表盘。按记忆层级(working/active/archive)分组展示观察。 |
210
213
  | `mem_registry` | 管理资源注册表:按需搜索技能/代理、列表、统计、导入/移除、重索引。 |
211
214
 
212
215
  ### 技能命令(在 Claude Code 聊天中使用)
@@ -214,9 +217,11 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
214
217
  ```
215
218
  /mem search <query> # 全文搜索所有记忆
216
219
  /mem recent [n] # 显示最近 N 条观察(默认 10)
220
+ /mem recall <file> # 显示文件相关的历史观察
217
221
  /mem save <text> # 保存手动记忆/笔记
218
222
  /mem stats # 显示记忆统计
219
223
  /mem timeline <query> # 围绕匹配结果浏览时间线
224
+ /mem browse # 分层记忆仪表盘
220
225
  /mem <query> # search 的简写
221
226
  ```
222
227
 
package/cli.mjs CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
- const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'help']);
2
+ const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'fts-check', 'registry', 'help']);
3
3
 
4
4
  const cmd = process.argv[2];
5
5
 
6
- if (CLI_COMMANDS.has(cmd)) {
6
+ if (cmd === '--help' || cmd === '-h') {
7
+ const { run } = await import('./mem-cli.mjs');
8
+ await run(['help']);
9
+ } else if (CLI_COMMANDS.has(cmd)) {
7
10
  const { run } = await import('./mem-cli.mjs');
8
11
  await run(process.argv.slice(2));
9
12
  } else {
package/hook-context.mjs CHANGED
@@ -82,14 +82,19 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
82
82
  const selectedSess = [];
83
83
  let totalTokens = 0;
84
84
 
85
- // Score each candidate: value = recency * importance, cost = tokens
85
+ // Type quality multipliers aligned with scoring-sql.mjs TYPE_QUALITY_CASE
86
+ // Demotes bugfix (noisy error logs) and promotes high-signal types
87
+ const TYPE_QUALITY = { decision: 1.5, discovery: 1.3, feature: 1.2, refactor: 1.0, change: 0.8, bugfix: 0.35 };
88
+
89
+ // Score each candidate: value = recency * type_quality * importance, cost = tokens
86
90
  // Recency uses exponential half-life (consistent with server.mjs BM25 scoring)
87
91
  const scoredObs = obsPool.map(o => {
88
92
  const halfLifeMs = DECAY_HALF_LIFE_BY_TYPE[o.type] || DEFAULT_DECAY_HALF_LIFE_MS;
89
93
  const recency = 1.0 + Math.exp(-0.693 * (now_ms - o.created_at_epoch) / halfLifeMs);
94
+ const typeQuality = TYPE_QUALITY[o.type] || 1.0;
90
95
  const impBoost = 0.5 + 0.5 * (o.importance || 1);
91
96
  const lessonBoost = o.lesson_learned ? 1.3 : 1.0;
92
- const value = recency * impBoost * lessonBoost;
97
+ const value = recency * typeQuality * impBoost * lessonBoost;
93
98
  const cost = estimateTokens((o.title || '') + (o.narrative || ''));
94
99
  return { ...o, value, cost, valueDensity: cost > 0 ? value / Math.sqrt(cost) : 0 };
95
100
  });
package/hook-episode.mjs CHANGED
@@ -194,12 +194,15 @@ export function mergePendingEntries(episode) {
194
194
  if (pending.ts < oneHourAgo) { try { unlinkSync(fp); } catch {} continue; }
195
195
  // Only merge entries belonging to the same project
196
196
  if (pending.project && episode.project && pending.project !== episode.project) continue;
197
- unlinkSync(fp);
198
197
  if (pending.entry) {
198
+ unlinkSync(fp);
199
199
  episode.entries.push(pending.entry);
200
200
  episode.lastAt = Math.max(episode.lastAt, pending.entry.ts || pending.ts);
201
201
  addFileToEpisode(episode, pending.entry.files || []);
202
202
  merged++;
203
+ } else {
204
+ // No entry data — clean up the file without merging
205
+ try { unlinkSync(fp); } catch {}
203
206
  }
204
207
  } catch {
205
208
  // Corrupt pending file — remove
package/hook-memory.mjs CHANGED
@@ -5,7 +5,9 @@ import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, OBS_BM25 } from './uti
5
5
 
6
6
  const MAX_MEMORY_INJECTIONS = 3;
7
7
  const MEMORY_LOOKBACK_MS = 60 * 86400000; // 60 days
8
- const MEMORY_TYPE_BOOST = { bugfix: 1.5, decision: 1.3, discovery: 1.0, feature: 0.8, change: 0.5, refactor: 0.5 };
8
+ // Aligned with TYPE_QUALITY_CASE: high-signal types > noisy types
9
+ // Bugfix lessons are still surfaced via the separate lesson_learned boost (1.5×)
10
+ const MEMORY_TYPE_BOOST = { decision: 1.5, discovery: 1.3, feature: 1.2, refactor: 1.0, change: 0.8, bugfix: 0.5 };
9
11
 
10
12
  const FILE_RECALL_LOOKBACK_MS = 60 * 86400000; // 60 days
11
13
  const MAX_FILE_RECALL = 2;
package/hook-update.mjs CHANGED
@@ -193,7 +193,7 @@ export function getCurrentVersion() {
193
193
 
194
194
  // ── Source files to copy (must match install.mjs SOURCE_FILES) ──
195
195
  const SOURCE_FILES = [
196
- 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
196
+ 'cli.mjs', 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
197
197
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
198
198
  'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
199
199
  'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
@@ -218,10 +218,11 @@ async function downloadAndInstall(tarballUrl) {
218
218
  debugLog('WARN', 'hook-update', `Rejected suspicious tarball URL: ${tarballUrl}`);
219
219
  return false;
220
220
  }
221
- execSync(
222
- `curl -sL -H "Accept: application/vnd.github+json" "${tarballUrl}" | tar xz -C "${tmpDir}" --strip-components=1`,
223
- { timeout: 30000, stdio: 'pipe' }
224
- );
221
+ const tarballPath = join(tmpDir, 'release.tar.gz');
222
+ execFileSync('curl', ['-sL', '-H', 'Accept: application/vnd.github+json', tarballUrl, '-o', tarballPath],
223
+ { timeout: 30000, stdio: 'pipe' });
224
+ execFileSync('tar', ['xzf', tarballPath, '-C', tmpDir, '--strip-components=1'],
225
+ { timeout: 30000, stdio: 'pipe' });
225
226
 
226
227
  return installExtractedRelease(tmpDir);
227
228
  } catch (err) {
@@ -285,6 +286,9 @@ export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
285
286
  }
286
287
  } catch (e) { debugCatch(e, 'post-update-mcp-dedup'); }
287
288
 
289
+ // Post-update: prune old plugin cache versions (keep latest 3)
290
+ try { prunePluginCache(); } catch (e) { debugCatch(e, 'prunePluginCache'); }
291
+
288
292
  debugLog('DEBUG', 'hook-update', `Auto-update: switched ${installed.length} paths`);
289
293
  return true;
290
294
  } catch (err) {
@@ -344,6 +348,33 @@ function copyReleaseIntoStaging(sourceDir, stagingDir) {
344
348
  debugLog('DEBUG', 'hook-update', `Auto-update staged ${copied} source files`);
345
349
  }
346
350
 
351
+ // ── Plugin Cache Pruning ──────────────────────────────────
352
+ const PLUGIN_CACHE_KEEP = 3;
353
+
354
+ export function prunePluginCache() {
355
+ const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'claude-mem-lite');
356
+ if (!existsSync(cacheBase)) return 0;
357
+
358
+ const entries = readdirSync(cacheBase)
359
+ .filter(name => /^\d+\.\d+/.test(name)) // version-like dirs only
360
+ .sort((a, b) => compareVersions(b, a)); // newest first
361
+
362
+ if (entries.length <= PLUGIN_CACHE_KEEP) return 0;
363
+
364
+ const toRemove = entries.slice(PLUGIN_CACHE_KEEP);
365
+ let removed = 0;
366
+ for (const ver of toRemove) {
367
+ try {
368
+ rmSync(join(cacheBase, ver), { recursive: true, force: true });
369
+ removed++;
370
+ } catch {}
371
+ }
372
+ if (removed > 0) {
373
+ debugLog('DEBUG', 'hook-update', `Plugin cache pruned: removed ${removed} old version(s), kept latest ${PLUGIN_CACHE_KEEP}`);
374
+ }
375
+ return removed;
376
+ }
377
+
347
378
  // ── State Persistence ──────────────────────────────────────
348
379
  function readState() {
349
380
  try {
@@ -357,6 +388,8 @@ function saveState(state) {
357
388
  try {
358
389
  const dir = join(INSTALL_DIR, 'runtime');
359
390
  mkdirSync(dir, { recursive: true });
360
- writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
391
+ const tmpFile = STATE_FILE + `.tmp-${process.pid}`;
392
+ writeFileSync(tmpFile, JSON.stringify(state, null, 2));
393
+ renameSync(tmpFile, STATE_FILE);
361
394
  } catch {}
362
395
  }
package/hook.mjs CHANGED
@@ -812,7 +812,7 @@ async function handleUserPrompt() {
812
812
 
813
813
  // Read IDs already injected by user-prompt-search.js to avoid duplicate injection
814
814
  try {
815
- const injectedFile = `/tmp/.claude-mem-injected-${project}`;
815
+ const injectedFile = join(RUNTIME_DIR, `.claude-mem-injected-${project}`);
816
816
  const raw = readFileSync(injectedFile, 'utf8');
817
817
  const { ids, ts } = JSON.parse(raw);
818
818
  // Only use if written within last 10 seconds (same prompt cycle)
package/install.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { execSync, execFileSync } from 'child_process';
5
5
  import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, copyFileSync, cpSync, renameSync, symlinkSync, unlinkSync, readdirSync, statSync } from 'fs';
6
- import { join, resolve, dirname } from 'path';
6
+ import { join, resolve, dirname, isAbsolute } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { fileURLToPath } from 'url';
9
9
 
@@ -202,7 +202,7 @@ async function install() {
202
202
  if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
203
203
 
204
204
  const SOURCE_FILES = [
205
- 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
205
+ 'cli.mjs', 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
206
206
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
207
207
  'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
208
208
  'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
@@ -259,6 +259,8 @@ async function install() {
259
259
  if (existsSync(postToolSrc)) copyFileSync(postToolSrc, join(scriptsDir, 'post-tool-use.sh'));
260
260
  const promptSearchSrc = join(PROJECT_DIR, 'scripts', 'user-prompt-search.js');
261
261
  if (existsSync(promptSearchSrc)) copyFileSync(promptSearchSrc, join(scriptsDir, 'user-prompt-search.js'));
262
+ const promptSearchUtilsSrc = join(PROJECT_DIR, 'scripts', 'prompt-search-utils.mjs');
263
+ if (existsSync(promptSearchUtilsSrc)) copyFileSync(promptSearchUtilsSrc, join(scriptsDir, 'prompt-search-utils.mjs'));
262
264
  // Ensure bash script is executable
263
265
  try { execFileSync('chmod', ['+x', join(scriptsDir, 'post-tool-use.sh')], { stdio: 'pipe' }); } catch {}
264
266
  // Copy commands directory
@@ -292,6 +294,31 @@ async function install() {
292
294
  }
293
295
  }
294
296
 
297
+ // 2b. Create global CLI symlink (claude-mem-lite command)
298
+ const cliSource = join(INSTALL_DIR, 'cli.mjs');
299
+ if (existsSync(cliSource)) {
300
+ try { execFileSync('chmod', ['+x', cliSource], { stdio: 'pipe' }); } catch {}
301
+ // Try ~/.local/bin first (user-writable, commonly on PATH)
302
+ const localBin = join(homedir(), '.local', 'bin');
303
+ const cliLink = join(localBin, 'claude-mem-lite');
304
+ try {
305
+ if (!existsSync(localBin)) mkdirSync(localBin, { recursive: true });
306
+ if (existsSync(cliLink)) unlinkSync(cliLink);
307
+ symlinkSync(cliSource, cliLink);
308
+ ok(`CLI: ${cliLink} → ${cliSource}`);
309
+ } catch {
310
+ // Fallback: try /usr/local/bin (may need sudo)
311
+ try {
312
+ const globalLink = '/usr/local/bin/claude-mem-lite';
313
+ if (existsSync(globalLink)) unlinkSync(globalLink);
314
+ symlinkSync(cliSource, globalLink);
315
+ ok(`CLI: ${globalLink} → ${cliSource}`);
316
+ } catch {
317
+ warn('CLI symlink failed — run manually: ln -sf ' + cliSource + ' ~/.local/bin/claude-mem-lite');
318
+ }
319
+ }
320
+ }
321
+
295
322
  // 3. Register MCP server (skip if plugin system already handles it)
296
323
  // Plugin MCP must stay at root .mcp.json so Claude Code registers plugin:*:mem.
297
324
  // Duplicate mem registrations in practice come from old global install.mjs state
@@ -353,6 +380,21 @@ async function install() {
353
380
  }
354
381
  }
355
382
  } catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
383
+
384
+ // Sync launch.mjs to plugin cache — ensures MCP server loads dev code via symlink detection
385
+ try {
386
+ const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
387
+ if (existsSync(cacheBase)) {
388
+ const srcLaunch = join(PROJECT_DIR, 'scripts', 'launch.mjs');
389
+ for (const ver of readdirSync(cacheBase)) {
390
+ const dest = join(cacheBase, ver, 'scripts', 'launch.mjs');
391
+ if (existsSync(join(cacheBase, ver, 'scripts'))) {
392
+ copyFileSync(srcLaunch, dest);
393
+ }
394
+ }
395
+ ok('Plugin cache: launch.mjs synced (dev mode MCP routing)');
396
+ }
397
+ } catch (e) { warn(`Plugin cache sync: ${e.message}`); }
356
398
  }
357
399
 
358
400
  // 4. Configure hooks (merge: preserve user's existing hooks, replace ours)
@@ -552,6 +594,9 @@ async function install() {
552
594
  mkdirSync(join(managedDir, 'skills'), { recursive: true });
553
595
  mkdirSync(join(managedDir, 'agents'), { recursive: true });
554
596
  for (const entry of entries) {
597
+ // Path traversal guard: reject entries with '..' or absolute paths
598
+ if (entry.path.includes('..') || entry.name.includes('..') ||
599
+ isAbsolute(entry.path) || isAbsolute(entry.name)) continue;
555
600
  const srcPath = entry.path === '.' ? clonePath : join(clonePath, entry.path);
556
601
  const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
557
602
  const destPath = join(destDir, entry.name);
@@ -721,6 +766,14 @@ async function uninstall() {
721
766
  warn('MCP server not found or already removed');
722
767
  }
723
768
 
769
+ // 1b. Remove CLI symlink
770
+ for (const binDir of [join(homedir(), '.local', 'bin'), '/usr/local/bin']) {
771
+ const cliLink = join(binDir, 'claude-mem-lite');
772
+ try {
773
+ if (existsSync(cliLink)) { unlinkSync(cliLink); ok(`CLI symlink removed: ${cliLink}`); }
774
+ } catch { /* may not have permissions */ }
775
+ }
776
+
724
777
  // 2. Remove hooks from settings.json (match both npx and git-clone install paths)
725
778
  const settings = readSettings();
726
779
  cleanupMemHooksFromSettings(settings);
@@ -879,6 +932,18 @@ async function status() {
879
932
  warn('Database: not found');
880
933
  }
881
934
 
935
+ // CLI
936
+ try {
937
+ const cliVer = execSync('claude-mem-lite --help 2>/dev/null && echo OK', { encoding: 'utf8', timeout: 5000 });
938
+ if (cliVer.includes('OK')) {
939
+ ok('CLI: claude-mem-lite command available');
940
+ } else {
941
+ warn('CLI: command not on PATH');
942
+ }
943
+ } catch {
944
+ warn('CLI: command not on PATH — run install again to create symlink');
945
+ }
946
+
882
947
  // Old system
883
948
  const vectorDb = join(OLD_DATA_DIR, 'vector-db');
884
949
  if (existsSync(vectorDb)) {
@@ -1068,6 +1133,23 @@ async function doctor() {
1068
1133
  }
1069
1134
  }
1070
1135
 
1136
+ // Plugin cache versions
1137
+ const pluginCacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
1138
+ if (existsSync(pluginCacheBase)) {
1139
+ try {
1140
+ const versions = readdirSync(pluginCacheBase).filter(n => /^\d+\./.test(n));
1141
+ let sizeStr;
1142
+ try {
1143
+ sizeStr = execFileSync('du', ['-sh', pluginCacheBase], { encoding: 'utf8', timeout: 5000 }).trim().split('\t')[0];
1144
+ } catch { sizeStr = '?'; }
1145
+ if (versions.length > 3) {
1146
+ warn(`Plugin cache: ${versions.length} versions (${sizeStr}) — run setup.sh or update to auto-prune to 3`);
1147
+ } else {
1148
+ ok(`Plugin cache: ${versions.length} version(s) (${sizeStr})`);
1149
+ }
1150
+ } catch {}
1151
+ }
1152
+
1071
1153
  console.log(`\n ${issues === 0 ? 'All checks passed!' : `${issues} issue(s) found.`}\n`);
1072
1154
  }
1073
1155