claude-mem-lite 2.26.1 → 2.28.1
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +22 -5
- package/README.zh-CN.md +47 -63
- package/cli.mjs +1 -1
- package/commands/mem.md +1 -1
- package/commands/memory.md +1 -1
- package/commands/recall.md +1 -1
- package/commands/recent.md +1 -1
- package/commands/search.md +1 -1
- package/commands/timeline.md +1 -1
- package/commands/tools.md +9 -20
- package/commands/update.md +1 -1
- package/hook-llm.mjs +31 -4
- package/hook-memory.mjs +9 -3
- package/hook.mjs +19 -1
- package/hooks/hooks.json +10 -0
- package/install.mjs +23 -3
- package/mem-cli.mjs +155 -23
- package/package.json +5 -1
- package/registry-enricher.mjs +101 -0
- package/registry-github.mjs +54 -0
- package/registry-importer.mjs +352 -0
- package/registry.mjs +36 -2
- package/scripts/pre-skill-bridge.js +78 -0
- package/scripts/prompt-search-utils.mjs +47 -2
- package/scripts/user-prompt-search.js +105 -5
- package/server.mjs +184 -23
- package/tool-schemas.mjs +9 -2
- package/utils.mjs +6 -2
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// Runs as UserPromptSubmit hook — injects relevant memories before Claude sees the prompt
|
|
4
4
|
// Lightweight: only imports schema.mjs and utils.mjs, no MCP SDK
|
|
5
5
|
|
|
6
|
-
import { ensureDb, DB_DIR } from '../schema.mjs';
|
|
6
|
+
import { ensureDb, DB_DIR, REGISTRY_DB_PATH } from '../schema.mjs';
|
|
7
7
|
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE } from '../utils.mjs';
|
|
8
|
-
import { writeFileSync, readFileSync } from 'fs';
|
|
8
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
-
import {
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import Database from 'better-sqlite3';
|
|
12
|
+
import { shouldSkip, detectIntent, shouldSkipByDedup, extractFiles, DEDUP_STALE_MS, matchRegistrySkillName } from './prompt-search-utils.mjs';
|
|
11
13
|
|
|
12
14
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
13
15
|
|
|
@@ -152,6 +154,88 @@ function formatResults(rows) {
|
|
|
152
154
|
return lines.join('\n');
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// ─── Registry Skill Auto-Load ──────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
const SKILL_COOLDOWN_FILE = join(DB_DIR, 'runtime', `.skill-cooldown-${inferProject()}`);
|
|
160
|
+
const SKILL_COOLDOWN_MS = 300_000; // 5 minutes
|
|
161
|
+
const SKILL_TOKEN_LIMIT = 16000; // ~4000 tokens
|
|
162
|
+
|
|
163
|
+
function loadManagedSkillNames() {
|
|
164
|
+
if (!existsSync(REGISTRY_DB_PATH)) return new Set();
|
|
165
|
+
try {
|
|
166
|
+
const rdb = new Database(REGISTRY_DB_PATH, { readonly: true });
|
|
167
|
+
rdb.pragma('busy_timeout = 500');
|
|
168
|
+
try {
|
|
169
|
+
const rows = rdb.prepare(`
|
|
170
|
+
SELECT name FROM resources
|
|
171
|
+
WHERE status = 'active' AND local_path LIKE '%/.claude-mem-lite/managed/%'
|
|
172
|
+
`).all();
|
|
173
|
+
return new Set(rows.map(r => r.name.toLowerCase()));
|
|
174
|
+
} finally { rdb.close(); }
|
|
175
|
+
} catch { return new Set(); }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function toPortablePath(absPath) {
|
|
179
|
+
const home = homedir();
|
|
180
|
+
return absPath.startsWith(home) ? '~' + absPath.slice(home.length) : absPath;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function loadSkillContent(skillName) {
|
|
184
|
+
if (!existsSync(REGISTRY_DB_PATH)) return null;
|
|
185
|
+
try {
|
|
186
|
+
const rdb = new Database(REGISTRY_DB_PATH, { readonly: true });
|
|
187
|
+
rdb.pragma('busy_timeout = 500');
|
|
188
|
+
try {
|
|
189
|
+
const row = rdb.prepare(`
|
|
190
|
+
SELECT name, type, local_path FROM resources
|
|
191
|
+
WHERE status = 'active'
|
|
192
|
+
AND (name = ? OR invocation_name = ?)
|
|
193
|
+
AND local_path LIKE '%/.claude-mem-lite/managed/%'
|
|
194
|
+
LIMIT 1
|
|
195
|
+
`).get(skillName, skillName);
|
|
196
|
+
|
|
197
|
+
if (!row || !row.local_path) return null;
|
|
198
|
+
|
|
199
|
+
let path = row.local_path;
|
|
200
|
+
if (!path.endsWith('.md')) {
|
|
201
|
+
// Only skills can be directory paths (9 cases); agents always have full .md paths
|
|
202
|
+
const candidate = join(path, 'SKILL.md');
|
|
203
|
+
if (existsSync(candidate)) path = candidate;
|
|
204
|
+
}
|
|
205
|
+
if (!existsSync(path)) return null;
|
|
206
|
+
|
|
207
|
+
const portablePath = toPortablePath(path);
|
|
208
|
+
const sourceLabel = row.type === 'agent' ? 'managed-agent' : 'managed-skill';
|
|
209
|
+
const content = readFileSync(path, 'utf8');
|
|
210
|
+
if (content.length > SKILL_TOKEN_LIMIT) {
|
|
211
|
+
return `<skill-auto-loaded name="${row.name}" source="${sourceLabel}" path="${portablePath}" truncated="true">\n${content.slice(0, 800)}\n...\n</skill-auto-loaded>\nSkill truncated. Full content: Read("${portablePath}") or mem_use(name="${row.name}")`;
|
|
212
|
+
}
|
|
213
|
+
return `<skill-auto-loaded name="${row.name}" source="${sourceLabel}" path="${portablePath}">\n${content}\n</skill-auto-loaded>\nFollow the instructions above. Reload: Read("${portablePath}")`;
|
|
214
|
+
} finally { rdb.close(); }
|
|
215
|
+
} catch { return null; }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getSkillCooldown() {
|
|
219
|
+
try {
|
|
220
|
+
const raw = readFileSync(SKILL_COOLDOWN_FILE, 'utf8');
|
|
221
|
+
const data = JSON.parse(raw);
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const cleaned = {};
|
|
224
|
+
for (const [k, v] of Object.entries(data)) {
|
|
225
|
+
if (now - v < SKILL_COOLDOWN_MS) cleaned[k] = v;
|
|
226
|
+
}
|
|
227
|
+
return cleaned;
|
|
228
|
+
} catch { return {}; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function setSkillCooldown(name) {
|
|
232
|
+
try {
|
|
233
|
+
const data = getSkillCooldown();
|
|
234
|
+
data[name] = Date.now();
|
|
235
|
+
writeFileSync(SKILL_COOLDOWN_FILE, JSON.stringify(data));
|
|
236
|
+
} catch { /* silent */ }
|
|
237
|
+
}
|
|
238
|
+
|
|
155
239
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
156
240
|
|
|
157
241
|
async function main() {
|
|
@@ -209,9 +293,9 @@ async function main() {
|
|
|
209
293
|
}
|
|
210
294
|
|
|
211
295
|
const candidateIds = rows.map(r => r.id);
|
|
212
|
-
|
|
296
|
+
const dedupSkip = shouldSkipByDedup(candidateIds, INJECTED_IDS_FILE);
|
|
213
297
|
|
|
214
|
-
const output = formatResults(rows);
|
|
298
|
+
const output = !dedupSkip ? formatResults(rows) : null;
|
|
215
299
|
if (output) {
|
|
216
300
|
process.stdout.write(output + '\n');
|
|
217
301
|
// Write injected IDs for dedup with hook.mjs handleUserPrompt + self-dedup
|
|
@@ -228,6 +312,22 @@ async function main() {
|
|
|
228
312
|
}));
|
|
229
313
|
} catch {}
|
|
230
314
|
}
|
|
315
|
+
|
|
316
|
+
// ─── L1: Registry skill auto-load ───────────────────────────────────
|
|
317
|
+
try {
|
|
318
|
+
const skillNames = loadManagedSkillNames();
|
|
319
|
+
const matched = matchRegistrySkillName(promptText, skillNames);
|
|
320
|
+
if (matched) {
|
|
321
|
+
const cooldown = getSkillCooldown();
|
|
322
|
+
if (!cooldown[matched]) {
|
|
323
|
+
const skillContent = loadSkillContent(matched);
|
|
324
|
+
if (skillContent) {
|
|
325
|
+
process.stdout.write('\n' + skillContent + '\n');
|
|
326
|
+
setSkillCooldown(matched);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch { /* silent — never block on registry failure */ }
|
|
231
331
|
} catch {
|
|
232
332
|
// Hooks must never break Claude Code — swallow all errors
|
|
233
333
|
} finally {
|
package/server.mjs
CHANGED
|
@@ -9,8 +9,9 @@ import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
|
9
9
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH, checkFTSIntegrity, rebuildFTS } from './schema.mjs';
|
|
10
10
|
import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts, autoBoostIfNeeded, runIdleCleanup } from './server-internals.mjs';
|
|
11
11
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
12
|
-
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema } from './tool-schemas.mjs';
|
|
13
|
-
import { basename } from 'path';
|
|
12
|
+
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema } from './tool-schemas.mjs';
|
|
13
|
+
import { basename, join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
14
15
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
15
16
|
import { searchResources } from './registry-retriever.mjs';
|
|
16
17
|
import { getVocabulary, rebuildVocabulary, _resetVocabCache, computeVector, vectorSearch, rrfMerge } from './tfidf.mjs';
|
|
@@ -21,7 +22,7 @@ const { version: PKG_VERSION } = require('./package.json');
|
|
|
21
22
|
|
|
22
23
|
// ─── Database ───────────────────────────────────────────────────────────────
|
|
23
24
|
|
|
24
|
-
import { rmSync } from 'fs';
|
|
25
|
+
import { rmSync, existsSync, readFileSync } from 'fs';
|
|
25
26
|
|
|
26
27
|
let db;
|
|
27
28
|
try {
|
|
@@ -115,6 +116,15 @@ const server = new McpServer(
|
|
|
115
116
|
' • Starting work on a module → recall past decisions: claude-mem-lite search "module-name" --type decision',
|
|
116
117
|
' • After solving a non-obvious problem → save the lesson: mem_save with lesson_learned',
|
|
117
118
|
' • When hook-injected context mentions a relevant ID → get details: claude-mem-lite get ID',
|
|
119
|
+
'',
|
|
120
|
+
'Decision rules (use INSTEAD OF multi-step search):',
|
|
121
|
+
' • "what happened recently?" → mem_recent (NOT search with empty query)',
|
|
122
|
+
' • "what do we know about file.mjs?" → mem_recall (NOT grep + manual search)',
|
|
123
|
+
' • "show me around observation #42" → mem_timeline (NOT mem_get + manual navigation)',
|
|
124
|
+
' • "clean up old/duplicate memories" → mem_maintain (NOT manual mem_delete loop)',
|
|
125
|
+
' • "is the search index healthy?" → mem_fts_check (NOT manual COUNT queries)',
|
|
126
|
+
' • "overview of memory tiers" → mem_browse (NOT mem_search + manual grouping)',
|
|
127
|
+
' • "export for backup" → mem_export (NOT manual SELECT queries)',
|
|
118
128
|
].join('\n'),
|
|
119
129
|
},
|
|
120
130
|
);
|
|
@@ -514,7 +524,7 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCros
|
|
|
514
524
|
server.registerTool(
|
|
515
525
|
'mem_search',
|
|
516
526
|
{
|
|
517
|
-
description: '
|
|
527
|
+
description: 'Search project memory for past bugfixes, decisions, and discoveries. Use when: encountering a familiar error, investigating a module before changes, or looking for prior art on a problem. Returns compact index (use mem_get for full details).',
|
|
518
528
|
inputSchema: memSearchSchema,
|
|
519
529
|
},
|
|
520
530
|
safeHandler(async (args) => {
|
|
@@ -650,7 +660,7 @@ server.registerTool(
|
|
|
650
660
|
server.registerTool(
|
|
651
661
|
'mem_recent',
|
|
652
662
|
{
|
|
653
|
-
description: 'Show most recent observations
|
|
663
|
+
description: 'Show most recent observations. Use when: checking what happened recently in the project, reviewing progress after being away, or verifying that a recent change was captured.',
|
|
654
664
|
inputSchema: memRecentSchema,
|
|
655
665
|
},
|
|
656
666
|
safeHandler(async (args) => {
|
|
@@ -689,7 +699,7 @@ server.registerTool(
|
|
|
689
699
|
server.registerTool(
|
|
690
700
|
'mem_timeline',
|
|
691
701
|
{
|
|
692
|
-
description: 'Browse observations as a timeline around an anchor point. Use
|
|
702
|
+
description: 'Browse observations as a timeline around an anchor point. Use when: exploring what happened before/after a specific observation, understanding the sequence of changes that led to a bug, or reviewing a session chronologically.',
|
|
693
703
|
inputSchema: memTimelineSchema,
|
|
694
704
|
},
|
|
695
705
|
safeHandler(async (args) => {
|
|
@@ -790,7 +800,7 @@ server.registerTool(
|
|
|
790
800
|
server.registerTool(
|
|
791
801
|
'mem_get',
|
|
792
802
|
{
|
|
793
|
-
description: 'Get full details for one or more records by ID. Use after mem_search to drill into specific
|
|
803
|
+
description: 'Get full details for one or more records by ID. Use when: hook-injected context mentions a relevant observation ID, or after mem_search to drill into specific results for narrative, lesson_learned, and file details.',
|
|
794
804
|
inputSchema: memGetSchema,
|
|
795
805
|
},
|
|
796
806
|
safeHandler(async (args) => {
|
|
@@ -848,7 +858,7 @@ server.registerTool(
|
|
|
848
858
|
server.registerTool(
|
|
849
859
|
'mem_delete',
|
|
850
860
|
{
|
|
851
|
-
description: 'Delete observations by ID. Use confirm=false to preview, confirm=true to execute.
|
|
861
|
+
description: 'Delete observations by ID. Use when: cleaning up incorrect or duplicate observations, removing test data, or when the user asks to forget something. Use confirm=false to preview, confirm=true to execute.',
|
|
852
862
|
inputSchema: memDeleteSchema,
|
|
853
863
|
},
|
|
854
864
|
safeHandler(async (args) => {
|
|
@@ -912,7 +922,7 @@ server.registerTool(
|
|
|
912
922
|
server.registerTool(
|
|
913
923
|
'mem_save',
|
|
914
924
|
{
|
|
915
|
-
description: '
|
|
925
|
+
description: 'Save a memory/observation. Use when: solving a non-obvious bug (save the lesson), making an architecture decision, discovering something not obvious from code alone, or when the user asks to remember something.',
|
|
916
926
|
inputSchema: memSaveSchema,
|
|
917
927
|
},
|
|
918
928
|
safeHandler(async (args) => {
|
|
@@ -994,7 +1004,7 @@ server.registerTool(
|
|
|
994
1004
|
server.registerTool(
|
|
995
1005
|
'mem_stats',
|
|
996
1006
|
{
|
|
997
|
-
description: 'Get statistics
|
|
1007
|
+
description: 'Get memory statistics: counts, types, projects, daily activity, data health. Use when: assessing memory system health, checking how much project history exists, or diagnosing search quality issues.',
|
|
998
1008
|
inputSchema: memStatsSchema,
|
|
999
1009
|
},
|
|
1000
1010
|
safeHandler(async (args) => {
|
|
@@ -1104,7 +1114,7 @@ server.registerTool(
|
|
|
1104
1114
|
server.registerTool(
|
|
1105
1115
|
'mem_compress',
|
|
1106
1116
|
{
|
|
1107
|
-
description: 'Compress old low-value observations into weekly summaries. Use preview=true to see candidates first.',
|
|
1117
|
+
description: 'Compress old low-value observations into weekly summaries. Use when: memory database is growing large, observations are months old, or after a major project phase completes. Use preview=true to see candidates first.',
|
|
1108
1118
|
inputSchema: memCompressSchema,
|
|
1109
1119
|
},
|
|
1110
1120
|
safeHandler(async (args) => {
|
|
@@ -1217,7 +1227,7 @@ server.registerTool(
|
|
|
1217
1227
|
server.registerTool(
|
|
1218
1228
|
'mem_maintain',
|
|
1219
1229
|
{
|
|
1220
|
-
description: 'Memory maintenance: scan for duplicates/stale/broken items, then execute cleanup/decay/boost/dedup operations.',
|
|
1230
|
+
description: 'Memory maintenance: scan for duplicates/stale/broken items, then execute cleanup/decay/boost/dedup operations. Use when: search results seem noisy with duplicates, after bulk imports, or during periodic maintenance.',
|
|
1221
1231
|
inputSchema: memMaintainSchema,
|
|
1222
1232
|
},
|
|
1223
1233
|
safeHandler(async (args) => {
|
|
@@ -1478,7 +1488,7 @@ server.registerTool(
|
|
|
1478
1488
|
server.registerTool(
|
|
1479
1489
|
'mem_registry',
|
|
1480
1490
|
{
|
|
1481
|
-
description: 'Manage tool resource registry:
|
|
1491
|
+
description: 'Manage tool resource registry. Use when: looking for a skill or agent to solve a problem, importing tools from a repository, checking what resources are available, or managing installed tools.',
|
|
1482
1492
|
inputSchema: memRegistrySchema,
|
|
1483
1493
|
},
|
|
1484
1494
|
safeHandler(async (args) => {
|
|
@@ -1511,13 +1521,30 @@ server.registerTool(
|
|
|
1511
1521
|
if (results.length === 0) {
|
|
1512
1522
|
return { content: [{ type: 'text', text: `No matching resources for: "${args.query}"` }] };
|
|
1513
1523
|
}
|
|
1524
|
+
const home = homedir();
|
|
1525
|
+
const toPortable = (p) => p && p.startsWith(home) ? '~' + p.slice(home.length) : (p || '');
|
|
1514
1526
|
const lines = results.map(r => {
|
|
1515
1527
|
const qualityBadge = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
|
|
1516
1528
|
const categoryLabel = r.category ? ` [${r.category}]` : '';
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1529
|
+
const isManaged = r.local_path && r.local_path.includes('/.claude-mem-lite/managed/');
|
|
1530
|
+
const portablePath = isManaged ? toPortable(r.local_path) : '';
|
|
1531
|
+
let howToUse;
|
|
1532
|
+
if (isManaged) {
|
|
1533
|
+
// Managed: use Read(path) or mem_use — Skill() won't work for managed resources
|
|
1534
|
+
// Agents always have complete .md paths (e.g., agents/group/agents/name.md)
|
|
1535
|
+
// Only skills can be directory paths (9 cases) — resolve to /SKILL.md
|
|
1536
|
+
const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
|
|
1537
|
+
howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
|
|
1538
|
+
} else if (r.invocation_name) {
|
|
1539
|
+
// Native plugin/user skill: Skill() with full invocation name
|
|
1540
|
+
howToUse = r.type === 'skill'
|
|
1541
|
+
? `Skill("${r.invocation_name}")`
|
|
1542
|
+
: `Agent(subagent_type="${r.invocation_name}")`;
|
|
1543
|
+
} else {
|
|
1544
|
+
howToUse = `mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
|
|
1545
|
+
}
|
|
1546
|
+
const pathLine = portablePath ? `\n Path: ${portablePath}` : '';
|
|
1547
|
+
return `${qualityBadge} ${r.type === 'skill' ? 'S' : 'A'} **${r.name}**${categoryLabel} — ${truncate(r.capability_summary || '', 80)}${pathLine}\n Use: ${howToUse}`;
|
|
1521
1548
|
});
|
|
1522
1549
|
return { content: [{ type: 'text', text: `Found ${results.length} resource(s) for "${args.query}":\n\n${lines.join('\n\n')}` }] };
|
|
1523
1550
|
}
|
|
@@ -1597,16 +1624,150 @@ server.registerTool(
|
|
|
1597
1624
|
return { content: [{ type: 'text', text: `FTS5 reindexed. ${count.c} active resources.` }] };
|
|
1598
1625
|
}
|
|
1599
1626
|
|
|
1600
|
-
|
|
1627
|
+
if (action === 'import_url') {
|
|
1628
|
+
if (!args.url) {
|
|
1629
|
+
return { content: [{ type: 'text', text: 'import_url requires a url parameter' }], isError: true };
|
|
1630
|
+
}
|
|
1631
|
+
const { importFromGitHub } = await import('./registry-importer.mjs');
|
|
1632
|
+
try {
|
|
1633
|
+
const results = await importFromGitHub(rdb, args.url);
|
|
1634
|
+
if (results.length === 0) {
|
|
1635
|
+
return { content: [{ type: 'text', text: `No skills/agents found in: ${args.url}` }] };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
let enrichMsg = '';
|
|
1639
|
+
if (args.enrich) {
|
|
1640
|
+
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1641
|
+
let ok = 0;
|
|
1642
|
+
for (const r of results) {
|
|
1643
|
+
const row = rdb.prepare('SELECT local_path FROM resources WHERE id = ?').get(r.id);
|
|
1644
|
+
if (!row?.local_path) continue;
|
|
1645
|
+
try {
|
|
1646
|
+
const content = readFileSync(row.local_path, 'utf8');
|
|
1647
|
+
if (await enrichResource(rdb, r.name, r.type, content)) ok++;
|
|
1648
|
+
} catch {}
|
|
1649
|
+
}
|
|
1650
|
+
enrichMsg = `\nEnriched: ${ok}/${results.length}`;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const lines = results.map(r => `${r.type === 'skill' ? 'S' : 'A'} ${r.name} (id=${r.id})`);
|
|
1654
|
+
return { content: [{ type: 'text', text: `Imported ${results.length} resource(s) from ${args.url}:\n${lines.join('\n')}${enrichMsg}` }] };
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
return { content: [{ type: 'text', text: `Import failed: ${e.message}` }], isError: true };
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (action === 'enrich') {
|
|
1661
|
+
if (!args.name) {
|
|
1662
|
+
return { content: [{ type: 'text', text: 'enrich requires a name parameter' }], isError: true };
|
|
1663
|
+
}
|
|
1664
|
+
const row = rdb.prepare("SELECT name, type, local_path FROM resources WHERE name = ? AND status = 'active'").get(args.name);
|
|
1665
|
+
if (!row) {
|
|
1666
|
+
return { content: [{ type: 'text', text: `Resource not found: ${args.name}` }], isError: true };
|
|
1667
|
+
}
|
|
1668
|
+
if (!row.local_path) {
|
|
1669
|
+
return { content: [{ type: 'text', text: `No local_path for ${args.name}` }], isError: true };
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1673
|
+
try {
|
|
1674
|
+
const content = readFileSync(row.local_path, 'utf8');
|
|
1675
|
+
const ok = await enrichResource(rdb, row.name, row.type, content);
|
|
1676
|
+
return { content: [{ type: 'text', text: ok ? `Enriched: ${args.name}` : `Enrichment failed for ${args.name}` }] };
|
|
1677
|
+
} catch (e) {
|
|
1678
|
+
return { content: [{ type: 'text', text: `Enrich error: ${e.message}` }], isError: true };
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return { content: [{ type: 'text', text: `Unknown action: ${action}. Valid: search, list, stats, import, remove, reindex, import_url, enrich` }], isError: true };
|
|
1601
1683
|
})
|
|
1602
1684
|
);
|
|
1603
1685
|
|
|
1686
|
+
// ─── Tool: mem_use ──────────────────────────────────────────────────────────
|
|
1687
|
+
|
|
1688
|
+
server.registerTool(
|
|
1689
|
+
'mem_use',
|
|
1690
|
+
{
|
|
1691
|
+
description: 'Load and activate a skill or agent from the managed registry. '
|
|
1692
|
+
+ 'Returns the full skill/agent content for execution. '
|
|
1693
|
+
+ 'Use when: you found a skill via mem_registry search and want to use it, '
|
|
1694
|
+
+ 'or you know a managed skill/agent name and want to load its instructions.',
|
|
1695
|
+
inputSchema: memUseSchema,
|
|
1696
|
+
},
|
|
1697
|
+
safeHandler(async (args) => {
|
|
1698
|
+
const rdb = getRegistryDb();
|
|
1699
|
+
if (!rdb) {
|
|
1700
|
+
return { content: [{ type: 'text', text: 'Registry DB not available.' }], isError: true };
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
const name = args.name.trim();
|
|
1704
|
+
const type = args.type || 'skill';
|
|
1705
|
+
|
|
1706
|
+
// 1. Exact match by name or invocation_name
|
|
1707
|
+
let row = rdb.prepare(`
|
|
1708
|
+
SELECT id, name, type, local_path, invocation_name, capability_summary
|
|
1709
|
+
FROM resources
|
|
1710
|
+
WHERE status = 'active' AND type = ?
|
|
1711
|
+
AND (name = ? OR invocation_name = ?)
|
|
1712
|
+
LIMIT 1
|
|
1713
|
+
`).get(type, name, name);
|
|
1714
|
+
|
|
1715
|
+
// 2. Fuzzy fallback: FTS5 search, take top result
|
|
1716
|
+
if (!row) {
|
|
1717
|
+
const results = searchResources(rdb, name, { type, limit: 1 });
|
|
1718
|
+
if (results.length > 0) {
|
|
1719
|
+
row = rdb.prepare(`SELECT id, name, type, local_path, invocation_name, capability_summary FROM resources WHERE name = ? AND type = ? AND status = 'active'`).get(results[0].name, results[0].type);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if (!row) {
|
|
1724
|
+
return { content: [{ type: 'text', text: `No ${type} found for "${name}". Try mem_registry(action="search", query="${name}") to browse.` }] };
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// 3. Resolve path: directory skills → SKILL.md (agents always have full .md paths)
|
|
1728
|
+
let skillPath = row.local_path || '';
|
|
1729
|
+
if (skillPath && !skillPath.endsWith('.md')) {
|
|
1730
|
+
for (const candidate of [
|
|
1731
|
+
join(skillPath, 'SKILL.md'),
|
|
1732
|
+
join(skillPath, `skills/${row.name}/SKILL.md`),
|
|
1733
|
+
]) {
|
|
1734
|
+
if (existsSync(candidate)) { skillPath = candidate; break; }
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// 4. Read content
|
|
1739
|
+
let content;
|
|
1740
|
+
try {
|
|
1741
|
+
content = readFileSync(skillPath, 'utf8');
|
|
1742
|
+
} catch {
|
|
1743
|
+
const msg = skillPath.endsWith('.md')
|
|
1744
|
+
? `Found ${type} "${row.name}" but cannot read file: ${skillPath}`
|
|
1745
|
+
: `Found ${type} "${row.name}" but no .md file in: ${skillPath}`;
|
|
1746
|
+
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// 5. Record invocation
|
|
1750
|
+
try {
|
|
1751
|
+
rdb.prepare(`
|
|
1752
|
+
INSERT INTO invocations (resource_id, session_id, trigger, adopted, outcome)
|
|
1753
|
+
VALUES (?, ?, 'user_explicit', 1, 'success')
|
|
1754
|
+
`).run(row.id, process.env.CLAUDE_SESSION_ID || 'unknown');
|
|
1755
|
+
} catch { /* non-critical */ }
|
|
1756
|
+
|
|
1757
|
+
const _home = homedir();
|
|
1758
|
+
const portablePath = skillPath && skillPath.startsWith(_home) ? '~' + skillPath.slice(_home.length) : (skillPath || '');
|
|
1759
|
+
const pathAttr = portablePath ? ` path="${portablePath}"` : '';
|
|
1760
|
+
const reloadHint = portablePath ? ` Reload: Read("${portablePath}")` : '';
|
|
1761
|
+
return { content: [{ type: 'text', text: `<skill-loaded name="${row.name}" type="${row.type}"${pathAttr}>\n${content}\n</skill-loaded>\n\nFollow the instructions above to execute this ${row.type}.${reloadHint}` }] };
|
|
1762
|
+
}),
|
|
1763
|
+
);
|
|
1764
|
+
|
|
1604
1765
|
// ─── Tool: mem_update ────────────────────────────────────────────────────────
|
|
1605
1766
|
|
|
1606
1767
|
server.registerTool(
|
|
1607
1768
|
'mem_update',
|
|
1608
1769
|
{
|
|
1609
|
-
description: 'Update an existing observation in-place. Preserves original ID and references.',
|
|
1770
|
+
description: 'Update an existing observation in-place. Use when: an observation needs correction, additional context was discovered later, or the user asks to update a specific memory. Preserves original ID and references.',
|
|
1610
1771
|
inputSchema: memUpdateSchema,
|
|
1611
1772
|
},
|
|
1612
1773
|
safeHandler(async (args) => {
|
|
@@ -1658,7 +1819,7 @@ server.registerTool(
|
|
|
1658
1819
|
server.registerTool(
|
|
1659
1820
|
'mem_export',
|
|
1660
1821
|
{
|
|
1661
|
-
description: 'Export observations as JSON or JSONL
|
|
1822
|
+
description: 'Export observations as JSON or JSONL. Use when: backing up memory before migration, sharing observations between machines, or creating a snapshot before major changes.',
|
|
1662
1823
|
inputSchema: memExportSchema,
|
|
1663
1824
|
},
|
|
1664
1825
|
safeHandler(async (args) => {
|
|
@@ -1698,7 +1859,7 @@ server.registerTool(
|
|
|
1698
1859
|
server.registerTool(
|
|
1699
1860
|
'mem_recall',
|
|
1700
1861
|
{
|
|
1701
|
-
description: 'Recall observations related to a file. Use
|
|
1862
|
+
description: 'Recall observations related to a file. Use when: about to edit a file, investigating a file with past issues, or before refactoring to recall past bugfixes, decisions, and context.',
|
|
1702
1863
|
inputSchema: memRecallSchema,
|
|
1703
1864
|
},
|
|
1704
1865
|
safeHandler(async (args) => {
|
|
@@ -1741,7 +1902,7 @@ server.registerTool(
|
|
|
1741
1902
|
server.registerTool(
|
|
1742
1903
|
'mem_fts_check',
|
|
1743
1904
|
{
|
|
1744
|
-
description: 'Check FTS5 index integrity or rebuild indexes. Use when search results seem wrong or after database recovery.',
|
|
1905
|
+
description: 'Check FTS5 index integrity or rebuild indexes. Use when: search results seem wrong or missing, after database recovery, or after manual DB edits.',
|
|
1745
1906
|
inputSchema: memFtsCheckSchema,
|
|
1746
1907
|
},
|
|
1747
1908
|
safeHandler(async (args) => {
|
|
@@ -1767,7 +1928,7 @@ server.registerTool(
|
|
|
1767
1928
|
server.registerTool(
|
|
1768
1929
|
'mem_browse',
|
|
1769
1930
|
{
|
|
1770
|
-
description: 'Tier-grouped memory dashboard.
|
|
1931
|
+
description: 'Tier-grouped memory dashboard. Use when: getting an overview of memory health, seeing how observations are distributed across tiers, or assessing what to compress or clean up.',
|
|
1771
1932
|
inputSchema: memBrowseSchema,
|
|
1772
1933
|
},
|
|
1773
1934
|
safeHandler(async (args) => {
|
package/tool-schemas.mjs
CHANGED
|
@@ -130,12 +130,12 @@ export const memFtsCheckSchema = {
|
|
|
130
130
|
};
|
|
131
131
|
|
|
132
132
|
export const memRegistrySchema = {
|
|
133
|
-
action: z.enum(['list', 'stats', 'search', 'import', 'remove', 'reindex']).describe('Registry operation'),
|
|
133
|
+
action: z.enum(['list', 'stats', 'search', 'import', 'remove', 'reindex', 'import_url', 'enrich']).describe('Registry operation'),
|
|
134
134
|
query: z.string().optional().describe('Search query — keywords describing what you need (for search)'),
|
|
135
135
|
type: z.enum(['skill', 'agent']).optional().describe('Filter by resource type (for list/search)'),
|
|
136
136
|
name: z.string().optional().describe('Resource name (for import/remove)'),
|
|
137
137
|
resource_type: z.enum(['skill', 'agent']).optional().describe('Resource type (for import/remove)'),
|
|
138
|
-
source: z.enum(['preinstalled', 'user']).optional().describe('Source (for import, default: user)'),
|
|
138
|
+
source: z.enum(['preinstalled', 'user', 'github']).optional().describe('Source (for import, default: user)'),
|
|
139
139
|
repo_url: z.string().optional().describe('GitHub repository URL (for import)'),
|
|
140
140
|
local_path: z.string().optional().describe('Local file path (for import)'),
|
|
141
141
|
invocation_name: z.string().optional().describe('Invocation name like "plugin:skill" (for import)'),
|
|
@@ -146,10 +146,17 @@ export const memRegistrySchema = {
|
|
|
146
146
|
keywords: z.string().optional().describe('Search keywords (for import)'),
|
|
147
147
|
tech_stack: z.string().optional().describe('Technology stack tags (for import)'),
|
|
148
148
|
use_cases: z.string().optional().describe('Usage scenarios (for import)'),
|
|
149
|
+
url: z.string().optional().describe('GitHub repository URL (for import_url action)'),
|
|
150
|
+
enrich: coerceBool.optional().describe('Auto-enrich imported resources (for import_url action)'),
|
|
149
151
|
category: z.string().optional().describe("Filter by category (e.g., 'testing', 'code-quality', 'debugging')"),
|
|
150
152
|
quality: z.enum(['installed', 'verified', 'community']).optional().describe('Filter by quality tier (default: all)'),
|
|
151
153
|
};
|
|
152
154
|
|
|
155
|
+
export const memUseSchema = {
|
|
156
|
+
name: z.string().min(1).describe('Skill or agent name to load (exact name or search query)'),
|
|
157
|
+
type: z.enum(['skill', 'agent']).optional().describe('Resource type (default: skill)'),
|
|
158
|
+
};
|
|
159
|
+
|
|
153
160
|
export const memBrowseSchema = {
|
|
154
161
|
project: z.string().optional().describe('Filter by project (default: inferred from CWD)'),
|
|
155
162
|
tier: z.enum(['working', 'active', 'archive']).optional().describe('Show only this tier'),
|
package/utils.mjs
CHANGED
|
@@ -157,9 +157,11 @@ export function isRelatedToEpisode(episode, newFiles) {
|
|
|
157
157
|
* @param {string} toolName Name of the tool (Edit, Write, Bash, etc.)
|
|
158
158
|
* @param {object} input Tool input parameters
|
|
159
159
|
* @param {string} resp Tool response text
|
|
160
|
+
* @param {object} [opts] Optional signals from detectBashSignificance
|
|
161
|
+
* @param {boolean} [opts.isError] If provided, overrides inline error regex detection
|
|
160
162
|
* @returns {string} Concise description of the action
|
|
161
163
|
*/
|
|
162
|
-
export function makeEntryDesc(toolName, input, resp) {
|
|
164
|
+
export function makeEntryDesc(toolName, input, resp, opts) {
|
|
163
165
|
switch (toolName) {
|
|
164
166
|
case 'Edit':
|
|
165
167
|
return `${basename(input.file_path || '')}: "${truncate(input.old_string || '', 40)}" → "${truncate(input.new_string || '', 40)}"`;
|
|
@@ -169,7 +171,9 @@ export function makeEntryDesc(toolName, input, resp) {
|
|
|
169
171
|
return `Notebook cell: ${truncate(input.new_source || '', 60)}`;
|
|
170
172
|
case 'Bash': {
|
|
171
173
|
const cmd = truncate(input.command || '', 50);
|
|
172
|
-
|
|
174
|
+
// Use caller-provided bashSig.isError (word-boundary aware) when available;
|
|
175
|
+
// fall back to inline regex only for standalone callers (tests, etc.)
|
|
176
|
+
const isErr = opts?.isError ?? (/\berror\b|\bfail(ed|ure)?\b|\bexception\b|\bpanic\b/i.test(resp) && resp.length > 30);
|
|
173
177
|
const snippet = truncate(resp, 60);
|
|
174
178
|
return isErr ? `${cmd} → ERROR: ${snippet}` : `${cmd} → ${snippet}`;
|
|
175
179
|
}
|