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.
@@ -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 { shouldSkip, detectIntent, shouldSkipByDedup, extractFiles, DEDUP_STALE_MS } from './prompt-search-utils.mjs';
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
- if (shouldSkipByDedup(candidateIds, INJECTED_IDS_FILE)) return;
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: 'FTS5 full-text search across observations, sessions, and prompts with BM25 ranking. Returns compact index (use mem_get for details).',
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, ordered by time. Quick way to see latest activity without a search query.',
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 query to auto-find anchor, or specify anchor ID directly.',
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 records.',
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. FTS5 cleanup is automatic via triggers.',
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: 'Manually save a memory/observation. Use for important findings, decisions, or notes worth preserving.',
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 about stored memories: counts, types, projects, recent activity.',
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: search for skills/agents by need, list resources, view stats, import/remove tools, reindex FTS5.',
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 howToUse = r.type === 'skill'
1518
- ? (r.invocation_name ? `Skill tool: skill="${r.invocation_name}"` : `Community skill: ${r.name}`)
1519
- : `Agent tool: subagent_type="${r.invocation_name || r.name}"`;
1520
- return `${qualityBadge} ${r.type === 'skill' ? 'S' : 'A'} **${r.name}**${categoryLabel} — ${truncate(r.capability_summary || '', 80)}\n Use: ${howToUse}`;
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
- return { content: [{ type: 'text', text: `Unknown action: ${action}. Valid: search, list, stats, import, remove, reindex` }], isError: true };
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 for backup or migration.',
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 before editing a file to recall past bugfixes, decisions, and context.',
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. Shows observations organized by memory tier (working/active/archive).',
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
- const isErr = /error|fail|exception|panic/i.test(resp) && resp.length > 30;
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
  }