claude-mem-lite 2.26.1 → 2.28.2
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 +14 -2
- 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-episode.mjs +1 -1
- package/hook-llm.mjs +31 -4
- package/hook-memory.mjs +9 -3
- package/hook-semaphore.mjs +5 -4
- package/hook-update.mjs +1 -1
- package/hook.mjs +20 -2
- package/hooks/hooks.json +10 -0
- package/install.mjs +30 -13
- package/mem-cli.mjs +171 -30
- package/package.json +5 -1
- package/registry-enricher.mjs +101 -0
- package/registry-github.mjs +54 -0
- package/registry-importer.mjs +358 -0
- package/registry.mjs +39 -2
- package/schema.mjs +5 -5
- package/scoring-sql.mjs +3 -3
- package/scripts/post-tool-use.sh +1 -1
- package/scripts/pre-skill-bridge.js +83 -0
- package/scripts/prompt-search-utils.mjs +47 -2
- package/scripts/user-prompt-search.js +110 -6
- package/server.mjs +207 -31
- package/tfidf.mjs +2 -2
- package/tool-schemas.mjs +9 -2
- package/utils.mjs +22 -3
package/install.mjs
CHANGED
|
@@ -452,7 +452,7 @@ async function install() {
|
|
|
452
452
|
]
|
|
453
453
|
};
|
|
454
454
|
|
|
455
|
-
const
|
|
455
|
+
const memPreToolRecall = {
|
|
456
456
|
matcher: 'Edit|Write|NotebookEdit',
|
|
457
457
|
hooks: [
|
|
458
458
|
{
|
|
@@ -463,10 +463,30 @@ async function install() {
|
|
|
463
463
|
]
|
|
464
464
|
};
|
|
465
465
|
|
|
466
|
+
const memPreSkillBridge = {
|
|
467
|
+
matcher: 'Skill',
|
|
468
|
+
hooks: [
|
|
469
|
+
{
|
|
470
|
+
type: 'command',
|
|
471
|
+
command: `node "${join(SCRIPTS_PATH, 'pre-skill-bridge.js')}"`,
|
|
472
|
+
timeout: 3
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
};
|
|
476
|
+
|
|
466
477
|
// Filter out existing mem hooks, then append fresh ones
|
|
467
|
-
|
|
478
|
+
// PreToolUse has two separate matchers, so we register both
|
|
479
|
+
const hookConfigs = {
|
|
480
|
+
PreToolUse: [memPreToolRecall, memPreSkillBridge],
|
|
481
|
+
PostToolUse: [memPostToolUse],
|
|
482
|
+
SessionStart: [memSessionStart],
|
|
483
|
+
Stop: [memStop],
|
|
484
|
+
UserPromptSubmit: [memUserPrompt],
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
for (const [event, configs] of Object.entries(hookConfigs)) {
|
|
468
488
|
const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event].filter(cfg => !isMemHook(cfg)) : [];
|
|
469
|
-
settings.hooks[event] = [...existing,
|
|
489
|
+
settings.hooks[event] = [...existing, ...configs];
|
|
470
490
|
}
|
|
471
491
|
|
|
472
492
|
writeSettings(settings);
|
|
@@ -509,8 +529,9 @@ async function install() {
|
|
|
509
529
|
}
|
|
510
530
|
|
|
511
531
|
// 6. Install pre-installed resources (skills + agents)
|
|
512
|
-
|
|
513
|
-
|
|
532
|
+
if (process.env.CLAUDE_MEM_SKIP_REPOS) {
|
|
533
|
+
ok('Skill/agent registry: skipped (CLAUDE_MEM_SKIP_REPOS)');
|
|
534
|
+
} else try {
|
|
514
535
|
const manifestPath = join(INSTALL_DIR, 'registry', 'preinstalled.json');
|
|
515
536
|
if (!existsSync(manifestPath)) {
|
|
516
537
|
// For git-clone mode, check PROJECT_DIR
|
|
@@ -545,7 +566,7 @@ async function install() {
|
|
|
545
566
|
};
|
|
546
567
|
|
|
547
568
|
for (const [repoUrl, entries] of repos) {
|
|
548
|
-
const repoName = repoUrl.split('/').slice(-2).join('-');
|
|
569
|
+
const repoName = repoUrl.split('/').slice(-2).join('-').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
549
570
|
const clonePath = join(managedDir, 'repos', repoName);
|
|
550
571
|
let repoReady = false;
|
|
551
572
|
|
|
@@ -939,12 +960,8 @@ async function status() {
|
|
|
939
960
|
|
|
940
961
|
// CLI
|
|
941
962
|
try {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
ok('CLI: claude-mem-lite command available');
|
|
945
|
-
} else {
|
|
946
|
-
warn('CLI: command not on PATH');
|
|
947
|
-
}
|
|
963
|
+
execFileSync('claude-mem-lite', ['--help'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
964
|
+
ok('CLI: claude-mem-lite command available');
|
|
948
965
|
} catch {
|
|
949
966
|
warn('CLI: command not on PATH — run install again to create symlink');
|
|
950
967
|
}
|
|
@@ -1067,7 +1084,7 @@ async function doctor() {
|
|
|
1067
1084
|
|
|
1068
1085
|
// Check for stale processes
|
|
1069
1086
|
try {
|
|
1070
|
-
const procs =
|
|
1087
|
+
const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
|
|
1071
1088
|
// Filter out the pgrep process itself (matches its own pattern)
|
|
1072
1089
|
const real = procs.split('\n').filter(l => !l.includes('pgrep'));
|
|
1073
1090
|
if (real.length > 0) {
|
package/mem-cli.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// claude-mem-lite CLI — lightweight command layer for direct memory access
|
|
3
3
|
// No MCP SDK or heavy deps — only imports schema.mjs and utils.mjs
|
|
4
4
|
|
|
5
|
+
import { homedir } from 'os';
|
|
5
6
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH, checkFTSIntegrity, rebuildFTS } from './schema.mjs';
|
|
6
7
|
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, jaccardSimilarity, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, isoWeekKey, COMPRESSED_PENDING_PURGE, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, DEFAULT_DECAY_HALF_LIFE_MS, getCurrentBranch } from './utils.mjs';
|
|
7
8
|
import { resolveProject } from './project-utils.mjs';
|
|
@@ -51,7 +52,7 @@ function out(text) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function fail(text) {
|
|
54
|
-
process.
|
|
55
|
+
process.stderr.write(text + '\n');
|
|
55
56
|
process.exitCode = 1;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -114,13 +115,15 @@ function cmdSearch(db, args) {
|
|
|
114
115
|
fail(`[mem] Invalid --sort "${sort}". Use: relevance, time, importance`);
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
118
|
+
const useOr = flags.or === true || flags.or === 'true';
|
|
117
119
|
|
|
118
120
|
if (source && !['observations', 'sessions', 'prompts'].includes(source)) {
|
|
119
121
|
fail(`[mem] Invalid --source "${source}". Use: observations, sessions, prompts`);
|
|
120
122
|
return;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
let ftsQuery = sanitizeFtsQuery(query);
|
|
126
|
+
if (ftsQuery && useOr) ftsQuery = relaxFtsQueryToOr(ftsQuery) || ftsQuery;
|
|
124
127
|
if (!ftsQuery) {
|
|
125
128
|
fail(`[mem] No valid search terms in "${query}"`);
|
|
126
129
|
return;
|
|
@@ -164,20 +167,6 @@ function cmdSearch(db, args) {
|
|
|
164
167
|
LIMIT ?
|
|
165
168
|
`).all(...typeParams);
|
|
166
169
|
}
|
|
167
|
-
// Tier post-filter
|
|
168
|
-
if (tier && obsRows.length > 0) {
|
|
169
|
-
const rowIds = obsRows.map(r => r.id);
|
|
170
|
-
const ph = rowIds.map(() => '?').join(',');
|
|
171
|
-
const fullRows = db.prepare(
|
|
172
|
-
`SELECT id, compressed_into, superseded_at, memory_session_id, project, importance, last_accessed_at, created_at_epoch, type FROM observations WHERE id IN (${ph})`
|
|
173
|
-
).all(...rowIds);
|
|
174
|
-
const rowMap = new Map(fullRows.map(r => [r.id, r]));
|
|
175
|
-
const tierCtx = { now: Date.now(), currentProject: project || inferProject(), currentSessionId: '' };
|
|
176
|
-
obsRows = obsRows.filter(r => {
|
|
177
|
-
const full = rowMap.get(r.id);
|
|
178
|
-
return full && computeTier(full, tierCtx) === tier;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
170
|
for (const r of obsRows) results.push({ ...r, _source: 'obs', score: r.score ?? 0 });
|
|
182
171
|
|
|
183
172
|
// Concept co-occurrence + PRF expansion (aligned with MCP searchObservations)
|
|
@@ -222,6 +211,27 @@ function cmdSearch(db, args) {
|
|
|
222
211
|
}
|
|
223
212
|
}
|
|
224
213
|
}
|
|
214
|
+
|
|
215
|
+
// Tier post-filter — applied to ALL obs results (initial + expansion + PRF)
|
|
216
|
+
if (tier) {
|
|
217
|
+
const obsInResults = results.filter(r => r._source === 'obs');
|
|
218
|
+
if (obsInResults.length > 0) {
|
|
219
|
+
const obsIds = obsInResults.map(r => r.id);
|
|
220
|
+
const ph = obsIds.map(() => '?').join(',');
|
|
221
|
+
const fullRows = db.prepare(
|
|
222
|
+
`SELECT id, compressed_into, superseded_at, memory_session_id, project, importance, last_accessed_at, created_at_epoch, type FROM observations WHERE id IN (${ph})`
|
|
223
|
+
).all(...obsIds);
|
|
224
|
+
const rowMap = new Map(fullRows.map(r => [r.id, r]));
|
|
225
|
+
const tierCtx = { now: Date.now(), currentProject: project || inferProject(), currentSessionId: '' };
|
|
226
|
+
const allowedIds = new Set();
|
|
227
|
+
for (const [id, full] of rowMap) {
|
|
228
|
+
if (computeTier(full, tierCtx) === tier) allowedIds.add(id);
|
|
229
|
+
}
|
|
230
|
+
for (let i = results.length - 1; i >= 0; i--) {
|
|
231
|
+
if (results[i]._source === 'obs' && !allowedIds.has(results[i].id)) results.splice(i, 1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
// Search sessions (aligned with MCP mem_search)
|
|
@@ -441,7 +451,7 @@ function searchFts(db, ftsQuery, { type, project, limit, dateFrom, dateTo, minIm
|
|
|
441
451
|
function cmdRecent(db, args) {
|
|
442
452
|
const { positional, flags } = parseArgs(args);
|
|
443
453
|
const rawLimit = parseInt(positional[0], 10);
|
|
444
|
-
const limit =
|
|
454
|
+
const limit = (Number.isInteger(rawLimit) && rawLimit > 0) ? rawLimit : 10;
|
|
445
455
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
446
456
|
|
|
447
457
|
const params = [];
|
|
@@ -503,7 +513,9 @@ function cmdRecall(db, args) {
|
|
|
503
513
|
// Update access_count for recalled observations (aligned with MCP mem_recall)
|
|
504
514
|
const recalledIds = rows.map(r => r.id);
|
|
505
515
|
const recallPh = recalledIds.map(() => '?').join(',');
|
|
506
|
-
|
|
516
|
+
try {
|
|
517
|
+
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${recallPh})`).run(Date.now(), ...recalledIds);
|
|
518
|
+
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
507
519
|
|
|
508
520
|
out(`[mem] History for ${filename} (${rows.length}):`);
|
|
509
521
|
for (const r of rows) {
|
|
@@ -580,8 +592,10 @@ function cmdGet(db, args) {
|
|
|
580
592
|
}
|
|
581
593
|
|
|
582
594
|
// Update access_count + auto-boost (aligned with MCP mem_get)
|
|
583
|
-
|
|
584
|
-
|
|
595
|
+
try {
|
|
596
|
+
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
|
|
597
|
+
autoBoostIfNeeded(db, ids);
|
|
598
|
+
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
585
599
|
|
|
586
600
|
const rows = db.prepare(`
|
|
587
601
|
SELECT * FROM observations
|
|
@@ -672,7 +686,9 @@ function cmdTimeline(db, args) {
|
|
|
672
686
|
}
|
|
673
687
|
|
|
674
688
|
// Update access_count for anchor (aligned with MCP mem_timeline)
|
|
675
|
-
|
|
689
|
+
try {
|
|
690
|
+
db.prepare('UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id = ?').run(Date.now(), anchorId);
|
|
691
|
+
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
676
692
|
|
|
677
693
|
// Get anchor epoch
|
|
678
694
|
const anchorRow = db.prepare('SELECT created_at_epoch, project FROM observations WHERE id = ?').get(anchorId);
|
|
@@ -1650,30 +1666,48 @@ function cmdRegistry(_memDb, args) {
|
|
|
1650
1666
|
results = results.slice(0, 5);
|
|
1651
1667
|
if (results.length === 0) { out(`[mem] No matching resources for: "${query}"`); return; }
|
|
1652
1668
|
out(`[mem] ${results.length} resource(s) for "${query}":`);
|
|
1669
|
+
const home = homedir();
|
|
1653
1670
|
for (const r of results) {
|
|
1654
1671
|
const badge = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
|
|
1655
1672
|
const categoryLabel = r.category ? ` [${r.category}]` : '';
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1673
|
+
const isManaged = r.local_path && r.local_path.includes('/.claude-mem-lite/managed/');
|
|
1674
|
+
const portablePath = isManaged && r.local_path.startsWith(home) ? '~' + r.local_path.slice(home.length) : (r.local_path || '');
|
|
1675
|
+
let howToUse;
|
|
1676
|
+
if (isManaged) {
|
|
1677
|
+
const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
|
|
1678
|
+
howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
|
|
1679
|
+
} else if (r.invocation_name) {
|
|
1680
|
+
howToUse = r.type === 'skill'
|
|
1681
|
+
? `Skill("${r.invocation_name}")`
|
|
1682
|
+
: `Agent(subagent_type="${r.invocation_name}")`;
|
|
1683
|
+
} else {
|
|
1684
|
+
howToUse = `mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
|
|
1685
|
+
}
|
|
1686
|
+
const pathLine = portablePath ? `\n Path: ${portablePath}` : '';
|
|
1687
|
+
out(` ${badge} ${r.type === 'skill' ? 'S' : 'A'} ${r.name}${categoryLabel} — ${truncate(r.capability_summary || '', 80)}${pathLine}\n Use: ${howToUse}`);
|
|
1660
1688
|
}
|
|
1661
1689
|
return;
|
|
1662
1690
|
}
|
|
1663
1691
|
|
|
1664
1692
|
if (action === 'list') {
|
|
1665
1693
|
const typeFilter = flags.type;
|
|
1694
|
+
const rawLimit = parseInt(flags.limit, 10);
|
|
1695
|
+
const listLimit = Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : 20;
|
|
1666
1696
|
const where = typeFilter ? 'WHERE type = ? AND status = ?' : 'WHERE status = ?';
|
|
1667
1697
|
const params = typeFilter ? [typeFilter, 'active'] : ['active'];
|
|
1668
|
-
const
|
|
1698
|
+
const allResources = rdb.prepare(`
|
|
1669
1699
|
SELECT name, type, invocation_name, recommend_count, adopt_count, capability_summary
|
|
1670
|
-
FROM resources ${where} ORDER BY type, name
|
|
1700
|
+
FROM resources ${where} ORDER BY adopt_count DESC, recommend_count DESC, type, name
|
|
1671
1701
|
`).all(...params);
|
|
1672
|
-
if (
|
|
1673
|
-
|
|
1702
|
+
if (allResources.length === 0) { out('[mem] No resources found.'); return; }
|
|
1703
|
+
const resources = allResources.slice(0, listLimit);
|
|
1704
|
+
out(`[mem] Resources (showing ${resources.length} of ${allResources.length}):`);
|
|
1674
1705
|
for (const r of resources) {
|
|
1675
1706
|
out(` ${r.type === 'skill' ? 'S' : 'A'} ${r.name}${r.invocation_name ? ` (${r.invocation_name})` : ''} — rec:${r.recommend_count} adopt:${r.adopt_count} — ${truncate(r.capability_summary || '', 50)}`);
|
|
1676
1707
|
}
|
|
1708
|
+
if (allResources.length > listLimit) {
|
|
1709
|
+
out(`[mem] Use --limit N to see more, or "registry search <query>" to find specific resources.`);
|
|
1710
|
+
}
|
|
1677
1711
|
return;
|
|
1678
1712
|
}
|
|
1679
1713
|
|
|
@@ -1760,6 +1794,7 @@ Commands:
|
|
|
1760
1794
|
--offset N Skip first N results (pagination)
|
|
1761
1795
|
--tier T Filter by tier (working|active|archive, observations only)
|
|
1762
1796
|
--sort S Sort: relevance (default), time, importance
|
|
1797
|
+
--or Use OR instead of AND between search terms
|
|
1763
1798
|
|
|
1764
1799
|
recent [N] Show N most recent observations (default 10)
|
|
1765
1800
|
--project P Filter by project
|
|
@@ -1831,7 +1866,7 @@ Commands:
|
|
|
1831
1866
|
--limit N Max entries per tier (default 5)
|
|
1832
1867
|
|
|
1833
1868
|
registry <action> Manage tool resource registry
|
|
1834
|
-
list List
|
|
1869
|
+
list List resources [--type skill|agent] [--limit N] (default 20)
|
|
1835
1870
|
stats Registry statistics
|
|
1836
1871
|
search <query> Search resources [--type skill|agent] [--category C] [--quality Q]
|
|
1837
1872
|
import Import resource --name N --resource-type T [--repo-url U] [--local-path P] [--use-cases U]
|
|
@@ -1841,6 +1876,110 @@ Commands:
|
|
|
1841
1876
|
DB: ${DB_PATH}`);
|
|
1842
1877
|
}
|
|
1843
1878
|
|
|
1879
|
+
// ─── Import (GitHub) ────────────────────────────────────────────────────────
|
|
1880
|
+
|
|
1881
|
+
async function cmdImport(argv) {
|
|
1882
|
+
const { positional, flags } = parseArgs(argv);
|
|
1883
|
+
const url = positional[0];
|
|
1884
|
+
|
|
1885
|
+
if (!url) { fail('[mem] Usage: claude-mem-lite import <github-url> [--enrich]'); return; }
|
|
1886
|
+
|
|
1887
|
+
let rdb;
|
|
1888
|
+
try {
|
|
1889
|
+
rdb = ensureRegistryDb(REGISTRY_DB_PATH);
|
|
1890
|
+
rdb.pragma('busy_timeout = 3000');
|
|
1891
|
+
} catch (e) {
|
|
1892
|
+
fail(`[mem] Registry DB error: ${e.message}`);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
try {
|
|
1897
|
+
const { importFromGitHub } = await import('./registry-importer.mjs');
|
|
1898
|
+
out(`[mem] Importing from ${url}...`);
|
|
1899
|
+
const results = await importFromGitHub(rdb, url);
|
|
1900
|
+
|
|
1901
|
+
if (results.length === 0) {
|
|
1902
|
+
out('[mem] No skills/agents found in this repository.');
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
out(`[mem] Imported ${results.length} resource(s):`);
|
|
1907
|
+
for (const r of results) {
|
|
1908
|
+
out(` ${r.type === 'skill' ? 'S' : 'A'} ${r.name} (id=${r.id})`);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (flags.enrich) {
|
|
1912
|
+
out('[mem] Running LLM enrichment...');
|
|
1913
|
+
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1914
|
+
let enriched = 0;
|
|
1915
|
+
for (const r of results) {
|
|
1916
|
+
const row = rdb.prepare('SELECT local_path FROM resources WHERE id = ?').get(r.id);
|
|
1917
|
+
if (!row?.local_path) continue;
|
|
1918
|
+
try {
|
|
1919
|
+
const content = readFileSync(row.local_path, 'utf8');
|
|
1920
|
+
const ok = await enrichResource(rdb, r.name, r.type, content);
|
|
1921
|
+
if (ok) enriched++;
|
|
1922
|
+
} catch {}
|
|
1923
|
+
}
|
|
1924
|
+
out(`[mem] Enriched ${enriched}/${results.length} resources.`);
|
|
1925
|
+
}
|
|
1926
|
+
} catch (e) {
|
|
1927
|
+
fail(`[mem] Import failed: ${e.message}`);
|
|
1928
|
+
} finally {
|
|
1929
|
+
try { rdb.close(); } catch {}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// ─── Enrich ─────────────────────────────────────────────────────────────────
|
|
1934
|
+
|
|
1935
|
+
async function cmdEnrich(argv) {
|
|
1936
|
+
const { positional, flags } = parseArgs(argv);
|
|
1937
|
+
const name = positional[0];
|
|
1938
|
+
|
|
1939
|
+
let rdb;
|
|
1940
|
+
try {
|
|
1941
|
+
rdb = ensureRegistryDb(REGISTRY_DB_PATH);
|
|
1942
|
+
rdb.pragma('busy_timeout = 3000');
|
|
1943
|
+
} catch (e) {
|
|
1944
|
+
fail(`[mem] Registry DB error: ${e.message}`);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
try {
|
|
1949
|
+
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1950
|
+
|
|
1951
|
+
if (flags.all) {
|
|
1952
|
+
const rows = rdb.prepare("SELECT name, type, local_path FROM resources WHERE status = 'active' AND (enrichment_status IS NULL OR enrichment_status = 'failed')").all();
|
|
1953
|
+
if (rows.length === 0) { out('[mem] All resources already enriched.'); return; }
|
|
1954
|
+
out(`[mem] Enriching ${rows.length} resources...`);
|
|
1955
|
+
let ok = 0, failCount = 0;
|
|
1956
|
+
for (const r of rows) {
|
|
1957
|
+
if (!r.local_path) { failCount++; continue; }
|
|
1958
|
+
try {
|
|
1959
|
+
const content = readFileSync(r.local_path, 'utf8');
|
|
1960
|
+
const success = await enrichResource(rdb, r.name, r.type, content);
|
|
1961
|
+
if (success) ok++; else failCount++;
|
|
1962
|
+
if (!flags.batch) await new Promise(resolve => setTimeout(resolve, 500));
|
|
1963
|
+
} catch { failCount++; }
|
|
1964
|
+
}
|
|
1965
|
+
out(`[mem] Done: ${ok} enriched, ${failCount} failed.`);
|
|
1966
|
+
} else if (name) {
|
|
1967
|
+
const row = rdb.prepare("SELECT name, type, local_path FROM resources WHERE name = ? AND status = 'active'").get(name);
|
|
1968
|
+
if (!row) { fail(`[mem] Resource not found: ${name}`); return; }
|
|
1969
|
+
if (!row.local_path) { fail(`[mem] No local_path for ${name}`); return; }
|
|
1970
|
+
const content = readFileSync(row.local_path, 'utf8');
|
|
1971
|
+
const success = await enrichResource(rdb, row.name, row.type, content);
|
|
1972
|
+
out(success ? `[mem] Enriched: ${name}` : `[mem] Enrichment failed for ${name}`);
|
|
1973
|
+
} else {
|
|
1974
|
+
fail('[mem] Usage: claude-mem-lite enrich <name> OR claude-mem-lite enrich --all [--batch]');
|
|
1975
|
+
}
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
fail(`[mem] Enrich error: ${e.message}`);
|
|
1978
|
+
} finally {
|
|
1979
|
+
try { rdb.close(); } catch {}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1844
1983
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
1845
1984
|
|
|
1846
1985
|
export async function run(argv) {
|
|
@@ -1886,6 +2025,8 @@ export async function run(argv) {
|
|
|
1886
2025
|
case 'context': cmdContext(db, cmdArgs); break;
|
|
1887
2026
|
case 'browse': cmdBrowse(db, cmdArgs); break;
|
|
1888
2027
|
case 'registry': cmdRegistry(db, cmdArgs); break;
|
|
2028
|
+
case 'import': await cmdImport(cmdArgs); break;
|
|
2029
|
+
case 'enrich': await cmdEnrich(cmdArgs); break;
|
|
1889
2030
|
default:
|
|
1890
2031
|
out(`[mem] Unknown command: ${cmd}`);
|
|
1891
2032
|
out('[mem] Run "claude-mem-lite help" for usage');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.2",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"registry-retriever.mjs",
|
|
41
41
|
"registry-indexer.mjs",
|
|
42
42
|
"registry-scanner.mjs",
|
|
43
|
+
"registry-github.mjs",
|
|
44
|
+
"registry-importer.mjs",
|
|
45
|
+
"registry-enricher.mjs",
|
|
43
46
|
"resource-discovery.mjs",
|
|
44
47
|
"install.mjs",
|
|
45
48
|
"install-metadata.mjs",
|
|
@@ -73,6 +76,7 @@
|
|
|
73
76
|
"scripts/post-tool-use.sh",
|
|
74
77
|
"scripts/user-prompt-search.js",
|
|
75
78
|
"scripts/pre-tool-recall.js",
|
|
79
|
+
"scripts/pre-skill-bridge.js",
|
|
76
80
|
"scripts/prompt-search-utils.mjs",
|
|
77
81
|
".mcp.json",
|
|
78
82
|
".claude-plugin/plugin.json",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// claude-mem-lite: LLM enrichment for registry resources (Stage 2)
|
|
2
|
+
// Sends resource content to Haiku for semantic metadata generation
|
|
3
|
+
// Graceful degradation: failure preserves existing data
|
|
4
|
+
|
|
5
|
+
import { callHaikuJSON } from './haiku-client.mjs';
|
|
6
|
+
import { truncate, debugCatch } from './utils.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build the enrichment prompt for Haiku.
|
|
10
|
+
* @param {string} name Resource name
|
|
11
|
+
* @param {string} content SKILL.md/AGENT.md content
|
|
12
|
+
* @param {object} existingMeta Existing metadata from DB
|
|
13
|
+
* @returns {string} Prompt string
|
|
14
|
+
*/
|
|
15
|
+
export function buildEnrichPrompt(name, content, existingMeta) {
|
|
16
|
+
const truncated = truncate(content, 3000);
|
|
17
|
+
const existing = existingMeta.intent_tags ? `\n<existing-metadata>\ncurrent_tags: ${existingMeta.intent_tags}\n</existing-metadata>` : '';
|
|
18
|
+
|
|
19
|
+
return `You are a tool classification expert. Analyze this Claude Code skill and extract structured metadata.
|
|
20
|
+
|
|
21
|
+
<skill-name>${name}</skill-name>
|
|
22
|
+
<skill-content>
|
|
23
|
+
${truncated}
|
|
24
|
+
</skill-content>
|
|
25
|
+
${existing}
|
|
26
|
+
Return JSON only:
|
|
27
|
+
{"capability_summary":"One sentence (<80 chars)","intent_tags":"comma-separated intents","domain_tags":"comma-separated domains","trigger_patterns":"when to recommend","use_cases":"comma-separated scenarios","tech_stack":"comma-separated tech","quality_assessment":{"has_clear_instructions":true,"has_examples":false,"specificity":"high","estimated_utility":"high"}}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply LLM enrichment results to a resource.
|
|
32
|
+
* Only fills empty fields — never overwrites existing non-empty data.
|
|
33
|
+
* @param {Database} db Registry database
|
|
34
|
+
* @param {string} name Resource name
|
|
35
|
+
* @param {string} type Resource type
|
|
36
|
+
* @param {object} enrichResult LLM result JSON
|
|
37
|
+
*/
|
|
38
|
+
export function applyEnrichment(db, name, type, enrichResult) {
|
|
39
|
+
const row = db.prepare('SELECT * FROM resources WHERE name = ? AND type = ?').get(name, type);
|
|
40
|
+
if (!row) return;
|
|
41
|
+
|
|
42
|
+
const updates = [];
|
|
43
|
+
const params = [];
|
|
44
|
+
|
|
45
|
+
// Only fill empty fields
|
|
46
|
+
const fields = ['capability_summary', 'intent_tags', 'domain_tags', 'trigger_patterns', 'use_cases', 'tech_stack'];
|
|
47
|
+
for (const f of fields) {
|
|
48
|
+
if ((!row[f] || row[f].trim() === '') && enrichResult[f]) {
|
|
49
|
+
updates.push(`${f} = ?`);
|
|
50
|
+
params.push(enrichResult[f]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Always update enrichment status
|
|
55
|
+
updates.push("enrichment_status = 'done'");
|
|
56
|
+
updates.push('enriched_at = ?');
|
|
57
|
+
params.push(Date.now());
|
|
58
|
+
|
|
59
|
+
// Quality tier upgrade based on assessment
|
|
60
|
+
const qa = enrichResult.quality_assessment;
|
|
61
|
+
if (qa && row.quality_tier === 'community') {
|
|
62
|
+
if (qa.has_clear_instructions && (qa.specificity === 'high' || qa.specificity === 'medium')) {
|
|
63
|
+
updates.push("quality_tier = 'verified'");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
params.push(name, type);
|
|
68
|
+
db.prepare(`UPDATE resources SET ${updates.join(', ')} WHERE name = ? AND type = ?`).run(...params);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Enrich a single resource via Haiku LLM.
|
|
73
|
+
* @param {Database} db Registry database
|
|
74
|
+
* @param {string} name Resource name
|
|
75
|
+
* @param {string} type Resource type
|
|
76
|
+
* @param {string} content SKILL.md/AGENT.md content
|
|
77
|
+
* @returns {Promise<boolean>} true if enriched successfully
|
|
78
|
+
*/
|
|
79
|
+
export async function enrichResource(db, name, type, content) {
|
|
80
|
+
const existing = db.prepare('SELECT intent_tags, capability_summary FROM resources WHERE name = ? AND type = ?').get(name, type);
|
|
81
|
+
if (!existing) return false;
|
|
82
|
+
|
|
83
|
+
db.prepare("UPDATE resources SET enrichment_status = 'pending' WHERE name = ? AND type = ?").run(name, type);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const prompt = buildEnrichPrompt(name, content, existing);
|
|
87
|
+
const result = await callHaikuJSON(prompt, { timeout: 15000, maxTokens: 500 });
|
|
88
|
+
|
|
89
|
+
if (!result || !result.capability_summary) {
|
|
90
|
+
db.prepare("UPDATE resources SET enrichment_status = 'failed' WHERE name = ? AND type = ?").run(name, type);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
applyEnrichment(db, name, type, result);
|
|
95
|
+
return true;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
debugCatch(e, 'enricher');
|
|
98
|
+
db.prepare("UPDATE resources SET enrichment_status = 'failed' WHERE name = ? AND type = ?").run(name, type);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// claude-mem-lite: GitHub API helpers for smart import
|
|
2
|
+
// Pure functions for URL parsing and API URL construction
|
|
3
|
+
// Actual HTTP calls are in registry-importer.mjs
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a GitHub URL into owner, repo, branch, path.
|
|
7
|
+
* @param {string} url GitHub URL
|
|
8
|
+
* @returns {{ owner: string, repo: string, branch: string, path: string } | null}
|
|
9
|
+
*/
|
|
10
|
+
export function parseGitHubUrl(url) {
|
|
11
|
+
if (!url || typeof url !== 'string') return null;
|
|
12
|
+
const match = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?(?:\/tree\/([^/]+)(\/.*)?)?$/);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
const [, owner, repo, branch, pathRaw] = match;
|
|
15
|
+
return {
|
|
16
|
+
owner,
|
|
17
|
+
repo,
|
|
18
|
+
branch: branch || 'main',
|
|
19
|
+
path: pathRaw ? pathRaw.replace(/^\//, '') : '',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build GitHub API tree URL (recursive).
|
|
25
|
+
*/
|
|
26
|
+
export function buildTreeUrl(owner, repo, branch) {
|
|
27
|
+
return `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build raw content URL for a file.
|
|
32
|
+
*/
|
|
33
|
+
export function buildContentUrl(owner, repo, branch, path) {
|
|
34
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build GitHub API repo metadata URL.
|
|
39
|
+
*/
|
|
40
|
+
export function buildRepoUrl(owner, repo) {
|
|
41
|
+
return `https://api.github.com/repos/${owner}/${repo}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build headers for GitHub API requests.
|
|
46
|
+
* Uses GITHUB_TOKEN env var if available.
|
|
47
|
+
* @returns {Record<string, string>}
|
|
48
|
+
*/
|
|
49
|
+
export function buildHeaders() {
|
|
50
|
+
const headers = { 'User-Agent': 'claude-mem-lite', Accept: 'application/vnd.github.v3+json' };
|
|
51
|
+
const token = process.env.GITHUB_TOKEN;
|
|
52
|
+
if (token) headers.Authorization = `token ${token}`;
|
|
53
|
+
return headers;
|
|
54
|
+
}
|