claude-mem-lite 3.7.0 → 3.8.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.
package/install.mjs CHANGED
@@ -343,711 +343,748 @@ function isPartialSparseClone(clonePath) {
343
343
 
344
344
  // ─── Install ────────────────────────────────────────────────────────────────
345
345
 
346
- async function install() {
347
- console.log('\nclaude-mem-lite installer\n');
348
346
 
349
- // Resolve dynamic imports against the installed copy at INSTALL_DIR rather
350
- // than install.mjs's own directory. Lets install.mjs run correctly from a
351
- // /tmp staging dir (repair flow, `curl | tar xz | node install.mjs install`)
352
- // where PROJECT_DIR has no node_modules but INSTALL_DIR does — step 2 below
353
- // ran `npm install --cwd INSTALL_DIR`. Pre-fix, steps 6/7 fired
354
- // "Cannot find package 'better-sqlite3' imported from /tmp/…/registry.mjs"
355
- // and silently skipped registry-DB seeding + DB health check on every repair.
356
- const importFromInstall = (rel) => import(pathToFileURL(join(INSTALL_DIR, rel)).href);
357
- const requireFromInstall = createRequire(pathToFileURL(join(INSTALL_DIR, 'package.json')).href);
358
-
359
- // 1. Install source files to ~/.claude-mem-lite/
360
- const IS_DEV = flags.has('--dev');
347
+ // Dynamic-import helpers, resolved against the installed copy at INSTALL_DIR
348
+ // (lets install.mjs run from a /tmp staging dir whose node_modules is at
349
+ // INSTALL_DIR, not the script dir). Used by the resource / db-verify / adopt steps.
350
+ const importFromInstall = (rel) => import(pathToFileURL(join(INSTALL_DIR, rel)).href);
351
+ const requireFromInstall = createRequire(pathToFileURL(join(INSTALL_DIR, 'package.json')).href);
352
+
353
+ // ─── install() step helpers (audit P1-9) ──────────────────────────────────────
354
+ function installSourceFiles(IS_DEV) {
355
+ // Auto-migrate unhidden dir (~/claude-mem-lite/ → ~/.claude-mem-lite/)
356
+ const oldUnhidden = join(homedir(), 'claude-mem-lite');
357
+ if (!existsSync(DATA_DIR) && existsSync(oldUnhidden)) {
358
+ log('Migrating ~/claude-mem-lite/ ~/.claude-mem-lite/...');
359
+ renameSync(oldUnhidden, DATA_DIR);
360
+ ok('Directory migrated');
361
+ }
361
362
 
362
- // Auto-migrate unhidden dir (~/claude-mem-lite/ ~/.claude-mem-lite/)
363
- const oldUnhidden = join(homedir(), 'claude-mem-lite');
364
- if (!existsSync(DATA_DIR) && existsSync(oldUnhidden)) {
365
- log('Migrating ~/claude-mem-lite/ → ~/.claude-mem-lite/...');
366
- renameSync(oldUnhidden, DATA_DIR);
367
- ok('Directory migrated');
368
- }
369
-
370
- if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
371
- // Under relocation the DB/managed/runtime live here, not in the code dir — create it too.
372
- if (!existsSync(MEM_DATA_DIR)) mkdirSync(MEM_DATA_DIR, { recursive: true });
373
-
374
- if (IS_DEV) {
375
- log('Dev mode creating symlinks in ~/.claude-mem-lite/...');
376
- // Symlink individual source files
377
- for (const f of SOURCE_FILES) {
378
- const target = join(PROJECT_DIR, f);
379
- const link = join(DATA_DIR, f);
380
- if (existsSync(target)) {
381
- // Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
382
- const linkParent = dirname(link);
383
- if (!existsSync(linkParent)) mkdirSync(linkParent, { recursive: true });
384
- // Remove existing file/symlink before creating
385
- if (existsSync(link)) try { unlinkSync(link); } catch {}
386
- symlinkSync(target, link);
387
- }
363
+ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
364
+ // Under relocation the DB/managed/runtime live here, not in the code dir — create it too.
365
+ if (!existsSync(MEM_DATA_DIR)) mkdirSync(MEM_DATA_DIR, { recursive: true });
366
+
367
+ if (IS_DEV) {
368
+ log('Dev mode — creating symlinks in ~/.claude-mem-lite/...');
369
+ // Symlink individual source files
370
+ for (const f of SOURCE_FILES) {
371
+ const target = join(PROJECT_DIR, f);
372
+ const link = join(DATA_DIR, f);
373
+ if (existsSync(target)) {
374
+ // Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
375
+ const linkParent = dirname(link);
376
+ if (!existsSync(linkParent)) mkdirSync(linkParent, { recursive: true });
377
+ // Remove existing file/symlink before creating
378
+ if (existsSync(link)) try { unlinkSync(link); } catch {}
379
+ symlinkSync(target, link);
388
380
  }
389
- // Symlink scripts/ directory
390
- const scriptsLink = join(DATA_DIR, 'scripts');
391
- if (existsSync(scriptsLink)) try { rmSync(scriptsLink, { recursive: true, force: true }); } catch {}
392
- symlinkSync(join(PROJECT_DIR, 'scripts'), scriptsLink);
393
- // Symlink node_modules/
394
- const nmLink = join(DATA_DIR, 'node_modules');
395
- if (existsSync(nmLink)) try { rmSync(nmLink, { recursive: true, force: true }); } catch {}
396
- symlinkSync(join(PROJECT_DIR, 'node_modules'), nmLink);
397
- // Symlink registry/ directory
398
- const regLink = join(DATA_DIR, 'registry');
399
- if (existsSync(regLink)) try { rmSync(regLink, { recursive: true, force: true }); } catch {}
400
- if (existsSync(join(PROJECT_DIR, 'registry'))) {
401
- symlinkSync(join(PROJECT_DIR, 'registry'), regLink);
402
- }
403
- // commands/ is intentionally NOT linked: Claude Code reads slash commands
404
- // from the plugin cache (~/.claude/plugins/cache/<mp>/<plugin>/<ver>/commands/)
405
- // or user-level ~/.claude/commands/, never from ~/.claude-mem-lite/commands/.
406
- // Pre-v2.55 maintained a symlink/copy here that had no consumers.
407
- ok('Symlinks created in ~/.claude-mem-lite/ dev dir');
408
- } else {
409
- log('Installing to ~/.claude-mem-lite/...');
410
- const scriptsDir = join(DATA_DIR, 'scripts');
411
- if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
412
- for (const f of SOURCE_FILES) {
413
- const src = join(PROJECT_DIR, f);
414
- const dst = join(DATA_DIR, f);
415
- if (existsSync(src)) {
416
- // Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
417
- const dstParent = dirname(dst);
418
- if (!existsSync(dstParent)) mkdirSync(dstParent, { recursive: true });
419
- copyFileSync(src, dst);
420
- }
381
+ }
382
+ // Symlink scripts/ directory
383
+ const scriptsLink = join(DATA_DIR, 'scripts');
384
+ if (existsSync(scriptsLink)) try { rmSync(scriptsLink, { recursive: true, force: true }); } catch {}
385
+ symlinkSync(join(PROJECT_DIR, 'scripts'), scriptsLink);
386
+ // Symlink node_modules/
387
+ const nmLink = join(DATA_DIR, 'node_modules');
388
+ if (existsSync(nmLink)) try { rmSync(nmLink, { recursive: true, force: true }); } catch {}
389
+ symlinkSync(join(PROJECT_DIR, 'node_modules'), nmLink);
390
+ // Symlink registry/ directory
391
+ const regLink = join(DATA_DIR, 'registry');
392
+ if (existsSync(regLink)) try { rmSync(regLink, { recursive: true, force: true }); } catch {}
393
+ if (existsSync(join(PROJECT_DIR, 'registry'))) {
394
+ symlinkSync(join(PROJECT_DIR, 'registry'), regLink);
395
+ }
396
+ // commands/ is intentionally NOT linked: Claude Code reads slash commands
397
+ // from the plugin cache (~/.claude/plugins/cache/<mp>/<plugin>/<ver>/commands/)
398
+ // or user-level ~/.claude/commands/, never from ~/.claude-mem-lite/commands/.
399
+ // Pre-v2.55 maintained a symlink/copy here that had no consumers.
400
+ ok('Symlinks created in ~/.claude-mem-lite/ → dev dir');
401
+ } else {
402
+ log('Installing to ~/.claude-mem-lite/...');
403
+ const scriptsDir = join(DATA_DIR, 'scripts');
404
+ if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
405
+ for (const f of SOURCE_FILES) {
406
+ const src = join(PROJECT_DIR, f);
407
+ const dst = join(DATA_DIR, f);
408
+ if (existsSync(src)) {
409
+ // Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
410
+ const dstParent = dirname(dst);
411
+ if (!existsSync(dstParent)) mkdirSync(dstParent, { recursive: true });
412
+ copyFileSync(src, dst);
421
413
  }
422
- // Copy hook scripts (settings.json hook commands point at these — must
423
- // stay in sync with HOOK_SCRIPT_FILES manifest)
424
- copyHookScripts(join(PROJECT_DIR, 'scripts'), scriptsDir);
425
- // Ensure bash script is executable
426
- try { execFileSync('chmod', ['+x', join(scriptsDir, 'post-tool-use.sh')], { stdio: 'pipe' }); } catch {}
427
- // commands/ is intentionally NOT copied — see dev-mode branch above.
428
- // Copy registry manifest
429
- const registryDir = join(DATA_DIR, 'registry');
430
- if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
431
- const manifestSrc = join(PROJECT_DIR, 'registry', 'preinstalled.json');
432
- if (existsSync(manifestSrc)) copyFileSync(manifestSrc, join(registryDir, 'preinstalled.json'));
433
- ok('Source files copied to ~/.claude-mem-lite/');
434
-
435
- // v2.48 P1-4: prune stale top-level .mjs + 0-byte .db files left behind by
436
- // prior upgrades (e.g. dispatch.mjs removed in v2.20.0, zero-byte mem.db /
437
- // memory.db / registry.db from pre-consolidation installs). Subdirs +
438
- // symlinks + non-empty DBs are always preserved.
439
- try {
440
- const pruned = pruneStaleInstallFiles(DATA_DIR, SOURCE_FILES);
441
- if (pruned.length > 0) {
442
- ok(`Pruned ${pruned.length} stale file(s): ${pruned.map(p => p.split('/').pop()).join(', ')}`);
443
- }
444
- } catch (e) { /* prune is best-effort — never block install */ void e; }
445
414
  }
415
+ // Copy hook scripts (settings.json hook commands point at these — must
416
+ // stay in sync with HOOK_SCRIPT_FILES manifest)
417
+ copyHookScripts(join(PROJECT_DIR, 'scripts'), scriptsDir);
418
+ // Ensure bash script is executable
419
+ try { execFileSync('chmod', ['+x', join(scriptsDir, 'post-tool-use.sh')], { stdio: 'pipe' }); } catch {}
420
+ // commands/ is intentionally NOT copied — see dev-mode branch above.
421
+ // Copy registry manifest
422
+ const registryDir = join(DATA_DIR, 'registry');
423
+ if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
424
+ const manifestSrc = join(PROJECT_DIR, 'registry', 'preinstalled.json');
425
+ if (existsSync(manifestSrc)) copyFileSync(manifestSrc, join(registryDir, 'preinstalled.json'));
426
+ ok('Source files copied to ~/.claude-mem-lite/');
427
+
428
+ // v2.48 P1-4: prune stale top-level .mjs + 0-byte .db files left behind by
429
+ // prior upgrades (e.g. dispatch.mjs removed in v2.20.0, zero-byte mem.db /
430
+ // memory.db / registry.db from pre-consolidation installs). Subdirs +
431
+ // symlinks + non-empty DBs are always preserved.
432
+ try {
433
+ const pruned = pruneStaleInstallFiles(DATA_DIR, SOURCE_FILES);
434
+ if (pruned.length > 0) {
435
+ ok(`Pruned ${pruned.length} stale file(s): ${pruned.map(p => p.split('/').pop()).join(', ')}`);
436
+ }
437
+ } catch (e) { /* prune is best-effort — never block install */ void e; }
438
+ }
439
+ }
446
440
 
447
- // 2. npm install (skip for --dev: node_modules is symlinked)
448
- if (IS_DEV) {
449
- ok('Dependencies: using dev dir (symlinked)');
441
+ async function installDependencies(IS_DEV) {
442
+ // 2. npm install (skip for --dev: node_modules is symlinked)
443
+ if (IS_DEV) {
444
+ ok('Dependencies: using dev dir (symlinked)');
445
+ } else {
446
+ log('Ensuring dependencies installed...');
447
+ try {
448
+ // stderr inherited so users see real-time progress (network slowness,
449
+ // node-gyp compile spinner, prebuild-install fallback messages). With
450
+ // `stdio: 'pipe'` the install appeared to hang under the 5-min Bash
451
+ // timeout when better-sqlite3 had no Node v24 prebuild and had to
452
+ // compile from source — see bug audit 2026-05.
453
+ execSync(NPM_INSTALL_CMD, { cwd: INSTALL_DIR, stdio: ['ignore', 'pipe', 'inherit'] });
454
+ ok('Dependencies installed');
455
+ } catch (e) {
456
+ fail('npm install failed: ' + e.message);
457
+ process.exit(1);
458
+ }
459
+ // npm install exits 0 even when the better-sqlite3 prebuilt .node binary
460
+ // mismatches the running Node ABI (e.g. NODE_MODULE_VERSION 137 on Node v24).
461
+ // Probe and auto-rebuild before declaring success — otherwise the next
462
+ // launch FATALs with "Could not locate the bindings file".
463
+ const verify = await ensureBetterSqlite3Working(INSTALL_DIR);
464
+ if (verify.ok) {
465
+ ok(`better-sqlite3: ${verify.action}`);
450
466
  } else {
451
- log('Ensuring dependencies installed...');
452
- try {
453
- // stderr inherited so users see real-time progress (network slowness,
454
- // node-gyp compile spinner, prebuild-install fallback messages). With
455
- // `stdio: 'pipe'` the install appeared to hang under the 5-min Bash
456
- // timeout when better-sqlite3 had no Node v24 prebuild and had to
457
- // compile from source — see bug audit 2026-05.
458
- execSync(NPM_INSTALL_CMD, { cwd: INSTALL_DIR, stdio: ['ignore', 'pipe', 'inherit'] });
459
- ok('Dependencies installed');
460
- } catch (e) {
461
- fail('npm install failed: ' + e.message);
462
- process.exit(1);
463
- }
464
- // npm install exits 0 even when the better-sqlite3 prebuilt .node binary
465
- // mismatches the running Node ABI (e.g. NODE_MODULE_VERSION 137 on Node v24).
466
- // Probe and auto-rebuild before declaring success — otherwise the next
467
- // launch FATALs with "Could not locate the bindings file".
468
- const verify = await ensureBetterSqlite3Working(INSTALL_DIR);
469
- if (verify.ok) {
470
- ok(`better-sqlite3: ${verify.action}`);
471
- } else {
472
- fail(`better-sqlite3 binding unusable after rebuild: ${verify.error}`);
473
- log('Try manually: cd ' + INSTALL_DIR + ' && npm rebuild better-sqlite3 --build-from-source');
474
- process.exit(1);
475
- }
467
+ fail(`better-sqlite3 binding unusable after rebuild: ${verify.error}`);
468
+ log('Try manually: cd ' + INSTALL_DIR + ' && npm rebuild better-sqlite3 --build-from-source');
469
+ process.exit(1);
476
470
  }
471
+ }
472
+ }
477
473
 
478
- // 2b. Create global CLI symlink (claude-mem-lite command)
479
- const cliSource = join(INSTALL_DIR, 'cli.mjs');
480
- if (existsSync(cliSource)) {
481
- try { execFileSync('chmod', ['+x', cliSource], { stdio: 'pipe' }); } catch {}
482
- // Try ~/.local/bin first (user-writable, commonly on PATH)
483
- const localBin = join(homedir(), '.local', 'bin');
484
- const cliLink = join(localBin, 'claude-mem-lite');
474
+ function createCliSymlink() {
475
+ // 2b. Create global CLI symlink (claude-mem-lite command)
476
+ const cliSource = join(INSTALL_DIR, 'cli.mjs');
477
+ if (existsSync(cliSource)) {
478
+ try { execFileSync('chmod', ['+x', cliSource], { stdio: 'pipe' }); } catch {}
479
+ // Try ~/.local/bin first (user-writable, commonly on PATH)
480
+ const localBin = join(homedir(), '.local', 'bin');
481
+ const cliLink = join(localBin, 'claude-mem-lite');
482
+ try {
483
+ if (!existsSync(localBin)) mkdirSync(localBin, { recursive: true });
484
+ if (existsSync(cliLink)) unlinkSync(cliLink);
485
+ symlinkSync(cliSource, cliLink);
486
+ ok(`CLI: ${cliLink} → ${cliSource}`);
487
+ } catch {
488
+ // Fallback: try /usr/local/bin (may need sudo)
485
489
  try {
486
- if (!existsSync(localBin)) mkdirSync(localBin, { recursive: true });
487
- if (existsSync(cliLink)) unlinkSync(cliLink);
488
- symlinkSync(cliSource, cliLink);
489
- ok(`CLI: ${cliLink} → ${cliSource}`);
490
+ const globalLink = '/usr/local/bin/claude-mem-lite';
491
+ if (existsSync(globalLink)) unlinkSync(globalLink);
492
+ symlinkSync(cliSource, globalLink);
493
+ ok(`CLI: ${globalLink} → ${cliSource}`);
490
494
  } catch {
491
- // Fallback: try /usr/local/bin (may need sudo)
492
- try {
493
- const globalLink = '/usr/local/bin/claude-mem-lite';
494
- if (existsSync(globalLink)) unlinkSync(globalLink);
495
- symlinkSync(cliSource, globalLink);
496
- ok(`CLI: ${globalLink} → ${cliSource}`);
497
- } catch {
498
- warn('CLI symlink failed — run manually: ln -sf ' + cliSource + ' ~/.local/bin/claude-mem-lite');
499
- }
495
+ warn('CLI symlink failed — run manually: ln -sf ' + cliSource + ' ~/.local/bin/claude-mem-lite');
500
496
  }
501
497
  }
498
+ }
499
+ }
502
500
 
503
- // 3. Register MCP server (skip if plugin system already handles it)
504
- // Plugin MCP must stay at root .mcp.json so Claude Code registers plugin:*:mem-lite.
505
- // Duplicate registrations in practice come from old global install.mjs state
506
- // (claude mcp add) or stale marketplace copies, not from the cache root itself.
507
- // Global registration via `claude mcp add` creates a DUPLICATE mcp__mem-lite__* server.
508
- // The legacy generic name "mem" (pre-v2.78) is also purged so a user who installed in
509
- // either era ends up with a single canonical "mem-lite" registration.
510
- // Detect plugin mode: installed_plugins.json has our entry plugin handles MCP.
511
- const installedPluginsPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
512
- let pluginHandlesMcp = false;
501
+ function registerMcpServer() {
502
+ // 3. Register MCP server (skip if plugin system already handles it)
503
+ // Plugin MCP must stay at root .mcp.json so Claude Code registers plugin:*:mem-lite.
504
+ // Duplicate registrations in practice come from old global install.mjs state
505
+ // (claude mcp add) or stale marketplace copies, not from the cache root itself.
506
+ // Global registration via `claude mcp add` creates a DUPLICATE mcp__mem-lite__* server.
507
+ // The legacy generic name "mem" (pre-v2.78) is also purged so a user who installed in
508
+ // either era ends up with a single canonical "mem-lite" registration.
509
+ // Detect plugin mode: installed_plugins.json has our entry → plugin handles MCP.
510
+ const installedPluginsPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
511
+ let pluginHandlesMcp = false;
512
+ try {
513
+ const installed = JSON.parse(readFileSync(installedPluginsPath, 'utf8'));
514
+ pluginHandlesMcp = !!installed?.plugins?.[PLUGIN_KEY]?.length;
515
+ } catch { /* not installed via plugin system */ }
516
+
517
+ if (pluginHandlesMcp) {
518
+ log('MCP server: plugin system handles registration (skipping global)');
519
+ // Clean up stale global registrations (both legacy "mem" and current "mem-lite")
520
+ for (const name of ['mem', 'mem-lite']) {
521
+ try {
522
+ execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' });
523
+ ok(`Removed stale global MCP "${name}"`);
524
+ } catch {}
525
+ }
526
+ } else {
527
+ log('Registering MCP server...');
513
528
  try {
514
- const installed = JSON.parse(readFileSync(installedPluginsPath, 'utf8'));
515
- pluginHandlesMcp = !!installed?.plugins?.[PLUGIN_KEY]?.length;
516
- } catch { /* not installed via plugin system */ }
517
-
518
- if (pluginHandlesMcp) {
519
- log('MCP server: plugin system handles registration (skipping global)');
520
- // Clean up stale global registrations (both legacy "mem" and current "mem-lite")
529
+ // Purge legacy "mem" and any pre-existing "mem-lite" before re-registering
521
530
  for (const name of ['mem', 'mem-lite']) {
522
- try {
523
- execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' });
524
- ok(`Removed stale global MCP "${name}"`);
525
- } catch {}
526
- }
527
- } else {
528
- log('Registering MCP server...');
529
- try {
530
- // Purge legacy "mem" and any pre-existing "mem-lite" before re-registering
531
- for (const name of ['mem', 'mem-lite']) {
532
- try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' }); } catch {}
533
- try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', name], { stdio: 'pipe' }); } catch {}
534
- }
535
- execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem-lite', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
536
- ok('MCP server registered: mem-lite');
537
- } catch (e) {
538
- fail('MCP registration failed: ' + e.message);
539
- warn('Try manually: claude mcp add -s user -t stdio mem-lite -- node ' + SERVER_PATH);
531
+ try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' }); } catch {}
532
+ try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', name], { stdio: 'pipe' }); } catch {}
540
533
  }
534
+ execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem-lite', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
535
+ ok('MCP server registered: mem-lite');
536
+ } catch (e) {
537
+ fail('MCP registration failed: ' + e.message);
538
+ warn('Try manually: claude mcp add -s user -t stdio mem-lite -- node ' + SERVER_PATH);
541
539
  }
540
+ }
541
+ }
542
542
 
543
- // 3b. Deduplicate: if marketplace plugin also registers MCP + hooks,
544
- // clear them to prevent double execution. install.mjs hooks (in settings.json)
545
- // point to ~/.claude-mem-lite/ (latest code in dev mode via symlinks),
546
- // while plugin hooks use ${CLAUDE_PLUGIN_ROOT} (potentially stale marketplace copy).
547
- //
548
- // MCP dedup: Claude Code copies .mcp.json from marketplace clone → plugin cache.
549
- // Do NOT modify marketplace .mcp.json it breaks the MCP server registration chain.
550
- // Dedup is handled by skipping global `claude mcp add` when plugin system is active.
551
- const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY);
552
- const pluginHooksPath = join(pluginDir, 'hooks', 'hooks.json');
553
-
554
- if (existsSync(pluginDir)) {
555
- // NOTE: Do NOT clear marketplace .mcp.json — Claude Code copies from
556
- // marketplace clone plugin cache on updates. Clearing it causes the
557
- // cache .mcp.json to lose the MCP server definition, breaking plugin MCP.
558
- // Dedup is already handled by skipping global `claude mcp add` above.
559
-
560
- // Clear plugin hooks to prevent double hook execution
561
- try {
562
- if (existsSync(pluginHooksPath)) {
563
- const pluginHooks = JSON.parse(readFileSync(pluginHooksPath, 'utf8'));
564
- if (pluginHooks.hooks && Object.keys(pluginHooks.hooks).length > 0) {
565
- writeFileSync(pluginHooksPath, JSON.stringify({
566
- description: pluginHooks.description || 'claude-mem-lite hooks',
567
- _note: 'Hooks managed by install.mjs in settings.json — this file cleared to prevent duplicates',
568
- hooks: {}
569
- }, null, 2) + '\n');
570
- ok('Marketplace plugin: hooks cleared (prevents duplicate)');
571
- }
543
+ function dedupePluginCacheAndHooks() {
544
+ // 3b. Deduplicate: if marketplace plugin also registers MCP + hooks,
545
+ // clear them to prevent double execution. install.mjs hooks (in settings.json)
546
+ // point to ~/.claude-mem-lite/ (latest code in dev mode via symlinks),
547
+ // while plugin hooks use ${CLAUDE_PLUGIN_ROOT} (potentially stale marketplace copy).
548
+ //
549
+ // MCP dedup: Claude Code copies .mcp.json from marketplace clone plugin cache.
550
+ // Do NOT modify marketplace .mcp.json it breaks the MCP server registration chain.
551
+ // Dedup is handled by skipping global `claude mcp add` when plugin system is active.
552
+ const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY);
553
+ const pluginHooksPath = join(pluginDir, 'hooks', 'hooks.json');
554
+
555
+ if (existsSync(pluginDir)) {
556
+ // NOTE: Do NOT clear marketplace .mcp.json Claude Code copies from
557
+ // marketplace clone plugin cache on updates. Clearing it causes the
558
+ // cache .mcp.json to lose the MCP server definition, breaking plugin MCP.
559
+ // Dedup is already handled by skipping global `claude mcp add` above.
560
+
561
+ // Clear plugin hooks to prevent double hook execution
562
+ try {
563
+ if (existsSync(pluginHooksPath)) {
564
+ const pluginHooks = JSON.parse(readFileSync(pluginHooksPath, 'utf8'));
565
+ if (pluginHooks.hooks && Object.keys(pluginHooks.hooks).length > 0) {
566
+ writeFileSync(pluginHooksPath, JSON.stringify({
567
+ description: pluginHooks.description || 'claude-mem-lite hooks',
568
+ _note: 'Hooks managed by install.mjs in settings.json — this file cleared to prevent duplicates',
569
+ hooks: {}
570
+ }, null, 2) + '\n');
571
+ ok('Marketplace plugin: hooks cleared (prevents duplicate)');
572
572
  }
573
- } catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
573
+ }
574
+ } catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
574
575
 
575
- // Sync launch.mjs to plugin cache — ensures MCP server loads dev code via symlink detection.
576
- // ALSO clear cached hooks.json in every version dir — Claude Code runtime reads hooks from
577
- // ~/.claude/plugins/cache/<mp>/<plugin>/<ver>/hooks/hooks.json, NOT from the marketplace source.
578
- // Clearing only the marketplace source (above) leaves stale cache copies that double-register
579
- // hooks alongside install.mjs-written settings.json entries.
580
- try {
581
- const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
582
- if (existsSync(cacheBase)) {
583
- const launchSyncFiles = ['launch.mjs', 'launch-preflight.mjs'];
584
- let clearedHooks = 0;
585
- for (const ver of readdirSync(cacheBase)) {
586
- const verDir = join(cacheBase, ver);
587
-
588
- // Sync launch.mjs + its preflight companion (issue #15)
589
- if (existsSync(join(verDir, 'scripts'))) {
590
- for (const f of launchSyncFiles) {
591
- const src = join(PROJECT_DIR, 'scripts', f);
592
- if (existsSync(src)) {
593
- try { copyFileSync(src, join(verDir, 'scripts', f)); } catch { /* keep going */ }
594
- }
576
+ // Sync launch.mjs to plugin cache — ensures MCP server loads dev code via symlink detection.
577
+ // ALSO clear cached hooks.json in every version dir — Claude Code runtime reads hooks from
578
+ // ~/.claude/plugins/cache/<mp>/<plugin>/<ver>/hooks/hooks.json, NOT from the marketplace source.
579
+ // Clearing only the marketplace source (above) leaves stale cache copies that double-register
580
+ // hooks alongside install.mjs-written settings.json entries.
581
+ try {
582
+ const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
583
+ if (existsSync(cacheBase)) {
584
+ const launchSyncFiles = ['launch.mjs', 'launch-preflight.mjs'];
585
+ let clearedHooks = 0;
586
+ for (const ver of readdirSync(cacheBase)) {
587
+ const verDir = join(cacheBase, ver);
588
+
589
+ // Sync launch.mjs + its preflight companion (issue #15)
590
+ if (existsSync(join(verDir, 'scripts'))) {
591
+ for (const f of launchSyncFiles) {
592
+ const src = join(PROJECT_DIR, 'scripts', f);
593
+ if (existsSync(src)) {
594
+ try { copyFileSync(src, join(verDir, 'scripts', f)); } catch { /* keep going */ }
595
595
  }
596
596
  }
597
+ }
597
598
 
598
- // Clear cached hooks.json (runtime reads here, not marketplace source)
599
- const cachedHooksPath = join(verDir, 'hooks', 'hooks.json');
600
- if (existsSync(cachedHooksPath)) {
601
- try {
602
- const h = JSON.parse(readFileSync(cachedHooksPath, 'utf8'));
603
- if (h.hooks && Object.keys(h.hooks).length > 0) {
604
- writeFileSync(cachedHooksPath, JSON.stringify({
605
- description: h.description || 'claude-mem-lite hooks',
606
- _note: `Hooks managed by install.mjs in settings.json — cache hooks.json cleared to prevent duplicate registration (cache ver: ${ver})`,
607
- hooks: {}
608
- }, null, 2) + '\n');
609
- clearedHooks++;
610
- }
611
- } catch { /* silent — never block install on one bad cache entry */ }
612
- }
599
+ // Clear cached hooks.json (runtime reads here, not marketplace source)
600
+ const cachedHooksPath = join(verDir, 'hooks', 'hooks.json');
601
+ if (existsSync(cachedHooksPath)) {
602
+ try {
603
+ const h = JSON.parse(readFileSync(cachedHooksPath, 'utf8'));
604
+ if (h.hooks && Object.keys(h.hooks).length > 0) {
605
+ writeFileSync(cachedHooksPath, JSON.stringify({
606
+ description: h.description || 'claude-mem-lite hooks',
607
+ _note: `Hooks managed by install.mjs in settings.json — cache hooks.json cleared to prevent duplicate registration (cache ver: ${ver})`,
608
+ hooks: {}
609
+ }, null, 2) + '\n');
610
+ clearedHooks++;
611
+ }
612
+ } catch { /* silent — never block install on one bad cache entry */ }
613
613
  }
614
- const parts = ['launch.mjs synced (dev mode MCP routing)'];
615
- if (clearedHooks > 0) parts.push(`${clearedHooks} stale hooks.json cleared`);
616
- ok(`Plugin cache: ${parts.join('; ')}`);
617
614
  }
618
- } catch (e) { warn(`Plugin cache sync: ${e.message}`); }
619
- }
615
+ const parts = ['launch.mjs synced (dev mode MCP routing)'];
616
+ if (clearedHooks > 0) parts.push(`${clearedHooks} stale hooks.json cleared`);
617
+ ok(`Plugin cache: ${parts.join('; ')}`);
618
+ }
619
+ } catch (e) { warn(`Plugin cache sync: ${e.message}`); }
620
+ }
621
+ }
620
622
 
621
- // 4. Configure hooks (merge: preserve user's existing hooks, replace ours)
622
- log('Configuring hooks...');
623
- const settings = readSettings();
624
- if (clearPluginDisabledMarkerForDirectInstall(settings)) {
625
- ok('Cleared stale disabled plugin flag so install.mjs-managed hooks can run');
626
- }
627
- settings.hooks = settings.hooks || {};
628
-
629
- const SCRIPTS_PATH = join(INSTALL_DIR, 'scripts');
630
- const PREFILTER_PATH = join(SCRIPTS_PATH, 'post-tool-use.sh');
631
- // v2.84: every Node hook invocation routes through hook-launcher.mjs so an
632
- // ERR_MODULE_NOT_FOUND from a partial-install drift auto-heals via
633
- // install.mjs repair instead of permanently bricking the hook chain.
634
- const LAUNCHER_PATH = join(SCRIPTS_PATH, 'hook-launcher.mjs');
635
- const nodeHook = (entry, ...args) => `node "${LAUNCHER_PATH}" ${entry} ${args.join(' ')}`.trim();
636
-
637
- const memPostToolUse = {
638
- matcher: '*',
639
- hooks: [{
623
+ function configureHooks() {
624
+ // 4. Configure hooks (merge: preserve user's existing hooks, replace ours)
625
+ log('Configuring hooks...');
626
+ const settings = readSettings();
627
+ if (clearPluginDisabledMarkerForDirectInstall(settings)) {
628
+ ok('Cleared stale disabled plugin flag so install.mjs-managed hooks can run');
629
+ }
630
+ settings.hooks = settings.hooks || {};
631
+
632
+ const SCRIPTS_PATH = join(INSTALL_DIR, 'scripts');
633
+ const PREFILTER_PATH = join(SCRIPTS_PATH, 'post-tool-use.sh');
634
+ // v2.84: every Node hook invocation routes through hook-launcher.mjs so an
635
+ // ERR_MODULE_NOT_FOUND from a partial-install drift auto-heals via
636
+ // install.mjs repair instead of permanently bricking the hook chain.
637
+ const LAUNCHER_PATH = join(SCRIPTS_PATH, 'hook-launcher.mjs');
638
+ const nodeHook = (entry, ...args) => `node "${LAUNCHER_PATH}" ${entry} ${args.join(' ')}`.trim();
639
+
640
+ const memPostToolUse = {
641
+ matcher: '*',
642
+ hooks: [{
643
+ type: 'command',
644
+ command: `bash "${PREFILTER_PATH}"`,
645
+ timeout: 5
646
+ }]
647
+ };
648
+
649
+ const memSessionStart = {
650
+ matcher: 'startup|clear|compact',
651
+ hooks: [{
652
+ type: 'command',
653
+ command: nodeHook('hook.mjs', 'session-start'),
654
+ timeout: 10
655
+ }]
656
+ };
657
+
658
+ const memStop = {
659
+ matcher: '*',
660
+ hooks: [{
661
+ type: 'command',
662
+ command: nodeHook('hook.mjs', 'stop'),
663
+ timeout: 5
664
+ }]
665
+ };
666
+
667
+ const memUserPrompt = {
668
+ matcher: '*',
669
+ hooks: [
670
+ {
671
+ type: 'command',
672
+ command: nodeHook('scripts/user-prompt-search.js'),
673
+ timeout: 2
674
+ },
675
+ {
640
676
  type: 'command',
641
- command: `bash "${PREFILTER_PATH}"`,
677
+ command: nodeHook('hook.mjs', 'user-prompt'),
642
678
  timeout: 5
643
- }]
644
- };
645
-
646
- const memSessionStart = {
647
- matcher: 'startup|clear|compact',
648
- hooks: [{
679
+ }
680
+ ]
681
+ };
682
+
683
+ const memPreToolRecall = {
684
+ // v2.34.6: Read added to cover planning-Read (pre-Edit exploration).
685
+ // Read-path uses a tighter filter (lesson_learned required, top-1,
686
+ // 120-char truncation, silent-on-empty) — see scripts/pre-tool-recall.js.
687
+ matcher: 'Edit|Write|NotebookEdit|Read',
688
+ hooks: [
689
+ {
649
690
  type: 'command',
650
- command: nodeHook('hook.mjs', 'session-start'),
651
- timeout: 10
652
- }]
653
- };
691
+ command: nodeHook('scripts/pre-tool-recall.js'),
692
+ timeout: 3
693
+ }
694
+ ]
695
+ };
654
696
 
655
- const memStop = {
656
- matcher: '*',
657
- hooks: [{
697
+ const memPreSkillBridge = {
698
+ matcher: 'Skill',
699
+ hooks: [
700
+ {
658
701
  type: 'command',
659
- command: nodeHook('hook.mjs', 'stop'),
660
- timeout: 5
661
- }]
662
- };
663
-
664
- const memUserPrompt = {
665
- matcher: '*',
666
- hooks: [
667
- {
668
- type: 'command',
669
- command: nodeHook('scripts/user-prompt-search.js'),
670
- timeout: 2
671
- },
672
- {
673
- type: 'command',
674
- command: nodeHook('hook.mjs', 'user-prompt'),
675
- timeout: 5
676
- }
677
- ]
678
- };
679
-
680
- const memPreToolRecall = {
681
- // v2.34.6: Read added to cover planning-Read (pre-Edit exploration).
682
- // Read-path uses a tighter filter (lesson_learned required, top-1,
683
- // 120-char truncation, silent-on-empty) — see scripts/pre-tool-recall.js.
684
- matcher: 'Edit|Write|NotebookEdit|Read',
685
- hooks: [
686
- {
687
- type: 'command',
688
- command: nodeHook('scripts/pre-tool-recall.js'),
689
- timeout: 3
690
- }
691
- ]
692
- };
702
+ command: nodeHook('scripts/pre-skill-bridge.js'),
703
+ timeout: 3
704
+ }
705
+ ]
706
+ };
707
+
708
+ // Filter out existing mem hooks, then append fresh ones
709
+ // PreToolUse has two separate matchers, so we register both
710
+ const hookConfigs = {
711
+ PreToolUse: [memPreToolRecall, memPreSkillBridge],
712
+ PostToolUse: [memPostToolUse],
713
+ SessionStart: [memSessionStart],
714
+ Stop: [memStop],
715
+ UserPromptSubmit: [memUserPrompt],
716
+ };
717
+
718
+ for (const [event, configs] of Object.entries(hookConfigs)) {
719
+ const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event].filter(cfg => !isMemHook(cfg)) : [];
720
+ settings.hooks[event] = [...existing, ...configs];
721
+ }
693
722
 
694
- const memPreSkillBridge = {
695
- matcher: 'Skill',
696
- hooks: [
697
- {
698
- type: 'command',
699
- command: nodeHook('scripts/pre-skill-bridge.js'),
700
- timeout: 3
701
- }
702
- ]
703
- };
723
+ writeSettings(settings);
724
+ ok('Hooks configured (PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit)');
725
+ }
704
726
 
705
- // Filter out existing mem hooks, then append fresh ones
706
- // PreToolUse has two separate matchers, so we register both
707
- const hookConfigs = {
708
- PreToolUse: [memPreToolRecall, memPreSkillBridge],
709
- PostToolUse: [memPostToolUse],
710
- SessionStart: [memSessionStart],
711
- Stop: [memStop],
712
- UserPromptSubmit: [memUserPrompt],
713
- };
727
+ function backupLegacyClaudeMemData() {
728
+ // 5. Legacy ~/.claude-mem/ ~/.claude-mem-lite/ back up, don't reuse.
729
+ // The legacy DB is schema v16 (schema_versions plural) and there's no
730
+ // bridge in MIGRATIONS[] to v28. Reusing it FATALs on first launch with
731
+ // "no such column: memory_session_id". Rename to a timestamped backup
732
+ // so the new install creates a fresh v28 DB.
733
+ try {
734
+ const r = migrateLegacyClaudeMemData(OLD_DATA_DIR, MEM_DATA_DIR);
735
+ if (r.action === 'backed-up') {
736
+ ok(`Legacy ~/.claude-mem/ DB backed up to ${r.backupPath}`);
737
+ log('New v28 DB will be created on first launch (legacy schema is incompatible).');
738
+ }
739
+ } catch (e) {
740
+ warn('Legacy DB backup failed: ' + e.message);
741
+ }
714
742
 
715
- for (const [event, configs] of Object.entries(hookConfigs)) {
716
- const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event].filter(cfg => !isMemHook(cfg)) : [];
717
- settings.hooks[event] = [...existing, ...configs];
743
+ // 5b. Rename claude-mem.db claude-mem-lite.db in same directory
744
+ const oldDbInDir = join(MEM_DATA_DIR, 'claude-mem.db');
745
+ if (existsSync(oldDbInDir) && !existsSync(DB_PATH)) {
746
+ renameSync(oldDbInDir, DB_PATH);
747
+ for (const ext of ['-wal', '-shm']) {
748
+ if (existsSync(oldDbInDir + ext)) try { renameSync(oldDbInDir + ext, DB_PATH + ext); } catch {}
718
749
  }
750
+ ok('Database renamed: claude-mem.db → claude-mem-lite.db');
751
+ }
752
+ }
719
753
 
720
- writeSettings(settings);
721
- ok('Hooks configured (PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit)');
722
-
723
- // 5. Legacy ~/.claude-mem/ ~/.claude-mem-lite/ — back up, don't reuse.
724
- // The legacy DB is schema v16 (schema_versions plural) and there's no
725
- // bridge in MIGRATIONS[] to v28. Reusing it FATALs on first launch with
726
- // "no such column: memory_session_id". Rename to a timestamped backup
727
- // so the new install creates a fresh v28 DB.
728
- try {
729
- const r = migrateLegacyClaudeMemData(OLD_DATA_DIR, MEM_DATA_DIR);
730
- if (r.action === 'backed-up') {
731
- ok(`Legacy ~/.claude-mem/ DB backed up to ${r.backupPath}`);
732
- log('New v28 DB will be created on first launch (legacy schema is incompatible).');
754
+ async function installPreinstalledResources() {
755
+ // 6. Install pre-installed resources (skills + agents)
756
+ if (process.env.CLAUDE_MEM_SKIP_REPOS) {
757
+ ok('Skill/agent registry: skipped (CLAUDE_MEM_SKIP_REPOS)');
758
+ } else try {
759
+ const manifestPath = join(INSTALL_DIR, 'registry', 'preinstalled.json');
760
+ if (!existsSync(manifestPath)) {
761
+ // For git-clone mode, check PROJECT_DIR
762
+ const altPath = join(PROJECT_DIR, 'registry', 'preinstalled.json');
763
+ if (existsSync(altPath)) {
764
+ const registryDir = join(INSTALL_DIR, 'registry');
765
+ if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
766
+ copyFileSync(altPath, manifestPath);
733
767
  }
734
- } catch (e) {
735
- warn('Legacy DB backup failed: ' + e.message);
736
768
  }
737
769
 
738
- // 5b. Rename claude-mem.db → claude-mem-lite.db in same directory
739
- const oldDbInDir = join(MEM_DATA_DIR, 'claude-mem.db');
740
- if (existsSync(oldDbInDir) && !existsSync(DB_PATH)) {
741
- renameSync(oldDbInDir, DB_PATH);
742
- for (const ext of ['-wal', '-shm']) {
743
- if (existsSync(oldDbInDir + ext)) try { renameSync(oldDbInDir + ext, DB_PATH + ext); } catch {}
744
- }
745
- ok('Database renamed: claude-mem.db → claude-mem-lite.db');
746
- }
747
-
748
- // 6. Install pre-installed resources (skills + agents)
749
- if (process.env.CLAUDE_MEM_SKIP_REPOS) {
750
- ok('Skill/agent registry: skipped (CLAUDE_MEM_SKIP_REPOS)');
751
- } else try {
752
- const manifestPath = join(INSTALL_DIR, 'registry', 'preinstalled.json');
753
- if (!existsSync(manifestPath)) {
754
- // For git-clone mode, check PROJECT_DIR
755
- const altPath = join(PROJECT_DIR, 'registry', 'preinstalled.json');
756
- if (existsSync(altPath)) {
757
- const registryDir = join(INSTALL_DIR, 'registry');
758
- if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
759
- copyFileSync(altPath, manifestPath);
760
- }
761
- }
762
-
763
- if (existsSync(manifestPath)) {
764
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
765
- const resources = manifest.resources || [];
770
+ if (existsSync(manifestPath)) {
771
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
772
+ const resources = manifest.resources || [];
766
773
 
767
- if (resources.length > 0) {
768
- const managedDir = join(MEM_DATA_DIR, 'managed');
774
+ if (resources.length > 0) {
775
+ const managedDir = join(MEM_DATA_DIR, 'managed');
769
776
 
770
- // 6a. Git shallow clone unique repos
771
- const repos = new Map();
772
- for (const r of resources) {
773
- if (!repos.has(r.repo)) repos.set(r.repo, []);
774
- repos.get(r.repo).push(r);
775
- }
776
-
777
- let cloned = 0, updated = 0;
778
- const deadRepos = new Set(); // repos that no longer exist (404)
777
+ // 6a. Git shallow clone unique repos
778
+ const repos = new Map();
779
+ for (const r of resources) {
780
+ if (!repos.has(r.repo)) repos.set(r.repo, []);
781
+ repos.get(r.repo).push(r);
782
+ }
779
783
 
780
- const isRepoNotFound = (err) => {
781
- const msg = (err?.stderr ? err.stderr.toString() : '') + (err?.message || '');
782
- return /repository.*not found|404/i.test(msg);
784
+ let cloned = 0, updated = 0;
785
+ const deadRepos = new Set(); // repos that no longer exist (404)
786
+
787
+ const isRepoNotFound = (err) => {
788
+ const msg = (err?.stderr ? err.stderr.toString() : '') + (err?.message || '');
789
+ return /repository.*not found|404/i.test(msg);
790
+ };
791
+
792
+ for (const [repoUrl, entries] of repos) {
793
+ const repoName = repoUrl.split('/').slice(-2).join('-').replace(/[^a-zA-Z0-9._-]/g, '_');
794
+ const clonePath = join(managedDir, 'repos', repoName);
795
+ let repoReady = false;
796
+
797
+ const plan = planRepoSparsePaths(entries);
798
+ const cloneUrl = `${repoUrl.replace(/\.git$/, '')}.git`;
799
+ // Clone only what we extract: a partial (blob:none) + sparse clone fetches
800
+ // just the manifest subpaths' subtrees instead of the whole repo. Falls
801
+ // back to a plain shallow clone if partial-clone/sparse-checkout is
802
+ // unsupported (old git/server) — identical to the prior behavior.
803
+ const cloneSlim = () => {
804
+ if (plan.full) {
805
+ execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
806
+ return;
807
+ }
808
+ try {
809
+ execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
810
+ execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 });
811
+ execFileSync('git', ['-C', clonePath, 'checkout'], { stdio: 'pipe', timeout: 30000 });
812
+ } catch {
813
+ try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
814
+ execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
815
+ }
783
816
  };
784
817
 
785
- for (const [repoUrl, entries] of repos) {
786
- const repoName = repoUrl.split('/').slice(-2).join('-').replace(/[^a-zA-Z0-9._-]/g, '_');
787
- const clonePath = join(managedDir, 'repos', repoName);
788
- let repoReady = false;
789
-
790
- const plan = planRepoSparsePaths(entries);
791
- const cloneUrl = `${repoUrl.replace(/\.git$/, '')}.git`;
792
- // Clone only what we extract: a partial (blob:none) + sparse clone fetches
793
- // just the manifest subpaths' subtrees instead of the whole repo. Falls
794
- // back to a plain shallow clone if partial-clone/sparse-checkout is
795
- // unsupported (old git/server) — identical to the prior behavior.
796
- const cloneSlim = () => {
797
- if (plan.full) {
798
- execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
799
- return;
800
- }
801
- try {
802
- execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
803
- execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 });
804
- execFileSync('git', ['-C', clonePath, 'checkout'], { stdio: 'pipe', timeout: 30000 });
805
- } catch {
806
- try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
807
- execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
808
- }
809
- };
818
+ // Migrate a legacy full clone: drop it so the fresh-clone path below
819
+ // rebuilds it slim. managed/repos is a rebuildable cache, so this loses
820
+ // nothing and reclaims the bulk of its footprint on the next install run.
821
+ if (!plan.full && existsSync(clonePath) && !isPartialSparseClone(clonePath)) {
822
+ try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
823
+ }
810
824
 
811
- // Migrate a legacy full clone: drop it so the fresh-clone path below
812
- // rebuilds it slim. managed/repos is a rebuildable cache, so this loses
813
- // nothing and reclaims the bulk of its footprint on the next install run.
814
- if (!plan.full && existsSync(clonePath) && !isPartialSparseClone(clonePath)) {
815
- try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
825
+ if (!existsSync(clonePath)) {
826
+ // Fresh clone (also the rebuild path for a just-migrated legacy clone)
827
+ try {
828
+ mkdirSync(join(managedDir, 'repos'), { recursive: true });
829
+ cloneSlim();
830
+ cloned++;
831
+ repoReady = true;
832
+ } catch (err) {
833
+ if (isRepoNotFound(err)) {
834
+ deadRepos.add(repoUrl);
835
+ warn(` Repo not found (removed?): ${repoUrl}`);
836
+ } else {
837
+ warn(` Clone failed: ${repoUrl}`);
838
+ }
839
+ continue;
816
840
  }
817
-
818
- if (!existsSync(clonePath)) {
819
- // Fresh clone (also the rebuild path for a just-migrated legacy clone)
820
- try {
821
- mkdirSync(join(managedDir, 'repos'), { recursive: true });
822
- cloneSlim();
823
- cloned++;
824
- repoReady = true;
825
- } catch (err) {
826
- if (isRepoNotFound(err)) {
827
- deadRepos.add(repoUrl);
828
- warn(` Repo not found (removed?): ${repoUrl}`);
829
- } else {
830
- warn(` Clone failed: ${repoUrl}`);
831
- }
832
- continue;
841
+ } else {
842
+ // Update existing: fetch latest and fast-forward
843
+ try {
844
+ // Re-assert the sparse set so a newer manifest that adds a subpath to
845
+ // an already-slim clone checks it out (idempotent; no-op for full clones).
846
+ if (!plan.full && isPartialSparseClone(clonePath)) {
847
+ try { execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 }); } catch {}
833
848
  }
834
- } else {
835
- // Update existing: fetch latest and fast-forward
836
- try {
837
- // Re-assert the sparse set so a newer manifest that adds a subpath to
838
- // an already-slim clone checks it out (idempotent; no-op for full clones).
839
- if (!plan.full && isPartialSparseClone(clonePath)) {
840
- try { execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 }); } catch {}
841
- }
842
- const localHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
843
- execFileSync('git', ['-C', clonePath, 'fetch', '--depth', '1', 'origin'], { stdio: 'pipe', timeout: 30000 });
844
- const remoteHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'FETCH_HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
845
- if (localHash !== remoteHash) {
846
- execFileSync('git', ['-C', clonePath, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'pipe' });
847
- updated++;
848
- repoReady = true; // needs re-copy
849
- }
850
- } catch (err) {
851
- if (isRepoNotFound(err)) {
852
- deadRepos.add(repoUrl);
853
- warn(` Repo not found (removed?): ${repoUrl} — cleaning up`);
854
- // Remove local clone
855
- try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
856
- // Remove extracted resources
857
- for (const entry of entries) {
858
- const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
859
- const destPath = join(destDir, entry.name);
860
- try { if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true }); } catch {}
861
- }
862
- continue;
863
- }
864
- // Transient failure — use existing clone as-is
849
+ const localHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
850
+ execFileSync('git', ['-C', clonePath, 'fetch', '--depth', '1', 'origin'], { stdio: 'pipe', timeout: 30000 });
851
+ const remoteHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'FETCH_HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
852
+ if (localHash !== remoteHash) {
853
+ execFileSync('git', ['-C', clonePath, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'pipe' });
854
+ updated++;
855
+ repoReady = true; // needs re-copy
865
856
  }
866
- }
867
-
868
- // Copy resources to managed/skills/ or managed/agents/
869
- // Re-copy if repo was freshly cloned or updated
870
- mkdirSync(join(managedDir, 'skills'), { recursive: true });
871
- mkdirSync(join(managedDir, 'agents'), { recursive: true });
872
- for (const entry of entries) {
873
- // Path traversal guard: reject entries with '..' or absolute paths
874
- if (entry.path.includes('..') || entry.name.includes('..') ||
875
- isAbsolute(entry.path) || isAbsolute(entry.name)) continue;
876
- const srcPath = entry.path === '.' ? clonePath : join(clonePath, entry.path);
877
- const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
878
- const destPath = join(destDir, entry.name);
879
- if (existsSync(srcPath) && (repoReady || !existsSync(destPath))) {
880
- try {
881
- if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true });
882
- cpSync(srcPath, destPath, { recursive: true });
883
- } catch {}
857
+ } catch (err) {
858
+ if (isRepoNotFound(err)) {
859
+ deadRepos.add(repoUrl);
860
+ warn(` Repo not found (removed?): ${repoUrl} cleaning up`);
861
+ // Remove local clone
862
+ try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
863
+ // Remove extracted resources
864
+ for (const entry of entries) {
865
+ const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
866
+ const destPath = join(destDir, entry.name);
867
+ try { if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true }); } catch {}
868
+ }
869
+ continue;
884
870
  }
871
+ // Transient failure — use existing clone as-is
885
872
  }
886
873
  }
887
- ok(`Repos: ${cloned} cloned, ${updated} updated, ${repos.size - deadRepos.size} active` +
888
- (deadRepos.size > 0 ? `, ${deadRepos.size} dead removed` : ''));
889
-
890
- // 6b. Init registry DB and record preinstalled entries
891
- const { ensureRegistryDb } = await importFromInstall('registry.mjs');
892
- const regDbPath = join(MEM_DATA_DIR, 'resource-registry.db');
893
- const rdb = ensureRegistryDb(regDbPath);
894
-
895
- const insertPre = rdb.prepare(`
896
- INSERT OR REPLACE INTO preinstalled (name, type, repo_url, repo_path, tags, enabled)
897
- VALUES (?, ?, ?, ?, ?, 1)
898
- `);
899
- const activeResources = deadRepos.size > 0
900
- ? resources.filter(r => !deadRepos.has(r.repo))
901
- : resources;
902
- for (const r of activeResources) {
903
- insertPre.run(r.name, r.type, r.repo, r.path, JSON.stringify(r.tags || []));
904
- }
905
874
 
906
- // Clean up DB entries for dead repos
907
- if (deadRepos.size > 0) {
908
- const delPre = rdb.prepare('DELETE FROM preinstalled WHERE repo_url = ?');
909
- const delRes = rdb.prepare('DELETE FROM resources WHERE repo_url = ?');
910
- for (const deadUrl of deadRepos) {
911
- try { delPre.run(deadUrl); } catch {}
912
- try { delRes.run(deadUrl); } catch {}
913
- }
914
- }
915
- ok(`Registry DB initialized (${activeResources.length} preinstalled entries` +
916
- (deadRepos.size > 0 ? `, ${deadRepos.size} dead repos purged` : '') + ')');
917
-
918
- // 6c. Fetch GitHub stars (best-effort, unauthenticated)
919
- log(' Fetching GitHub stars...');
920
- const starCache = new Map();
921
- for (const [repoUrl] of repos) {
922
- if (deadRepos.has(repoUrl)) continue;
923
- const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
924
- if (match) {
875
+ // Copy resources to managed/skills/ or managed/agents/
876
+ // Re-copy if repo was freshly cloned or updated
877
+ mkdirSync(join(managedDir, 'skills'), { recursive: true });
878
+ mkdirSync(join(managedDir, 'agents'), { recursive: true });
879
+ for (const entry of entries) {
880
+ // Path traversal guard: reject entries with '..' or absolute paths
881
+ if (entry.path.includes('..') || entry.name.includes('..') ||
882
+ isAbsolute(entry.path) || isAbsolute(entry.name)) continue;
883
+ const srcPath = entry.path === '.' ? clonePath : join(clonePath, entry.path);
884
+ const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
885
+ const destPath = join(destDir, entry.name);
886
+ if (existsSync(srcPath) && (repoReady || !existsSync(destPath))) {
925
887
  try {
926
- const apiUrl = `https://api.github.com/repos/${match[1]}/${match[2]}`;
927
- const res = execFileSync('curl', ['-sf', apiUrl], { encoding: 'utf8', timeout: 10000 });
928
- const data = JSON.parse(res);
929
- if (typeof data.stargazers_count === 'number') {
930
- starCache.set(repoUrl, data.stargazers_count);
931
- }
888
+ if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true });
889
+ cpSync(srcPath, destPath, { recursive: true });
932
890
  } catch {}
933
891
  }
934
892
  }
935
- if (starCache.size > 0) ok(`Stars fetched (${starCache.size}/${repos.size} repos)`);
936
-
937
- // 6d. Scan and index resources (fallback-only, Haiku indexing deferred to first run)
938
- log(' Scanning resources...');
939
- const { scanAllResources, diffResources } = await importFromInstall('registry-scanner.mjs');
940
- const scanned = scanAllResources({ dataDir: MEM_DATA_DIR });
941
-
942
- // Attach star counts and repo URLs
943
- for (const s of scanned) {
944
- const entry = resources.find(r => r.name === s.name && r.type === s.type);
945
- if (entry) {
946
- s.repoUrl = entry.repo;
947
- s.repoStars = starCache.get(entry.repo) || 0;
948
- }
893
+ }
894
+ ok(`Repos: ${cloned} cloned, ${updated} updated, ${repos.size - deadRepos.size} active` +
895
+ (deadRepos.size > 0 ? `, ${deadRepos.size} dead removed` : ''));
896
+
897
+ // 6b. Init registry DB and record preinstalled entries
898
+ const { ensureRegistryDb } = await importFromInstall('registry.mjs');
899
+ const regDbPath = join(MEM_DATA_DIR, 'resource-registry.db');
900
+ const rdb = ensureRegistryDb(regDbPath);
901
+
902
+ const insertPre = rdb.prepare(`
903
+ INSERT OR REPLACE INTO preinstalled (name, type, repo_url, repo_path, tags, enabled)
904
+ VALUES (?, ?, ?, ?, ?, 1)
905
+ `);
906
+ const activeResources = deadRepos.size > 0
907
+ ? resources.filter(r => !deadRepos.has(r.repo))
908
+ : resources;
909
+ for (const r of activeResources) {
910
+ insertPre.run(r.name, r.type, r.repo, r.path, JSON.stringify(r.tags || []));
911
+ }
912
+
913
+ // Clean up DB entries for dead repos
914
+ if (deadRepos.size > 0) {
915
+ const delPre = rdb.prepare('DELETE FROM preinstalled WHERE repo_url = ?');
916
+ const delRes = rdb.prepare('DELETE FROM resources WHERE repo_url = ?');
917
+ for (const deadUrl of deadRepos) {
918
+ try { delPre.run(deadUrl); } catch {}
919
+ try { delRes.run(deadUrl); } catch {}
920
+ }
921
+ }
922
+ ok(`Registry DB initialized (${activeResources.length} preinstalled entries` +
923
+ (deadRepos.size > 0 ? `, ${deadRepos.size} dead repos purged` : '') + ')');
924
+
925
+ // 6c. Fetch GitHub stars (best-effort, unauthenticated)
926
+ log(' Fetching GitHub stars...');
927
+ const starCache = new Map();
928
+ for (const [repoUrl] of repos) {
929
+ if (deadRepos.has(repoUrl)) continue;
930
+ const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
931
+ if (match) {
932
+ try {
933
+ const apiUrl = `https://api.github.com/repos/${match[1]}/${match[2]}`;
934
+ const res = execFileSync('curl', ['-sf', apiUrl], { encoding: 'utf8', timeout: 10000 });
935
+ const data = JSON.parse(res);
936
+ if (typeof data.stargazers_count === 'number') {
937
+ starCache.set(repoUrl, data.stargazers_count);
938
+ }
939
+ } catch {}
940
+ }
941
+ }
942
+ if (starCache.size > 0) ok(`Stars fetched (${starCache.size}/${repos.size} repos)`);
943
+
944
+ // 6d. Scan and index resources (fallback-only, Haiku indexing deferred to first run)
945
+ log(' Scanning resources...');
946
+ const { scanAllResources, diffResources } = await importFromInstall('registry-scanner.mjs');
947
+ const scanned = scanAllResources({ dataDir: MEM_DATA_DIR });
948
+
949
+ // Attach star counts and repo URLs
950
+ for (const s of scanned) {
951
+ const entry = resources.find(r => r.name === s.name && r.type === s.type);
952
+ if (entry) {
953
+ s.repoUrl = entry.repo;
954
+ s.repoStars = starCache.get(entry.repo) || 0;
949
955
  }
956
+ }
950
957
 
951
- const { toIndex } = diffResources(rdb, scanned);
952
- if (toIndex.length > 0) {
953
- // Use fallback indexing at install time (no Haiku calls)
954
- // Full Haiku indexing happens on first SessionStart
955
- const { upsertResource } = await importFromInstall('registry.mjs');
956
- for (const res of toIndex) {
957
- try {
958
- const metaKey = `${res.type}:${res.name}`;
959
- const meta = RESOURCE_METADATA[metaKey];
960
- upsertResource(rdb, {
961
- name: res.name,
962
- type: res.type,
963
- status: 'active',
964
- source: res.source,
965
- repo_url: res.repoUrl || null,
966
- repo_stars: res.repoStars || 0,
967
- local_path: res.localPath,
968
- file_hash: res.fileHash,
969
- invocation_name: meta?.invocation_name || deriveInvocationName(res.name),
970
- intent_tags: meta?.intent_tags || res.name.replace(/-/g, ' '),
971
- domain_tags: meta?.domain_tags || '',
972
- trigger_patterns: meta?.trigger_patterns || `when user needs ${res.name.replace(/-/g, ' ')}`,
973
- capability_summary: meta?.capability_summary || `${res.type}: ${res.name.replace(/-/g, ' ')}`,
974
- });
975
- } catch {}
976
- }
977
- ok(`Resources registered: ${toIndex.length} indexed`);
958
+ const { toIndex } = diffResources(rdb, scanned);
959
+ if (toIndex.length > 0) {
960
+ // Use fallback indexing at install time (no Haiku calls)
961
+ // Full Haiku indexing happens on first SessionStart
962
+ const { upsertResource } = await importFromInstall('registry.mjs');
963
+ for (const res of toIndex) {
964
+ try {
965
+ const metaKey = `${res.type}:${res.name}`;
966
+ const meta = RESOURCE_METADATA[metaKey];
967
+ upsertResource(rdb, {
968
+ name: res.name,
969
+ type: res.type,
970
+ status: 'active',
971
+ source: res.source,
972
+ repo_url: res.repoUrl || null,
973
+ repo_stars: res.repoStars || 0,
974
+ local_path: res.localPath,
975
+ file_hash: res.fileHash,
976
+ invocation_name: meta?.invocation_name || deriveInvocationName(res.name),
977
+ intent_tags: meta?.intent_tags || res.name.replace(/-/g, ' '),
978
+ domain_tags: meta?.domain_tags || '',
979
+ trigger_patterns: meta?.trigger_patterns || `when user needs ${res.name.replace(/-/g, ' ')}`,
980
+ capability_summary: meta?.capability_summary || `${res.type}: ${res.name.replace(/-/g, ' ')}`,
981
+ });
982
+ } catch {}
978
983
  }
984
+ ok(`Resources registered: ${toIndex.length} indexed`);
985
+ }
979
986
 
980
- // Apply curated metadata to all known resources (fixes existing installs)
981
- reindexKnownResources(rdb);
982
- ok('Resource metadata curated (FTS5 reindexed)');
987
+ // Apply curated metadata to all known resources (fixes existing installs)
988
+ reindexKnownResources(rdb);
989
+ ok('Resource metadata curated (FTS5 reindexed)');
983
990
 
984
- // Register plugin resources (skills/agents from other plugins, no local files)
985
- const virtualCount = registerVirtualResources(rdb);
986
- if (virtualCount > 0) ok(`Plugin resources registered: ${virtualCount} virtual entries`);
991
+ // Register plugin resources (skills/agents from other plugins, no local files)
992
+ const virtualCount = registerVirtualResources(rdb);
993
+ if (virtualCount > 0) ok(`Plugin resources registered: ${virtualCount} virtual entries`);
987
994
 
988
- rdb.close();
989
- }
990
- } else {
991
- log(' No preinstalled manifest found, skipping');
995
+ rdb.close();
992
996
  }
993
- } catch (e) {
994
- warn('Resource setup: ' + e.message);
995
- log(' Skills/agents will be indexed on first use');
997
+ } else {
998
+ log(' No preinstalled manifest found, skipping');
996
999
  }
1000
+ } catch (e) {
1001
+ warn('Resource setup: ' + e.message);
1002
+ log(' Skills/agents will be indexed on first use');
1003
+ }
1004
+ }
997
1005
 
998
- // 7. Verify database
999
- if (existsSync(DB_PATH)) {
1000
- try {
1001
- const Database = requireFromInstall('better-sqlite3');
1002
- const db = new Database(DB_PATH, { readonly: true });
1003
- const count = db.prepare('SELECT COUNT(*) as c FROM observations').get();
1004
- db.close();
1005
- ok(`Database accessible: ${count.c} observations`);
1006
- } catch (e) {
1007
- warn('Database check failed: ' + e.message);
1008
- }
1009
- } else {
1010
- log('No existing database — will be created on first use');
1006
+ function verifyDatabase() {
1007
+ // 7. Verify database
1008
+ if (existsSync(DB_PATH)) {
1009
+ try {
1010
+ const Database = requireFromInstall('better-sqlite3');
1011
+ const db = new Database(DB_PATH, { readonly: true });
1012
+ const count = db.prepare('SELECT COUNT(*) as c FROM observations').get();
1013
+ db.close();
1014
+ ok(`Database accessible: ${count.c} observations`);
1015
+ } catch (e) {
1016
+ warn('Database check failed: ' + e.message);
1011
1017
  }
1018
+ } else {
1019
+ log('No existing database — will be created on first use');
1020
+ }
1021
+ }
1012
1022
 
1013
- // 7b. Dogfood auto-adopt (invited-memory, Phase C T13).
1014
- // Only fires when install.mjs is running from the claude-mem-lite source repo
1015
- // itself (detected via git remote match). In npm/npx flows PROJECT_DIR is a
1016
- // cache dir with no git metadata, so this is a no-op for end users.
1017
- // --no-adopt override respected.
1018
- if (!flags.has('--no-adopt')) {
1019
- try {
1020
- const remote = execFileSync('git', ['-C', PROJECT_DIR, 'config', '--get', 'remote.origin.url'], { encoding: 'utf8', stdio: 'pipe' }).trim();
1021
- const isDogfood = /github\.com[:/]sdsrss\/claude-mem-lite(\.git)?$/i.test(remote);
1022
- if (isDogfood) {
1023
- const { cmdAdopt } = await importFromInstall('adopt-cli.mjs');
1024
- cmdAdopt([]);
1025
- ok('Invited-memory: auto-adopt for claude-mem-lite dogfood repo');
1026
- }
1027
- } catch {
1028
- // Not a git repo, or git missing — silent skip (this is the normal npm path).
1023
+ async function dogfoodAutoAdopt() {
1024
+ // 7b. Dogfood auto-adopt (invited-memory, Phase C T13).
1025
+ // Only fires when install.mjs is running from the claude-mem-lite source repo
1026
+ // itself (detected via git remote match). In npm/npx flows PROJECT_DIR is a
1027
+ // cache dir with no git metadata, so this is a no-op for end users.
1028
+ // --no-adopt override respected.
1029
+ if (!flags.has('--no-adopt')) {
1030
+ try {
1031
+ const remote = execFileSync('git', ['-C', PROJECT_DIR, 'config', '--get', 'remote.origin.url'], { encoding: 'utf8', stdio: 'pipe' }).trim();
1032
+ const isDogfood = /github\.com[:/]sdsrss\/claude-mem-lite(\.git)?$/i.test(remote);
1033
+ if (isDogfood) {
1034
+ const { cmdAdopt } = await importFromInstall('adopt-cli.mjs');
1035
+ cmdAdopt([]);
1036
+ ok('Invited-memory: auto-adopt for claude-mem-lite dogfood repo');
1029
1037
  }
1038
+ } catch {
1039
+ // Not a git repo, or git missing — silent skip (this is the normal npm path).
1030
1040
  }
1041
+ }
1042
+ }
1031
1043
 
1032
- // 8. Disable old claude-mem plugin
1033
- if (settings.enabledPlugins?.['claude-mem@thedotmack'] !== undefined) {
1034
- settings.enabledPlugins['claude-mem@thedotmack'] = false;
1035
- writeSettings(settings);
1036
- ok('Old claude-mem plugin disabled');
1037
- }
1044
+ function disableOldClaudeMemPlugin() {
1045
+ const settings = readSettings();
1046
+ // 8. Disable old claude-mem plugin
1047
+ if (settings.enabledPlugins?.['claude-mem@thedotmack'] !== undefined) {
1048
+ settings.enabledPlugins['claude-mem@thedotmack'] = false;
1049
+ writeSettings(settings);
1050
+ ok('Old claude-mem plugin disabled');
1051
+ }
1052
+ }
1038
1053
 
1039
- // 9. Offer to clean old vector-db
1040
- const vectorDbPath = join(OLD_DATA_DIR, 'vector-db');
1041
- if (existsSync(vectorDbPath)) {
1042
- try {
1043
- const size = execFileSync('du', ['-sh', vectorDbPath], { encoding: 'utf8' }).trim().split('\t')[0];
1044
- warn(`Old vector-db exists (${size}). Run: rm -rf ~/.claude-mem/vector-db/`);
1045
- } catch {}
1046
- }
1054
+ function offerCleanOldVectorDb() {
1055
+ // 9. Offer to clean old vector-db
1056
+ const vectorDbPath = join(OLD_DATA_DIR, 'vector-db');
1057
+ if (existsSync(vectorDbPath)) {
1058
+ try {
1059
+ const size = execFileSync('du', ['-sh', vectorDbPath], { encoding: 'utf8' }).trim().split('\t')[0];
1060
+ warn(`Old vector-db exists (${size}). Run: rm -rf ~/.claude-mem/vector-db/`);
1061
+ } catch {}
1062
+ }
1063
+ }
1064
+
1065
+ async function install() {
1066
+ console.log('\nclaude-mem-lite installer\n');
1067
+
1068
+ // 1. Install source files to ~/.claude-mem-lite/
1069
+ const IS_DEV = flags.has('--dev');
1070
+
1071
+ installSourceFiles(IS_DEV);
1072
+ await installDependencies(IS_DEV);
1073
+ createCliSymlink();
1074
+ registerMcpServer();
1075
+ dedupePluginCacheAndHooks();
1076
+ configureHooks();
1077
+ backupLegacyClaudeMemData();
1078
+ await installPreinstalledResources();
1079
+ verifyDatabase();
1080
+ await dogfoodAutoAdopt();
1081
+ disableOldClaudeMemPlugin();
1082
+ offerCleanOldVectorDb();
1047
1083
 
1048
1084
  console.log('\n Done! Restart Claude Code to activate.\n');
1049
1085
  }
1050
1086
 
1087
+
1051
1088
  // ─── Uninstall ──────────────────────────────────────────────────────────────
1052
1089
 
1053
1090
  async function uninstall() {