@appsoftwareltd/etherpk-mcp 0.4.1 → 0.4.3
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/dist/main.js +41 -16
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parse, stringify } from "yaml";
|
|
|
25
25
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26
26
|
var package_default = {
|
|
27
27
|
name: "@appsoftwareltd/etherpk-mcp",
|
|
28
|
-
version: "0.4.
|
|
28
|
+
version: "0.4.3",
|
|
29
29
|
license: "Elastic-2.0",
|
|
30
30
|
description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
|
|
31
31
|
type: "module",
|
|
@@ -3445,13 +3445,16 @@ async function loadLocalCache(dir, graphId) {
|
|
|
3445
3445
|
return 0;
|
|
3446
3446
|
}
|
|
3447
3447
|
}
|
|
3448
|
+
/** A `.tmp` younger than this may be a snapshot another process is still writing. */
|
|
3449
|
+
var ABANDONED_TMP_AFTER_MS = 10 * 6e4;
|
|
3448
3450
|
/**
|
|
3449
3451
|
* Remove what an older build or a killed write left in a graph's directory: index and vectors
|
|
3450
3452
|
* files stamped with another version (a bump renames them, so they are never opened again and
|
|
3451
3453
|
* were 52 MB of litter per graph), and `.tmp` files from an atomic write that never reached its
|
|
3452
|
-
* rename
|
|
3454
|
+
* rename - only once they are old enough that no live `serve` can be writing them. The files
|
|
3455
|
+
* this build reads are left alone.
|
|
3453
3456
|
*/
|
|
3454
|
-
async function removeStaleFiles(dir) {
|
|
3457
|
+
async function removeStaleFiles(dir, now = Date.now()) {
|
|
3455
3458
|
const keep = new Set([
|
|
3456
3459
|
cacheFile(dir),
|
|
3457
3460
|
indexFile(dir),
|
|
@@ -3460,8 +3463,11 @@ async function removeStaleFiles(dir) {
|
|
|
3460
3463
|
const removed = [];
|
|
3461
3464
|
for (const name of await readdir(dir).catch(() => [])) {
|
|
3462
3465
|
const stale = /^(index|vectors)\.v\d+\.sqlite$/.test(name) && !keep.has(name);
|
|
3463
|
-
const
|
|
3464
|
-
if (!stale && !
|
|
3466
|
+
const temp = /\.\d+\.tmp$/.test(name);
|
|
3467
|
+
if (!stale && !temp) continue;
|
|
3468
|
+
if (temp) {
|
|
3469
|
+
if (now - ((await stat(join(dir, name)).catch(() => null))?.mtimeMs ?? now) < ABANDONED_TMP_AFTER_MS) continue;
|
|
3470
|
+
}
|
|
3465
3471
|
await rm(join(dir, name), { force: true });
|
|
3466
3472
|
removed.push(name);
|
|
3467
3473
|
}
|
|
@@ -3469,7 +3475,8 @@ async function removeStaleFiles(dir) {
|
|
|
3469
3475
|
}
|
|
3470
3476
|
/** Hosts opened in this process, so each gets its own paths in sqlite-wasm's filesystem. */
|
|
3471
3477
|
var hostSerial = 0;
|
|
3472
|
-
function nodeIndexHost(dir) {
|
|
3478
|
+
function nodeIndexHost(dir, options = {}) {
|
|
3479
|
+
const tidy = options.tidy ?? true;
|
|
3473
3480
|
let sqlite3;
|
|
3474
3481
|
let live;
|
|
3475
3482
|
const path = indexFile(dir);
|
|
@@ -3502,7 +3509,7 @@ function nodeIndexHost(dir) {
|
|
|
3502
3509
|
}
|
|
3503
3510
|
} catch (error) {
|
|
3504
3511
|
console.error(`etherpk-mcp: ignoring an unreadable vectors file ${vectorsPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3505
|
-
await rm(vectorsPath, { force: true });
|
|
3512
|
+
if (tidy) await rm(vectorsPath, { force: true });
|
|
3506
3513
|
s.capi.sqlite3_js_posix_create_file(vectorsVfsName, new Uint8Array(0));
|
|
3507
3514
|
}
|
|
3508
3515
|
}
|
|
@@ -3529,7 +3536,7 @@ function nodeIndexHost(dir) {
|
|
|
3529
3536
|
const db = wrapOo1Db(oo1);
|
|
3530
3537
|
if (!isUsableIndex(db)) {
|
|
3531
3538
|
db.close();
|
|
3532
|
-
await rm(path, { force: true });
|
|
3539
|
+
if (tidy) await rm(path, { force: true });
|
|
3533
3540
|
return null;
|
|
3534
3541
|
}
|
|
3535
3542
|
attachVectors(db);
|
|
@@ -3539,14 +3546,14 @@ function nodeIndexHost(dir) {
|
|
|
3539
3546
|
};
|
|
3540
3547
|
} catch (error) {
|
|
3541
3548
|
console.error(`etherpk-mcp: ignoring an unreadable index file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3542
|
-
await rm(path, { force: true });
|
|
3549
|
+
if (tidy) await rm(path, { force: true });
|
|
3543
3550
|
return null;
|
|
3544
3551
|
}
|
|
3545
3552
|
}
|
|
3546
3553
|
return {
|
|
3547
3554
|
async open() {
|
|
3548
3555
|
const s = await init();
|
|
3549
|
-
await removeStaleFiles(dir);
|
|
3556
|
+
if (tidy) await removeStaleFiles(dir);
|
|
3550
3557
|
await importVectors(s);
|
|
3551
3558
|
live = await openSaved(s) ?? openFresh(s);
|
|
3552
3559
|
return {
|
|
@@ -3593,7 +3600,7 @@ async function listGraphStores(env, model) {
|
|
|
3593
3600
|
for (const graphId of await readdir(hostDir).catch(() => [])) {
|
|
3594
3601
|
const dir = join(hostDir, graphId);
|
|
3595
3602
|
if (!await exists$1(indexFile(dir))) continue;
|
|
3596
|
-
const opened = await nodeIndexHost(dir).open(graphId);
|
|
3603
|
+
const opened = await nodeIndexHost(dir, { tidy: false }).open(graphId);
|
|
3597
3604
|
try {
|
|
3598
3605
|
prepareEmbeddingStore(opened.db);
|
|
3599
3606
|
const status = semanticStatus(opened.db, model);
|
|
@@ -3612,6 +3619,22 @@ async function listGraphStores(env, model) {
|
|
|
3612
3619
|
}
|
|
3613
3620
|
return stores;
|
|
3614
3621
|
}
|
|
3622
|
+
/**
|
|
3623
|
+
* A store's state as one line a person can act on: a word first, then the counts, then how
|
|
3624
|
+
* fresh they are. A building `serve` refreshes the snapshot every 30 seconds, so a snapshot
|
|
3625
|
+
* under a minute and a half old means "building now" and an older one means nothing is - the
|
|
3626
|
+
* distinction the raw count and timestamp left the reader to infer (2026-09-17).
|
|
3627
|
+
*/
|
|
3628
|
+
function describeGraphStore(store, now = Date.now()) {
|
|
3629
|
+
const { embedded, total } = store.status;
|
|
3630
|
+
const ageMs = Math.max(0, now - store.updatedAt.getTime());
|
|
3631
|
+
const age = ageMs < 9e4 ? `${Math.round(ageMs / 1e3)} s ago` : ageMs < 36e5 ? `${Math.round(ageMs / 6e4)} min ago` : `${Math.round(ageMs / 36e5)} h ago`;
|
|
3632
|
+
const count = `${embedded.toLocaleString("en-GB")} of ${total.toLocaleString("en-GB")} passages`;
|
|
3633
|
+
if (total === 0) return `empty - no passages indexed here yet (snapshot ${age})`;
|
|
3634
|
+
if (embedded >= total) return `up to date - ${count} embedded (snapshot ${age})`;
|
|
3635
|
+
const percent = Math.floor(embedded / total * 100);
|
|
3636
|
+
return ageMs < 9e4 ? `building - ${count} (${percent}%) embedded, snapshot ${age}, refreshed every 30 s while it builds` : `paused - ${count} (${percent}%) embedded, last snapshot ${age}; nothing is building it now, it continues when serve next runs`;
|
|
3637
|
+
}
|
|
3615
3638
|
/** Remove every persisted graph under the cache root (logout of the last server). */
|
|
3616
3639
|
async function removeCacheRoot(env) {
|
|
3617
3640
|
await rm(cacheRoot(env), {
|
|
@@ -5617,6 +5640,10 @@ function createGraphSync(deps) {
|
|
|
5617
5640
|
for (const waiter of connectionWaiters) waiter.resolve();
|
|
5618
5641
|
connectionWaiters.clear();
|
|
5619
5642
|
subscribeRetained();
|
|
5643
|
+
if (deps.subscribeAll) sendNow({
|
|
5644
|
+
type: "subscribe",
|
|
5645
|
+
all: true
|
|
5646
|
+
});
|
|
5620
5647
|
for (const data of outboundSnapshots.splice(0)) s.send(data);
|
|
5621
5648
|
for (const docId of retained.keys()) {
|
|
5622
5649
|
if (docId === deps.rootDocId) continue;
|
|
@@ -8996,6 +9023,7 @@ async function openHeadlessGraph(deps) {
|
|
|
8996
9023
|
token: deps.token,
|
|
8997
9024
|
cache,
|
|
8998
9025
|
connect: deps.connect ?? nodeTransport,
|
|
9026
|
+
subscribeAll: true,
|
|
8999
9027
|
collectRowWhen: (doc) => documentProtection(doc.getText("content").toString()).kind === "document",
|
|
9000
9028
|
presence: {
|
|
9001
9029
|
name: deps.presenceName,
|
|
@@ -9841,11 +9869,8 @@ async function semanticCommand(what) {
|
|
|
9841
9869
|
const stores = await listGraphStores(process.env, MODEL.id);
|
|
9842
9870
|
if (stores.length === 0) return;
|
|
9843
9871
|
console.log("");
|
|
9844
|
-
console.log("Cached graphs
|
|
9845
|
-
for (const store of stores) {
|
|
9846
|
-
const done = store.status.embedded >= store.status.total;
|
|
9847
|
-
console.log(` ${store.host} ${store.graphId} ${store.status.embedded} of ${store.status.total} passages embedded${done ? " - up to date" : ""} (${store.updatedAt.toISOString()})`);
|
|
9848
|
-
}
|
|
9872
|
+
console.log("Cached graphs:");
|
|
9873
|
+
for (const store of stores) console.log(` ${store.host} ${store.graphId}\n ${describeGraphStore(store)}`);
|
|
9849
9874
|
return;
|
|
9850
9875
|
}
|
|
9851
9876
|
case "remove":
|