@llmnesia/mcp 0.2.4 → 0.2.6

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.
@@ -3,7 +3,7 @@ import { createRequire as __llmnesiaCreateRequire } from "node:module";
3
3
  const require = __llmnesiaCreateRequire(import.meta.url);
4
4
  import {
5
5
  VERSION
6
- } from "./chunk-JSNSTAFD.js";
6
+ } from "./chunk-YRK2DRGG.js";
7
7
 
8
8
  // src/config.ts
9
9
  import { readFileSync } from "fs";
@@ -13,14 +13,22 @@ var CORPUS_FORMAT_VERSION = 2;
13
13
  function nativeHostConfigPath() {
14
14
  return join(homedir(), ".llmnesia", "config.json");
15
15
  }
16
- function readNativeHostCorpusRoot() {
16
+ function readNativeHostConfig() {
17
17
  try {
18
18
  const parsed = JSON.parse(readFileSync(nativeHostConfigPath(), "utf8"));
19
- return typeof parsed.corpusRoot === "string" ? parsed.corpusRoot : void 0;
19
+ return parsed && typeof parsed === "object" ? parsed : void 0;
20
20
  } catch {
21
21
  return void 0;
22
22
  }
23
23
  }
24
+ function readNativeHostCorpusRoot() {
25
+ const root = readNativeHostConfig()?.corpusRoot;
26
+ return typeof root === "string" ? root : void 0;
27
+ }
28
+ function readNativeHostViewerExtensionId() {
29
+ const id = readNativeHostConfig()?.viewerExtensionId;
30
+ return typeof id === "string" ? id : void 0;
31
+ }
24
32
  function resolveCorpusDir(explicit) {
25
33
  const raw = explicit ?? process.env.LLMNESIA_CORPUS_DIR ?? readNativeHostCorpusRoot() ?? "~/.llmnesia/corpus";
26
34
  return expandHome(raw);
@@ -63,6 +71,14 @@ var CorpusPaths = class {
63
71
  get metaFile() {
64
72
  return join2(this.root, "meta.json");
65
73
  }
74
+ /** Content-free state for automatic storage maintenance. */
75
+ get maintenanceStateFile() {
76
+ return join2(this.root, "maintenance-v1.json");
77
+ }
78
+ /** Cross-process lease preventing several MCP clients compacting together. */
79
+ get maintenanceLockFile() {
80
+ return join2(this.root, ".maintenance-v1.lock");
81
+ }
66
82
  get inboxDir() {
67
83
  return join2(this.root, "inbox");
68
84
  }
@@ -228,6 +244,8 @@ var DIR_MODE = 448;
228
244
  var FILE_MODE = 384;
229
245
  var gzip = promisify(gzipCallback);
230
246
  var gunzip = promisify(gunzipCallback);
247
+ var MAINTENANCE_WRITE_WAIT_MS = 2e3;
248
+ var MAINTENANCE_WRITE_POLL_MS = 50;
231
249
  var Corpus = class {
232
250
  paths;
233
251
  // Serialize writes within this process so concurrent upserts (inbox drain +
@@ -291,6 +309,7 @@ var Corpus = class {
291
309
  */
292
310
  async upsert(record) {
293
311
  return this.enqueueWrite(async () => {
312
+ await waitForMaintenanceLease(this.paths.maintenanceLockFile);
294
313
  const canonical = canonicalizeConversation(record);
295
314
  const { conversation } = canonical;
296
315
  const file = this.paths.compressedConversationFile(conversation.platform, conversation.docId);
@@ -473,6 +492,7 @@ var Corpus = class {
473
492
  */
474
493
  async migrateLegacyConversations(options = {}) {
475
494
  const dryRun = options.dryRun === true;
495
+ const yieldEvery = Math.max(0, Math.floor(options.yieldEvery ?? 0));
476
496
  const before = await this.conversationStorageStats();
477
497
  const report = {
478
498
  ...before,
@@ -495,18 +515,22 @@ var Corpus = class {
495
515
  const legacyFile = join3(dir, name);
496
516
  const compressedFile = join3(dir, `${name}.gz`);
497
517
  try {
498
- const legacy = await readPlainConversationIfExists(legacyFile);
499
- if (!legacy) throw new Error("legacy JSON could not be parsed");
500
- const compressed = await readCompressedConversationIfExists(compressedFile);
501
- const winner = pickNewerConversation(compressed, legacy) ?? legacy;
502
- const serialized = serializeConversation(canonicalizeConversation(winner));
503
- const compressedBytes = await gzip(Buffer.from(serialized, "utf8"), { level: 9 });
504
- report.projectedCompressedBytes -= await fileSizeIfExists(compressedFile);
505
- report.projectedCompressedBytes += compressedBytes.byteLength;
506
- if (!dryRun) {
507
- await writeVerifiedCompressedConversationAtomic(compressedFile, compressedBytes, serialized);
508
- await fs.rm(legacyFile);
509
- }
518
+ const migrateOne = async () => {
519
+ const legacy = await readPlainConversationIfExists(legacyFile);
520
+ if (!legacy) throw new Error("legacy JSON could not be parsed");
521
+ const compressed = await readCompressedConversationIfExists(compressedFile);
522
+ const winner = pickNewerConversation(compressed, legacy) ?? legacy;
523
+ const serialized = serializeConversation(canonicalizeConversation(winner));
524
+ const compressedBytes = await gzip(Buffer.from(serialized, "utf8"), { level: 9 });
525
+ report.projectedCompressedBytes -= await fileSizeIfExists(compressedFile);
526
+ report.projectedCompressedBytes += compressedBytes.byteLength;
527
+ if (!dryRun) {
528
+ await writeVerifiedCompressedConversationAtomic(compressedFile, compressedBytes, serialized);
529
+ await fs.rm(legacyFile);
530
+ }
531
+ };
532
+ if (dryRun) await migrateOne();
533
+ else await this.enqueueWrite(migrateOne);
510
534
  report.migrated += 1;
511
535
  report.legacyRemoved += 1;
512
536
  } catch (error) {
@@ -515,6 +539,9 @@ var Corpus = class {
515
539
  error: error instanceof Error ? error.message : String(error)
516
540
  });
517
541
  }
542
+ if (yieldEvery > 0 && (report.migrated + report.failures.length) % yieldEvery === 0) {
543
+ await yieldToEventLoop();
544
+ }
518
545
  }
519
546
  }
520
547
  if (!dryRun && report.failures.length === 0) {
@@ -558,6 +585,23 @@ var Corpus = class {
558
585
  );
559
586
  }
560
587
  };
588
+ function yieldToEventLoop() {
589
+ return new Promise((resolve2) => setImmediate(resolve2));
590
+ }
591
+ async function waitForMaintenanceLease(file) {
592
+ const deadline = Date.now() + MAINTENANCE_WRITE_WAIT_MS;
593
+ while (true) {
594
+ try {
595
+ await fs.access(file);
596
+ } catch {
597
+ return;
598
+ }
599
+ if (Date.now() >= deadline) {
600
+ throw new Error("automatic corpus maintenance is in progress; retry this write shortly");
601
+ }
602
+ await new Promise((resolve2) => setTimeout(resolve2, MAINTENANCE_WRITE_POLL_MS));
603
+ }
604
+ }
561
605
  async function readJsonIfExists(file) {
562
606
  try {
563
607
  const raw = await fs.readFile(file, "utf8");
@@ -1359,43 +1403,421 @@ function normalizeMatchRole(role) {
1359
1403
  return role === "assistant" || role === "user" || role === "system" ? role : "unknown";
1360
1404
  }
1361
1405
 
1406
+ // src/storageMaintenance.ts
1407
+ import { promises as fs4 } from "fs";
1408
+ import { join as join5 } from "path";
1409
+ async function inspectCorpusStorage(corpus) {
1410
+ const conversations = await corpus.conversationStorageStats();
1411
+ const processed = await directoryFiles(corpus.paths.processedDir);
1412
+ const quarantine = await directoryFiles(corpus.paths.quarantineDir);
1413
+ const activeNames = [
1414
+ corpus.paths.indexDbFile,
1415
+ `${corpus.paths.indexDbFile}-wal`,
1416
+ `${corpus.paths.indexDbFile}-shm`
1417
+ ];
1418
+ const activeIndex = await explicitFiles(activeNames);
1419
+ const staleNames = await staleIndexFiles(corpus.paths.indexDir);
1420
+ const stalePaths = staleNames.flatMap((name) => {
1421
+ const base = join5(corpus.paths.indexDir, name);
1422
+ return [base, `${base}-wal`, `${base}-shm`];
1423
+ });
1424
+ const staleIndexes = await explicitFiles(stalePaths);
1425
+ const root = await directoryFiles(corpus.paths.root);
1426
+ const classifiedBytes = conversations.legacyBytes + conversations.compressedBytes + processed.bytes + quarantine.bytes + activeIndex.bytes + staleIndexes.bytes;
1427
+ const classifiedFiles = conversations.legacyFiles + conversations.compressedFiles + processed.files + quarantine.files + activeIndex.files + staleIndexes.files;
1428
+ const other = {
1429
+ files: Math.max(0, root.files - classifiedFiles),
1430
+ bytes: Math.max(0, root.bytes - classifiedBytes)
1431
+ };
1432
+ return {
1433
+ root: corpus.paths.root,
1434
+ conversations,
1435
+ processed,
1436
+ quarantine,
1437
+ activeIndex,
1438
+ staleIndexes,
1439
+ other,
1440
+ totalBytes: root.bytes,
1441
+ immediatelyReclaimableBytes: processed.bytes + staleIndexes.bytes
1442
+ };
1443
+ }
1444
+ async function compactCorpus(corpus, options = {}) {
1445
+ const dryRun = options.dryRun === true;
1446
+ const before = await inspectCorpusStorage(corpus);
1447
+ if (dryRun) {
1448
+ const compression = await corpus.migrateLegacyConversations({ dryRun: true });
1449
+ const compressionSavings = Math.max(
1450
+ 0,
1451
+ compression.legacyBytes + compression.compressedBytes - compression.projectedCompressedBytes
1452
+ );
1453
+ return {
1454
+ dryRun: true,
1455
+ before,
1456
+ compression,
1457
+ processedRemoved: before.processed,
1458
+ staleIndexesRemoved: before.staleIndexes,
1459
+ after: null,
1460
+ reclaimedBytes: before.immediatelyReclaimableBytes + compressionSavings
1461
+ };
1462
+ }
1463
+ await corpus.init();
1464
+ const processedRemoved = await removeDirectoryFiles(corpus.paths.processedDir);
1465
+ const index = options.index ?? await openSearchIndex(corpus);
1466
+ try {
1467
+ index.compact();
1468
+ } finally {
1469
+ if (!options.index) index.close();
1470
+ }
1471
+ const appliedCompression = await corpus.migrateLegacyConversations({
1472
+ yieldEvery: options.yieldEvery
1473
+ });
1474
+ if (appliedCompression.failures.length > 0) {
1475
+ throw new Error(
1476
+ `Corpus compression left ${appliedCompression.failures.length} legacy file(s) untouched; stale indexes were preserved. Run compact again after fixing the reported files.`
1477
+ );
1478
+ }
1479
+ const staleIndexesRemoved = await removeStaleIndexes(corpus);
1480
+ const after = await inspectCorpusStorage(corpus);
1481
+ return {
1482
+ dryRun: false,
1483
+ before,
1484
+ compression: appliedCompression,
1485
+ processedRemoved,
1486
+ staleIndexesRemoved,
1487
+ after,
1488
+ reclaimedBytes: Math.max(0, before.totalBytes - after.totalBytes)
1489
+ };
1490
+ }
1491
+ async function removeStaleIndexes(corpus) {
1492
+ const names = await staleIndexFiles(corpus.paths.indexDir);
1493
+ const paths = names.flatMap((name) => {
1494
+ const base = join5(corpus.paths.indexDir, name);
1495
+ return [base, `${base}-wal`, `${base}-shm`];
1496
+ });
1497
+ const before = await explicitFiles(paths);
1498
+ for (const name of names) {
1499
+ await deleteIndexDb(join5(corpus.paths.indexDir, name));
1500
+ }
1501
+ return before;
1502
+ }
1503
+ async function removeDirectoryFiles(dir) {
1504
+ const before = await directoryFiles(dir);
1505
+ await removeFilesRecursively(dir);
1506
+ return before;
1507
+ }
1508
+ async function removeFilesRecursively(dir) {
1509
+ let entries;
1510
+ try {
1511
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1512
+ } catch {
1513
+ return;
1514
+ }
1515
+ for (const entry of entries) {
1516
+ const path = join5(dir, entry.name);
1517
+ if (entry.isDirectory()) await removeFilesRecursively(path);
1518
+ else if (entry.isFile()) await fs4.rm(path, { force: true });
1519
+ }
1520
+ }
1521
+ async function explicitFiles(files) {
1522
+ let count = 0;
1523
+ let bytes = 0;
1524
+ for (const file of files) {
1525
+ try {
1526
+ const stat = await fs4.stat(file);
1527
+ if (stat.isFile()) {
1528
+ count += 1;
1529
+ bytes += stat.size;
1530
+ }
1531
+ } catch {
1532
+ }
1533
+ }
1534
+ return { files: count, bytes };
1535
+ }
1536
+ async function directoryFiles(dir) {
1537
+ let entries;
1538
+ try {
1539
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1540
+ } catch {
1541
+ return { files: 0, bytes: 0 };
1542
+ }
1543
+ let files = 0;
1544
+ let bytes = 0;
1545
+ for (const entry of entries) {
1546
+ const path = join5(dir, entry.name);
1547
+ if (entry.isDirectory()) {
1548
+ const nested = await directoryFiles(path);
1549
+ files += nested.files;
1550
+ bytes += nested.bytes;
1551
+ } else if (entry.isFile()) {
1552
+ files += 1;
1553
+ bytes += (await fs4.stat(path)).size;
1554
+ }
1555
+ }
1556
+ return { files, bytes };
1557
+ }
1558
+
1559
+ // src/autoMaintenance.ts
1560
+ import { constants as fsConstants, promises as fs5 } from "fs";
1561
+ import { dirname as dirname2 } from "path";
1562
+ var FILE_MODE2 = 384;
1563
+ var STATE_SCHEMA_VERSION = 1;
1564
+ var FORMAT_KEY = `corpus-${CORPUS_FORMAT_VERSION}-index-${INDEX_FORMAT_VERSION}`;
1565
+ var AUTO_MAINTENANCE_GRACE_MS = 3 * 24 * 60 * 60 * 1e3;
1566
+ var AUTO_MAINTENANCE_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
1567
+ var AUTO_MAINTENANCE_RETRY_MS = 24 * 60 * 60 * 1e3;
1568
+ var AUTO_MAINTENANCE_LOCK_STALE_MS = 6 * 60 * 60 * 1e3;
1569
+ var AUTO_MAINTENANCE_YIELD_EVERY = 25;
1570
+ var AUTO_MAINTENANCE_SETTLE_MS = 1e3;
1571
+ function isAutomaticMaintenanceDisabled(env = process.env) {
1572
+ const raw = env.LLMNESIA_NO_AUTO_MAINTENANCE;
1573
+ return raw === "1" || raw === "true";
1574
+ }
1575
+ async function runAutomaticMaintenance(corpus, index, options = {}) {
1576
+ const env = options.env ?? process.env;
1577
+ if (isAutomaticMaintenanceDisabled(env)) return { action: "disabled" };
1578
+ await corpus.init();
1579
+ const now = options.now ?? Date.now;
1580
+ const at = now();
1581
+ const graceMs = options.graceMs ?? AUTO_MAINTENANCE_GRACE_MS;
1582
+ const recheckMs = options.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS;
1583
+ const retryMs = options.retryMs ?? AUTO_MAINTENANCE_RETRY_MS;
1584
+ const state = await readState(corpus);
1585
+ if (state?.formatKey === FORMAT_KEY && state.status === "complete" && state.completedAt !== void 0 && at - state.completedAt < recheckMs) {
1586
+ return { action: "recently-checked", completedAt: state.completedAt };
1587
+ }
1588
+ if (state?.formatKey === FORMAT_KEY && state.nextEligibleAt !== void 0 && at < state.nextEligibleAt) {
1589
+ return { action: "deferred", until: state.nextEligibleAt };
1590
+ }
1591
+ const before = await inspectCorpusStorage(corpus);
1592
+ if (!hasWork(before)) {
1593
+ await writeState(corpus, {
1594
+ schemaVersion: STATE_SCHEMA_VERSION,
1595
+ formatKey: FORMAT_KEY,
1596
+ status: "complete",
1597
+ firstSeenAt: state?.formatKey === FORMAT_KEY ? state.firstSeenAt : at,
1598
+ completedAt: at,
1599
+ beforeBytes: before.totalBytes,
1600
+ afterBytes: before.totalBytes,
1601
+ reclaimedBytes: 0
1602
+ });
1603
+ return { action: "no-work" };
1604
+ }
1605
+ const firstSeenAt = state?.formatKey === FORMAT_KEY ? state.firstSeenAt : at;
1606
+ const eligibleAt = firstSeenAt + Math.max(0, graceMs);
1607
+ if (at < eligibleAt) {
1608
+ await writeState(corpus, {
1609
+ schemaVersion: STATE_SCHEMA_VERSION,
1610
+ formatKey: FORMAT_KEY,
1611
+ status: "waiting",
1612
+ firstSeenAt,
1613
+ nextEligibleAt: eligibleAt,
1614
+ beforeBytes: before.totalBytes
1615
+ });
1616
+ return { action: "deferred", until: eligibleAt };
1617
+ }
1618
+ const lease = await acquireLease(
1619
+ corpus,
1620
+ at,
1621
+ options.lockStaleMs ?? AUTO_MAINTENANCE_LOCK_STALE_MS
1622
+ );
1623
+ if (!lease) return { action: "locked" };
1624
+ try {
1625
+ await delay(options.settleMs ?? AUTO_MAINTENANCE_SETTLE_MS);
1626
+ const lockedBefore = await inspectCorpusStorage(corpus);
1627
+ if (!hasWork(lockedBefore)) {
1628
+ await writeState(corpus, {
1629
+ schemaVersion: STATE_SCHEMA_VERSION,
1630
+ formatKey: FORMAT_KEY,
1631
+ status: "complete",
1632
+ firstSeenAt,
1633
+ completedAt: now(),
1634
+ beforeBytes: lockedBefore.totalBytes,
1635
+ afterBytes: lockedBefore.totalBytes,
1636
+ reclaimedBytes: 0
1637
+ });
1638
+ return { action: "no-work" };
1639
+ }
1640
+ await writeState(corpus, {
1641
+ schemaVersion: STATE_SCHEMA_VERSION,
1642
+ formatKey: FORMAT_KEY,
1643
+ status: "running",
1644
+ firstSeenAt,
1645
+ lastAttemptAt: at,
1646
+ beforeBytes: lockedBefore.totalBytes
1647
+ });
1648
+ try {
1649
+ const report = await compactCorpus(corpus, {
1650
+ index,
1651
+ yieldEvery: options.yieldEvery ?? AUTO_MAINTENANCE_YIELD_EVERY
1652
+ });
1653
+ const completedAt = now();
1654
+ await writeState(corpus, {
1655
+ schemaVersion: STATE_SCHEMA_VERSION,
1656
+ formatKey: FORMAT_KEY,
1657
+ status: "complete",
1658
+ firstSeenAt,
1659
+ lastAttemptAt: at,
1660
+ completedAt,
1661
+ beforeBytes: report.before.totalBytes,
1662
+ afterBytes: report.after?.totalBytes ?? report.before.totalBytes,
1663
+ reclaimedBytes: report.reclaimedBytes
1664
+ });
1665
+ return { action: "compacted", report };
1666
+ } catch (error) {
1667
+ const message = error instanceof Error ? error.message : String(error);
1668
+ const retryAt = now() + Math.max(0, retryMs);
1669
+ await writeState(corpus, {
1670
+ schemaVersion: STATE_SCHEMA_VERSION,
1671
+ formatKey: FORMAT_KEY,
1672
+ status: "failed",
1673
+ firstSeenAt,
1674
+ lastAttemptAt: at,
1675
+ nextEligibleAt: retryAt,
1676
+ beforeBytes: lockedBefore.totalBytes,
1677
+ lastError: message
1678
+ });
1679
+ return { action: "failed", error: message, retryAt };
1680
+ }
1681
+ } finally {
1682
+ await lease.release();
1683
+ }
1684
+ }
1685
+ async function readAutomaticMaintenanceState(corpus) {
1686
+ return readState(corpus);
1687
+ }
1688
+ function hasWork(report) {
1689
+ return report.conversations.legacyFiles > 0 || report.processed.files > 0 || report.staleIndexes.files > 0;
1690
+ }
1691
+ async function readState(corpus) {
1692
+ try {
1693
+ const parsed = JSON.parse(
1694
+ await fs5.readFile(corpus.paths.maintenanceStateFile, "utf8")
1695
+ );
1696
+ return parsed?.schemaVersion === STATE_SCHEMA_VERSION ? parsed : null;
1697
+ } catch {
1698
+ return null;
1699
+ }
1700
+ }
1701
+ async function writeState(corpus, state) {
1702
+ const file = corpus.paths.maintenanceStateFile;
1703
+ const temp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1704
+ await fs5.mkdir(dirname2(file), { recursive: true });
1705
+ try {
1706
+ const handle = await fs5.open(
1707
+ temp,
1708
+ fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
1709
+ FILE_MODE2
1710
+ );
1711
+ try {
1712
+ await handle.writeFile(`${JSON.stringify(state, null, 2)}
1713
+ `, "utf8");
1714
+ } finally {
1715
+ await handle.close();
1716
+ }
1717
+ await fs5.rename(temp, file);
1718
+ } finally {
1719
+ await fs5.rm(temp, { force: true }).catch(() => {
1720
+ });
1721
+ }
1722
+ }
1723
+ async function acquireLease(corpus, now, staleMs) {
1724
+ const file = corpus.paths.maintenanceLockFile;
1725
+ const token = `${process.pid}-${now}-${Math.random().toString(16).slice(2)}`;
1726
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1727
+ try {
1728
+ const handle = await fs5.open(file, "wx", FILE_MODE2);
1729
+ try {
1730
+ await handle.writeFile(`${JSON.stringify({ token, pid: process.pid, startedAt: now })}
1731
+ `, "utf8");
1732
+ } finally {
1733
+ await handle.close();
1734
+ }
1735
+ return {
1736
+ token,
1737
+ release: async () => {
1738
+ try {
1739
+ const current = JSON.parse(await fs5.readFile(file, "utf8"));
1740
+ if (current.token === token) await fs5.rm(file, { force: true });
1741
+ } catch {
1742
+ }
1743
+ }
1744
+ };
1745
+ } catch (error) {
1746
+ if (!isAlreadyExists(error)) throw error;
1747
+ const existing = await readLease(file);
1748
+ const alive = existing?.pid ? isProcessAlive(existing.pid) : false;
1749
+ const age = existing?.startedAt ? now - existing.startedAt : Number.POSITIVE_INFINITY;
1750
+ if (alive && age < staleMs) return null;
1751
+ const stale = `${file}.stale-${token}`;
1752
+ try {
1753
+ await fs5.rename(file, stale);
1754
+ await fs5.rm(stale, { force: true });
1755
+ } catch {
1756
+ return null;
1757
+ }
1758
+ }
1759
+ }
1760
+ return null;
1761
+ }
1762
+ async function readLease(file) {
1763
+ try {
1764
+ return JSON.parse(await fs5.readFile(file, "utf8"));
1765
+ } catch {
1766
+ return null;
1767
+ }
1768
+ }
1769
+ function isProcessAlive(pid) {
1770
+ try {
1771
+ process.kill(pid, 0);
1772
+ return true;
1773
+ } catch (error) {
1774
+ return error.code === "EPERM";
1775
+ }
1776
+ }
1777
+ function isAlreadyExists(error) {
1778
+ return error.code === "EEXIST";
1779
+ }
1780
+ function delay(ms) {
1781
+ return ms > 0 ? new Promise((resolve2) => setTimeout(resolve2, ms)) : Promise.resolve();
1782
+ }
1783
+
1362
1784
  // src/runtimeInstall.ts
1363
1785
  import { cp, mkdir, readdir as readdir2, rename, rm, writeFile } from "fs/promises";
1364
1786
  import { existsSync } from "fs";
1365
1787
  import { homedir as homedir2 } from "os";
1366
- import { dirname as dirname2, join as join5 } from "path";
1788
+ import { dirname as dirname3, join as join6 } from "path";
1367
1789
  import { fileURLToPath } from "url";
1368
1790
  function defaultSourceDir() {
1369
- return dirname2(fileURLToPath(import.meta.url));
1791
+ return dirname3(fileURLToPath(import.meta.url));
1370
1792
  }
1371
1793
  function runtimeBinDir(home = homedir2()) {
1372
- return join5(home, ".llmnesia", "bin");
1794
+ return join6(home, ".llmnesia", "bin");
1373
1795
  }
1374
1796
  async function installRuntime(options = {}) {
1375
1797
  const home = options.home ?? homedir2();
1376
1798
  const sourceDir = options.sourceDir ?? defaultSourceDir();
1377
1799
  const nodePath = options.nodePath ?? process.execPath;
1378
1800
  const binDir = runtimeBinDir(home);
1379
- const sourceEntry = join5(sourceDir, "cli.js");
1801
+ const sourceEntry = join6(sourceDir, "cli.js");
1380
1802
  if (!existsSync(sourceEntry)) {
1381
1803
  throw new Error(
1382
1804
  `Cannot locate the built runtime: no cli.js in ${sourceDir}. Run this from an installed package (npx -y @llmnesia/mcp@latest install) or build first (npm run build).`
1383
1805
  );
1384
1806
  }
1385
- const parentDir = dirname2(binDir);
1807
+ const parentDir = dirname3(binDir);
1386
1808
  const nonce = `${process.pid}-${Date.now()}`;
1387
- const stagingDir = join5(parentDir, `.bin-install-${nonce}`);
1388
- const backupDir = join5(parentDir, `.bin-backup-${nonce}`);
1809
+ const stagingDir = join6(parentDir, `.bin-install-${nonce}`);
1810
+ const backupDir = join6(parentDir, `.bin-backup-${nonce}`);
1389
1811
  await mkdir(parentDir, { recursive: true });
1390
1812
  await rm(stagingDir, { recursive: true, force: true });
1391
1813
  await rm(backupDir, { recursive: true, force: true });
1392
1814
  await mkdir(stagingDir, { recursive: true });
1393
1815
  try {
1394
1816
  for (const name of await readdir2(sourceDir)) {
1395
- await cp(join5(sourceDir, name), join5(stagingDir, name), { recursive: true });
1817
+ await cp(join6(sourceDir, name), join6(stagingDir, name), { recursive: true });
1396
1818
  }
1397
1819
  await writeFile(
1398
- join5(stagingDir, "package.json"),
1820
+ join6(stagingDir, "package.json"),
1399
1821
  `${JSON.stringify(
1400
1822
  {
1401
1823
  name: "llmnesia-mcp-runtime",
@@ -1429,7 +1851,7 @@ async function installRuntime(options = {}) {
1429
1851
  } finally {
1430
1852
  await rm(stagingDir, { recursive: true, force: true });
1431
1853
  }
1432
- const entry = join5(binDir, "cli.js");
1854
+ const entry = join6(binDir, "cli.js");
1433
1855
  return {
1434
1856
  binDir,
1435
1857
  entry,
@@ -1450,7 +1872,7 @@ async function uninstallRuntime(home = homedir2()) {
1450
1872
  // src/selfUpdate.ts
1451
1873
  import { spawn } from "child_process";
1452
1874
  import { mkdtemp, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1453
- import { dirname as dirname3, join as join6 } from "path";
1875
+ import { dirname as dirname4, join as join7 } from "path";
1454
1876
  import { homedir as homedir3, tmpdir } from "os";
1455
1877
  import { fileURLToPath as fileURLToPath2 } from "url";
1456
1878
  var PACKAGE_NAME = "@llmnesia/mcp";
@@ -1460,9 +1882,9 @@ var REGISTRY_TIMEOUT_MS = 5e3;
1460
1882
  var INSTALL_TIMEOUT_MS = 12e4;
1461
1883
  var UPDATE_CHECK_DELAY_MS = 3e4;
1462
1884
  function stateFile(home) {
1463
- return join6(home, ".llmnesia", "update-state.json");
1885
+ return join7(home, ".llmnesia", "update-state.json");
1464
1886
  }
1465
- async function readState(home) {
1887
+ async function readState2(home) {
1466
1888
  try {
1467
1889
  const parsed = JSON.parse(await readFile(stateFile(home), "utf8"));
1468
1890
  return typeof parsed === "object" && parsed !== null ? parsed : {};
@@ -1470,7 +1892,7 @@ async function readState(home) {
1470
1892
  return {};
1471
1893
  }
1472
1894
  }
1473
- async function writeState(home, state) {
1895
+ async function writeState2(home, state) {
1474
1896
  try {
1475
1897
  await writeFile2(stateFile(home), `${JSON.stringify(state, null, 2)}
1476
1898
  `, "utf8");
@@ -1522,11 +1944,11 @@ async function fetchLatestFromRegistry() {
1522
1944
  }
1523
1945
  }
1524
1946
  function npmCommand() {
1525
- const binDir = dirname3(process.execPath);
1526
- return process.platform === "win32" ? join6(binDir, "npm.cmd") : join6(binDir, "npm");
1947
+ const binDir = dirname4(process.execPath);
1948
+ return process.platform === "win32" ? join7(binDir, "npm.cmd") : join7(binDir, "npm");
1527
1949
  }
1528
1950
  async function npmDownload(version) {
1529
- const prefix = await mkdtemp(join6(tmpdir(), "llmnesia-update-"));
1951
+ const prefix = await mkdtemp(join7(tmpdir(), "llmnesia-update-"));
1530
1952
  await new Promise((resolve2, reject) => {
1531
1953
  const child = spawn(
1532
1954
  npmCommand(),
@@ -1561,7 +1983,7 @@ async function npmDownload(version) {
1561
1983
  }
1562
1984
  });
1563
1985
  });
1564
- return join6(prefix, "node_modules", PACKAGE_NAME, "dist");
1986
+ return join7(prefix, "node_modules", PACKAGE_NAME, "dist");
1565
1987
  }
1566
1988
  async function maybeSelfUpdate(options = {}) {
1567
1989
  const env = options.env ?? process.env;
@@ -1570,7 +1992,7 @@ async function maybeSelfUpdate(options = {}) {
1570
1992
  }
1571
1993
  const home = options.home ?? homedir3();
1572
1994
  const currentVersion = options.currentVersion ?? VERSION;
1573
- const runningDir = options.runningDir ?? dirname3(fileURLToPath2(import.meta.url));
1995
+ const runningDir = options.runningDir ?? dirname4(fileURLToPath2(import.meta.url));
1574
1996
  if (runningDir !== runtimeBinDir(home)) {
1575
1997
  return { action: "not-installed-runtime" };
1576
1998
  }
@@ -1578,7 +2000,7 @@ async function maybeSelfUpdate(options = {}) {
1578
2000
  return { action: "not-a-release-build", currentVersion };
1579
2001
  }
1580
2002
  const now = options.now ?? Date.now;
1581
- const state = await readState(home);
2003
+ const state = await readState2(home);
1582
2004
  if (state.lastCheckedAt !== void 0 && now() - state.lastCheckedAt < UPDATE_CHECK_INTERVAL_MS) {
1583
2005
  return { action: "throttled" };
1584
2006
  }
@@ -1586,30 +2008,30 @@ async function maybeSelfUpdate(options = {}) {
1586
2008
  try {
1587
2009
  latest = await (options.fetchLatestVersion ?? fetchLatestFromRegistry)();
1588
2010
  } catch (err) {
1589
- await writeState(home, { ...state, lastCheckedAt: now() });
2011
+ await writeState2(home, { ...state, lastCheckedAt: now() });
1590
2012
  return { action: "check-failed", error: err instanceof Error ? err.message : String(err) };
1591
2013
  }
1592
2014
  const checked = { ...state, lastCheckedAt: now(), lastSeenVersion: latest };
1593
2015
  if (!isStableRelease(latest) || compareVersions(latest, currentVersion) <= 0) {
1594
- await writeState(home, checked);
2016
+ await writeState2(home, checked);
1595
2017
  return { action: "up-to-date", latest };
1596
2018
  }
1597
2019
  if (state.lastFailedVersion === latest) {
1598
- await writeState(home, checked);
2020
+ await writeState2(home, checked);
1599
2021
  return { action: "skipped-failed-before", latest };
1600
2022
  }
1601
2023
  let sourceDir;
1602
2024
  try {
1603
2025
  sourceDir = await (options.downloadPackage ?? npmDownload)(latest);
1604
2026
  await installRuntime({ home, sourceDir, version: latest });
1605
- await writeState(home, { ...checked, lastUpdatedTo: latest, lastFailedVersion: void 0 });
2027
+ await writeState2(home, { ...checked, lastUpdatedTo: latest, lastFailedVersion: void 0 });
1606
2028
  return { action: "updated", from: currentVersion, to: latest };
1607
2029
  } catch (err) {
1608
- await writeState(home, { ...checked, lastFailedVersion: latest });
2030
+ await writeState2(home, { ...checked, lastFailedVersion: latest });
1609
2031
  return { action: "update-failed", latest, error: err instanceof Error ? err.message : String(err) };
1610
2032
  } finally {
1611
2033
  if (sourceDir) {
1612
- await rm2(join6(sourceDir, "..", "..", ".."), { recursive: true, force: true }).catch(() => {
2034
+ await rm2(join7(sourceDir, "..", "..", ".."), { recursive: true, force: true }).catch(() => {
1613
2035
  });
1614
2036
  }
1615
2037
  }
@@ -1649,6 +2071,7 @@ function describeOutcome(outcome) {
1649
2071
  }
1650
2072
 
1651
2073
  export {
2074
+ readNativeHostViewerExtensionId,
1652
2075
  resolveCorpusDir,
1653
2076
  staleIndexFiles,
1654
2077
  Corpus,
@@ -1658,6 +2081,11 @@ export {
1658
2081
  SearchIndex,
1659
2082
  openSearchIndex,
1660
2083
  deleteIndexDb,
2084
+ inspectCorpusStorage,
2085
+ compactCorpus,
2086
+ AUTO_MAINTENANCE_RECHECK_MS,
2087
+ runAutomaticMaintenance,
2088
+ readAutomaticMaintenanceState,
1661
2089
  runtimeBinDir,
1662
2090
  installRuntime,
1663
2091
  uninstallRuntime,