@llmnesia/mcp 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -9
- package/dist/{chunk-ZYE6VY6A.js → chunk-GHKXNM7R.js} +1 -1
- package/dist/{chunk-7ISC4REX.js → chunk-QIRYUQIU.js} +241 -72
- package/dist/{cli-RQMFEW5J.js → cli-CMCIFSWH.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 +301 -230
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -14,20 +14,21 @@ import {
|
|
|
14
14
|
openSearchIndex,
|
|
15
15
|
readAutomaticMaintenanceState,
|
|
16
16
|
resolveCorpusDir,
|
|
17
|
+
retryQuarantine,
|
|
17
18
|
runtimeBinDir,
|
|
18
19
|
staleIndexFiles,
|
|
19
20
|
uninstallRuntime
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-QIRYUQIU.js";
|
|
21
22
|
import {
|
|
22
23
|
VERSION,
|
|
23
24
|
__commonJS,
|
|
24
25
|
__require,
|
|
25
26
|
__toESM
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-GHKXNM7R.js";
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
+
// ../node_modules/isexe/windows.js
|
|
29
30
|
var require_windows = __commonJS({
|
|
30
|
-
"
|
|
31
|
+
"../node_modules/isexe/windows.js"(exports, module) {
|
|
31
32
|
"use strict";
|
|
32
33
|
module.exports = isexe;
|
|
33
34
|
isexe.sync = sync;
|
|
@@ -66,9 +67,9 @@ var require_windows = __commonJS({
|
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
+
// ../node_modules/isexe/mode.js
|
|
70
71
|
var require_mode = __commonJS({
|
|
71
|
-
"
|
|
72
|
+
"../node_modules/isexe/mode.js"(exports, module) {
|
|
72
73
|
"use strict";
|
|
73
74
|
module.exports = isexe;
|
|
74
75
|
isexe.sync = sync;
|
|
@@ -100,9 +101,9 @@ var require_mode = __commonJS({
|
|
|
100
101
|
}
|
|
101
102
|
});
|
|
102
103
|
|
|
103
|
-
//
|
|
104
|
+
// ../node_modules/isexe/index.js
|
|
104
105
|
var require_isexe = __commonJS({
|
|
105
|
-
"
|
|
106
|
+
"../node_modules/isexe/index.js"(exports, module) {
|
|
106
107
|
"use strict";
|
|
107
108
|
var fs = __require("fs");
|
|
108
109
|
var core;
|
|
@@ -156,9 +157,9 @@ var require_isexe = __commonJS({
|
|
|
156
157
|
}
|
|
157
158
|
});
|
|
158
159
|
|
|
159
|
-
//
|
|
160
|
+
// ../node_modules/which/which.js
|
|
160
161
|
var require_which = __commonJS({
|
|
161
|
-
"
|
|
162
|
+
"../node_modules/which/which.js"(exports, module) {
|
|
162
163
|
"use strict";
|
|
163
164
|
var isWindows2 = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
|
|
164
165
|
var path = __require("path");
|
|
@@ -253,9 +254,9 @@ var require_which = __commonJS({
|
|
|
253
254
|
}
|
|
254
255
|
});
|
|
255
256
|
|
|
256
|
-
//
|
|
257
|
+
// ../node_modules/path-key/index.js
|
|
257
258
|
var require_path_key = __commonJS({
|
|
258
|
-
"
|
|
259
|
+
"../node_modules/path-key/index.js"(exports, module) {
|
|
259
260
|
"use strict";
|
|
260
261
|
var pathKey = (options = {}) => {
|
|
261
262
|
const environment = options.env || process.env;
|
|
@@ -270,9 +271,9 @@ var require_path_key = __commonJS({
|
|
|
270
271
|
}
|
|
271
272
|
});
|
|
272
273
|
|
|
273
|
-
//
|
|
274
|
+
// ../node_modules/cross-spawn/lib/util/resolveCommand.js
|
|
274
275
|
var require_resolveCommand = __commonJS({
|
|
275
|
-
"
|
|
276
|
+
"../node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) {
|
|
276
277
|
"use strict";
|
|
277
278
|
var path = __require("path");
|
|
278
279
|
var which = require_which();
|
|
@@ -312,9 +313,9 @@ var require_resolveCommand = __commonJS({
|
|
|
312
313
|
}
|
|
313
314
|
});
|
|
314
315
|
|
|
315
|
-
//
|
|
316
|
+
// ../node_modules/cross-spawn/lib/util/escape.js
|
|
316
317
|
var require_escape = __commonJS({
|
|
317
|
-
"
|
|
318
|
+
"../node_modules/cross-spawn/lib/util/escape.js"(exports, module) {
|
|
318
319
|
"use strict";
|
|
319
320
|
var metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g;
|
|
320
321
|
function escapeCommand(arg) {
|
|
@@ -337,17 +338,17 @@ var require_escape = __commonJS({
|
|
|
337
338
|
}
|
|
338
339
|
});
|
|
339
340
|
|
|
340
|
-
//
|
|
341
|
+
// ../node_modules/shebang-regex/index.js
|
|
341
342
|
var require_shebang_regex = __commonJS({
|
|
342
|
-
"
|
|
343
|
+
"../node_modules/shebang-regex/index.js"(exports, module) {
|
|
343
344
|
"use strict";
|
|
344
345
|
module.exports = /^#!(.*)/;
|
|
345
346
|
}
|
|
346
347
|
});
|
|
347
348
|
|
|
348
|
-
//
|
|
349
|
+
// ../node_modules/shebang-command/index.js
|
|
349
350
|
var require_shebang_command = __commonJS({
|
|
350
|
-
"
|
|
351
|
+
"../node_modules/shebang-command/index.js"(exports, module) {
|
|
351
352
|
"use strict";
|
|
352
353
|
var shebangRegex = require_shebang_regex();
|
|
353
354
|
module.exports = (string = "") => {
|
|
@@ -365,9 +366,9 @@ var require_shebang_command = __commonJS({
|
|
|
365
366
|
}
|
|
366
367
|
});
|
|
367
368
|
|
|
368
|
-
//
|
|
369
|
+
// ../node_modules/cross-spawn/lib/util/readShebang.js
|
|
369
370
|
var require_readShebang = __commonJS({
|
|
370
|
-
"
|
|
371
|
+
"../node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
|
|
371
372
|
"use strict";
|
|
372
373
|
var fs = __require("fs");
|
|
373
374
|
var shebangCommand = require_shebang_command();
|
|
@@ -387,9 +388,9 @@ var require_readShebang = __commonJS({
|
|
|
387
388
|
}
|
|
388
389
|
});
|
|
389
390
|
|
|
390
|
-
//
|
|
391
|
+
// ../node_modules/cross-spawn/lib/parse.js
|
|
391
392
|
var require_parse = __commonJS({
|
|
392
|
-
"
|
|
393
|
+
"../node_modules/cross-spawn/lib/parse.js"(exports, module) {
|
|
393
394
|
"use strict";
|
|
394
395
|
var path = __require("path");
|
|
395
396
|
var resolveCommand = require_resolveCommand();
|
|
@@ -449,9 +450,9 @@ var require_parse = __commonJS({
|
|
|
449
450
|
}
|
|
450
451
|
});
|
|
451
452
|
|
|
452
|
-
//
|
|
453
|
+
// ../node_modules/cross-spawn/lib/enoent.js
|
|
453
454
|
var require_enoent = __commonJS({
|
|
454
|
-
"
|
|
455
|
+
"../node_modules/cross-spawn/lib/enoent.js"(exports, module) {
|
|
455
456
|
"use strict";
|
|
456
457
|
var isWin = process.platform === "win32";
|
|
457
458
|
function notFoundError(original, syscall) {
|
|
@@ -499,9 +500,9 @@ var require_enoent = __commonJS({
|
|
|
499
500
|
}
|
|
500
501
|
});
|
|
501
502
|
|
|
502
|
-
//
|
|
503
|
+
// ../node_modules/cross-spawn/index.js
|
|
503
504
|
var require_cross_spawn = __commonJS({
|
|
504
|
-
"
|
|
505
|
+
"../node_modules/cross-spawn/index.js"(exports, module) {
|
|
505
506
|
"use strict";
|
|
506
507
|
var cp = __require("child_process");
|
|
507
508
|
var parse = require_parse();
|
|
@@ -888,13 +889,13 @@ function parseArgs(argv) {
|
|
|
888
889
|
return { command: positionals[0], positionals: positionals.slice(1), options, flags };
|
|
889
890
|
}
|
|
890
891
|
|
|
891
|
-
//
|
|
892
|
+
// ../node_modules/chokidar/esm/index.js
|
|
892
893
|
import { stat as statcb } from "fs";
|
|
893
894
|
import { stat as stat4, readdir as readdir2 } from "fs/promises";
|
|
894
895
|
import { EventEmitter } from "events";
|
|
895
896
|
import * as sysPath2 from "path";
|
|
896
897
|
|
|
897
|
-
//
|
|
898
|
+
// ../node_modules/readdirp/esm/index.js
|
|
898
899
|
import { stat as stat2, lstat, readdir, realpath } from "fs/promises";
|
|
899
900
|
import { Readable } from "stream";
|
|
900
901
|
import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "path";
|
|
@@ -1113,7 +1114,7 @@ function readdirp(root, options = {}) {
|
|
|
1113
1114
|
return new ReaddirpStream(options);
|
|
1114
1115
|
}
|
|
1115
1116
|
|
|
1116
|
-
//
|
|
1117
|
+
// ../node_modules/chokidar/esm/handler.js
|
|
1117
1118
|
import { watchFile, unwatchFile, watch as fs_watch } from "fs";
|
|
1118
1119
|
import { open, stat as stat3, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
|
|
1119
1120
|
import * as sysPath from "path";
|
|
@@ -1869,7 +1870,7 @@ var NodeFsHandler = class {
|
|
|
1869
1870
|
}
|
|
1870
1871
|
};
|
|
1871
1872
|
|
|
1872
|
-
//
|
|
1873
|
+
// ../node_modules/chokidar/esm/index.js
|
|
1873
1874
|
var SLASH = "/";
|
|
1874
1875
|
var SLASH_SLASH = "//";
|
|
1875
1876
|
var ONE_DOT = ".";
|
|
@@ -3829,6 +3830,8 @@ Power-user / debugging:
|
|
|
3829
3830
|
ingest <file...> Ingest one or more NDJSON backup files into the corpus
|
|
3830
3831
|
drain Ingest every file in <corpus>/inbox; discard clean
|
|
3831
3832
|
payloads and quarantine malformed ones
|
|
3833
|
+
retry-quarantine Retry quarantined files; retain repaired originals in
|
|
3834
|
+
<corpus>/inbox/recovered (close MCP clients first)
|
|
3832
3835
|
watch Watch <corpus>/inbox and ingest new deltas live
|
|
3833
3836
|
stats Print corpus counts (conversations, messages, platforms)
|
|
3834
3837
|
compact Losslessly compress conversations and remove redundant
|
|
@@ -4058,6 +4061,20 @@ async function main() {
|
|
|
4058
4061
|
}
|
|
4059
4062
|
return 0;
|
|
4060
4063
|
}
|
|
4064
|
+
case "retry-quarantine": {
|
|
4065
|
+
const index = await openSearchIndex(corpus);
|
|
4066
|
+
try {
|
|
4067
|
+
const report = await retryQuarantine(corpus, index);
|
|
4068
|
+
process.stdout.write(
|
|
4069
|
+
`Quarantine recovery: ${report.recovered} recovered, ${report.remaining} remaining, ${report.failed} failed.
|
|
4070
|
+
` + (report.archiveDir ? `Original files retained at ${report.archiveDir}
|
|
4071
|
+
` : "")
|
|
4072
|
+
);
|
|
4073
|
+
return report.remaining > 0 || report.failed > 0 ? 1 : 0;
|
|
4074
|
+
} finally {
|
|
4075
|
+
index.close();
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4061
4078
|
case "watch": {
|
|
4062
4079
|
process.stdout.write(`Watching ${corpus.paths.inboxDir} for new deltas (Ctrl+C to stop)\u2026
|
|
4063
4080
|
`);
|
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-GHKXNM7R.js";
|
|
7
7
|
|
|
8
8
|
// src/nodeDownload.ts
|
|
9
9
|
var NODE_DOWNLOAD_URLS = {
|
|
@@ -141,7 +141,7 @@ async function bootstrap() {
|
|
|
141
141
|
await guideUnsupportedNodeVersion(process.versions.node);
|
|
142
142
|
return 1;
|
|
143
143
|
}
|
|
144
|
-
const { main } = await import("./cli-
|
|
144
|
+
const { main } = await import("./cli-CMCIFSWH.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|
|
@@ -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
|