@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.
- package/README.md +13 -4
- package/dist/{chunk-NSVHWVEK.js → chunk-HL5QE57W.js} +469 -41
- package/dist/{chunk-JSNSTAFD.js → chunk-YRK2DRGG.js} +1 -1
- package/dist/{cli-ZEA7WTVW.js → cli-DRB4DVH3.js} +36 -169
- package/dist/cli.js +2 -2
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +122 -11
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -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-
|
|
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
|
|
16
|
+
function readNativeHostConfig() {
|
|
17
17
|
try {
|
|
18
18
|
const parsed = JSON.parse(readFileSync(nativeHostConfigPath(), "utf8"));
|
|
19
|
-
return typeof parsed
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
1788
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1367
1789
|
import { fileURLToPath } from "url";
|
|
1368
1790
|
function defaultSourceDir() {
|
|
1369
|
-
return
|
|
1791
|
+
return dirname3(fileURLToPath(import.meta.url));
|
|
1370
1792
|
}
|
|
1371
1793
|
function runtimeBinDir(home = homedir2()) {
|
|
1372
|
-
return
|
|
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 =
|
|
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 =
|
|
1807
|
+
const parentDir = dirname3(binDir);
|
|
1386
1808
|
const nonce = `${process.pid}-${Date.now()}`;
|
|
1387
|
-
const stagingDir =
|
|
1388
|
-
const backupDir =
|
|
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(
|
|
1817
|
+
await cp(join6(sourceDir, name), join6(stagingDir, name), { recursive: true });
|
|
1396
1818
|
}
|
|
1397
1819
|
await writeFile(
|
|
1398
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1885
|
+
return join7(home, ".llmnesia", "update-state.json");
|
|
1464
1886
|
}
|
|
1465
|
-
async function
|
|
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
|
|
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 =
|
|
1526
|
-
return process.platform === "win32" ?
|
|
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(
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
2016
|
+
await writeState2(home, checked);
|
|
1595
2017
|
return { action: "up-to-date", latest };
|
|
1596
2018
|
}
|
|
1597
2019
|
if (state.lastFailedVersion === latest) {
|
|
1598
|
-
await
|
|
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
|
|
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
|
|
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(
|
|
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,
|