@llmnesia/mcp 0.2.12 → 0.2.13
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 +23 -6
- package/dist/{chunk-SEBISBN5.js → chunk-GHKXNM7R.js} +1 -1
- package/dist/{chunk-3QDA3XWA.js → chunk-QIRYUQIU.js} +79 -56
- package/dist/{cli-IF63HSCG.js → cli-CMCIFSWH.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +3 -3
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -239,7 +239,24 @@ Cursors are opaque, they do not survive an MCP server restart, and an expired
|
|
|
239
239
|
or unknown cursor returns a clear error telling the caller to begin a new
|
|
240
240
|
scan.
|
|
241
241
|
|
|
242
|
-
Images are
|
|
242
|
+
Images are included in the `fp2` fingerprint. MCP 0.2.13 automatically rebuilds
|
|
243
|
+
the derived index as format v8 on first use, recomputing every inventory
|
|
244
|
+
fingerprint from the same canonical records used by retrieval. This repairs
|
|
245
|
+
mixed `fp1`/`fp2` inventories left by earlier versions. The canonical transcripts
|
|
246
|
+
are unchanged, and later opens reuse the rebuilt index. Stop all MCP clients
|
|
247
|
+
before updating and restart them afterward; the first read can take longer
|
|
248
|
+
while rebuilding. Clients starting together wait for the index to become ready.
|
|
249
|
+
An interrupted initial rebuild is retried on the next open. No extension
|
|
250
|
+
history rebuild or backup reimport is needed.
|
|
251
|
+
|
|
252
|
+
Consumer migration: begin a new complete inventory scan without an old cursor.
|
|
253
|
+
Replace the saved `fp1` baseline with the new `fp2` values and reverify those
|
|
254
|
+
records. Different scheme prefixes are not comparable content revisions.
|
|
255
|
+
Existing `fp2` digests retain their definition and values for unchanged content.
|
|
256
|
+
Future incompatible fingerprint changes require a new scheme prefix, an index
|
|
257
|
+
migration and a documented consumer migration before release.
|
|
258
|
+
|
|
259
|
+
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
260
|
|
|
244
261
|
The fingerprint is `fp2:` followed by a SHA-256 hex digest. It covers exactly:
|
|
245
262
|
|
|
@@ -256,8 +273,8 @@ state, account and model identifiers, summaries, and other derived fields are
|
|
|
256
273
|
excluded too. An identical re-ingest therefore never changes the fingerprint,
|
|
257
274
|
while an edited message or a rename always does, even when nothing else about
|
|
258
275
|
the record moved. The fingerprint is stored in the derived search index
|
|
259
|
-
(index format
|
|
260
|
-
fingerprints for the existing corpus), so a scan never decompresses a single
|
|
276
|
+
(index format v8; opening an older index triggers a rebuild that computes
|
|
277
|
+
`fp2` fingerprints for the existing corpus), so a scan never decompresses a single
|
|
261
278
|
transcript.
|
|
262
279
|
|
|
263
280
|
A periodic-processing loop:
|
|
@@ -269,9 +286,9 @@ A periodic-processing loop:
|
|
|
269
286
|
be skipped.
|
|
270
287
|
3. Call `get_conversation` only for the new or changed ids. Its response
|
|
271
288
|
carries `contentFingerprint`, the fingerprint of the transcript it returns.
|
|
272
|
-
|
|
273
|
-
your snapshot was taken and the transcript
|
|
274
|
-
|
|
289
|
+
Within the same scheme, a different digest means the conversation changed
|
|
290
|
+
after your snapshot was taken and the fetched transcript is current. A
|
|
291
|
+
different scheme prefix requires the baseline migration described above.
|
|
275
292
|
|
|
276
293
|
### Read-only mode (`LLMNESIA_READ_ONLY`)
|
|
277
294
|
|
|
@@ -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-GHKXNM7R.js";
|
|
7
7
|
|
|
8
8
|
// src/images.ts
|
|
9
9
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -85,7 +85,7 @@ function expandHome(p) {
|
|
|
85
85
|
import { join as join2 } from "path";
|
|
86
86
|
import { readdir } from "fs/promises";
|
|
87
87
|
import { createHash } from "crypto";
|
|
88
|
-
var INDEX_FORMAT_VERSION =
|
|
88
|
+
var INDEX_FORMAT_VERSION = 8;
|
|
89
89
|
function indexDbFilename(version) {
|
|
90
90
|
return version === 1 ? "llmnesia.db" : `llmnesia-v${version}.db`;
|
|
91
91
|
}
|
|
@@ -1217,6 +1217,8 @@ var TITLE_ROW_INDEX = -1;
|
|
|
1217
1217
|
var TITLE_ROLE = "title";
|
|
1218
1218
|
var DEFAULT_LIMIT = 10;
|
|
1219
1219
|
var SNIPPET_TOKENS = 12;
|
|
1220
|
+
var INDEX_OPEN_BUSY_TIMEOUT_MS = 5 * 6e4;
|
|
1221
|
+
var INDEX_OPEN_BUSY_RETRY_MS = 50;
|
|
1220
1222
|
var META_SQL = `
|
|
1221
1223
|
CREATE TABLE IF NOT EXISTS index_meta (
|
|
1222
1224
|
key TEXT PRIMARY KEY,
|
|
@@ -1301,52 +1303,64 @@ var SearchIndex = class {
|
|
|
1301
1303
|
this.corpus = corpus;
|
|
1302
1304
|
hardenDbFile(dbPath);
|
|
1303
1305
|
this.db = new (loadSqlite()).DatabaseSync(dbPath);
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1306
|
+
try {
|
|
1307
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1308
|
+
this.db.exec("PRAGMA foreign_keys = OFF");
|
|
1309
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1310
|
+
try {
|
|
1311
|
+
this.db.exec(META_SQL);
|
|
1312
|
+
this.staleOnOpen = this.getMeta("format_version") !== String(INDEX_FORMAT_VERSION);
|
|
1313
|
+
if (this.staleOnOpen) {
|
|
1314
|
+
this.db.exec(DROP_DERIVED_SQL);
|
|
1315
|
+
}
|
|
1316
|
+
this.db.exec(SCHEMA_SQL);
|
|
1317
|
+
this.setMeta("format_version", String(INDEX_FORMAT_VERSION));
|
|
1318
|
+
this.db.exec("COMMIT");
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
this.db.exec("ROLLBACK");
|
|
1321
|
+
throw error;
|
|
1322
|
+
}
|
|
1323
|
+
this.db.exec("PRAGMA temp_store = MEMORY");
|
|
1324
|
+
this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS temp.snippet_fts USING fts5(
|
|
1325
|
+
text, tokenize = 'porter unicode61'
|
|
1326
|
+
)`);
|
|
1327
|
+
this.stmts = {
|
|
1328
|
+
selectFtsRows: this.db.prepare("SELECT rowid FROM message_rows WHERE docId = ?"),
|
|
1329
|
+
deleteFts: this.db.prepare("DELETE FROM messages_fts WHERE rowid = ?"),
|
|
1330
|
+
deleteMessageRows: this.db.prepare("DELETE FROM message_rows WHERE docId = ?"),
|
|
1331
|
+
insertMessageRow: this.db.prepare(
|
|
1332
|
+
"INSERT INTO message_rows (docId, msgIndex, role) VALUES (?, ?, ?) RETURNING rowid"
|
|
1333
|
+
),
|
|
1334
|
+
insertFts: this.db.prepare("INSERT INTO messages_fts (rowid, text) VALUES (?, ?)"),
|
|
1335
|
+
deleteConversation: this.db.prepare("DELETE FROM conversations WHERE docId = ?"),
|
|
1336
|
+
insertConversation: this.db.prepare(
|
|
1337
|
+
`INSERT INTO conversations
|
|
1338
|
+
(docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned, accountId, accountLabel, providerProject, providerWorkspace, models, fingerprint)
|
|
1339
|
+
VALUES (@docId, @platform, @title, @url, @createdAt, @updatedAt, @messageCount, @preview, @pinned, @accountId, @accountLabel, @providerProject, @providerWorkspace, @models, @fingerprint)`
|
|
1340
|
+
),
|
|
1341
|
+
deleteMemoryFts: this.db.prepare("DELETE FROM memories_fts WHERE memoryId = ?"),
|
|
1342
|
+
insertMemoryFts: this.db.prepare(
|
|
1343
|
+
"INSERT INTO memories_fts (memoryId, label, text) VALUES (?, ?, ?)"
|
|
1344
|
+
),
|
|
1345
|
+
deleteMemory: this.db.prepare("DELETE FROM memories WHERE memoryId = ?"),
|
|
1346
|
+
insertMemory: this.db.prepare(
|
|
1347
|
+
`INSERT INTO memories
|
|
1348
|
+
(memoryId, kind, label, text, source, pinned, enabled, createdAt, updatedAt)
|
|
1349
|
+
VALUES (@memoryId, @kind, @label, @text, @source, @pinned, @enabled, @createdAt, @updatedAt)`
|
|
1350
|
+
),
|
|
1351
|
+
clearSnippetFts: this.db.prepare("DELETE FROM temp.snippet_fts"),
|
|
1352
|
+
insertSnippetFts: this.db.prepare("INSERT INTO temp.snippet_fts (text) VALUES (?)"),
|
|
1353
|
+
renderSnippet: this.db.prepare(
|
|
1354
|
+
`SELECT snippet(snippet_fts, 0, '[', ']', '\u2026', ${SNIPPET_TOKENS}) AS snippet
|
|
1355
|
+
FROM temp.snippet_fts WHERE snippet_fts MATCH ?`
|
|
1356
|
+
)
|
|
1357
|
+
};
|
|
1358
|
+
for (const statement of Object.values(this.stmts)) {
|
|
1359
|
+
statement.setReadBigInts(true);
|
|
1360
|
+
}
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.db.close();
|
|
1363
|
+
throw error;
|
|
1350
1364
|
}
|
|
1351
1365
|
}
|
|
1352
1366
|
/** True when the stored format version was missing or stale — caller rebuilds. */
|
|
@@ -1661,15 +1675,24 @@ var SearchIndex = class {
|
|
|
1661
1675
|
};
|
|
1662
1676
|
async function openSearchIndex(corpus, options = {}) {
|
|
1663
1677
|
await corpus.init();
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1678
|
+
const deadline = Date.now() + (options.busyTimeoutMs ?? INDEX_OPEN_BUSY_TIMEOUT_MS);
|
|
1679
|
+
for (; ; ) {
|
|
1680
|
+
let index;
|
|
1681
|
+
try {
|
|
1682
|
+
index = new SearchIndex(corpus.paths.indexDbFile, corpus);
|
|
1683
|
+
if (options.autoBuild !== false && (index.needsRebuild() || index.count() === 0 && index.countMemories() === 0)) {
|
|
1684
|
+
await index.rebuildFrom(corpus);
|
|
1685
|
+
}
|
|
1686
|
+
return index;
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
index?.close();
|
|
1689
|
+
const code = error?.errcode;
|
|
1690
|
+
if (typeof code !== "number" || ![5, 6].includes(code & 255)) throw error;
|
|
1691
|
+
if (Date.now() >= deadline) {
|
|
1692
|
+
throw new Error("The local conversation index is busy. Wait for other MCP clients to finish updating it, then restart this client.", { cause: error });
|
|
1693
|
+
}
|
|
1694
|
+
await new Promise((resolve2) => setTimeout(resolve2, INDEX_OPEN_BUSY_RETRY_MS));
|
|
1668
1695
|
}
|
|
1669
|
-
return index;
|
|
1670
|
-
} catch (error) {
|
|
1671
|
-
index.close();
|
|
1672
|
-
throw error;
|
|
1673
1696
|
}
|
|
1674
1697
|
}
|
|
1675
1698
|
async function deleteIndexDb(dbFile) {
|
|
@@ -18,13 +18,13 @@ import {
|
|
|
18
18
|
runtimeBinDir,
|
|
19
19
|
staleIndexFiles,
|
|
20
20
|
uninstallRuntime
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-QIRYUQIU.js";
|
|
22
22
|
import {
|
|
23
23
|
VERSION,
|
|
24
24
|
__commonJS,
|
|
25
25
|
__require,
|
|
26
26
|
__toESM
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-GHKXNM7R.js";
|
|
28
28
|
|
|
29
29
|
// ../node_modules/isexe/windows.js
|
|
30
30
|
var require_windows = __commonJS({
|
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-GHKXNM7R.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-CMCIFSWH.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|
package/dist/llmnesia.mcpb
CHANGED
|
Binary file
|
package/dist/mcp.js
CHANGED
|
@@ -15,13 +15,13 @@ import {
|
|
|
15
15
|
runAutomaticMaintenance,
|
|
16
16
|
scheduleSelfUpdate,
|
|
17
17
|
upsertIndexedConversation
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-QIRYUQIU.js";
|
|
19
19
|
import {
|
|
20
20
|
VERSION,
|
|
21
21
|
__commonJS,
|
|
22
22
|
__export,
|
|
23
23
|
__toESM
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GHKXNM7R.js";
|
|
25
25
|
|
|
26
26
|
// ../node_modules/ajv/dist/compile/codegen/code.js
|
|
27
27
|
var require_code = __commonJS({
|
|
@@ -22190,7 +22190,7 @@ async function runServe() {
|
|
|
22190
22190
|
"conversation_inventory",
|
|
22191
22191
|
{
|
|
22192
22192
|
title: "Conversation inventory",
|
|
22193
|
-
description: "Read-only, paginated inventory of every conversation in the corpus as compact metadata: stable id, platform, title, and a versioned content fingerprint. Never returns transcripts. Built for services that periodically process the corpus: call it with no arguments to start a scan, then pass each page's nextCursor back as cursor until a page comes back without one. All pages of a scan reflect the same consistent snapshot taken when the scan started, so conversations that arrive or change mid-scan never shuffle the results; start a new scan (omit cursor) to see them. Compare ids and fingerprints against the previous scan and call get_conversation only for new or changed ids. Cursors expire and do not survive a server restart; on a cursor error, simply begin a new scan.",
|
|
22193
|
+
description: "Read-only, paginated inventory of every conversation in the corpus as compact metadata: stable id, platform, title, and a versioned content fingerprint. Never returns transcripts. Built for services that periodically process the corpus: call it with no arguments to start a scan, then pass each page's nextCursor back as cursor until a page comes back without one. All pages of a scan reflect the same consistent snapshot taken when the scan started, so conversations that arrive or change mid-scan never shuffle the results; start a new scan (omit cursor) to see them. Compare ids and fingerprints against the previous scan and call get_conversation only for new or changed ids. Cursors expire and do not survive a server restart; on a cursor error, simply begin a new scan. Fingerprints use fp2, including images, matching get_conversation's contentFingerprint for unchanged content. When upgrading from fp1, take a new complete inventory as your baseline; different scheme prefixes cannot be compared as content revisions.",
|
|
22194
22194
|
inputSchema: {
|
|
22195
22195
|
cursor: external_exports.string().optional().describe("Opaque nextCursor from the previous page. Omit to start a new scan."),
|
|
22196
22196
|
limit: external_exports.number().int().min(1).max(100).optional().describe("Items per page (default 100).")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmnesia/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"comment:mcpName": "The MCP Registry's ownership marker: it must equal server.json's `name`, and the namespace must match how you authenticate with mcp-publisher. `com.llmnesia/...` requires DNS auth (a TXT record on llmnesia.com) — see docs/MCP_INSTALLER_BOARD.md.",
|
package/server.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"name": "com.llmnesia/mcp",
|
|
4
4
|
"title": "LLMnesia",
|
|
5
5
|
"description": "Search your AI chat history from Claude, Cursor, Codex, and other MCP clients.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.13",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
11
11
|
"identifier": "@llmnesia/mcp",
|
|
12
|
-
"version": "0.2.
|
|
12
|
+
"version": "0.2.13",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|