@llmnesia/mcp 0.2.3 → 0.2.5

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.
@@ -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-G42RV536.js";
20
+ } from "./chunk-MTPXE734.js";
18
21
  import {
19
22
  VERSION,
20
23
  __commonJS,
21
24
  __require,
22
25
  __toESM
23
- } from "./chunk-6H2TI5IW.js";
26
+ } from "./chunk-BDR4JVMF.js";
24
27
 
25
28
  // ../node_modules/isexe/windows.js
26
29
  var require_windows = __commonJS({
@@ -824,7 +827,7 @@ var OPTION_ALIASES = {
824
827
  h: "help",
825
828
  y: "yes"
826
829
  };
827
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["force", "yes", "version", "help"]);
830
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["force", "yes", "version", "help", "dry-run"]);
828
831
  var VALUE_OPTIONS = /* @__PURE__ */ new Set(["corpus", "limit", "platform", "title", "from", "to"]);
829
832
  function parseArgs(argv) {
830
833
  const positionals = [];
@@ -3765,9 +3768,12 @@ Setup:
3765
3768
  Power-user / debugging:
3766
3769
  init Create the corpus directory skeleton
3767
3770
  ingest <file...> Ingest one or more NDJSON backup files into the corpus
3768
- drain Ingest every file in <corpus>/inbox, then archive them
3771
+ drain Ingest every file in <corpus>/inbox; discard clean
3772
+ payloads and quarantine malformed ones
3769
3773
  watch Watch <corpus>/inbox and ingest new deltas live
3770
3774
  stats Print corpus counts (conversations, messages, platforms)
3775
+ compact Losslessly compress conversations and remove redundant
3776
+ processed batches/stale indexes (confirmation required)
3771
3777
  reindex Drop and rebuild the search index from the corpus
3772
3778
  search <query> Full-text search the corpus (BM25)
3773
3779
 
@@ -3780,6 +3786,7 @@ Options:
3780
3786
  --to <iso|ms> search: only conversations created on/before this date
3781
3787
  --force install: overwrite a differing corpus root in config.json
3782
3788
  --yes, -y uninstall: skip its confirmation prompt
3789
+ --dry-run compact: calculate exact savings without writing
3783
3790
  --version, -v print the version and exit
3784
3791
  `;
3785
3792
  function parseDate(value) {
@@ -3984,7 +3991,7 @@ async function main() {
3984
3991
  `)
3985
3992
  });
3986
3993
  process.stdout.write(
3987
- `Drained ${report.files} inbox file(s): ${report.conversations} conversation(s) \u2014 ${report.created} created, ${report.updated} updated, ${report.skipped} unchanged` + (report.failed > 0 ? `, ${report.failed} file(s) failed` : "") + `
3994
+ `Drained ${report.files} inbox file(s): ${report.conversations} conversation(s) \u2014 ${report.created} created, ${report.updated} updated, ${report.skipped} unchanged` + (report.quarantined > 0 ? `, ${report.quarantined} quarantined` : "") + (report.failed > 0 ? `, ${report.failed} file(s) failed` : "") + `
3988
3995
  `
3989
3996
  );
3990
3997
  } finally {
@@ -4012,6 +4019,8 @@ async function main() {
4012
4019
  }
4013
4020
  case "stats": {
4014
4021
  const stats = await corpus.stats();
4022
+ const storage = await inspectCorpusStorage(corpus);
4023
+ const maintenance = await readAutomaticMaintenanceState(corpus);
4015
4024
  const platforms = Object.entries(stats.byPlatform).sort((a, b) => b[1] - a[1]).map(([p, n]) => ` ${p}: ${n}`).join("\n");
4016
4025
  process.stdout.write(
4017
4026
  `Corpus: ${root}
@@ -4019,14 +4028,43 @@ async function main() {
4019
4028
  messages: ${stats.messageCount}
4020
4029
  ` + (platforms ? ` by platform:
4021
4030
  ${platforms}
4031
+ ` : "") + ` storage: ${formatBytes(storage.totalBytes)}
4032
+ conversations: ${formatBytes(storage.conversations.legacyBytes + storage.conversations.compressedBytes)} (${storage.conversations.compressedFiles} compressed, ${storage.conversations.legacyFiles} legacy)
4033
+ active index: ${formatBytes(storage.activeIndex.bytes)}
4034
+ processed: ${formatBytes(storage.processed.bytes)} (${storage.processed.files} reclaimable files)
4035
+ stale indexes: ${formatBytes(storage.staleIndexes.bytes)}
4036
+ quarantine: ${formatBytes(storage.quarantine.bytes)}
4037
+ ` + (maintenance ? ` maintenance: ${formatMaintenanceState(maintenance)}
4022
4038
  ` : "")
4023
4039
  );
4024
4040
  return 0;
4025
4041
  }
4042
+ case "compact": {
4043
+ const dryRun = flags.has("dry-run");
4044
+ const preview = await compactCorpus(corpus, { dryRun: true });
4045
+ process.stdout.write(formatCompactPreview(preview));
4046
+ if (dryRun) return 0;
4047
+ if (!flags.has("yes")) {
4048
+ const ok = await promptConfirm(
4049
+ "Compact this corpus now? Close desktop AI/MCP clients first. Clean processed sync payloads and stale derived indexes will be removed; canonical conversations are migrated losslessly with read-back verification."
4050
+ );
4051
+ if (!ok) {
4052
+ process.stdout.write("Cancelled.\n");
4053
+ return 0;
4054
+ }
4055
+ }
4056
+ const result = await compactCorpus(corpus);
4057
+ process.stdout.write(
4058
+ `Compaction complete: reclaimed ${formatBytes(result.reclaimedBytes)}; ${result.compression.migrated} conversation(s) compressed, ${result.processedRemoved.files} processed file(s) removed, ${result.staleIndexesRemoved.files} stale index file(s) removed.
4059
+ Corpus now uses ${formatBytes(result.after?.totalBytes ?? 0)}.
4060
+ `
4061
+ );
4062
+ return 0;
4063
+ }
4026
4064
  case "reindex": {
4027
4065
  await corpus.init();
4028
4066
  await deleteIndexDb(corpus.paths.indexDbFile);
4029
- const index = new SearchIndex(corpus.paths.indexDbFile);
4067
+ const index = new SearchIndex(corpus.paths.indexDbFile, corpus);
4030
4068
  try {
4031
4069
  const { conversations } = await index.rebuildFrom(corpus);
4032
4070
  process.stdout.write(`Rebuilt index: ${conversations} conversation(s)
@@ -4092,6 +4130,43 @@ ${USAGE}`);
4092
4130
  function collapse(text) {
4093
4131
  return text.replace(/\s+/g, " ").trim();
4094
4132
  }
4133
+ function formatBytes(bytes) {
4134
+ if (bytes < 1024) return `${bytes} B`;
4135
+ const units = ["KiB", "MiB", "GiB", "TiB"];
4136
+ let value = bytes / 1024;
4137
+ let unit = units[0];
4138
+ for (let i = 1; i < units.length && value >= 1024; i += 1) {
4139
+ value /= 1024;
4140
+ unit = units[i];
4141
+ }
4142
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`;
4143
+ }
4144
+ function formatMaintenanceState(state) {
4145
+ if (state.status === "complete") {
4146
+ return `complete${state.completedAt ? ` (${new Date(state.completedAt).toISOString()})` : ""}`;
4147
+ }
4148
+ if (state.status === "waiting" && state.nextEligibleAt) {
4149
+ return `automatic cleanup scheduled after ${new Date(state.nextEligibleAt).toISOString()}`;
4150
+ }
4151
+ if (state.status === "failed" && state.nextEligibleAt) {
4152
+ return `will retry after ${new Date(state.nextEligibleAt).toISOString()}`;
4153
+ }
4154
+ return state.status;
4155
+ }
4156
+ function formatCompactPreview(result) {
4157
+ const { before, compression } = result;
4158
+ const compressionSavings = Math.max(
4159
+ 0,
4160
+ compression.legacyBytes + compression.compressedBytes - compression.projectedCompressedBytes
4161
+ );
4162
+ return `Compaction preview for ${before.root}:
4163
+ current size: ${formatBytes(before.totalBytes)}
4164
+ processed sync payloads: ${formatBytes(before.processed.bytes)} (${before.processed.files} files)
4165
+ stale indexes: ${formatBytes(before.staleIndexes.bytes)} (${before.staleIndexes.files} files)
4166
+ lossless compression: ${formatBytes(compressionSavings)} (${compression.candidates} legacy conversations)
4167
+ estimated reclaimable: ${formatBytes(result.reclaimedBytes)}
4168
+ `;
4169
+ }
4095
4170
  export {
4096
4171
  formatInstallSuccess,
4097
4172
  main,
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-6H2TI5IW.js";
6
+ } from "./chunk-BDR4JVMF.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-77JECNWA.js");
144
+ const { main } = await import("./cli-OXKBAUBE.js");
145
145
  return main();
146
146
  }
147
147
  bootstrap().then((code) => {
Binary file
package/dist/mcp.js CHANGED
@@ -2,20 +2,22 @@
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,
10
11
  resolveCorpusDir,
12
+ runAutomaticMaintenance,
11
13
  scheduleSelfUpdate
12
- } from "./chunk-G42RV536.js";
14
+ } from "./chunk-MTPXE734.js";
13
15
  import {
14
16
  VERSION,
15
17
  __commonJS,
16
18
  __export,
17
19
  __toESM
18
- } from "./chunk-6H2TI5IW.js";
20
+ } from "./chunk-BDR4JVMF.js";
19
21
 
20
22
  // ../node_modules/ajv/dist/compile/codegen/code.js
21
23
  var require_code = __commonJS({
@@ -21393,6 +21395,9 @@ function describeIssues(error2) {
21393
21395
 
21394
21396
  // src/service.ts
21395
21397
  var STALE_AFTER_HOURS = 48;
21398
+ var AUTO_MAINTENANCE_START_DELAY_MS = 6e4;
21399
+ var AUTO_MAINTENANCE_IDLE_MS = 3e4;
21400
+ var AUTO_MAINTENANCE_BUSY_RETRY_MS = 3e4;
21396
21401
  var CompanionService = class _CompanionService {
21397
21402
  constructor(corpus, index, onDrainError, memoryWriteCap) {
21398
21403
  this.corpus = corpus;
@@ -21409,6 +21414,10 @@ var CompanionService = class _CompanionService {
21409
21414
  refreshChain = Promise.resolve();
21410
21415
  /** Memories written since this process started; the per-session cap counts these. */
21411
21416
  memoriesWritten = 0;
21417
+ activeOperations = 0;
21418
+ lastOperationAt = Date.now();
21419
+ maintenanceTimer;
21420
+ closed = false;
21412
21421
  static async open(root, options = {}) {
21413
21422
  const corpus = new Corpus(root);
21414
21423
  const index = await openSearchIndex(corpus);
@@ -21419,8 +21428,71 @@ var CompanionService = class _CompanionService {
21419
21428
  options.memoryWriteCap ?? DEFAULT_MEMORY_WRITE_CAP
21420
21429
  );
21421
21430
  await service.refresh();
21431
+ if (options.automaticMaintenance) {
21432
+ service.scheduleAutomaticMaintenance(options.automaticMaintenance);
21433
+ }
21422
21434
  return service;
21423
21435
  }
21436
+ /** Let the MCP wrapper expose activity to the idle-only maintenance scheduler. */
21437
+ async runWithActivity(operation) {
21438
+ this.activeOperations += 1;
21439
+ this.lastOperationAt = Date.now();
21440
+ try {
21441
+ return await operation();
21442
+ } finally {
21443
+ this.activeOperations -= 1;
21444
+ this.lastOperationAt = Date.now();
21445
+ }
21446
+ }
21447
+ scheduleAutomaticMaintenance(options) {
21448
+ const {
21449
+ delayMs = AUTO_MAINTENANCE_START_DELAY_MS,
21450
+ idleMs = AUTO_MAINTENANCE_IDLE_MS,
21451
+ busyRetryMs = AUTO_MAINTENANCE_BUSY_RETRY_MS,
21452
+ onOutcome,
21453
+ ...runOptions
21454
+ } = options;
21455
+ const schedule = (delay) => {
21456
+ if (this.closed) return;
21457
+ this.maintenanceTimer = setTimeout(
21458
+ attempt,
21459
+ Math.max(0, Math.min(delay, 2147483647))
21460
+ );
21461
+ this.maintenanceTimer.unref?.();
21462
+ };
21463
+ const attempt = () => {
21464
+ this.maintenanceTimer = void 0;
21465
+ if (this.closed) return;
21466
+ const idleFor = Date.now() - this.lastOperationAt;
21467
+ if (this.activeOperations > 0 || idleFor < idleMs) {
21468
+ schedule(Math.max(busyRetryMs, idleMs - idleFor));
21469
+ return;
21470
+ }
21471
+ void runAutomaticMaintenance(this.corpus, this.index, runOptions).then(
21472
+ (outcome) => {
21473
+ onOutcome?.(outcome);
21474
+ const current = Date.now();
21475
+ if (outcome.action === "deferred") schedule(outcome.until - current);
21476
+ else if (outcome.action === "failed") schedule(outcome.retryAt - current);
21477
+ else if (outcome.action === "locked") schedule(busyRetryMs);
21478
+ else if (outcome.action === "recently-checked") {
21479
+ schedule(outcome.completedAt + (runOptions.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS) - current);
21480
+ } else if (outcome.action === "compacted" || outcome.action === "no-work") {
21481
+ schedule(runOptions.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS);
21482
+ }
21483
+ },
21484
+ (error2) => {
21485
+ onOutcome?.({
21486
+ action: "failed",
21487
+ error: error2 instanceof Error ? error2.message : String(error2),
21488
+ retryAt: Date.now() + busyRetryMs
21489
+ });
21490
+ schedule(busyRetryMs);
21491
+ }
21492
+ );
21493
+ };
21494
+ schedule(delayMs);
21495
+ }
21424
21496
  /**
21425
21497
  * Ingest whatever is waiting in `inbox/`. Never throws: a corrupt or
21426
21498
  * unreadable delta is reported and skipped rather than failing the read.
@@ -21547,6 +21619,9 @@ var CompanionService = class _CompanionService {
21547
21619
  };
21548
21620
  }
21549
21621
  close() {
21622
+ this.closed = true;
21623
+ if (this.maintenanceTimer) clearTimeout(this.maintenanceTimer);
21624
+ this.maintenanceTimer = void 0;
21550
21625
  this.index.close();
21551
21626
  }
21552
21627
  };
@@ -21636,12 +21711,28 @@ async function runServe() {
21636
21711
  onDrainError: (error2) => process.stderr.write(
21637
21712
  `llmnesia: inbox drain failed, serving the existing index: ${error2 instanceof Error ? error2.message : String(error2)}
21638
21713
  `
21639
- )
21714
+ ),
21715
+ automaticMaintenance: {
21716
+ onOutcome: (outcome) => {
21717
+ if (outcome.action === "failed") {
21718
+ process.stderr.write(
21719
+ `llmnesia: automatic storage maintenance will retry later: ${outcome.error}
21720
+ `
21721
+ );
21722
+ } else if (outcome.action === "compacted" && outcome.report.reclaimedBytes > 0) {
21723
+ process.stderr.write(
21724
+ `llmnesia: automatic storage maintenance reclaimed ${outcome.report.reclaimedBytes} bytes
21725
+ `
21726
+ );
21727
+ }
21728
+ }
21729
+ }
21640
21730
  })
21641
21731
  );
21642
21732
  const withService = async (operation) => {
21643
21733
  try {
21644
- return await operation(await service.get(5e3));
21734
+ const ready = await service.get(5e3);
21735
+ return await ready.runWithActivity(() => operation(ready));
21645
21736
  } catch (err) {
21646
21737
  if (err instanceof ResourcePreparingError) {
21647
21738
  return errorResult("LLMnesia is still preparing the local conversation index. Retry this tool shortly.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmnesia/mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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.3",
6
+ "version": "0.2.5",
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.3",
12
+ "version": "0.2.5",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }