@llmnesia/mcp 0.2.11 → 0.2.12
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 +18 -4
- package/dist/{chunk-7ISC4REX.js → chunk-3QDA3XWA.js} +163 -17
- package/dist/{chunk-ZYE6VY6A.js → chunk-SEBISBN5.js} +1 -1
- package/dist/{cli-RQMFEW5J.js → cli-IF63HSCG.js} +49 -32
- package/dist/cli.js +2 -2
- package/dist/llmnesia-claude-host.mjs +93 -13
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +300 -229
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -19,11 +19,13 @@
|
|
|
19
19
|
// without ~/.llmnesia/config.json this host is exactly as read-only as it
|
|
20
20
|
// has always been for plain session indexing.
|
|
21
21
|
//
|
|
22
|
-
// Protocol
|
|
22
|
+
// Protocol v5 (extension → host). v1 was Claude-Code-only: `list` carried no
|
|
23
23
|
// `source` and `get` took a bare sessionId. Both remain accepted, so an
|
|
24
24
|
// extension older than this host keeps working; an extension NEWER than the
|
|
25
25
|
// host detects v1 from the pong and syncs Claude Code only.
|
|
26
26
|
//
|
|
27
|
+
// v5 adds per-source listing health and putSourceHealth metadata updates,
|
|
28
|
+
// keeping diagnostics separate from the conversation inbox.
|
|
27
29
|
// v4 adds ZCode to `list`/`get` (the first source backed by a SQLite database
|
|
28
30
|
// rather than per-session files; `get` materializes the transcript as JSONL).
|
|
29
31
|
// The version is informational in both directions, as below: a v3 host never
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
// treats that as "nothing to fetch", so the version is informational: hosts are
|
|
35
37
|
// installed out of band and an extension upgraded ahead of its host is the
|
|
36
38
|
// ordinary case, not a fault.
|
|
37
|
-
// { cmd: "ping" } → { type: "pong", version:
|
|
39
|
+
// { cmd: "ping" } → { type: "pong", version: 5 }
|
|
38
40
|
// { cmd: "list" } → { type: "list", sessions:
|
|
39
41
|
// [{source, sessionId, mtime, project?, title?}] }
|
|
40
42
|
// { cmd: "get", source, sessionId } → one or more { type: "chunk", sessionId, seq, total, data }
|
|
@@ -59,10 +61,10 @@ import { chmod, mkdir, readdir, readFile, rename, stat, writeFile } from "node:f
|
|
|
59
61
|
import { homedir } from "node:os";
|
|
60
62
|
import { join } from "node:path";
|
|
61
63
|
|
|
62
|
-
//
|
|
64
|
+
// 5 adds per-source listing health (see the protocol comment above). Do not make it a gate: hosts
|
|
63
65
|
// are installed out of band and a version check would strand every user who
|
|
64
66
|
// upgraded the extension first.
|
|
65
|
-
const PROTOCOL_VERSION =
|
|
67
|
+
const PROTOCOL_VERSION = 5;
|
|
66
68
|
const PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
67
69
|
// ZCode keeps every session in one SQLite database (WAL mode) — the first
|
|
68
70
|
// source with no per-session file. The host opens it READ-ONLY and materializes
|
|
@@ -134,12 +136,13 @@ function send(message) {
|
|
|
134
136
|
// One entry per session: { source, sessionId, mtime, project?, title?, path }.
|
|
135
137
|
// `path` is stripped before anything goes on the wire.
|
|
136
138
|
|
|
137
|
-
async function listClaudeCodeSessions() {
|
|
139
|
+
async function listClaudeCodeSessions(issues) {
|
|
138
140
|
const sessions = [];
|
|
139
141
|
let projectDirs;
|
|
140
142
|
try {
|
|
141
143
|
projectDirs = await readdir(PROJECTS_DIR, { withFileTypes: true });
|
|
142
144
|
} catch {
|
|
145
|
+
if (issues) issues.failed += 1;
|
|
143
146
|
return sessions; // no ~/.claude/projects yet
|
|
144
147
|
}
|
|
145
148
|
for (const entry of projectDirs) {
|
|
@@ -151,6 +154,7 @@ async function listClaudeCodeSessions() {
|
|
|
151
154
|
try {
|
|
152
155
|
files = await readdir(dir);
|
|
153
156
|
} catch {
|
|
157
|
+
if (issues) issues.failed += 1;
|
|
154
158
|
continue;
|
|
155
159
|
}
|
|
156
160
|
for (const file of files) {
|
|
@@ -168,6 +172,7 @@ async function listClaudeCodeSessions() {
|
|
|
168
172
|
path
|
|
169
173
|
});
|
|
170
174
|
} catch {
|
|
175
|
+
if (issues) issues.failed += 1;
|
|
171
176
|
// Skip unreadable files.
|
|
172
177
|
}
|
|
173
178
|
}
|
|
@@ -186,18 +191,19 @@ function codexSessionId(filename) {
|
|
|
186
191
|
return match ? match[1].toLowerCase() : base;
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
async function walkCodexDir(dir, depth, out) {
|
|
194
|
+
async function walkCodexDir(dir, depth, out, issues) {
|
|
190
195
|
let entries;
|
|
191
196
|
try {
|
|
192
197
|
entries = await readdir(dir, { withFileTypes: true });
|
|
193
198
|
} catch {
|
|
199
|
+
if (issues) issues.failed += 1;
|
|
194
200
|
return out; // missing or unreadable — nothing to add
|
|
195
201
|
}
|
|
196
202
|
for (const entry of entries) {
|
|
197
203
|
const path = join(dir, entry.name);
|
|
198
204
|
if (entry.isDirectory()) {
|
|
199
205
|
if (depth > 0) {
|
|
200
|
-
await walkCodexDir(path, depth - 1, out);
|
|
206
|
+
await walkCodexDir(path, depth - 1, out, issues);
|
|
201
207
|
}
|
|
202
208
|
continue;
|
|
203
209
|
}
|
|
@@ -213,6 +219,7 @@ async function walkCodexDir(dir, depth, out) {
|
|
|
213
219
|
path
|
|
214
220
|
});
|
|
215
221
|
} catch {
|
|
222
|
+
if (issues) issues.failed += 1;
|
|
216
223
|
// Skip unreadable files.
|
|
217
224
|
}
|
|
218
225
|
}
|
|
@@ -249,8 +256,8 @@ async function readCodexThreadNames(codexHome) {
|
|
|
249
256
|
return titles;
|
|
250
257
|
}
|
|
251
258
|
|
|
252
|
-
async function listCodexSessions(codexHome) {
|
|
253
|
-
const sessions = await walkCodexDir(join(codexHome, "sessions"), CODEX_MAX_DEPTH, []);
|
|
259
|
+
async function listCodexSessions(codexHome, issues) {
|
|
260
|
+
const sessions = await walkCodexDir(join(codexHome, "sessions"), CODEX_MAX_DEPTH, [], issues);
|
|
254
261
|
if (sessions.length === 0) {
|
|
255
262
|
return sessions;
|
|
256
263
|
}
|
|
@@ -299,9 +306,10 @@ async function openZcodeDb() {
|
|
|
299
306
|
* title ("New Session"); only real titles are attached, so the extension's
|
|
300
307
|
* derived "<project> · <first prompt>" still wins when ZCode has no name.
|
|
301
308
|
*/
|
|
302
|
-
async function listZcodeSessions() {
|
|
309
|
+
async function listZcodeSessions(issues) {
|
|
303
310
|
const db = await openZcodeDb();
|
|
304
311
|
if (!db) {
|
|
312
|
+
if (issues) issues.failed += 1;
|
|
305
313
|
return [];
|
|
306
314
|
}
|
|
307
315
|
try {
|
|
@@ -331,6 +339,7 @@ async function listZcodeSessions() {
|
|
|
331
339
|
}
|
|
332
340
|
return sessions;
|
|
333
341
|
} catch {
|
|
342
|
+
if (issues) issues.failed += 1;
|
|
334
343
|
return []; // unreadable or mid-migration database — ZCode contributes nothing
|
|
335
344
|
} finally {
|
|
336
345
|
db.close();
|
|
@@ -467,6 +476,36 @@ async function listSessions() {
|
|
|
467
476
|
return [...claude, ...codex, ...zcode];
|
|
468
477
|
}
|
|
469
478
|
|
|
479
|
+
// A missing root and an unreadable source must not both appear as a healthy,
|
|
480
|
+
// empty scan. Paths and transcript data never enter these diagnostics.
|
|
481
|
+
async function listSessionsWithHealth() {
|
|
482
|
+
const config = await loadConfig();
|
|
483
|
+
const codexHome = config?.codexHome || DEFAULT_CODEX_HOME;
|
|
484
|
+
const sources = [
|
|
485
|
+
["claude_code", PROJECTS_DIR, (issues) => listClaudeCodeSessions(issues)],
|
|
486
|
+
["codex", join(codexHome, "sessions"), (issues) => listCodexSessions(codexHome, issues)],
|
|
487
|
+
["zcode", ZCODE_DB, (issues) => listZcodeSessions(issues)]
|
|
488
|
+
];
|
|
489
|
+
const sessions = [];
|
|
490
|
+
const sourceStatus = {};
|
|
491
|
+
for (const [source, path, list] of sources) {
|
|
492
|
+
try {
|
|
493
|
+
await stat(path);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
sourceStatus[source] = { state: error?.code === "ENOENT" ? "missing" : "error" };
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const issues = { failed: 0 };
|
|
499
|
+
try {
|
|
500
|
+
sessions.push(...await list(issues));
|
|
501
|
+
sourceStatus[source] = { state: issues.failed > 0 ? "partial" : "ok" };
|
|
502
|
+
} catch {
|
|
503
|
+
sourceStatus[source] = { state: "error" };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { sessions, sourceStatus };
|
|
507
|
+
}
|
|
508
|
+
|
|
470
509
|
async function loadConfig() {
|
|
471
510
|
try {
|
|
472
511
|
const raw = await readFile(CONFIG_FILE, "utf8");
|
|
@@ -648,6 +687,43 @@ async function readCorpusMemories(corpusRoot) {
|
|
|
648
687
|
return { memories, skipped };
|
|
649
688
|
}
|
|
650
689
|
|
|
690
|
+
// A separate metadata channel keeps health heartbeats out of the conversation
|
|
691
|
+
// inbox. Older MCP readers ignore this file and keep their true data age.
|
|
692
|
+
function sourceHealthRow(value) {
|
|
693
|
+
if (!value || typeof value !== "object") return null;
|
|
694
|
+
const { source, observedAt, connection, state, lastAttemptAt, lastSuccessfulSyncAt, lastCaptureAt, failedSessions } = value;
|
|
695
|
+
const time = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
696
|
+
if (!["claude_code", "codex", "zcode"].includes(source) || !["connected", "disconnected"].includes(connection) ||
|
|
697
|
+
!["ok", "partial", "missing", "error", "unknown"].includes(state) || !time(observedAt) ||
|
|
698
|
+
observedAt > Date.now() + 300000 || !time(lastAttemptAt) || lastAttemptAt > observedAt ||
|
|
699
|
+
(lastSuccessfulSyncAt !== null && (!time(lastSuccessfulSyncAt) || lastSuccessfulSyncAt > observedAt)) ||
|
|
700
|
+
(lastCaptureAt !== null && (!time(lastCaptureAt) || lastCaptureAt > observedAt)) || !time(failedSessions)) return null;
|
|
701
|
+
return { source, observedAt, connection, state, lastAttemptAt, lastSuccessfulSyncAt, lastCaptureAt, failedSessions };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function handleSourceHealth(message) {
|
|
705
|
+
const config = await loadConfig();
|
|
706
|
+
if (!config?.corpusRoot || !Array.isArray(message.rows) || message.rows.length > 3) {
|
|
707
|
+
send({ type: "error", error: "source health: invalid rows or no corpus configured" }); return;
|
|
708
|
+
}
|
|
709
|
+
const rows = message.rows.map(sourceHealthRow);
|
|
710
|
+
if (rows.some((row) => !row)) {
|
|
711
|
+
send({ type: "error", error: "source health: invalid report" }); return;
|
|
712
|
+
}
|
|
713
|
+
const path = join(config.corpusRoot, "source-health-v1.json");
|
|
714
|
+
let previous = [];
|
|
715
|
+
try { previous = JSON.parse(await readFile(path, "utf8")); } catch { /* First report. */ }
|
|
716
|
+
const current = new Map((Array.isArray(previous) ? previous : []).map(sourceHealthRow).filter(Boolean).map((row) => [row.source, row]));
|
|
717
|
+
for (const row of rows) {
|
|
718
|
+
if ((current.get(row.source)?.observedAt ?? -1) < row.observedAt) current.set(row.source, row);
|
|
719
|
+
}
|
|
720
|
+
await mkdir(config.corpusRoot, { recursive: true, mode: 0o700 });
|
|
721
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
722
|
+
await writeFile(temp, JSON.stringify([...current.values()]) + "\n", { mode: FILE_MODE });
|
|
723
|
+
await rename(temp, path);
|
|
724
|
+
send({ type: "sourceHealthAck" });
|
|
725
|
+
}
|
|
726
|
+
|
|
651
727
|
async function handle(message) {
|
|
652
728
|
if (!message || typeof message !== "object") {
|
|
653
729
|
return;
|
|
@@ -662,7 +738,11 @@ async function handle(message) {
|
|
|
662
738
|
send({ type: "corpusInfo", present: false });
|
|
663
739
|
return;
|
|
664
740
|
}
|
|
665
|
-
send({ type: "corpusInfo", present: true, root: config.corpusRoot });
|
|
741
|
+
send({ type: "corpusInfo", present: true, root: config.corpusRoot, sourceHealthSupported: true });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (message.cmd === "putSourceHealth") {
|
|
745
|
+
await handleSourceHealth(message);
|
|
666
746
|
return;
|
|
667
747
|
}
|
|
668
748
|
if (message.cmd === "putChunk") {
|
|
@@ -680,8 +760,8 @@ async function handle(message) {
|
|
|
680
760
|
return;
|
|
681
761
|
}
|
|
682
762
|
if (message.cmd === "list") {
|
|
683
|
-
const sessions = await
|
|
684
|
-
send({ type: "list", sessions: sessions.map(({ path, ...rest }) => rest) });
|
|
763
|
+
const { sessions, sourceStatus } = await listSessionsWithHealth();
|
|
764
|
+
send({ type: "list", sessions: sessions.map(({ path, ...rest }) => rest), sourceStatus });
|
|
685
765
|
return;
|
|
686
766
|
}
|
|
687
767
|
if (message.cmd === "get") {
|
package/dist/llmnesia.mcpb
CHANGED
|
Binary file
|