@llmnesia/mcp 0.2.6 → 0.2.8
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 +13 -5
- package/dist/{chunk-HL5QE57W.js → chunk-FIH7ZNYH.js} +44 -12
- package/dist/{chunk-YRK2DRGG.js → chunk-RJJRIPHA.js} +1 -1
- package/dist/{cli-DRB4DVH3.js → cli-D2IVH5S2.js} +68 -15
- package/dist/cli.js +2 -2
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +23 -12
- 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
|
|
@@ -205,6 +212,7 @@ received data — check it before concluding a chat does not exist.
|
|
|
205
212
|
| 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
213
|
| Cursor | Edits `~/.cursor/mcp.json`, preparing it in advance if Cursor is installed later. |
|
|
207
214
|
| 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. |
|
|
215
|
+
| 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
216
|
| Others (Windsurf, Zed, LM Studio, …) | Prints the generic stdio block for you to paste. |
|
|
209
217
|
|
|
210
218
|
**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-RJJRIPHA.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 = 4;
|
|
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,8 @@ 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.models && c.models.length > 0) out.models = [...c.models];
|
|
214
|
+
assignIfDefined(out, "modelMetadataUpdatedAt", c.modelMetadataUpdatedAt);
|
|
211
215
|
if (c.summary) {
|
|
212
216
|
out.summary = {
|
|
213
217
|
text: c.summary.text,
|
|
@@ -227,6 +231,7 @@ function canonicalMessageDoc(m) {
|
|
|
227
231
|
text: m.text
|
|
228
232
|
};
|
|
229
233
|
assignIfDefined(out, "formattedText", m.formattedText);
|
|
234
|
+
assignIfDefined(out, "model", m.model);
|
|
230
235
|
out.createdAt = m.createdAt;
|
|
231
236
|
out.updatedAt = m.updatedAt;
|
|
232
237
|
out.contentHash = m.contentHash;
|
|
@@ -904,6 +909,9 @@ function coerceMessage(docId, value) {
|
|
|
904
909
|
if (typeof m.formattedText === "string") {
|
|
905
910
|
message.formattedText = m.formattedText;
|
|
906
911
|
}
|
|
912
|
+
if (typeof m.model === "string" && m.model.trim()) {
|
|
913
|
+
message.model = m.model.trim();
|
|
914
|
+
}
|
|
907
915
|
if (typeof m.platformMessageId === "string") {
|
|
908
916
|
message.platformMessageId = m.platformMessageId;
|
|
909
917
|
} else if (m.platformMessageId === null) {
|
|
@@ -1008,7 +1016,8 @@ CREATE TABLE IF NOT EXISTS conversations (
|
|
|
1008
1016
|
preview TEXT NOT NULL,
|
|
1009
1017
|
pinned INTEGER NOT NULL,
|
|
1010
1018
|
accountId TEXT,
|
|
1011
|
-
accountLabel TEXT
|
|
1019
|
+
accountLabel TEXT,
|
|
1020
|
+
models TEXT
|
|
1012
1021
|
);
|
|
1013
1022
|
CREATE INDEX IF NOT EXISTS idx_conversations_platform ON conversations(platform);
|
|
1014
1023
|
CREATE INDEX IF NOT EXISTS idx_conversations_createdAt ON conversations(createdAt);
|
|
@@ -1082,8 +1091,8 @@ var SearchIndex = class {
|
|
|
1082
1091
|
deleteConversation: this.db.prepare("DELETE FROM conversations WHERE docId = ?"),
|
|
1083
1092
|
insertConversation: this.db.prepare(
|
|
1084
1093
|
`INSERT INTO conversations
|
|
1085
|
-
(docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned, accountId, accountLabel)
|
|
1086
|
-
VALUES (@docId, @platform, @title, @url, @createdAt, @updatedAt, @messageCount, @preview, @pinned, @accountId, @accountLabel)`
|
|
1094
|
+
(docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned, accountId, accountLabel, models)
|
|
1095
|
+
VALUES (@docId, @platform, @title, @url, @createdAt, @updatedAt, @messageCount, @preview, @pinned, @accountId, @accountLabel, @models)`
|
|
1087
1096
|
),
|
|
1088
1097
|
deleteMemoryFts: this.db.prepare("DELETE FROM memories_fts WHERE memoryId = ?"),
|
|
1089
1098
|
insertMemoryFts: this.db.prepare(
|
|
@@ -1127,14 +1136,16 @@ var SearchIndex = class {
|
|
|
1127
1136
|
const orderBy = sort === "oldest" ? "createdAt ASC, updatedAt ASC" : sort === "newest" ? "createdAt DESC, updatedAt DESC" : "updatedAt DESC, createdAt DESC";
|
|
1128
1137
|
const rows = this.db.prepare(
|
|
1129
1138
|
`SELECT docId, platform, title, url, createdAt, updatedAt, messageCount, preview, pinned,
|
|
1130
|
-
accountId, accountLabel
|
|
1139
|
+
accountId, accountLabel, models
|
|
1131
1140
|
FROM conversations ORDER BY ${orderBy} LIMIT ?`
|
|
1132
1141
|
).all(limit);
|
|
1133
1142
|
return rows.map((r) => {
|
|
1134
|
-
const { accountId, accountLabel, ...rest } = r;
|
|
1143
|
+
const { accountId, accountLabel, models, ...rest } = r;
|
|
1135
1144
|
const entry = { ...rest, pinned: r.pinned === 1 };
|
|
1136
1145
|
if (accountId) entry.accountId = accountId;
|
|
1137
1146
|
if (accountLabel) entry.accountLabel = accountLabel;
|
|
1147
|
+
const parsedModels = parseModels(models);
|
|
1148
|
+
if (parsedModels.length > 0) entry.models = parsedModels;
|
|
1138
1149
|
return entry;
|
|
1139
1150
|
});
|
|
1140
1151
|
}
|
|
@@ -1199,9 +1210,14 @@ var SearchIndex = class {
|
|
|
1199
1210
|
// transaction() helper for single upserts, or rebuildFrom's manual one).
|
|
1200
1211
|
writeRecord(rec) {
|
|
1201
1212
|
const { conversation, messages } = rec;
|
|
1213
|
+
const displayTitle = conversation.localTitle?.trim() || conversation.title;
|
|
1202
1214
|
this.deleteFtsRows(conversation.docId);
|
|
1203
1215
|
this.stmts.deleteConversation.run(conversation.docId);
|
|
1204
|
-
const titleText = [
|
|
1216
|
+
const titleText = [
|
|
1217
|
+
...conversation.localTitle ? [conversation.localTitle, conversation.localTitle] : [],
|
|
1218
|
+
conversation.title,
|
|
1219
|
+
conversation.preview
|
|
1220
|
+
].filter(Boolean).join("\n");
|
|
1205
1221
|
if (titleText.trim()) {
|
|
1206
1222
|
this.insertFtsRow(conversation.docId, TITLE_ROW_INDEX, TITLE_ROLE, titleText);
|
|
1207
1223
|
}
|
|
@@ -1213,7 +1229,7 @@ var SearchIndex = class {
|
|
|
1213
1229
|
this.stmts.insertConversation.run({
|
|
1214
1230
|
docId: conversation.docId,
|
|
1215
1231
|
platform: conversation.platform,
|
|
1216
|
-
title:
|
|
1232
|
+
title: displayTitle,
|
|
1217
1233
|
url: conversation.url,
|
|
1218
1234
|
createdAt: conversation.createdAt,
|
|
1219
1235
|
updatedAt: conversation.updatedAt ?? conversation.contentUpdatedAt ?? 0,
|
|
@@ -1221,7 +1237,8 @@ var SearchIndex = class {
|
|
|
1221
1237
|
preview: conversation.preview ?? "",
|
|
1222
1238
|
pinned: conversation.pinned ? 1 : 0,
|
|
1223
1239
|
accountId: conversation.accountId ?? null,
|
|
1224
|
-
accountLabel: conversation.accountLabel ?? null
|
|
1240
|
+
accountLabel: conversation.accountLabel ?? null,
|
|
1241
|
+
models: conversation.models?.length ? JSON.stringify(conversation.models) : null
|
|
1225
1242
|
});
|
|
1226
1243
|
}
|
|
1227
1244
|
insertFtsRow(docId, msgIndex, role, text) {
|
|
@@ -1286,7 +1303,7 @@ var SearchIndex = class {
|
|
|
1286
1303
|
const sql = `
|
|
1287
1304
|
SELECT
|
|
1288
1305
|
c.docId, c.platform, c.title, c.url, c.createdAt, c.updatedAt,
|
|
1289
|
-
c.messageCount, c.preview, c.pinned, c.accountId, c.accountLabel,
|
|
1306
|
+
c.messageCount, c.preview, c.pinned, c.accountId, c.accountLabel, c.models,
|
|
1290
1307
|
m.msgIndex AS msgIndex, m.role AS role,
|
|
1291
1308
|
bm25(messages_fts) AS bm25
|
|
1292
1309
|
FROM messages_fts
|
|
@@ -1322,6 +1339,8 @@ var SearchIndex = class {
|
|
|
1322
1339
|
};
|
|
1323
1340
|
if (row.accountId) hit.accountId = row.accountId;
|
|
1324
1341
|
if (row.accountLabel) hit.accountLabel = row.accountLabel;
|
|
1342
|
+
const models = parseModels(row.models);
|
|
1343
|
+
if (models.length > 0) hit.models = models;
|
|
1325
1344
|
hits.push(hit);
|
|
1326
1345
|
if (hits.length >= limit) {
|
|
1327
1346
|
break;
|
|
@@ -1338,7 +1357,11 @@ var SearchIndex = class {
|
|
|
1338
1357
|
}
|
|
1339
1358
|
hydrateSnippet(row, match) {
|
|
1340
1359
|
const record = this.corpus?.getFromPlatformSync(row.platform, row.docId);
|
|
1341
|
-
const source = row.msgIndex === TITLE_ROW_INDEX ? [
|
|
1360
|
+
const source = row.msgIndex === TITLE_ROW_INDEX ? [
|
|
1361
|
+
record?.conversation.localTitle,
|
|
1362
|
+
record?.conversation.title ?? row.title,
|
|
1363
|
+
record?.conversation.preview ?? row.preview
|
|
1364
|
+
].filter(Boolean).join("\n") : record?.messages.find((message) => message.msgIndex === row.msgIndex)?.text ?? row.preview;
|
|
1342
1365
|
this.stmts.clearSnippetFts.run();
|
|
1343
1366
|
try {
|
|
1344
1367
|
this.stmts.insertSnippetFts.run(source);
|
|
@@ -1402,6 +1425,15 @@ function normalizeMatchRole(role) {
|
|
|
1402
1425
|
}
|
|
1403
1426
|
return role === "assistant" || role === "user" || role === "system" ? role : "unknown";
|
|
1404
1427
|
}
|
|
1428
|
+
function parseModels(value) {
|
|
1429
|
+
if (!value) return [];
|
|
1430
|
+
try {
|
|
1431
|
+
const parsed = JSON.parse(value);
|
|
1432
|
+
return Array.isArray(parsed) ? parsed.filter((model) => typeof model === "string" && model.trim().length > 0) : [];
|
|
1433
|
+
} catch {
|
|
1434
|
+
return [];
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1405
1437
|
|
|
1406
1438
|
// src/storageMaintenance.ts
|
|
1407
1439
|
import { promises as fs4 } from "fs";
|
|
@@ -17,13 +17,13 @@ import {
|
|
|
17
17
|
runtimeBinDir,
|
|
18
18
|
staleIndexFiles,
|
|
19
19
|
uninstallRuntime
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-FIH7ZNYH.js";
|
|
21
21
|
import {
|
|
22
22
|
VERSION,
|
|
23
23
|
__commonJS,
|
|
24
24
|
__require,
|
|
25
25
|
__toESM
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-RJJRIPHA.js";
|
|
27
27
|
|
|
28
28
|
// ../node_modules/isexe/windows.js
|
|
29
29
|
var require_windows = __commonJS({
|
|
@@ -2841,7 +2841,8 @@ function paths(home2, osPlatform2, appData) {
|
|
|
2841
2841
|
claudeCode: join4(claudeCodeRoot, ".claude.json"),
|
|
2842
2842
|
claudeDesktop,
|
|
2843
2843
|
cursor: join4(home2, ".cursor", "mcp.json"),
|
|
2844
|
-
codex: join4(home2, ".codex", "config.toml")
|
|
2844
|
+
codex: join4(home2, ".codex", "config.toml"),
|
|
2845
|
+
zcode: join4(home2, ".zcode", "cli", "config.json")
|
|
2845
2846
|
};
|
|
2846
2847
|
}
|
|
2847
2848
|
async function readJsonFile(path) {
|
|
@@ -2873,6 +2874,38 @@ function mcpServersOf(data, configPath) {
|
|
|
2873
2874
|
}
|
|
2874
2875
|
return data.mcpServers;
|
|
2875
2876
|
}
|
|
2877
|
+
function nestedMcpServersOf(data, configPath) {
|
|
2878
|
+
if (data.mcp === void 0) {
|
|
2879
|
+
return {};
|
|
2880
|
+
}
|
|
2881
|
+
if (!data.mcp || typeof data.mcp !== "object" || Array.isArray(data.mcp)) {
|
|
2882
|
+
throw new Error(`Could not update ${configPath}: "mcp" must be a JSON object. The file was left unchanged.`);
|
|
2883
|
+
}
|
|
2884
|
+
const mcp = data.mcp;
|
|
2885
|
+
if (mcp.servers === void 0) {
|
|
2886
|
+
return {};
|
|
2887
|
+
}
|
|
2888
|
+
if (!mcp.servers || typeof mcp.servers !== "object" || Array.isArray(mcp.servers)) {
|
|
2889
|
+
throw new Error(
|
|
2890
|
+
`Could not update ${configPath}: "mcp.servers" must be a JSON object. The file was left unchanged.`
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
return mcp.servers;
|
|
2894
|
+
}
|
|
2895
|
+
function asObject(value) {
|
|
2896
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2897
|
+
}
|
|
2898
|
+
var topLevelServers = {
|
|
2899
|
+
read: (data, configPath) => mcpServersOf(data, configPath),
|
|
2900
|
+
merge: (data, servers) => ({ ...data, mcpServers: servers }),
|
|
2901
|
+
preview: (entry) => JSON.stringify({ mcpServers: { [SERVER_NAME]: entry } }, null, 2)
|
|
2902
|
+
};
|
|
2903
|
+
var zcodeNestedServers = {
|
|
2904
|
+
read: (data, configPath) => nestedMcpServersOf(data, configPath),
|
|
2905
|
+
// Keep any other settings ZCode stores beside `servers` inside its `mcp` key.
|
|
2906
|
+
merge: (data, servers) => ({ ...data, mcp: { ...asObject(data.mcp), servers } }),
|
|
2907
|
+
preview: (entry) => JSON.stringify({ mcp: { servers: { [SERVER_NAME]: entry } } }, null, 2)
|
|
2908
|
+
};
|
|
2876
2909
|
function isErrnoException(value) {
|
|
2877
2910
|
return value instanceof Error && "code" in value;
|
|
2878
2911
|
}
|
|
@@ -3091,7 +3124,7 @@ codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
|
|
|
3091
3124
|
detail: `Ran: codex mcp add ${SERVER_NAME} -- ${serve.command} ${serve.args.join(" ")}`
|
|
3092
3125
|
};
|
|
3093
3126
|
}
|
|
3094
|
-
async function registerJsonClient(clientLabel, configPath, serve, confirm, now, prepareMissing = false, prepareWhenConfigAbsent = false, includeTransportType = true) {
|
|
3127
|
+
async function registerJsonClient(clientLabel, configPath, serve, confirm, now, prepareMissing = false, prepareWhenConfigAbsent = false, includeTransportType = true, layout = topLevelServers) {
|
|
3095
3128
|
const configDirExists = await clientDirExists(configPath);
|
|
3096
3129
|
if (!configDirExists && !prepareMissing) {
|
|
3097
3130
|
return {
|
|
@@ -3117,7 +3150,7 @@ async function registerJsonClient(clientLabel, configPath, serve, confirm, now,
|
|
|
3117
3150
|
}
|
|
3118
3151
|
let servers;
|
|
3119
3152
|
try {
|
|
3120
|
-
servers =
|
|
3153
|
+
servers = layout.read(data, configPath);
|
|
3121
3154
|
} catch (err) {
|
|
3122
3155
|
return { client: clientLabel, action: "error", detail: errorMessage(err) };
|
|
3123
3156
|
}
|
|
@@ -3126,7 +3159,7 @@ async function registerJsonClient(clientLabel, configPath, serve, confirm, now,
|
|
|
3126
3159
|
if (already && JSON.stringify(already) === JSON.stringify(nextEntry)) {
|
|
3127
3160
|
return { client: clientLabel, action: "skipped", detail: `Already registered at ${configPath}.` };
|
|
3128
3161
|
}
|
|
3129
|
-
const preview =
|
|
3162
|
+
const preview = layout.preview(nextEntry);
|
|
3130
3163
|
const message = existed ? `${clientLabel}: update ${configPath} to add the "${SERVER_NAME}" MCP server? Existing servers and settings are preserved.
|
|
3131
3164
|
Add:
|
|
3132
3165
|
${preview}` : `${clientLabel}: create ${configPath} with the "${SERVER_NAME}" MCP server?
|
|
@@ -3140,7 +3173,7 @@ ${preview}`;
|
|
|
3140
3173
|
const backupPath = `${configPath}.bak-${now()}`;
|
|
3141
3174
|
await writeFile2(backupPath, raw ?? JSON.stringify(data, null, 2), "utf8");
|
|
3142
3175
|
}
|
|
3143
|
-
const nextData =
|
|
3176
|
+
const nextData = layout.merge(data, { ...servers, [SERVER_NAME]: nextEntry });
|
|
3144
3177
|
await writeFile2(configPath, `${JSON.stringify(nextData, null, 2)}
|
|
3145
3178
|
`, "utf8");
|
|
3146
3179
|
return {
|
|
@@ -3245,7 +3278,7 @@ async function registerClients(options) {
|
|
|
3245
3278
|
const osPlatform2 = options.osPlatform ?? platform2();
|
|
3246
3279
|
const now = options.now ?? (() => Date.now());
|
|
3247
3280
|
const prepareMissing = options.prepareMissingDesktopApps === true;
|
|
3248
|
-
const { claudeCode, claudeDesktop, cursor, codex } = paths(home2, osPlatform2, options.appData);
|
|
3281
|
+
const { claudeCode, claudeDesktop, cursor, codex, zcode } = paths(home2, osPlatform2, options.appData);
|
|
3249
3282
|
const results = [];
|
|
3250
3283
|
const safe = async (client, fn) => {
|
|
3251
3284
|
try {
|
|
@@ -3292,6 +3325,22 @@ async function registerClients(options) {
|
|
|
3292
3325
|
() => registerCodex(options.serve, options.codexCli ?? realCodexCli, codex, options.confirm, now, prepareMissing)
|
|
3293
3326
|
)
|
|
3294
3327
|
);
|
|
3328
|
+
results.push(
|
|
3329
|
+
await safe(
|
|
3330
|
+
"ZCode",
|
|
3331
|
+
() => registerJsonClient(
|
|
3332
|
+
"ZCode",
|
|
3333
|
+
zcode,
|
|
3334
|
+
options.serve,
|
|
3335
|
+
options.confirm,
|
|
3336
|
+
now,
|
|
3337
|
+
prepareMissing,
|
|
3338
|
+
false,
|
|
3339
|
+
true,
|
|
3340
|
+
zcodeNestedServers
|
|
3341
|
+
)
|
|
3342
|
+
)
|
|
3343
|
+
);
|
|
3295
3344
|
results.push({
|
|
3296
3345
|
client: "Other MCP clients (Windsurf, Zed, LM Studio, ...)",
|
|
3297
3346
|
action: "printed",
|
|
@@ -3368,7 +3417,7 @@ async function unregisterCodex(cli, configPath, now) {
|
|
|
3368
3417
|
return { client: "Codex", action: "error", detail: `codex mcp remove failed: ${errorMessage(err)}` };
|
|
3369
3418
|
}
|
|
3370
3419
|
}
|
|
3371
|
-
async function unregisterJsonClient(clientLabel, configPath, now) {
|
|
3420
|
+
async function unregisterJsonClient(clientLabel, configPath, now, layout = topLevelServers) {
|
|
3372
3421
|
let data;
|
|
3373
3422
|
let existed;
|
|
3374
3423
|
let raw;
|
|
@@ -3382,7 +3431,7 @@ async function unregisterJsonClient(clientLabel, configPath, now) {
|
|
|
3382
3431
|
}
|
|
3383
3432
|
let servers;
|
|
3384
3433
|
try {
|
|
3385
|
-
servers =
|
|
3434
|
+
servers = layout.read(data, configPath);
|
|
3386
3435
|
} catch (err) {
|
|
3387
3436
|
return { client: clientLabel, action: "error", detail: errorMessage(err) };
|
|
3388
3437
|
}
|
|
@@ -3393,7 +3442,7 @@ async function unregisterJsonClient(clientLabel, configPath, now) {
|
|
|
3393
3442
|
try {
|
|
3394
3443
|
await writeFile2(backupPath, raw ?? JSON.stringify(data, null, 2), "utf8");
|
|
3395
3444
|
const { [SERVER_NAME]: _removed, ...rest } = servers;
|
|
3396
|
-
const nextData =
|
|
3445
|
+
const nextData = layout.merge(data, rest);
|
|
3397
3446
|
await writeFile2(configPath, `${JSON.stringify(nextData, null, 2)}
|
|
3398
3447
|
`, "utf8");
|
|
3399
3448
|
return { client: clientLabel, action: "registered", detail: `Removed from ${configPath} (backup: ${backupPath}).` };
|
|
@@ -3405,7 +3454,7 @@ async function unregisterClients(options = {}) {
|
|
|
3405
3454
|
const home2 = options.home ?? homedir2();
|
|
3406
3455
|
const osPlatform2 = options.osPlatform ?? platform2();
|
|
3407
3456
|
const now = options.now ?? (() => Date.now());
|
|
3408
|
-
const { claudeDesktop, cursor, codex } = paths(home2, osPlatform2);
|
|
3457
|
+
const { claudeDesktop, cursor, codex, zcode } = paths(home2, osPlatform2);
|
|
3409
3458
|
const safe = async (client, fn) => {
|
|
3410
3459
|
try {
|
|
3411
3460
|
return await fn();
|
|
@@ -3421,6 +3470,7 @@ async function unregisterClients(options = {}) {
|
|
|
3421
3470
|
}
|
|
3422
3471
|
results.push(await safe("Cursor", () => unregisterJsonClient("Cursor", cursor, now)));
|
|
3423
3472
|
results.push(await safe("Codex", () => unregisterCodex(options.codexCli ?? realCodexCli, codex, now)));
|
|
3473
|
+
results.push(await safe("ZCode", () => unregisterJsonClient("ZCode", zcode, now, zcodeNestedServers)));
|
|
3424
3474
|
results.push({
|
|
3425
3475
|
client: "Other MCP clients",
|
|
3426
3476
|
action: "skipped",
|
|
@@ -3592,7 +3642,7 @@ function checkAutoUpdate(home2, env) {
|
|
|
3592
3642
|
const latest = state.lastSeenVersion ? `; latest published ${state.lastSeenVersion}` : "";
|
|
3593
3643
|
return { name, status: "ok", detail: `On. Last checked ${checkedAt}${latest}.` };
|
|
3594
3644
|
}
|
|
3595
|
-
async function checkJsonClient(label, configPath) {
|
|
3645
|
+
async function checkJsonClient(label, configPath, nestedServers = false) {
|
|
3596
3646
|
if (!existsSync(dirname5(configPath))) {
|
|
3597
3647
|
return { name: label, status: "warn", detail: `Not installed (no ${dirname5(configPath)}).` };
|
|
3598
3648
|
}
|
|
@@ -3605,7 +3655,8 @@ async function checkJsonClient(label, configPath) {
|
|
|
3605
3655
|
}
|
|
3606
3656
|
try {
|
|
3607
3657
|
const data = JSON.parse(readFileSync(configPath, "utf8"));
|
|
3608
|
-
const
|
|
3658
|
+
const servers = nestedServers ? data.mcp?.servers : data.mcpServers;
|
|
3659
|
+
const entry = servers?.[SERVER_NAME];
|
|
3609
3660
|
if (!entry) {
|
|
3610
3661
|
return {
|
|
3611
3662
|
name: label,
|
|
@@ -3741,6 +3792,7 @@ async function runDoctor(options = {}) {
|
|
|
3741
3792
|
await run("Cursor", () => checkJsonClient("Cursor", join5(home2, ".cursor", "mcp.json")));
|
|
3742
3793
|
await run("Claude Code", () => checkClaudeCode(claudeCli));
|
|
3743
3794
|
await run("Codex", () => checkCodex(home2, codexCli));
|
|
3795
|
+
await run("ZCode", () => checkJsonClient("ZCode", join5(home2, ".zcode", "cli", "config.json"), true));
|
|
3744
3796
|
checks.push({
|
|
3745
3797
|
name: "Reminder",
|
|
3746
3798
|
status: "warn",
|
|
@@ -3760,7 +3812,8 @@ Usage: llmnesia-mcp <command> [options]
|
|
|
3760
3812
|
|
|
3761
3813
|
Setup:
|
|
3762
3814
|
install [ext-id...] Install the native host + register this MCP server with
|
|
3763
|
-
detected clients (Claude Code/Desktop, Cursor, Codex,
|
|
3815
|
+
detected clients (Claude Code/Desktop, Cursor, Codex,
|
|
3816
|
+
ZCode, ...)
|
|
3764
3817
|
doctor Diagnose: host installed? corpus present? clients wired?
|
|
3765
3818
|
uninstall Remove native host + client registrations (never deletes
|
|
3766
3819
|
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-RJJRIPHA.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-D2IVH5S2.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|
package/dist/llmnesia.mcpb
CHANGED
|
Binary file
|
package/dist/mcp.js
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
resolveCorpusDir,
|
|
13
13
|
runAutomaticMaintenance,
|
|
14
14
|
scheduleSelfUpdate
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-FIH7ZNYH.js";
|
|
16
16
|
import {
|
|
17
17
|
VERSION,
|
|
18
18
|
__commonJS,
|
|
19
19
|
__export,
|
|
20
20
|
__toESM
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-RJJRIPHA.js";
|
|
22
22
|
|
|
23
23
|
// ../node_modules/ajv/dist/compile/codegen/code.js
|
|
24
24
|
var require_code = __commonJS({
|
|
@@ -21678,19 +21678,26 @@ var LazyResource = class {
|
|
|
21678
21678
|
function jsonResult(payload) {
|
|
21679
21679
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
21680
21680
|
}
|
|
21681
|
-
var SOURCE_LINK_INSTRUCTION = "When answering from these conversations, always include a Sources section with a clickable Markdown link to every chat you relied on. Link its `sourceUrl`: this is the original chat URL when available, otherwise
|
|
21681
|
+
var SOURCE_LINK_INSTRUCTION = "When answering from these conversations, always include a Sources section with a clickable Markdown link to every chat you relied on. Link its `sourceUrl`: this is the original chat URL when available, otherwise an HTTPS hand-off that opens the local LLMnesia Viewer without sending the conversation id to the website. Do this automatically; do not wait for the user to ask for links.";
|
|
21682
21682
|
var STORE_EXTENSION_ID = "leekfgbdojiaabifbjbbgiiclannjdkf";
|
|
21683
21683
|
var EXTENSION_ID_RE = /^[a-p]{32}$/;
|
|
21684
|
+
var VIEWER_HANDOFF_URL = "https://llmnesia.com/open";
|
|
21684
21685
|
function viewerExtensionId() {
|
|
21685
21686
|
const configured = process.env.LLMNESIA_VIEWER_EXTENSION_ID?.trim() ?? readNativeHostViewerExtensionId();
|
|
21686
21687
|
return configured && EXTENSION_ID_RE.test(configured) ? configured : STORE_EXTENSION_ID;
|
|
21687
21688
|
}
|
|
21688
|
-
function
|
|
21689
|
-
|
|
21689
|
+
function viewerSourceUrl(docId, extensionId) {
|
|
21690
|
+
const fragment = new URLSearchParams({ docId, extensionId });
|
|
21691
|
+
return `${VIEWER_HANDOFF_URL}#${fragment.toString()}`;
|
|
21690
21692
|
}
|
|
21691
21693
|
function sourceLinks(docId, originalUrl) {
|
|
21692
|
-
const
|
|
21693
|
-
|
|
21694
|
+
const extensionId = viewerExtensionId();
|
|
21695
|
+
const localViewerUrl = `chrome-extension://${extensionId}/viewer.html?docId=${encodeURIComponent(docId)}`;
|
|
21696
|
+
return originalUrl ? { viewerUrl: localViewerUrl, sourceUrl: originalUrl, sourceKind: "original" } : {
|
|
21697
|
+
viewerUrl: localViewerUrl,
|
|
21698
|
+
sourceUrl: viewerSourceUrl(docId, extensionId),
|
|
21699
|
+
sourceKind: "viewer"
|
|
21700
|
+
};
|
|
21694
21701
|
}
|
|
21695
21702
|
function sourcedJsonResult(payload) {
|
|
21696
21703
|
return jsonResult({ responseInstruction: SOURCE_LINK_INSTRUCTION, ...payload });
|
|
@@ -21757,14 +21764,14 @@ async function runServe() {
|
|
|
21757
21764
|
const server = new McpServer(
|
|
21758
21765
|
{ name: "llmnesia", version: VERSION },
|
|
21759
21766
|
{
|
|
21760
|
-
instructions: "LLMnesia is the user's searchable archive of past AI chats. Whenever the user asks about 'my chats', 'my conversations', or anything they previously said, discussed, decided, or worked on, use LLMnesia before built-in task/thread lists, local file scans, or memory, unless they explicitly ask only about Codex tasks. Use list_conversations with sort='oldest' for their earliest chat, search_conversations to find relevant prior chats, get_conversation to read a full transcript, and list_recent to see what they worked on lately. Search and list results carry a `corpus` block saying when the corpus last received data; if a chat the user expects is missing, check corpus_status before concluding it does not exist. Whenever you answer using LLMnesia, always include a Sources section with clickable links to the chats you used. Link each result's `sourceUrl`, which is its original URL when available or its local LLMnesia Viewer
|
|
21767
|
+
instructions: "LLMnesia is the user's searchable archive of past AI chats. Whenever the user asks about 'my chats', 'my conversations', or anything they previously said, discussed, decided, or worked on, use LLMnesia before built-in task/thread lists, local file scans, or memory, unless they explicitly ask only about Codex tasks. Use list_conversations with sort='oldest' for their earliest chat, search_conversations to find relevant prior chats, get_conversation to read a full transcript, and list_recent to see what they worked on lately. Search and list results carry a `corpus` block saying when the corpus last received data; if a chat the user expects is missing, check corpus_status before concluding it does not exist. Whenever you answer using LLMnesia, always include a Sources section with clickable links to the chats you used. Link each result's `sourceUrl`, which is its original URL when available or a clickable HTTPS hand-off to its local LLMnesia Viewer otherwise; never wait for the user to ask."
|
|
21761
21768
|
}
|
|
21762
21769
|
);
|
|
21763
21770
|
server.registerTool(
|
|
21764
21771
|
"search_conversations",
|
|
21765
21772
|
{
|
|
21766
21773
|
title: "Search conversations",
|
|
21767
|
-
description: "Full-text search across the user's past AI conversations. Call this whenever the user asks what they said, thought, discussed, decided, or worked on previously \u2014 even if the question doesn't say 'search' \u2014 rather than answering from conversation memory alone or claiming no record exists. Returns matching conversations with a highlighted snippet, ranked by relevance. Defaults to recall-first matching (any term); pass match_mode to answer questions where a wrong hit matters, such as whether or how often something was discussed. Every result includes `sourceUrl`: the original chat URL when available, otherwise its local LLMnesia Viewer
|
|
21774
|
+
description: "Full-text search across the user's past AI conversations. Call this whenever the user asks what they said, thought, discussed, decided, or worked on previously \u2014 even if the question doesn't say 'search' \u2014 rather than answering from conversation memory alone or claiming no record exists. Returns matching conversations with a highlighted snippet, ranked by relevance. Defaults to recall-first matching (any term); pass match_mode to answer questions where a wrong hit matters, such as whether or how often something was discussed. Every result includes `sourceUrl`: the original chat URL when available, otherwise a clickable HTTPS hand-off to its local LLMnesia Viewer. Always link the chats used in the answer.",
|
|
21768
21775
|
inputSchema: {
|
|
21769
21776
|
query: external_exports.string().describe("Search terms (natural words; FTS operators are not needed)."),
|
|
21770
21777
|
match_mode: external_exports.enum(["any", "all", "phrase"]).optional().describe(
|
|
@@ -21813,6 +21820,7 @@ async function runServe() {
|
|
|
21813
21820
|
};
|
|
21814
21821
|
if (h.accountId) entry.accountId = h.accountId;
|
|
21815
21822
|
if (h.accountLabel) entry.accountLabel = h.accountLabel;
|
|
21823
|
+
if (h.models?.length) entry.models = h.models;
|
|
21816
21824
|
return entry;
|
|
21817
21825
|
})
|
|
21818
21826
|
});
|
|
@@ -21822,7 +21830,7 @@ async function runServe() {
|
|
|
21822
21830
|
"get_conversation",
|
|
21823
21831
|
{
|
|
21824
21832
|
title: "Get conversation",
|
|
21825
|
-
description: "Return the full transcript of one conversation by its id (the `id` from search results). The conversation includes `sourceUrl`: its original-chat URL when available, otherwise its local LLMnesia Viewer
|
|
21833
|
+
description: "Return the full transcript of one conversation by its id (the `id` from search results). The conversation includes `sourceUrl`: its original-chat URL when available, otherwise a clickable HTTPS hand-off to its local LLMnesia Viewer. Always link it in an answer based on this transcript.",
|
|
21826
21834
|
inputSchema: {
|
|
21827
21835
|
id: external_exports.string().describe("The conversation id (docId), e.g. 'claude:abc-123'.")
|
|
21828
21836
|
},
|
|
@@ -21846,6 +21854,7 @@ async function runServe() {
|
|
|
21846
21854
|
messages: record2.messages.map((m) => ({
|
|
21847
21855
|
index: m.msgIndex,
|
|
21848
21856
|
role: m.role,
|
|
21857
|
+
...m.model ? { model: m.model } : {},
|
|
21849
21858
|
// Prefer the block-formatted copy; fall back to the search-collapsed text.
|
|
21850
21859
|
text: m.formattedText ?? m.text
|
|
21851
21860
|
}))
|
|
@@ -21856,7 +21865,7 @@ async function runServe() {
|
|
|
21856
21865
|
"list_recent",
|
|
21857
21866
|
{
|
|
21858
21867
|
title: "List recent conversations",
|
|
21859
|
-
description: "List the user's most recently updated conversations (title + summary + source URL), newest first. `sourceUrl` is the original chat URL when available, otherwise its local LLMnesia Viewer
|
|
21868
|
+
description: "List the user's most recently updated conversations (title + summary + source URL), newest first. `sourceUrl` is the original chat URL when available, otherwise a clickable HTTPS hand-off to its local LLMnesia Viewer. Always link the chats used in the answer.",
|
|
21860
21869
|
inputSchema: {
|
|
21861
21870
|
limit: external_exports.number().int().min(1).max(100).optional().describe("How many to return (default 10).")
|
|
21862
21871
|
},
|
|
@@ -21887,6 +21896,7 @@ async function runServe() {
|
|
|
21887
21896
|
};
|
|
21888
21897
|
if (e.accountId) entry.accountId = e.accountId;
|
|
21889
21898
|
if (e.accountLabel) entry.accountLabel = e.accountLabel;
|
|
21899
|
+
if (e.models?.length) entry.models = e.models;
|
|
21890
21900
|
return entry;
|
|
21891
21901
|
})
|
|
21892
21902
|
});
|
|
@@ -21896,7 +21906,7 @@ async function runServe() {
|
|
|
21896
21906
|
"list_conversations",
|
|
21897
21907
|
{
|
|
21898
21908
|
title: "List conversations by date",
|
|
21899
|
-
description: "List conversation metadata in chronological order. Use sort='oldest' for the user's earliest or oldest chat, sort='newest' for most recently created chats, and sort='recent' for most recently updated chats. This is the complete corpus chronology, so do not scan LLMnesia's private files with shell commands. Entries include `sourceUrl`: the original chat URL when available, otherwise the local LLMnesia Viewer
|
|
21909
|
+
description: "List conversation metadata in chronological order. Use sort='oldest' for the user's earliest or oldest chat, sort='newest' for most recently created chats, and sort='recent' for most recently updated chats. This is the complete corpus chronology, so do not scan LLMnesia's private files with shell commands. Entries include `sourceUrl`: the original chat URL when available, otherwise a clickable HTTPS hand-off to the local LLMnesia Viewer. Always link the chats used in the answer.",
|
|
21900
21910
|
inputSchema: {
|
|
21901
21911
|
sort: external_exports.enum(["recent", "oldest", "newest"]).optional().describe("Chronology: recent (default, updated), oldest (created), or newest (created)."),
|
|
21902
21912
|
limit: external_exports.number().int().min(1).max(100).optional().describe("How many to return (default 10).")
|
|
@@ -21930,6 +21940,7 @@ async function runServe() {
|
|
|
21930
21940
|
};
|
|
21931
21941
|
if (e.accountId) entry.accountId = e.accountId;
|
|
21932
21942
|
if (e.accountLabel) entry.accountLabel = e.accountLabel;
|
|
21943
|
+
if (e.models?.length) entry.models = e.models;
|
|
21933
21944
|
return entry;
|
|
21934
21945
|
})
|
|
21935
21946
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmnesia/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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.8",
|
|
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.8",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|