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/install.mjs CHANGED
@@ -452,7 +452,7 @@ async function install() {
452
452
  ]
453
453
  };
454
454
 
455
- const memPreToolUse = {
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
- for (const [event, config] of [['PreToolUse', memPreToolUse], ['PostToolUse', memPostToolUse], ['SessionStart', memSessionStart], ['Stop', memStop], ['UserPromptSubmit', memUserPrompt]]) {
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, config];
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
- log('Setting up skill/agent registry...');
513
- try {
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
- const cliVer = execSync('claude-mem-lite --help 2>/dev/null && echo OK', { encoding: 'utf8', timeout: 5000 });
943
- if (cliVer.includes('OK')) {
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 = execSync('pgrep -af "chroma|claude-mem.*worker" 2>/dev/null', { encoding: 'utf8' }).trim();
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.stdout.write(text + '\n');
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
- const ftsQuery = sanitizeFtsQuery(query);
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 = Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 10);
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
- db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${recallPh})`).run(Date.now(), ...recalledIds);
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
- db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
584
- autoBoostIfNeeded(db, ids);
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
- db.prepare('UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id = ?').run(Date.now(), anchorId);
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 howToUse = r.type === 'skill'
1657
- ? (r.invocation_name ? `skill="${r.invocation_name}"` : r.name)
1658
- : `subagent_type="${r.invocation_name || r.name}"`;
1659
- out(` ${badge} ${r.type === 'skill' ? 'S' : 'A'} ${r.name}${categoryLabel} — ${truncate(r.capability_summary || '', 80)} | Use: ${howToUse}`);
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 resources = rdb.prepare(`
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 (resources.length === 0) { out('[mem] No resources found.'); return; }
1673
- out(`[mem] Resources (${resources.length}):`);
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 all resources [--type skill|agent]
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.26.1",
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
+ }