@llmnesia/mcp 0.2.11 → 0.2.12

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 CHANGED
@@ -160,6 +160,15 @@ the canonical conversation and active index have accepted it. Malformed input
160
160
  is kept under `inbox/quarantine/` for inspection. Older releases archived every
161
161
  successful payload and may also have left versioned indexes behind.
162
162
 
163
+ Valid blank-text turns are retained with their message metadata. If an older
164
+ importer quarantined a backup containing those turns, close connected MCP
165
+ clients and run `llmnesia-mcp retry-quarantine`. The command validates each
166
+ entire file before retrying, updates the active index, and moves successfully
167
+ recovered originals into `inbox/recovered/` without deleting their contents.
168
+ Files with unresolved malformed records remain quarantined. The warning clears
169
+ on the next MCP read once no pending or quarantined files remain. This does not
170
+ require rebuilding local history.
171
+
163
172
  No maintenance command is required. After an upgrade, the MCP service waits
164
173
  through a three-day compatibility window, then reclaims the historical files
165
174
  automatically while the service is ready and idle. Work is single-process,
@@ -187,6 +196,7 @@ Once registered, your MCP client gains eight tools over the corpus:
187
196
  | Tool | Purpose |
188
197
  | --- | --- |
189
198
  | `search_conversations` | Full-text search across every past conversation. Returns ranked matches with highlighted snippets. Supports `platform`, `title`, `date_from`/`date_to` filters and a `match_mode` (`any` / `all` / `phrase`) for when a wrong hit matters. |
199
+ | `get_conversation_image` | Read a retained local raster image using the message and image index returned by `get_conversation`. No private provider URL fetching or OCR. |
190
200
  | `get_conversation` | Fetch the full transcript of one conversation by its id (the `id` field from search results). The response carries `contentFingerprint`, the current fingerprint of the returned transcript (see below). |
191
201
  | `list_conversations` | List conversations by chronology, including oldest-first for questions about the user's earliest chat. |
192
202
  | `list_recent` | List the most recently updated conversations with title, summary, and platform. |
@@ -214,7 +224,7 @@ items (`limit` can go lower):
214
224
  "items": [
215
225
  { "id": "claude:rust-borrow", "platform": "claude",
216
226
  "title": "Debugging a Rust borrow checker error",
217
- "fingerprint": "fp1:9af3c0d7…" }
227
+ "fingerprint": "fp2:9af3c0d7…" }
218
228
  ],
219
229
  "nextCursor": "inv1.eyJzIjoi…"
220
230
  }
@@ -229,12 +239,15 @@ Cursors are opaque, they do not survive an MCP server restart, and an expired
229
239
  or unknown cursor returns a clear error telling the caller to begin a new
230
240
  scan.
231
241
 
232
- The fingerprint is `fp1:` followed by a SHA-256 hex digest. It covers exactly:
242
+ Images are now included in the fingerprint. Clients comparing an older `fp1` inventory should refresh it once. Local raster copies are bounded to 2 MiB per image and 8 MiB per captured conversation. Vault sync retains URL metadata only. Protected or expired images without a retained copy need recapture or a supported bundled export.
243
+
244
+ The fingerprint is `fp2:` followed by a SHA-256 hex digest. It covers exactly:
233
245
 
234
246
  - the conversation title, and the user's local alias (`localTitle`) when set;
235
247
  - every message in `msgIndex` order: its index, role, stored `text`, and
236
- `formattedText` when present (because `get_conversation` prefers it, a
237
- backfill that adds it changes what a caller would receive).
248
+ `formattedText` when present;
249
+ - image references, metadata and retained local bytes. Image-only changes are
250
+ visible to an inventory client even when transcript text is unchanged.
238
251
 
239
252
  It deliberately excludes every timestamp, including the provider's
240
253
  `updatedAt`: historical imports can retain old source dates, and a genuinely
@@ -322,6 +335,7 @@ llmnesia-mcp uninstall # remove manifests, config, client entries
322
335
  llmnesia-mcp init # create an empty corpus
323
336
  llmnesia-mcp ingest <…> # ingest an NDJSON backup file
324
337
  llmnesia-mcp drain # ingest everything in <corpus>/inbox
338
+ llmnesia-mcp retry-quarantine # repair eligible files; retain original backups
325
339
  llmnesia-mcp watch # ingest new inbox deltas live (Ctrl+C to stop)
326
340
  llmnesia-mcp stats # corpus summary
327
341
  llmnesia-mcp compact --dry-run # exact read-only storage/savings report
@@ -3,7 +3,45 @@ import { createRequire as __llmnesiaCreateRequire } from "node:module";
3
3
  const require = __llmnesiaCreateRequire(import.meta.url);
4
4
  import {
5
5
  VERSION
6
- } from "./chunk-ZYE6VY6A.js";
6
+ } from "./chunk-SEBISBN5.js";
7
+
8
+ // src/images.ts
9
+ var MAX_BYTES = 2 * 1024 * 1024;
10
+ function decodeImage(value) {
11
+ if (typeof value !== "string" || value.length > Math.ceil(MAX_BYTES * 4 / 3) + 80) return null;
12
+ const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]+={0,2})$/.exec(value);
13
+ if (!match || match[2].length % 4 !== 0) return null;
14
+ const bytes = Buffer.from(match[2], "base64");
15
+ if (!bytes.length || bytes.length > MAX_BYTES || bytes.toString("base64") !== match[2]) return null;
16
+ const head = bytes.toString("latin1", 0, 12);
17
+ const valid = match[1] === "image/png" ? head.startsWith("\x89PNG\r\n\n") : match[1] === "image/jpeg" ? head.startsWith("\xFF\xD8\xFF") : match[1] === "image/gif" ? /^GIF8[79]a/.test(head) : head.startsWith("RIFF") && head.slice(8, 12) === "WEBP";
18
+ if (!valid) return null;
19
+ return { mimeType: match[1], data: match[2] };
20
+ }
21
+ function normalizeImages(value) {
22
+ if (!Array.isArray(value)) return [];
23
+ return value.slice(0, 12).flatMap((raw) => {
24
+ if (!raw || typeof raw !== "object") return [];
25
+ const row = raw;
26
+ const dataUrl = decodeImage(row.dataUrl) ? row.dataUrl : void 0;
27
+ let url = "";
28
+ try {
29
+ if (typeof row.url === "string" && row.url.length <= 8e3) {
30
+ const parsed = new URL(row.url);
31
+ if (["http:", "https:"].includes(parsed.protocol)) url = parsed.href;
32
+ }
33
+ } catch {
34
+ }
35
+ if (!url && !dataUrl) return [];
36
+ return [{
37
+ url,
38
+ ...dataUrl ? { dataUrl } : {},
39
+ ...typeof row.alt === "string" ? { alt: row.alt.slice(0, 500) } : {},
40
+ ...typeof row.width === "number" && row.width > 0 && row.width <= 2e4 ? { width: row.width } : {},
41
+ ...typeof row.height === "number" && row.height > 0 && row.height <= 2e4 ? { height: row.height } : {}
42
+ }];
43
+ });
44
+ }
7
45
 
8
46
  // src/config.ts
9
47
  import { readFileSync } from "fs";
@@ -147,6 +185,31 @@ import { dirname, join as join3 } from "path";
147
185
  import { promisify } from "util";
148
186
  import { gzip as gzipCallback, gunzip as gunzipCallback, gunzipSync } from "zlib";
149
187
 
188
+ // src/sourceHealth.ts
189
+ function parseSourceHealth(value) {
190
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
191
+ const row = value;
192
+ if (!["claude_code", "codex", "zcode"].includes(String(row.source))) return null;
193
+ if (row.connection !== "connected" && row.connection !== "disconnected") return null;
194
+ if (!["ok", "partial", "missing", "error", "unknown"].includes(String(row.state))) return null;
195
+ const time = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
196
+ if (!time(row.observedAt) || !time(row.lastAttemptAt)) return null;
197
+ if (row.observedAt > Date.now() + 3e5 || row.lastAttemptAt > row.observedAt) return null;
198
+ if (row.lastSuccessfulSyncAt !== null && (!time(row.lastSuccessfulSyncAt) || row.lastSuccessfulSyncAt > row.observedAt)) return null;
199
+ if (row.lastCaptureAt !== null && (!time(row.lastCaptureAt) || row.lastCaptureAt > row.observedAt)) return null;
200
+ if (!time(row.failedSessions)) return null;
201
+ return {
202
+ source: row.source,
203
+ observedAt: row.observedAt,
204
+ connection: row.connection,
205
+ state: row.state,
206
+ lastAttemptAt: row.lastAttemptAt,
207
+ lastSuccessfulSyncAt: row.lastSuccessfulSyncAt,
208
+ lastCaptureAt: row.lastCaptureAt,
209
+ failedSessions: row.failedSessions
210
+ };
211
+ }
212
+
150
213
  // src/serialize.ts
151
214
  function canonicalizeConversation(record) {
152
215
  return {
@@ -244,6 +307,8 @@ function canonicalMessageDoc(m) {
244
307
  role: m.role,
245
308
  text: m.text
246
309
  };
310
+ const images = normalizeImages(m.images);
311
+ if (images.length) out.images = images;
247
312
  assignIfDefined(out, "formattedText", m.formattedText);
248
313
  assignIfDefined(out, "model", m.model);
249
314
  out.createdAt = m.createdAt;
@@ -586,14 +651,46 @@ var Corpus = class {
586
651
  return { conversationCount, messageCount, byPlatform };
587
652
  }
588
653
  /** Refresh meta.json's conversationCount / lastIngestAt after an ingest run. */
589
- async touchIngestMeta(receipt) {
654
+ async touchIngestMeta(receipt, sources = {}) {
590
655
  const { conversationCount } = await this.stats();
656
+ const previous = (await this.readMeta())?.sourceLastIngestAt ?? {};
657
+ const sourceLastIngestAt = { ...previous };
658
+ for (const [source, at] of Object.entries(sources)) {
659
+ sourceLastIngestAt[source] = Math.max(previous[source] ?? 0, at);
660
+ }
591
661
  await this.writeMeta({
592
662
  lastIngestAt: Date.now(),
593
663
  conversationCount,
664
+ sourceLastIngestAt,
594
665
  ...receipt ? { lastIngestReceipt: receipt } : {}
595
666
  });
596
667
  }
668
+ async readSourceHealth() {
669
+ try {
670
+ const rows = JSON.parse(await fs.readFile(join3(this.paths.root, "source-health-v1.json"), "utf8"));
671
+ return Array.isArray(rows) ? rows.flatMap((value) => {
672
+ const parsed = parseSourceHealth(value);
673
+ return parsed ? [parsed] : [];
674
+ }) : [];
675
+ } catch {
676
+ return [];
677
+ }
678
+ }
679
+ async recordSourceHealth(value) {
680
+ const row = parseSourceHealth(value);
681
+ if (!row) return false;
682
+ await this.enqueueWrite(async () => {
683
+ const current = new Map((await this.readSourceHealth()).map((entry) => [entry.source, entry]));
684
+ if ((current.get(row.source)?.observedAt ?? -1) >= row.observedAt) return;
685
+ current.set(row.source, row);
686
+ await writeFileAtomic(
687
+ join3(this.paths.root, "source-health-v1.json"),
688
+ `${JSON.stringify([...current.values()])}
689
+ `
690
+ );
691
+ });
692
+ return true;
693
+ }
597
694
  enqueueWrite(fn) {
598
695
  const run = this.writeChain.then(fn, fn);
599
696
  this.writeChain = run.then(
@@ -787,7 +884,7 @@ async function upsertIndexedConversation(corpus, record, index) {
787
884
  }
788
885
  return result;
789
886
  }
790
- async function ingestFile(corpus, file, index) {
887
+ async function ingestFile(corpus, file, index, options = {}) {
791
888
  const report = { ...EMPTY_REPORT };
792
889
  let current = null;
793
890
  let buffered = [];
@@ -796,7 +893,11 @@ async function ingestFile(corpus, file, index) {
796
893
  return;
797
894
  }
798
895
  const record = { conversation: current, messages: buffered };
799
- const result = await upsertIndexedConversation(corpus, record, index);
896
+ const result = options.validateOnly ? { docId: current.docId, outcome: "skipped" } : await upsertIndexedConversation(corpus, record, index);
897
+ if (result.outcome !== "skipped") {
898
+ report.sourceLastIngestAt ??= {};
899
+ report.sourceLastIngestAt[record.conversation.platform] = Date.now();
900
+ }
800
901
  tally(report, result);
801
902
  report.conversations += 1;
802
903
  report.messages += buffered.length;
@@ -825,6 +926,12 @@ async function ingestFile(corpus, file, index) {
825
926
  if (type === "meta") {
826
927
  continue;
827
928
  }
929
+ if (type === "source_health") {
930
+ await flush();
931
+ const valid = options.validateOnly ? Boolean(parseSourceHealth(record.health)) : await corpus.recordSourceHealth(record.health);
932
+ if (!valid) report.malformedLines += 1;
933
+ continue;
934
+ }
828
935
  if (type === "conversation") {
829
936
  await flush();
830
937
  current = coerceConversation(record.conversation);
@@ -846,7 +953,9 @@ async function ingestFile(corpus, file, index) {
846
953
  } else {
847
954
  report.malformedLines += 1;
848
955
  }
956
+ continue;
849
957
  }
958
+ report.malformedLines += 1;
850
959
  }
851
960
  await flush();
852
961
  return report;
@@ -859,6 +968,30 @@ async function pendingInboxFiles(corpus) {
859
968
  throw error;
860
969
  }
861
970
  }
971
+ async function retryQuarantine(corpus, index) {
972
+ await corpus.init();
973
+ const entries = (await fs2.readdir(corpus.paths.quarantineDir, { withFileTypes: true })).filter((entry) => entry.isFile() && isNdjson(entry.name)).map((entry) => entry.name).sort();
974
+ const result = { files: entries.length, recovered: 0, remaining: 0, failed: 0, archiveDir: null };
975
+ for (const name of entries) {
976
+ const src = join4(corpus.paths.quarantineDir, name);
977
+ try {
978
+ if ((await ingestFile(corpus, src, void 0, { validateOnly: true })).malformedLines > 0) continue;
979
+ const report = await ingestFile(corpus, src, index);
980
+ if (report.malformedLines > 0) continue;
981
+ if (!result.archiveDir) {
982
+ const recoveredDir = join4(corpus.paths.inboxDir, "recovered");
983
+ await fs2.mkdir(recoveredDir, { recursive: true, mode: 448 });
984
+ result.archiveDir = await fs2.mkdtemp(join4(recoveredDir, "quarantine-"));
985
+ }
986
+ await fs2.rename(src, join4(result.archiveDir, name));
987
+ result.recovered += 1;
988
+ } catch {
989
+ result.failed += 1;
990
+ }
991
+ }
992
+ result.remaining = (await fs2.readdir(corpus.paths.quarantineDir, { withFileTypes: true })).filter((entry) => entry.isFile()).length;
993
+ return result;
994
+ }
862
995
  async function drainInbox(corpus, index, options = {}) {
863
996
  await corpus.init();
864
997
  const inbox = corpus.paths.inboxDir;
@@ -894,7 +1027,7 @@ async function drainInbox(corpus, index, options = {}) {
894
1027
  }
895
1028
  }
896
1029
  }
897
- if (total.files > 0) {
1030
+ if (total.conversations > 0) {
898
1031
  await corpus.touchIngestMeta({
899
1032
  files: total.files,
900
1033
  conversations: total.conversations,
@@ -902,7 +1035,7 @@ async function drainInbox(corpus, index, options = {}) {
902
1035
  malformedLines: total.malformedLines,
903
1036
  discarded: total.discarded,
904
1037
  quarantined: total.quarantined
905
- });
1038
+ }, total.sourceLastIngestAt);
906
1039
  }
907
1040
  return total;
908
1041
  }
@@ -947,12 +1080,12 @@ function coerceConversation(value) {
947
1080
  return null;
948
1081
  }
949
1082
  const c = value;
950
- if (typeof c.docId !== "string" || !c.docId) {
1083
+ if (typeof c.docId !== "string" || !c.docId || typeof c.platform !== "string" || !c.platform || typeof c.title !== "string" || typeof c.url !== "string" || typeof c.createdAt !== "number" || !Number.isFinite(c.createdAt)) {
951
1084
  return null;
952
1085
  }
953
1086
  const conversation = { ...c };
954
- if (typeof conversation.updatedAt !== "number") {
955
- conversation.updatedAt = typeof conversation.contentUpdatedAt === "number" ? conversation.contentUpdatedAt : 0;
1087
+ if (typeof conversation.updatedAt !== "number" || !Number.isFinite(conversation.updatedAt)) {
1088
+ conversation.updatedAt = typeof conversation.contentUpdatedAt === "number" && Number.isFinite(conversation.contentUpdatedAt) ? conversation.contentUpdatedAt : 0;
956
1089
  }
957
1090
  return conversation;
958
1091
  }
@@ -961,11 +1094,12 @@ function coerceMessage(docId, value) {
961
1094
  return null;
962
1095
  }
963
1096
  const m = value;
964
- const text = typeof m.text === "string" ? m.text : "";
965
- if (!text.trim()) {
966
- return null;
967
- }
968
- const msgIndex = typeof m.msgIndex === "number" ? m.msgIndex : 0;
1097
+ if (!Number.isSafeInteger(m.msgIndex) || m.msgIndex < 0) return null;
1098
+ const formattedText = typeof m.formattedText === "string" ? m.formattedText : "";
1099
+ const text = typeof m.text === "string" && m.text.trim() ? m.text : formattedText.trim() ? formattedText : typeof m.text === "string" ? m.text : "";
1100
+ const images = normalizeImages(m.images);
1101
+ if (typeof m.text !== "string" && !formattedText.trim() && !images.length) return null;
1102
+ const msgIndex = m.msgIndex;
969
1103
  const message = {
970
1104
  // The backup line omits msgKey; reconstruct the extension's convention.
971
1105
  msgKey: `${docId}:${msgIndex}`,
@@ -973,6 +1107,7 @@ function coerceMessage(docId, value) {
973
1107
  msgIndex,
974
1108
  role: coerceRole(m.role),
975
1109
  text,
1110
+ ...images.length ? { images } : {},
976
1111
  createdAt: typeof m.createdAt === "number" ? m.createdAt : 0,
977
1112
  updatedAt: typeof m.updatedAt === "number" ? m.updatedAt : 0,
978
1113
  contentHash: typeof m.contentHash === "string" ? m.contentHash : ""
@@ -1003,6 +1138,10 @@ function tally(report, result) {
1003
1138
  }
1004
1139
  }
1005
1140
  function accumulate(total, report) {
1141
+ for (const [source, at] of Object.entries(report.sourceLastIngestAt ?? {})) {
1142
+ total.sourceLastIngestAt ??= {};
1143
+ total.sourceLastIngestAt[source] = Math.max(total.sourceLastIngestAt[source] ?? 0, at);
1144
+ }
1006
1145
  total.conversations += report.conversations;
1007
1146
  total.created += report.created;
1008
1147
  total.updated += report.updated;
@@ -1013,18 +1152,19 @@ function accumulate(total, report) {
1013
1152
 
1014
1153
  // src/fingerprint.ts
1015
1154
  import { createHash as createHash2 } from "crypto";
1016
- var FINGERPRINT_SCHEME = "fp1";
1155
+ var FINGERPRINT_SCHEME = "fp2";
1017
1156
  function fingerprintConversation(record) {
1018
1157
  const conversation = record.conversation;
1019
1158
  const payload = {
1020
- v: 1,
1159
+ v: 2,
1021
1160
  title: conversation.title,
1022
1161
  localTitle: conversation.localTitle ?? null,
1023
1162
  messages: [...record.messages].sort((a, b) => a.msgIndex - b.msgIndex).map((message) => ({
1024
1163
  i: message.msgIndex,
1025
1164
  r: message.role,
1026
1165
  t: message.text,
1027
- f: message.formattedText ?? null
1166
+ f: message.formattedText ?? null,
1167
+ images: message.images ?? []
1028
1168
  }))
1029
1169
  };
1030
1170
  const digest = createHash2("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
@@ -1218,6 +1358,10 @@ var SearchIndex = class {
1218
1358
  const row = this.db.prepare("SELECT COUNT(*) AS n FROM conversations").get();
1219
1359
  return row.n;
1220
1360
  }
1361
+ sourceSummary() {
1362
+ return this.db.prepare(`SELECT platform AS source, COUNT(*) AS conversationCount,
1363
+ MAX(updatedAt) AS newestConversationAt FROM conversations GROUP BY platform`).all();
1364
+ }
1221
1365
  /** Number of indexed memories. */
1222
1366
  countMemories() {
1223
1367
  const row = this.db.prepare("SELECT COUNT(*) AS n FROM memories").get();
@@ -2254,6 +2398,7 @@ function describeOutcome(outcome) {
2254
2398
  }
2255
2399
 
2256
2400
  export {
2401
+ decodeImage,
2257
2402
  readNativeHostViewerExtensionId,
2258
2403
  resolveCorpusDir,
2259
2404
  staleIndexFiles,
@@ -2261,6 +2406,7 @@ export {
2261
2406
  upsertIndexedConversation,
2262
2407
  ingestFile,
2263
2408
  pendingInboxFiles,
2409
+ retryQuarantine,
2264
2410
  drainInbox,
2265
2411
  fingerprintConversation,
2266
2412
  SearchIndex,
@@ -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.11" : "0.0.0-dev";
41
+ var VERSION = true ? "0.2.12" : "0.0.0-dev";
42
42
 
43
43
  export {
44
44
  __require,
@@ -14,20 +14,21 @@ import {
14
14
  openSearchIndex,
15
15
  readAutomaticMaintenanceState,
16
16
  resolveCorpusDir,
17
+ retryQuarantine,
17
18
  runtimeBinDir,
18
19
  staleIndexFiles,
19
20
  uninstallRuntime
20
- } from "./chunk-7ISC4REX.js";
21
+ } from "./chunk-3QDA3XWA.js";
21
22
  import {
22
23
  VERSION,
23
24
  __commonJS,
24
25
  __require,
25
26
  __toESM
26
- } from "./chunk-ZYE6VY6A.js";
27
+ } from "./chunk-SEBISBN5.js";
27
28
 
28
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/isexe/windows.js
29
+ // ../node_modules/isexe/windows.js
29
30
  var require_windows = __commonJS({
30
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/isexe/windows.js"(exports, module) {
31
+ "../node_modules/isexe/windows.js"(exports, module) {
31
32
  "use strict";
32
33
  module.exports = isexe;
33
34
  isexe.sync = sync;
@@ -66,9 +67,9 @@ var require_windows = __commonJS({
66
67
  }
67
68
  });
68
69
 
69
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/isexe/mode.js
70
+ // ../node_modules/isexe/mode.js
70
71
  var require_mode = __commonJS({
71
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/isexe/mode.js"(exports, module) {
72
+ "../node_modules/isexe/mode.js"(exports, module) {
72
73
  "use strict";
73
74
  module.exports = isexe;
74
75
  isexe.sync = sync;
@@ -100,9 +101,9 @@ var require_mode = __commonJS({
100
101
  }
101
102
  });
102
103
 
103
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/isexe/index.js
104
+ // ../node_modules/isexe/index.js
104
105
  var require_isexe = __commonJS({
105
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/isexe/index.js"(exports, module) {
106
+ "../node_modules/isexe/index.js"(exports, module) {
106
107
  "use strict";
107
108
  var fs = __require("fs");
108
109
  var core;
@@ -156,9 +157,9 @@ var require_isexe = __commonJS({
156
157
  }
157
158
  });
158
159
 
159
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/which/which.js
160
+ // ../node_modules/which/which.js
160
161
  var require_which = __commonJS({
161
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/which/which.js"(exports, module) {
162
+ "../node_modules/which/which.js"(exports, module) {
162
163
  "use strict";
163
164
  var isWindows2 = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
164
165
  var path = __require("path");
@@ -253,9 +254,9 @@ var require_which = __commonJS({
253
254
  }
254
255
  });
255
256
 
256
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/path-key/index.js
257
+ // ../node_modules/path-key/index.js
257
258
  var require_path_key = __commonJS({
258
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/path-key/index.js"(exports, module) {
259
+ "../node_modules/path-key/index.js"(exports, module) {
259
260
  "use strict";
260
261
  var pathKey = (options = {}) => {
261
262
  const environment = options.env || process.env;
@@ -270,9 +271,9 @@ var require_path_key = __commonJS({
270
271
  }
271
272
  });
272
273
 
273
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/util/resolveCommand.js
274
+ // ../node_modules/cross-spawn/lib/util/resolveCommand.js
274
275
  var require_resolveCommand = __commonJS({
275
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) {
276
+ "../node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) {
276
277
  "use strict";
277
278
  var path = __require("path");
278
279
  var which = require_which();
@@ -312,9 +313,9 @@ var require_resolveCommand = __commonJS({
312
313
  }
313
314
  });
314
315
 
315
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/util/escape.js
316
+ // ../node_modules/cross-spawn/lib/util/escape.js
316
317
  var require_escape = __commonJS({
317
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/util/escape.js"(exports, module) {
318
+ "../node_modules/cross-spawn/lib/util/escape.js"(exports, module) {
318
319
  "use strict";
319
320
  var metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g;
320
321
  function escapeCommand(arg) {
@@ -337,17 +338,17 @@ var require_escape = __commonJS({
337
338
  }
338
339
  });
339
340
 
340
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/shebang-regex/index.js
341
+ // ../node_modules/shebang-regex/index.js
341
342
  var require_shebang_regex = __commonJS({
342
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/shebang-regex/index.js"(exports, module) {
343
+ "../node_modules/shebang-regex/index.js"(exports, module) {
343
344
  "use strict";
344
345
  module.exports = /^#!(.*)/;
345
346
  }
346
347
  });
347
348
 
348
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/shebang-command/index.js
349
+ // ../node_modules/shebang-command/index.js
349
350
  var require_shebang_command = __commonJS({
350
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/shebang-command/index.js"(exports, module) {
351
+ "../node_modules/shebang-command/index.js"(exports, module) {
351
352
  "use strict";
352
353
  var shebangRegex = require_shebang_regex();
353
354
  module.exports = (string = "") => {
@@ -365,9 +366,9 @@ var require_shebang_command = __commonJS({
365
366
  }
366
367
  });
367
368
 
368
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/util/readShebang.js
369
+ // ../node_modules/cross-spawn/lib/util/readShebang.js
369
370
  var require_readShebang = __commonJS({
370
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
371
+ "../node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
371
372
  "use strict";
372
373
  var fs = __require("fs");
373
374
  var shebangCommand = require_shebang_command();
@@ -387,9 +388,9 @@ var require_readShebang = __commonJS({
387
388
  }
388
389
  });
389
390
 
390
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/parse.js
391
+ // ../node_modules/cross-spawn/lib/parse.js
391
392
  var require_parse = __commonJS({
392
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/parse.js"(exports, module) {
393
+ "../node_modules/cross-spawn/lib/parse.js"(exports, module) {
393
394
  "use strict";
394
395
  var path = __require("path");
395
396
  var resolveCommand = require_resolveCommand();
@@ -449,9 +450,9 @@ var require_parse = __commonJS({
449
450
  }
450
451
  });
451
452
 
452
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/enoent.js
453
+ // ../node_modules/cross-spawn/lib/enoent.js
453
454
  var require_enoent = __commonJS({
454
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/lib/enoent.js"(exports, module) {
455
+ "../node_modules/cross-spawn/lib/enoent.js"(exports, module) {
455
456
  "use strict";
456
457
  var isWin = process.platform === "win32";
457
458
  function notFoundError(original, syscall) {
@@ -499,9 +500,9 @@ var require_enoent = __commonJS({
499
500
  }
500
501
  });
501
502
 
502
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/index.js
503
+ // ../node_modules/cross-spawn/index.js
503
504
  var require_cross_spawn = __commonJS({
504
- "../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/cross-spawn/index.js"(exports, module) {
505
+ "../node_modules/cross-spawn/index.js"(exports, module) {
505
506
  "use strict";
506
507
  var cp = __require("child_process");
507
508
  var parse = require_parse();
@@ -888,13 +889,13 @@ function parseArgs(argv) {
888
889
  return { command: positionals[0], positionals: positionals.slice(1), options, flags };
889
890
  }
890
891
 
891
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/chokidar/esm/index.js
892
+ // ../node_modules/chokidar/esm/index.js
892
893
  import { stat as statcb } from "fs";
893
894
  import { stat as stat4, readdir as readdir2 } from "fs/promises";
894
895
  import { EventEmitter } from "events";
895
896
  import * as sysPath2 from "path";
896
897
 
897
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/readdirp/esm/index.js
898
+ // ../node_modules/readdirp/esm/index.js
898
899
  import { stat as stat2, lstat, readdir, realpath } from "fs/promises";
899
900
  import { Readable } from "stream";
900
901
  import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "path";
@@ -1113,7 +1114,7 @@ function readdirp(root, options = {}) {
1113
1114
  return new ReaddirpStream(options);
1114
1115
  }
1115
1116
 
1116
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/chokidar/esm/handler.js
1117
+ // ../node_modules/chokidar/esm/handler.js
1117
1118
  import { watchFile, unwatchFile, watch as fs_watch } from "fs";
1118
1119
  import { open, stat as stat3, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
1119
1120
  import * as sysPath from "path";
@@ -1869,7 +1870,7 @@ var NodeFsHandler = class {
1869
1870
  }
1870
1871
  };
1871
1872
 
1872
- // ../../../../Users/Kdog/Code-Projects/LLMnesia/node_modules/chokidar/esm/index.js
1873
+ // ../node_modules/chokidar/esm/index.js
1873
1874
  var SLASH = "/";
1874
1875
  var SLASH_SLASH = "//";
1875
1876
  var ONE_DOT = ".";
@@ -3829,6 +3830,8 @@ Power-user / debugging:
3829
3830
  ingest <file...> Ingest one or more NDJSON backup files into the corpus
3830
3831
  drain Ingest every file in <corpus>/inbox; discard clean
3831
3832
  payloads and quarantine malformed ones
3833
+ retry-quarantine Retry quarantined files; retain repaired originals in
3834
+ <corpus>/inbox/recovered (close MCP clients first)
3832
3835
  watch Watch <corpus>/inbox and ingest new deltas live
3833
3836
  stats Print corpus counts (conversations, messages, platforms)
3834
3837
  compact Losslessly compress conversations and remove redundant
@@ -4058,6 +4061,20 @@ async function main() {
4058
4061
  }
4059
4062
  return 0;
4060
4063
  }
4064
+ case "retry-quarantine": {
4065
+ const index = await openSearchIndex(corpus);
4066
+ try {
4067
+ const report = await retryQuarantine(corpus, index);
4068
+ process.stdout.write(
4069
+ `Quarantine recovery: ${report.recovered} recovered, ${report.remaining} remaining, ${report.failed} failed.
4070
+ ` + (report.archiveDir ? `Original files retained at ${report.archiveDir}
4071
+ ` : "")
4072
+ );
4073
+ return report.remaining > 0 || report.failed > 0 ? 1 : 0;
4074
+ } finally {
4075
+ index.close();
4076
+ }
4077
+ }
4061
4078
  case "watch": {
4062
4079
  process.stdout.write(`Watching ${corpus.paths.inboxDir} for new deltas (Ctrl+C to stop)\u2026
4063
4080
  `);
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-ZYE6VY6A.js";
6
+ } from "./chunk-SEBISBN5.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-RQMFEW5J.js");
144
+ const { main } = await import("./cli-IF63HSCG.js");
145
145
  return main();
146
146
  }
147
147
  bootstrap().then((code) => {