claude-mem-lite 2.2.3 → 2.3.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/install.mjs CHANGED
@@ -564,6 +564,27 @@ const RESOURCE_METADATA = {
564
564
  capability_summary: 'Autonomous skill extraction and continuous learning from Claude Code work sessions',
565
565
  trigger_patterns: 'when user wants to extract reusable skills from work sessions or enable continuous learning',
566
566
  },
567
+ 'skill:mem-memory': {
568
+ intent_tags: 'memory,save,store,remember,note,record,persist,auto-save',
569
+ domain_tags: 'memory,ai,claude',
570
+ capability_summary: 'Save content to memory — with explicit content, instructions, or auto-summarize current session',
571
+ trigger_patterns: 'when user wants to save something to memory or auto-save session highlights',
572
+ invocation_name: 'claude-mem-lite:memory',
573
+ },
574
+ 'skill:mem-update': {
575
+ intent_tags: 'maintenance,cleanup,deduplicate,decay,optimize,reindex,health',
576
+ domain_tags: 'memory,registry,maintenance',
577
+ capability_summary: 'Auto-maintain memory and resource registry — deduplicate, merge, decay, cleanup, reindex',
578
+ trigger_patterns: 'when user wants to clean up memory database or maintain the tool registry',
579
+ invocation_name: 'claude-mem-lite:update',
580
+ },
581
+ 'skill:mem-tools': {
582
+ intent_tags: 'import,tools,github,skills,agents,registry,add,discover',
583
+ domain_tags: 'memory,registry,github,tools',
584
+ capability_summary: 'Import skills and agents from GitHub repositories into the tool resource registry',
585
+ trigger_patterns: 'when user wants to import or add new skills and agents from GitHub to the tool registry',
586
+ invocation_name: 'claude-mem-lite:tools',
587
+ },
567
588
  'skill:anthropic-architect': {
568
589
  intent_tags: 'anthropic,architecture,system-design,claude,patterns,best-practices',
569
590
  domain_tags: 'anthropic,architecture,ai',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
package/server.mjs CHANGED
@@ -5,9 +5,11 @@
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryToOr, inferProject, computeMinHash, scrubSecrets, fmtDate, isoWeekKey, debugLog, debugCatch } from './utils.mjs';
8
- import { ensureDb, DB_PATH } from './schema.mjs';
8
+ import { ensureDb, DB_PATH, DB_DIR } from './schema.mjs';
9
9
  import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts } from './server-internals.mjs';
10
- import { memSearchSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema } from './tool-schemas.mjs';
10
+ import { memSearchSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memRegistrySchema } from './tool-schemas.mjs';
11
+ import { ensureRegistryDb, upsertResource } from './registry.mjs';
12
+ import { join } from 'path';
11
13
 
12
14
  // ─── Database ───────────────────────────────────────────────────────────────
13
15
 
@@ -34,6 +36,22 @@ try {
34
36
  // Server process uses longer busy_timeout for concurrent MCP requests
35
37
  db.pragma('busy_timeout = 5000');
36
38
 
39
+ // ─── Registry Database (lazy-loaded on first mem_registry call) ─────────────
40
+
41
+ const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
42
+ let registryDb = null;
43
+
44
+ function getRegistryDb() {
45
+ if (registryDb) return registryDb;
46
+ try {
47
+ registryDb = ensureRegistryDb(REGISTRY_DB_PATH);
48
+ registryDb.pragma('busy_timeout = 3000');
49
+ } catch (e) {
50
+ debugLog('WARN', 'server', `Registry DB not available: ${e.message}`);
51
+ }
52
+ return registryDb;
53
+ }
54
+
37
55
  // inferProject, jaccardSimilarity, sanitizeFtsQuery, typeIcon, truncate, fmtDate imported from utils.mjs
38
56
 
39
57
  // ─── Scoring Model Constants ────────────────────────────────────────────────
@@ -977,6 +995,245 @@ server.registerTool(
977
995
  })
978
996
  );
979
997
 
998
+ // ─── Tool: mem_maintain ──────────────────────────────────────────────────────
999
+
1000
+ server.registerTool(
1001
+ 'mem_maintain',
1002
+ {
1003
+ description: 'Memory maintenance: scan for duplicates/stale/broken items, then execute cleanup/decay/boost/dedup operations.',
1004
+ inputSchema: memMaintainSchema,
1005
+ },
1006
+ safeHandler(async (args) => {
1007
+ const STALE_AGE_MS = 30 * 86400000;
1008
+ const SIMILARITY_THRESHOLD = 0.7;
1009
+ const SCAN_LIMIT = 500;
1010
+ const DUPLICATE_LIMIT = 50;
1011
+ const DUPLICATE_DISPLAY = 15;
1012
+
1013
+ const action = args.action;
1014
+ const project = args.project;
1015
+ const projectFilter = project ? 'AND project = ?' : '';
1016
+ const baseParams = project ? [project] : [];
1017
+
1018
+ if (action === 'scan') {
1019
+ // 1. Find near-duplicate titles (pre-compute word sets, then O(n²) Jaccard)
1020
+ const recent = db.prepare(`
1021
+ SELECT id, title, project, importance, access_count, created_at_epoch
1022
+ FROM observations
1023
+ WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}
1024
+ ORDER BY created_at_epoch DESC
1025
+ LIMIT ${SCAN_LIMIT}
1026
+ `).all(...baseParams);
1027
+
1028
+ const wordSets = recent.map(r => new Set((r.title || '').toLowerCase().split(/\s+/).filter(w => w.length > 2)));
1029
+ const duplicates = [];
1030
+ for (let i = 0; i < recent.length && duplicates.length < DUPLICATE_LIMIT; i++) {
1031
+ if (wordSets[i].size === 0) continue;
1032
+ for (let j = i + 1; j < recent.length; j++) {
1033
+ if (wordSets[j].size === 0) continue;
1034
+ const sim = jaccardSimilarity(wordSets[i], wordSets[j]);
1035
+ if (sim > SIMILARITY_THRESHOLD) {
1036
+ duplicates.push({
1037
+ a: { id: recent[i].id, title: recent[i].title, importance: recent[i].importance },
1038
+ b: { id: recent[j].id, title: recent[j].title, importance: recent[j].importance },
1039
+ similarity: sim.toFixed(2),
1040
+ });
1041
+ }
1042
+ if (duplicates.length >= DUPLICATE_LIMIT) break;
1043
+ }
1044
+ }
1045
+
1046
+ // 2. Consolidated stats query (single table scan instead of 4 separate COUNTs)
1047
+ const staleAge = Date.now() - STALE_AGE_MS;
1048
+ const stats = db.prepare(`
1049
+ SELECT
1050
+ COUNT(*) as total,
1051
+ SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
1052
+ AND created_at_epoch < ? THEN 1 ELSE 0 END) as stale,
1053
+ SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
1054
+ THEN 1 ELSE 0 END) as broken,
1055
+ SUM(CASE WHEN COALESCE(access_count, 0) > 3 AND COALESCE(importance, 1) < 3
1056
+ THEN 1 ELSE 0 END) as boostable
1057
+ FROM observations
1058
+ WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}
1059
+ `).get(staleAge, ...baseParams);
1060
+
1061
+ const lines = [
1062
+ `Memory maintenance scan:`,
1063
+ ` Total active observations: ${stats.total}`,
1064
+ ` Near-duplicate pairs: ${duplicates.length}`,
1065
+ ` Stale (>30d, imp=1, no access): ${stats.stale}`,
1066
+ ` Broken (no title/narrative): ${stats.broken}`,
1067
+ ` Boostable (accessed>3, imp<3): ${stats.boostable}`,
1068
+ ];
1069
+ if (duplicates.length > 0) {
1070
+ lines.push('', 'Top duplicates:');
1071
+ for (const d of duplicates.slice(0, DUPLICATE_DISPLAY)) {
1072
+ lines.push(` [${d.a.id}] "${truncate(d.a.title, 40)}" <-> [${d.b.id}] "${truncate(d.b.title, 40)}" (${d.similarity})`);
1073
+ }
1074
+ }
1075
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1076
+ }
1077
+
1078
+ if (action === 'execute') {
1079
+ const ops = args.operations || [];
1080
+ const results = [];
1081
+ const staleAge = Date.now() - STALE_AGE_MS;
1082
+
1083
+ db.transaction(() => {
1084
+ if (ops.includes('cleanup')) {
1085
+ const deleted = db.prepare(`
1086
+ DELETE FROM observations
1087
+ WHERE COALESCE(compressed_into, 0) = 0
1088
+ AND (title IS NULL OR title = '')
1089
+ AND (narrative IS NULL OR narrative = '')
1090
+ ${projectFilter}
1091
+ `).run(...baseParams);
1092
+ results.push(`Cleaned up ${deleted.changes} broken observations`);
1093
+ }
1094
+
1095
+ if (ops.includes('decay')) {
1096
+ const decayed = db.prepare(`
1097
+ UPDATE observations SET importance = MAX(1, COALESCE(importance, 1) - 1)
1098
+ WHERE COALESCE(compressed_into, 0) = 0
1099
+ AND COALESCE(importance, 1) > 1
1100
+ AND COALESCE(access_count, 0) = 0
1101
+ AND created_at_epoch < ?
1102
+ ${projectFilter}
1103
+ `).run(staleAge, ...baseParams);
1104
+ results.push(`Decayed ${decayed.changes} stale observations`);
1105
+ }
1106
+
1107
+ if (ops.includes('boost')) {
1108
+ const boosted = db.prepare(`
1109
+ UPDATE observations SET importance = MIN(3, COALESCE(importance, 1) + 1)
1110
+ WHERE COALESCE(compressed_into, 0) = 0
1111
+ AND COALESCE(access_count, 0) > 3
1112
+ AND COALESCE(importance, 1) < 3
1113
+ ${projectFilter}
1114
+ `).run(...baseParams);
1115
+ results.push(`Boosted ${boosted.changes} frequently-accessed observations`);
1116
+ }
1117
+
1118
+ if (ops.includes('dedup') && args.merge_ids) {
1119
+ let totalMerged = 0;
1120
+ const mergeStmt = db.prepare('UPDATE observations SET compressed_into = ? WHERE id = ? AND COALESCE(compressed_into, 0) = 0');
1121
+ for (const group of args.merge_ids) {
1122
+ if (group.length < 2) continue;
1123
+ const [keepId, ...removeIds] = group;
1124
+ for (const removeId of removeIds) mergeStmt.run(keepId, removeId);
1125
+ totalMerged += removeIds.length;
1126
+ }
1127
+ results.push(`Merged ${totalMerged} duplicate observations`);
1128
+ }
1129
+ })();
1130
+
1131
+ // FTS5 optimize (outside transaction)
1132
+ db.exec("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
1133
+ results.push('FTS5 index optimized');
1134
+
1135
+ return { content: [{ type: 'text', text: results.join('\n') }] };
1136
+ }
1137
+
1138
+ return { content: [{ type: 'text', text: `Unknown action: ${action}. Use "scan" or "execute".` }], isError: true };
1139
+ })
1140
+ );
1141
+
1142
+ // ─── Tool: mem_registry ─────────────────────────────────────────────────────
1143
+
1144
+ server.registerTool(
1145
+ 'mem_registry',
1146
+ {
1147
+ description: 'Manage tool resource registry: list resources, view stats, import/remove tools, reindex FTS5.',
1148
+ inputSchema: memRegistrySchema,
1149
+ },
1150
+ safeHandler(async (args) => {
1151
+ const rdb = getRegistryDb();
1152
+ if (!rdb) {
1153
+ return { content: [{ type: 'text', text: 'Registry DB not available. Run install first.' }], isError: true };
1154
+ }
1155
+
1156
+ const action = args.action;
1157
+
1158
+ if (action === 'list') {
1159
+ const typeFilter = args.type;
1160
+ const where = typeFilter ? 'WHERE type = ? AND status = ?' : 'WHERE status = ?';
1161
+ const params = typeFilter ? [typeFilter, 'active'] : ['active'];
1162
+ const resources = rdb.prepare(`
1163
+ SELECT name, type, invocation_name, recommend_count, adopt_count, capability_summary
1164
+ FROM resources ${where} ORDER BY type, name
1165
+ `).all(...params);
1166
+
1167
+ if (resources.length === 0) return { content: [{ type: 'text', text: 'No resources found.' }] };
1168
+
1169
+ const lines = resources.map(r =>
1170
+ `${r.type === 'skill' ? 'S' : 'A'} ${r.name}${r.invocation_name ? ` (${r.invocation_name})` : ''} — rec:${r.recommend_count} adopt:${r.adopt_count} — ${truncate(r.capability_summary || '', 60)}`
1171
+ );
1172
+ return { content: [{ type: 'text', text: `Resources (${resources.length}):\n${lines.join('\n')}` }] };
1173
+ }
1174
+
1175
+ if (action === 'stats') {
1176
+ const total = rdb.prepare('SELECT COUNT(*) as c FROM resources WHERE status = ?').get('active');
1177
+ const byType = rdb.prepare('SELECT type, COUNT(*) as c FROM resources WHERE status = ? GROUP BY type').all('active');
1178
+ const topAdopted = rdb.prepare(`
1179
+ SELECT name, type, adopt_count, recommend_count
1180
+ FROM resources WHERE status = ? AND adopt_count > 0
1181
+ ORDER BY adopt_count DESC LIMIT 10
1182
+ `).all('active');
1183
+ const zeroAdopt = rdb.prepare(`
1184
+ SELECT COUNT(*) as c FROM resources
1185
+ WHERE status = ? AND recommend_count > 0 AND adopt_count = 0
1186
+ `).get('active');
1187
+ const userAdded = rdb.prepare(`
1188
+ SELECT COUNT(*) as c FROM resources WHERE status = ? AND source = 'user'
1189
+ `).get('active');
1190
+
1191
+ const lines = [
1192
+ `Registry Stats:`,
1193
+ ` Total active: ${total.c}`,
1194
+ ...byType.map(t => ` ${t.type}: ${t.c}`),
1195
+ ` User-added: ${userAdded.c}`,
1196
+ ` Zero adoption (recommended but never adopted): ${zeroAdopt.c}`,
1197
+ ];
1198
+ if (topAdopted.length > 0) {
1199
+ lines.push('', 'Top adopted:');
1200
+ for (const r of topAdopted) {
1201
+ lines.push(` ${r.name} (${r.type}): ${r.adopt_count}/${r.recommend_count}`);
1202
+ }
1203
+ }
1204
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1205
+ }
1206
+
1207
+ if (action === 'import') {
1208
+ if (!args.name || !args.resource_type) {
1209
+ return { content: [{ type: 'text', text: 'import requires name and resource_type' }], isError: true };
1210
+ }
1211
+ const IMPORT_STRING_FIELDS = ['repo_url', 'local_path', 'invocation_name', 'intent_tags',
1212
+ 'domain_tags', 'trigger_patterns', 'capability_summary', 'keywords', 'tech_stack', 'use_cases'];
1213
+ const fields = { name: args.name, type: args.resource_type, status: 'active', source: args.source || 'user' };
1214
+ for (const f of IMPORT_STRING_FIELDS) fields[f] = args[f] || '';
1215
+ const id = upsertResource(rdb, fields);
1216
+ return { content: [{ type: 'text', text: `Imported: ${args.resource_type}:${args.name} (id=${id})` }] };
1217
+ }
1218
+
1219
+ if (action === 'remove') {
1220
+ if (!args.name || !args.resource_type) {
1221
+ return { content: [{ type: 'text', text: 'remove requires name and resource_type' }], isError: true };
1222
+ }
1223
+ const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(args.resource_type, args.name);
1224
+ return { content: [{ type: 'text', text: result.changes > 0 ? `Removed: ${args.resource_type}:${args.name}` : 'Not found.' }] };
1225
+ }
1226
+
1227
+ if (action === 'reindex') {
1228
+ rdb.exec("INSERT INTO resources_fts(resources_fts) VALUES('rebuild')");
1229
+ const count = rdb.prepare('SELECT COUNT(*) as c FROM resources WHERE status = ?').get('active');
1230
+ return { content: [{ type: 'text', text: `FTS5 reindexed. ${count.c} active resources.` }] };
1231
+ }
1232
+
1233
+ return { content: [{ type: 'text', text: `Unknown action: ${action}. Valid: list, stats, import, remove, reindex` }], isError: true };
1234
+ })
1235
+ );
1236
+
980
1237
  // ─── WAL Checkpoint (periodic) ───────────────────────────────────────────────
981
1238
 
982
1239
  // Checkpoint WAL every 5 minutes to prevent unbounded growth
@@ -1042,6 +1299,7 @@ function shutdown(exitCode = 0) {
1042
1299
  clearInterval(idleTimer);
1043
1300
  try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch {}
1044
1301
  try { db.close(); } catch {}
1302
+ try { if (registryDb) registryDb.close(); } catch {}
1045
1303
  process.exit(exitCode);
1046
1304
  }
1047
1305
  process.on('SIGINT', () => shutdown(0));
package/tool-schemas.mjs CHANGED
@@ -54,3 +54,30 @@ export const memCompressSchema = {
54
54
  age_days: z.number().int().min(30).max(365).optional().describe('Min age in days (default: 60)'),
55
55
  project: z.string().optional().describe('Filter by project'),
56
56
  };
57
+
58
+ export const memMaintainSchema = {
59
+ action: z.enum(['scan', 'execute']).describe('scan=analyze candidates, execute=apply changes'),
60
+ operations: z.array(z.enum(['dedup', 'decay', 'cleanup', 'boost'])).optional()
61
+ .describe('Operations to execute (for action=execute)'),
62
+ merge_ids: z.array(z.array(z.number().int()).min(2)).optional()
63
+ .describe('For dedup: [[keepId, removeId1, removeId2], ...] — first ID in each group is kept'),
64
+ project: z.string().optional().describe('Filter by project'),
65
+ };
66
+
67
+ export const memRegistrySchema = {
68
+ action: z.enum(['list', 'stats', 'import', 'remove', 'reindex']).describe('Registry operation'),
69
+ type: z.enum(['skill', 'agent']).optional().describe('Filter by resource type (for list)'),
70
+ name: z.string().optional().describe('Resource name (for import/remove)'),
71
+ resource_type: z.enum(['skill', 'agent']).optional().describe('Resource type (for import/remove)'),
72
+ source: z.enum(['preinstalled', 'user']).optional().describe('Source (for import, default: user)'),
73
+ repo_url: z.string().optional().describe('GitHub repository URL (for import)'),
74
+ local_path: z.string().optional().describe('Local file path (for import)'),
75
+ invocation_name: z.string().optional().describe('Invocation name like "plugin:skill" (for import)'),
76
+ intent_tags: z.string().optional().describe('Comma-separated intent tags (for import)'),
77
+ domain_tags: z.string().optional().describe('Comma-separated domain/tech tags (for import)'),
78
+ trigger_patterns: z.string().optional().describe('When to recommend this tool (for import)'),
79
+ capability_summary: z.string().optional().describe('What this tool does (for import)'),
80
+ keywords: z.string().optional().describe('Search keywords (for import)'),
81
+ tech_stack: z.string().optional().describe('Technology stack tags (for import)'),
82
+ use_cases: z.string().optional().describe('Usage scenarios (for import)'),
83
+ };