@llmnesia/mcp 0.2.7 → 0.2.9
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 +82 -8
- package/dist/{chunk-O34A5U2L.js → chunk-BWEDXPNM.js} +61 -9
- package/dist/{chunk-CMQHGLAD.js → chunk-YT6GNKKG.js} +1 -1
- package/dist/{cli-XQUMZ3BU.js → cli-KHY7ADCU.js} +76 -18
- package/dist/cli.js +2 -2
- package/dist/llmnesia-claude-host.mjs +237 -28
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +169 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -24,12 +24,19 @@ Claude Desktop step on Windows:
|
|
|
24
24
|
|
|
25
25
|
1. Install LLMnesia and import the chat history you want available.
|
|
26
26
|
2. Open **Full settings → Connect desktop AI**.
|
|
27
|
-
3.
|
|
28
|
-
|
|
27
|
+
3. Run the exact connector command shown there.
|
|
28
|
+
4. Fully quit and reopen the browser.
|
|
29
|
+
5. Return to **Connect desktop AI** and enable automatic sync.
|
|
30
|
+
|
|
31
|
+
> **Installing the npm package is not setup.** Running
|
|
32
|
+
> `npm install @llmnesia/mcp` only downloads package files; it does not register
|
|
33
|
+
> the native browser connector or configure desktop AI apps. Use the exact
|
|
34
|
+
> `npx -y @llmnesia/mcp@latest install` command below (or `npx.cmd` on Windows),
|
|
35
|
+
> including the final `install` argument.
|
|
29
36
|
|
|
30
37
|
The commands below are the connector-install step from that guided flow. They
|
|
31
|
-
are also provided as a reference for technical users;
|
|
32
|
-
|
|
38
|
+
are also provided as a reference for technical users; the browser restart and
|
|
39
|
+
permission step are still required afterward.
|
|
33
40
|
|
|
34
41
|
### Connector command
|
|
35
42
|
|
|
@@ -64,7 +71,7 @@ The installer:
|
|
|
64
71
|
corpus,
|
|
65
72
|
2. copies the server into `~/.llmnesia/bin` so it has a permanent home,
|
|
66
73
|
3. sets up the supported desktop AI apps (Claude Code, Cursor,
|
|
67
|
-
|
|
74
|
+
Codex/ChatGPT, and ZCode), and
|
|
68
75
|
4. registers that copy using the client's own CLI where available, or with an
|
|
69
76
|
automatic backup before editing its documented user config. It prepares all
|
|
70
77
|
of those standard config paths, so an app installed later does not require
|
|
@@ -175,24 +182,90 @@ quarantined input.
|
|
|
175
182
|
|
|
176
183
|
## Tools
|
|
177
184
|
|
|
178
|
-
Once registered, your MCP client gains
|
|
185
|
+
Once registered, your MCP client gains eight tools over the corpus:
|
|
179
186
|
|
|
180
187
|
| Tool | Purpose |
|
|
181
188
|
| --- | --- |
|
|
182
189
|
| `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. |
|
|
183
|
-
| `get_conversation` | Fetch the full transcript of one conversation by its id (the `id` field from search results). |
|
|
190
|
+
| `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). |
|
|
184
191
|
| `list_conversations` | List conversations by chronology, including oldest-first for questions about the user's earliest chat. |
|
|
185
192
|
| `list_recent` | List the most recently updated conversations with title, summary, and platform. |
|
|
193
|
+
| `conversation_inventory` | Paginated, read-only inventory of every conversation as compact metadata: id, platform, title, and a versioned content fingerprint. Built for services that periodically process the corpus; see below. |
|
|
186
194
|
| `corpus_status` | Report when the corpus last received data and how many conversations it holds. Use this when an expected conversation cannot be found, to tell a genuinely missing chat apart from a corpus that has stopped receiving updates. |
|
|
187
195
|
| `save_conversation` | Add or update a conversation in the corpus, so MCP is also an ingestion route. |
|
|
188
196
|
| `save_memory` | Record a durable fact (a decision, preference, or correction) worth carrying into later chats. Stored turned off until the user reviews and enables it. |
|
|
189
197
|
|
|
198
|
+
### Conversation inventory
|
|
199
|
+
|
|
200
|
+
`conversation_inventory` exists for programs that periodically process the
|
|
201
|
+
whole corpus, not for interactive questions. Each item is compact metadata:
|
|
202
|
+
the stable `id`, `platform`, `title`, and a versioned content `fingerprint`.
|
|
203
|
+
It never returns transcripts.
|
|
204
|
+
|
|
205
|
+
Call it with no arguments to start a scan, then pass each page's `nextCursor`
|
|
206
|
+
back as `cursor` until a page comes back without one. Pages hold up to 100
|
|
207
|
+
items (`limit` can go lower):
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"count": 100,
|
|
212
|
+
"total": 250,
|
|
213
|
+
"scanStartedAt": 1769000000000,
|
|
214
|
+
"items": [
|
|
215
|
+
{ "id": "claude:rust-borrow", "platform": "claude",
|
|
216
|
+
"title": "Debugging a Rust borrow checker error",
|
|
217
|
+
"fingerprint": "fp1:9af3c0d7…" }
|
|
218
|
+
],
|
|
219
|
+
"nextCursor": "inv1.eyJzIjoi…"
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Every page of a scan reads the same snapshot, captured (sorted by id) when the
|
|
224
|
+
scan started. Conversations that arrive or change while you are paging never
|
|
225
|
+
appear in, disappear from, or change values within an in-flight scan; start a
|
|
226
|
+
new scan (omit `cursor`) to see them. Snapshots are bounded: at most eight are
|
|
227
|
+
held at once (the oldest is evicted) and each expires after about 15 minutes.
|
|
228
|
+
Cursors are opaque, they do not survive an MCP server restart, and an expired
|
|
229
|
+
or unknown cursor returns a clear error telling the caller to begin a new
|
|
230
|
+
scan.
|
|
231
|
+
|
|
232
|
+
The fingerprint is `fp1:` followed by a SHA-256 hex digest. It covers exactly:
|
|
233
|
+
|
|
234
|
+
- the conversation title, and the user's local alias (`localTitle`) when set;
|
|
235
|
+
- 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).
|
|
238
|
+
|
|
239
|
+
It deliberately excludes every timestamp, including the provider's
|
|
240
|
+
`updatedAt`: historical imports can retain old source dates, and a genuinely
|
|
241
|
+
edited conversation can arrive with its source dates unchanged. URLs, pin
|
|
242
|
+
state, account and model identifiers, summaries, and other derived fields are
|
|
243
|
+
excluded too. An identical re-ingest therefore never changes the fingerprint,
|
|
244
|
+
while an edited message or a rename always does, even when nothing else about
|
|
245
|
+
the record moved. The fingerprint is stored in the derived search index
|
|
246
|
+
(index format v5; opening an older index triggers a rebuild that computes
|
|
247
|
+
fingerprints for the existing corpus), so a scan never decompresses a single
|
|
248
|
+
transcript.
|
|
249
|
+
|
|
250
|
+
A periodic-processing loop:
|
|
251
|
+
|
|
252
|
+
1. Scan all pages of `conversation_inventory`, collecting each `id` and
|
|
253
|
+
`fingerprint`.
|
|
254
|
+
2. Compare with the previous run: an id not seen before is new; a changed
|
|
255
|
+
fingerprint means that conversation's content changed. Everything else can
|
|
256
|
+
be skipped.
|
|
257
|
+
3. Call `get_conversation` only for the new or changed ids. Its response
|
|
258
|
+
carries `contentFingerprint`, the fingerprint of the transcript it returns.
|
|
259
|
+
If that differs from the inventory entry, the conversation changed after
|
|
260
|
+
your snapshot was taken and the transcript you just fetched is the current
|
|
261
|
+
content.
|
|
262
|
+
|
|
190
263
|
### Read-only mode (`LLMNESIA_READ_ONLY`)
|
|
191
264
|
|
|
192
265
|
The Claude Desktop extension (`.mcpb`) ships with `LLMNESIA_READ_ONLY=1`, which
|
|
193
266
|
disables the two write tools (`save_conversation` and `save_memory`) so that
|
|
194
267
|
ordinary history searches run without a per-call approval prompt. The general
|
|
195
|
-
npm install exposes all
|
|
268
|
+
npm install exposes all eight tools.
|
|
196
269
|
|
|
197
270
|
Search and list results carry a `corpus` block saying when the corpus last
|
|
198
271
|
received data — check it before concluding a chat does not exist.
|
|
@@ -205,6 +278,7 @@ received data — check it before concluding a chat does not exist.
|
|
|
205
278
|
| Claude Desktop | macOS uses the working local JSON registration. On Windows, run `install` for browser sync, then install the official `llmnesia.mcpb` once in Claude Desktop; Claude exposes no supported external installation API for custom desktop extensions. |
|
|
206
279
|
| Cursor | Edits `~/.cursor/mcp.json`, preparing it in advance if Cursor is installed later. |
|
|
207
280
|
| Codex / ChatGPT desktop | Runs `codex mcp add` with the absolute runtime paths when available. If the bundled CLI is unavailable, the installer safely edits `~/.codex/config.toml` itself and backs up existing settings first, including preparing it for a later installation. Read-only LLMnesia tools are configured to run without repeated approval prompts; writes still ask. |
|
|
281
|
+
| ZCode | Edits the user-scope `~/.zcode/cli/config.json`, adding the server under ZCode's nested `mcp.servers` key with a backup first, and prepares that file in advance if ZCode is installed later. ZCode validates this file strictly and connects servers from it automatically at session start. |
|
|
208
282
|
| Others (Windsurf, Zed, LM Studio, …) | Prints the generic stdio block for you to paste. |
|
|
209
283
|
|
|
210
284
|
**Cloud-only clients cannot reach a local stdio server** (web ChatGPT, Lovable,
|
|
@@ -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-YT6GNKKG.js";
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
9
9
|
import { readFileSync } from "fs";
|
|
@@ -47,7 +47,7 @@ function expandHome(p) {
|
|
|
47
47
|
import { join as join2 } from "path";
|
|
48
48
|
import { readdir } from "fs/promises";
|
|
49
49
|
import { createHash } from "crypto";
|
|
50
|
-
var INDEX_FORMAT_VERSION =
|
|
50
|
+
var INDEX_FORMAT_VERSION = 5;
|
|
51
51
|
function indexDbFilename(version) {
|
|
52
52
|
return version === 1 ? "llmnesia.db" : `llmnesia-v${version}.db`;
|
|
53
53
|
}
|
|
@@ -198,6 +198,8 @@ function canonicalConversationDoc(c) {
|
|
|
198
198
|
indexLevel: c.indexLevel,
|
|
199
199
|
estimatedBytes: c.estimatedBytes
|
|
200
200
|
};
|
|
201
|
+
assignIfDefined(out, "localTitle", c.localTitle);
|
|
202
|
+
assignIfDefined(out, "localTitleUpdatedAt", c.localTitleUpdatedAt);
|
|
201
203
|
assignIfDefined(out, "searchDocLength", c.searchDocLength);
|
|
202
204
|
assignIfDefined(out, "contentHash", c.contentHash);
|
|
203
205
|
assignIfDefined(out, "truncated", c.truncated);
|
|
@@ -208,6 +210,13 @@ function canonicalConversationDoc(c) {
|
|
|
208
210
|
assignIfDefined(out, "origin", c.origin);
|
|
209
211
|
assignIfDefined(out, "accountId", c.accountId);
|
|
210
212
|
assignIfDefined(out, "accountLabel", c.accountLabel);
|
|
213
|
+
if (c.providerProject) {
|
|
214
|
+
out.providerProject = {
|
|
215
|
+
projectId: c.providerProject.projectId,
|
|
216
|
+
source: c.providerProject.source,
|
|
217
|
+
observedAt: c.providerProject.observedAt
|
|
218
|
+
};
|
|
219
|
+
}
|
|
211
220
|
if (c.models && c.models.length > 0) out.models = [...c.models];
|
|
212
221
|
assignIfDefined(out, "modelMetadataUpdatedAt", c.modelMetadataUpdatedAt);
|
|
213
222
|
if (c.summary) {
|
|
@@ -938,6 +947,26 @@ function accumulate(total, report) {
|
|
|
938
947
|
total.malformedLines += report.malformedLines;
|
|
939
948
|
}
|
|
940
949
|
|
|
950
|
+
// src/fingerprint.ts
|
|
951
|
+
import { createHash as createHash2 } from "crypto";
|
|
952
|
+
var FINGERPRINT_SCHEME = "fp1";
|
|
953
|
+
function fingerprintConversation(record) {
|
|
954
|
+
const conversation = record.conversation;
|
|
955
|
+
const payload = {
|
|
956
|
+
v: 1,
|
|
957
|
+
title: conversation.title,
|
|
958
|
+
localTitle: conversation.localTitle ?? null,
|
|
959
|
+
messages: [...record.messages].sort((a, b) => a.msgIndex - b.msgIndex).map((message) => ({
|
|
960
|
+
i: message.msgIndex,
|
|
961
|
+
r: message.role,
|
|
962
|
+
t: message.text,
|
|
963
|
+
f: message.formattedText ?? null
|
|
964
|
+
}))
|
|
965
|
+
};
|
|
966
|
+
const digest = createHash2("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
|
|
967
|
+
return `${FINGERPRINT_SCHEME}:${digest}`;
|
|
968
|
+
}
|
|
969
|
+
|
|
941
970
|
// src/searchIndex.ts
|
|
942
971
|
import { promises as fs3, chmodSync, closeSync, openSync } from "fs";
|
|
943
972
|
import { createRequire } from "module";
|
|
@@ -1015,7 +1044,8 @@ CREATE TABLE IF NOT EXISTS conversations (
|
|
|
1015
1044
|
pinned INTEGER NOT NULL,
|
|
1016
1045
|
accountId TEXT,
|
|
1017
1046
|
accountLabel TEXT,
|
|
1018
|
-
models TEXT
|
|
1047
|
+
models TEXT,
|
|
1048
|
+
fingerprint TEXT NOT NULL
|
|
1019
1049
|
);
|
|
1020
1050
|
CREATE INDEX IF NOT EXISTS idx_conversations_platform ON conversations(platform);
|
|
1021
1051
|
CREATE INDEX IF NOT EXISTS idx_conversations_createdAt ON conversations(createdAt);
|
|
@@ -1089,8 +1119,8 @@ var SearchIndex = class {
|
|
|
1089
1119
|
deleteConversation: this.db.prepare("DELETE FROM conversations WHERE docId = ?"),
|
|
1090
1120
|
insertConversation: this.db.prepare(
|
|
1091
1121
|
`INSERT INTO conversations
|
|
1092
|
-
(docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned, accountId, accountLabel, models)
|
|
1093
|
-
VALUES (@docId, @platform, @title, @url, @createdAt, @updatedAt, @messageCount, @preview, @pinned, @accountId, @accountLabel, @models)`
|
|
1122
|
+
(docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned, accountId, accountLabel, models, fingerprint)
|
|
1123
|
+
VALUES (@docId, @platform, @title, @url, @createdAt, @updatedAt, @messageCount, @preview, @pinned, @accountId, @accountLabel, @models, @fingerprint)`
|
|
1094
1124
|
),
|
|
1095
1125
|
deleteMemoryFts: this.db.prepare("DELETE FROM memories_fts WHERE memoryId = ?"),
|
|
1096
1126
|
insertMemoryFts: this.db.prepare(
|
|
@@ -1147,6 +1177,17 @@ var SearchIndex = class {
|
|
|
1147
1177
|
return entry;
|
|
1148
1178
|
});
|
|
1149
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* The full conversation inventory as compact metadata, sorted by stable
|
|
1182
|
+
* docId. One statement, so the caller's snapshot is a consistent cut of the
|
|
1183
|
+
* index (new conversations and edits that commit after this read belong to
|
|
1184
|
+
* the next scan), and no transcript is ever decompressed: fingerprints are
|
|
1185
|
+
* computed at upsert/rebuild time and stored in the index.
|
|
1186
|
+
*/
|
|
1187
|
+
inventoryRows() {
|
|
1188
|
+
const rows = this.db.prepare("SELECT docId, platform, title, fingerprint FROM conversations ORDER BY docId ASC").all();
|
|
1189
|
+
return rows.map((r) => ({ id: r.docId, platform: r.platform, title: r.title, fingerprint: r.fingerprint }));
|
|
1190
|
+
}
|
|
1150
1191
|
/**
|
|
1151
1192
|
* Run `fn` inside a transaction, rolling back if it throws. node:sqlite has
|
|
1152
1193
|
* no transaction() wrapper, so this is the one place BEGIN/COMMIT/ROLLBACK
|
|
@@ -1208,9 +1249,14 @@ var SearchIndex = class {
|
|
|
1208
1249
|
// transaction() helper for single upserts, or rebuildFrom's manual one).
|
|
1209
1250
|
writeRecord(rec) {
|
|
1210
1251
|
const { conversation, messages } = rec;
|
|
1252
|
+
const displayTitle = conversation.localTitle?.trim() || conversation.title;
|
|
1211
1253
|
this.deleteFtsRows(conversation.docId);
|
|
1212
1254
|
this.stmts.deleteConversation.run(conversation.docId);
|
|
1213
|
-
const titleText = [
|
|
1255
|
+
const titleText = [
|
|
1256
|
+
...conversation.localTitle ? [conversation.localTitle, conversation.localTitle] : [],
|
|
1257
|
+
conversation.title,
|
|
1258
|
+
conversation.preview
|
|
1259
|
+
].filter(Boolean).join("\n");
|
|
1214
1260
|
if (titleText.trim()) {
|
|
1215
1261
|
this.insertFtsRow(conversation.docId, TITLE_ROW_INDEX, TITLE_ROLE, titleText);
|
|
1216
1262
|
}
|
|
@@ -1222,7 +1268,7 @@ var SearchIndex = class {
|
|
|
1222
1268
|
this.stmts.insertConversation.run({
|
|
1223
1269
|
docId: conversation.docId,
|
|
1224
1270
|
platform: conversation.platform,
|
|
1225
|
-
title:
|
|
1271
|
+
title: displayTitle,
|
|
1226
1272
|
url: conversation.url,
|
|
1227
1273
|
createdAt: conversation.createdAt,
|
|
1228
1274
|
updatedAt: conversation.updatedAt ?? conversation.contentUpdatedAt ?? 0,
|
|
@@ -1231,7 +1277,8 @@ var SearchIndex = class {
|
|
|
1231
1277
|
pinned: conversation.pinned ? 1 : 0,
|
|
1232
1278
|
accountId: conversation.accountId ?? null,
|
|
1233
1279
|
accountLabel: conversation.accountLabel ?? null,
|
|
1234
|
-
models: conversation.models?.length ? JSON.stringify(conversation.models) : null
|
|
1280
|
+
models: conversation.models?.length ? JSON.stringify(conversation.models) : null,
|
|
1281
|
+
fingerprint: fingerprintConversation(rec)
|
|
1235
1282
|
});
|
|
1236
1283
|
}
|
|
1237
1284
|
insertFtsRow(docId, msgIndex, role, text) {
|
|
@@ -1350,7 +1397,11 @@ var SearchIndex = class {
|
|
|
1350
1397
|
}
|
|
1351
1398
|
hydrateSnippet(row, match) {
|
|
1352
1399
|
const record = this.corpus?.getFromPlatformSync(row.platform, row.docId);
|
|
1353
|
-
const source = row.msgIndex === TITLE_ROW_INDEX ? [
|
|
1400
|
+
const source = row.msgIndex === TITLE_ROW_INDEX ? [
|
|
1401
|
+
record?.conversation.localTitle,
|
|
1402
|
+
record?.conversation.title ?? row.title,
|
|
1403
|
+
record?.conversation.preview ?? row.preview
|
|
1404
|
+
].filter(Boolean).join("\n") : record?.messages.find((message) => message.msgIndex === row.msgIndex)?.text ?? row.preview;
|
|
1354
1405
|
this.stmts.clearSnippetFts.run();
|
|
1355
1406
|
try {
|
|
1356
1407
|
this.stmts.insertSnippetFts.run(source);
|
|
@@ -2099,6 +2150,7 @@ export {
|
|
|
2099
2150
|
ingestFile,
|
|
2100
2151
|
pendingInboxFiles,
|
|
2101
2152
|
drainInbox,
|
|
2153
|
+
fingerprintConversation,
|
|
2102
2154
|
SearchIndex,
|
|
2103
2155
|
openSearchIndex,
|
|
2104
2156
|
deleteIndexDb,
|
|
@@ -17,13 +17,13 @@ import {
|
|
|
17
17
|
runtimeBinDir,
|
|
18
18
|
staleIndexFiles,
|
|
19
19
|
uninstallRuntime
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-BWEDXPNM.js";
|
|
21
21
|
import {
|
|
22
22
|
VERSION,
|
|
23
23
|
__commonJS,
|
|
24
24
|
__require,
|
|
25
25
|
__toESM
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-YT6GNKKG.js";
|
|
27
27
|
|
|
28
28
|
// ../node_modules/isexe/windows.js
|
|
29
29
|
var require_windows = __commonJS({
|
|
@@ -543,6 +543,7 @@ var STORE_EXTENSION_ID = "leekfgbdojiaabifbjbbgiiclannjdkf";
|
|
|
543
543
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
544
544
|
var home = homedir();
|
|
545
545
|
var CLAUDE_PROJECTS_DIR = join(home, ".claude", "projects");
|
|
546
|
+
var ZCODE_DB = join(home, ".zcode", "cli", "db", "db.sqlite");
|
|
546
547
|
var osPlatform = process.env.LLMNESIA_FAKE_PLATFORM || platform();
|
|
547
548
|
function expandHome(p) {
|
|
548
549
|
if (p === "~") {
|
|
@@ -708,7 +709,7 @@ async function installNativeHost({ ids = [], corpus, codexHome, force = false, q
|
|
|
708
709
|
log("\u2713 Helper installed");
|
|
709
710
|
const manifestBase = {
|
|
710
711
|
name: HOST_NAME,
|
|
711
|
-
description: "LLMnesia
|
|
712
|
+
description: "LLMnesia: reads local Claude Code, Codex, and ZCode sessions",
|
|
712
713
|
type: "stdio",
|
|
713
714
|
allowed_origins: allowedOrigins
|
|
714
715
|
};
|
|
@@ -742,9 +743,10 @@ async function installNativeHost({ ids = [], corpus, codexHome, force = false, q
|
|
|
742
743
|
log(` Conversation data: ${corpusRoot}`);
|
|
743
744
|
log(` Codex data: ${resolvedCodexHome}`);
|
|
744
745
|
}
|
|
745
|
-
const [hasClaudeCode, hasCodex] = await Promise.all([
|
|
746
|
+
const [hasClaudeCode, hasCodex, hasZcode] = await Promise.all([
|
|
746
747
|
exists(CLAUDE_PROJECTS_DIR),
|
|
747
|
-
exists(join(resolvedCodexHome, "sessions"))
|
|
748
|
+
exists(join(resolvedCodexHome, "sessions")),
|
|
749
|
+
exists(ZCODE_DB)
|
|
748
750
|
]);
|
|
749
751
|
log("");
|
|
750
752
|
log(
|
|
@@ -753,6 +755,9 @@ async function installNativeHost({ ids = [], corpus, codexHome, force = false, q
|
|
|
753
755
|
log(
|
|
754
756
|
hasCodex ? `\u2713 Found Codex sessions at ${resolvedCodexHome}` : `\u2013 No Codex sessions found yet (fine if you don't use Codex).`
|
|
755
757
|
);
|
|
758
|
+
log(
|
|
759
|
+
hasZcode ? `\u2713 Found ZCode sessions at ${ZCODE_DB}` : `\u2013 No ZCode sessions found yet (fine if you don't use ZCode).`
|
|
760
|
+
);
|
|
756
761
|
const extraIdCount = allIds.length - 1;
|
|
757
762
|
if (extraIdCount > 0) {
|
|
758
763
|
log(`\u2713 Also authorized ${extraIdCount} unpacked extension ${extraIdCount === 1 ? "build" : "builds"}`);
|
|
@@ -2841,7 +2846,8 @@ function paths(home2, osPlatform2, appData) {
|
|
|
2841
2846
|
claudeCode: join4(claudeCodeRoot, ".claude.json"),
|
|
2842
2847
|
claudeDesktop,
|
|
2843
2848
|
cursor: join4(home2, ".cursor", "mcp.json"),
|
|
2844
|
-
codex: join4(home2, ".codex", "config.toml")
|
|
2849
|
+
codex: join4(home2, ".codex", "config.toml"),
|
|
2850
|
+
zcode: join4(home2, ".zcode", "cli", "config.json")
|
|
2845
2851
|
};
|
|
2846
2852
|
}
|
|
2847
2853
|
async function readJsonFile(path) {
|
|
@@ -2873,6 +2879,38 @@ function mcpServersOf(data, configPath) {
|
|
|
2873
2879
|
}
|
|
2874
2880
|
return data.mcpServers;
|
|
2875
2881
|
}
|
|
2882
|
+
function nestedMcpServersOf(data, configPath) {
|
|
2883
|
+
if (data.mcp === void 0) {
|
|
2884
|
+
return {};
|
|
2885
|
+
}
|
|
2886
|
+
if (!data.mcp || typeof data.mcp !== "object" || Array.isArray(data.mcp)) {
|
|
2887
|
+
throw new Error(`Could not update ${configPath}: "mcp" must be a JSON object. The file was left unchanged.`);
|
|
2888
|
+
}
|
|
2889
|
+
const mcp = data.mcp;
|
|
2890
|
+
if (mcp.servers === void 0) {
|
|
2891
|
+
return {};
|
|
2892
|
+
}
|
|
2893
|
+
if (!mcp.servers || typeof mcp.servers !== "object" || Array.isArray(mcp.servers)) {
|
|
2894
|
+
throw new Error(
|
|
2895
|
+
`Could not update ${configPath}: "mcp.servers" must be a JSON object. The file was left unchanged.`
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
return mcp.servers;
|
|
2899
|
+
}
|
|
2900
|
+
function asObject(value) {
|
|
2901
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2902
|
+
}
|
|
2903
|
+
var topLevelServers = {
|
|
2904
|
+
read: (data, configPath) => mcpServersOf(data, configPath),
|
|
2905
|
+
merge: (data, servers) => ({ ...data, mcpServers: servers }),
|
|
2906
|
+
preview: (entry) => JSON.stringify({ mcpServers: { [SERVER_NAME]: entry } }, null, 2)
|
|
2907
|
+
};
|
|
2908
|
+
var zcodeNestedServers = {
|
|
2909
|
+
read: (data, configPath) => nestedMcpServersOf(data, configPath),
|
|
2910
|
+
// Keep any other settings ZCode stores beside `servers` inside its `mcp` key.
|
|
2911
|
+
merge: (data, servers) => ({ ...data, mcp: { ...asObject(data.mcp), servers } }),
|
|
2912
|
+
preview: (entry) => JSON.stringify({ mcp: { servers: { [SERVER_NAME]: entry } } }, null, 2)
|
|
2913
|
+
};
|
|
2876
2914
|
function isErrnoException(value) {
|
|
2877
2915
|
return value instanceof Error && "code" in value;
|
|
2878
2916
|
}
|
|
@@ -3091,7 +3129,7 @@ codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
|
|
|
3091
3129
|
detail: `Ran: codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
|
|
3092
3130
|
};
|
|
3093
3131
|
}
|
|
3094
|
-
async function registerJsonClient(clientLabel, configPath, serve, confirm, now, prepareMissing = false, prepareWhenConfigAbsent = false, includeTransportType = true) {
|
|
3132
|
+
async function registerJsonClient(clientLabel, configPath, serve, confirm, now, prepareMissing = false, prepareWhenConfigAbsent = false, includeTransportType = true, layout = topLevelServers) {
|
|
3095
3133
|
const configDirExists = await clientDirExists(configPath);
|
|
3096
3134
|
if (!configDirExists && !prepareMissing) {
|
|
3097
3135
|
return {
|
|
@@ -3117,7 +3155,7 @@ async function registerJsonClient(clientLabel, configPath, serve, confirm, now,
|
|
|
3117
3155
|
}
|
|
3118
3156
|
let servers;
|
|
3119
3157
|
try {
|
|
3120
|
-
servers =
|
|
3158
|
+
servers = layout.read(data, configPath);
|
|
3121
3159
|
} catch (err) {
|
|
3122
3160
|
return { client: clientLabel, action: "error", detail: errorMessage(err) };
|
|
3123
3161
|
}
|
|
@@ -3126,7 +3164,7 @@ async function registerJsonClient(clientLabel, configPath, serve, confirm, now,
|
|
|
3126
3164
|
if (already && JSON.stringify(already) === JSON.stringify(nextEntry)) {
|
|
3127
3165
|
return { client: clientLabel, action: "skipped", detail: `Already registered at ${configPath}.` };
|
|
3128
3166
|
}
|
|
3129
|
-
const preview =
|
|
3167
|
+
const preview = layout.preview(nextEntry);
|
|
3130
3168
|
const message = existed ? `${clientLabel}: update ${configPath} to add the "${SERVER_NAME}" MCP server? Existing servers and settings are preserved.
|
|
3131
3169
|
Add:
|
|
3132
3170
|
${preview}` : `${clientLabel}: create ${configPath} with the "${SERVER_NAME}" MCP server?
|
|
@@ -3140,7 +3178,7 @@ ${preview}`;
|
|
|
3140
3178
|
const backupPath = `${configPath}.bak-${now()}`;
|
|
3141
3179
|
await writeFile2(backupPath, raw ?? JSON.stringify(data, null, 2), "utf8");
|
|
3142
3180
|
}
|
|
3143
|
-
const nextData =
|
|
3181
|
+
const nextData = layout.merge(data, { ...servers, [SERVER_NAME]: nextEntry });
|
|
3144
3182
|
await writeFile2(configPath, `${JSON.stringify(nextData, null, 2)}
|
|
3145
3183
|
`, "utf8");
|
|
3146
3184
|
return {
|
|
@@ -3245,7 +3283,7 @@ async function registerClients(options) {
|
|
|
3245
3283
|
const osPlatform2 = options.osPlatform ?? platform2();
|
|
3246
3284
|
const now = options.now ?? (() => Date.now());
|
|
3247
3285
|
const prepareMissing = options.prepareMissingDesktopApps === true;
|
|
3248
|
-
const { claudeCode, claudeDesktop, cursor, codex } = paths(home2, osPlatform2, options.appData);
|
|
3286
|
+
const { claudeCode, claudeDesktop, cursor, codex, zcode } = paths(home2, osPlatform2, options.appData);
|
|
3249
3287
|
const results = [];
|
|
3250
3288
|
const safe = async (client, fn) => {
|
|
3251
3289
|
try {
|
|
@@ -3292,6 +3330,22 @@ async function registerClients(options) {
|
|
|
3292
3330
|
() => registerCodex(options.serve, options.codexCli ?? realCodexCli, codex, options.confirm, now, prepareMissing)
|
|
3293
3331
|
)
|
|
3294
3332
|
);
|
|
3333
|
+
results.push(
|
|
3334
|
+
await safe(
|
|
3335
|
+
"ZCode",
|
|
3336
|
+
() => registerJsonClient(
|
|
3337
|
+
"ZCode",
|
|
3338
|
+
zcode,
|
|
3339
|
+
options.serve,
|
|
3340
|
+
options.confirm,
|
|
3341
|
+
now,
|
|
3342
|
+
prepareMissing,
|
|
3343
|
+
false,
|
|
3344
|
+
true,
|
|
3345
|
+
zcodeNestedServers
|
|
3346
|
+
)
|
|
3347
|
+
)
|
|
3348
|
+
);
|
|
3295
3349
|
results.push({
|
|
3296
3350
|
client: "Other MCP clients (Windsurf, Zed, LM Studio, ...)",
|
|
3297
3351
|
action: "printed",
|
|
@@ -3368,7 +3422,7 @@ async function unregisterCodex(cli, configPath, now) {
|
|
|
3368
3422
|
return { client: "Codex", action: "error", detail: `codex mcp remove failed: ${errorMessage(err)}` };
|
|
3369
3423
|
}
|
|
3370
3424
|
}
|
|
3371
|
-
async function unregisterJsonClient(clientLabel, configPath, now) {
|
|
3425
|
+
async function unregisterJsonClient(clientLabel, configPath, now, layout = topLevelServers) {
|
|
3372
3426
|
let data;
|
|
3373
3427
|
let existed;
|
|
3374
3428
|
let raw;
|
|
@@ -3382,7 +3436,7 @@ async function unregisterJsonClient(clientLabel, configPath, now) {
|
|
|
3382
3436
|
}
|
|
3383
3437
|
let servers;
|
|
3384
3438
|
try {
|
|
3385
|
-
servers =
|
|
3439
|
+
servers = layout.read(data, configPath);
|
|
3386
3440
|
} catch (err) {
|
|
3387
3441
|
return { client: clientLabel, action: "error", detail: errorMessage(err) };
|
|
3388
3442
|
}
|
|
@@ -3393,7 +3447,7 @@ async function unregisterJsonClient(clientLabel, configPath, now) {
|
|
|
3393
3447
|
try {
|
|
3394
3448
|
await writeFile2(backupPath, raw ?? JSON.stringify(data, null, 2), "utf8");
|
|
3395
3449
|
const { [SERVER_NAME]: _removed, ...rest } = servers;
|
|
3396
|
-
const nextData =
|
|
3450
|
+
const nextData = layout.merge(data, rest);
|
|
3397
3451
|
await writeFile2(configPath, `${JSON.stringify(nextData, null, 2)}
|
|
3398
3452
|
`, "utf8");
|
|
3399
3453
|
return { client: clientLabel, action: "registered", detail: `Removed from ${configPath} (backup: ${backupPath}).` };
|
|
@@ -3405,7 +3459,7 @@ async function unregisterClients(options = {}) {
|
|
|
3405
3459
|
const home2 = options.home ?? homedir2();
|
|
3406
3460
|
const osPlatform2 = options.osPlatform ?? platform2();
|
|
3407
3461
|
const now = options.now ?? (() => Date.now());
|
|
3408
|
-
const { claudeDesktop, cursor, codex } = paths(home2, osPlatform2);
|
|
3462
|
+
const { claudeDesktop, cursor, codex, zcode } = paths(home2, osPlatform2);
|
|
3409
3463
|
const safe = async (client, fn) => {
|
|
3410
3464
|
try {
|
|
3411
3465
|
return await fn();
|
|
@@ -3421,6 +3475,7 @@ async function unregisterClients(options = {}) {
|
|
|
3421
3475
|
}
|
|
3422
3476
|
results.push(await safe("Cursor", () => unregisterJsonClient("Cursor", cursor, now)));
|
|
3423
3477
|
results.push(await safe("Codex", () => unregisterCodex(options.codexCli ?? realCodexCli, codex, now)));
|
|
3478
|
+
results.push(await safe("ZCode", () => unregisterJsonClient("ZCode", zcode, now, zcodeNestedServers)));
|
|
3424
3479
|
results.push({
|
|
3425
3480
|
client: "Other MCP clients",
|
|
3426
3481
|
action: "skipped",
|
|
@@ -3592,7 +3647,7 @@ function checkAutoUpdate(home2, env) {
|
|
|
3592
3647
|
const latest = state.lastSeenVersion ? `; latest published ${state.lastSeenVersion}` : "";
|
|
3593
3648
|
return { name, status: "ok", detail: `On. Last checked ${checkedAt}${latest}.` };
|
|
3594
3649
|
}
|
|
3595
|
-
async function checkJsonClient(label, configPath) {
|
|
3650
|
+
async function checkJsonClient(label, configPath, nestedServers = false) {
|
|
3596
3651
|
if (!existsSync(dirname5(configPath))) {
|
|
3597
3652
|
return { name: label, status: "warn", detail: `Not installed (no ${dirname5(configPath)}).` };
|
|
3598
3653
|
}
|
|
@@ -3605,7 +3660,8 @@ async function checkJsonClient(label, configPath) {
|
|
|
3605
3660
|
}
|
|
3606
3661
|
try {
|
|
3607
3662
|
const data = JSON.parse(readFileSync(configPath, "utf8"));
|
|
3608
|
-
const
|
|
3663
|
+
const servers = nestedServers ? data.mcp?.servers : data.mcpServers;
|
|
3664
|
+
const entry = servers?.[SERVER_NAME];
|
|
3609
3665
|
if (!entry) {
|
|
3610
3666
|
return {
|
|
3611
3667
|
name: label,
|
|
@@ -3741,6 +3797,7 @@ async function runDoctor(options = {}) {
|
|
|
3741
3797
|
await run("Cursor", () => checkJsonClient("Cursor", join5(home2, ".cursor", "mcp.json")));
|
|
3742
3798
|
await run("Claude Code", () => checkClaudeCode(claudeCli));
|
|
3743
3799
|
await run("Codex", () => checkCodex(home2, codexCli));
|
|
3800
|
+
await run("ZCode", () => checkJsonClient("ZCode", join5(home2, ".zcode", "cli", "config.json"), true));
|
|
3744
3801
|
checks.push({
|
|
3745
3802
|
name: "Reminder",
|
|
3746
3803
|
status: "warn",
|
|
@@ -3760,7 +3817,8 @@ Usage: llmnesia-mcp <command> [options]
|
|
|
3760
3817
|
|
|
3761
3818
|
Setup:
|
|
3762
3819
|
install [ext-id...] Install the native host + register this MCP server with
|
|
3763
|
-
detected clients (Claude Code/Desktop, Cursor, Codex,
|
|
3820
|
+
detected clients (Claude Code/Desktop, Cursor, Codex,
|
|
3821
|
+
ZCode, ...)
|
|
3764
3822
|
doctor Diagnose: host installed? corpus present? clients wired?
|
|
3765
3823
|
uninstall Remove native host + client registrations (never deletes
|
|
3766
3824
|
your corpus \u2014 see the printed path if you want that too)
|
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-YT6GNKKG.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-KHY7ADCU.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// LLMnesia native messaging host — reads local coding-agent sessions (Claude
|
|
3
|
-
// Code from ~/.claude/projects, Codex from the configured Codex home
|
|
4
|
-
// streams them to the LLMnesia browser
|
|
5
|
-
// the extension sends back into the
|
|
6
|
-
// packages/../companion/src/paths.ts).
|
|
3
|
+
// Code from ~/.claude/projects, Codex from the configured Codex home, ZCode
|
|
4
|
+
// from ~/.zcode/cli/db/db.sqlite) and streams them to the LLMnesia browser
|
|
5
|
+
// extension, and writes corpus sync deltas the extension sends back into the
|
|
6
|
+
// LLMnesia corpus's inbox/ directory (see packages/../companion/src/paths.ts).
|
|
7
7
|
//
|
|
8
|
-
// It reads .jsonl session files under
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// JSON, in both
|
|
8
|
+
// It reads .jsonl session files under the first two roots, materializes ZCode
|
|
9
|
+
// sessions out of its SQLite database, and writes only inside the corpus root
|
|
10
|
+
// named in its own config (~/.llmnesia/config.json) — never a path the caller
|
|
11
|
+
// supplies. It makes no network calls. The browser launches it on demand (see
|
|
12
|
+
// the installed com.llmnesia.claude_code.json manifest) and talks to it over
|
|
13
|
+
// stdio using Chrome's native-messaging framing: a little-endian uint32
|
|
14
|
+
// byte-length prefix followed by that many bytes of UTF-8 JSON, in both
|
|
15
|
+
// directions.
|
|
15
16
|
//
|
|
16
17
|
// The write path only ever activates for a user who has explicitly run the
|
|
17
18
|
// installer with --corpus (see install.mjs) to opt into the MCP companion;
|
|
18
19
|
// without ~/.llmnesia/config.json this host is exactly as read-only as it
|
|
19
20
|
// has always been for plain session indexing.
|
|
20
21
|
//
|
|
21
|
-
// Protocol
|
|
22
|
+
// Protocol v4 (extension → host). v1 was Claude-Code-only: `list` carried no
|
|
22
23
|
// `source` and `get` took a bare sessionId. Both remain accepted, so an
|
|
23
24
|
// extension older than this host keeps working; an extension NEWER than the
|
|
24
25
|
// host detects v1 from the pong and syncs Claude Code only.
|
|
25
26
|
//
|
|
27
|
+
// v4 adds ZCode to `list`/`get` (the first source backed by a SQLite database
|
|
28
|
+
// rather than per-session files; `get` materializes the transcript as JSONL).
|
|
29
|
+
// The version is informational in both directions, as below: a v3 host never
|
|
30
|
+
// lists zcode, and an extension gates the ZCode row on the reported version.
|
|
31
|
+
//
|
|
26
32
|
// v3 adds `listMemories`, the RC-03 reverse channel. A v2 host answers it with
|
|
27
33
|
// `{ type: "error", error: "unknown command: listMemories" }` and the extension
|
|
28
34
|
// treats that as "nothing to fetch", so the version is informational: hosts are
|
|
29
35
|
// installed out of band and an extension upgraded ahead of its host is the
|
|
30
36
|
// ordinary case, not a fault.
|
|
31
|
-
// { cmd: "ping" } → { type: "pong", version:
|
|
37
|
+
// { cmd: "ping" } → { type: "pong", version: 4 }
|
|
32
38
|
// { cmd: "list" } → { type: "list", sessions:
|
|
33
39
|
// [{source, sessionId, mtime, project?, title?}] }
|
|
34
40
|
// { cmd: "get", source, sessionId } → one or more { type: "chunk", sessionId, seq, total, data }
|
|
@@ -53,14 +59,17 @@ import { chmod, mkdir, readdir, readFile, rename, stat, writeFile } from "node:f
|
|
|
53
59
|
import { homedir } from "node:os";
|
|
54
60
|
import { join } from "node:path";
|
|
55
61
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// extension treats as "this host is too old, there is nothing to fetch" — so
|
|
59
|
-
// bumping this is informational, not a gate. Do not make it a gate: hosts are
|
|
60
|
-
// installed out of band and a version check would strand every user who
|
|
62
|
+
// 4 adds ZCode (see the protocol comment above). Do not make it a gate: hosts
|
|
63
|
+
// are installed out of band and a version check would strand every user who
|
|
61
64
|
// upgraded the extension first.
|
|
62
|
-
const PROTOCOL_VERSION =
|
|
65
|
+
const PROTOCOL_VERSION = 4;
|
|
63
66
|
const PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
67
|
+
// ZCode keeps every session in one SQLite database (WAL mode) — the first
|
|
68
|
+
// source with no per-session file. The host opens it READ-ONLY and materializes
|
|
69
|
+
// a session into the JSONL shape extension/src/background/zcode_import.ts
|
|
70
|
+
// parses. node:sqlite is imported lazily so a Node without it (older than
|
|
71
|
+
// 22.13) keeps serving every other source: ZCode just lists zero sessions.
|
|
72
|
+
const ZCODE_DB = join(homedir(), ".zcode", "cli", "db", "db.sqlite");
|
|
64
73
|
const CONFIG_FILE = join(homedir(), ".llmnesia", "config.json");
|
|
65
74
|
// Codex's home, resolved at install time (--codex-home > CODEX_HOME > ~/.codex)
|
|
66
75
|
// and persisted into config.json. The env var is read here too so a host
|
|
@@ -235,6 +244,192 @@ async function listCodexSessions(codexHome) {
|
|
|
235
244
|
return sessions;
|
|
236
245
|
}
|
|
237
246
|
|
|
247
|
+
// --- ZCode (SQLite source) -------------------------------------------------
|
|
248
|
+
|
|
249
|
+
let sqliteModule; // undefined = not yet imported, null = unavailable on this Node
|
|
250
|
+
async function getSqlite() {
|
|
251
|
+
if (sqliteModule === undefined) {
|
|
252
|
+
try {
|
|
253
|
+
sqliteModule = await import("node:sqlite");
|
|
254
|
+
} catch {
|
|
255
|
+
sqliteModule = null; // Node without node:sqlite (< 22.13): degrade quietly
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return sqliteModule;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function openZcodeDb() {
|
|
262
|
+
const sqlite = await getSqlite();
|
|
263
|
+
if (!sqlite) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
// readOnly: the database is ZCode's live WAL database, often being written
|
|
268
|
+
// as we read it. Opening read-only fails cleanly when the file is absent.
|
|
269
|
+
return new sqlite.DatabaseSync(ZCODE_DB, { readOnly: true });
|
|
270
|
+
} catch {
|
|
271
|
+
return null; // no ZCode install, or the db is momentarily unreadable
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Interactive ZCode sessions. Subagent runs (`task_type = 'subagent_child'`)
|
|
277
|
+
* are the harness's internal workers, not chats the user searched for, so they
|
|
278
|
+
* stay out of the listing. `title_source = 'default'` marks a placeholder
|
|
279
|
+
* title ("New Session"); only real titles are attached, so the extension's
|
|
280
|
+
* derived "<project> · <first prompt>" still wins when ZCode has no name.
|
|
281
|
+
*/
|
|
282
|
+
async function listZcodeSessions() {
|
|
283
|
+
const db = await openZcodeDb();
|
|
284
|
+
if (!db) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const rows = db
|
|
289
|
+
.prepare(
|
|
290
|
+
`SELECT id, title, title_source, time_updated, task_type
|
|
291
|
+
FROM session
|
|
292
|
+
WHERE task_type = 'interactive' OR task_type IS NULL`
|
|
293
|
+
)
|
|
294
|
+
.all();
|
|
295
|
+
const sessions = [];
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
const updated = Math.floor(Number(row.time_updated));
|
|
298
|
+
if (!Number.isFinite(updated) || updated <= 0) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const session = {
|
|
302
|
+
source: "zcode",
|
|
303
|
+
sessionId: String(row.id),
|
|
304
|
+
mtime: updated,
|
|
305
|
+
path: ZCODE_DB
|
|
306
|
+
};
|
|
307
|
+
if (typeof row.title === "string" && row.title && row.title_source !== "default") {
|
|
308
|
+
session.title = row.title;
|
|
309
|
+
}
|
|
310
|
+
sessions.push(session);
|
|
311
|
+
}
|
|
312
|
+
return sessions;
|
|
313
|
+
} catch {
|
|
314
|
+
return []; // unreadable or mid-migration database — ZCode contributes nothing
|
|
315
|
+
} finally {
|
|
316
|
+
db.close();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Build one ZCode session's transcript as JSONL for the extension parser: a
|
|
322
|
+
* `session` header line naming the project directory, then one `message` line
|
|
323
|
+
* per user/assistant prose turn. Only text parts survive; parts flagged
|
|
324
|
+
* `synthetic` (ZCode-injected bookkeeping such as `<task-notification>`) are
|
|
325
|
+
* dropped here and defensively re-checked in the parser. Multi-part messages
|
|
326
|
+
* are joined with a blank line, mirroring how the file-based parsers treat
|
|
327
|
+
* text blocks.
|
|
328
|
+
*
|
|
329
|
+
* Returns null when the session id is unknown (or node:sqlite is unavailable —
|
|
330
|
+
* a session that could not be listed cannot be fetched either).
|
|
331
|
+
*/
|
|
332
|
+
async function materializeZcodeSession(sessionId) {
|
|
333
|
+
const db = await openZcodeDb();
|
|
334
|
+
if (!db) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const sessionRow = db.prepare("SELECT directory FROM session WHERE id = ?").get(sessionId);
|
|
339
|
+
if (!sessionRow) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const lines = [];
|
|
343
|
+
const header = { type: "session" };
|
|
344
|
+
if (typeof sessionRow.directory === "string" && sessionRow.directory) {
|
|
345
|
+
header.directory = sessionRow.directory;
|
|
346
|
+
}
|
|
347
|
+
lines.push(JSON.stringify(header));
|
|
348
|
+
|
|
349
|
+
const rows = db
|
|
350
|
+
.prepare(
|
|
351
|
+
`SELECT m.id, m.time_created, m.data AS message_data, p.data AS part_data
|
|
352
|
+
FROM part p JOIN message m ON m.id = p.message_id
|
|
353
|
+
WHERE m.session_id = ?
|
|
354
|
+
ORDER BY m.sequence, p.sequence`
|
|
355
|
+
)
|
|
356
|
+
.all(sessionId);
|
|
357
|
+
|
|
358
|
+
// Rows arrive grouped by message (ORDER BY m.sequence, p.sequence), so a
|
|
359
|
+
// message's parts are contiguous: accumulate one outgoing line per group.
|
|
360
|
+
let currentId = null;
|
|
361
|
+
let current = null;
|
|
362
|
+
const flush = () => {
|
|
363
|
+
if (!current) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const text = current.texts.join("\n\n").trim();
|
|
367
|
+
if (text) {
|
|
368
|
+
const line = { type: "message", role: current.role, text };
|
|
369
|
+
if (current.timestamp !== undefined) {
|
|
370
|
+
line.timestamp = current.timestamp;
|
|
371
|
+
}
|
|
372
|
+
if (current.role === "assistant" && current.model) {
|
|
373
|
+
line.model = current.model;
|
|
374
|
+
}
|
|
375
|
+
lines.push(JSON.stringify(line));
|
|
376
|
+
}
|
|
377
|
+
current = null;
|
|
378
|
+
};
|
|
379
|
+
for (const row of rows) {
|
|
380
|
+
if (row.id !== currentId) {
|
|
381
|
+
flush();
|
|
382
|
+
currentId = row.id;
|
|
383
|
+
let meta = null;
|
|
384
|
+
try {
|
|
385
|
+
meta = JSON.parse(row.message_data);
|
|
386
|
+
} catch {
|
|
387
|
+
meta = null;
|
|
388
|
+
}
|
|
389
|
+
const role = meta && meta.role === "assistant" ? "assistant" : meta && meta.role === "user" ? "user" : null;
|
|
390
|
+
if (!role) {
|
|
391
|
+
current = null; // unknown shape — drop the whole message, keep the rest
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const dataTs = meta && meta.time && typeof meta.time.created === "number" ? meta.time.created : undefined;
|
|
395
|
+
current = {
|
|
396
|
+
role,
|
|
397
|
+
texts: [],
|
|
398
|
+
timestamp:
|
|
399
|
+
dataTs !== undefined
|
|
400
|
+
? dataTs
|
|
401
|
+
: typeof row.time_created === "number"
|
|
402
|
+
? row.time_created
|
|
403
|
+
: undefined,
|
|
404
|
+
model: role === "assistant" && typeof meta.modelID === "string" && meta.modelID ? meta.modelID : null
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (!current) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
let part = null;
|
|
411
|
+
try {
|
|
412
|
+
part = JSON.parse(row.part_data);
|
|
413
|
+
} catch {
|
|
414
|
+
part = null;
|
|
415
|
+
}
|
|
416
|
+
if (!part || part.type !== "text" || part.synthetic === true || typeof part.text !== "string") {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const text = part.text.trim();
|
|
420
|
+
if (text) {
|
|
421
|
+
current.texts.push(text);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
flush();
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
} finally {
|
|
429
|
+
db.close();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
238
433
|
/**
|
|
239
434
|
* Every session across every source. Sources are listed independently and
|
|
240
435
|
* failures are contained per source (each helper swallows its own errors), so
|
|
@@ -244,11 +439,12 @@ async function listCodexSessions(codexHome) {
|
|
|
244
439
|
async function listSessions() {
|
|
245
440
|
const config = await loadConfig();
|
|
246
441
|
const codexHome = config?.codexHome || DEFAULT_CODEX_HOME;
|
|
247
|
-
const [claude, codex] = await Promise.all([
|
|
442
|
+
const [claude, codex, zcode] = await Promise.all([
|
|
248
443
|
listClaudeCodeSessions().catch(() => []),
|
|
249
|
-
listCodexSessions(codexHome).catch(() => [])
|
|
444
|
+
listCodexSessions(codexHome).catch(() => []),
|
|
445
|
+
listZcodeSessions().catch(() => [])
|
|
250
446
|
]);
|
|
251
|
-
return [...claude, ...codex];
|
|
447
|
+
return [...claude, ...codex, ...zcode];
|
|
252
448
|
}
|
|
253
449
|
|
|
254
450
|
async function loadConfig() {
|
|
@@ -469,18 +665,31 @@ async function handle(message) {
|
|
|
469
665
|
return;
|
|
470
666
|
}
|
|
471
667
|
if (message.cmd === "get") {
|
|
472
|
-
const sessions = await listSessions();
|
|
473
668
|
// Session ids are only unique WITHIN a source, so a v2 caller names both.
|
|
474
669
|
// A v1 caller sends no source; everything it knows about is Claude Code, so
|
|
475
670
|
// that's what an absent source resolves to — never a bare id match that
|
|
476
671
|
// could hand back the wrong source's file.
|
|
477
672
|
const wantSource = typeof message.source === "string" ? message.source : "claude_code";
|
|
478
|
-
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
673
|
+
let text;
|
|
674
|
+
if (wantSource === "zcode") {
|
|
675
|
+
// No file to read: the transcript is materialized out of the database.
|
|
676
|
+
// The session id is only ever a bound query parameter, never a path
|
|
677
|
+
// component, so the trust boundary holds unchanged.
|
|
678
|
+
const materialized = await materializeZcodeSession(String(message.sessionId ?? ""));
|
|
679
|
+
if (materialized === null) {
|
|
680
|
+
send({ type: "session", sessionId: message.sessionId, notFound: true });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
text = materialized;
|
|
684
|
+
} else {
|
|
685
|
+
const sessions = await listSessions();
|
|
686
|
+
const match = sessions.find((s) => s.source === wantSource && s.sessionId === message.sessionId);
|
|
687
|
+
if (!match) {
|
|
688
|
+
send({ type: "session", sessionId: message.sessionId, notFound: true });
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
text = await readFile(match.path, "utf8");
|
|
482
692
|
}
|
|
483
|
-
const text = await readFile(match.path, "utf8");
|
|
484
693
|
const total = Math.max(1, Math.ceil(text.length / CHUNK_CHARS));
|
|
485
694
|
for (let i = 0, seq = 0; seq < total; i += CHUNK_CHARS, seq++) {
|
|
486
695
|
send({
|
package/dist/llmnesia.mcpb
CHANGED
|
Binary file
|
package/dist/mcp.js
CHANGED
|
@@ -6,19 +6,20 @@ import {
|
|
|
6
6
|
Corpus,
|
|
7
7
|
describeOutcome,
|
|
8
8
|
drainInbox,
|
|
9
|
+
fingerprintConversation,
|
|
9
10
|
openSearchIndex,
|
|
10
11
|
pendingInboxFiles,
|
|
11
12
|
readNativeHostViewerExtensionId,
|
|
12
13
|
resolveCorpusDir,
|
|
13
14
|
runAutomaticMaintenance,
|
|
14
15
|
scheduleSelfUpdate
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-BWEDXPNM.js";
|
|
16
17
|
import {
|
|
17
18
|
VERSION,
|
|
18
19
|
__commonJS,
|
|
19
20
|
__export,
|
|
20
21
|
__toESM
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-YT6GNKKG.js";
|
|
22
23
|
|
|
23
24
|
// ../node_modules/ajv/dist/compile/codegen/code.js
|
|
24
25
|
var require_code = __commonJS({
|
|
@@ -21141,6 +21142,109 @@ import { realpathSync } from "fs";
|
|
|
21141
21142
|
import { fileURLToPath } from "url";
|
|
21142
21143
|
import path from "path";
|
|
21143
21144
|
|
|
21145
|
+
// src/inventory.ts
|
|
21146
|
+
import { randomBytes } from "crypto";
|
|
21147
|
+
var InventoryCursorError = class extends Error {
|
|
21148
|
+
};
|
|
21149
|
+
var DEFAULT_INVENTORY_TTL_MS = 15 * 6e4;
|
|
21150
|
+
var MAX_INVENTORY_SNAPSHOTS = 8;
|
|
21151
|
+
var CURSOR_PREFIX = "inv1.";
|
|
21152
|
+
var DEFAULT_PAGE_LIMIT = 100;
|
|
21153
|
+
var MAX_PAGE_LIMIT = 100;
|
|
21154
|
+
var CURSOR_ERROR_MESSAGE = "The inventory cursor cannot be served: it is malformed, it has expired, it was evicted by newer scans, or the MCP server that created it was restarted. Begin a new scan by calling conversation_inventory again without a cursor.";
|
|
21155
|
+
var InventoryScanner = class {
|
|
21156
|
+
constructor(loadRows, options = {}) {
|
|
21157
|
+
this.loadRows = loadRows;
|
|
21158
|
+
this.options = options;
|
|
21159
|
+
}
|
|
21160
|
+
loadRows;
|
|
21161
|
+
options;
|
|
21162
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
21163
|
+
page(cursor, limit) {
|
|
21164
|
+
const pageSize = Math.min(MAX_PAGE_LIMIT, Math.max(1, limit ?? DEFAULT_PAGE_LIMIT));
|
|
21165
|
+
if (cursor === void 0 || cursor === "") {
|
|
21166
|
+
return this.startScan(pageSize);
|
|
21167
|
+
}
|
|
21168
|
+
const { snapshotId, offset } = decodeCursor(cursor);
|
|
21169
|
+
const snapshot = this.snapshots.get(snapshotId);
|
|
21170
|
+
if (!snapshot || Date.now() - snapshot.createdAt > this.ttlMs()) {
|
|
21171
|
+
this.snapshots.delete(snapshotId);
|
|
21172
|
+
throw new InventoryCursorError(CURSOR_ERROR_MESSAGE);
|
|
21173
|
+
}
|
|
21174
|
+
return this.slice(snapshot, offset, pageSize);
|
|
21175
|
+
}
|
|
21176
|
+
/** Drop all snapshots (called when the owning service closes). */
|
|
21177
|
+
clear() {
|
|
21178
|
+
this.snapshots.clear();
|
|
21179
|
+
}
|
|
21180
|
+
startScan(pageSize) {
|
|
21181
|
+
const rows = this.loadRows();
|
|
21182
|
+
const now = Date.now();
|
|
21183
|
+
if (rows.length === 0) {
|
|
21184
|
+
return { items: [], total: 0, scanStartedAt: now, nextCursor: null };
|
|
21185
|
+
}
|
|
21186
|
+
const snapshot = {
|
|
21187
|
+
id: randomBytes(9).toString("base64url"),
|
|
21188
|
+
createdAt: now,
|
|
21189
|
+
rows
|
|
21190
|
+
};
|
|
21191
|
+
this.snapshots.set(snapshot.id, snapshot);
|
|
21192
|
+
this.evict(now);
|
|
21193
|
+
return this.slice(snapshot, 0, pageSize);
|
|
21194
|
+
}
|
|
21195
|
+
slice(snapshot, offset, pageSize) {
|
|
21196
|
+
const items = snapshot.rows.slice(offset, offset + pageSize);
|
|
21197
|
+
const end = offset + items.length;
|
|
21198
|
+
return {
|
|
21199
|
+
items,
|
|
21200
|
+
total: snapshot.rows.length,
|
|
21201
|
+
scanStartedAt: snapshot.createdAt,
|
|
21202
|
+
nextCursor: end < snapshot.rows.length ? encodeCursor(snapshot.id, end) : null
|
|
21203
|
+
};
|
|
21204
|
+
}
|
|
21205
|
+
ttlMs() {
|
|
21206
|
+
return this.options.ttlMs ?? DEFAULT_INVENTORY_TTL_MS;
|
|
21207
|
+
}
|
|
21208
|
+
evict(now) {
|
|
21209
|
+
const max = Math.max(1, this.options.maxSnapshots ?? MAX_INVENTORY_SNAPSHOTS);
|
|
21210
|
+
for (const [id, snapshot] of this.snapshots) {
|
|
21211
|
+
if (Date.now() - snapshot.createdAt > this.ttlMs()) {
|
|
21212
|
+
this.snapshots.delete(id);
|
|
21213
|
+
}
|
|
21214
|
+
}
|
|
21215
|
+
while (this.snapshots.size > max) {
|
|
21216
|
+
let oldestId;
|
|
21217
|
+
let oldestAt = now + 1;
|
|
21218
|
+
for (const [id, snapshot] of this.snapshots) {
|
|
21219
|
+
if (snapshot.createdAt < oldestAt) {
|
|
21220
|
+
oldestAt = snapshot.createdAt;
|
|
21221
|
+
oldestId = id;
|
|
21222
|
+
}
|
|
21223
|
+
}
|
|
21224
|
+
if (oldestId === void 0) break;
|
|
21225
|
+
this.snapshots.delete(oldestId);
|
|
21226
|
+
}
|
|
21227
|
+
}
|
|
21228
|
+
};
|
|
21229
|
+
function encodeCursor(snapshotId, offset) {
|
|
21230
|
+
const payload = Buffer.from(JSON.stringify({ s: snapshotId, o: offset }), "utf8");
|
|
21231
|
+
return `${CURSOR_PREFIX}${payload.toString("base64url")}`;
|
|
21232
|
+
}
|
|
21233
|
+
function decodeCursor(cursor) {
|
|
21234
|
+
if (cursor.startsWith(CURSOR_PREFIX)) {
|
|
21235
|
+
try {
|
|
21236
|
+
const parsed = JSON.parse(
|
|
21237
|
+
Buffer.from(cursor.slice(CURSOR_PREFIX.length), "base64url").toString("utf8")
|
|
21238
|
+
);
|
|
21239
|
+
if (typeof parsed?.s === "string" && parsed.s.length > 0 && typeof parsed?.o === "number" && Number.isInteger(parsed.o) && parsed.o >= 0) {
|
|
21240
|
+
return { snapshotId: parsed.s, offset: parsed.o };
|
|
21241
|
+
}
|
|
21242
|
+
} catch {
|
|
21243
|
+
}
|
|
21244
|
+
}
|
|
21245
|
+
throw new InventoryCursorError(CURSOR_ERROR_MESSAGE);
|
|
21246
|
+
}
|
|
21247
|
+
|
|
21144
21248
|
// src/saveSchema.ts
|
|
21145
21249
|
import { createHash } from "crypto";
|
|
21146
21250
|
var saveMessageSchema = external_exports.object({
|
|
@@ -21413,6 +21517,8 @@ var CompanionService = class _CompanionService {
|
|
|
21413
21517
|
// Serialize drains so concurrent reads queue behind one ingest rather than
|
|
21414
21518
|
// racing to consume the same file (mirrors Corpus.enqueueWrite).
|
|
21415
21519
|
refreshChain = Promise.resolve();
|
|
21520
|
+
/** Holds the bounded, in-memory snapshots behind inventoryPage. */
|
|
21521
|
+
inventoryScanner = new InventoryScanner(() => this.index.inventoryRows());
|
|
21416
21522
|
/** Memories written since this process started; the per-session cap counts these. */
|
|
21417
21523
|
memoriesWritten = 0;
|
|
21418
21524
|
activeOperations = 0;
|
|
@@ -21530,6 +21636,23 @@ var CompanionService = class _CompanionService {
|
|
|
21530
21636
|
getConversation(docId) {
|
|
21531
21637
|
return this.corpus.get(docId);
|
|
21532
21638
|
}
|
|
21639
|
+
/**
|
|
21640
|
+
* One page of the paginated conversation inventory (metadata only, never
|
|
21641
|
+
* transcripts). Passing no cursor starts a new scan: the inbox is drained so
|
|
21642
|
+
* the snapshot reflects the corpus as of now, then a consistent docId-sorted
|
|
21643
|
+
* snapshot is captured and held in memory. Passing a previous page's
|
|
21644
|
+
* nextCursor serves later pages from that same snapshot, so conversations
|
|
21645
|
+
* that arrive or change mid-scan cannot shuffle or rewrite earlier pages;
|
|
21646
|
+
* resume pages deliberately skip the drain, which would only cost latency.
|
|
21647
|
+
* Throws InventoryCursorError for a cursor that has expired or outlived the
|
|
21648
|
+
* process; the caller should begin a new scan.
|
|
21649
|
+
*/
|
|
21650
|
+
async inventoryPage(options = {}) {
|
|
21651
|
+
if (!options.cursor) {
|
|
21652
|
+
await this.refresh();
|
|
21653
|
+
}
|
|
21654
|
+
return this.inventoryScanner.page(options.cursor, options.limit);
|
|
21655
|
+
}
|
|
21533
21656
|
/** Recent conversations enriched with their cached summary when available. */
|
|
21534
21657
|
async listRecent(n) {
|
|
21535
21658
|
return this.listConversations(n, "recent");
|
|
@@ -21623,6 +21746,7 @@ var CompanionService = class _CompanionService {
|
|
|
21623
21746
|
this.closed = true;
|
|
21624
21747
|
if (this.maintenanceTimer) clearTimeout(this.maintenanceTimer);
|
|
21625
21748
|
this.maintenanceTimer = void 0;
|
|
21749
|
+
this.inventoryScanner.clear();
|
|
21626
21750
|
this.index.close();
|
|
21627
21751
|
}
|
|
21628
21752
|
};
|
|
@@ -21847,6 +21971,12 @@ async function runServe() {
|
|
|
21847
21971
|
return errorResult(`No conversation found with id '${args.id}'.`);
|
|
21848
21972
|
}
|
|
21849
21973
|
return sourcedJsonResult({
|
|
21974
|
+
// The current fingerprint of the transcript being returned, matching
|
|
21975
|
+
// the `fingerprint` a conversation_inventory entry carries for the
|
|
21976
|
+
// same content. A caller diffing against an inventory snapshot taken
|
|
21977
|
+
// earlier can detect a change made after that snapshot by comparing
|
|
21978
|
+
// the two values.
|
|
21979
|
+
contentFingerprint: fingerprintConversation(record2),
|
|
21850
21980
|
conversation: {
|
|
21851
21981
|
...record2.conversation,
|
|
21852
21982
|
...sourceLinks(record2.conversation.docId, record2.conversation.url)
|
|
@@ -21946,6 +22076,43 @@ async function runServe() {
|
|
|
21946
22076
|
});
|
|
21947
22077
|
})
|
|
21948
22078
|
);
|
|
22079
|
+
server.registerTool(
|
|
22080
|
+
"conversation_inventory",
|
|
22081
|
+
{
|
|
22082
|
+
title: "Conversation inventory",
|
|
22083
|
+
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.",
|
|
22084
|
+
inputSchema: {
|
|
22085
|
+
cursor: external_exports.string().optional().describe("Opaque nextCursor from the previous page. Omit to start a new scan."),
|
|
22086
|
+
limit: external_exports.number().int().min(1).max(100).optional().describe("Items per page (default 100).")
|
|
22087
|
+
},
|
|
22088
|
+
annotations: {
|
|
22089
|
+
readOnlyHint: true,
|
|
22090
|
+
destructiveHint: false,
|
|
22091
|
+
idempotentHint: true,
|
|
22092
|
+
openWorldHint: false
|
|
22093
|
+
}
|
|
22094
|
+
},
|
|
22095
|
+
async (args) => withService(async (service2) => {
|
|
22096
|
+
try {
|
|
22097
|
+
const page = await service2.inventoryPage({ cursor: args.cursor, limit: args.limit });
|
|
22098
|
+
const payload = {
|
|
22099
|
+
count: page.items.length,
|
|
22100
|
+
total: page.total,
|
|
22101
|
+
scanStartedAt: page.scanStartedAt,
|
|
22102
|
+
items: page.items
|
|
22103
|
+
};
|
|
22104
|
+
if (page.nextCursor) {
|
|
22105
|
+
payload.nextCursor = page.nextCursor;
|
|
22106
|
+
}
|
|
22107
|
+
return jsonResult(payload);
|
|
22108
|
+
} catch (err) {
|
|
22109
|
+
if (err instanceof InventoryCursorError) {
|
|
22110
|
+
return errorResult(err.message);
|
|
22111
|
+
}
|
|
22112
|
+
throw err;
|
|
22113
|
+
}
|
|
22114
|
+
})
|
|
22115
|
+
);
|
|
21949
22116
|
server.registerTool(
|
|
21950
22117
|
"corpus_status",
|
|
21951
22118
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmnesia/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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.9",
|
|
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.9",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|