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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/README.zh-CN.md +1 -1
- package/hook-update.mjs +102 -2
- package/hook.mjs +403 -373
- package/install.mjs +666 -629
- package/lib/doctor-benchmark.mjs +4 -4
- package/lib/release-digest.mjs +106 -0
- package/lib/search-core.mjs +272 -16
- package/mem-cli.mjs +55 -174
- package/package.json +3 -2
- package/schema.mjs +7 -1
- package/scripts/setup.sh +2 -0
- package/search-engine.mjs +1 -1
- package/{server-internals.mjs → search-scoring.mjs} +6 -2
- package/server.mjs +72 -293
- package/source-files.mjs +5 -1
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
if (existsSync(
|
|
488
|
-
symlinkSync(cliSource,
|
|
489
|
-
ok(`CLI: ${
|
|
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
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
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
|
-
}
|
|
573
|
+
}
|
|
574
|
+
} catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
|
|
574
575
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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:
|
|
677
|
+
command: nodeHook('hook.mjs', 'user-prompt'),
|
|
642
678
|
timeout: 5
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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('
|
|
651
|
-
timeout:
|
|
652
|
-
}
|
|
653
|
-
|
|
691
|
+
command: nodeHook('scripts/pre-tool-recall.js'),
|
|
692
|
+
timeout: 3
|
|
693
|
+
}
|
|
694
|
+
]
|
|
695
|
+
};
|
|
654
696
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
697
|
+
const memPreSkillBridge = {
|
|
698
|
+
matcher: 'Skill',
|
|
699
|
+
hooks: [
|
|
700
|
+
{
|
|
658
701
|
type: 'command',
|
|
659
|
-
command: nodeHook('
|
|
660
|
-
timeout:
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
768
|
-
|
|
774
|
+
if (resources.length > 0) {
|
|
775
|
+
const managedDir = join(MEM_DATA_DIR, 'managed');
|
|
769
776
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
812
|
-
//
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
//
|
|
907
|
-
if
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
927
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
987
|
+
// Apply curated metadata to all known resources (fixes existing installs)
|
|
988
|
+
reindexKnownResources(rdb);
|
|
989
|
+
ok('Resource metadata curated (FTS5 reindexed)');
|
|
983
990
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
989
|
-
}
|
|
990
|
-
} else {
|
|
991
|
-
log(' No preinstalled manifest found, skipping');
|
|
995
|
+
rdb.close();
|
|
992
996
|
}
|
|
993
|
-
}
|
|
994
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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() {
|