claude-mem-lite 2.2.2 → 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/dispatch.mjs CHANGED
@@ -693,7 +693,7 @@ function reRankByKeywords(results, rawKeywords) {
693
693
  * @returns {object[]} Filtered results with decayed scores
694
694
  */
695
695
  function applyAdoptionDecay(results, db) {
696
- return results.map(r => {
696
+ const decayed = results.map(r => {
697
697
  const recs = r.recommend_count || 0;
698
698
  const adopts = r.adopt_count || 0;
699
699
  if (recs < 10) return r; // Cold start protection
@@ -721,6 +721,12 @@ function applyAdoptionDecay(results, db) {
721
721
  }
722
722
  return r;
723
723
  }).filter(Boolean);
724
+ // Re-sort after decay: decayed zombies must drop in ranking.
725
+ // BM25 scores are negative (more negative = better match), sort ascending.
726
+ if (decayed.some(r => r._decayed)) {
727
+ decayed.sort((a, b) => (a.composite_score ?? a.relevance) - (b.composite_score ?? b.relevance));
728
+ }
729
+ return decayed;
724
730
  }
725
731
 
726
732
  /**
@@ -763,6 +769,24 @@ function passesConfidenceGate(results, signals) {
763
769
  });
764
770
  }
765
771
 
772
+ // ─── Shared Post-Processing Pipeline ────────────────────────────────────────
773
+
774
+ /**
775
+ * Standard post-processing pipeline for dispatch results.
776
+ * Applies keyword re-ranking, adoption decay, confidence gating, and limit.
777
+ * @param {object[]} results FTS5 results
778
+ * @param {object} signals Context signals
779
+ * @param {object} db Registry database
780
+ * @param {number} [limit=3] Maximum results to return
781
+ * @returns {object[]} Post-processed results
782
+ */
783
+ function postProcessResults(results, signals, db, limit = 3) {
784
+ results = reRankByKeywords(results, signals.rawKeywords);
785
+ results = applyAdoptionDecay(results, db);
786
+ results = passesConfidenceGate(results, signals);
787
+ return results.slice(0, limit);
788
+ }
789
+
766
790
  // ─── Main Dispatch Functions ─────────────────────────────────────────────────
767
791
 
768
792
  /**
@@ -807,10 +831,7 @@ export async function dispatchOnSessionStart(db, userPrompt, sessionId, { hasHan
807
831
  }
808
832
  }
809
833
 
810
- results = reRankByKeywords(results, signals.rawKeywords);
811
- results = applyAdoptionDecay(results, db);
812
- results = passesConfidenceGate(results, signals);
813
- results = results.slice(0, 3);
834
+ results = postProcessResults(results, signals, db);
814
835
 
815
836
  let tier = 2;
816
837
 
@@ -827,10 +848,7 @@ export async function dispatchOnSessionStart(db, userPrompt, sessionId, { hasHan
827
848
  projectDomains,
828
849
  });
829
850
  if (haikuResults.length > 0) {
830
- // Apply same post-processing as Tier2 to prevent zombie/low-confidence bypass
831
- haikuResults = reRankByKeywords(haikuResults, signals.rawKeywords);
832
- haikuResults = applyAdoptionDecay(haikuResults, db);
833
- haikuResults = passesConfidenceGate(haikuResults, signals);
851
+ haikuResults = postProcessResults(haikuResults, signals, db);
834
852
  if (haikuResults.length > 0) results = haikuResults;
835
853
  }
836
854
  }
@@ -935,13 +953,7 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId, { sessionE
935
953
  }
936
954
  }
937
955
 
938
- // Re-rank: when rawKeywords are present, prefer resources whose intent_tags
939
- // match those keywords. "帮我做一下SEO审查" → rawKeywords=["seo"] → SEO audit
940
- // resources should rank above generic code-review resources.
941
- results = reRankByKeywords(results, signals.rawKeywords);
942
- results = applyAdoptionDecay(results, db);
943
- results = passesConfidenceGate(results, signals);
944
- results = results.slice(0, 3);
956
+ results = postProcessResults(results, signals, db);
945
957
 
946
958
  if (results.length === 0) return null;
947
959
 
@@ -1008,8 +1020,7 @@ export async function dispatchOnPreToolUse(db, event, sessionCtx = {}) {
1008
1020
 
1009
1021
  // Tier 2: FTS5 retrieval
1010
1022
  let results = retrieveResources(db, query, { limit: 3, projectDomains });
1011
- results = applyAdoptionDecay(results, db);
1012
- results = passesConfidenceGate(results, signals);
1023
+ results = postProcessResults(results, signals, db);
1013
1024
  if (results.length === 0) return null;
1014
1025
 
1015
1026
  const tier = 2; // Tier 3 disabled for PreToolUse — 2s hook timeout insufficient
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.2",
3
+ "version": "2.3.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -196,8 +196,9 @@ export function buildEnhancedQuery(signals) {
196
196
  // would dilute BM25 precision. Literal matching is sufficient — "seo" matches "seo"
197
197
  // directly across name, intent_tags, capability_summary, trigger_patterns.
198
198
  if (signals.rawKeywords?.length > 0) {
199
+ const isSafe = t => /^[a-zA-Z0-9]+$/.test(t) || /[\u4e00-\u9fff\u3400-\u4dbf]/.test(t);
199
200
  for (const kw of signals.rawKeywords) {
200
- const safeKw = expandToken(kw);
201
+ const safeKw = isSafe(kw) ? kw : `"${kw.replace(/"/g, '""')}"`;
201
202
  parts.push(`intent_tags:${safeKw}`);
202
203
  parts.push(safeKw);
203
204
  }
package/registry.mjs CHANGED
@@ -104,7 +104,7 @@ const INVOCATIONS_SCHEMA = `
104
104
  tier INTEGER CHECK(tier IN (1,2,3)),
105
105
  recommended INTEGER DEFAULT 1,
106
106
  adopted INTEGER DEFAULT 0,
107
- outcome TEXT CHECK(outcome IN ('success','partial','failure','skipped','ignored',NULL)),
107
+ outcome TEXT CHECK(outcome IN ('success','partial','failure','skipped','ignored') OR outcome IS NULL),
108
108
  score REAL,
109
109
  created_at TEXT DEFAULT (datetime('now'))
110
110
  );
@@ -161,7 +161,7 @@ export function ensureRegistryDb(dbPath) {
161
161
  if (!cols.some(c => c.name === 'invocation_name')) {
162
162
  db.exec("ALTER TABLE resources ADD COLUMN invocation_name TEXT DEFAULT ''");
163
163
  }
164
- } catch {}
164
+ } catch (e) { debugCatch(e, 'invocation_name-migration'); }
165
165
 
166
166
  // FTS5 + triggers: only create if not exists
167
167
  const hasFts = db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='resources_fts'`).get();
@@ -215,6 +215,10 @@ export function ensureRegistryDb(dbPath) {
215
215
  return db;
216
216
  }
217
217
 
218
+ // ─── Exported Schema (for test-helpers.mjs) ─────────────────────────────────
219
+
220
+ export { RESOURCES_SCHEMA, FTS5_SCHEMA, TRIGGERS_SCHEMA, INVOCATIONS_SCHEMA, PREINSTALLED_SCHEMA };
221
+
218
222
  // ─── Resource CRUD ───────────────────────────────────────────────────────────
219
223
 
220
224
  const UPSERT_SQL = `
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
@@ -1001,7 +1258,9 @@ const idleTimer = setInterval(() => {
1001
1258
  const thirtyDaysAgo = Date.now() - 30 * 86400000;
1002
1259
 
1003
1260
  db.transaction(() => {
1004
- // Delete old low-quality observations (importance<=1, never accessed, 30+ days)
1261
+ // Delete old low-quality observations (importance<=1, never accessed, 30+ days).
1262
+ // NOTE: no project filter — MCP server is global, operates across all projects.
1263
+ // This is intentionally broader than hook.mjs auto-compress (which scopes to current project).
1005
1264
  const deleted = db.prepare(`
1006
1265
  DELETE FROM observations
1007
1266
  WHERE importance <= 1 AND COALESCE(access_count, 0) = 0
@@ -1040,6 +1299,7 @@ function shutdown(exitCode = 0) {
1040
1299
  clearInterval(idleTimer);
1041
1300
  try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch {}
1042
1301
  try { db.close(); } catch {}
1302
+ try { if (registryDb) registryDb.close(); } catch {}
1043
1303
  process.exit(exitCode);
1044
1304
  }
1045
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
+ };