claude-mem-lite 2.9.7 → 2.10.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.9.7",
13
+ "version": "2.10.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.9.7",
3
+ "version": "2.10.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/dispatch.mjs CHANGED
@@ -899,28 +899,45 @@ function passesConfidenceGate(results, signals) {
899
899
 
900
900
  // ─── Auto-loaded Skill Filter ────────────────────────────────────────────────
901
901
 
902
+ // Plugin-namespaced skills with high adoption rates deserve proactive recommendations
903
+ // even though they're listed in system-reminder. The listing alone doesn't guarantee
904
+ // Claude invokes them at the right moment — a contextual nudge at the right time
905
+ // is more effective than a static list.
906
+ const AUTOLOADED_MIN_ADOPTIONS = 3; // Must have been adopted at least N times total
907
+ const AUTOLOADED_MIN_ADOPT_RATE = 0.08; // Minimum adoption rate to keep recommending
908
+
902
909
  /**
903
- * Filter out skills that are auto-loaded via plugin hooks (listed in system-reminder).
904
- * These skills don't need dispatch recommendations because the plugin's own hooks
905
- * already surface them to Claude at the right moment.
910
+ * Filter auto-loaded skills with adoption-aware logic.
911
+ *
912
+ * Plugin-namespaced skills (e.g. "superpowers:systematic-debugging") are listed
913
+ * in system-reminder, so Claude already knows about them. However, blanket filtering
914
+ * removes high-value skills that users actually adopt when recommended contextually.
915
+ *
916
+ * Strategy: filter auto-loaded skills that have poor adoption history (recommend fatigue),
917
+ * but keep those that users actually adopt — the contextual timing adds real value.
906
918
  *
907
- * User-installed standalone skills (non-namespaced invocation_name like "build-error-resolver")
908
- * are KEPT — users may not remember to invoke them at the right time, so contextual
909
- * recommendations still add value (installed skills have 11.5% adoption vs 6.1% community).
919
+ * User-installed standalone skills (non-namespaced like "build-error-resolver")
920
+ * are always KEPT — contextual recommendations still add value.
910
921
  *
911
922
  * @param {object[]} results FTS5 results
912
- * @returns {object[]} Filtered results — community + standalone installed skills
923
+ * @returns {object[]} Filtered results
913
924
  */
914
925
  function filterAutoLoadedSkills(results) {
915
926
  return results.filter(r => {
916
927
  if (r.type !== 'skill') return true;
917
928
  const inv = (r.invocation_name || '').trim();
918
929
  if (inv === '') return true; // Community resource — always recommend
919
- // Plugin-namespaced skills (e.g. "superpowers:systematic-debugging") are auto-loaded
920
- // via the plugin's own hooks in system-reminder — dispatch recommendation is redundant
921
- if (inv.includes(':')) return false;
922
- // Standalone installed skills (e.g. "build-error-resolver") keep for contextual recommendations
923
- return true;
930
+ // Standalone installed skills (e.g. "build-error-resolver") keep
931
+ if (!inv.includes(':')) return true;
932
+ // Plugin-namespaced: adoption-aware filter
933
+ // Cold start: keep if never recommended (no data to judge yet)
934
+ const recs = r.recommend_count || 0;
935
+ if (recs < 5) return true;
936
+ // Keep if adoption rate is healthy
937
+ const adopts = r.adopt_count || 0;
938
+ if (adopts >= AUTOLOADED_MIN_ADOPTIONS && (adopts + 1) / (recs + 2) >= AUTOLOADED_MIN_ADOPT_RATE) return true;
939
+ // Poor adoption history — suppress proactive recommendation
940
+ return false;
924
941
  });
925
942
  }
926
943
 
package/hook-update.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  import { execSync, execFileSync } from 'node:child_process';
6
6
  import { readFileSync, writeFileSync, copyFileSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync } from 'node:fs';
7
7
  import { join, dirname } from 'node:path';
8
- import { tmpdir } from 'node:os';
8
+ import { tmpdir, homedir } from 'node:os';
9
9
  import { DB_DIR } from './schema.mjs';
10
10
  import { debugCatch, debugLog } from './utils.mjs';
11
11
 
@@ -270,6 +270,20 @@ export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
270
270
 
271
271
  rmSync(stagingDir, { recursive: true, force: true });
272
272
  rmSync(backupDir, { recursive: true, force: true });
273
+
274
+ // Post-update migration: clean stale global MCP if plugin handles it
275
+ try {
276
+ if (isPluginMode()) {
277
+ const claudeJsonPath = join(homedir(), '.claude.json');
278
+ const cfg = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
279
+ if (cfg.mcpServers?.mem) {
280
+ delete cfg.mcpServers.mem;
281
+ writeFileSync(claudeJsonPath, JSON.stringify(cfg, null, 2) + '\n');
282
+ debugLog('DEBUG', 'hook-update', 'Post-update: removed stale global MCP "mem"');
283
+ }
284
+ }
285
+ } catch (e) { debugCatch(e, 'post-update-mcp-dedup'); }
286
+
273
287
  debugLog('DEBUG', 'hook-update', `Auto-update: switched ${installed.length} paths`);
274
288
  return true;
275
289
  } catch (err) {
package/install.mjs CHANGED
@@ -236,37 +236,57 @@ async function install() {
236
236
  }
237
237
  }
238
238
 
239
- // 3. Register MCP server
240
- log('Registering MCP server...');
239
+ // 3. Register MCP server (skip if plugin system already handles it)
240
+ // Plugin system registers MCP from .claude-plugin/.mcp.json → mcp__plugin_claude-mem-lite_mem__*
241
+ // Global registration via `claude mcp add` creates a DUPLICATE mcp__mem__* server.
242
+ // Detect plugin mode: installed_plugins.json has our entry → plugin handles MCP.
243
+ const installedPluginsPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
244
+ let pluginHandlesMcp = false;
241
245
  try {
242
- // Remove existing first (ignore errors)
243
- try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', 'mem'], { stdio: 'pipe' }); } catch {}
244
- execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
245
- // Remove project-level registration that shadows global (from .mcp.json)
246
- try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', 'mem'], { stdio: 'pipe' }); } catch {}
247
- ok('MCP server registered: mem');
248
- } catch (e) {
249
- fail('MCP registration failed: ' + e.message);
250
- warn('Try manually: claude mcp add -s user -t stdio mem -- node ' + SERVER_PATH);
246
+ const installed = JSON.parse(readFileSync(installedPluginsPath, 'utf8'));
247
+ pluginHandlesMcp = !!installed?.plugins?.[PLUGIN_KEY]?.length;
248
+ } catch { /* not installed via plugin system */ }
249
+
250
+ if (pluginHandlesMcp) {
251
+ log('MCP server: plugin system handles registration (skipping global)');
252
+ // Clean up stale global registration if it exists (from older install.mjs versions)
253
+ try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', 'mem'], { stdio: 'pipe' }); ok('Removed stale global MCP registration'); } catch {}
254
+ } else {
255
+ log('Registering MCP server...');
256
+ try {
257
+ try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', 'mem'], { stdio: 'pipe' }); } catch {}
258
+ execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
259
+ try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', 'mem'], { stdio: 'pipe' }); } catch {}
260
+ ok('MCP server registered: mem');
261
+ } catch (e) {
262
+ fail('MCP registration failed: ' + e.message);
263
+ warn('Try manually: claude mcp add -s user -t stdio mem -- node ' + SERVER_PATH);
264
+ }
251
265
  }
252
266
 
253
267
  // 3b. Deduplicate: if marketplace plugin also registers MCP + hooks,
254
268
  // clear them to prevent double execution. install.mjs hooks (in settings.json)
255
269
  // point to ~/.claude-mem-lite/ (latest code in dev mode via symlinks),
256
270
  // while plugin hooks use ${CLAUDE_PLUGIN_ROOT} (potentially stale marketplace copy).
271
+ //
272
+ // MCP dedup: Claude Code loads .mcp.json from BOTH marketplace root (generic scan)
273
+ // and cache dir (plugin system). Root-level .mcp.json → duplicate mcp__mem__*.
274
+ // Fix: .mcp.json moved to .claude-plugin/ (plugin installer reads from there,
275
+ // generic scanner only scans root). Clear any stale root copy here.
257
276
  const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY);
258
- const pluginMcpPath = join(pluginDir, '.mcp.json');
259
277
  const pluginHooksPath = join(pluginDir, 'hooks', 'hooks.json');
260
278
 
261
279
  if (existsSync(pluginDir)) {
262
- // Clear plugin MCP to prevent duplicate "mem" server
280
+ // Clear root-level .mcp.json if it exists (stale from older git versions).
281
+ // .mcp.json is now in .claude-plugin/ to avoid generic scanner duplicate.
282
+ const rootMcpPath = join(pluginDir, '.mcp.json');
263
283
  try {
264
- if (existsSync(pluginMcpPath)) {
265
- const pluginMcp = JSON.parse(readFileSync(pluginMcpPath, 'utf8'));
284
+ if (existsSync(rootMcpPath)) {
285
+ const pluginMcp = JSON.parse(readFileSync(rootMcpPath, 'utf8'));
266
286
  if (pluginMcp.mcpServers?.mem) {
267
287
  delete pluginMcp.mcpServers.mem;
268
- writeFileSync(pluginMcpPath, JSON.stringify(pluginMcp, null, 2) + '\n');
269
- ok('Marketplace plugin: MCP cleared (prevents duplicate)');
288
+ writeFileSync(rootMcpPath, JSON.stringify(pluginMcp, null, 2) + '\n');
289
+ ok('Marketplace plugin: root .mcp.json cleared (moved to .claude-plugin/)');
270
290
  }
271
291
  }
272
292
  } catch (e) { warn(`Marketplace MCP dedup: ${e.message}`); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.9.7",
3
+ "version": "2.10.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
package/scripts/setup.sh CHANGED
@@ -85,5 +85,49 @@ if [[ ! -d "$ROOT/node_modules/better-sqlite3" ]]; then
85
85
  fi
86
86
  fi
87
87
 
88
+ # 7. One-time MCP migration: clean stale registrations from pre-2.10 versions.
89
+ # Only runs once per version — skips if marker file exists.
90
+ # Before 2.10: .mcp.json at repo root caused duplicate MCP servers.
91
+ # - Global mcpServers.mem in ~/.claude.json (from old install.mjs)
92
+ # - Marketplace root .mcp.json (from old git clone)
93
+ # Now: .mcp.json in .claude-plugin/ → plugin system handles MCP exclusively.
94
+ MCP_MIGRATION="$DATA_DIR/runtime/.mcp-dedup-v2.10"
95
+ if [[ ! -f "$MCP_MIGRATION" && -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
96
+ CLAUDE_JSON="$HOME/.claude.json" ROOT="$ROOT" node -e '
97
+ const fs = require("fs");
98
+ let changed = false;
99
+ // 1. Remove stale global MCP registration
100
+ try {
101
+ const p = process.env.CLAUDE_JSON;
102
+ const d = JSON.parse(fs.readFileSync(p, "utf8"));
103
+ if (d.mcpServers?.mem) {
104
+ delete d.mcpServers.mem;
105
+ fs.writeFileSync(p, JSON.stringify(d, null, 2) + "\n");
106
+ process.stderr.write("✓ Removed stale global MCP \"mem\" (plugin handles it)\n");
107
+ changed = true;
108
+ }
109
+ } catch {}
110
+ // 2. Remove stale marketplace root .mcp.json
111
+ try {
112
+ const root = process.env.ROOT;
113
+ if (root.includes("/plugins/cache/")) {
114
+ const key = root.split("/plugins/cache/")[1].split("/")[0];
115
+ const mktMcp = require("path").join(require("os").homedir(), ".claude/plugins/marketplaces", key, ".mcp.json");
116
+ if (fs.existsSync(mktMcp)) {
117
+ const m = JSON.parse(fs.readFileSync(mktMcp, "utf8"));
118
+ if (m.mcpServers?.mem) {
119
+ delete m.mcpServers.mem;
120
+ fs.writeFileSync(mktMcp, JSON.stringify(m, null, 2) + "\n");
121
+ process.stderr.write("✓ Cleared marketplace root .mcp.json (moved to .claude-plugin/)\n");
122
+ changed = true;
123
+ }
124
+ }
125
+ }
126
+ } catch {}
127
+ if (!changed) process.stderr.write("✓ MCP migration: already clean\n");
128
+ ' 2>&2 || true
129
+ touch "$MCP_MIGRATION"
130
+ fi
131
+
88
132
  log_ok "claude-mem-lite ready"
89
133
  exit 0
package/server.mjs CHANGED
@@ -9,6 +9,7 @@ import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
9
9
  import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts, autoBoostIfNeeded, runIdleCleanup } from './server-internals.mjs';
10
10
  import { memSearchSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memRegistrySchema } from './tool-schemas.mjs';
11
11
  import { ensureRegistryDb, upsertResource } from './registry.mjs';
12
+ import { searchResources } from './registry-retriever.mjs';
12
13
  import { createRequire } from 'module';
13
14
 
14
15
  const require = createRequire(import.meta.url);
@@ -123,6 +124,13 @@ const server = new McpServer(
123
124
  'SEARCH WORKFLOW: mem_search → mem_timeline(anchor=ID) for surrounding context → mem_get(ids=[...]) for full details.',
124
125
  'Search tips: use short keywords (2-3 words), not full sentences. Filter with obs_type when relevant.',
125
126
  '',
127
+ 'SKILL/AGENT DISCOVERY (mem_registry):',
128
+ '- When your installed skills/agents don\'t cover the current workflow → mem_registry(action="search", query="<what you need>")',
129
+ '- When the user asks for a capability you lack → search the registry first',
130
+ '- When starting complex multi-step work → check if specialized agents exist: mem_registry(action="search", query="<task domain>", type="agent")',
131
+ '- Filter by type: type="skill" for workflow skills, type="agent" for specialized agents',
132
+ '- Say: "Searching mem for [purpose]..." when using this feature',
133
+ '',
126
134
  'MAINTENANCE: mem_compress consolidates old observations. mem_maintain runs dedup/cleanup/reindex.',
127
135
  ].join('\n'),
128
136
  },
@@ -1201,7 +1209,7 @@ server.registerTool(
1201
1209
  server.registerTool(
1202
1210
  'mem_registry',
1203
1211
  {
1204
- description: 'Manage tool resource registry: list resources, view stats, import/remove tools, reindex FTS5.',
1212
+ description: 'Manage tool resource registry: search for skills/agents by need, list resources, view stats, import/remove tools, reindex FTS5.',
1205
1213
  inputSchema: memRegistrySchema,
1206
1214
  },
1207
1215
  safeHandler(async (args) => {
@@ -1212,6 +1220,26 @@ server.registerTool(
1212
1220
 
1213
1221
  const action = args.action;
1214
1222
 
1223
+ if (action === 'search') {
1224
+ if (!args.query) {
1225
+ return { content: [{ type: 'text', text: 'search requires a query parameter' }], isError: true };
1226
+ }
1227
+ const results = searchResources(rdb, args.query, {
1228
+ type: args.type || undefined,
1229
+ limit: 5,
1230
+ });
1231
+ if (results.length === 0) {
1232
+ return { content: [{ type: 'text', text: `No matching resources for: "${args.query}"` }] };
1233
+ }
1234
+ const lines = results.map(r => {
1235
+ const howToUse = r.type === 'skill'
1236
+ ? (r.invocation_name ? `Skill tool: skill="${r.invocation_name}"` : `Community skill: ${r.name}`)
1237
+ : `Agent tool: subagent_type="${r.name}"`;
1238
+ return `${r.type === 'skill' ? 'S' : 'A'} **${r.name}** — ${truncate(r.capability_summary || '', 80)}\n Use: ${howToUse}`;
1239
+ });
1240
+ return { content: [{ type: 'text', text: `Found ${results.length} resource(s) for "${args.query}":\n\n${lines.join('\n\n')}` }] };
1241
+ }
1242
+
1215
1243
  if (action === 'list') {
1216
1244
  const typeFilter = args.type;
1217
1245
  const where = typeFilter ? 'WHERE type = ? AND status = ?' : 'WHERE status = ?';
@@ -1287,7 +1315,7 @@ server.registerTool(
1287
1315
  return { content: [{ type: 'text', text: `FTS5 reindexed. ${count.c} active resources.` }] };
1288
1316
  }
1289
1317
 
1290
- return { content: [{ type: 'text', text: `Unknown action: ${action}. Valid: list, stats, import, remove, reindex` }], isError: true };
1318
+ return { content: [{ type: 'text', text: `Unknown action: ${action}. Valid: search, list, stats, import, remove, reindex` }], isError: true };
1291
1319
  })
1292
1320
  );
1293
1321
 
package/tool-schemas.mjs CHANGED
@@ -92,8 +92,9 @@ export const memMaintainSchema = {
92
92
  };
93
93
 
94
94
  export const memRegistrySchema = {
95
- action: z.enum(['list', 'stats', 'import', 'remove', 'reindex']).describe('Registry operation'),
96
- type: z.enum(['skill', 'agent']).optional().describe('Filter by resource type (for list)'),
95
+ action: z.enum(['list', 'stats', 'search', 'import', 'remove', 'reindex']).describe('Registry operation'),
96
+ query: z.string().optional().describe('Search query keywords describing what you need (for search)'),
97
+ type: z.enum(['skill', 'agent']).optional().describe('Filter by resource type (for list/search)'),
97
98
  name: z.string().optional().describe('Resource name (for import/remove)'),
98
99
  resource_type: z.enum(['skill', 'agent']).optional().describe('Resource type (for import/remove)'),
99
100
  source: z.enum(['preinstalled', 'user']).optional().describe('Source (for import, default: user)'),
File without changes