@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.
@@ -38,7 +38,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
38
  ));
39
39
 
40
40
  // src/version.ts
41
- var VERSION = true ? "0.2.4" : "0.0.0-dev";
41
+ var VERSION = true ? "0.2.6" : "0.0.0-dev";
42
42
 
43
43
  export {
44
44
  __require,
@@ -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-NSVHWVEK.js";
20
+ } from "./chunk-HL5QE57W.js";
18
21
  import {
19
22
  VERSION,
20
23
  __commonJS,
21
24
  __require,
22
25
  __toESM
23
- } from "./chunk-JSNSTAFD.js";
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 fs2 = __require("fs");
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
- fs2.stat(path, function(er, stat6) {
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(fs2.statSync(path), path, options);
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 fs2 = __require("fs");
75
+ var fs = __require("fs");
73
76
  function isexe(path, options, cb) {
74
- fs2.stat(path, function(er, stat6) {
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(fs2.statSync(path), options);
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 fs2 = __require("fs");
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 fs2 = __require("fs");
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 = fs2.openSync(command, "r");
377
- fs2.readSync(fd, buffer, 0, size, 0);
378
- fs2.closeSync(fd);
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 join7 } from "path";
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(join7(corpus.paths.indexDir, name));
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-JSNSTAFD.js";
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-ZEA7WTVW.js");
144
+ const { main } = await import("./cli-DRB4DVH3.js");
145
145
  return main();
146
146
  }
147
147
  bootstrap().then((code) => {
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-NSVHWVEK.js";
15
+ } from "./chunk-HL5QE57W.js";
13
16
  import {
14
17
  VERSION,
15
18
  __commonJS,
16
19
  __export,
17
20
  __toESM
18
- } from "./chunk-JSNSTAFD.js";
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 original chat you relied on whose `url` is non-empty. Do this automatically; do not wait for the user to ask for links.";
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
- return await operation(await service.get(5e3));
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 original chats you used when their `url` fields are non-empty; never wait for the user to ask."
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 its original-chat URL when available; always link the chats used in the answer.",
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; always link it in an answer based on this transcript.",
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: record2.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 + original-chat URL), newest first. Always link the chats used in the answer.",
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-chat URLs when available; always link the chats used in the answer.",
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.4",
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.4",
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.4",
12
+ "version": "0.2.6",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }