@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
|
@@ -4,23 +4,26 @@ const require = __llmnesiaCreateRequire(import.meta.url);
|
|
|
4
4
|
import {
|
|
5
5
|
Corpus,
|
|
6
6
|
SearchIndex,
|
|
7
|
+
compactCorpus,
|
|
7
8
|
deleteIndexDb,
|
|
8
9
|
drainInbox,
|
|
9
10
|
ingestFile,
|
|
11
|
+
inspectCorpusStorage,
|
|
10
12
|
installRuntime,
|
|
11
13
|
isSelfUpdateDisabled,
|
|
12
14
|
openSearchIndex,
|
|
15
|
+
readAutomaticMaintenanceState,
|
|
13
16
|
resolveCorpusDir,
|
|
14
17
|
runtimeBinDir,
|
|
15
18
|
staleIndexFiles,
|
|
16
19
|
uninstallRuntime
|
|
17
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-HL5QE57W.js";
|
|
18
21
|
import {
|
|
19
22
|
VERSION,
|
|
20
23
|
__commonJS,
|
|
21
24
|
__require,
|
|
22
25
|
__toESM
|
|
23
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-YRK2DRGG.js";
|
|
24
27
|
|
|
25
28
|
// ../node_modules/isexe/windows.js
|
|
26
29
|
var require_windows = __commonJS({
|
|
@@ -28,7 +31,7 @@ var require_windows = __commonJS({
|
|
|
28
31
|
"use strict";
|
|
29
32
|
module.exports = isexe;
|
|
30
33
|
isexe.sync = sync;
|
|
31
|
-
var
|
|
34
|
+
var fs = __require("fs");
|
|
32
35
|
function checkPathExt(path, options) {
|
|
33
36
|
var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
|
|
34
37
|
if (!pathext) {
|
|
@@ -53,12 +56,12 @@ var require_windows = __commonJS({
|
|
|
53
56
|
return checkPathExt(path, options);
|
|
54
57
|
}
|
|
55
58
|
function isexe(path, options, cb) {
|
|
56
|
-
|
|
59
|
+
fs.stat(path, function(er, stat6) {
|
|
57
60
|
cb(er, er ? false : checkStat(stat6, path, options));
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
function sync(path, options) {
|
|
61
|
-
return checkStat(
|
|
64
|
+
return checkStat(fs.statSync(path), path, options);
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
});
|
|
@@ -69,14 +72,14 @@ var require_mode = __commonJS({
|
|
|
69
72
|
"use strict";
|
|
70
73
|
module.exports = isexe;
|
|
71
74
|
isexe.sync = sync;
|
|
72
|
-
var
|
|
75
|
+
var fs = __require("fs");
|
|
73
76
|
function isexe(path, options, cb) {
|
|
74
|
-
|
|
77
|
+
fs.stat(path, function(er, stat6) {
|
|
75
78
|
cb(er, er ? false : checkStat(stat6, options));
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
81
|
function sync(path, options) {
|
|
79
|
-
return checkStat(
|
|
82
|
+
return checkStat(fs.statSync(path), options);
|
|
80
83
|
}
|
|
81
84
|
function checkStat(stat6, options) {
|
|
82
85
|
return stat6.isFile() && checkMode(stat6, options);
|
|
@@ -101,7 +104,7 @@ var require_mode = __commonJS({
|
|
|
101
104
|
var require_isexe = __commonJS({
|
|
102
105
|
"../node_modules/isexe/index.js"(exports, module) {
|
|
103
106
|
"use strict";
|
|
104
|
-
var
|
|
107
|
+
var fs = __require("fs");
|
|
105
108
|
var core;
|
|
106
109
|
if (process.platform === "win32" || global.TESTING_WINDOWS) {
|
|
107
110
|
core = require_windows();
|
|
@@ -366,16 +369,16 @@ var require_shebang_command = __commonJS({
|
|
|
366
369
|
var require_readShebang = __commonJS({
|
|
367
370
|
"../node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
|
|
368
371
|
"use strict";
|
|
369
|
-
var
|
|
372
|
+
var fs = __require("fs");
|
|
370
373
|
var shebangCommand = require_shebang_command();
|
|
371
374
|
function readShebang(command) {
|
|
372
375
|
const size = 150;
|
|
373
376
|
const buffer = Buffer.alloc(size);
|
|
374
377
|
let fd;
|
|
375
378
|
try {
|
|
376
|
-
fd =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
fd = fs.openSync(command, "r");
|
|
380
|
+
fs.readSync(fd, buffer, 0, size, 0);
|
|
381
|
+
fs.closeSync(fd);
|
|
379
382
|
} catch (e) {
|
|
380
383
|
}
|
|
381
384
|
return shebangCommand(buffer.toString());
|
|
@@ -525,7 +528,7 @@ var require_cross_spawn = __commonJS({
|
|
|
525
528
|
|
|
526
529
|
// src/cli.ts
|
|
527
530
|
import { createInterface } from "readline/promises";
|
|
528
|
-
import { join as
|
|
531
|
+
import { join as join6 } from "path";
|
|
529
532
|
|
|
530
533
|
// ../packages/native-host/install.mjs
|
|
531
534
|
import { execFile } from "child_process";
|
|
@@ -684,6 +687,7 @@ async function installNativeHost({ ids = [], corpus, codexHome, force = false, q
|
|
|
684
687
|
}
|
|
685
688
|
const allIds = [.../* @__PURE__ */ new Set([STORE_EXTENSION_ID, ...ids])];
|
|
686
689
|
const allowedOrigins = allIds.map((id) => `chrome-extension://${id}/`);
|
|
690
|
+
const viewerExtensionId = ids[0] ?? STORE_EXTENSION_ID;
|
|
687
691
|
if (await supportedBrowserCount(isWindows2) === 0) {
|
|
688
692
|
throw new Error(
|
|
689
693
|
"LLMnesia couldn't find Chrome, Brave, Edge, or Chromium. Open your browser once so it creates a profile, then retry. Safari and Firefox are not supported yet. Nothing was installed."
|
|
@@ -717,7 +721,7 @@ async function installNativeHost({ ids = [], corpus, codexHome, force = false, q
|
|
|
717
721
|
corpusRootChanged = false;
|
|
718
722
|
await writeFile(
|
|
719
723
|
configPath,
|
|
720
|
-
`${JSON.stringify({ ...existing, corpusRoot: existingRoot, codexHome: resolvedCodexHome }, null, 2)}
|
|
724
|
+
`${JSON.stringify({ ...existing, corpusRoot: existingRoot, codexHome: resolvedCodexHome, viewerExtensionId }, null, 2)}
|
|
721
725
|
`,
|
|
722
726
|
{ encoding: "utf8", mode: 384 }
|
|
723
727
|
);
|
|
@@ -729,7 +733,7 @@ async function installNativeHost({ ids = [], corpus, codexHome, force = false, q
|
|
|
729
733
|
} else {
|
|
730
734
|
await writeFile(
|
|
731
735
|
configPath,
|
|
732
|
-
`${JSON.stringify({ ...existing ?? {}, corpusRoot, codexHome: resolvedCodexHome }, null, 2)}
|
|
736
|
+
`${JSON.stringify({ ...existing ?? {}, corpusRoot, codexHome: resolvedCodexHome, viewerExtensionId }, null, 2)}
|
|
733
737
|
`,
|
|
734
738
|
{ encoding: "utf8", mode: 384 }
|
|
735
739
|
);
|
|
@@ -3749,157 +3753,6 @@ function formatDoctorReport(report) {
|
|
|
3749
3753
|
return report.checks.map((c) => `${icon[c.status]} ${c.name}: ${c.detail}`).join("\n");
|
|
3750
3754
|
}
|
|
3751
3755
|
|
|
3752
|
-
// src/storageMaintenance.ts
|
|
3753
|
-
import { promises as fs } from "fs";
|
|
3754
|
-
import { join as join6 } from "path";
|
|
3755
|
-
async function inspectCorpusStorage(corpus) {
|
|
3756
|
-
const conversations = await corpus.conversationStorageStats();
|
|
3757
|
-
const processed = await directoryFiles(corpus.paths.processedDir);
|
|
3758
|
-
const quarantine = await directoryFiles(corpus.paths.quarantineDir);
|
|
3759
|
-
const activeNames = [
|
|
3760
|
-
corpus.paths.indexDbFile,
|
|
3761
|
-
`${corpus.paths.indexDbFile}-wal`,
|
|
3762
|
-
`${corpus.paths.indexDbFile}-shm`
|
|
3763
|
-
];
|
|
3764
|
-
const activeIndex = await explicitFiles(activeNames);
|
|
3765
|
-
const staleNames = await staleIndexFiles(corpus.paths.indexDir);
|
|
3766
|
-
const stalePaths = staleNames.flatMap((name) => {
|
|
3767
|
-
const base = join6(corpus.paths.indexDir, name);
|
|
3768
|
-
return [base, `${base}-wal`, `${base}-shm`];
|
|
3769
|
-
});
|
|
3770
|
-
const staleIndexes = await explicitFiles(stalePaths);
|
|
3771
|
-
const root = await directoryFiles(corpus.paths.root);
|
|
3772
|
-
const classifiedBytes = conversations.legacyBytes + conversations.compressedBytes + processed.bytes + quarantine.bytes + activeIndex.bytes + staleIndexes.bytes;
|
|
3773
|
-
const classifiedFiles = conversations.legacyFiles + conversations.compressedFiles + processed.files + quarantine.files + activeIndex.files + staleIndexes.files;
|
|
3774
|
-
const other = {
|
|
3775
|
-
files: Math.max(0, root.files - classifiedFiles),
|
|
3776
|
-
bytes: Math.max(0, root.bytes - classifiedBytes)
|
|
3777
|
-
};
|
|
3778
|
-
return {
|
|
3779
|
-
root: corpus.paths.root,
|
|
3780
|
-
conversations,
|
|
3781
|
-
processed,
|
|
3782
|
-
quarantine,
|
|
3783
|
-
activeIndex,
|
|
3784
|
-
staleIndexes,
|
|
3785
|
-
other,
|
|
3786
|
-
totalBytes: root.bytes,
|
|
3787
|
-
immediatelyReclaimableBytes: processed.bytes + staleIndexes.bytes
|
|
3788
|
-
};
|
|
3789
|
-
}
|
|
3790
|
-
async function compactCorpus(corpus, options = {}) {
|
|
3791
|
-
const dryRun = options.dryRun === true;
|
|
3792
|
-
const before = await inspectCorpusStorage(corpus);
|
|
3793
|
-
if (dryRun) {
|
|
3794
|
-
const compression = await corpus.migrateLegacyConversations({ dryRun: true });
|
|
3795
|
-
const compressionSavings = Math.max(
|
|
3796
|
-
0,
|
|
3797
|
-
compression.legacyBytes + compression.compressedBytes - compression.projectedCompressedBytes
|
|
3798
|
-
);
|
|
3799
|
-
return {
|
|
3800
|
-
dryRun: true,
|
|
3801
|
-
before,
|
|
3802
|
-
compression,
|
|
3803
|
-
processedRemoved: before.processed,
|
|
3804
|
-
staleIndexesRemoved: before.staleIndexes,
|
|
3805
|
-
after: null,
|
|
3806
|
-
reclaimedBytes: before.immediatelyReclaimableBytes + compressionSavings
|
|
3807
|
-
};
|
|
3808
|
-
}
|
|
3809
|
-
await corpus.init();
|
|
3810
|
-
const processedRemoved = await removeDirectoryFiles(corpus.paths.processedDir);
|
|
3811
|
-
const index = await openSearchIndex(corpus);
|
|
3812
|
-
try {
|
|
3813
|
-
index.compact();
|
|
3814
|
-
} finally {
|
|
3815
|
-
index.close();
|
|
3816
|
-
}
|
|
3817
|
-
const appliedCompression = await corpus.migrateLegacyConversations();
|
|
3818
|
-
if (appliedCompression.failures.length > 0) {
|
|
3819
|
-
throw new Error(
|
|
3820
|
-
`Corpus compression left ${appliedCompression.failures.length} legacy file(s) untouched; stale indexes were preserved. Run compact again after fixing the reported files.`
|
|
3821
|
-
);
|
|
3822
|
-
}
|
|
3823
|
-
const staleIndexesRemoved = await removeStaleIndexes(corpus);
|
|
3824
|
-
const after = await inspectCorpusStorage(corpus);
|
|
3825
|
-
return {
|
|
3826
|
-
dryRun: false,
|
|
3827
|
-
before,
|
|
3828
|
-
compression: appliedCompression,
|
|
3829
|
-
processedRemoved,
|
|
3830
|
-
staleIndexesRemoved,
|
|
3831
|
-
after,
|
|
3832
|
-
reclaimedBytes: Math.max(0, before.totalBytes - after.totalBytes)
|
|
3833
|
-
};
|
|
3834
|
-
}
|
|
3835
|
-
async function removeStaleIndexes(corpus) {
|
|
3836
|
-
const names = await staleIndexFiles(corpus.paths.indexDir);
|
|
3837
|
-
const paths2 = names.flatMap((name) => {
|
|
3838
|
-
const base = join6(corpus.paths.indexDir, name);
|
|
3839
|
-
return [base, `${base}-wal`, `${base}-shm`];
|
|
3840
|
-
});
|
|
3841
|
-
const before = await explicitFiles(paths2);
|
|
3842
|
-
for (const name of names) {
|
|
3843
|
-
await deleteIndexDb(join6(corpus.paths.indexDir, name));
|
|
3844
|
-
}
|
|
3845
|
-
return before;
|
|
3846
|
-
}
|
|
3847
|
-
async function removeDirectoryFiles(dir) {
|
|
3848
|
-
const before = await directoryFiles(dir);
|
|
3849
|
-
await removeFilesRecursively(dir);
|
|
3850
|
-
return before;
|
|
3851
|
-
}
|
|
3852
|
-
async function removeFilesRecursively(dir) {
|
|
3853
|
-
let entries;
|
|
3854
|
-
try {
|
|
3855
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
3856
|
-
} catch {
|
|
3857
|
-
return;
|
|
3858
|
-
}
|
|
3859
|
-
for (const entry of entries) {
|
|
3860
|
-
const path = join6(dir, entry.name);
|
|
3861
|
-
if (entry.isDirectory()) await removeFilesRecursively(path);
|
|
3862
|
-
else if (entry.isFile()) await fs.rm(path, { force: true });
|
|
3863
|
-
}
|
|
3864
|
-
}
|
|
3865
|
-
async function explicitFiles(files) {
|
|
3866
|
-
let count = 0;
|
|
3867
|
-
let bytes = 0;
|
|
3868
|
-
for (const file of files) {
|
|
3869
|
-
try {
|
|
3870
|
-
const stat6 = await fs.stat(file);
|
|
3871
|
-
if (stat6.isFile()) {
|
|
3872
|
-
count += 1;
|
|
3873
|
-
bytes += stat6.size;
|
|
3874
|
-
}
|
|
3875
|
-
} catch {
|
|
3876
|
-
}
|
|
3877
|
-
}
|
|
3878
|
-
return { files: count, bytes };
|
|
3879
|
-
}
|
|
3880
|
-
async function directoryFiles(dir) {
|
|
3881
|
-
let entries;
|
|
3882
|
-
try {
|
|
3883
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
3884
|
-
} catch {
|
|
3885
|
-
return { files: 0, bytes: 0 };
|
|
3886
|
-
}
|
|
3887
|
-
let files = 0;
|
|
3888
|
-
let bytes = 0;
|
|
3889
|
-
for (const entry of entries) {
|
|
3890
|
-
const path = join6(dir, entry.name);
|
|
3891
|
-
if (entry.isDirectory()) {
|
|
3892
|
-
const nested = await directoryFiles(path);
|
|
3893
|
-
files += nested.files;
|
|
3894
|
-
bytes += nested.bytes;
|
|
3895
|
-
} else if (entry.isFile()) {
|
|
3896
|
-
files += 1;
|
|
3897
|
-
bytes += (await fs.stat(path)).size;
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
return { files, bytes };
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
3756
|
// src/cli.ts
|
|
3904
3757
|
var USAGE = `llmnesia-mcp ${VERSION}
|
|
3905
3758
|
|
|
@@ -4168,6 +4021,7 @@ async function main() {
|
|
|
4168
4021
|
case "stats": {
|
|
4169
4022
|
const stats = await corpus.stats();
|
|
4170
4023
|
const storage = await inspectCorpusStorage(corpus);
|
|
4024
|
+
const maintenance = await readAutomaticMaintenanceState(corpus);
|
|
4171
4025
|
const platforms = Object.entries(stats.byPlatform).sort((a, b) => b[1] - a[1]).map(([p, n]) => ` ${p}: ${n}`).join("\n");
|
|
4172
4026
|
process.stdout.write(
|
|
4173
4027
|
`Corpus: ${root}
|
|
@@ -4181,7 +4035,8 @@ ${platforms}
|
|
|
4181
4035
|
processed: ${formatBytes(storage.processed.bytes)} (${storage.processed.files} reclaimable files)
|
|
4182
4036
|
stale indexes: ${formatBytes(storage.staleIndexes.bytes)}
|
|
4183
4037
|
quarantine: ${formatBytes(storage.quarantine.bytes)}
|
|
4184
|
-
`
|
|
4038
|
+
` + (maintenance ? ` maintenance: ${formatMaintenanceState(maintenance)}
|
|
4039
|
+
` : "")
|
|
4185
4040
|
);
|
|
4186
4041
|
return 0;
|
|
4187
4042
|
}
|
|
@@ -4220,7 +4075,7 @@ Corpus now uses ${formatBytes(result.after?.totalBytes ?? 0)}.
|
|
|
4220
4075
|
}
|
|
4221
4076
|
const stale = await staleIndexFiles(corpus.paths.indexDir);
|
|
4222
4077
|
for (const name of stale) {
|
|
4223
|
-
await deleteIndexDb(
|
|
4078
|
+
await deleteIndexDb(join6(corpus.paths.indexDir, name));
|
|
4224
4079
|
}
|
|
4225
4080
|
if (stale.length > 0) {
|
|
4226
4081
|
process.stdout.write(
|
|
@@ -4287,6 +4142,18 @@ function formatBytes(bytes) {
|
|
|
4287
4142
|
}
|
|
4288
4143
|
return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`;
|
|
4289
4144
|
}
|
|
4145
|
+
function formatMaintenanceState(state) {
|
|
4146
|
+
if (state.status === "complete") {
|
|
4147
|
+
return `complete${state.completedAt ? ` (${new Date(state.completedAt).toISOString()})` : ""}`;
|
|
4148
|
+
}
|
|
4149
|
+
if (state.status === "waiting" && state.nextEligibleAt) {
|
|
4150
|
+
return `automatic cleanup scheduled after ${new Date(state.nextEligibleAt).toISOString()}`;
|
|
4151
|
+
}
|
|
4152
|
+
if (state.status === "failed" && state.nextEligibleAt) {
|
|
4153
|
+
return `will retry after ${new Date(state.nextEligibleAt).toISOString()}`;
|
|
4154
|
+
}
|
|
4155
|
+
return state.status;
|
|
4156
|
+
}
|
|
4290
4157
|
function formatCompactPreview(result) {
|
|
4291
4158
|
const { before, compression } = result;
|
|
4292
4159
|
const compressionSavings = Math.max(
|
package/dist/cli.js
CHANGED
|
@@ -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/nodeDownload.ts
|
|
9
9
|
var NODE_DOWNLOAD_URLS = {
|
|
@@ -141,7 +141,7 @@ async function bootstrap() {
|
|
|
141
141
|
await guideUnsupportedNodeVersion(process.versions.node);
|
|
142
142
|
return 1;
|
|
143
143
|
}
|
|
144
|
-
const { main } = await import("./cli-
|
|
144
|
+
const { main } = await import("./cli-DRB4DVH3.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|
package/dist/llmnesia.mcpb
CHANGED
|
Binary file
|
package/dist/mcp.js
CHANGED
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
import { createRequire as __llmnesiaCreateRequire } from "node:module";
|
|
3
3
|
const require = __llmnesiaCreateRequire(import.meta.url);
|
|
4
4
|
import {
|
|
5
|
+
AUTO_MAINTENANCE_RECHECK_MS,
|
|
5
6
|
Corpus,
|
|
6
7
|
describeOutcome,
|
|
7
8
|
drainInbox,
|
|
8
9
|
openSearchIndex,
|
|
9
10
|
pendingInboxFiles,
|
|
11
|
+
readNativeHostViewerExtensionId,
|
|
10
12
|
resolveCorpusDir,
|
|
13
|
+
runAutomaticMaintenance,
|
|
11
14
|
scheduleSelfUpdate
|
|
12
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-HL5QE57W.js";
|
|
13
16
|
import {
|
|
14
17
|
VERSION,
|
|
15
18
|
__commonJS,
|
|
16
19
|
__export,
|
|
17
20
|
__toESM
|
|
18
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-YRK2DRGG.js";
|
|
19
22
|
|
|
20
23
|
// ../node_modules/ajv/dist/compile/codegen/code.js
|
|
21
24
|
var require_code = __commonJS({
|
|
@@ -21393,6 +21396,9 @@ function describeIssues(error2) {
|
|
|
21393
21396
|
|
|
21394
21397
|
// src/service.ts
|
|
21395
21398
|
var STALE_AFTER_HOURS = 48;
|
|
21399
|
+
var AUTO_MAINTENANCE_START_DELAY_MS = 6e4;
|
|
21400
|
+
var AUTO_MAINTENANCE_IDLE_MS = 3e4;
|
|
21401
|
+
var AUTO_MAINTENANCE_BUSY_RETRY_MS = 3e4;
|
|
21396
21402
|
var CompanionService = class _CompanionService {
|
|
21397
21403
|
constructor(corpus, index, onDrainError, memoryWriteCap) {
|
|
21398
21404
|
this.corpus = corpus;
|
|
@@ -21409,6 +21415,10 @@ var CompanionService = class _CompanionService {
|
|
|
21409
21415
|
refreshChain = Promise.resolve();
|
|
21410
21416
|
/** Memories written since this process started; the per-session cap counts these. */
|
|
21411
21417
|
memoriesWritten = 0;
|
|
21418
|
+
activeOperations = 0;
|
|
21419
|
+
lastOperationAt = Date.now();
|
|
21420
|
+
maintenanceTimer;
|
|
21421
|
+
closed = false;
|
|
21412
21422
|
static async open(root, options = {}) {
|
|
21413
21423
|
const corpus = new Corpus(root);
|
|
21414
21424
|
const index = await openSearchIndex(corpus);
|
|
@@ -21419,8 +21429,71 @@ var CompanionService = class _CompanionService {
|
|
|
21419
21429
|
options.memoryWriteCap ?? DEFAULT_MEMORY_WRITE_CAP
|
|
21420
21430
|
);
|
|
21421
21431
|
await service.refresh();
|
|
21432
|
+
if (options.automaticMaintenance) {
|
|
21433
|
+
service.scheduleAutomaticMaintenance(options.automaticMaintenance);
|
|
21434
|
+
}
|
|
21422
21435
|
return service;
|
|
21423
21436
|
}
|
|
21437
|
+
/** Let the MCP wrapper expose activity to the idle-only maintenance scheduler. */
|
|
21438
|
+
async runWithActivity(operation) {
|
|
21439
|
+
this.activeOperations += 1;
|
|
21440
|
+
this.lastOperationAt = Date.now();
|
|
21441
|
+
try {
|
|
21442
|
+
return await operation();
|
|
21443
|
+
} finally {
|
|
21444
|
+
this.activeOperations -= 1;
|
|
21445
|
+
this.lastOperationAt = Date.now();
|
|
21446
|
+
}
|
|
21447
|
+
}
|
|
21448
|
+
scheduleAutomaticMaintenance(options) {
|
|
21449
|
+
const {
|
|
21450
|
+
delayMs = AUTO_MAINTENANCE_START_DELAY_MS,
|
|
21451
|
+
idleMs = AUTO_MAINTENANCE_IDLE_MS,
|
|
21452
|
+
busyRetryMs = AUTO_MAINTENANCE_BUSY_RETRY_MS,
|
|
21453
|
+
onOutcome,
|
|
21454
|
+
...runOptions
|
|
21455
|
+
} = options;
|
|
21456
|
+
const schedule = (delay) => {
|
|
21457
|
+
if (this.closed) return;
|
|
21458
|
+
this.maintenanceTimer = setTimeout(
|
|
21459
|
+
attempt,
|
|
21460
|
+
Math.max(0, Math.min(delay, 2147483647))
|
|
21461
|
+
);
|
|
21462
|
+
this.maintenanceTimer.unref?.();
|
|
21463
|
+
};
|
|
21464
|
+
const attempt = () => {
|
|
21465
|
+
this.maintenanceTimer = void 0;
|
|
21466
|
+
if (this.closed) return;
|
|
21467
|
+
const idleFor = Date.now() - this.lastOperationAt;
|
|
21468
|
+
if (this.activeOperations > 0 || idleFor < idleMs) {
|
|
21469
|
+
schedule(Math.max(busyRetryMs, idleMs - idleFor));
|
|
21470
|
+
return;
|
|
21471
|
+
}
|
|
21472
|
+
void runAutomaticMaintenance(this.corpus, this.index, runOptions).then(
|
|
21473
|
+
(outcome) => {
|
|
21474
|
+
onOutcome?.(outcome);
|
|
21475
|
+
const current = Date.now();
|
|
21476
|
+
if (outcome.action === "deferred") schedule(outcome.until - current);
|
|
21477
|
+
else if (outcome.action === "failed") schedule(outcome.retryAt - current);
|
|
21478
|
+
else if (outcome.action === "locked") schedule(busyRetryMs);
|
|
21479
|
+
else if (outcome.action === "recently-checked") {
|
|
21480
|
+
schedule(outcome.completedAt + (runOptions.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS) - current);
|
|
21481
|
+
} else if (outcome.action === "compacted" || outcome.action === "no-work") {
|
|
21482
|
+
schedule(runOptions.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS);
|
|
21483
|
+
}
|
|
21484
|
+
},
|
|
21485
|
+
(error2) => {
|
|
21486
|
+
onOutcome?.({
|
|
21487
|
+
action: "failed",
|
|
21488
|
+
error: error2 instanceof Error ? error2.message : String(error2),
|
|
21489
|
+
retryAt: Date.now() + busyRetryMs
|
|
21490
|
+
});
|
|
21491
|
+
schedule(busyRetryMs);
|
|
21492
|
+
}
|
|
21493
|
+
);
|
|
21494
|
+
};
|
|
21495
|
+
schedule(delayMs);
|
|
21496
|
+
}
|
|
21424
21497
|
/**
|
|
21425
21498
|
* Ingest whatever is waiting in `inbox/`. Never throws: a corrupt or
|
|
21426
21499
|
* unreadable delta is reported and skipped rather than failing the read.
|
|
@@ -21547,6 +21620,9 @@ var CompanionService = class _CompanionService {
|
|
|
21547
21620
|
};
|
|
21548
21621
|
}
|
|
21549
21622
|
close() {
|
|
21623
|
+
this.closed = true;
|
|
21624
|
+
if (this.maintenanceTimer) clearTimeout(this.maintenanceTimer);
|
|
21625
|
+
this.maintenanceTimer = void 0;
|
|
21550
21626
|
this.index.close();
|
|
21551
21627
|
}
|
|
21552
21628
|
};
|
|
@@ -21602,7 +21678,20 @@ var LazyResource = class {
|
|
|
21602
21678
|
function jsonResult(payload) {
|
|
21603
21679
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
21604
21680
|
}
|
|
21605
|
-
var SOURCE_LINK_INSTRUCTION = "When answering from these conversations, always include a Sources section with a clickable Markdown link to every
|
|
21681
|
+
var SOURCE_LINK_INSTRUCTION = "When answering from these conversations, always include a Sources section with a clickable Markdown link to every chat you relied on. Link its `sourceUrl`: this is the original chat URL when available, otherwise its local LLMnesia Viewer URL. Do this automatically; do not wait for the user to ask for links.";
|
|
21682
|
+
var STORE_EXTENSION_ID = "leekfgbdojiaabifbjbbgiiclannjdkf";
|
|
21683
|
+
var EXTENSION_ID_RE = /^[a-p]{32}$/;
|
|
21684
|
+
function viewerExtensionId() {
|
|
21685
|
+
const configured = process.env.LLMNESIA_VIEWER_EXTENSION_ID?.trim() ?? readNativeHostViewerExtensionId();
|
|
21686
|
+
return configured && EXTENSION_ID_RE.test(configured) ? configured : STORE_EXTENSION_ID;
|
|
21687
|
+
}
|
|
21688
|
+
function viewerUrl(docId) {
|
|
21689
|
+
return `chrome-extension://${viewerExtensionId()}/viewer.html?docId=${encodeURIComponent(docId)}`;
|
|
21690
|
+
}
|
|
21691
|
+
function sourceLinks(docId, originalUrl) {
|
|
21692
|
+
const localViewerUrl = viewerUrl(docId);
|
|
21693
|
+
return originalUrl ? { viewerUrl: localViewerUrl, sourceUrl: originalUrl, sourceKind: "original" } : { viewerUrl: localViewerUrl, sourceUrl: localViewerUrl, sourceKind: "viewer" };
|
|
21694
|
+
}
|
|
21606
21695
|
function sourcedJsonResult(payload) {
|
|
21607
21696
|
return jsonResult({ responseInstruction: SOURCE_LINK_INSTRUCTION, ...payload });
|
|
21608
21697
|
}
|
|
@@ -21636,12 +21725,28 @@ async function runServe() {
|
|
|
21636
21725
|
onDrainError: (error2) => process.stderr.write(
|
|
21637
21726
|
`llmnesia: inbox drain failed, serving the existing index: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
21638
21727
|
`
|
|
21639
|
-
)
|
|
21728
|
+
),
|
|
21729
|
+
automaticMaintenance: {
|
|
21730
|
+
onOutcome: (outcome) => {
|
|
21731
|
+
if (outcome.action === "failed") {
|
|
21732
|
+
process.stderr.write(
|
|
21733
|
+
`llmnesia: automatic storage maintenance will retry later: ${outcome.error}
|
|
21734
|
+
`
|
|
21735
|
+
);
|
|
21736
|
+
} else if (outcome.action === "compacted" && outcome.report.reclaimedBytes > 0) {
|
|
21737
|
+
process.stderr.write(
|
|
21738
|
+
`llmnesia: automatic storage maintenance reclaimed ${outcome.report.reclaimedBytes} bytes
|
|
21739
|
+
`
|
|
21740
|
+
);
|
|
21741
|
+
}
|
|
21742
|
+
}
|
|
21743
|
+
}
|
|
21640
21744
|
})
|
|
21641
21745
|
);
|
|
21642
21746
|
const withService = async (operation) => {
|
|
21643
21747
|
try {
|
|
21644
|
-
|
|
21748
|
+
const ready = await service.get(5e3);
|
|
21749
|
+
return await ready.runWithActivity(() => operation(ready));
|
|
21645
21750
|
} catch (err) {
|
|
21646
21751
|
if (err instanceof ResourcePreparingError) {
|
|
21647
21752
|
return errorResult("LLMnesia is still preparing the local conversation index. Retry this tool shortly.");
|
|
@@ -21652,14 +21757,14 @@ async function runServe() {
|
|
|
21652
21757
|
const server = new McpServer(
|
|
21653
21758
|
{ name: "llmnesia", version: VERSION },
|
|
21654
21759
|
{
|
|
21655
|
-
instructions: "LLMnesia is the user's searchable archive of past AI chats. Whenever the user asks about 'my chats', 'my conversations', or anything they previously said, discussed, decided, or worked on, use LLMnesia before built-in task/thread lists, local file scans, or memory, unless they explicitly ask only about Codex tasks. Use list_conversations with sort='oldest' for their earliest chat, search_conversations to find relevant prior chats, get_conversation to read a full transcript, and list_recent to see what they worked on lately. Search and list results carry a `corpus` block saying when the corpus last received data; if a chat the user expects is missing, check corpus_status before concluding it does not exist. Whenever you answer using LLMnesia, always include a Sources section with clickable links to the
|
|
21760
|
+
instructions: "LLMnesia is the user's searchable archive of past AI chats. Whenever the user asks about 'my chats', 'my conversations', or anything they previously said, discussed, decided, or worked on, use LLMnesia before built-in task/thread lists, local file scans, or memory, unless they explicitly ask only about Codex tasks. Use list_conversations with sort='oldest' for their earliest chat, search_conversations to find relevant prior chats, get_conversation to read a full transcript, and list_recent to see what they worked on lately. Search and list results carry a `corpus` block saying when the corpus last received data; if a chat the user expects is missing, check corpus_status before concluding it does not exist. Whenever you answer using LLMnesia, always include a Sources section with clickable links to the chats you used. Link each result's `sourceUrl`, which is its original URL when available or its local LLMnesia Viewer URL otherwise; never wait for the user to ask."
|
|
21656
21761
|
}
|
|
21657
21762
|
);
|
|
21658
21763
|
server.registerTool(
|
|
21659
21764
|
"search_conversations",
|
|
21660
21765
|
{
|
|
21661
21766
|
title: "Search conversations",
|
|
21662
|
-
description: "Full-text search across the user's past AI conversations. Call this whenever the user asks what they said, thought, discussed, decided, or worked on previously \u2014 even if the question doesn't say 'search' \u2014 rather than answering from conversation memory alone or claiming no record exists. Returns matching conversations with a highlighted snippet, ranked by relevance. Defaults to recall-first matching (any term); pass match_mode to answer questions where a wrong hit matters, such as whether or how often something was discussed. Every result includes
|
|
21767
|
+
description: "Full-text search across the user's past AI conversations. Call this whenever the user asks what they said, thought, discussed, decided, or worked on previously \u2014 even if the question doesn't say 'search' \u2014 rather than answering from conversation memory alone or claiming no record exists. Returns matching conversations with a highlighted snippet, ranked by relevance. Defaults to recall-first matching (any term); pass match_mode to answer questions where a wrong hit matters, such as whether or how often something was discussed. Every result includes `sourceUrl`: the original chat URL when available, otherwise its local LLMnesia Viewer URL. Always link the chats used in the answer.",
|
|
21663
21768
|
inputSchema: {
|
|
21664
21769
|
query: external_exports.string().describe("Search terms (natural words; FTS operators are not needed)."),
|
|
21665
21770
|
match_mode: external_exports.enum(["any", "all", "phrase"]).optional().describe(
|
|
@@ -21698,6 +21803,7 @@ async function runServe() {
|
|
|
21698
21803
|
title: h.title,
|
|
21699
21804
|
platform: h.platform,
|
|
21700
21805
|
url: h.url,
|
|
21806
|
+
...sourceLinks(h.docId, h.url),
|
|
21701
21807
|
createdAt: h.createdAt,
|
|
21702
21808
|
score: Number(h.score.toFixed(4)),
|
|
21703
21809
|
snippet: h.snippet,
|
|
@@ -21716,7 +21822,7 @@ async function runServe() {
|
|
|
21716
21822
|
"get_conversation",
|
|
21717
21823
|
{
|
|
21718
21824
|
title: "Get conversation",
|
|
21719
|
-
description: "Return the full transcript of one conversation by its id (the `id` from search results). The conversation includes its original-chat URL when available
|
|
21825
|
+
description: "Return the full transcript of one conversation by its id (the `id` from search results). The conversation includes `sourceUrl`: its original-chat URL when available, otherwise its local LLMnesia Viewer URL. Always link it in an answer based on this transcript.",
|
|
21720
21826
|
inputSchema: {
|
|
21721
21827
|
id: external_exports.string().describe("The conversation id (docId), e.g. 'claude:abc-123'.")
|
|
21722
21828
|
},
|
|
@@ -21733,7 +21839,10 @@ async function runServe() {
|
|
|
21733
21839
|
return errorResult(`No conversation found with id '${args.id}'.`);
|
|
21734
21840
|
}
|
|
21735
21841
|
return sourcedJsonResult({
|
|
21736
|
-
conversation:
|
|
21842
|
+
conversation: {
|
|
21843
|
+
...record2.conversation,
|
|
21844
|
+
...sourceLinks(record2.conversation.docId, record2.conversation.url)
|
|
21845
|
+
},
|
|
21737
21846
|
messages: record2.messages.map((m) => ({
|
|
21738
21847
|
index: m.msgIndex,
|
|
21739
21848
|
role: m.role,
|
|
@@ -21747,7 +21856,7 @@ async function runServe() {
|
|
|
21747
21856
|
"list_recent",
|
|
21748
21857
|
{
|
|
21749
21858
|
title: "List recent conversations",
|
|
21750
|
-
description: "List the user's most recently updated conversations (title + summary +
|
|
21859
|
+
description: "List the user's most recently updated conversations (title + summary + source URL), newest first. `sourceUrl` is the original chat URL when available, otherwise its local LLMnesia Viewer URL. Always link the chats used in the answer.",
|
|
21751
21860
|
inputSchema: {
|
|
21752
21861
|
limit: external_exports.number().int().min(1).max(100).optional().describe("How many to return (default 10).")
|
|
21753
21862
|
},
|
|
@@ -21770,6 +21879,7 @@ async function runServe() {
|
|
|
21770
21879
|
title: e.title,
|
|
21771
21880
|
platform: e.platform,
|
|
21772
21881
|
url: e.url,
|
|
21882
|
+
...sourceLinks(e.docId, e.url),
|
|
21773
21883
|
updatedAt: e.updatedAt,
|
|
21774
21884
|
messageCount: e.messageCount,
|
|
21775
21885
|
pinned: e.pinned,
|
|
@@ -21786,7 +21896,7 @@ async function runServe() {
|
|
|
21786
21896
|
"list_conversations",
|
|
21787
21897
|
{
|
|
21788
21898
|
title: "List conversations by date",
|
|
21789
|
-
description: "List conversation metadata in chronological order. Use sort='oldest' for the user's earliest or oldest chat, sort='newest' for most recently created chats, and sort='recent' for most recently updated chats. This is the complete corpus chronology, so do not scan LLMnesia's private files with shell commands. Entries include original
|
|
21899
|
+
description: "List conversation metadata in chronological order. Use sort='oldest' for the user's earliest or oldest chat, sort='newest' for most recently created chats, and sort='recent' for most recently updated chats. This is the complete corpus chronology, so do not scan LLMnesia's private files with shell commands. Entries include `sourceUrl`: the original chat URL when available, otherwise the local LLMnesia Viewer URL. Always link the chats used in the answer.",
|
|
21790
21900
|
inputSchema: {
|
|
21791
21901
|
sort: external_exports.enum(["recent", "oldest", "newest"]).optional().describe("Chronology: recent (default, updated), oldest (created), or newest (created)."),
|
|
21792
21902
|
limit: external_exports.number().int().min(1).max(100).optional().describe("How many to return (default 10).")
|
|
@@ -21811,6 +21921,7 @@ async function runServe() {
|
|
|
21811
21921
|
title: e.title,
|
|
21812
21922
|
platform: e.platform,
|
|
21813
21923
|
url: e.url,
|
|
21924
|
+
...sourceLinks(e.docId, e.url),
|
|
21814
21925
|
createdAt: e.createdAt,
|
|
21815
21926
|
updatedAt: e.updatedAt,
|
|
21816
21927
|
messageCount: e.messageCount,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmnesia/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"comment:mcpName": "The MCP Registry's ownership marker: it must equal server.json's `name`, and the namespace must match how you authenticate with mcp-publisher. `com.llmnesia/...` requires DNS auth (a TXT record on llmnesia.com) — see docs/MCP_INSTALLER_BOARD.md.",
|
package/server.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"name": "com.llmnesia/mcp",
|
|
4
4
|
"title": "LLMnesia",
|
|
5
5
|
"description": "Search your AI chat history from Claude, Cursor, Codex, and other MCP clients.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.6",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
11
11
|
"identifier": "@llmnesia/mcp",
|
|
12
|
-
"version": "0.2.
|
|
12
|
+
"version": "0.2.6",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|