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.
- package/.claude-plugin/marketplace.json +0 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/README.zh-CN.md +0 -0
- package/commands/mem.md +0 -0
- package/commands/memory.md +0 -0
- package/commands/tools.md +0 -0
- package/commands/update.md +0 -0
- package/dispatch-feedback.mjs +0 -0
- package/dispatch-inject.mjs +0 -0
- package/dispatch-patterns.mjs +0 -0
- package/dispatch-workflow.mjs +0 -0
- package/dispatch.mjs +108 -159
- package/haiku-client.mjs +0 -0
- package/hook-context.mjs +0 -0
- package/hook-episode.mjs +11 -7
- package/hook-handoff.mjs +0 -0
- package/hook-llm.mjs +0 -0
- package/hook-memory.mjs +0 -0
- package/hook-semaphore.mjs +0 -0
- package/hook-shared.mjs +0 -0
- package/hook-update.mjs +0 -0
- package/hook.mjs +2 -14
- package/hooks/hooks.json +0 -0
- package/install.mjs +0 -0
- package/package.json +1 -1
- package/registry/preinstalled.json +0 -0
- package/registry-indexer.mjs +0 -0
- package/registry-retriever.mjs +5 -0
- package/registry-scanner.mjs +0 -0
- package/registry.mjs +11 -0
- package/resource-discovery.mjs +0 -0
- package/schema.mjs +0 -0
- package/scripts/launch.mjs +0 -0
- package/server-internals.mjs +0 -0
- package/server.mjs +51 -8
- package/skill.md +0 -0
- package/tool-schemas.mjs +3 -1
- package/utils.mjs +8 -2
|
File without changes
|
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
|
package/commands/memory.md
CHANGED
|
File without changes
|
package/commands/tools.md
CHANGED
|
File without changes
|
package/commands/update.md
CHANGED
|
File without changes
|
package/dispatch-feedback.mjs
CHANGED
|
File without changes
|
package/dispatch-inject.mjs
CHANGED
|
File without changes
|
package/dispatch-patterns.mjs
CHANGED
|
File without changes
|
package/dispatch-workflow.mjs
CHANGED
|
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
|
|
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 {
|
|
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:
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
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
|
|
1088
|
+
export async function dispatchOnUserPrompt(db, userPrompt, sessionId) {
|
|
1088
1089
|
if (!userPrompt || !db) return null;
|
|
1089
1090
|
|
|
1090
1091
|
try {
|
|
1091
|
-
//
|
|
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
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1163
|
-
if (sessionId &&
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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:
|
|
1193
|
-
*
|
|
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
|
|
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 (!
|
|
1127
|
+
if (!event) return null;
|
|
1200
1128
|
|
|
1201
1129
|
try {
|
|
1202
|
-
//
|
|
1203
|
-
|
|
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
|
-
//
|
|
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
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1155
|
+
// ─── Error Recall ─────────────────────────────────────────────────────────────
|
|
1236
1156
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1243
|
-
const
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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, '
|
|
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
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
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 (
|
|
228
|
+
if (hasTestOrBuildError) return true;
|
|
225
229
|
|
|
226
|
-
//
|
|
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
|
-
//
|
|
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
|
package/hook-semaphore.mjs
CHANGED
|
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:
|
|
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
|
|
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
|
File without changes
|
package/registry-indexer.mjs
CHANGED
|
File without changes
|
package/registry-retriever.mjs
CHANGED
|
@@ -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
|
package/registry-scanner.mjs
CHANGED
|
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
|
package/resource-discovery.mjs
CHANGED
|
File without changes
|
package/schema.mjs
CHANGED
|
File without changes
|
package/scripts/launch.mjs
CHANGED
|
File without changes
|
package/server-internals.mjs
CHANGED
|
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 ??
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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);
|