claude-mem-lite 3.6.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +21 -13
- package/README.zh-CN.md +1 -1
- package/deep-search.mjs +26 -4
- package/hook-update.mjs +17 -1
- package/hook.mjs +403 -373
- package/install.mjs +691 -639
- package/lib/atomic-write.mjs +38 -0
- package/lib/doctor-benchmark.mjs +4 -4
- package/lib/err-sampler.mjs +7 -3
- package/lib/lesson-idents.mjs +32 -0
- package/lib/proc-lock.mjs +112 -0
- package/lib/search-core.mjs +272 -16
- package/mem-cli.mjs +56 -175
- package/package.json +6 -2
- package/schema.mjs +119 -65
- package/scoring-sql.mjs +25 -0
- package/scripts/post-tool-recall.js +71 -0
- package/scripts/pre-tool-recall.js +27 -2
- package/search-engine.mjs +1 -1
- package/{server-internals.mjs → search-scoring.mjs} +6 -2
- package/server.mjs +85 -295
- package/source-files.mjs +11 -1
package/install.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// claude-mem-lite Installer — Smart install/uninstall/status/doctor
|
|
3
3
|
|
|
4
4
|
import { execSync, execFileSync } from 'child_process';
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, copyFileSync, cpSync, renameSync, symlinkSync, unlinkSync, readdirSync, statSync, lstatSync } from 'fs';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, mkdtempSync, copyFileSync, cpSync, renameSync, symlinkSync, unlinkSync, readdirSync, statSync, lstatSync } from 'fs';
|
|
6
6
|
import { join, resolve, dirname, isAbsolute } from 'path';
|
|
7
7
|
import { homedir, tmpdir } from 'os';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -41,6 +41,8 @@ import { scanPluginCacheHookPollution } from './plugin-cache-guard.mjs';
|
|
|
41
41
|
import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
|
|
42
42
|
import { probeBetterSqlite3Binding, ensureBetterSqlite3Working } from './lib/binding-probe.mjs';
|
|
43
43
|
import { sweepStaleTestFixtures } from './lib/tmp-fixture-sweep.mjs';
|
|
44
|
+
import { acquireLock } from './lib/proc-lock.mjs';
|
|
45
|
+
import { atomicWriteFileSync } from './lib/atomic-write.mjs';
|
|
44
46
|
|
|
45
47
|
// Re-export for backward compatibility — tests/install-hook-scripts.test.mjs
|
|
46
48
|
// and any external consumers still import HOOK_SCRIPT_FILES from install.mjs.
|
|
@@ -341,711 +343,748 @@ function isPartialSparseClone(clonePath) {
|
|
|
341
343
|
|
|
342
344
|
// ─── Install ────────────────────────────────────────────────────────────────
|
|
343
345
|
|
|
344
|
-
async function install() {
|
|
345
|
-
console.log('\nclaude-mem-lite installer\n');
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
362
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const link = join(DATA_DIR, f);
|
|
378
|
-
if (existsSync(target)) {
|
|
379
|
-
// Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
|
|
380
|
-
const linkParent = dirname(link);
|
|
381
|
-
if (!existsSync(linkParent)) mkdirSync(linkParent, { recursive: true });
|
|
382
|
-
// Remove existing file/symlink before creating
|
|
383
|
-
if (existsSync(link)) try { unlinkSync(link); } catch {}
|
|
384
|
-
symlinkSync(target, link);
|
|
385
|
-
}
|
|
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);
|
|
386
380
|
}
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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);
|
|
419
413
|
}
|
|
420
|
-
// Copy hook scripts (settings.json hook commands point at these — must
|
|
421
|
-
// stay in sync with HOOK_SCRIPT_FILES manifest)
|
|
422
|
-
copyHookScripts(join(PROJECT_DIR, 'scripts'), scriptsDir);
|
|
423
|
-
// Ensure bash script is executable
|
|
424
|
-
try { execFileSync('chmod', ['+x', join(scriptsDir, 'post-tool-use.sh')], { stdio: 'pipe' }); } catch {}
|
|
425
|
-
// commands/ is intentionally NOT copied — see dev-mode branch above.
|
|
426
|
-
// Copy registry manifest
|
|
427
|
-
const registryDir = join(DATA_DIR, 'registry');
|
|
428
|
-
if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
|
|
429
|
-
const manifestSrc = join(PROJECT_DIR, 'registry', 'preinstalled.json');
|
|
430
|
-
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, join(registryDir, 'preinstalled.json'));
|
|
431
|
-
ok('Source files copied to ~/.claude-mem-lite/');
|
|
432
|
-
|
|
433
|
-
// v2.48 P1-4: prune stale top-level .mjs + 0-byte .db files left behind by
|
|
434
|
-
// prior upgrades (e.g. dispatch.mjs removed in v2.20.0, zero-byte mem.db /
|
|
435
|
-
// memory.db / registry.db from pre-consolidation installs). Subdirs +
|
|
436
|
-
// symlinks + non-empty DBs are always preserved.
|
|
437
|
-
try {
|
|
438
|
-
const pruned = pruneStaleInstallFiles(DATA_DIR, SOURCE_FILES);
|
|
439
|
-
if (pruned.length > 0) {
|
|
440
|
-
ok(`Pruned ${pruned.length} stale file(s): ${pruned.map(p => p.split('/').pop()).join(', ')}`);
|
|
441
|
-
}
|
|
442
|
-
} catch (e) { /* prune is best-effort — never block install */ void e; }
|
|
443
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
|
+
}
|
|
444
440
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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}`);
|
|
448
466
|
} else {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// node-gyp compile spinner, prebuild-install fallback messages). With
|
|
453
|
-
// `stdio: 'pipe'` the install appeared to hang under the 5-min Bash
|
|
454
|
-
// timeout when better-sqlite3 had no Node v24 prebuild and had to
|
|
455
|
-
// compile from source — see bug audit 2026-05.
|
|
456
|
-
execSync(NPM_INSTALL_CMD, { cwd: INSTALL_DIR, stdio: ['ignore', 'pipe', 'inherit'] });
|
|
457
|
-
ok('Dependencies installed');
|
|
458
|
-
} catch (e) {
|
|
459
|
-
fail('npm install failed: ' + e.message);
|
|
460
|
-
process.exit(1);
|
|
461
|
-
}
|
|
462
|
-
// npm install exits 0 even when the better-sqlite3 prebuilt .node binary
|
|
463
|
-
// mismatches the running Node ABI (e.g. NODE_MODULE_VERSION 137 on Node v24).
|
|
464
|
-
// Probe and auto-rebuild before declaring success — otherwise the next
|
|
465
|
-
// launch FATALs with "Could not locate the bindings file".
|
|
466
|
-
const verify = await ensureBetterSqlite3Working(INSTALL_DIR);
|
|
467
|
-
if (verify.ok) {
|
|
468
|
-
ok(`better-sqlite3: ${verify.action}`);
|
|
469
|
-
} else {
|
|
470
|
-
fail(`better-sqlite3 binding unusable after rebuild: ${verify.error}`);
|
|
471
|
-
log('Try manually: cd ' + INSTALL_DIR + ' && npm rebuild better-sqlite3 --build-from-source');
|
|
472
|
-
process.exit(1);
|
|
473
|
-
}
|
|
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);
|
|
474
470
|
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
475
473
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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)
|
|
483
489
|
try {
|
|
484
|
-
|
|
485
|
-
if (existsSync(
|
|
486
|
-
symlinkSync(cliSource,
|
|
487
|
-
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}`);
|
|
488
494
|
} catch {
|
|
489
|
-
|
|
490
|
-
try {
|
|
491
|
-
const globalLink = '/usr/local/bin/claude-mem-lite';
|
|
492
|
-
if (existsSync(globalLink)) unlinkSync(globalLink);
|
|
493
|
-
symlinkSync(cliSource, globalLink);
|
|
494
|
-
ok(`CLI: ${globalLink} → ${cliSource}`);
|
|
495
|
-
} catch {
|
|
496
|
-
warn('CLI symlink failed — run manually: ln -sf ' + cliSource + ' ~/.local/bin/claude-mem-lite');
|
|
497
|
-
}
|
|
495
|
+
warn('CLI symlink failed — run manually: ln -sf ' + cliSource + ' ~/.local/bin/claude-mem-lite');
|
|
498
496
|
}
|
|
499
497
|
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
500
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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...');
|
|
511
528
|
try {
|
|
512
|
-
|
|
513
|
-
pluginHandlesMcp = !!installed?.plugins?.[PLUGIN_KEY]?.length;
|
|
514
|
-
} catch { /* not installed via plugin system */ }
|
|
515
|
-
|
|
516
|
-
if (pluginHandlesMcp) {
|
|
517
|
-
log('MCP server: plugin system handles registration (skipping global)');
|
|
518
|
-
// 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
|
|
519
530
|
for (const name of ['mem', 'mem-lite']) {
|
|
520
|
-
try {
|
|
521
|
-
|
|
522
|
-
ok(`Removed stale global MCP "${name}"`);
|
|
523
|
-
} catch {}
|
|
524
|
-
}
|
|
525
|
-
} else {
|
|
526
|
-
log('Registering MCP server...');
|
|
527
|
-
try {
|
|
528
|
-
// Purge legacy "mem" and any pre-existing "mem-lite" before re-registering
|
|
529
|
-
for (const name of ['mem', 'mem-lite']) {
|
|
530
|
-
try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' }); } catch {}
|
|
531
|
-
try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', name], { stdio: 'pipe' }); } catch {}
|
|
532
|
-
}
|
|
533
|
-
execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem-lite', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
|
|
534
|
-
ok('MCP server registered: mem-lite');
|
|
535
|
-
} catch (e) {
|
|
536
|
-
fail('MCP registration failed: ' + e.message);
|
|
537
|
-
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 {}
|
|
538
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);
|
|
539
539
|
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
540
542
|
|
|
541
|
-
|
|
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
|
-
|
|
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)');
|
|
570
572
|
}
|
|
571
|
-
}
|
|
573
|
+
}
|
|
574
|
+
} catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
|
|
572
575
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
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 */ }
|
|
593
595
|
}
|
|
594
596
|
}
|
|
597
|
+
}
|
|
595
598
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
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 */ }
|
|
611
613
|
}
|
|
612
|
-
const parts = ['launch.mjs synced (dev mode MCP routing)'];
|
|
613
|
-
if (clearedHooks > 0) parts.push(`${clearedHooks} stale hooks.json cleared`);
|
|
614
|
-
ok(`Plugin cache: ${parts.join('; ')}`);
|
|
615
614
|
}
|
|
616
|
-
|
|
617
|
-
|
|
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
|
+
}
|
|
618
622
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
+
{
|
|
638
671
|
type: 'command',
|
|
639
|
-
command:
|
|
672
|
+
command: nodeHook('scripts/user-prompt-search.js'),
|
|
673
|
+
timeout: 2
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
type: 'command',
|
|
677
|
+
command: nodeHook('hook.mjs', 'user-prompt'),
|
|
640
678
|
timeout: 5
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
{
|
|
647
690
|
type: 'command',
|
|
648
|
-
command: nodeHook('
|
|
649
|
-
timeout:
|
|
650
|
-
}
|
|
651
|
-
|
|
691
|
+
command: nodeHook('scripts/pre-tool-recall.js'),
|
|
692
|
+
timeout: 3
|
|
693
|
+
}
|
|
694
|
+
]
|
|
695
|
+
};
|
|
652
696
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
697
|
+
const memPreSkillBridge = {
|
|
698
|
+
matcher: 'Skill',
|
|
699
|
+
hooks: [
|
|
700
|
+
{
|
|
656
701
|
type: 'command',
|
|
657
|
-
command: nodeHook('
|
|
658
|
-
timeout:
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const memPreToolRecall = {
|
|
679
|
-
// v2.34.6: Read added to cover planning-Read (pre-Edit exploration).
|
|
680
|
-
// Read-path uses a tighter filter (lesson_learned required, top-1,
|
|
681
|
-
// 120-char truncation, silent-on-empty) — see scripts/pre-tool-recall.js.
|
|
682
|
-
matcher: 'Edit|Write|NotebookEdit|Read',
|
|
683
|
-
hooks: [
|
|
684
|
-
{
|
|
685
|
-
type: 'command',
|
|
686
|
-
command: nodeHook('scripts/pre-tool-recall.js'),
|
|
687
|
-
timeout: 3
|
|
688
|
-
}
|
|
689
|
-
]
|
|
690
|
-
};
|
|
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
|
+
}
|
|
691
722
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
{
|
|
696
|
-
type: 'command',
|
|
697
|
-
command: nodeHook('scripts/pre-skill-bridge.js'),
|
|
698
|
-
timeout: 3
|
|
699
|
-
}
|
|
700
|
-
]
|
|
701
|
-
};
|
|
723
|
+
writeSettings(settings);
|
|
724
|
+
ok('Hooks configured (PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit)');
|
|
725
|
+
}
|
|
702
726
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
+
}
|
|
712
742
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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 {}
|
|
716
749
|
}
|
|
750
|
+
ok('Database renamed: claude-mem.db → claude-mem-lite.db');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
717
753
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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);
|
|
731
767
|
}
|
|
732
|
-
} catch (e) {
|
|
733
|
-
warn('Legacy DB backup failed: ' + e.message);
|
|
734
768
|
}
|
|
735
769
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
renameSync(oldDbInDir, DB_PATH);
|
|
740
|
-
for (const ext of ['-wal', '-shm']) {
|
|
741
|
-
if (existsSync(oldDbInDir + ext)) try { renameSync(oldDbInDir + ext, DB_PATH + ext); } catch {}
|
|
742
|
-
}
|
|
743
|
-
ok('Database renamed: claude-mem.db → claude-mem-lite.db');
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// 6. Install pre-installed resources (skills + agents)
|
|
747
|
-
if (process.env.CLAUDE_MEM_SKIP_REPOS) {
|
|
748
|
-
ok('Skill/agent registry: skipped (CLAUDE_MEM_SKIP_REPOS)');
|
|
749
|
-
} else try {
|
|
750
|
-
const manifestPath = join(INSTALL_DIR, 'registry', 'preinstalled.json');
|
|
751
|
-
if (!existsSync(manifestPath)) {
|
|
752
|
-
// For git-clone mode, check PROJECT_DIR
|
|
753
|
-
const altPath = join(PROJECT_DIR, 'registry', 'preinstalled.json');
|
|
754
|
-
if (existsSync(altPath)) {
|
|
755
|
-
const registryDir = join(INSTALL_DIR, 'registry');
|
|
756
|
-
if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
|
|
757
|
-
copyFileSync(altPath, manifestPath);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (existsSync(manifestPath)) {
|
|
762
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
763
|
-
const resources = manifest.resources || [];
|
|
764
|
-
|
|
765
|
-
if (resources.length > 0) {
|
|
766
|
-
const managedDir = join(MEM_DATA_DIR, 'managed');
|
|
770
|
+
if (existsSync(manifestPath)) {
|
|
771
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
772
|
+
const resources = manifest.resources || [];
|
|
767
773
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
for (const r of resources) {
|
|
771
|
-
if (!repos.has(r.repo)) repos.set(r.repo, []);
|
|
772
|
-
repos.get(r.repo).push(r);
|
|
773
|
-
}
|
|
774
|
+
if (resources.length > 0) {
|
|
775
|
+
const managedDir = join(MEM_DATA_DIR, 'managed');
|
|
774
776
|
|
|
775
|
-
|
|
776
|
-
|
|
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
|
+
}
|
|
777
783
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
}
|
|
781
816
|
};
|
|
782
817
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const cloneUrl = `${repoUrl.replace(/\.git$/, '')}.git`;
|
|
790
|
-
// Clone only what we extract: a partial (blob:none) + sparse clone fetches
|
|
791
|
-
// just the manifest subpaths' subtrees instead of the whole repo. Falls
|
|
792
|
-
// back to a plain shallow clone if partial-clone/sparse-checkout is
|
|
793
|
-
// unsupported (old git/server) — identical to the prior behavior.
|
|
794
|
-
const cloneSlim = () => {
|
|
795
|
-
if (plan.full) {
|
|
796
|
-
execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
try {
|
|
800
|
-
execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
|
|
801
|
-
execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 });
|
|
802
|
-
execFileSync('git', ['-C', clonePath, 'checkout'], { stdio: 'pipe', timeout: 30000 });
|
|
803
|
-
} catch {
|
|
804
|
-
try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
|
|
805
|
-
execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
|
|
806
|
-
}
|
|
807
|
-
};
|
|
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
|
+
}
|
|
808
824
|
|
|
809
|
-
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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;
|
|
814
840
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
repoReady = true;
|
|
823
|
-
} catch (err) {
|
|
824
|
-
if (isRepoNotFound(err)) {
|
|
825
|
-
deadRepos.add(repoUrl);
|
|
826
|
-
warn(` Repo not found (removed?): ${repoUrl}`);
|
|
827
|
-
} else {
|
|
828
|
-
warn(` Clone failed: ${repoUrl}`);
|
|
829
|
-
}
|
|
830
|
-
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 {}
|
|
831
848
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
const localHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
841
|
-
execFileSync('git', ['-C', clonePath, 'fetch', '--depth', '1', 'origin'], { stdio: 'pipe', timeout: 30000 });
|
|
842
|
-
const remoteHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'FETCH_HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
843
|
-
if (localHash !== remoteHash) {
|
|
844
|
-
execFileSync('git', ['-C', clonePath, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'pipe' });
|
|
845
|
-
updated++;
|
|
846
|
-
repoReady = true; // needs re-copy
|
|
847
|
-
}
|
|
848
|
-
} catch (err) {
|
|
849
|
-
if (isRepoNotFound(err)) {
|
|
850
|
-
deadRepos.add(repoUrl);
|
|
851
|
-
warn(` Repo not found (removed?): ${repoUrl} — cleaning up`);
|
|
852
|
-
// Remove local clone
|
|
853
|
-
try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
|
|
854
|
-
// Remove extracted resources
|
|
855
|
-
for (const entry of entries) {
|
|
856
|
-
const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
|
|
857
|
-
const destPath = join(destDir, entry.name);
|
|
858
|
-
try { if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true }); } catch {}
|
|
859
|
-
}
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
// 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
|
|
863
856
|
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (existsSync(srcPath) && (repoReady || !existsSync(destPath))) {
|
|
878
|
-
try {
|
|
879
|
-
if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true });
|
|
880
|
-
cpSync(srcPath, destPath, { recursive: true });
|
|
881
|
-
} 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;
|
|
882
870
|
}
|
|
871
|
+
// Transient failure — use existing clone as-is
|
|
883
872
|
}
|
|
884
873
|
}
|
|
885
|
-
ok(`Repos: ${cloned} cloned, ${updated} updated, ${repos.size - deadRepos.size} active` +
|
|
886
|
-
(deadRepos.size > 0 ? `, ${deadRepos.size} dead removed` : ''));
|
|
887
|
-
|
|
888
|
-
// 6b. Init registry DB and record preinstalled entries
|
|
889
|
-
const { ensureRegistryDb } = await importFromInstall('registry.mjs');
|
|
890
|
-
const regDbPath = join(MEM_DATA_DIR, 'resource-registry.db');
|
|
891
|
-
const rdb = ensureRegistryDb(regDbPath);
|
|
892
|
-
|
|
893
|
-
const insertPre = rdb.prepare(`
|
|
894
|
-
INSERT OR REPLACE INTO preinstalled (name, type, repo_url, repo_path, tags, enabled)
|
|
895
|
-
VALUES (?, ?, ?, ?, ?, 1)
|
|
896
|
-
`);
|
|
897
|
-
const activeResources = deadRepos.size > 0
|
|
898
|
-
? resources.filter(r => !deadRepos.has(r.repo))
|
|
899
|
-
: resources;
|
|
900
|
-
for (const r of activeResources) {
|
|
901
|
-
insertPre.run(r.name, r.type, r.repo, r.path, JSON.stringify(r.tags || []));
|
|
902
|
-
}
|
|
903
874
|
|
|
904
|
-
//
|
|
905
|
-
if
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
// 6c. Fetch GitHub stars (best-effort, unauthenticated)
|
|
917
|
-
log(' Fetching GitHub stars...');
|
|
918
|
-
const starCache = new Map();
|
|
919
|
-
for (const [repoUrl] of repos) {
|
|
920
|
-
if (deadRepos.has(repoUrl)) continue;
|
|
921
|
-
const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
922
|
-
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))) {
|
|
923
887
|
try {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const data = JSON.parse(res);
|
|
927
|
-
if (typeof data.stargazers_count === 'number') {
|
|
928
|
-
starCache.set(repoUrl, data.stargazers_count);
|
|
929
|
-
}
|
|
888
|
+
if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true });
|
|
889
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
930
890
|
} catch {}
|
|
931
891
|
}
|
|
932
892
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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;
|
|
947
955
|
}
|
|
956
|
+
}
|
|
948
957
|
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
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 {}
|
|
976
983
|
}
|
|
984
|
+
ok(`Resources registered: ${toIndex.length} indexed`);
|
|
985
|
+
}
|
|
977
986
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
987
|
+
// Apply curated metadata to all known resources (fixes existing installs)
|
|
988
|
+
reindexKnownResources(rdb);
|
|
989
|
+
ok('Resource metadata curated (FTS5 reindexed)');
|
|
981
990
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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`);
|
|
985
994
|
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
} else {
|
|
989
|
-
log(' No preinstalled manifest found, skipping');
|
|
995
|
+
rdb.close();
|
|
990
996
|
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
log(' Skills/agents will be indexed on first use');
|
|
997
|
+
} else {
|
|
998
|
+
log(' No preinstalled manifest found, skipping');
|
|
994
999
|
}
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
warn('Resource setup: ' + e.message);
|
|
1002
|
+
log(' Skills/agents will be indexed on first use');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
995
1005
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
} else {
|
|
1008
|
-
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);
|
|
1009
1017
|
}
|
|
1018
|
+
} else {
|
|
1019
|
+
log('No existing database — will be created on first use');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1010
1022
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
} catch {
|
|
1026
|
-
// 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');
|
|
1027
1037
|
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
// Not a git repo, or git missing — silent skip (this is the normal npm path).
|
|
1028
1040
|
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1029
1043
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
+
}
|
|
1036
1053
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
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();
|
|
1045
1083
|
|
|
1046
1084
|
console.log('\n Done! Restart Claude Code to activate.\n');
|
|
1047
1085
|
}
|
|
1048
1086
|
|
|
1087
|
+
|
|
1049
1088
|
// ─── Uninstall ──────────────────────────────────────────────────────────────
|
|
1050
1089
|
|
|
1051
1090
|
async function uninstall() {
|
|
@@ -1807,11 +1846,11 @@ function readSettings() {
|
|
|
1807
1846
|
}
|
|
1808
1847
|
|
|
1809
1848
|
function writeSettings(settings) {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1849
|
+
// Atomic (pid-unique temp + rename) with a one-time .bak: settings.json is the
|
|
1850
|
+
// user's Claude Code config. The old fixed ".tmp" name let concurrent installs
|
|
1851
|
+
// clobber each other's temp mid-write, and there was no recovery artifact if a
|
|
1852
|
+
// hook-merge bug dropped user config. atomicWriteFileSync handles dir creation.
|
|
1853
|
+
atomicWriteFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', { backup: true });
|
|
1815
1854
|
}
|
|
1816
1855
|
|
|
1817
1856
|
// ─── Cleanup Stale Files ─────────────────────────────────────────────────────
|
|
@@ -1919,8 +1958,7 @@ async function manualUpdate() {
|
|
|
1919
1958
|
// buggy on disk.
|
|
1920
1959
|
async function repair() {
|
|
1921
1960
|
console.log('\nclaude-mem-lite repair — re-syncing from latest GitHub release\n');
|
|
1922
|
-
const stagingDir = join(tmpdir(),
|
|
1923
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
1961
|
+
const stagingDir = mkdtempSync(join(tmpdir(), 'claude-mem-lite-repair-'));
|
|
1924
1962
|
try {
|
|
1925
1963
|
const tarballUrl = 'https://api.github.com/repos/sdsrss/claude-mem-lite/tarball';
|
|
1926
1964
|
const tarballPath = join(stagingDir, 'release.tgz');
|
|
@@ -2027,13 +2065,27 @@ function regenerateLockfile() {
|
|
|
2027
2065
|
|
|
2028
2066
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
2029
2067
|
|
|
2068
|
+
// Cross-process gate around the install write phase. repair() is intentionally
|
|
2069
|
+
// NOT locked here: it spawns `install.mjs install` as a child, which takes this
|
|
2070
|
+
// lock — locking the parent too would deadlock. A live peer (another session's
|
|
2071
|
+
// install/self-heal) holds it → skip rather than race into a torn install. Lock
|
|
2072
|
+
// path is shared with hook-update.installExtractedRelease (both env-aware).
|
|
2073
|
+
async function runLockedInstall() {
|
|
2074
|
+
const release = acquireLock(join(MEM_DATA_DIR, 'runtime', 'install.lock'));
|
|
2075
|
+
if (!release) {
|
|
2076
|
+
console.log('[install] Another install/repair is in progress — skipping to avoid a torn write.');
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
try { await install(); } finally { release(); }
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2030
2082
|
export async function main(argv = process.argv.slice(2)) {
|
|
2031
2083
|
cmd = argv[0];
|
|
2032
2084
|
flags = new Set(argv.slice(1));
|
|
2033
2085
|
|
|
2034
2086
|
switch (cmd) {
|
|
2035
2087
|
case 'install':
|
|
2036
|
-
await
|
|
2088
|
+
await runLockedInstall();
|
|
2037
2089
|
break;
|
|
2038
2090
|
case 'uninstall':
|
|
2039
2091
|
await uninstall();
|
|
@@ -2064,7 +2116,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2064
2116
|
default:
|
|
2065
2117
|
if (IS_NPX) {
|
|
2066
2118
|
// npx claude-mem-lite (no args) → auto install
|
|
2067
|
-
await
|
|
2119
|
+
await runLockedInstall();
|
|
2068
2120
|
} else {
|
|
2069
2121
|
// Name the unknown token before the usage block. Pre-fix `install frobnicate`
|
|
2070
2122
|
// dumped usage silently, which read like the user had typed nothing — they had
|