@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 +18 -4
- package/dist/{chunk-7ISC4REX.js → chunk-3QDA3XWA.js} +163 -17
- package/dist/{chunk-ZYE6VY6A.js → chunk-SEBISBN5.js} +1 -1
- package/dist/{cli-RQMFEW5J.js → cli-IF63HSCG.js} +49 -32
- package/dist/cli.js +2 -2
- package/dist/llmnesia-claude-host.mjs +93 -13
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +300 -229
- package/package.json +1 -1
- package/server.json +2 -2
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": "
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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 = "
|
|
1155
|
+
var FINGERPRINT_SCHEME = "fp2";
|
|
1017
1156
|
function fingerprintConversation(record) {
|
|
1018
1157
|
const conversation = record.conversation;
|
|
1019
1158
|
const payload = {
|
|
1020
|
-
v:
|
|
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,
|
|
@@ -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-
|
|
21
|
+
} from "./chunk-3QDA3XWA.js";
|
|
21
22
|
import {
|
|
22
23
|
VERSION,
|
|
23
24
|
__commonJS,
|
|
24
25
|
__require,
|
|
25
26
|
__toESM
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-SEBISBN5.js";
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
+
// ../node_modules/isexe/windows.js
|
|
29
30
|
var require_windows = __commonJS({
|
|
30
|
-
"
|
|
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
|
-
//
|
|
70
|
+
// ../node_modules/isexe/mode.js
|
|
70
71
|
var require_mode = __commonJS({
|
|
71
|
-
"
|
|
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
|
-
//
|
|
104
|
+
// ../node_modules/isexe/index.js
|
|
104
105
|
var require_isexe = __commonJS({
|
|
105
|
-
"
|
|
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
|
-
//
|
|
160
|
+
// ../node_modules/which/which.js
|
|
160
161
|
var require_which = __commonJS({
|
|
161
|
-
"
|
|
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
|
-
//
|
|
257
|
+
// ../node_modules/path-key/index.js
|
|
257
258
|
var require_path_key = __commonJS({
|
|
258
|
-
"
|
|
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
|
-
//
|
|
274
|
+
// ../node_modules/cross-spawn/lib/util/resolveCommand.js
|
|
274
275
|
var require_resolveCommand = __commonJS({
|
|
275
|
-
"
|
|
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
|
-
//
|
|
316
|
+
// ../node_modules/cross-spawn/lib/util/escape.js
|
|
316
317
|
var require_escape = __commonJS({
|
|
317
|
-
"
|
|
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
|
-
//
|
|
341
|
+
// ../node_modules/shebang-regex/index.js
|
|
341
342
|
var require_shebang_regex = __commonJS({
|
|
342
|
-
"
|
|
343
|
+
"../node_modules/shebang-regex/index.js"(exports, module) {
|
|
343
344
|
"use strict";
|
|
344
345
|
module.exports = /^#!(.*)/;
|
|
345
346
|
}
|
|
346
347
|
});
|
|
347
348
|
|
|
348
|
-
//
|
|
349
|
+
// ../node_modules/shebang-command/index.js
|
|
349
350
|
var require_shebang_command = __commonJS({
|
|
350
|
-
"
|
|
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
|
-
//
|
|
369
|
+
// ../node_modules/cross-spawn/lib/util/readShebang.js
|
|
369
370
|
var require_readShebang = __commonJS({
|
|
370
|
-
"
|
|
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
|
-
//
|
|
391
|
+
// ../node_modules/cross-spawn/lib/parse.js
|
|
391
392
|
var require_parse = __commonJS({
|
|
392
|
-
"
|
|
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
|
-
//
|
|
453
|
+
// ../node_modules/cross-spawn/lib/enoent.js
|
|
453
454
|
var require_enoent = __commonJS({
|
|
454
|
-
"
|
|
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
|
-
//
|
|
503
|
+
// ../node_modules/cross-spawn/index.js
|
|
503
504
|
var require_cross_spawn = __commonJS({
|
|
504
|
-
"
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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-
|
|
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-
|
|
144
|
+
const { main } = await import("./cli-IF63HSCG.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|