@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 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. Follow every step shown for your computer, including fully quitting and
28
- reopening the browser and connected AI app when instructed.
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; running one by itself is
32
- not the complete LLMnesia setup.
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
- and Codex/ChatGPT), and
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-YRK2DRGG.js";
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 = 3;
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 = [conversation.title, conversation.preview].filter(Boolean).join("\n");
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: conversation.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 ? [record?.conversation.title ?? row.title, record?.conversation.preview ?? row.preview].filter(Boolean).join("\n") : record?.messages.find((message) => message.msgIndex === row.msgIndex)?.text ?? row.preview;
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";
@@ -38,7 +38,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
38
  ));
39
39
 
40
40
  // src/version.ts
41
- var VERSION = true ? "0.2.6" : "0.0.0-dev";
41
+ var VERSION = true ? "0.2.8" : "0.0.0-dev";
42
42
 
43
43
  export {
44
44
  __require,
@@ -17,13 +17,13 @@ import {
17
17
  runtimeBinDir,
18
18
  staleIndexFiles,
19
19
  uninstallRuntime
20
- } from "./chunk-HL5QE57W.js";
20
+ } from "./chunk-FIH7ZNYH.js";
21
21
  import {
22
22
  VERSION,
23
23
  __commonJS,
24
24
  __require,
25
25
  __toESM
26
- } from "./chunk-YRK2DRGG.js";
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 = mcpServersOf(data, configPath);
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 = JSON.stringify({ mcpServers: { [SERVER_NAME]: nextEntry } }, null, 2);
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 = { ...data, mcpServers: { ...servers, [SERVER_NAME]: nextEntry } };
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 = mcpServersOf(data, configPath);
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 = { ...data, mcpServers: rest };
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 entry = data.mcpServers?.[SERVER_NAME];
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-YRK2DRGG.js";
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-DRB4DVH3.js");
144
+ const { main } = await import("./cli-D2IVH5S2.js");
145
145
  return main();
146
146
  }
147
147
  bootstrap().then((code) => {
Binary file
package/dist/mcp.js CHANGED
@@ -12,13 +12,13 @@ import {
12
12
  resolveCorpusDir,
13
13
  runAutomaticMaintenance,
14
14
  scheduleSelfUpdate
15
- } from "./chunk-HL5QE57W.js";
15
+ } from "./chunk-FIH7ZNYH.js";
16
16
  import {
17
17
  VERSION,
18
18
  __commonJS,
19
19
  __export,
20
20
  __toESM
21
- } from "./chunk-YRK2DRGG.js";
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 its local LLMnesia Viewer URL. Do this automatically; do not wait for the user to ask for links.";
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 viewerUrl(docId) {
21689
- return `chrome-extension://${viewerExtensionId()}/viewer.html?docId=${encodeURIComponent(docId)}`;
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 localViewerUrl = viewerUrl(docId);
21693
- return originalUrl ? { viewerUrl: localViewerUrl, sourceUrl: originalUrl, sourceKind: "original" } : { viewerUrl: localViewerUrl, sourceUrl: localViewerUrl, sourceKind: "viewer" };
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 URL otherwise; never wait for the user to ask."
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 URL. Always link the chats used in the answer.",
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 URL. Always link it in an answer based on this transcript.",
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 URL. Always link the chats used in the answer.",
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 URL. Always link the chats used in the answer.",
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.6",
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",
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.6",
12
+ "version": "0.2.8",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }