claude-mem-lite 2.9.2 → 2.9.4

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.9.2",
13
+ "version": "2.9.4",
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.9.2",
3
+ "version": "2.9.4",
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
@@ -121,7 +121,7 @@ The original sends **everything to the LLM and hopes it filters well**. claude-m
121
121
  /plugin install claude-mem-lite
122
122
  ```
123
123
 
124
- The plugin system handles everything: hooks, MCP server, and dependency installation (via Setup hook). Dependencies are installed automatically on first run.
124
+ Plugin mode manages its own hooks/runtime. On session start it only **checks and reports** new claude-mem-lite versions; it does **not** self-overwrite plugin files in place. Update plugin-mode installs through Claude's plugin workflow.
125
125
 
126
126
  ### Method 2: npx (one-liner)
127
127
 
@@ -375,6 +375,8 @@ node install.mjs uninstall # Remove (keep data)
375
375
  node install.mjs uninstall --purge # Remove and delete all data
376
376
  node install.mjs status # Show current status
377
377
  node install.mjs doctor # Diagnose issues
378
+ node install.mjs cleanup-hooks # Remove only stale claude-mem-lite hooks from settings.json
379
+ node install.mjs update # Force-check for updates and install them (direct install / npx mode)
378
380
 
379
381
  # npx install:
380
382
  npx claude-mem-lite # Install / reinstall
@@ -382,13 +384,18 @@ npx claude-mem-lite uninstall # Remove (keep data)
382
384
  npx claude-mem-lite doctor # Diagnose issues
383
385
  ```
384
386
 
387
+ Notes:
388
+ - Plugin mode only reports available updates; it does not self-update plugin files.
389
+ - Direct install / npx mode keeps auto-update enabled and uses staged replacement with rollback on install failure.
390
+ - If you disabled the plugin but still have old mem hooks in `~/.claude/settings.json`, run `node install.mjs cleanup-hooks`.
391
+
385
392
  ### doctor
386
393
 
387
394
  Checks Node.js version, dependencies, server/hook files, database integrity, FTS5 indexes, and stale processes.
388
395
 
389
396
  ### status
390
397
 
391
- Shows MCP registration, hook configuration, and database stats (observation/session counts).
398
+ Shows MCP registration, hook configuration, plugin disabled state, and database stats (observation/session counts).
392
399
 
393
400
  ## Uninstall
394
401
 
package/README.zh-CN.md CHANGED
@@ -121,7 +121,7 @@
121
121
  /plugin install claude-mem-lite
122
122
  ```
123
123
 
124
- 插件系统会处理一切:钩子、MCP 服务器和依赖安装(通过 Setup 钩子)。依赖会在首次运行时自动安装。
124
+ 插件模式会管理自己的运行时与钩子。SessionStart 时它现在只会**检查并提示**新版本,不会直接覆盖插件目录中的文件。插件模式请通过 Claude 的插件更新流程完成升级。
125
125
 
126
126
  ### 方式二:npx(一行命令)
127
127
 
@@ -375,6 +375,8 @@ node install.mjs uninstall # 移除(保留数据)
375
375
  node install.mjs uninstall --purge # 移除并删除所有数据
376
376
  node install.mjs status # 显示当前状态
377
377
  node install.mjs doctor # 诊断问题
378
+ node install.mjs cleanup-hooks # 只清理 settings.json 中残留的 claude-mem-lite hooks
379
+ node install.mjs update # 强制检查并安装更新(direct install / npx 模式)
378
380
 
379
381
  # npx 安装:
380
382
  npx claude-mem-lite # 安装 / 重新安装
@@ -382,13 +384,18 @@ npx claude-mem-lite uninstall # 移除(保留数据)
382
384
  npx claude-mem-lite doctor # 诊断问题
383
385
  ```
384
386
 
387
+ 说明:
388
+ - 插件模式只提示可用更新,不会自更新插件文件。
389
+ - direct install / npx 模式保留自动更新,并使用 staged replacement;若依赖安装失败会回滚。
390
+ - 如果你禁用了插件,但 `~/.claude/settings.json` 里还有旧的 mem hooks,可运行 `node install.mjs cleanup-hooks`。
391
+
385
392
  ### doctor
386
393
 
387
394
  检查 Node.js 版本、依赖、服务器/钩子文件、数据库完整性、FTS5 索引和残留进程。
388
395
 
389
396
  ### status
390
397
 
391
- 显示 MCP 注册状态、钩子配置和数据库统计(观察/会话数量)。
398
+ 显示 MCP 注册状态、钩子配置、插件禁用状态和数据库统计(观察/会话数量)。
392
399
 
393
400
  ## 卸载
394
401
 
package/cli.mjs CHANGED
@@ -1,2 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import './install.mjs';
2
+ import { main } from './install.mjs';
3
+
4
+ await main(process.argv.slice(2));
package/dispatch.mjs CHANGED
@@ -840,8 +840,11 @@ function passesConfidenceGate(results, signals) {
840
840
  const top1 = Math.abs(results[0].composite_score ?? results[0].relevance ?? 0);
841
841
  const top2 = Math.abs(results[1].composite_score ?? results[1].relevance ?? 0);
842
842
  // After keyword re-ranking, #1 may have lower raw BM25 than #2.
843
- // The keyword match provides additional confidence, so skip the gap check.
844
- const wasReRanked = signals?.rawKeywords?.length > 0 && top1 < top2;
843
+ // Only skip gap check if #1 was actually promoted by a keyword match
844
+ // (not just any rawKeywords present with incidentally inverted scores).
845
+ const top1Tags = (results[0].intent_tags || '').toLowerCase();
846
+ const top1MatchesKw = signals?.rawKeywords?.some(kw => top1Tags.includes(kw));
847
+ const wasReRanked = top1MatchesKw && top1 < top2;
845
848
  if (!wasReRanked && top1 > 0) {
846
849
  const gapRatio = (top1 - top2) / top1;
847
850
  if (gapRatio < 0.2) {
@@ -1007,8 +1010,8 @@ function decideTier(resource, signals) {
1007
1010
  else signalBoost += 0.1;
1008
1011
  }
1009
1012
  if (signals?.rawKeywords?.length > 0) {
1010
- const tags = (resource.intent_tags || '').toLowerCase();
1011
- if (signals.rawKeywords.some(kw => tags.includes(kw))) signalBoost += 0.2;
1013
+ const tagArr = (resource.intent_tags || '').toLowerCase().split(/[\s,]+/);
1014
+ if (signals.rawKeywords.some(kw => tagArr.includes(kw))) signalBoost += 0.2;
1012
1015
  }
1013
1016
 
1014
1017
  const confidence = Math.min(1.0, normalized + patternBoost * 0.3 + signalBoost);
package/hook-update.mjs CHANGED
@@ -3,8 +3,8 @@
3
3
  // Skips in dev mode (symlinked installs). Silent on network failure.
4
4
 
5
5
  import { execSync } from 'node:child_process';
6
- import { readFileSync, writeFileSync, copyFileSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync } from 'node:fs';
7
- import { join } from 'node:path';
6
+ import { readFileSync, writeFileSync, copyFileSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
8
  import { tmpdir } from 'node:os';
9
9
  import { DB_DIR } from './schema.mjs';
10
10
  import { debugCatch, debugLog } from './utils.mjs';
@@ -16,17 +16,29 @@ const STATE_FILE = join(INSTALL_DIR, 'runtime', 'update-state.json');
16
16
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
17
17
  const FETCH_TIMEOUT_MS = 3000; // 3s network timeout
18
18
  const RATE_LIMIT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h if rate-limited
19
+ const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
19
20
 
20
21
  // ── Main Entry ─────────────────────────────────────────────
21
- export async function checkForUpdate() {
22
+ export async function checkForUpdate(options = {}) {
22
23
  try {
24
+ const pluginMode = isPluginMode();
25
+ const force = Boolean(options.force);
26
+ const allowInstall = options.allowInstall ?? !pluginMode;
27
+
23
28
  if (isDevMode() || process.env.CLAUDE_MEM_SKIP_UPDATE) return null;
24
29
 
25
30
  const state = readState();
26
- if (!shouldCheck(state)) {
31
+ if (!force && !shouldCheck(state)) {
27
32
  // Return cached update info if previously detected
28
33
  if (state.updateAvailable && state.latestVersion) {
29
- return { updateAvailable: true, from: state.installedVersion, to: state.latestVersion };
34
+ return {
35
+ updateAvailable: true,
36
+ updated: false,
37
+ from: state.installedVersion,
38
+ to: state.latestVersion,
39
+ installDeferred: pluginMode || !allowInstall,
40
+ pluginMode,
41
+ };
30
42
  }
31
43
  return null;
32
44
  }
@@ -42,7 +54,8 @@ export async function checkForUpdate() {
42
54
 
43
55
  if (hasUpdate) {
44
56
  debugLog('DEBUG', 'hook-update', `Update available: ${currentVersion} → ${latest.version}`);
45
- const success = await downloadAndInstall(latest.tarballUrl);
57
+ const canInstall = !pluginMode && Boolean(allowInstall);
58
+ const success = canInstall ? await downloadAndInstall(latest.tarballUrl) : false;
46
59
  const newState = {
47
60
  lastCheck: new Date().toISOString(),
48
61
  installedVersion: success ? latest.version : currentVersion,
@@ -58,6 +71,8 @@ export async function checkForUpdate() {
58
71
  updated: success,
59
72
  from: currentVersion,
60
73
  to: latest.version,
74
+ installDeferred: !canInstall,
75
+ pluginMode,
61
76
  };
62
77
  }
63
78
 
@@ -76,6 +91,10 @@ export async function checkForUpdate() {
76
91
  }
77
92
  }
78
93
 
94
+ function isPluginMode() {
95
+ return Boolean(process.env.CLAUDE_PLUGIN_ROOT);
96
+ }
97
+
79
98
  // ── Dev Mode Detection ─────────────────────────────────────
80
99
  function isDevMode() {
81
100
  try {
@@ -172,12 +191,13 @@ const SOURCE_FILES = [
172
191
  'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
173
192
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs',
174
193
  'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
175
- 'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'skill.md',
194
+ 'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
176
195
  'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
177
196
  'registry-retriever.mjs', 'resource-discovery.mjs',
178
- 'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-workflow.mjs',
197
+ 'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-patterns.mjs', 'dispatch-workflow.mjs',
179
198
  'install.mjs',
180
199
  ];
200
+ const SWITCHABLE_PATHS = [...SOURCE_FILES, 'scripts', 'registry', 'node_modules'];
181
201
 
182
202
  // ── Download & Install ─────────────────────────────────────
183
203
  // Direct file copy instead of running old install.mjs (avoids symlink overwrite in dev)
@@ -197,53 +217,113 @@ async function downloadAndInstall(tarballUrl) {
197
217
  { timeout: 30000, stdio: 'pipe' }
198
218
  );
199
219
 
200
- // Direct copy: overwrite source files in INSTALL_DIR
201
- // Safer than running old install.mjs which may not respect CLAUDE_MEM_DIR
202
- let copied = 0;
203
- for (const f of SOURCE_FILES) {
204
- const src = join(tmpDir, f);
205
- const dest = join(INSTALL_DIR, f);
206
- if (existsSync(src)) {
207
- copyFileSync(src, dest);
208
- copied++;
209
- }
210
- }
220
+ return installExtractedRelease(tmpDir);
221
+ } catch (err) {
222
+ debugCatch(err, 'downloadAndInstall');
223
+ return false;
224
+ } finally {
225
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
226
+ }
227
+ }
228
+
229
+ export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
230
+ const ts = `${Date.now()}-${process.pid}`;
231
+ const stagingDir = join(targetDir, `.update-staging-${ts}`);
232
+ const backupDir = join(targetDir, `.update-backup-${ts}`);
233
+ const backedUp = [];
234
+ const installed = [];
235
+
236
+ try {
237
+ mkdirSync(stagingDir, { recursive: true });
238
+ mkdirSync(backupDir, { recursive: true });
239
+
240
+ copyReleaseIntoStaging(sourceDir, stagingDir);
241
+ execSync(NPM_INSTALL_CMD, {
242
+ cwd: stagingDir,
243
+ timeout: 60000,
244
+ stdio: 'pipe',
245
+ });
246
+
247
+ for (const relPath of SWITCHABLE_PATHS) {
248
+ const stagedPath = join(stagingDir, relPath);
249
+ if (!existsSync(stagedPath)) continue;
250
+
251
+ const targetPath = join(targetDir, relPath);
252
+ const backupPath = join(backupDir, relPath);
211
253
 
212
- // Copy scripts/ directory if present
213
- const srcScripts = join(tmpDir, 'scripts');
214
- if (existsSync(srcScripts)) {
215
- const destScripts = join(INSTALL_DIR, 'scripts');
216
- mkdirSync(destScripts, { recursive: true });
217
- for (const f of readdirSync(srcScripts)) {
218
- copyFileSync(join(srcScripts, f), join(destScripts, f));
254
+ mkdirSync(dirname(targetPath), { recursive: true });
255
+ mkdirSync(dirname(backupPath), { recursive: true });
256
+
257
+ if (existsSync(targetPath)) {
258
+ renameSync(targetPath, backupPath);
259
+ backedUp.push(relPath);
219
260
  }
261
+
262
+ renameSync(stagedPath, targetPath);
263
+ installed.push(relPath);
220
264
  }
221
265
 
222
- // Run npm install for dependencies (skip if node_modules is a symlink = dev mode)
223
- const nmPath = join(INSTALL_DIR, 'node_modules');
224
- if (!existsSync(nmPath) || !lstatSync(nmPath).isSymbolicLink()) {
266
+ rmSync(stagingDir, { recursive: true, force: true });
267
+ rmSync(backupDir, { recursive: true, force: true });
268
+ debugLog('DEBUG', 'hook-update', `Auto-update: switched ${installed.length} paths`);
269
+ return true;
270
+ } catch (err) {
271
+ debugCatch(err, 'installExtractedRelease');
272
+
273
+ for (const relPath of installed.reverse()) {
274
+ try { rmSync(join(targetDir, relPath), { recursive: true, force: true }); } catch {}
275
+ }
276
+ for (const relPath of backedUp.reverse()) {
277
+ const backupPath = join(backupDir, relPath);
278
+ const targetPath = join(targetDir, relPath);
225
279
  try {
226
- execSync('npm install --omit=dev', {
227
- cwd: INSTALL_DIR,
228
- timeout: 60000,
229
- stdio: 'pipe',
230
- });
231
- } catch (err) {
232
- debugCatch(err, 'downloadAndInstall-npm');
233
- // Non-fatal: old node_modules may still work
280
+ if (existsSync(backupPath)) {
281
+ mkdirSync(dirname(targetPath), { recursive: true });
282
+ renameSync(backupPath, targetPath);
283
+ }
284
+ } catch (restoreErr) {
285
+ debugCatch(restoreErr, `installExtractedRelease-restore-${relPath}`);
234
286
  }
235
287
  }
236
288
 
237
- debugLog('DEBUG', 'hook-update', `Auto-update: ${copied} files copied`);
238
- return true;
239
- } catch (err) {
240
- debugCatch(err, 'downloadAndInstall');
289
+ try { rmSync(stagingDir, { recursive: true, force: true }); } catch {}
290
+ try { rmSync(backupDir, { recursive: true, force: true }); } catch {}
241
291
  return false;
242
- } finally {
243
- try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
244
292
  }
245
293
  }
246
294
 
295
+ function copyReleaseIntoStaging(sourceDir, stagingDir) {
296
+ let copied = 0;
297
+
298
+ for (const f of SOURCE_FILES) {
299
+ const src = join(sourceDir, f);
300
+ const dest = join(stagingDir, f);
301
+ if (!existsSync(src)) continue;
302
+ mkdirSync(dirname(dest), { recursive: true });
303
+ copyFileSync(src, dest);
304
+ copied++;
305
+ }
306
+
307
+ for (const dirName of ['scripts', 'registry']) {
308
+ const srcDir = join(sourceDir, dirName);
309
+ const destDir = join(stagingDir, dirName);
310
+ if (!existsSync(srcDir)) continue;
311
+ mkdirSync(destDir, { recursive: true });
312
+ for (const entry of readdirSync(srcDir)) {
313
+ copyFileSync(join(srcDir, entry), join(destDir, entry));
314
+ }
315
+ }
316
+
317
+ const stagedScripts = join(stagingDir, 'scripts');
318
+ if (existsSync(stagedScripts)) {
319
+ for (const sf of readdirSync(stagedScripts).filter(n => n.endsWith('.sh'))) {
320
+ try { execSync(`chmod +x "${join(stagedScripts, sf)}"`, { stdio: 'pipe' }); } catch {}
321
+ }
322
+ }
323
+
324
+ debugLog('DEBUG', 'hook-update', `Auto-update staged ${copied} source files`);
325
+ }
326
+
247
327
  // ── State Persistence ──────────────────────────────────────
248
328
  function readState() {
249
329
  try {
package/hook.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  import { randomUUID } from 'crypto';
8
8
  import { join, basename } from 'path';
9
9
  import { readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync, statSync } from 'fs';
10
+ import { homedir } from 'os';
10
11
  import {
11
12
  truncate, typeIcon, inferProject, detectBashSignificance,
12
13
  extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
@@ -40,6 +41,22 @@ import { checkForUpdate } from './hook-update.mjs';
40
41
  // Background workers (llm-episode, llm-summary, resource-scan) are exempt — they're ours
41
42
  const event = process.argv[2];
42
43
  const BG_EVENTS = new Set(['llm-episode', 'llm-summary', 'resource-scan']);
44
+
45
+ // Respect Claude Code plugin disable state even when legacy settings.json hooks remain.
46
+ // install.mjs writes direct hooks into ~/.claude/settings.json, so disabling the plugin
47
+ // in Claude UI does not automatically remove them. Exit early to make disable actually work.
48
+ const PLUGIN_KEY = 'claude-mem-lite@sdsrss';
49
+ function isPluginExplicitlyDisabled() {
50
+ try {
51
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
52
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
53
+ return settings.enabledPlugins?.[PLUGIN_KEY] === false;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ if (event && isPluginExplicitlyDisabled()) process.exit(0);
43
60
  if (process.env.CLAUDE_MEM_HOOK_RUNNING && !BG_EVENTS.has(event)) process.exit(0);
44
61
 
45
62
  // Crash-safe: flush episode buffer on unexpected termination to prevent data loss
@@ -820,7 +837,10 @@ async function handleSessionStart() {
820
837
  if (updateResult?.updated) {
821
838
  process.stdout.write(`\n🔄 claude-mem-lite: v${updateResult.from} → v${updateResult.to} updated\n`);
822
839
  } else if (updateResult?.updateAvailable) {
823
- process.stdout.write(`\n📦 claude-mem-lite: v${updateResult.to} available (current: v${updateResult.from})\n`);
840
+ const hint = updateResult.installDeferred
841
+ ? ' — plugin mode only checks for updates; reinstall/update the plugin to apply it'
842
+ : '';
843
+ process.stdout.write(`\n📦 claude-mem-lite: v${updateResult.to} available (current: v${updateResult.from})${hint}\n`);
824
844
  }
825
845
  } catch (e) { debugCatch(e, 'session-start-update'); }
826
846
 
package/install.mjs CHANGED
@@ -22,6 +22,8 @@ const INSTALL_DIR = DATA_DIR;
22
22
  const SERVER_PATH = join(INSTALL_DIR, 'server.mjs');
23
23
  const HOOK_PATH = join(INSTALL_DIR, 'hook.mjs');
24
24
  const MARKETPLACE_KEY = 'sdsrss';
25
+ const PLUGIN_KEY = `claude-mem-lite@${MARKETPLACE_KEY}`;
26
+ const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
25
27
 
26
28
  // ─── Curated Resource Metadata ───────────────────────────────────────────────
27
29
  // Replaces generic name-echo fallback with FTS5-optimized metadata per resource.
@@ -2033,8 +2035,8 @@ function registerVirtualResources(rdb) {
2033
2035
  return count;
2034
2036
  }
2035
2037
 
2036
- const cmd = process.argv[2];
2037
- const flags = new Set(process.argv.slice(3));
2038
+ let cmd = process.argv[2];
2039
+ let flags = new Set(process.argv.slice(3));
2038
2040
 
2039
2041
  function log(msg) { console.log(` ${msg}`); }
2040
2042
  function ok(msg) { console.log(` ✓ ${msg}`); }
@@ -2063,7 +2065,7 @@ async function install() {
2063
2065
  'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
2064
2066
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs',
2065
2067
  'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
2066
- 'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'skill.md',
2068
+ 'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
2067
2069
  'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
2068
2070
  'registry-retriever.mjs', 'resource-discovery.mjs',
2069
2071
  'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-patterns.mjs', 'dispatch-workflow.mjs',
@@ -2120,17 +2122,15 @@ async function install() {
2120
2122
  // 2. npm install (skip for --dev: node_modules is symlinked)
2121
2123
  if (IS_DEV) {
2122
2124
  ok('Dependencies: using dev dir (symlinked)');
2123
- } else if (!existsSync(join(INSTALL_DIR, 'node_modules'))) {
2124
- log('Installing dependencies...');
2125
+ } else {
2126
+ log('Ensuring dependencies installed...');
2125
2127
  try {
2126
- execSync('npm install --omit=dev', { cwd: INSTALL_DIR, stdio: 'pipe' });
2128
+ execSync(NPM_INSTALL_CMD, { cwd: INSTALL_DIR, stdio: 'pipe' });
2127
2129
  ok('Dependencies installed');
2128
2130
  } catch (e) {
2129
2131
  fail('npm install failed: ' + e.message);
2130
2132
  process.exit(1);
2131
2133
  }
2132
- } else {
2133
- ok('Dependencies already installed');
2134
2134
  }
2135
2135
 
2136
2136
  // 3. Register MCP server
@@ -2187,6 +2187,9 @@ async function install() {
2187
2187
  // 4. Configure hooks (merge: preserve user's existing hooks, replace ours)
2188
2188
  log('Configuring hooks...');
2189
2189
  const settings = readSettings();
2190
+ if (clearPluginDisabledMarkerForDirectInstall(settings)) {
2191
+ ok('Cleared stale disabled plugin flag so install.mjs-managed hooks can run');
2192
+ }
2190
2193
  settings.hooks = settings.hooks || {};
2191
2194
 
2192
2195
  const PREFILTER_PATH = join(INSTALL_DIR, 'scripts', 'post-tool-use.sh');
@@ -2543,74 +2546,72 @@ async function uninstall() {
2543
2546
 
2544
2547
  // 2. Remove hooks from settings.json (match both npx and git-clone install paths)
2545
2548
  const settings = readSettings();
2546
- if (settings.hooks) {
2547
- for (const [event, configs] of Object.entries(settings.hooks)) {
2548
- if (!Array.isArray(configs)) continue;
2549
- settings.hooks[event] = configs.filter(cfg => !isMemHook(cfg));
2550
- if (settings.hooks[event].length === 0) delete settings.hooks[event];
2549
+ cleanupMemHooksFromSettings(settings);
2550
+
2551
+ // 3. Clean plugin registry entries conservatively (avoid deleting other plugins
2552
+ // from the same marketplace publisher)
2553
+ const pluginsDir = join(homedir(), '.claude', 'plugins');
2554
+ const installedPath = join(pluginsDir, 'installed_plugins.json');
2555
+ let canRemoveMarketplaceArtifacts;
2556
+ try {
2557
+ const installed = JSON.parse(readFileSync(installedPath, 'utf8'));
2558
+ const plugins = getInstalledPluginEntries(installed);
2559
+ let cleaned = false;
2560
+ if (PLUGIN_KEY in plugins) {
2561
+ delete plugins[PLUGIN_KEY];
2562
+ cleaned = true;
2551
2563
  }
2552
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
2564
+ canRemoveMarketplaceArtifacts = !hasOtherMarketplacePlugins(installed);
2565
+ if (cleaned) {
2566
+ writeFileSync(installedPath, JSON.stringify(installed, null, 2) + '\n');
2567
+ ok('Removed from installed_plugins.json');
2568
+ }
2569
+ } catch {
2570
+ // Conservative default: if registry shape is unknown, preserve marketplace cache.
2571
+ canRemoveMarketplaceArtifacts = false;
2553
2572
  }
2554
2573
 
2555
- // 3. Clean plugin system entries from settings.json
2556
- const pluginKey = `claude-mem-lite@${MARKETPLACE_KEY}`;
2574
+ // 4. Clean plugin system entries from settings.json
2557
2575
  const marketplaceKey = MARKETPLACE_KEY;
2558
2576
  if (settings.enabledPlugins) {
2559
- delete settings.enabledPlugins[pluginKey];
2577
+ delete settings.enabledPlugins[PLUGIN_KEY];
2560
2578
  }
2561
- if (settings.extraKnownMarketplaces) {
2579
+ if (settings.extraKnownMarketplaces && canRemoveMarketplaceArtifacts) {
2562
2580
  delete settings.extraKnownMarketplaces[marketplaceKey];
2563
2581
  }
2564
2582
  writeSettings(settings);
2565
2583
  ok('Hooks and plugin settings cleaned');
2566
2584
 
2567
- // 4. Clean plugin system registry files
2568
- const pluginsDir = join(homedir(), '.claude', 'plugins');
2569
-
2570
- // 4a. Remove marketplace directory
2585
+ // 5. Clean plugin system registry files (only if no other marketplace plugins remain)
2571
2586
  const marketplaceDir = join(pluginsDir, 'marketplaces', marketplaceKey);
2572
- if (existsSync(marketplaceDir)) {
2587
+ if (canRemoveMarketplaceArtifacts && existsSync(marketplaceDir)) {
2573
2588
  rmSync(marketplaceDir, { recursive: true, force: true });
2574
2589
  ok('Marketplace directory removed');
2575
2590
  }
2576
2591
 
2577
- // 4b. Remove cache directory
2592
+ // 5b. Remove cache directory
2578
2593
  const cacheDir = join(pluginsDir, 'cache', marketplaceKey);
2579
- if (existsSync(cacheDir)) {
2594
+ if (canRemoveMarketplaceArtifacts && existsSync(cacheDir)) {
2580
2595
  rmSync(cacheDir, { recursive: true, force: true });
2581
2596
  ok('Plugin cache removed');
2582
2597
  }
2583
2598
 
2584
- // 4c. Clean known_marketplaces.json
2599
+ // 5c. Clean known_marketplaces.json
2585
2600
  const knownPath = join(pluginsDir, 'known_marketplaces.json');
2586
2601
  try {
2587
2602
  const known = JSON.parse(readFileSync(knownPath, 'utf8'));
2588
- if (marketplaceKey in known) {
2603
+ if (canRemoveMarketplaceArtifacts && marketplaceKey in known) {
2589
2604
  delete known[marketplaceKey];
2590
2605
  writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n');
2591
2606
  ok('Removed from known_marketplaces.json');
2592
2607
  }
2593
2608
  } catch { /* file may not exist */ }
2594
2609
 
2595
- // 4d. Clean installed_plugins.json
2596
- const installedPath = join(pluginsDir, 'installed_plugins.json');
2597
- try {
2598
- const installed = JSON.parse(readFileSync(installedPath, 'utf8'));
2599
- const plugins = installed.plugins || installed;
2600
- let cleaned = false;
2601
- for (const key of Object.keys(plugins)) {
2602
- if (key.includes('claude-mem-lite') || key.includes('sdsrss')) {
2603
- delete plugins[key];
2604
- cleaned = true;
2605
- }
2606
- }
2607
- if (cleaned) {
2608
- writeFileSync(installedPath, JSON.stringify(installed, null, 2) + '\n');
2609
- ok('Removed from installed_plugins.json');
2610
- }
2611
- } catch { /* file may not exist */ }
2610
+ if (!canRemoveMarketplaceArtifacts && (existsSync(marketplaceDir) || existsSync(cacheDir))) {
2611
+ log('Marketplace cache preserved (other plugins may still depend on sdsrss marketplace)');
2612
+ }
2612
2613
 
2613
- // 5. Purge data if requested
2614
+ // 6. Purge data if requested
2614
2615
  if (flags.has('--purge')) {
2615
2616
  const expectedPurgePath = join(homedir(), '.claude-mem-lite');
2616
2617
  if (existsSync(DATA_DIR) && DATA_DIR === expectedPurgePath) {
@@ -2626,6 +2627,24 @@ async function uninstall() {
2626
2627
  console.log('\n Done!\n');
2627
2628
  }
2628
2629
 
2630
+ // ─── Cleanup Hooks ───────────────────────────────────────────────────────────
2631
+
2632
+ async function cleanupHooks() {
2633
+ console.log('\nclaude-mem-lite cleanup-hooks\n');
2634
+
2635
+ const settings = readSettings();
2636
+ const removed = cleanupMemHooksFromSettings(settings);
2637
+
2638
+ if (removed > 0) {
2639
+ writeSettings(settings);
2640
+ ok(`Removed ${removed} claude-mem-lite hook configuration${removed === 1 ? '' : 's'} from settings.json`);
2641
+ } else {
2642
+ ok('No claude-mem-lite hooks found in settings.json');
2643
+ }
2644
+
2645
+ console.log('');
2646
+ }
2647
+
2629
2648
  // ─── Status ─────────────────────────────────────────────────────────────────
2630
2649
 
2631
2650
  async function status() {
@@ -2645,11 +2664,24 @@ async function status() {
2645
2664
 
2646
2665
  // Hooks
2647
2666
  const settings = readSettings();
2648
- const hasHooks = settings.hooks && Object.values(settings.hooks).some(configs =>
2649
- configs.some(cfg => cfg.hooks?.some(h => h.command?.includes('hook.mjs')))
2650
- );
2651
- if (hasHooks) {
2667
+ const hasHooks = hasMemHooksConfigured(settings);
2668
+ const pluginDisabled = isPluginExplicitlyDisabled(settings);
2669
+ const pluginEnabled = settings.enabledPlugins?.[PLUGIN_KEY] === true;
2670
+
2671
+ if (pluginEnabled) {
2672
+ ok('Plugin: enabled in settings');
2673
+ } else if (pluginDisabled) {
2674
+ warn('Plugin: disabled in settings');
2675
+ } else {
2676
+ warn('Plugin: not present in enabledPlugins');
2677
+ }
2678
+
2679
+ if (hasHooks && pluginDisabled) {
2680
+ warn('Hooks: still configured in settings.json while plugin is disabled (runtime ignores them; run cleanup-hooks or uninstall to clean up)');
2681
+ } else if (hasHooks) {
2652
2682
  ok('Hooks: configured');
2683
+ } else if (pluginDisabled) {
2684
+ ok('Hooks: not configured');
2653
2685
  } else {
2654
2686
  fail('Hooks: not configured');
2655
2687
  }
@@ -2727,6 +2759,21 @@ async function doctor() {
2727
2759
  issues++;
2728
2760
  }
2729
2761
 
2762
+ // Plugin/hook lifecycle state
2763
+ const settings = readSettings();
2764
+ const hasHooks = hasMemHooksConfigured(settings);
2765
+ const pluginDisabled = isPluginExplicitlyDisabled(settings);
2766
+ if (pluginDisabled && hasHooks) {
2767
+ fail('Plugin lifecycle: plugin is disabled but claude-mem-lite hooks still remain in settings.json');
2768
+ issues++;
2769
+ } else if (pluginDisabled) {
2770
+ ok('Plugin lifecycle: disabled cleanly (no active mem hooks)');
2771
+ } else if (hasHooks) {
2772
+ ok('Plugin lifecycle: hooks active');
2773
+ } else {
2774
+ warn('Plugin lifecycle: hooks not configured');
2775
+ }
2776
+
2730
2777
  // Database
2731
2778
  if (existsSync(DB_PATH)) {
2732
2779
  try {
@@ -2799,6 +2846,50 @@ function isMemHook(cfg) {
2799
2846
  });
2800
2847
  }
2801
2848
 
2849
+ function hasMemHooksConfigured(settings) {
2850
+ if (!settings?.hooks) return false;
2851
+ return Object.values(settings.hooks).some(configs =>
2852
+ Array.isArray(configs) && configs.some(cfg => isMemHook(cfg))
2853
+ );
2854
+ }
2855
+
2856
+ export function clearPluginDisabledMarkerForDirectInstall(settings) {
2857
+ if (settings?.enabledPlugins?.[PLUGIN_KEY] !== false) return false;
2858
+ delete settings.enabledPlugins[PLUGIN_KEY];
2859
+ if (Object.keys(settings.enabledPlugins).length === 0) delete settings.enabledPlugins;
2860
+ return true;
2861
+ }
2862
+
2863
+ function cleanupMemHooksFromSettings(settings) {
2864
+ if (!settings?.hooks) return 0;
2865
+
2866
+ let removed = 0;
2867
+ for (const [event, configs] of Object.entries(settings.hooks)) {
2868
+ if (!Array.isArray(configs)) continue;
2869
+ const kept = configs.filter(cfg => !isMemHook(cfg));
2870
+ removed += configs.length - kept.length;
2871
+ if (kept.length > 0) settings.hooks[event] = kept;
2872
+ else delete settings.hooks[event];
2873
+ }
2874
+
2875
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
2876
+ return removed;
2877
+ }
2878
+
2879
+ function isPluginExplicitlyDisabled(settings) {
2880
+ return settings?.enabledPlugins?.[PLUGIN_KEY] === false;
2881
+ }
2882
+
2883
+ function getInstalledPluginEntries(installed) {
2884
+ if (installed?.plugins && typeof installed.plugins === 'object') return installed.plugins;
2885
+ return installed && typeof installed === 'object' ? installed : {};
2886
+ }
2887
+
2888
+ export function hasOtherMarketplacePlugins(installed, marketplaceKey = MARKETPLACE_KEY, pluginKey = PLUGIN_KEY) {
2889
+ const plugins = getInstalledPluginEntries(installed);
2890
+ return Object.keys(plugins).some(key => key !== pluginKey && key.endsWith(`@${marketplaceKey}`));
2891
+ }
2892
+
2802
2893
  function readSettings() {
2803
2894
  try {
2804
2895
  return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
@@ -2823,10 +2914,12 @@ async function manualUpdate() {
2823
2914
  // Force check by importing hook-update (bypasses throttle for manual use)
2824
2915
  const { checkForUpdate, getCurrentVersion } = await import('./hook-update.mjs');
2825
2916
  log('Checking for updates...');
2826
- const result = await checkForUpdate();
2917
+ const result = await checkForUpdate({ force: true, allowInstall: true });
2827
2918
 
2828
2919
  if (result?.updated) {
2829
2920
  ok(`Updated: v${result.from} → v${result.to}`);
2921
+ } else if (result?.updateAvailable && result?.installDeferred) {
2922
+ warn(`v${result.to} available — plugin mode only checks for updates, reinstall/update the plugin to apply it`);
2830
2923
  } else if (result?.updateAvailable) {
2831
2924
  warn(`v${result.to} available but install failed — try: node install.mjs install`);
2832
2925
  } else {
@@ -2881,31 +2974,38 @@ function syncVersions() {
2881
2974
 
2882
2975
  // ─── Main ───────────────────────────────────────────────────────────────────
2883
2976
 
2884
- switch (cmd) {
2885
- case 'install':
2886
- await install();
2887
- break;
2888
- case 'uninstall':
2889
- await uninstall();
2890
- break;
2891
- case 'status':
2892
- await status();
2893
- break;
2894
- case 'doctor':
2895
- await doctor();
2896
- break;
2897
- case 'update':
2898
- await manualUpdate();
2899
- break;
2900
- case 'release':
2901
- syncVersions();
2902
- break;
2903
- default:
2904
- if (IS_NPX) {
2905
- // npx claude-mem-lite (no args) → auto install
2977
+ export async function main(argv = process.argv.slice(2)) {
2978
+ cmd = argv[0];
2979
+ flags = new Set(argv.slice(1));
2980
+
2981
+ switch (cmd) {
2982
+ case 'install':
2906
2983
  await install();
2907
- } else {
2908
- console.log(`
2984
+ break;
2985
+ case 'uninstall':
2986
+ await uninstall();
2987
+ break;
2988
+ case 'status':
2989
+ await status();
2990
+ break;
2991
+ case 'doctor':
2992
+ await doctor();
2993
+ break;
2994
+ case 'cleanup-hooks':
2995
+ await cleanupHooks();
2996
+ break;
2997
+ case 'update':
2998
+ await manualUpdate();
2999
+ break;
3000
+ case 'release':
3001
+ syncVersions();
3002
+ break;
3003
+ default:
3004
+ if (IS_NPX) {
3005
+ // npx claude-mem-lite (no args) → auto install
3006
+ await install();
3007
+ } else {
3008
+ console.log(`
2909
3009
  claude-mem-lite — Lightweight memory system for Claude Code
2910
3010
 
2911
3011
  Usage:
@@ -2915,10 +3015,15 @@ Usage:
2915
3015
  node install.mjs uninstall --purge Remove and delete all data
2916
3016
  node install.mjs status Show current status
2917
3017
  node install.mjs doctor Diagnose issues
3018
+ node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
2918
3019
  node install.mjs update Check for and install updates
2919
3020
  node install.mjs release Sync version to plugin.json + marketplace.json
2920
3021
 
2921
3022
  npx claude-mem-lite Install via npx (one-liner)
2922
3023
  `);
2923
- }
3024
+ }
3025
+ }
2924
3026
  }
3027
+
3028
+ const IS_MAIN = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
3029
+ if (IS_MAIN) await main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.9.2",
3
+ "version": "2.9.4",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {