claude-mem-lite 2.26.0 → 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.
Files changed (62) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +0 -0
  4. package/LICENSE +0 -0
  5. package/README.md +22 -5
  6. package/README.zh-CN.md +47 -63
  7. package/bash-utils.mjs +0 -0
  8. package/cli.mjs +1 -1
  9. package/commands/mem.md +1 -1
  10. package/commands/memory.md +1 -1
  11. package/commands/recall.md +1 -1
  12. package/commands/recent.md +1 -1
  13. package/commands/search.md +1 -1
  14. package/commands/timeline.md +1 -1
  15. package/commands/tools.md +9 -20
  16. package/commands/update.md +1 -1
  17. package/format-utils.mjs +0 -0
  18. package/haiku-client.mjs +0 -0
  19. package/hash-utils.mjs +0 -0
  20. package/hook-context.mjs +0 -0
  21. package/hook-episode.mjs +0 -0
  22. package/hook-handoff.mjs +1 -2
  23. package/hook-llm.mjs +35 -8
  24. package/hook-memory.mjs +9 -3
  25. package/hook-semaphore.mjs +0 -0
  26. package/hook-shared.mjs +0 -0
  27. package/hook-update.mjs +0 -0
  28. package/hook.mjs +19 -1
  29. package/hooks/hooks.json +10 -0
  30. package/install-metadata.mjs +0 -0
  31. package/install.mjs +23 -3
  32. package/mem-cli.mjs +155 -23
  33. package/nlp.mjs +0 -0
  34. package/package.json +5 -1
  35. package/project-utils.mjs +0 -0
  36. package/registry/preinstalled.json +0 -0
  37. package/registry-enricher.mjs +101 -0
  38. package/registry-github.mjs +54 -0
  39. package/registry-importer.mjs +352 -0
  40. package/registry-indexer.mjs +0 -0
  41. package/registry-retriever.mjs +0 -0
  42. package/registry-scanner.mjs +0 -0
  43. package/registry.mjs +36 -2
  44. package/resource-discovery.mjs +0 -0
  45. package/schema.mjs +0 -0
  46. package/scoring-sql.mjs +0 -0
  47. package/scripts/launch.mjs +0 -0
  48. package/scripts/pre-skill-bridge.js +78 -0
  49. package/scripts/pre-tool-recall.js +0 -0
  50. package/scripts/prompt-search-utils.mjs +47 -2
  51. package/scripts/user-prompt-search.js +105 -5
  52. package/secret-scrub.mjs +0 -0
  53. package/server-internals.mjs +0 -0
  54. package/server.mjs +184 -23
  55. package/skill.md +0 -0
  56. package/skip-tools.mjs +0 -0
  57. package/stop-words.mjs +0 -0
  58. package/synonyms.mjs +0 -0
  59. package/tfidf.mjs +0 -0
  60. package/tier.mjs +0 -0
  61. package/tool-schemas.mjs +9 -2
  62. package/utils.mjs +9 -2
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);
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';
@@ -164,20 +165,6 @@ function cmdSearch(db, args) {
164
165
  LIMIT ?
165
166
  `).all(...typeParams);
166
167
  }
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
168
  for (const r of obsRows) results.push({ ...r, _source: 'obs', score: r.score ?? 0 });
182
169
 
183
170
  // Concept co-occurrence + PRF expansion (aligned with MCP searchObservations)
@@ -222,6 +209,27 @@ function cmdSearch(db, args) {
222
209
  }
223
210
  }
224
211
  }
212
+
213
+ // Tier post-filter — applied to ALL obs results (initial + expansion + PRF)
214
+ if (tier) {
215
+ const obsInResults = results.filter(r => r._source === 'obs');
216
+ if (obsInResults.length > 0) {
217
+ const obsIds = obsInResults.map(r => r.id);
218
+ const ph = obsIds.map(() => '?').join(',');
219
+ const fullRows = db.prepare(
220
+ `SELECT id, compressed_into, superseded_at, memory_session_id, project, importance, last_accessed_at, created_at_epoch, type FROM observations WHERE id IN (${ph})`
221
+ ).all(...obsIds);
222
+ const rowMap = new Map(fullRows.map(r => [r.id, r]));
223
+ const tierCtx = { now: Date.now(), currentProject: project || inferProject(), currentSessionId: '' };
224
+ const allowedIds = new Set();
225
+ for (const [id, full] of rowMap) {
226
+ if (computeTier(full, tierCtx) === tier) allowedIds.add(id);
227
+ }
228
+ for (let i = results.length - 1; i >= 0; i--) {
229
+ if (results[i]._source === 'obs' && !allowedIds.has(results[i].id)) results.splice(i, 1);
230
+ }
231
+ }
232
+ }
225
233
  }
226
234
 
227
235
  // Search sessions (aligned with MCP mem_search)
@@ -1650,30 +1658,48 @@ function cmdRegistry(_memDb, args) {
1650
1658
  results = results.slice(0, 5);
1651
1659
  if (results.length === 0) { out(`[mem] No matching resources for: "${query}"`); return; }
1652
1660
  out(`[mem] ${results.length} resource(s) for "${query}":`);
1661
+ const home = homedir();
1653
1662
  for (const r of results) {
1654
1663
  const badge = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
1655
1664
  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}`);
1665
+ const isManaged = r.local_path && r.local_path.includes('/.claude-mem-lite/managed/');
1666
+ const portablePath = isManaged && r.local_path.startsWith(home) ? '~' + r.local_path.slice(home.length) : (r.local_path || '');
1667
+ let howToUse;
1668
+ if (isManaged) {
1669
+ const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
1670
+ howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
1671
+ } else if (r.invocation_name) {
1672
+ howToUse = r.type === 'skill'
1673
+ ? `Skill("${r.invocation_name}")`
1674
+ : `Agent(subagent_type="${r.invocation_name}")`;
1675
+ } else {
1676
+ howToUse = `mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
1677
+ }
1678
+ const pathLine = portablePath ? `\n Path: ${portablePath}` : '';
1679
+ out(` ${badge} ${r.type === 'skill' ? 'S' : 'A'} ${r.name}${categoryLabel} — ${truncate(r.capability_summary || '', 80)}${pathLine}\n Use: ${howToUse}`);
1660
1680
  }
1661
1681
  return;
1662
1682
  }
1663
1683
 
1664
1684
  if (action === 'list') {
1665
1685
  const typeFilter = flags.type;
1686
+ const rawLimit = parseInt(flags.limit, 10);
1687
+ const listLimit = Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : 20;
1666
1688
  const where = typeFilter ? 'WHERE type = ? AND status = ?' : 'WHERE status = ?';
1667
1689
  const params = typeFilter ? [typeFilter, 'active'] : ['active'];
1668
- const resources = rdb.prepare(`
1690
+ const allResources = rdb.prepare(`
1669
1691
  SELECT name, type, invocation_name, recommend_count, adopt_count, capability_summary
1670
- FROM resources ${where} ORDER BY type, name
1692
+ FROM resources ${where} ORDER BY adopt_count DESC, recommend_count DESC, type, name
1671
1693
  `).all(...params);
1672
- if (resources.length === 0) { out('[mem] No resources found.'); return; }
1673
- out(`[mem] Resources (${resources.length}):`);
1694
+ if (allResources.length === 0) { out('[mem] No resources found.'); return; }
1695
+ const resources = allResources.slice(0, listLimit);
1696
+ out(`[mem] Resources (showing ${resources.length} of ${allResources.length}):`);
1674
1697
  for (const r of resources) {
1675
1698
  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
1699
  }
1700
+ if (allResources.length > listLimit) {
1701
+ out(`[mem] Use --limit N to see more, or "registry search <query>" to find specific resources.`);
1702
+ }
1677
1703
  return;
1678
1704
  }
1679
1705
 
@@ -1831,7 +1857,7 @@ Commands:
1831
1857
  --limit N Max entries per tier (default 5)
1832
1858
 
1833
1859
  registry <action> Manage tool resource registry
1834
- list List all resources [--type skill|agent]
1860
+ list List resources [--type skill|agent] [--limit N] (default 20)
1835
1861
  stats Registry statistics
1836
1862
  search <query> Search resources [--type skill|agent] [--category C] [--quality Q]
1837
1863
  import Import resource --name N --resource-type T [--repo-url U] [--local-path P] [--use-cases U]
@@ -1841,6 +1867,110 @@ Commands:
1841
1867
  DB: ${DB_PATH}`);
1842
1868
  }
1843
1869
 
1870
+ // ─── Import (GitHub) ────────────────────────────────────────────────────────
1871
+
1872
+ async function cmdImport(db, argv) {
1873
+ const { positional, flags } = parseArgs(argv);
1874
+ const url = positional[0];
1875
+
1876
+ if (!url) { fail('[mem] Usage: claude-mem-lite import <github-url> [--enrich]'); return; }
1877
+
1878
+ let rdb;
1879
+ try {
1880
+ rdb = ensureRegistryDb(REGISTRY_DB_PATH);
1881
+ rdb.pragma('busy_timeout = 3000');
1882
+ } catch (e) {
1883
+ fail(`[mem] Registry DB error: ${e.message}`);
1884
+ return;
1885
+ }
1886
+
1887
+ try {
1888
+ const { importFromGitHub } = await import('./registry-importer.mjs');
1889
+ out(`[mem] Importing from ${url}...`);
1890
+ const results = await importFromGitHub(rdb, url);
1891
+
1892
+ if (results.length === 0) {
1893
+ out('[mem] No skills/agents found in this repository.');
1894
+ return;
1895
+ }
1896
+
1897
+ out(`[mem] Imported ${results.length} resource(s):`);
1898
+ for (const r of results) {
1899
+ out(` ${r.type === 'skill' ? 'S' : 'A'} ${r.name} (id=${r.id})`);
1900
+ }
1901
+
1902
+ if (flags.enrich) {
1903
+ out('[mem] Running LLM enrichment...');
1904
+ const { enrichResource } = await import('./registry-enricher.mjs');
1905
+ let enriched = 0;
1906
+ for (const r of results) {
1907
+ const row = rdb.prepare('SELECT local_path FROM resources WHERE id = ?').get(r.id);
1908
+ if (!row?.local_path) continue;
1909
+ try {
1910
+ const content = readFileSync(row.local_path, 'utf8');
1911
+ const ok = await enrichResource(rdb, r.name, r.type, content);
1912
+ if (ok) enriched++;
1913
+ } catch {}
1914
+ }
1915
+ out(`[mem] Enriched ${enriched}/${results.length} resources.`);
1916
+ }
1917
+ } catch (e) {
1918
+ fail(`[mem] Import failed: ${e.message}`);
1919
+ } finally {
1920
+ try { rdb.close(); } catch {}
1921
+ }
1922
+ }
1923
+
1924
+ // ─── Enrich ─────────────────────────────────────────────────────────────────
1925
+
1926
+ async function cmdEnrich(db, argv) {
1927
+ const { positional, flags } = parseArgs(argv);
1928
+ const name = positional[0];
1929
+
1930
+ let rdb;
1931
+ try {
1932
+ rdb = ensureRegistryDb(REGISTRY_DB_PATH);
1933
+ rdb.pragma('busy_timeout = 3000');
1934
+ } catch (e) {
1935
+ fail(`[mem] Registry DB error: ${e.message}`);
1936
+ return;
1937
+ }
1938
+
1939
+ try {
1940
+ const { enrichResource } = await import('./registry-enricher.mjs');
1941
+
1942
+ if (flags.all) {
1943
+ const rows = rdb.prepare("SELECT name, type, local_path FROM resources WHERE status = 'active' AND (enrichment_status IS NULL OR enrichment_status = 'failed')").all();
1944
+ if (rows.length === 0) { out('[mem] All resources already enriched.'); return; }
1945
+ out(`[mem] Enriching ${rows.length} resources...`);
1946
+ let ok = 0, failCount = 0;
1947
+ for (const r of rows) {
1948
+ if (!r.local_path) { failCount++; continue; }
1949
+ try {
1950
+ const content = readFileSync(r.local_path, 'utf8');
1951
+ const success = await enrichResource(rdb, r.name, r.type, content);
1952
+ if (success) ok++; else failCount++;
1953
+ if (!flags.batch) await new Promise(resolve => setTimeout(resolve, 500));
1954
+ } catch { failCount++; }
1955
+ }
1956
+ out(`[mem] Done: ${ok} enriched, ${failCount} failed.`);
1957
+ } else if (name) {
1958
+ const row = rdb.prepare("SELECT name, type, local_path FROM resources WHERE name = ? AND status = 'active'").get(name);
1959
+ if (!row) { fail(`[mem] Resource not found: ${name}`); return; }
1960
+ if (!row.local_path) { fail(`[mem] No local_path for ${name}`); return; }
1961
+ const content = readFileSync(row.local_path, 'utf8');
1962
+ const success = await enrichResource(rdb, row.name, row.type, content);
1963
+ out(success ? `[mem] Enriched: ${name}` : `[mem] Enrichment failed for ${name}`);
1964
+ } else {
1965
+ fail('[mem] Usage: claude-mem-lite enrich <name> OR claude-mem-lite enrich --all [--batch]');
1966
+ }
1967
+ } catch (e) {
1968
+ fail(`[mem] Enrich error: ${e.message}`);
1969
+ } finally {
1970
+ try { rdb.close(); } catch {}
1971
+ }
1972
+ }
1973
+
1844
1974
  // ─── Main Entry Point ────────────────────────────────────────────────────────
1845
1975
 
1846
1976
  export async function run(argv) {
@@ -1886,6 +2016,8 @@ export async function run(argv) {
1886
2016
  case 'context': cmdContext(db, cmdArgs); break;
1887
2017
  case 'browse': cmdBrowse(db, cmdArgs); break;
1888
2018
  case 'registry': cmdRegistry(db, cmdArgs); break;
2019
+ case 'import': await cmdImport(db, cmdArgs); break;
2020
+ case 'enrich': await cmdEnrich(db, cmdArgs); break;
1889
2021
  default:
1890
2022
  out(`[mem] Unknown command: ${cmd}`);
1891
2023
  out('[mem] Run "claude-mem-lite help" for usage');
package/nlp.mjs CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.26.0",
3
+ "version": "2.28.1",
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",
package/project-utils.mjs CHANGED
File without changes
File without changes
@@ -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
+ }