claude-mem-lite 2.95.1 → 2.96.0

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.95.1",
13
+ "version": "2.96.0",
14
14
  "source": "./",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.95.1",
3
+ "version": "2.96.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/install.mjs CHANGED
@@ -302,6 +302,43 @@ function isDevInstall() {
302
302
  }
303
303
  }
304
304
 
305
+ // Decide what to check out of a registry repo. We only ever copy the manifest's
306
+ // `entry.path` subdirs into managed/skills|agents, so the working tree never
307
+ // needs the rest of the repo — a partial+sparse clone fetches just those subtrees
308
+ // (e.g. davila7/claude-code-templates: 197MB whole repo → a few MB of 3 paths).
309
+ // Returns { full, paths }: `full` forces a normal checkout when any entry maps to
310
+ // the repo root ('.') — sparse buys nothing there. Unsafe paths ('..'/absolute)
311
+ // are dropped here exactly as the copy loop drops them, so they never reach
312
+ // `sparse-checkout set`. Pure + exported for unit testing.
313
+ export function planRepoSparsePaths(entries) {
314
+ let needsFull = false;
315
+ const paths = [];
316
+ for (const e of entries || []) {
317
+ const p = e && e.path;
318
+ if (!p || p === '.' || p === './') { needsFull = true; continue; }
319
+ if (isAbsolute(p) || String(p).includes('..')) continue; // unsafe — skipped at copy too
320
+ const norm = String(p).replace(/^\.\//, '').replace(/\/+$/, '');
321
+ if (norm && !paths.includes(norm)) paths.push(norm);
322
+ }
323
+ // No usable sparse paths (all root or all unsafe) → a full checkout is the only
324
+ // thing that can produce content; sparse would be an empty, pointless tree.
325
+ return { full: needsFull || paths.length === 0, paths };
326
+ }
327
+
328
+ // True only for a clone this code produced (partial-clone promisor + sparse-checkout
329
+ // both on). A legacy full clone returns false → caller re-clones it slim. Detection
330
+ // erring false only costs a one-time re-clone (the dir is a rebuildable cache), so
331
+ // over-eager migration is safe; under-eager just keeps a fat clone one more cycle.
332
+ function isPartialSparseClone(clonePath) {
333
+ const cfg = (key) => {
334
+ try {
335
+ return execFileSync('git', ['-C', clonePath, 'config', '--get', key],
336
+ { encoding: 'utf8', stdio: 'pipe' }).trim();
337
+ } catch { return ''; } // missing key → git exits non-zero → treat as unset
338
+ };
339
+ return cfg('remote.origin.promisor') === 'true' && cfg('core.sparseCheckout') === 'true';
340
+ }
341
+
305
342
  // ─── Install ────────────────────────────────────────────────────────────────
306
343
 
307
344
  async function install() {
@@ -748,11 +785,39 @@ async function install() {
748
785
  const clonePath = join(managedDir, 'repos', repoName);
749
786
  let repoReady = false;
750
787
 
788
+ const plan = planRepoSparsePaths(entries);
789
+ const cloneUrl = `${repoUrl.replace(/\.git$/, '')}.git`;
790
+ // Clone only what we extract: a partial (blob:none) + sparse clone fetches
791
+ // just the manifest subpaths' subtrees instead of the whole repo. Falls
792
+ // back to a plain shallow clone if partial-clone/sparse-checkout is
793
+ // unsupported (old git/server) — identical to the prior behavior.
794
+ const cloneSlim = () => {
795
+ if (plan.full) {
796
+ execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
797
+ return;
798
+ }
799
+ try {
800
+ execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
801
+ execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 });
802
+ execFileSync('git', ['-C', clonePath, 'checkout'], { stdio: 'pipe', timeout: 30000 });
803
+ } catch {
804
+ try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
805
+ execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
806
+ }
807
+ };
808
+
809
+ // Migrate a legacy full clone: drop it so the fresh-clone path below
810
+ // rebuilds it slim. managed/repos is a rebuildable cache, so this loses
811
+ // nothing and reclaims the bulk of its footprint on the next install run.
812
+ if (!plan.full && existsSync(clonePath) && !isPartialSparseClone(clonePath)) {
813
+ try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
814
+ }
815
+
751
816
  if (!existsSync(clonePath)) {
752
- // Fresh clone
817
+ // Fresh clone (also the rebuild path for a just-migrated legacy clone)
753
818
  try {
754
819
  mkdirSync(join(managedDir, 'repos'), { recursive: true });
755
- execFileSync('git', ['clone', '--depth', '1', `${repoUrl.replace(/\.git$/, '')}.git`, clonePath], { stdio: 'pipe', timeout: 30000 });
820
+ cloneSlim();
756
821
  cloned++;
757
822
  repoReady = true;
758
823
  } catch (err) {
@@ -767,6 +832,11 @@ async function install() {
767
832
  } else {
768
833
  // Update existing: fetch latest and fast-forward
769
834
  try {
835
+ // Re-assert the sparse set so a newer manifest that adds a subpath to
836
+ // an already-slim clone checks it out (idempotent; no-op for full clones).
837
+ if (!plan.full && isPartialSparseClone(clonePath)) {
838
+ try { execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 }); } catch {}
839
+ }
770
840
  const localHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
771
841
  execFileSync('git', ['-C', clonePath, 'fetch', '--depth', '1', 'origin'], { stdio: 'pipe', timeout: 30000 });
772
842
  const remoteHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'FETCH_HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.95.1",
3
+ "version": "2.96.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",