claude-mem-lite 2.10.7 → 2.11.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.
File without changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.10.7",
3
+ "version": "2.11.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/.mcp.json CHANGED
File without changes
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
package/README.zh-CN.md CHANGED
File without changes
package/commands/mem.md CHANGED
File without changes
File without changes
package/commands/tools.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dispatch.mjs CHANGED
@@ -6,11 +6,11 @@
6
6
  import { basename, join } from 'path';
7
7
  import { existsSync } from 'fs';
8
8
  import { retrieveResources, buildEnhancedQuery, buildQueryFromText, DISPATCH_SYNONYMS } from './registry-retriever.mjs';
9
- import { renderInjection, renderHint } from './dispatch-inject.mjs';
9
+ import { renderInjection } from './dispatch-inject.mjs';
10
10
  import { updateResourceStats, recordInvocation } from './registry.mjs';
11
- import { debugCatch } from './utils.mjs';
12
- import { peekToolEvents } from './hook-shared.mjs';
13
- import { detectActiveSuite, shouldRecommendForStage, detectExplicitRequest, inferCurrentStage } from './dispatch-workflow.mjs';
11
+ import { debugCatch, extractErrorKeywords, truncate, inferProject, sanitizeFtsQuery } from './utils.mjs';
12
+ import { peekToolEvents, openDb } from './hook-shared.mjs';
13
+ import { detectExplicitRequest } from './dispatch-workflow.mjs';
14
14
  import { detectFailurePattern } from './dispatch-patterns.mjs';
15
15
 
16
16
  // ─── Constants ───────────────────────────────────────────────────────────────
@@ -1076,112 +1076,38 @@ export async function dispatchOnSessionStart() {
1076
1076
  }
1077
1077
 
1078
1078
  /**
1079
- * Dispatch on UserPromptSubmit: analyze user's actual prompt, return best resource suggestion.
1080
- * Tier 1+2 only (no Haiku fallback) for fast response within hook timeout.
1081
- * Cooldown + session dedup prevents double-recommending with SessionStart.
1079
+ * Dispatch on UserPromptSubmit: only fires for explicit user requests.
1080
+ * All ambient/proactive recommendations removed 9.7% adoption rate showed they were noise.
1081
+ * Users who need a skill/agent should explicitly ask ("I need X", "find me a tool for Y")
1082
+ * or use mem_registry search directly.
1082
1083
  * @param {Database} db Registry database
1083
1084
  * @param {string} userPrompt User's prompt text
1084
1085
  * @param {string} [sessionId] Session identifier for dedup
1085
1086
  * @returns {Promise<string|null>} Injection text or null
1086
1087
  */
1087
- export async function dispatchOnUserPrompt(db, userPrompt, sessionId, { sessionEvents, prevContext } = {}) {
1088
+ export async function dispatchOnUserPrompt(db, userPrompt, sessionId) {
1088
1089
  if (!userPrompt || !db) return null;
1089
1090
 
1090
1091
  try {
1091
- // 1. Explicit request highest priority, bypass cooldown but apply adoption decay
1092
+ // Only dispatch on explicit user requests ("I need a skill for X", "find me a tool for Y")
1092
1093
  const explicit = detectExplicitRequest(userPrompt);
1093
- if (explicit.isExplicit) {
1094
- const textQuery = buildQueryFromText(explicit.searchTerm);
1095
- if (textQuery) {
1096
- let explicitResults = retrieveResources(db, textQuery, { limit: 3, projectDomains: detectProjectDomains() });
1097
- explicitResults = filterAutoLoadedSkills(explicitResults);
1098
- explicitResults = filterGarbageMetadata(explicitResults);
1099
- explicitResults = applyAdoptionDecay(explicitResults, db);
1100
- if (explicitResults.length > 0) {
1101
- const best = explicitResults[0];
1102
- if (!sessionId || !isRecentlyRecommended(db, best.id, sessionId)) {
1103
- recordInvocation(db, { resource_id: best.id, session_id: sessionId, trigger: 'user_prompt', tier: 1, recommended: 1 });
1104
- updateResourceStats(db, best.id, 'recommend_count');
1105
- return renderInjection(best, buildRecommendReason(null, { explicit: true }));
1106
- }
1107
- }
1108
- }
1109
- }
1110
-
1111
- // 2. Suite auto-flow protection
1112
- const events = sessionEvents || peekToolEvents();
1113
- const activeSuite = detectActiveSuite(events);
1114
-
1115
- const projectDomains = detectProjectDomains();
1116
-
1117
- // Enrich prompt with previous session context (cached at session-start).
1118
- // Combines project history (next_steps) with user intent for richer signal.
1119
- const enrichedPrompt = prevContext
1120
- ? `${userPrompt}\n[Previous session: ${prevContext}]`
1121
- : userPrompt;
1094
+ if (!explicit.isExplicit) return null;
1122
1095
 
1123
- // Intent-aware enhanced query (column-targeted)
1124
- const signals = extractContextSignals({ tool_name: '_user_prompt' }, { userPrompt: enrichedPrompt });
1125
-
1126
- // Check if active suite covers the current stage
1127
- if (activeSuite) {
1128
- const currentStage = inferCurrentStage(signals.primaryIntent, activeSuite, signals.suppressedIntents);
1129
- if (currentStage) {
1130
- const { shouldRecommend } = shouldRecommendForStage(activeSuite, currentStage);
1131
- if (!shouldRecommend) return null;
1132
- }
1133
- }
1134
-
1135
- // 3. Normal FTS flow
1136
- const enhancedQuery = buildEnhancedQuery(signals);
1137
-
1138
- // Fetch extra results when rawKeywords are present — the top-3 by BM25 may be
1139
- // dominated by intent synonyms (e.g. "review" expands to many code-review terms),
1140
- // pushing domain-specific resources (e.g. SEO) below the limit. Extra headroom
1141
- // lets reRankByKeywords() promote domain-matched resources to the top.
1142
- const fetchLimit = signals.rawKeywords.length > 0 ? 8 : 3;
1143
- let results = enhancedQuery ? retrieveResources(db, enhancedQuery, { limit: fetchLimit, projectDomains }) : [];
1144
-
1145
- // Fallback: broad text query
1146
- if (results.length === 0) {
1147
- const textQuery = buildQueryFromText(userPrompt);
1148
- if (!textQuery) return null;
1149
- results = retrieveResources(db, textQuery, { limit: 3, projectDomains });
1150
- if (signals.suppressedIntents.length > 0) {
1151
- results = results.filter(r => {
1152
- const tags = (r.intent_tags || '').toLowerCase().split(/[\s,]+/);
1153
- return !signals.suppressedIntents.some(s => tags.includes(s));
1154
- });
1155
- }
1156
- }
1157
-
1158
- results = postProcessResults(results, signals, db);
1096
+ const textQuery = buildQueryFromText(explicit.searchTerm);
1097
+ if (!textQuery) return null;
1159
1098
 
1099
+ let results = retrieveResources(db, textQuery, { limit: 3, projectDomains: detectProjectDomains() });
1100
+ results = filterAutoLoadedSkills(results);
1101
+ results = filterGarbageMetadata(results);
1102
+ results = applyAdoptionDecay(results, db);
1160
1103
  if (results.length === 0) return null;
1161
1104
 
1162
- // Filter by cooldown + session dedup (hoisted cap + cooldown avoids N queries)
1163
- if (sessionId && isSessionCapped(db, sessionId)) return null;
1164
- const cooldown = getAdaptiveCooldown(db);
1165
- const viable = sessionId
1166
- ? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId, { skipCapCheck: true, cooldown }))
1167
- : results;
1168
- if (viable.length === 0) return null;
1169
-
1170
- const best = viable[0];
1171
-
1172
- recordInvocation(db, {
1173
- resource_id: best.id,
1174
- session_id: sessionId || null,
1175
- trigger: 'user_prompt',
1176
- tier: 2,
1177
- recommended: 1,
1178
- });
1179
- updateResourceStats(db, best.id, 'recommend_count');
1105
+ const best = results[0];
1106
+ if (sessionId && isRecentlyRecommended(db, best.id, sessionId)) return null;
1180
1107
 
1181
- const tier = decideTier(best, signals);
1182
- if (tier === 'silent') return null;
1183
- if (tier === 'hint') return renderHint(best);
1184
- return renderInjection(best, buildRecommendReason(signals));
1108
+ recordInvocation(db, { resource_id: best.id, session_id: sessionId, trigger: 'user_prompt', tier: 1, recommended: 1 });
1109
+ updateResourceStats(db, best.id, 'recommend_count');
1110
+ return renderInjection(best, buildRecommendReason(null, { explicit: true }));
1185
1111
  } catch (e) {
1186
1112
  debugCatch(e, 'dispatchOnUserPrompt');
1187
1113
  return null;
@@ -1189,82 +1115,105 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId, { sessionE
1189
1115
  }
1190
1116
 
1191
1117
  /**
1192
- * Dispatch on PreToolUse: filter, analyze, and optionally recommend.
1193
- * @param {Database} db Registry database
1118
+ * Dispatch on PreToolUse: error recall only.
1119
+ * When Claude is stuck in an error loop, search past observations for similar errors.
1120
+ * No ambient resource recommendations — only inject past solutions.
1121
+ * @param {Database} db Registry database (unused, kept for API compat)
1194
1122
  * @param {object} event Hook event data
1195
- * @param {object} [sessionCtx] Session context (userPrompt, recentFiles, sessionId)
1123
+ * @param {object} [sessionCtx] Session context
1196
1124
  * @returns {Promise<string|null>} Injection text or null
1197
1125
  */
1198
1126
  export async function dispatchOnPreToolUse(db, event, sessionCtx = {}) {
1199
- if (!db || !event) return null;
1127
+ if (!event) return null;
1200
1128
 
1201
1129
  try {
1202
- // Tier 0: Fast filter
1203
- const { skip } = shouldSkipDispatch(event);
1204
- if (skip) return null;
1130
+ // Only process Bash tool events (error patterns come from bash)
1131
+ if (event.tool_name !== 'Bash') return null;
1205
1132
 
1206
- // Phase transition gate: only dispatch on phase transitions to reduce noise.
1207
- // The first few events (≤3) always pass to allow initial recommendations.
1133
+ // Detect failure patterns from session event history
1208
1134
  const allEvents = peekToolEvents();
1209
- const currentPhase = inferSessionPhase(allEvents);
1210
- const prevPhase = allEvents.length > 1 ? inferSessionPhase(allEvents.slice(0, -1)) : null;
1211
- const phaseChanged = isPhaseTransition(prevPhase, currentPhase);
1212
-
1213
- if (!phaseChanged && allEvents.length > 3) return null;
1214
-
1215
- // Tier 1: Extract context signals
1216
- const signals = extractContextSignals(event, sessionCtx);
1217
-
1218
- // Suite protection: if a suite auto-flow is active, suppress recommendations
1219
- // for stages the suite already covers
1220
- const activeSuite = detectActiveSuite(allEvents);
1221
- if (activeSuite) {
1222
- const stage = inferCurrentStage(signals.primaryIntent, activeSuite, signals.suppressedIntents);
1223
- if (stage) {
1224
- const { shouldRecommend } = shouldRecommendForStage(activeSuite, stage);
1225
- if (!shouldRecommend) return null;
1226
- }
1227
- }
1228
- let query = buildEnhancedQuery(signals);
1229
- if (!query && sessionCtx?.userPrompt) {
1230
- query = buildQueryFromText(sessionCtx.userPrompt);
1231
- if (!query) return null;
1232
- }
1233
- if (!query) return null;
1135
+ const failurePattern = detectFailurePattern(allEvents);
1136
+ if (!failurePattern) return null;
1137
+
1138
+ // Extract error keywords from PREVIOUS failing events (not current — tool hasn't executed yet)
1139
+ const failingEvents = allEvents.filter(e =>
1140
+ e.tool_name === 'Bash' && /error|fail|exception/i.test(e.tool_response || '')
1141
+ ).slice(-3);
1142
+ const keywords = [...new Set(failingEvents.flatMap(e =>
1143
+ extractErrorKeywords(e.tool_input?.command || '', e.tool_response || '') || []
1144
+ ))];
1145
+ if (keywords.length === 0) return null;
1146
+
1147
+ // Search past observations for similar errors (Option A: observations only, no registry)
1148
+ return recallSimilarErrors(keywords, inferProject(), sessionCtx?._obsDb);
1149
+ } catch (e) {
1150
+ debugCatch(e, 'dispatchOnPreToolUse');
1151
+ return null;
1152
+ }
1153
+ }
1234
1154
 
1235
- const projectDomains = detectProjectDomains();
1155
+ // ─── Error Recall ─────────────────────────────────────────────────────────────
1236
1156
 
1237
- // Tier 2: FTS5 retrieval
1238
- let results = retrieveResources(db, query, { limit: 3, projectDomains });
1239
- results = postProcessResults(results, signals, db);
1240
- if (results.length === 0) return null;
1157
+ /**
1158
+ * Search past observations for similar errors and return formatted context.
1159
+ * @param {string[]} errorKeywords Keywords extracted from current error
1160
+ * @param {string} project Current project name
1161
+ * @returns {string|null} Formatted recall text or null
1162
+ */
1163
+ export function recallSimilarErrors(errorKeywords, project, externalDb) {
1164
+ const db = externalDb || openDb();
1165
+ if (!db) return null;
1166
+ const shouldClose = !externalDb;
1241
1167
 
1242
- // Apply DB-persisted cooldown and session dedup (hoisted cap + cooldown avoids N queries)
1243
- const sid = sessionCtx.sessionId || null;
1244
- if (sid && isSessionCapped(db, sid)) return null;
1245
- const cooldown = getAdaptiveCooldown(db);
1246
- const viable = sid
1247
- ? results.filter(r => !isRecentlyRecommended(db, r.id, sid, { skipCapCheck: true, cooldown }))
1248
- : results;
1249
- if (viable.length === 0) return null;
1250
- const best = viable[0];
1251
-
1252
- // Record invocation (also serves as cooldown/dedup marker)
1253
- recordInvocation(db, {
1254
- resource_id: best.id,
1255
- session_id: sid,
1256
- trigger: 'pre_tool_use',
1257
- tier: 2,
1258
- recommended: 1,
1259
- });
1260
- updateResourceStats(db, best.id, 'recommend_count');
1168
+ try {
1169
+ const query = sanitizeFtsQuery(errorKeywords.join(' '));
1170
+ if (!query || !query.trim()) return null;
1171
+
1172
+ // Search project-scoped bugfixes first
1173
+ let rows = db.prepare(`
1174
+ SELECT id, type, title, narrative, lesson_learned, importance, created_at
1175
+ FROM observations
1176
+ WHERE id IN (
1177
+ SELECT rowid FROM observations_fts WHERE observations_fts MATCH ?
1178
+ )
1179
+ AND type = 'bugfix'
1180
+ AND importance >= 2
1181
+ AND project = ?
1182
+ ORDER BY importance DESC, created_at DESC
1183
+ LIMIT 3
1184
+ `).all(query, project);
1185
+
1186
+ // Fallback: search across all projects
1187
+ if (rows.length === 0) {
1188
+ rows = db.prepare(`
1189
+ SELECT id, type, title, narrative, lesson_learned, importance, created_at
1190
+ FROM observations
1191
+ WHERE id IN (
1192
+ SELECT rowid FROM observations_fts WHERE observations_fts MATCH ?
1193
+ )
1194
+ AND type = 'bugfix'
1195
+ AND importance >= 2
1196
+ ORDER BY importance DESC, created_at DESC
1197
+ LIMIT 3
1198
+ `).all(query);
1199
+ }
1200
+
1201
+ if (rows.length === 0) return null;
1261
1202
 
1262
- const tier = decideTier(best, signals);
1263
- if (tier === 'silent') return null;
1264
- if (tier === 'hint') return renderHint(best);
1265
- return renderInjection(best, buildRecommendReason(signals));
1203
+ const lines = ['<memory-recall source="error-pattern">'];
1204
+ lines.push('You encountered similar errors before:');
1205
+ for (const r of rows) {
1206
+ lines.push(`- #${r.id}: ${truncate(r.title, 80)}`);
1207
+ if (r.lesson_learned) lines.push(` Lesson: ${r.lesson_learned}`);
1208
+ else if (r.narrative) lines.push(` Context: ${truncate(r.narrative, 120)}`);
1209
+ }
1210
+ lines.push('Use mem_get(ids=[...]) for full details.');
1211
+ lines.push('</memory-recall>');
1212
+ return lines.join('\n');
1266
1213
  } catch (e) {
1267
- debugCatch(e, 'dispatchOnPreToolUse');
1214
+ debugCatch(e, 'recallSimilarErrors');
1268
1215
  return null;
1216
+ } finally {
1217
+ if (shouldClose) db.close();
1269
1218
  }
1270
1219
  }
package/haiku-client.mjs CHANGED
File without changes
package/hook-context.mjs CHANGED
File without changes
package/hook-episode.mjs CHANGED
@@ -216,14 +216,18 @@ export function mergePendingEntries(episode) {
216
216
  * @returns {boolean} true if the episode has significant content
217
217
  */
218
218
  export function episodeHasSignificantContent(episode) {
219
- // 1. File edits or Bash errors always significant
220
- const hasEditsOrErrors = episode.entries.some(e =>
221
- EDIT_TOOLS.has(e.tool) ||
222
- (e.tool === 'Bash' && e.isError)
219
+ // 1. File edits always significant (code changes matter)
220
+ const hasEdits = episode.entries.some(e => EDIT_TOOLS.has(e.tool));
221
+ if (hasEdits) return true;
222
+
223
+ // 2. Test/build errors → significant (actionable failures)
224
+ // Plain bash errors without edits are noise (e.g. typos, exploration errors)
225
+ const hasTestOrBuildError = episode.entries.some(e =>
226
+ e.tool === 'Bash' && e.isError && (e.bashSig?.isTest || e.bashSig?.isBuild)
223
227
  );
224
- if (hasEditsOrErrors) return true;
228
+ if (hasTestOrBuildError) return true;
225
229
 
226
- // 2. Important files touched (config, schema, security, migration)
230
+ // 3. Important files touched (config, schema, security, migration)
227
231
  // Checks episode.files (all touched files, including reads) — catches important-file investigation
228
232
  const allFiles = episode.files || [];
229
233
  const hasImportantFile = allFiles.some(f =>
@@ -232,7 +236,7 @@ export function episodeHasSignificantContent(episode) {
232
236
  );
233
237
  if (hasImportantFile) return true;
234
238
 
235
- // 3. Research pattern: reading many files indicates investigation
239
+ // 4. Research pattern: reading many files indicates investigation
236
240
  const readCount = episode.entries.filter(e =>
237
241
  e.tool === 'Read' || e.tool === 'Grep'
238
242
  ).length;
package/hook-handoff.mjs CHANGED
File without changes
package/hook-llm.mjs CHANGED
File without changes
package/hook-memory.mjs CHANGED
File without changes
File without changes
package/hook-shared.mjs CHANGED
File without changes
package/hook-update.mjs CHANGED
File without changes
package/hook.mjs CHANGED
@@ -30,7 +30,6 @@ import {
30
30
  sessionFile, getSessionId, createSessionId, openDb, getRegistryDb,
31
31
  closeRegistryDb, spawnBackground, appendToolEvent, readAndClearToolEvents,
32
32
  resetInjectionBudget, hasInjectionBudget, incrementInjection,
33
- cachePrevContext, readAndClearPrevContext,
34
33
  } from './hook-shared.mjs';
35
34
  import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
36
35
  import { searchRelevantMemories, recallForFile } from './hook-memory.mjs';
@@ -758,13 +757,6 @@ async function handleSessionStart() {
758
757
  updateClaudeMd([...summaryLines, ...handoffLines].join('\n'));
759
758
 
760
759
  // Cache previous session context for user-prompt dispatch enrichment.
761
- // Session-start has project history but zero user intent — dispatching here
762
- // produced 0/119 adoption. Instead, cache next_steps and combine with
763
- // the first user-prompt for richer signal (see handleUserPrompt).
764
- if (latestSummary?.next_steps) {
765
- cachePrevContext(latestSummary.next_steps);
766
- }
767
-
768
760
  // Background rescan: detect changed/new managed resources since last scan.
769
761
  // TTL-based (1h) — avoids redundant filesystem scans on every session.
770
762
  // Non-blocking: spawns detached worker, results available before first user prompt.
@@ -920,15 +912,11 @@ async function handleUserPrompt() {
920
912
  db.close();
921
913
  }
922
914
 
923
- // Dispatch: recommend skill/agent based on user's actual prompt.
924
- // This is the ideal dispatch point — fires when the user submits their prompt,
925
- // before Claude starts working. Previous session's next_steps (cached at session-start)
926
- // enriches the signal when available, combining project history with user intent.
915
+ // Dispatch: only fires for explicit user requests ("I need X skill", "find me a tool for Y")
927
916
  try {
928
917
  const rdb = getRegistryDb();
929
918
  if (rdb && hasInjectionBudget()) {
930
- const prevContext = readAndClearPrevContext();
931
- const result = await dispatchOnUserPrompt(rdb, promptText, sessionId, { prevContext });
919
+ const result = await dispatchOnUserPrompt(rdb, promptText, sessionId);
932
920
  if (result) {
933
921
  process.stdout.write(result + '\n');
934
922
  incrementInjection();
package/hooks/hooks.json CHANGED
File without changes
package/install.mjs CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.10.7",
3
+ "version": "2.11.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
File without changes
File without changes
@@ -349,6 +349,11 @@ export function filterByProjectDomain(results, projectDomains) {
349
349
  // Sign convention: more negative = better. BM25 is negative, behavioral signals are subtracted.
350
350
  const COMPOSITE_EXPR = `(
351
351
  bm25(resources_fts, 3.0, 3.0, 3.0, 2.0, 2.0, 1.0, 1.0, 1.0) * 0.4
352
+ * CASE COALESCE(r.quality_tier, 'community')
353
+ WHEN 'installed' THEN 3.0
354
+ WHEN 'verified' THEN 2.0
355
+ ELSE 1.0
356
+ END
352
357
  - COALESCE(r.repo_stars * 1.0 / (r.repo_stars + 100.0), 0) * 0.15
353
358
  - (
354
359
  (COALESCE(r.success_count, 0) + 1.0) / (COALESCE(r.recommend_count, 0) + 2.0) * 0.5
File without changes
package/registry.mjs CHANGED
@@ -34,6 +34,10 @@ const RESOURCES_SCHEMA = `
34
34
  tech_stack TEXT DEFAULT '',
35
35
  use_cases TEXT DEFAULT '',
36
36
  complexity TEXT DEFAULT 'intermediate',
37
+ category TEXT,
38
+ quality_tier TEXT DEFAULT 'community',
39
+ popularity_score REAL DEFAULT 0,
40
+ personal_score REAL DEFAULT 0,
37
41
  recommend_count INTEGER DEFAULT 0,
38
42
  adopt_count INTEGER DEFAULT 0,
39
43
  weighted_adopt_sum REAL DEFAULT 0,
@@ -173,6 +177,13 @@ export function ensureRegistryDb(dbPath) {
173
177
  if (!resCols.has('recommendation_mode')) db.exec("ALTER TABLE resources ADD COLUMN recommendation_mode TEXT DEFAULT 'proactive'");
174
178
  // weighted_adopt_sum: continuous adoption score accumulator (vs binary adopt_count)
175
179
  if (!resCols.has('weighted_adopt_sum')) db.exec("ALTER TABLE resources ADD COLUMN weighted_adopt_sum REAL DEFAULT 0");
180
+ // Phase 2: Registry optimization columns
181
+ if (!resCols.has('category')) db.exec("ALTER TABLE resources ADD COLUMN category TEXT");
182
+ if (!resCols.has('quality_tier')) db.exec("ALTER TABLE resources ADD COLUMN quality_tier TEXT DEFAULT 'community'");
183
+ if (!resCols.has('popularity_score')) db.exec("ALTER TABLE resources ADD COLUMN popularity_score REAL DEFAULT 0");
184
+ if (!resCols.has('personal_score')) db.exec("ALTER TABLE resources ADD COLUMN personal_score REAL DEFAULT 0");
185
+ // Auto-set quality_tier for installed preinstalled resources
186
+ db.exec("UPDATE resources SET quality_tier = 'installed' WHERE source = 'preinstalled' AND quality_tier = 'community'");
176
187
  } catch (e) { debugCatch(e, 'resources-column-migration'); }
177
188
 
178
189
  // FTS5 + triggers: only create if not exists
File without changes
package/schema.mjs CHANGED
File without changes
File without changes
File without changes
package/server.mjs CHANGED
@@ -918,7 +918,7 @@ server.registerTool(
918
918
  safeHandler(async (args) => {
919
919
  if (args.project) args = { ...args, project: resolveProject(args.project) };
920
920
  const preview = args.preview !== false;
921
- const ageDays = args.age_days ?? 60;
921
+ const ageDays = args.age_days ?? 30;
922
922
  const cutoff = Date.now() - ageDays * 86400000;
923
923
  const projectFilter = args.project ? 'AND project = ?' : '';
924
924
  const baseParams = args.project ? [args.project] : [];
@@ -1103,9 +1103,32 @@ server.registerTool(
1103
1103
  ` Pending purge (idle-marked): ${pendingPurge.count}`,
1104
1104
  ];
1105
1105
  if (duplicates.length > 0) {
1106
- lines.push('', 'Top duplicates:');
1107
- for (const d of duplicates.slice(0, DUPLICATE_DISPLAY)) {
1108
- lines.push(` [${d.a.id}] "${truncate(d.a.title, 40)}" <-> [${d.b.id}] "${truncate(d.b.title, 40)}" (${d.similarity})`);
1106
+ const AUTO_MERGE_THRESHOLD = 0.85;
1107
+ const autoMergeable = duplicates.filter(d => parseFloat(d.similarity) >= AUTO_MERGE_THRESHOLD);
1108
+ const manualReview = duplicates.filter(d => parseFloat(d.similarity) < AUTO_MERGE_THRESHOLD);
1109
+
1110
+ if (autoMergeable.length > 0) {
1111
+ lines.push('', `Auto-mergeable pairs (similarity >= ${AUTO_MERGE_THRESHOLD}):`);
1112
+ for (const d of autoMergeable.slice(0, DUPLICATE_DISPLAY)) {
1113
+ // Keep the higher-importance or newer observation
1114
+ const keep = d.a.importance >= d.b.importance ? d.a : d.b;
1115
+ const remove = keep === d.a ? d.b : d.a;
1116
+ lines.push(` [${keep.id}] "${truncate(keep.title, 40)}" <-> [${remove.id}] "${truncate(remove.title, 40)}" (${d.similarity})`);
1117
+ }
1118
+ // Build ready-to-use merge_ids for auto-mergeable pairs
1119
+ const mergeIds = autoMergeable.map(d => {
1120
+ const keep = d.a.importance >= d.b.importance ? d.a : d.b;
1121
+ const remove = keep === d.a ? d.b : d.a;
1122
+ return [keep.id, remove.id];
1123
+ });
1124
+ lines.push('', `Ready-to-use command:`, ` mem_maintain(action="execute", operations=["dedup"], merge_ids=${JSON.stringify(mergeIds)})`);
1125
+ }
1126
+
1127
+ if (manualReview.length > 0) {
1128
+ lines.push('', 'Needs review:');
1129
+ for (const d of manualReview.slice(0, DUPLICATE_DISPLAY)) {
1130
+ lines.push(` [${d.a.id}] "${truncate(d.a.title, 40)}" <-> [${d.b.id}] "${truncate(d.b.title, 40)}" (${d.similarity})`);
1131
+ }
1109
1132
  }
1110
1133
  }
1111
1134
  return { content: [{ type: 'text', text: lines.join('\n') }] };
@@ -1146,7 +1169,21 @@ server.registerTool(
1146
1169
  LIMIT ${OP_ROW_CAP}
1147
1170
  )
1148
1171
  `).run(staleAge, ...baseParams);
1149
- results.push(`Decayed ${decayed.changes} stale observations` + (decayed.changes >= OP_ROW_CAP ? ' (cap reached, re-run for more)' : ''));
1172
+
1173
+ // Mark importance=1, never-accessed, old observations as pending-purge
1174
+ const idleMarked = db.prepare(`
1175
+ UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
1176
+ WHERE id IN (
1177
+ SELECT id FROM observations
1178
+ WHERE COALESCE(compressed_into, 0) = 0
1179
+ AND COALESCE(importance, 1) = 1
1180
+ AND COALESCE(access_count, 0) = 0
1181
+ AND created_at_epoch < ?
1182
+ ${projectFilter}
1183
+ LIMIT ${OP_ROW_CAP}
1184
+ )
1185
+ `).run(staleAge, ...baseParams);
1186
+ results.push(`Decayed ${decayed.changes} stale observations, marked ${idleMarked.changes} idle as pending-purge` + ((decayed.changes >= OP_ROW_CAP || idleMarked.changes >= OP_ROW_CAP) ? ' (cap reached, re-run for more)' : ''));
1150
1187
  }
1151
1188
 
1152
1189
  if (ops.includes('boost')) {
@@ -1224,18 +1261,24 @@ server.registerTool(
1224
1261
  if (!args.query) {
1225
1262
  return { content: [{ type: 'text', text: 'search requires a query parameter' }], isError: true };
1226
1263
  }
1227
- const results = searchResources(rdb, args.query, {
1264
+ let results = searchResources(rdb, args.query, {
1228
1265
  type: args.type || undefined,
1229
- limit: 5,
1266
+ limit: args.category || args.quality ? 20 : 5, // fetch more when filtering
1230
1267
  });
1268
+ // Apply category/quality filters if provided
1269
+ if (args.category) results = results.filter(r => r.category === args.category);
1270
+ if (args.quality) results = results.filter(r => r.quality_tier === args.quality);
1271
+ results = results.slice(0, 5);
1231
1272
  if (results.length === 0) {
1232
1273
  return { content: [{ type: 'text', text: `No matching resources for: "${args.query}"` }] };
1233
1274
  }
1234
1275
  const lines = results.map(r => {
1276
+ const qualityBadge = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
1277
+ const categoryLabel = r.category ? ` [${r.category}]` : '';
1235
1278
  const howToUse = r.type === 'skill'
1236
1279
  ? (r.invocation_name ? `Skill tool: skill="${r.invocation_name}"` : `Community skill: ${r.name}`)
1237
1280
  : `Agent tool: subagent_type="${r.name}"`;
1238
- return `${r.type === 'skill' ? 'S' : 'A'} **${r.name}** — ${truncate(r.capability_summary || '', 80)}\n Use: ${howToUse}`;
1281
+ return `${qualityBadge} ${r.type === 'skill' ? 'S' : 'A'} **${r.name}**${categoryLabel} — ${truncate(r.capability_summary || '', 80)}\n Use: ${howToUse}`;
1239
1282
  });
1240
1283
  return { content: [{ type: 'text', text: `Found ${results.length} resource(s) for "${args.query}":\n\n${lines.join('\n\n')}` }] };
1241
1284
  }
package/skill.md CHANGED
File without changes
package/tool-schemas.mjs CHANGED
@@ -74,7 +74,7 @@ export const memStatsSchema = {
74
74
 
75
75
  export const memCompressSchema = {
76
76
  preview: coerceBool.optional().describe('true=count candidates, false=execute compression (default: true)'),
77
- age_days: coerceInt.pipe(z.number().int().min(30).max(365)).optional().describe('Min age in days (default: 60)'),
77
+ age_days: coerceInt.pipe(z.number().int().min(30).max(365)).optional().describe('Min age in days (default: 30)'),
78
78
  project: z.string().optional().describe('Filter by project'),
79
79
  };
80
80
 
@@ -108,4 +108,6 @@ export const memRegistrySchema = {
108
108
  keywords: z.string().optional().describe('Search keywords (for import)'),
109
109
  tech_stack: z.string().optional().describe('Technology stack tags (for import)'),
110
110
  use_cases: z.string().optional().describe('Usage scenarios (for import)'),
111
+ category: z.string().optional().describe("Filter by category (e.g., 'testing', 'code-quality', 'debugging')"),
112
+ quality: z.enum(['installed', 'verified', 'community']).optional().describe('Filter by quality tier (default: all)'),
111
113
  };
package/utils.mjs CHANGED
@@ -561,9 +561,15 @@ export function inferProject() {
561
561
  */
562
562
  export function detectBashSignificance(input, response) {
563
563
  const cmd = (input.command || '').toLowerCase();
564
- const isError = /\berror\b|\bERR!|fail(ed|ure)?|exception|panic|traceback|errno|enoent|command not found/i.test(response)
564
+ // Skip error keyword matching when the command is a read/search operation
565
+ // (grep output naturally contains matched keywords like "error")
566
+ const isSearchCmd = /\b(grep|rg|ag|ack|cat|head|tail|less|more|find|locate|wc|file|which|type)\b/i.test(cmd);
567
+ const isError = !isSearchCmd
568
+ && /\berror\b|\bERR!|fail(ed|ure)?|exception|panic|traceback|errno|enoent|command not found/i.test(response)
565
569
  && response.length > 15;
566
- const isTest = /\b(test|jest|pytest|vitest|mocha|spec|cypress|playwright)\b/i.test(cmd);
570
+ // Match actual test runner invocations, not commands that merely reference "test" as a keyword
571
+ const isTest = /\b(npm\s+test|npm\s+run\s+test|yarn\s+test|pnpm\s+test|pnpm\s+run\s+test|bun\s+test|go\s+test|cargo\s+test)\b/i.test(cmd)
572
+ || /\b(jest|pytest|vitest|mocha|cypress|playwright)\b/i.test(cmd);
567
573
  const isBuild = /\b(build|compile|tsc|webpack|vite|rollup|esbuild|make|cargo)\b/i.test(cmd);
568
574
  const isGit = /\bgit\s+(commit|merge|rebase|cherry-pick|push)\b/i.test(cmd);
569
575
  const isDeploy = /\b(deploy|docker|kubectl|terraform)\b/i.test(cmd);