@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 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.1",
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. The files this build reads are left alone.
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 abandoned = /\.\d+\.tmp$/.test(name);
3464
- if (!stale && !abandoned) continue;
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, as of the last snapshot each serve wrote (a running build refreshes it every 30 seconds):");
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":