@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 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 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.
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 v5; opening an older index triggers a rebuild that computes
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
- If that differs from the inventory entry, the conversation changed after
273
- your snapshot was taken and the transcript you just fetched is the current
274
- content.
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
 
@@ -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.12" : "0.0.0-dev";
41
+ var VERSION = true ? "0.2.13" : "0.0.0-dev";
42
42
 
43
43
  export {
44
44
  __require,
@@ -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-SEBISBN5.js";
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 = 7;
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
- this.db.exec("PRAGMA journal_mode = WAL");
1305
- this.db.exec("PRAGMA foreign_keys = OFF");
1306
- this.db.exec(META_SQL);
1307
- this.staleOnOpen = this.getMeta("format_version") !== String(INDEX_FORMAT_VERSION);
1308
- if (this.staleOnOpen) {
1309
- this.db.exec(DROP_DERIVED_SQL);
1310
- }
1311
- this.db.exec(SCHEMA_SQL);
1312
- this.db.exec("PRAGMA temp_store = MEMORY");
1313
- this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS temp.snippet_fts USING fts5(
1314
- text, tokenize = 'porter unicode61'
1315
- )`);
1316
- this.setMeta("format_version", String(INDEX_FORMAT_VERSION));
1317
- this.stmts = {
1318
- selectFtsRows: this.db.prepare("SELECT rowid FROM message_rows WHERE docId = ?"),
1319
- deleteFts: this.db.prepare("DELETE FROM messages_fts WHERE rowid = ?"),
1320
- deleteMessageRows: this.db.prepare("DELETE FROM message_rows WHERE docId = ?"),
1321
- insertMessageRow: this.db.prepare(
1322
- "INSERT INTO message_rows (docId, msgIndex, role) VALUES (?, ?, ?) RETURNING rowid"
1323
- ),
1324
- insertFts: this.db.prepare("INSERT INTO messages_fts (rowid, text) VALUES (?, ?)"),
1325
- deleteConversation: this.db.prepare("DELETE FROM conversations WHERE docId = ?"),
1326
- insertConversation: this.db.prepare(
1327
- `INSERT INTO conversations
1328
- (docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned, accountId, accountLabel, providerProject, providerWorkspace, models, fingerprint)
1329
- VALUES (@docId, @platform, @title, @url, @createdAt, @updatedAt, @messageCount, @preview, @pinned, @accountId, @accountLabel, @providerProject, @providerWorkspace, @models, @fingerprint)`
1330
- ),
1331
- deleteMemoryFts: this.db.prepare("DELETE FROM memories_fts WHERE memoryId = ?"),
1332
- insertMemoryFts: this.db.prepare(
1333
- "INSERT INTO memories_fts (memoryId, label, text) VALUES (?, ?, ?)"
1334
- ),
1335
- deleteMemory: this.db.prepare("DELETE FROM memories WHERE memoryId = ?"),
1336
- insertMemory: this.db.prepare(
1337
- `INSERT INTO memories
1338
- (memoryId, kind, label, text, source, pinned, enabled, createdAt, updatedAt)
1339
- VALUES (@memoryId, @kind, @label, @text, @source, @pinned, @enabled, @createdAt, @updatedAt)`
1340
- ),
1341
- clearSnippetFts: this.db.prepare("DELETE FROM temp.snippet_fts"),
1342
- insertSnippetFts: this.db.prepare("INSERT INTO temp.snippet_fts (text) VALUES (?)"),
1343
- renderSnippet: this.db.prepare(
1344
- `SELECT snippet(snippet_fts, 0, '[', ']', '\u2026', ${SNIPPET_TOKENS}) AS snippet
1345
- FROM temp.snippet_fts WHERE snippet_fts MATCH ?`
1346
- )
1347
- };
1348
- for (const statement of Object.values(this.stmts)) {
1349
- statement.setReadBigInts(true);
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 index = new SearchIndex(corpus.paths.indexDbFile, corpus);
1665
- try {
1666
- if (options.autoBuild !== false && (index.needsRebuild() || index.count() === 0 && index.countMemories() === 0)) {
1667
- await index.rebuildFrom(corpus);
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-3QDA3XWA.js";
21
+ } from "./chunk-QIRYUQIU.js";
22
22
  import {
23
23
  VERSION,
24
24
  __commonJS,
25
25
  __require,
26
26
  __toESM
27
- } from "./chunk-SEBISBN5.js";
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-SEBISBN5.js";
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-IF63HSCG.js");
144
+ const { main } = await import("./cli-CMCIFSWH.js");
145
145
  return main();
146
146
  }
147
147
  bootstrap().then((code) => {
Binary file
package/dist/mcp.js CHANGED
@@ -15,13 +15,13 @@ import {
15
15
  runAutomaticMaintenance,
16
16
  scheduleSelfUpdate,
17
17
  upsertIndexedConversation
18
- } from "./chunk-3QDA3XWA.js";
18
+ } from "./chunk-QIRYUQIU.js";
19
19
  import {
20
20
  VERSION,
21
21
  __commonJS,
22
22
  __export,
23
23
  __toESM
24
- } from "./chunk-SEBISBN5.js";
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.12",
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.12",
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",
12
+ "version": "0.2.13",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }