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 +29 -18
- package/install.mjs +21 -0
- package/package.json +1 -1
- package/registry-retriever.mjs +2 -1
- package/registry.mjs +6 -2
- package/server.mjs +263 -3
- package/tool-schemas.mjs +27 -0
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
package/registry-retriever.mjs
CHANGED
|
@@ -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 =
|
|
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'
|
|
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
|
+
};
|