@appsoftwareltd/etherpk-mcp 0.8.3 → 0.8.4

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
@@ -34,7 +34,7 @@ import { request } from "node:https";
34
34
  import { BlockList, isIP } from "node:net";
35
35
  var package_default = {
36
36
  name: "@appsoftwareltd/etherpk-mcp",
37
- version: "0.8.3",
37
+ version: "0.8.4",
38
38
  license: "Elastic-2.0",
39
39
  description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
40
40
  type: "module",
@@ -613,11 +613,12 @@ function createGraphNamePublisher(deps) {
613
613
  //#endregion
614
614
  //#region ../client/src/lib/sync/sync-api.ts
615
615
  var SyncApiError = class extends Error {
616
- constructor(message, status, code, retryable = false) {
616
+ constructor(message, status, code, retryable = false, connectionKind) {
617
617
  super(message);
618
618
  this.status = status;
619
619
  this.code = code;
620
620
  this.retryable = retryable;
621
+ this.connectionKind = connectionKind;
621
622
  }
622
623
  };
623
624
  function createSyncApi(deps) {
@@ -637,7 +638,7 @@ function createSyncApi(deps) {
637
638
  const body = await res.json().catch(() => null);
638
639
  const nested = typeof body?.error === "object" && body.error !== null ? body.error : null;
639
640
  const code = nested?.code ?? body?.code;
640
- throw new SyncApiError(nested?.message ?? body?.code ?? `HTTP ${res.status}`, res.status, code, body?.retryable === true);
641
+ throw new SyncApiError(nested?.message ?? body?.code ?? `HTTP ${res.status}`, res.status, code, body?.retryable === true, deps.kind);
641
642
  }
642
643
  return await res.json();
643
644
  }
@@ -720,7 +721,7 @@ function createSyncApi(deps) {
720
721
  };
721
722
  }
722
723
  //#endregion
723
- //#region ../client/src/lib/sync/sync-config.ts
724
+ //#region ../client/src/lib/sync/sync-connections.ts
724
725
  /** Derive the `ws(s)://…/sync` relay URL from the server base URL. */
725
726
  function relayUrlFrom(serverBaseUrl) {
726
727
  return serverBaseUrl.replace(/\/$/, "").replace(/^http/, "ws") + "/sync";
@@ -4299,7 +4300,7 @@ async function openCacheDb() {
4299
4300
  const db = await request$1(indexedDB.open(CACHE_DB, CACHE_DB_VERSION));
4300
4301
  for (const store of CACHE_STORES) if (!db.objectStoreNames.contains(store)) {
4301
4302
  db.close();
4302
- throw new Error(`Local Cache database has no "${store}" store; open the graph cache before restoring it.`);
4303
+ throw new Error(`Local Cache database has no "${store}" store - open the graph cache before restoring it.`);
4303
4304
  }
4304
4305
  return db;
4305
4306
  }
@@ -4587,7 +4588,7 @@ function describeGraphStore(store, now = Date.now()) {
4587
4588
  if (total === 0) return `empty - no passages indexed here yet (snapshot ${age})`;
4588
4589
  if (embedded >= total) return `up to date - ${count} embedded (snapshot ${age})`;
4589
4590
  const percent = Math.floor(embedded / total * 100);
4590
- 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`;
4591
+ 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`;
4591
4592
  }
4592
4593
  /** Remove every persisted graph under the cache root (logout of the last server). */
4593
4594
  async function removeCacheRoot(env) {
@@ -4784,7 +4785,7 @@ async function setupSemantic(io) {
4784
4785
  const files = io.files ?? MODEL.files;
4785
4786
  const status = await semanticSetupStatus(io.env, files);
4786
4787
  if (!status.runtime) {
4787
- io.say(`Installing ${RUNTIME.name} ${RUNTIME.version} into ${status.runtimeDir} (about 300 MB; native, so it is not part of the etherpk-mcp package)…`);
4788
+ io.say(`Installing ${RUNTIME.name} ${RUNTIME.version} into ${status.runtimeDir} (about 300 MB - native, so it is not part of the etherpk-mcp package)…`);
4788
4789
  await mkdir(status.runtimeDir, {
4789
4790
  recursive: true,
4790
4791
  mode: 448
@@ -4857,7 +4858,7 @@ async function download(fetchFn, file, path) {
4857
4858
  const digest = hash.digest("hex");
4858
4859
  if (digest !== file.sha256) {
4859
4860
  await rm(temp, { force: true });
4860
- throw new Error(`${file.name} downloaded with sha256 ${digest}, expected ${file.sha256}; not kept.`);
4861
+ throw new Error(`${file.name} downloaded with sha256 ${digest}, expected ${file.sha256} - not kept.`);
4861
4862
  }
4862
4863
  await rename(temp, path);
4863
4864
  }
@@ -4879,7 +4880,7 @@ var SemanticUnavailable = class extends Error {
4879
4880
  this.name = "SemanticUnavailable";
4880
4881
  }
4881
4882
  };
4882
- var SETUP_HINT = "run `npx @appsoftwareltd/etherpk-mcp semantic setup` on this computer once (it installs a ~300 MB runtime and a 23 MB model into the cache directory); the next semantic search will use it, no restart needed.";
4883
+ var SETUP_HINT = "run `npx @appsoftwareltd/etherpk-mcp semantic setup` on this computer once (it installs a ~300 MB runtime and a 23 MB model into the cache directory) - the next semantic search will use it, no restart needed.";
4883
4884
  /** The thread count `serve` uses: the option, else the environment, else the conservative default. */
4884
4885
  function semanticThreads(env, requested) {
4885
4886
  if (requested !== void 0) return Math.max(1, Math.floor(requested));
@@ -4900,7 +4901,7 @@ async function loadEmbeddingModel(env, options = {}) {
4900
4901
  const loaded = await import(pathToFileURL(require.resolve(RUNTIME.name)).href);
4901
4902
  ort = loaded.default?.InferenceSession ? loaded.default : loaded;
4902
4903
  } catch (error) {
4903
- throw new SemanticUnavailable(`The embedding runtime in ${status.runtimeDir} failed to load (${error instanceof Error ? error.message : String(error)}); ${SETUP_HINT}`);
4904
+ throw new SemanticUnavailable(`The embedding runtime in ${status.runtimeDir} failed to load (${error instanceof Error ? error.message : String(error)}): ${SETUP_HINT}`);
4904
4905
  }
4905
4906
  const tokenizer = new Tokenizer(JSON.parse(await readFile(join(status.modelDir, "tokenizer.json"), "utf8")), JSON.parse(await readFile(join(status.modelDir, "tokenizer_config.json"), "utf8")));
4906
4907
  const threads = semanticThreads(env, options.threads);
@@ -6882,7 +6883,7 @@ function readPageDate(doc) {
6882
6883
  return { issues: [{
6883
6884
  level: "warning",
6884
6885
  code: "invalid-date",
6885
- message: `"${doc.concept}" has \`date: ${String(date)}\`, which is not a calendar day (YYYY-MM-DD); the document is undated.`,
6886
+ message: `"${doc.concept}" has \`date: ${String(date)}\`, which is not a calendar day (YYYY-MM-DD) - the document is undated.`,
6886
6887
  concept: doc.concept
6887
6888
  }] };
6888
6889
  }
@@ -6911,7 +6912,7 @@ function readPublicationDefinition(doc) {
6911
6912
  };
6912
6913
  }
6913
6914
  if (doc.kind === "journal") {
6914
- issue("publication-on-journal", "A journal entry cannot define a publication; use a page.");
6915
+ issue("publication-on-journal", "A journal entry cannot define a publication - use a page.");
6915
6916
  return {
6916
6917
  publication: null,
6917
6918
  issues
@@ -6944,7 +6945,7 @@ function readPublicationDefinition(doc) {
6944
6945
  else issues.push({
6945
6946
  level: "warning",
6946
6947
  code: "publication-invalid-include",
6947
- message: `Include \`${slot}\` must name the page that fills it; it is ignored.`,
6948
+ message: `Include \`${slot}\` must name the page that fills it - it is ignored.`,
6948
6949
  concept: doc.concept
6949
6950
  });
6950
6951
  }
@@ -7006,7 +7007,7 @@ function discoverPublications(documents) {
7006
7007
  for (const publication of list) issues.push({
7007
7008
  level: "error",
7008
7009
  code: "publication-duplicate-id",
7009
- message: `Two pages define the publication "${id}" (${list.map((p) => `"${p.concept}"`).join(" and ")}); neither is published until one is changed.`,
7010
+ message: `Two pages define the publication "${id}" (${list.map((p) => `"${p.concept}"`).join(" and ")}) - neither is published until one is changed.`,
7010
7011
  concept: publication.concept
7011
7012
  });
7012
7013
  }
@@ -7043,7 +7044,7 @@ function validateThemeManifest(raw) {
7043
7044
  const version = typeof data.version === "string" && data.version.trim() !== "" ? data.version.trim() : "0.0.0";
7044
7045
  const contract = typeof data.contract === "number" && Number.isInteger(data.contract) ? data.contract : null;
7045
7046
  if (contract === null) errors.push("theme.json needs an integer `contract`, the view version the theme was written for.");
7046
- else if (contract > 1) errors.push(`The theme was written for view contract ${contract}; this EtherPK writes contract 1. Update EtherPK, or use an older theme.`);
7047
+ else if (contract > 1) errors.push(`The theme was written for view contract ${contract} - this EtherPK writes contract 1. Update EtherPK, or use an older theme.`);
7047
7048
  const kinds = [];
7048
7049
  if (Array.isArray(data.kinds)) {
7049
7050
  for (const k of data.kinds) if (KINDS.has(k)) kinds.push(k);
@@ -11045,7 +11046,7 @@ var RenameUnconfirmedError = class extends Error {
11045
11046
  name = "RenameUnconfirmedError";
11046
11047
  constructor(concepts) {
11047
11048
  const list = concepts.slice(0, 5).join(", ") + (concepts.length > 5 ? "…" : "");
11048
- super(`${concepts.length === 1 ? "A document" : `${concepts.length} documents`} the rename would change ${concepts.length === 1 ? "has" : "have"} not finished syncing to this device: ${list}. Nothing was renamed; try again once sync has caught up.`);
11049
+ super(`${concepts.length === 1 ? "A document" : `${concepts.length} documents`} the rename would change ${concepts.length === 1 ? "has" : "have"} not finished syncing to this device: ${list}. Nothing was renamed - try again once sync has caught up.`);
11049
11050
  this.concepts = concepts;
11050
11051
  }
11051
11052
  };
@@ -13790,7 +13791,7 @@ async function openHeadlessFolder(deps) {
13790
13791
  for (const conflict of raised) {
13791
13792
  await store.resolveConflict(conflict.target, "take-disk");
13792
13793
  saveErrors.delete(conceptKey(conflict.target));
13793
- warn(`"${conflict.target}" changed on disk while an edit to it could not be written; took the file on disk and discarded the edit.`);
13794
+ warn(`"${conflict.target}" changed on disk while an edit to it could not be written - took the file on disk and discarded the edit.`);
13794
13795
  }
13795
13796
  })();
13796
13797
  const pass = reconciling;
@@ -13969,7 +13970,7 @@ async function openHeadlessFolder(deps) {
13969
13970
  /** The account has no vault yet, so there is nothing for a Recovery Code to open. */
13970
13971
  var NoVaultError = class extends Error {
13971
13972
  constructor() {
13972
- super("This account has no encryption keys yet; there is nothing for a Recovery Code to open.");
13973
+ super("This account has no encryption keys yet - there is nothing for a Recovery Code to open.");
13973
13974
  this.name = "NoVaultError";
13974
13975
  }
13975
13976
  };
@@ -13985,7 +13986,7 @@ async function openVaultWithRecoveryCode(api, code) {
13985
13986
  try {
13986
13987
  return (await openVault(fromBase64Url(stored.vault), wrapKey)).vaultKey;
13987
13988
  } catch (error) {
13988
- if (error instanceof EnvelopeError) throw new RecoveryCodeError("That Recovery Code does not open this account’s keys. Check it character by character; only the most recently issued code works.");
13989
+ if (error instanceof EnvelopeError) throw new RecoveryCodeError("That Recovery Code does not open this account’s keys. Check it character by character - only the most recently issued code works.");
13989
13990
  throw error;
13990
13991
  }
13991
13992
  }
@@ -14114,7 +14115,7 @@ async function unlockByDeviceApproval(api, io) {
14114
14115
  const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : "open EtherPK in a browser";
14115
14116
  io.say("");
14116
14117
  io.say(`To approve this device, ${where} (any page - it need not be a note), connected to this account`);
14117
- io.say("with its keys unlocked. A prompt will show a code; confirm it matches this one:");
14118
+ io.say("with its keys unlocked. A prompt will show a code - confirm it matches this one:");
14118
14119
  io.say("");
14119
14120
  io.say(` ${request.sas}`);
14120
14121
  io.say("");
@@ -14337,7 +14338,7 @@ var files = /* @__PURE__ */ Object.assign({
14337
14338
  "../etherpk-al-folio/assets/theme.js": "/*\n * The light / dark toggle: a `.theme-toggle` button flips the page between the two and the\n * choice is remembered in this browser; with no choice made the page follows the system. The\n * stylesheet keys on `data-theme` on <html>, and shell-top applies the remembered choice before\n * the first paint so a dark reader never sees a light flash.\n */\n(function () {\n 'use strict';\n var KEY = 'etherpk-theme';\n var root = document.documentElement;\n function isDark() {\n var chosen = root.getAttribute('data-theme');\n if (chosen === 'dark' || chosen === 'light') return chosen === 'dark';\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n function apply(theme) {\n if (theme === 'dark' || theme === 'light') root.setAttribute('data-theme', theme);\n else root.removeAttribute('data-theme');\n var buttons = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('aria-pressed', isDark() ? 'true' : 'false');\n }\n try { apply(localStorage.getItem(KEY)); } catch (_e) { apply(null); }\n var toggles = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < toggles.length; i++) {\n toggles[i].addEventListener('click', function () {\n var next = isDark() ? 'light' : 'dark';\n apply(next);\n try { localStorage.setItem(KEY, next); } catch (_e) { /* private mode: the choice lasts the page */ }\n });\n }\n})();\n",
14338
14339
  "../etherpk-al-folio/layouts/404.html": "{{> shell-top}}\n <div class=\"post\">\n <header class=\"post-header\"><h1 class=\"post-title\">Page not found</h1></header>\n <div class=\"post-content\">\n <p>The page you followed a link to is not part of this site. <a href=\"index.html\">Back to {{site.title}}</a>.</p>\n </div>\n </div>\n{{> shell-bottom}}\n",
14339
14340
  "../etherpk-al-folio/layouts/archive.html": "{{> shell-top}}\n <div class=\"post\">\n <header class=\"post-header\"><h1 class=\"post-title\">{{#page.isPostsArchive}}posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}journal{{/page.isJournalArchive}}</h1></header>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <h2 class=\"archive-year\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"post-list archive-list\">\n {{#posts}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a></li>{{/posts}}\n </ul>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <ul class=\"post-list\">\n {{#site.journals}}\n <li>\n <h3><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n <p class=\"post-meta\"><time datetime=\"{{date}}\">{{date}}</time></p>\n </li>\n {{/site.journals}}\n </ul>\n {{/page.isJournalArchive}}\n </div>\n{{> shell-bottom}}\n",
14340
- "../etherpk-al-folio/layouts/home.html": "{{> shell-top}}\n <article class=\"post about\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{^page.isGeneratedIndex}}{{{page.titleHtml}}}{{/page.isGeneratedIndex}}{{#page.isGeneratedIndex}}{{site.title}}{{/page.isGeneratedIndex}}</h1>\n </header>\n {{^page.isGeneratedIndex}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}{{^site.isBlog}}\n <div class=\"post-content page-contents\">\n{{{page.content}}}\n </div>\n {{/site.isBlog}}{{/page.isGeneratedIndex}}\n {{#site.hasJournals}}\n <section class=\"news\">\n <h2><a href=\"journal.html\">news</a></h2>\n <table class=\"news-table\">\n {{#site.journals}}\n <tr>\n <th scope=\"row\"><time datetime=\"{{date}}\">{{date}}</time></th>\n <td><a href=\"{{url}}\">{{title}}</a>{{#excerpt}} <span class=\"news-excerpt\">{{excerpt}}</span>{{/excerpt}}</td>\n </tr>\n {{/site.journals}}\n </table>\n </section>\n {{/site.hasJournals}}\n {{#site.isBlog}}\n <section class=\"posts\">\n <h2>posts</h2>\n {{#site.hasPosts}}\n <ul class=\"post-list\">\n {{#site.recentPosts}}\n <li>\n <h3><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n <p class=\"post-meta\"><time datetime=\"{{date}}\">{{date}}</time></p>\n </li>\n {{/site.recentPosts}}\n </ul>\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day; a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}\n </section>\n {{/site.isBlog}}\n </article>\n{{> shell-bottom}}\n",
14341
+ "../etherpk-al-folio/layouts/home.html": "{{> shell-top}}\n <article class=\"post about\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{^page.isGeneratedIndex}}{{{page.titleHtml}}}{{/page.isGeneratedIndex}}{{#page.isGeneratedIndex}}{{site.title}}{{/page.isGeneratedIndex}}</h1>\n </header>\n {{^page.isGeneratedIndex}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}{{^site.isBlog}}\n <div class=\"post-content page-contents\">\n{{{page.content}}}\n </div>\n {{/site.isBlog}}{{/page.isGeneratedIndex}}\n {{#site.hasJournals}}\n <section class=\"news\">\n <h2><a href=\"journal.html\">news</a></h2>\n <table class=\"news-table\">\n {{#site.journals}}\n <tr>\n <th scope=\"row\"><time datetime=\"{{date}}\">{{date}}</time></th>\n <td><a href=\"{{url}}\">{{title}}</a>{{#excerpt}} <span class=\"news-excerpt\">{{excerpt}}</span>{{/excerpt}}</td>\n </tr>\n {{/site.journals}}\n </table>\n </section>\n {{/site.hasJournals}}\n {{#site.isBlog}}\n <section class=\"posts\">\n <h2>posts</h2>\n {{#site.hasPosts}}\n <ul class=\"post-list\">\n {{#site.recentPosts}}\n <li>\n <h3><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n <p class=\"post-meta\"><time datetime=\"{{date}}\">{{date}}</time></p>\n </li>\n {{/site.recentPosts}}\n </ul>\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}\n </section>\n {{/site.isBlog}}\n </article>\n{{> shell-bottom}}\n",
14341
14342
  "../etherpk-al-folio/layouts/journal.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <p class=\"post-meta\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}{{page.readingMinutes}} min read</p>\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"post-nav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"older\" href=\"{{url}}\">← {{title}}</a>{{/page.older}}\n {{#page.newer}}<a class=\"newer\" href=\"{{url}}\">{{title}} →</a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
14342
14343
  "../etherpk-al-folio/layouts/page.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <p class=\"post-meta\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}{{page.readingMinutes}} min read</p>\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"post-nav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"older\" href=\"{{url}}\">← {{title}}</a>{{/page.older}}\n {{#page.newer}}<a class=\"newer\" href=\"{{url}}\">{{title}} →</a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
14343
14344
  "../etherpk-al-folio/partials/after-content.html": "",
@@ -14358,7 +14359,7 @@ var files = /* @__PURE__ */ Object.assign({
14358
14359
  "../etherpk-blog/assets/theme.css": "/* EtherPK blog theme. Tokens follow the app's own (agent blue, the gk surface scale); the font\n is the system stack so the site loads nothing from anywhere. Dark mode follows the reader's\n preference. Classes the publisher emits and this file styles: .wikilink, .wikilink-missing,\n .task, .task-tag, .toc, .backlinks, .site-nav-list, .diagram, .math, .tok-* (code). */\n\n:root {\n --font-sans: \"Inter\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n --agent-500: #007acc;\n --agent-600: #005f9e;\n --agent-100: #c2e3ff;\n --surface-0: #ffffff;\n --surface-1: #f9fafb;\n --surface-2: #e5e7eb;\n --text-strong: #030712;\n --text-default: #374151;\n --text-muted: #6b7280;\n --border-soft: rgba(3, 7, 18, 0.08);\n --border-strong: rgba(3, 7, 18, 0.16);\n --mark: #fff3a3;\n --missing: #b45309;\n --nav-width: 17rem;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --agent-500: #5ab3ff;\n --agent-600: #94ccff;\n --agent-100: #002e4f;\n --surface-0: #18181b;\n --surface-1: #1c1c20;\n --surface-2: #2a2a30;\n --text-strong: #fafafa;\n --text-default: #d4d4d8;\n --text-muted: #a1a1aa;\n --border-soft: rgba(255, 255, 255, 0.08);\n --border-strong: rgba(255, 255, 255, 0.16);\n --mark: #6b5a00;\n --missing: #f59e0b;\n }\n}\n\n*, *::before, *::after { box-sizing: border-box; }\nhtml { font-family: var(--font-sans); -webkit-font-smoothing: antialiased; }\nbody {\n margin: 0;\n color: var(--text-default);\n background: var(--surface-0);\n line-height: 1.6;\n font-size: 16px;\n}\na { color: var(--agent-500); }\na:hover { color: var(--agent-600); }\n.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.skip-link { position: absolute; left: -999px; top: 0; background: var(--surface-1); padding: 0.5rem 1rem; z-index: 100; }\n.skip-link:focus { left: 1rem; top: 1rem; }\n\n/* -- Header ----------------------------------------------------------------------------- */\n.site-header {\n display: flex;\n align-items: center;\n gap: 1rem;\n padding: 0.75rem 1.5rem;\n border-bottom: 1px solid var(--border-soft);\n background: var(--surface-1);\n position: sticky;\n top: 0;\n z-index: 20;\n}\n.site-title { display: inline-flex; align-items: center; gap: 0.5rem; min-width: 0; font-weight: 600; color: var(--text-strong); text-decoration: none; font-size: 1.05rem; }\n.site-title:hover { color: var(--agent-500); }\n.site-logo { flex-shrink: 0; }\n.site-title-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.site-search { position: relative; margin-left: auto; flex: 0 1 22rem; }\n.site-search input {\n width: 100%;\n padding: 0.45rem 0.75rem;\n border: 1px solid var(--border-strong);\n border-radius: 0.5rem;\n background: var(--surface-0);\n color: var(--text-default);\n font: inherit;\n}\n.site-search input:focus { outline: 2px solid var(--agent-500); outline-offset: 1px; }\n.site-search-results {\n position: absolute;\n top: calc(100% + 0.35rem);\n left: 0;\n right: 0;\n max-height: 24rem;\n overflow: auto;\n background: var(--surface-0);\n border: 1px solid var(--border-strong);\n border-radius: 0.5rem;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);\n z-index: 30;\n}\n.site-search-hit { display: block; padding: 0.55rem 0.75rem; text-decoration: none; color: var(--text-default); border-bottom: 1px solid var(--border-soft); }\n.site-search-hit:hover, .site-search-hit:focus { background: var(--surface-1); }\n.site-search-title { display: block; font-weight: 600; color: var(--text-strong); }\n.site-search-snippet { display: block; font-size: 0.9rem; color: var(--text-muted); }\n.site-search-empty { margin: 0; padding: 0.6rem 0.75rem; color: var(--text-muted); }\n\n/* -- Shell: top navigation, centred column --------------------------------------------- */\n.site-header { display: block; padding: 1rem 1.5rem 0; position: static; }\n.site-header-row { display: flex; align-items: center; gap: 1rem; max-width: 46rem; margin: 0 auto; }\n.site-title { font-size: 1.35rem; }\n.site-nav { max-width: 46rem; margin: 0.75rem auto 0; }\n.site-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; }\n.site-nav-list .site-nav-list { display: block; padding-left: 0.75rem; }\n.site-nav a, .site-nav .nav-group-label { color: var(--text-default); text-decoration: none; font-size: 0.95rem; }\n.site-nav a:hover { color: var(--text-strong); }\n.site-nav .nav-current > a { color: var(--agent-500); font-weight: 600; }\n.site-nav .nav-group-label { font-weight: 600; color: var(--text-strong); }\n.site-nav a .wikilink { color: inherit; text-decoration: none; }\n.nav-toggle, .nav-backdrop { display: none; }\n\n.content { padding: 2rem 1.5rem 4rem; max-width: 46rem; margin: 0 auto; }\n.post-header h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 0.5rem; color: var(--text-strong); }\n.post-header h1 .wikilink { color: inherit; text-decoration: none; }\n.post-header h1 .wikilink:hover { text-decoration: underline; }\n.post-date { color: var(--text-muted); font-size: 0.95rem; }\n.post-body h2 { margin-top: 2rem; font-size: 1.4rem; color: var(--text-strong); }\n.post-body h3 { margin-top: 1.5rem; font-size: 1.15rem; color: var(--text-strong); }\n.post-body ul { padding-left: 1.4rem; }\n.post-body li { margin: 0.2rem 0; }\n.post-body img { max-width: 100%; height: auto; border-radius: 0.35rem; }\n.post-body table { border-collapse: collapse; margin: 1rem 0; width: 100%; }\n.post-body th, .post-body td { border: 1px solid var(--border-strong); padding: 0.4rem 0.6rem; text-align: left; }\n.post-body th { background: var(--surface-1); }\n.post-body blockquote { border-left: 3px solid var(--border-strong); margin: 1rem 0; padding: 0.25rem 1rem; color: var(--text-muted); }\n.post-body code { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-1); padding: 0.1em 0.35em; border-radius: 0.25rem; }\n.post-body pre { background: var(--surface-1); border: 1px solid var(--border-soft); border-radius: 0.5rem; padding: 0.9rem 1rem; overflow-x: auto; }\n.post-body pre code { background: none; padding: 0; font-size: 0.88rem; }\nmark { background: var(--mark); color: inherit; padding: 0 0.15em; border-radius: 0.2em; }\n\n/* Wikilinks, as the editor colours them. A missing target is visibly missing. */\na.wikilink { text-decoration: none; border-bottom: 1px solid var(--agent-100); }\na.wikilink:hover { border-bottom-color: var(--agent-500); }\na.wikilink-missing { color: var(--missing); text-decoration: none; border-bottom: 1px dashed var(--missing); }\n\n/* Tasks and their badges. */\nli.task { list-style: none; margin-left: -1.4rem; padding-left: 0; }\nli.task input[type=\"checkbox\"] { margin-right: 0.35rem; vertical-align: -0.1em; }\nli.task.done { color: var(--text-muted); }\n.task-tag { display: inline-block; font-size: 0.8rem; font-weight: 600; padding: 0.05em 0.5em; border-radius: 999px; background: var(--surface-2); color: var(--text-strong); margin-right: 0.25rem; vertical-align: 0.1em; }\n.task-tag.priority-1 { background: #fee2e2; color: #991b1b; }\n.task-tag.priority-2 { background: #ffedd5; color: #9a3412; }\n.task-tag.priority-3 { background: #fef9c3; color: #854d0e; }\n.task-tag.waiting, .task-tag.doing { background: var(--agent-100); color: var(--agent-600); }\n.task-tag.cancelled { text-decoration: line-through; }\n\n/* Table of contents, diagrams, maths. */\n.toc { margin: 0 0 1.5rem; font-size: 0.95rem; }\n.toc summary { cursor: pointer; font-weight: 600; color: var(--text-strong); }\n.toc ul { list-style: none; padding-left: 0; margin: 0.4rem 0 0; }\n.toc li.toc-level-3 { padding-left: 1rem; }\n.toc li.toc-level-4 { padding-left: 2rem; }\n.diagram { margin: 1rem 0; text-align: center; }\n.diagram svg { max-width: 100%; height: auto; }\n.math-block { overflow-x: auto; margin: 1rem 0; }\n\n/* Linked references, beneath the content. */\n.backlinks { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border-soft); }\n.backlinks h2 { font-size: 1.1rem; color: var(--text-strong); margin: 0 0 0.75rem; }\n.backlink-list { list-style: none; padding: 0; margin: 0; }\n.backlink-source { margin: 0 0 0.9rem; }\n.backlink-source > a { font-weight: 600; }\n.backlink-source ul { list-style: none; padding-left: 1rem; margin: 0.25rem 0 0; border-left: 2px solid var(--border-soft); }\n.backlink-context { color: var(--text-muted); font-size: 0.95rem; margin: 0.15rem 0; }\n\n/* Archive and index lists. */\n.journal-archive, .page-index-list { list-style: none; padding: 0; }\n.journal-archive li { margin: 0 0 1rem; }\n.journal-archive time { color: var(--text-muted); margin-right: 0.5rem; font-variant-numeric: tabular-nums; }\n.archive-excerpt { margin: 0.2rem 0 0; color: var(--text-muted); }\n.archive-year { margin: 1.5rem 0 0.5rem; font-size: 1.1rem; }\n.all-posts { margin: 1.5rem 0 0; }\n\n.site-footer { border-top: 1px solid var(--border-soft); padding: 1rem 1.5rem; color: var(--text-muted); font-size: 0.9rem; text-align: center; }\n\n/* Code tokens (lezer classHighlighter names). */\n.tok-keyword { color: #0a7; }\n.tok-string, .tok-string2 { color: #690; }\n.tok-comment, .tok-lineComment, .tok-blockComment { color: #999; font-style: italic; }\n.tok-number, .tok-bool, .tok-null, .tok-atom { color: #905; }\n.tok-function, .tok-definition.tok-variableName { color: #c60; }\n.tok-typeName, .tok-className, .tok-namespace { color: #b58900; }\n.tok-operator, .tok-punctuation { color: #555; }\n.tok-propertyName, .tok-attributeName { color: #268bd2; }\n.tok-tagName { color: #22863a; }\n.tok-invalid { color: #d00; }\n@media (prefers-color-scheme: dark) {\n .tok-keyword { color: #4ec9b0; }\n .tok-string, .tok-string2 { color: #b5cea8; }\n .tok-comment, .tok-lineComment, .tok-blockComment { color: #7f848e; }\n .tok-number, .tok-bool, .tok-null, .tok-atom { color: #d19a66; }\n .tok-function, .tok-definition.tok-variableName { color: #dcdcaa; }\n .tok-typeName, .tok-className, .tok-namespace { color: #e5c07b; }\n .tok-operator, .tok-punctuation { color: #abb2bf; }\n .tok-propertyName, .tok-attributeName { color: #61afef; }\n .tok-tagName { color: #98c379; }\n}\n\n/* -- Posts ------------------------------------------------------------------------------ */\n.post-summary { margin: 0 0 2rem; }\n.post-summary h2 { margin: 0 0 0.2rem; font-size: 1.35rem; }\n.post-summary h2 a { color: var(--text-strong); text-decoration: none; }\n.post-summary h2 a:hover { color: var(--agent-500); }\n.post-summary time { color: var(--text-muted); font-size: 0.9rem; font-variant-numeric: tabular-nums; }\n.post-summary p { margin: 0.4rem 0 0; color: var(--text-default); }\n.home-intro { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-soft); }\n.post-list-empty { color: var(--text-muted); }\n@media (max-width: 40rem) {\n .site-header-row { flex-wrap: wrap; }\n .site-search { flex-basis: 100%; margin-left: 0; }\n .content { padding: 1.25rem 1rem 3rem; }\n}\n",
14359
14360
  "../etherpk-blog/layouts/404.html": "{{> shell-top}}\n <article class=\"post not-published\">\n <header class=\"post-header\">\n <h1>Not published</h1>\n </header>\n <div class=\"post-body\">\n <p>The page you followed a link to is not part of this site.</p>\n <p><a href=\"index.html\">Back to {{site.title}}</a></p>\n </div>\n </article>\n{{> shell-bottom}}\n",
14360
14361
  "../etherpk-blog/layouts/archive.html": "{{> shell-top}}\n <article class=\"post archive\">\n <header class=\"post-header\">\n <h1>{{#page.isPostsArchive}}Posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}Journal{{/page.isJournalArchive}}</h1>\n </header>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <h2 class=\"archive-year\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"journal-archive\">\n {{#posts}}\n <li>\n <time datetime=\"{{date}}\">{{date}}</time>\n <a href=\"{{url}}\">{{title}}</a>\n </li>\n {{/posts}}\n </ul>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <ul class=\"journal-archive\">\n {{#site.journals}}\n <li>\n <time datetime=\"{{date}}\">{{date}}</time>\n <a href=\"{{url}}\">{{title}}</a>\n {{#excerpt}}<p class=\"archive-excerpt\">{{excerpt}}</p>{{/excerpt}}\n </li>\n {{/site.journals}}\n </ul>\n {{/page.isJournalArchive}}\n </article>\n{{> shell-bottom}}\n",
14361
- "../etherpk-blog/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <section class=\"home-intro\">\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </section>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"Posts\">\n {{#site.hasPosts}}\n {{#site.recentPosts}}\n <article class=\"post-summary\">\n <h2><a href=\"{{url}}\">{{title}}</a></h2>\n <time datetime=\"{{date}}\">{{date}}</time>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n </article>\n {{/site.recentPosts}}\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}\n <p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day; a page by a <code>date:</code> line in its frontmatter.</p>\n {{/site.hasPosts}}\n </section>\n{{> shell-bottom}}\n",
14362
+ "../etherpk-blog/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <section class=\"home-intro\">\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </section>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"Posts\">\n {{#site.hasPosts}}\n {{#site.recentPosts}}\n <article class=\"post-summary\">\n <h2><a href=\"{{url}}\">{{title}}</a></h2>\n <time datetime=\"{{date}}\">{{date}}</time>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n </article>\n {{/site.recentPosts}}\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}\n <p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>\n {{/site.hasPosts}}\n </section>\n{{> shell-bottom}}\n",
14362
14363
  "../etherpk-blog/layouts/journal.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1>{{{page.titleHtml}}}</h1>\n {{#page.hasDate}}<time class=\"post-date\" datetime=\"{{page.date}}\">{{page.date}}</time>{{/page.hasDate}}\n </header>\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </article>\n{{> shell-bottom}}\n",
14363
14364
  "../etherpk-blog/layouts/page.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1>{{{page.titleHtml}}}</h1>\n {{#page.hasDate}}<time class=\"post-date\" datetime=\"{{page.date}}\">{{page.date}}</time>{{/page.hasDate}}\n </header>\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </article>\n{{> shell-bottom}}\n",
14364
14365
  "../etherpk-blog/partials/after-content.html": "",
@@ -14386,7 +14387,7 @@ var files = /* @__PURE__ */ Object.assign({
14386
14387
  "../etherpk-docs/partials/before-content.html": "",
14387
14388
  "../etherpk-docs/partials/footer.html": " <footer class=\"site-footer\">\n <p>{{site.title}} · published with <a href=\"https://etherpk.com\" rel=\"noopener\">EtherPK</a></p>\n </footer>\n",
14388
14389
  "../etherpk-docs/partials/head.html": "",
14389
- "../etherpk-docs/partials/header.html": " <header class=\"site-header\">\n <a class=\"site-title\" href=\"index.html\">{{> logo}}<span class=\"site-title-text\">{{site.title}}</span></a>\n <form class=\"site-search\" role=\"search\" data-search-index=\"{{site.search.url}}\" data-search-script=\"{{site.search.scriptUrl}}\">\n <label class=\"visually-hidden\" for=\"site-search-input\">Search</label>\n <input id=\"site-search-input\" type=\"search\" name=\"q\" placeholder=\"Search\" autocomplete=\"off\" aria-describedby=\"site-search-hint\">\n <span id=\"site-search-hint\" class=\"visually-hidden\">Type to search the published pages; results appear below the box.</span>\n <div class=\"site-search-results\" role=\"listbox\" hidden></div>\n </form>\n </header>\n",
14390
+ "../etherpk-docs/partials/header.html": " <header class=\"site-header\">\n <a class=\"site-title\" href=\"index.html\">{{> logo}}<span class=\"site-title-text\">{{site.title}}</span></a>\n <form class=\"site-search\" role=\"search\" data-search-index=\"{{site.search.url}}\" data-search-script=\"{{site.search.scriptUrl}}\">\n <label class=\"visually-hidden\" for=\"site-search-input\">Search</label>\n <input id=\"site-search-input\" type=\"search\" name=\"q\" placeholder=\"Search\" autocomplete=\"off\" aria-describedby=\"site-search-hint\">\n <span id=\"site-search-hint\" class=\"visually-hidden\">Type to search the published pages - results appear below the box.</span>\n <div class=\"site-search-results\" role=\"listbox\" hidden></div>\n </form>\n </header>\n",
14390
14391
  "../etherpk-docs/partials/logo.html": "<!-- The EtherPK icon (resources/brand/etherpk-icon-*.svg), inline so the page fetches nothing and it takes the text colour; aria-hidden because the title beside it is the name. Fill the \"logo\" include, or edit this partial, for your own. -->\n<svg class=\"site-logo\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 191.82 166.13\" width=\"24\" height=\"21\" aria-hidden=\"true\" focusable=\"false\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M67.09,132.92,47.93,166.13H9.59L0,149.52,86.33,0H105.5L19.17,149.52H38.34l19.17-33.2h76.8l9.59,16.6Zm48-83.12,9.65,16.71H143.9L115.08,16.6q-24,41.57-48,83.12H143.9q14.37,24.9,28.76,49.8h-96l-9.59,16.61H182.24l9.58-16.61-38.34-66.4H96C95.85,82.9,113.39,52.73,115.08,49.8Z\"/></svg>\n",
14391
14392
  "../etherpk-docs/partials/nav-item.html": " <li class=\"{{#current}}nav-current {{/current}}{{#open}}nav-open {{/open}}{{^href}}nav-group{{/href}}\">\n {{#href}}<a href=\"{{href}}\"{{#current}} aria-current=\"page\"{{/current}}{{#external}} rel=\"noopener\"{{/external}}>{{label}}</a>{{/href}}\n {{^href}}<span class=\"nav-group-label\">{{{labelHtml}}}</span>{{/href}}\n {{#hasChildren}}<ul class=\"site-nav-list\">{{#children}}{{> nav-item}}{{/children}}</ul>{{/hasChildren}}\n </li>\n",
14392
14393
  "../etherpk-docs/partials/nav.html": " <nav id=\"site-nav\" class=\"site-nav\" aria-label=\"Site\">\n <ul class=\"site-nav-list\">\n{{#nav}}{{> nav-item}}{{/nav}}\n </ul>\n </nav>\n",
@@ -14401,7 +14402,7 @@ var files = /* @__PURE__ */ Object.assign({
14401
14402
  "../etherpk-papermod/assets/theme.js": "/*\n * The light / dark toggle: a `.theme-toggle` button flips the page between the two and the\n * choice is remembered in this browser; with no choice made the page follows the system. The\n * stylesheet keys on `data-theme` on <html>, and shell-top applies the remembered choice before\n * the first paint so a dark reader never sees a light flash.\n */\n(function () {\n 'use strict';\n var KEY = 'etherpk-theme';\n var root = document.documentElement;\n function isDark() {\n var chosen = root.getAttribute('data-theme');\n if (chosen === 'dark' || chosen === 'light') return chosen === 'dark';\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n function apply(theme) {\n if (theme === 'dark' || theme === 'light') root.setAttribute('data-theme', theme);\n else root.removeAttribute('data-theme');\n var buttons = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('aria-pressed', isDark() ? 'true' : 'false');\n }\n try { apply(localStorage.getItem(KEY)); } catch (_e) { apply(null); }\n var toggles = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < toggles.length; i++) {\n toggles[i].addEventListener('click', function () {\n var next = isDark() ? 'light' : 'dark';\n apply(next);\n try { localStorage.setItem(KEY, next); } catch (_e) { /* private mode: the choice lasts the page */ }\n });\n }\n})();\n",
14402
14403
  "../etherpk-papermod/layouts/404.html": "{{> shell-top}}\n <div class=\"not-found\">\n <h1>404</h1>\n <p>This page is not published. <a href=\"index.html\">Back to {{site.title}}</a>.</p>\n </div>\n{{> shell-bottom}}\n",
14403
14404
  "../etherpk-papermod/layouts/archive.html": "{{> shell-top}}\n <header class=\"page-header\"><h1>{{#page.isPostsArchive}}Posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}Journal{{/page.isJournalArchive}}</h1></header>\n <div class=\"archive-posts\">\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <div class=\"archive-year\">\n <h2 class=\"archive-year-header\" id=\"y{{year}}\">{{year}}</h2>\n {{#posts}}\n <div class=\"archive-entry\">\n <h3 class=\"archive-entry-title\"><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<div class=\"archive-entry-content\">{{excerpt}}</div>{{/excerpt}}\n <footer class=\"entry-footer\"><time datetime=\"{{date}}\">{{date}}</time></footer>\n </div>\n {{/posts}}\n </div>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n {{#site.journals}}\n <div class=\"archive-entry\">\n <h3 class=\"archive-entry-title\"><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<div class=\"archive-entry-content\">{{excerpt}}</div>{{/excerpt}}\n <footer class=\"entry-footer\"><time datetime=\"{{date}}\">{{date}}</time></footer>\n </div>\n {{/site.journals}}\n {{/page.isJournalArchive}}\n </div>\n{{> shell-bottom}}\n",
14404
- "../etherpk-papermod/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post-single home-info\">\n <header class=\"post-header\"><h1 class=\"post-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <header class=\"page-header\"><h1>{{site.title}}</h1></header>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day; a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<div class=\"post-content page-contents\">{{{page.content}}}</div>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </section>\n{{> shell-bottom}}\n",
14405
+ "../etherpk-papermod/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post-single home-info\">\n <header class=\"post-header\"><h1 class=\"post-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <header class=\"page-header\"><h1>{{site.title}}</h1></header>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<div class=\"post-content page-contents\">{{{page.content}}}</div>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </section>\n{{> shell-bottom}}\n",
14405
14406
  "../etherpk-papermod/layouts/journal.html": "{{> shell-top}}\n <article class=\"post-single\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <footer class=\"post-footer\">\n <nav class=\"paginav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"prev\" href=\"{{url}}\"><span class=\"title\">« Prev</span><br><span>{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"next\" href=\"{{url}}\"><span class=\"title\">Next »</span><br><span>{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
14406
14407
  "../etherpk-papermod/layouts/page.html": "{{> shell-top}}\n <article class=\"post-single\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <footer class=\"post-footer\">\n <nav class=\"paginav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"prev\" href=\"{{url}}\"><span class=\"title\">« Prev</span><br><span>{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"next\" href=\"{{url}}\"><span class=\"title\">Next »</span><br><span>{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
14407
14408
  "../etherpk-papermod/partials/after-content.html": "",
@@ -14425,7 +14426,7 @@ var files = /* @__PURE__ */ Object.assign({
14425
14426
  "../etherpk-stack/assets/theme.js": "/*\n * The light / dark toggle: a `.theme-toggle` button flips the page between the two and the\n * choice is remembered in this browser; with no choice made the page follows the system. The\n * stylesheet keys on `data-theme` on <html>, and shell-top applies the remembered choice before\n * the first paint so a dark reader never sees a light flash.\n */\n(function () {\n 'use strict';\n var KEY = 'etherpk-theme';\n var root = document.documentElement;\n function isDark() {\n var chosen = root.getAttribute('data-theme');\n if (chosen === 'dark' || chosen === 'light') return chosen === 'dark';\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n function apply(theme) {\n if (theme === 'dark' || theme === 'light') root.setAttribute('data-theme', theme);\n else root.removeAttribute('data-theme');\n var buttons = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('aria-pressed', isDark() ? 'true' : 'false');\n }\n try { apply(localStorage.getItem(KEY)); } catch (_e) { apply(null); }\n var toggles = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < toggles.length; i++) {\n toggles[i].addEventListener('click', function () {\n var next = isDark() ? 'light' : 'dark';\n apply(next);\n try { localStorage.setItem(KEY, next); } catch (_e) { /* private mode: the choice lasts the page */ }\n });\n }\n})();\n",
14426
14427
  "../etherpk-stack/layouts/404.html": "{{> shell-top}}\n <article class=\"article-page not-found\">\n <header class=\"article-header\"><h1 class=\"article-title\">Not published</h1></header>\n <div class=\"article-content\">\n <p>The page you followed a link to is not part of this site.</p>\n <p><a href=\"index.html\">Back to {{site.title}}</a></p>\n </div>\n </article>\n{{> shell-bottom}}\n",
14427
14428
  "../etherpk-stack/layouts/archive.html": "{{> shell-top}}\n <header class=\"section-header\"><h1>{{#page.isPostsArchive}}Posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}Journal{{/page.isJournalArchive}}</h1></header>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <section class=\"archive-year\" aria-label=\"{{year}}\">\n <h2 class=\"archive-year-title\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"archive-list\">\n {{#posts}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a></li>{{/posts}}\n </ul>\n </section>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <section class=\"article-list\" aria-label=\"Journal\">\n {{#site.journals}}{{> article-card}}{{/site.journals}}\n </section>\n {{/page.isJournalArchive}}\n{{> shell-bottom}}\n",
14428
- "../etherpk-stack/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"article-page home-intro\">\n <header class=\"article-header\"><h1 class=\"article-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n <section class=\"article-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> article-card}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"article-list-empty\">No dated documents are published yet. A journal entry is dated by its day; a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<article class=\"article-page page-contents\"><header class=\"article-header\"><h1 class=\"article-title\">{{site.title}}</h1></header><div class=\"article-content\">{{{page.content}}}</div></article>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </section>\n{{> shell-bottom}}\n",
14429
+ "../etherpk-stack/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"article-page home-intro\">\n <header class=\"article-header\"><h1 class=\"article-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n <section class=\"article-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> article-card}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"article-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<article class=\"article-page page-contents\"><header class=\"article-header\"><h1 class=\"article-title\">{{site.title}}</h1></header><div class=\"article-content\">{{{page.content}}}</div></article>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </section>\n{{> shell-bottom}}\n",
14429
14430
  "../etherpk-stack/layouts/journal.html": "{{> shell-top}}\n <article class=\"article-page\">\n <header class=\"article-header\">\n <span class=\"article-category\">{{page.kind}}</span>\n <h1 class=\"article-title\">{{{page.titleHtml}}}</h1>\n <div class=\"article-details\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}<span>{{page.readingMinutes}} minute read</span></div>\n </header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n <footer class=\"article-footer\">\n <nav class=\"article-pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"article-card pagination-card\" href=\"{{url}}\"><span class=\"pagination-label\">Previous</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"article-card pagination-card next\" href=\"{{url}}\"><span class=\"pagination-label\">Next</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
14430
14431
  "../etherpk-stack/layouts/page.html": "{{> shell-top}}\n <article class=\"article-page\">\n <header class=\"article-header\">\n <span class=\"article-category\">{{page.kind}}</span>\n <h1 class=\"article-title\">{{{page.titleHtml}}}</h1>\n <div class=\"article-details\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}<span>{{page.readingMinutes}} minute read</span></div>\n </header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n <footer class=\"article-footer\">\n <nav class=\"article-pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"article-card pagination-card\" href=\"{{url}}\"><span class=\"pagination-label\">Previous</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"article-card pagination-card next\" href=\"{{url}}\"><span class=\"pagination-label\">Next</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
14431
14432
  "../etherpk-stack/partials/after-content.html": "",
@@ -14443,12 +14444,12 @@ var files = /* @__PURE__ */ Object.assign({
14443
14444
  "../etherpk-stack/partials/theme-scripts.html": " <script src=\"theme/search.js\" defer><\/script>\n <script src=\"theme/theme.js\" defer><\/script>\n",
14444
14445
  "../etherpk-stack/partials/toc.html": " <nav class=\"toc\" aria-label=\"Table of contents\">\n <ul>\n {{#page.toc}}<li class=\"toc-level-{{level}}\"><a href=\"#{{id}}\">{{text}}</a></li>{{/page.toc}}\n </ul>\n </nav>\n",
14445
14446
  "../etherpk-stack/partials/widgets.html": " <section class=\"widget search-widget\" aria-label=\"Search\">\n <form class=\"site-search\" role=\"search\" data-search-index=\"{{site.search.url}}\" data-search-script=\"{{site.search.scriptUrl}}\">\n <label class=\"visually-hidden\" for=\"site-search-input\">Search</label>\n <input id=\"site-search-input\" type=\"search\" name=\"q\" placeholder=\"Search\" autocomplete=\"off\">\n <div class=\"site-search-results\" role=\"listbox\" hidden></div>\n </form>\n </section>\n {{#page.hasToc}}\n <section class=\"widget toc-widget\">\n <h2 class=\"widget-title\">Table of contents</h2>\n{{> toc}}\n </section>\n {{/page.hasToc}}\n {{#site.hasPostsArchive}}\n <!-- Years with counts, as Stack's archives widget does: a list of every post would grow\n with the blog; the archive page carries them. -->\n <section class=\"widget archives\">\n <h2 class=\"widget-title\">Archives</h2>\n <ul class=\"widget-list\">\n {{#site.postsByYear}}<li><a href=\"{{site.postsUrl}}#y{{year}}\"><span>{{year}}</span><span class=\"widget-count\">{{count}}</span></a></li>{{/site.postsByYear}}\n </ul>\n <p class=\"widget-more\"><a href=\"{{site.postsUrl}}\">All posts</a></p>\n </section>\n {{/site.hasPostsArchive}}\n {{#site.hasJournals}}\n <section class=\"widget journal\">\n <h2 class=\"widget-title\">Journal</h2>\n <p class=\"widget-more\"><a href=\"journal.html\">Every entry, by day</a></p>\n </section>\n {{/site.hasJournals}}\n <!-- A blog's posts are in the archive above; only its other pages belong here. -->\n {{#site.isBlog}}{{#site.hasUndatedPages}}\n <section class=\"widget pages\">\n <h2 class=\"widget-title\">Pages</h2>\n <ul class=\"widget-list\">\n {{#site.undatedPages}}<li><a href=\"{{url}}\"><span>{{title}}</span></a></li>{{/site.undatedPages}}\n </ul>\n </section>\n {{/site.hasUndatedPages}}{{/site.isBlog}}\n {{^site.isBlog}}\n <section class=\"widget pages\">\n <h2 class=\"widget-title\">Pages</h2>\n <ul class=\"widget-list\">\n {{#site.pages}}<li><a href=\"{{url}}\"><span>{{title}}</span></a></li>{{/site.pages}}\n </ul>\n </section>\n {{/site.isBlog}}\n",
14446
- "../etherpk-stack/theme.json": "{\n \"name\": \"etherpk-stack\",\n \"title\": \"EtherPK Stack\",\n \"version\": \"1.0.0\",\n \"contract\": 1,\n \"description\": \"In the mould of Hugo's Stack: a card-based blog with a left sidebar of site identity and menu, article cards in the middle, and widgets (search, contents, archive, pages) on the right.\",\n \"kinds\": [\"blog\", \"docs\"],\n \"includes\": [\n { \"name\": \"head\", \"description\": \"Extra markup inside <head>: analytics, fonts, meta tags. Raw HTML in the page body.\" },\n { \"name\": \"logo\", \"description\": \"The avatar above the site title in the sidebar: an image or inline SVG. Replaces the EtherPK icon.\" },\n { \"name\": \"header\", \"description\": \"The left sidebar's identity block (avatar, title, description). Replaces the theme's; the menu stays.\" },\n { \"name\": \"before-content\", \"description\": \"Rendered above every page's content: a banner, a notice.\" },\n { \"name\": \"after-content\", \"description\": \"Rendered below every page's content, above the linked references: an author bio, a comments widget.\" },\n { \"name\": \"footer\", \"description\": \"The footer under the article column.\" },\n { \"name\": \"scripts\", \"description\": \"Scripts before </body>, added after the theme's own (search, the theme toggle): analytics, a widget. Raw HTML in the page body.\" },\n { \"name\": \"styles\", \"description\": \"CSS appended after the theme's stylesheet: the page's first css fence.\", \"kind\": \"css\" }\n ],\n \"files\": [\n \"layouts/page.html\", \"layouts/home.html\", \"layouts/journal.html\", \"layouts/archive.html\", \"layouts/404.html\",\n \"partials/shell-top.html\", \"partials/shell-bottom.html\", \"partials/head.html\", \"partials/logo.html\", \"partials/header.html\",\n \"partials/nav-item.html\", \"partials/toc.html\", \"partials/backlinks.html\", \"partials/article-card.html\", \"partials/widgets.html\",\n \"partials/before-content.html\", \"partials/after-content.html\", \"partials/footer.html\", \"partials/theme-scripts.html\", \"partials/scripts.html\",\n \"assets/theme.css\", \"assets/search.js\", \"assets/theme.js\"\n ]\n}\n",
14447
+ "../etherpk-stack/theme.json": "{\n \"name\": \"etherpk-stack\",\n \"title\": \"EtherPK Stack\",\n \"version\": \"1.0.0\",\n \"contract\": 1,\n \"description\": \"In the mould of Hugo's Stack: a card-based blog with a left sidebar of site identity and menu, article cards in the middle, and widgets (search, contents, archive, pages) on the right.\",\n \"kinds\": [\"blog\", \"docs\"],\n \"includes\": [\n { \"name\": \"head\", \"description\": \"Extra markup inside <head>: analytics, fonts, meta tags. Raw HTML in the page body.\" },\n { \"name\": \"logo\", \"description\": \"The avatar above the site title in the sidebar: an image or inline SVG. Replaces the EtherPK icon.\" },\n { \"name\": \"header\", \"description\": \"The left sidebar's identity block (avatar, title, description). Replaces the theme's - the menu stays.\" },\n { \"name\": \"before-content\", \"description\": \"Rendered above every page's content: a banner, a notice.\" },\n { \"name\": \"after-content\", \"description\": \"Rendered below every page's content, above the linked references: an author bio, a comments widget.\" },\n { \"name\": \"footer\", \"description\": \"The footer under the article column.\" },\n { \"name\": \"scripts\", \"description\": \"Scripts before </body>, added after the theme's own (search, the theme toggle): analytics, a widget. Raw HTML in the page body.\" },\n { \"name\": \"styles\", \"description\": \"CSS appended after the theme's stylesheet: the page's first css fence.\", \"kind\": \"css\" }\n ],\n \"files\": [\n \"layouts/page.html\", \"layouts/home.html\", \"layouts/journal.html\", \"layouts/archive.html\", \"layouts/404.html\",\n \"partials/shell-top.html\", \"partials/shell-bottom.html\", \"partials/head.html\", \"partials/logo.html\", \"partials/header.html\",\n \"partials/nav-item.html\", \"partials/toc.html\", \"partials/backlinks.html\", \"partials/article-card.html\", \"partials/widgets.html\",\n \"partials/before-content.html\", \"partials/after-content.html\", \"partials/footer.html\", \"partials/theme-scripts.html\", \"partials/scripts.html\",\n \"assets/theme.css\", \"assets/search.js\", \"assets/theme.js\"\n ]\n}\n",
14447
14448
  "../etherpk-terminal/assets/search.js": "/*\n * Search for a Published Site: reads search.json (built over the publication's own documents\n * and nothing else) and scores documents by title and text matches. No dependencies, no\n * network beyond the one index file, and nothing runs until the search box is used. The same\n * script runs the sidebar toggle on small screens, so a page carries one script.\n */\n(function () {\n 'use strict';\n\n // -- Navigation toggle (small screens) -------------------------------------------------\n var toggle = document.querySelector('.nav-toggle');\n var backdrop = document.querySelector('.nav-backdrop');\n var nav = document.getElementById('site-nav');\n function setNavOpen(open) {\n var was = document.body.classList.contains('nav-open');\n document.body.classList.toggle('nav-open', open);\n if (toggle) toggle.setAttribute('aria-expanded', open ? 'true' : 'false');\n if (backdrop) backdrop.hidden = !open;\n // Focus follows the drawer: into its first link on open, back to the button on close,\n // so a keyboard or screen-reader user is never left on something now hidden.\n if (open && !was) {\n var first = nav.querySelector('a');\n if (first) first.focus();\n } else if (!open && was && toggle) {\n toggle.focus();\n }\n }\n if (toggle && nav) {\n toggle.addEventListener('click', function () { setNavOpen(!document.body.classList.contains('nav-open')); });\n if (backdrop) backdrop.addEventListener('click', function () { setNavOpen(false); });\n document.addEventListener('keydown', function (e) {\n if (e.key === 'Escape' && document.body.classList.contains('nav-open')) setNavOpen(false);\n });\n }\n\n // -- Search --------------------------------------------------------------------------\n var form = document.querySelector('.site-search');\n if (!form) return;\n var input = form.querySelector('input[type=\"search\"]');\n var results = form.querySelector('.site-search-results');\n var indexUrl = form.getAttribute('data-search-index') || 'search.json';\n var scriptUrl = form.getAttribute('data-search-script') || 'search-index.js';\n var index = null;\n var loading = null;\n /* True once neither route produced the index: the results then say so rather than \"nothing matches\". */\n var unavailable = false;\n\n /* The index as a script assigning window.etherpkSearchIndex: what a site opened from disk can\n load, since file:// refuses fetch but allows <script src>. */\n function loadScript() {\n return new Promise(function (resolve, reject) {\n if (window.etherpkSearchIndex) { resolve(window.etherpkSearchIndex); return; }\n var s = document.createElement('script');\n s.src = scriptUrl;\n s.onload = function () {\n if (window.etherpkSearchIndex) resolve(window.etherpkSearchIndex);\n else reject(new Error('no index'));\n };\n s.onerror = function () { reject(new Error('no script')); };\n document.head.appendChild(s);\n });\n }\n\n function load() {\n if (index) return Promise.resolve(index);\n if (!loading) {\n loading = fetch(indexUrl).then(function (r) {\n if (!r.ok) throw new Error(String(r.status));\n return r.json();\n }).catch(loadScript).then(function (data) {\n index = (data && data.documents) || [];\n return index;\n }).catch(function () { unavailable = true; index = []; return index; });\n }\n return loading;\n }\n\n function tokens(q) {\n return q.toLowerCase().split(/\\s+/).filter(function (t) { return t.length > 0; });\n }\n\n function score(doc, terms) {\n var title = (doc.title || '').toLowerCase();\n var text = (doc.text || '').toLowerCase();\n var s = 0;\n for (var i = 0; i < terms.length; i++) {\n var t = terms[i];\n if (title === t) s += 20;\n else if (title.indexOf(t) === 0) s += 10;\n else if (title.indexOf(t) !== -1) s += 6;\n var at = text.indexOf(t);\n if (at !== -1) s += 2;\n else if (title.indexOf(t) === -1) return 0; // every term must match somewhere\n }\n return s;\n }\n\n function snippet(doc, terms) {\n var text = doc.text || '';\n var lower = text.toLowerCase();\n var at = -1;\n for (var i = 0; i < terms.length && at === -1; i++) at = lower.indexOf(terms[i]);\n if (at === -1) return doc.excerpt || '';\n var start = Math.max(0, at - 60);\n var end = Math.min(text.length, at + 100);\n return (start > 0 ? '…' : '') + text.slice(start, end).replace(/\\s+/g, ' ') + (end < text.length ? '…' : '');\n }\n\n function escapeHtml(s) {\n return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n }\n\n function render(list, q) {\n if (!q) { results.hidden = true; results.innerHTML = ''; return; }\n if (unavailable) {\n results.innerHTML = '<p class=\"site-search-empty\">The search index could not be loaded.</p>';\n results.hidden = false;\n return;\n }\n if (list.length === 0) {\n results.innerHTML = '<p class=\"site-search-empty\">Nothing matches \"' + escapeHtml(q) + '\".</p>';\n results.hidden = false;\n return;\n }\n results.innerHTML = list.map(function (hit) {\n return '<a class=\"site-search-hit\" role=\"option\" href=\"' + escapeHtml(hit.doc.url) + '\">' +\n '<span class=\"site-search-title\">' + escapeHtml(hit.doc.title) + '</span>' +\n '<span class=\"site-search-snippet\">' + escapeHtml(hit.snippet) + '</span></a>';\n }).join('');\n results.hidden = false;\n }\n\n var timer = null;\n function run() {\n var q = input.value.trim();\n var terms = tokens(q);\n if (terms.length === 0) { render([], ''); return; }\n load().then(function (docs) {\n var hits = [];\n for (var i = 0; i < docs.length; i++) {\n var s = score(docs[i], terms);\n if (s > 0) hits.push({ doc: docs[i], score: s, snippet: snippet(docs[i], terms) });\n }\n hits.sort(function (a, b) { return b.score - a.score || a.doc.title.localeCompare(b.doc.title); });\n render(hits.slice(0, 12), q);\n });\n }\n\n input.addEventListener('input', function () {\n clearTimeout(timer);\n timer = setTimeout(run, 120);\n });\n input.addEventListener('focus', function () { load(); });\n form.addEventListener('submit', function (e) {\n e.preventDefault();\n var first = results.querySelector('a');\n if (first) window.location.href = first.getAttribute('href');\n });\n document.addEventListener('keydown', function (e) {\n if (e.key === '/' && document.activeElement !== input && !/input|textarea/i.test(document.activeElement.tagName)) {\n e.preventDefault();\n input.focus();\n }\n if (e.key === 'Escape' && !results.hidden) { render([], ''); }\n });\n document.addEventListener('click', function (e) {\n if (!form.contains(e.target)) render([], '');\n });\n})();\n",
14448
14449
  "../etherpk-terminal/assets/theme.css": "/* EtherPK's Terminal: after Hugo's Terminal (panr, MIT), written afresh for EtherPK's view - one\n monospace face, a dark ground, no colour at all - the accent is a brighter shade of the text,\n so everything is drawn in tones of one ink - the block logo with its blinking cursor,\n underlined titles, framed boxes and bordered buttons. Uses Fira Code\n or the system monospace, so the site loads nothing from anywhere. Classes the publisher emits\n and this file styles: .wikilink, .wikilink-missing, .task, .task-tag, .toc, .backlinks,\n .site-nav-list, .diagram, .math, .tok-* (code). */\n\n:root {\n --accent: #f2f2f2;\n --background: #1d1e28;\n --color: #cfcfd6;\n --color-muted: #8f8f9a;\n --border-color: rgba(255, 255, 255, 0.1);\n --code-bg: rgba(255, 255, 255, 0.06);\n --font: \"Fira Code\", ui-monospace, Monaco, Consolas, \"Ubuntu Mono\", monospace;\n --container-width: 1080px;\n --mark: rgba(255, 255, 255, 0.22);\n --missing: var(--color-muted);\n}\n/* Terminal is dark by nature; a light system preference is respected, the ink inverted. */\n@media (prefers-color-scheme: light) {\n :root:not([data-theme=\"dark\"]) {\n --accent: #111111;\n --background: #f7f4ef;\n --color: #2b2b2b;\n --color-muted: #6a6a6a;\n --mark: rgba(0, 0, 0, 0.14);\n --border-color: rgba(0, 0, 0, 0.12);\n --code-bg: rgba(0, 0, 0, 0.05);\n }\n}\n\n*, *::before, *::after { box-sizing: border-box; }\nhtml { color-scheme: dark light; }\nbody {\n margin: 0;\n font-family: var(--font);\n font-size: 0.9375rem;\n line-height: 1.6;\n background: var(--background);\n color: var(--color);\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n font-feature-settings: \"liga\", \"tnum\", \"zero\", \"ss01\", \"locl\";\n}\na { color: var(--accent); }\na:hover { text-decoration: none; }\nimg { max-width: 100%; height: auto; display: block; }\n.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.skip-link { position: absolute; left: -999px; top: 0; background: var(--accent); color: var(--background); padding: 8px 16px; z-index: 100; }\n.skip-link:focus { left: 16px; top: 16px; }\n\n.container { max-width: var(--container-width); margin: 0 auto; padding: 40px; }\n\n/* -- Header: the prompt-style logo ------------------------------------------------------ */\n.header { margin-bottom: 40px; }\n.header__inner { display: flex; align-items: center; justify-content: space-between; gap: 20px; }\n/* The block logo: the mark, the title and a blinking cursor on one line, the mark's box sized\n to the text's cap height so the two read as one glyph run. */\n.logo {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n padding: 8px 16px;\n background: var(--accent);\n color: var(--background);\n font-size: 1.375rem;\n line-height: 1;\n font-weight: bold;\n text-decoration: none;\n}\n.logo:hover { text-decoration: none; }\n.logo__mark { display: inline-flex; align-items: center; line-height: 0; }\n.logo__mark .site-logo { display: block; width: 1.3em; height: 1.125em; }\n.logo__cursor {\n display: inline-block;\n width: 0.5em;\n height: 1em;\n background: var(--background);\n animation: cursor 1s infinite;\n}\n@keyframes cursor { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } }\n@media (prefers-reduced-motion: reduce) { .logo__cursor { animation: none; } }\n.menu { margin-top: 20px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 30px; }\n.menu__inner { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 30px; }\n.menu__inner li a, .menu__inner .nav-group-label { color: var(--color); text-decoration: none; }\n.menu__inner li a:hover { color: var(--accent); }\n.menu__inner .nav-current > a { color: var(--accent); }\n.menu__inner .nav-current > a::after { content: \" *\"; }\n.menu__inner .nav-group-label { color: var(--color-muted); }\n.menu__inner .nav-group-label::after { content: \":\"; }\n/* A nested outline level follows its parent on the same line, the way a path reads. */\n.menu__inner li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 12px; }\n.menu__inner .site-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0 12px; }\n.menu__inner .site-nav-list li::before { content: \"/\"; color: var(--color-muted); margin-right: 12px; }\n.nav-toggle { display: none; }\n.nav-backdrop { display: none; }\n.site-search { position: relative; margin-left: auto; flex: 0 1 16rem; }\n.site-search input {\n width: 100%;\n padding: 5px 10px;\n border: 1px solid var(--accent);\n background: transparent;\n color: var(--color);\n font: inherit;\n}\n.site-search input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }\n.site-search input::placeholder { color: var(--color-muted); }\n.site-search-results {\n position: absolute;\n right: 0;\n top: calc(100% + 6px);\n width: min(26rem, 90vw);\n max-height: 24rem;\n overflow: auto;\n background: var(--background);\n border: 1px solid var(--accent);\n z-index: 30;\n}\n.site-search-hit { display: block; padding: 8px 12px; color: var(--color); text-decoration: none; border-bottom: 1px solid var(--border-color); }\n.site-search-hit:hover, .site-search-hit:focus { background: var(--code-bg); }\n.site-search-title { display: block; color: var(--accent); }\n.site-search-snippet { display: block; color: var(--color-muted); font-size: 0.9rem; }\n.site-search-empty { margin: 0; padding: 8px 12px; color: var(--color-muted); }\n\n/* -- Posts ------------------------------------------------------------------------------ */\n.posts { width: 100%; }\n.post { width: 100%; }\n.post.on-list { margin-bottom: 40px; padding-bottom: 40px; border-bottom: 1px dotted var(--accent); }\n.post.on-list:last-child { border-bottom: 0; }\n.post-title {\n position: relative;\n margin: 0 0 15px;\n font-size: 1.6rem;\n line-height: 1.3;\n color: var(--accent);\n text-decoration: underline;\n text-decoration-color: var(--accent);\n text-decoration-thickness: 2px;\n text-underline-offset: 6px;\n}\n.post-title a { color: inherit; text-decoration: none; }\n.post-title .wikilink { color: inherit; text-decoration: none; }\n.index-title { text-decoration: none; }\n.index-title::before { content: \"# \"; }\n.post-meta { margin: 0 0 20px; font-size: 0.9rem; color: var(--color-muted); }\n.post-content { margin-top: 20px; }\n.post-content h1, .post-content h2, .post-content h3, .post-content h4 { font-weight: bold; line-height: 1.3; margin: 40px 0 20px; }\n.post-content h2 { font-size: 1.3rem; }\n.post-content h3 { font-size: 1.2rem; }\n.post-content h2::before { content: \"## \"; color: var(--accent); }\n.post-content h3::before { content: \"### \"; color: var(--accent); }\n.post-content p { margin: 0 0 20px; }\n.post-content ul, .post-content ol { margin: 0 0 20px; padding-left: 24px; }\n.post-content li { margin: 4px 0; }\n.post-content ul li::marker { color: var(--accent); }\n.post-content img { margin: 20px 0; border: 1px solid var(--border-color); }\n.post-content .diagram { margin: 20px 0; text-align: center; }\n.post-content .diagram svg { max-width: 100%; height: auto; }\n.post-content blockquote {\n position: relative;\n margin: 40px 0;\n padding: 25px;\n border-top: 1px solid var(--accent);\n border-bottom: 1px solid var(--accent);\n font-style: italic;\n}\n.post-content blockquote::before { content: \"”\"; position: absolute; top: -40px; left: 10px; font-size: 3.875rem; font-style: normal; color: var(--accent); }\n.post-content blockquote p:last-child { margin-bottom: 0; }\n.post-content hr { border: 0; border-top: 1px dashed var(--accent); margin: 40px 0; }\n.post-content table { width: 100%; border-collapse: collapse; margin: 20px 0; }\n.post-content th, .post-content td { padding: 8px 12px; border: 1px dashed var(--accent); text-align: left; }\n.post-content th { color: var(--accent); }\n.post-content code { padding: 1px 6px; background: var(--code-bg); font-family: inherit; font-size: 0.95em; }\n.post-content pre {\n margin: 20px 0;\n padding: 20px;\n background: var(--code-bg);\n border-left: 3px solid var(--accent);\n overflow-x: auto;\n font-family: inherit;\n font-size: 0.95rem;\n line-height: 1.5;\n}\n.post-content pre code { padding: 0; background: none; font-size: inherit; }\n.math-block { overflow-x: auto; margin: 20px 0; }\nmark { background: var(--mark); color: inherit; padding: 0 0.15em; }\n\n/* Wikilinks and tasks. */\na.wikilink { color: var(--accent); }\na.wikilink-missing { color: var(--missing); text-decoration-style: dotted; }\nli.task { list-style: none; margin-left: -24px; }\nli.task input[type=\"checkbox\"] { accent-color: var(--accent); margin-right: 6px; vertical-align: -0.1em; }\nli.task.done { color: var(--color-muted); text-decoration: line-through; }\n.task-tag { display: inline-block; padding: 0 6px; margin-right: 4px; border: 1px solid var(--accent); color: var(--accent); font-size: 0.8rem; }\n.task-tag.cancelled { text-decoration: line-through; }\n\n/* Buttons, frames, the table of contents and pagination. */\n.button {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 8px 18px;\n border: 1px solid var(--accent);\n color: var(--accent);\n font: inherit;\n text-decoration: none;\n background: transparent;\n cursor: pointer;\n}\n.button:hover { background: var(--accent); color: var(--background); }\n.button.inline { display: inline-flex; }\n.read-more { margin-top: 10px; }\n.all-posts { margin: 30px 0 0; }\n/* The contents page of a docs site: the outline as a tree, then any page it leaves out. */\n.page-index-list { list-style: none; margin: 0; padding: 0; }\n.page-index-list .page-index-list { padding-left: 24px; }\n.page-index-list li { margin: 6px 0; }\n.page-index-list li::before { content: \"- \"; color: var(--accent); }\n.page-index-list a { color: var(--color); }\n.page-index-list a:hover { color: var(--accent); }\n.page-index-group-label { color: var(--color-muted); }\n.page-index-group-label::after { content: \":\"; }\n.page-index-other { margin: 30px 0 10px; font-size: 1.1rem; }\n.page-index-other::before { content: \"## \"; color: var(--accent); }\n/* The archives: a year as a prompt-style heading, then one line per post. */\n.archive-year { margin: 30px 0 10px; font-size: 1.1rem; }\n.archive-year::before { content: \"## \"; color: var(--accent); }\n.archive-list { list-style: none; margin: 0; padding: 0; }\n.archive-list li { margin: 6px 0; }\n.archive-list time { color: var(--color-muted); margin-right: 12px; font-variant-numeric: tabular-nums; }\n.archive-list a { color: var(--color); }\n.archive-list a:hover { color: var(--accent); }\n.archive-excerpt { color: var(--color-muted); }\n.framed { border: 1px solid var(--accent); padding: 20px; }\n.toc { margin: 20px 0 40px; }\n.toc__title { margin: 0 0 10px; color: var(--accent); }\n.toc ul { list-style: none; margin: 0; padding: 0; }\n.toc li { margin: 4px 0; }\n.toc li::before { content: \"- \"; color: var(--accent); }\n.toc li.toc-level-3 { padding-left: 1.5rem; }\n.toc li.toc-level-4 { padding-left: 3rem; }\n.toc a { color: var(--color); text-decoration: none; }\n.toc a:hover { color: var(--accent); }\n.pagination { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; margin-top: 50px; }\n.pagination__next { margin-left: auto; }\n.button__text { max-width: 20rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n\n/* Linked references, framed like everything else worth a box. */\n.backlinks { margin-top: 40px; border: 1px solid var(--accent); padding: 20px; }\n.backlinks h2 { margin: 0 0 12px; font-size: 1rem; color: var(--accent); }\n.backlinks h2::before { content: \":: \"; }\n.backlink-list { list-style: none; margin: 0; padding: 0; }\n.backlink-source { margin: 0 0 12px; }\n.backlink-source > a { color: var(--color); }\n.backlink-source ul { list-style: none; margin: 4px 0 0; padding: 0 0 0 16px; border-left: 1px dashed var(--accent); }\n.backlink-context { color: var(--color-muted); font-size: 0.9rem; margin: 2px 0; }\n\n/* -- Footer ----------------------------------------------------------------------------- */\n.footer { margin-top: 60px; padding-top: 20px; border-top: 1px dotted var(--accent); font-size: 0.9rem; color: var(--color-muted); }\n.footer__inner { display: flex; flex-wrap: wrap; gap: 10px; }\n.copyright { display: flex; flex-wrap: wrap; gap: 0 8px; }\n.footer__separator { color: var(--accent); }\n\n/* Code tokens (lezer classHighlighter names). */\n/* Code tokens (lezer classHighlighter names), in tones of the one ink: weight and italics carry\n the distinction the usual colours would. */\n.tok-keyword { color: var(--accent); font-weight: bold; }\n.tok-string, .tok-string2 { color: var(--color); font-style: italic; }\n.tok-comment, .tok-lineComment, .tok-blockComment { color: var(--color-muted); font-style: italic; }\n.tok-number, .tok-bool, .tok-null, .tok-atom { color: var(--accent); }\n.tok-function, .tok-definition.tok-variableName { color: var(--accent); }\n.tok-typeName, .tok-className, .tok-namespace { color: var(--accent); font-weight: bold; }\n.tok-operator, .tok-punctuation { color: var(--color-muted); }\n.tok-propertyName, .tok-attributeName { color: var(--color); }\n.tok-tagName { color: var(--accent); }\n.tok-invalid { color: var(--missing); text-decoration: underline wavy; }\n\n/* -- Small screens: the menu behind a Terminal-style button ----------------------------- */\n@media (max-width: 684px) {\n .container { padding: 20px; }\n .nav-toggle {\n display: inline-block;\n padding: 5px 10px;\n border: 1px solid var(--accent);\n background: transparent;\n color: var(--accent);\n font: inherit;\n cursor: pointer;\n }\n .nav-toggle::before { content: \"$ \"; }\n .menu { display: none; flex-direction: column; align-items: stretch; gap: 12px; margin-top: 20px; padding: 20px; border: 1px solid var(--accent); }\n body.nav-open .menu { display: flex; }\n .menu__inner { flex-direction: column; gap: 8px; }\n .menu__inner li { flex-direction: column; align-items: flex-start; }\n .menu__inner .site-nav-list { flex-direction: column; padding-left: 12px; }\n .menu__inner .site-nav-list li::before { margin-right: 6px; }\n .site-search { margin-left: 0; flex-basis: auto; }\n .post-title { font-size: 1.3rem; }\n .pagination__prev, .pagination__next { flex-basis: 100%; }\n .pagination__next { justify-content: flex-end; }\n}\n",
14449
14450
  "../etherpk-terminal/layouts/404.html": "{{> shell-top}}\n <article class=\"post\">\n <h1 class=\"post-title\">404 :: not published</h1>\n <div class=\"post-content\">\n <p>$ the page you followed a link to is not part of this site.</p>\n <p><a class=\"button inline\" href=\"index.html\">← {{site.title}}</a></p>\n </div>\n </article>\n{{> shell-bottom}}\n",
14450
14451
  "../etherpk-terminal/layouts/archive.html": "{{> shell-top}}\n <article class=\"post archive\">\n <h1 class=\"post-title index-title\">{{#page.isPostsArchive}}posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}journal{{/page.isJournalArchive}}</h1>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <h2 class=\"archive-year\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"archive-list\">\n {{#posts}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a></li>{{/posts}}\n </ul>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <ul class=\"archive-list\">\n {{#site.journals}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a>{{#excerpt}} <span class=\"archive-excerpt\">{{excerpt}}</span>{{/excerpt}}</li>{{/site.journals}}\n </ul>\n {{/page.isJournalArchive}}\n </article>\n{{> shell-bottom}}\n",
14451
- "../etherpk-terminal/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post home-info\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <h1 class=\"post-title index-title\">{{site.title}}</h1>\n {{/page.isGeneratedIndex}}\n <div class=\"posts\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day; a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<div class=\"post-content\">{{{page.content}}}</div>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </div>\n {{#site.isBlog}}{{#site.hasPosts}}\n <p class=\"all-posts\"><a class=\"button inline\" href=\"{{site.postsUrl}}\">$ ls all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}{{/site.isBlog}}\n{{> shell-bottom}}\n",
14452
+ "../etherpk-terminal/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post home-info\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <h1 class=\"post-title index-title\">{{site.title}}</h1>\n {{/page.isGeneratedIndex}}\n <div class=\"posts\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<div class=\"post-content\">{{{page.content}}}</div>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </div>\n {{#site.isBlog}}{{#site.hasPosts}}\n <p class=\"all-posts\"><a class=\"button inline\" href=\"{{site.postsUrl}}\">$ ls all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}{{/site.isBlog}}\n{{> shell-bottom}}\n",
14452
14453
  "../etherpk-terminal/layouts/journal.html": "{{> shell-top}}\n <article class=\"post\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"button pagination__prev\" href=\"{{url}}\"><span class=\"button__icon\">←</span><span class=\"button__text\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"button pagination__next\" href=\"{{url}}\"><span class=\"button__text\">{{title}}</span><span class=\"button__icon\">→</span></a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
14453
14454
  "../etherpk-terminal/layouts/page.html": "{{> shell-top}}\n <article class=\"post\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"button pagination__prev\" href=\"{{url}}\"><span class=\"button__icon\">←</span><span class=\"button__text\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"button pagination__next\" href=\"{{url}}\"><span class=\"button__text\">{{title}}</span><span class=\"button__icon\">→</span></a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
14454
14455
  "../etherpk-terminal/partials/after-content.html": "",
@@ -14567,7 +14568,7 @@ function seededFiles(publication) {
14567
14568
  return new Map([
14568
14569
  [PAGES_WORKFLOW_PATH, `# Publishes this folder to GitHub Pages as it is: the site is already built by EtherPK,
14569
14570
  # so there is no build step. In the repository's Settings → Pages, set the source to
14570
- # "GitHub Actions" once; every push to main then deploys.
14571
+ # "GitHub Actions" once - every push to main then deploys.
14571
14572
  name: Deploy to GitHub Pages
14572
14573
  on:
14573
14574
  push:
@@ -14598,7 +14599,7 @@ jobs:
14598
14599
  [WRANGLER_PATH, `// Cloudflare Workers with static assets (Cloudflare's successor to Pages for new sites).
14599
14600
  // Deploy with \`npx wrangler deploy\`, or connect the repository in the dashboard under
14600
14601
  // Workers → Create → Import a repository and leave the build command empty. The Worker is
14601
- // named after the publication; Cloudflare warns when this name and the Worker's differ.
14602
+ // named after the publication - Cloudflare warns when this name and the Worker's differ.
14602
14603
  {
14603
14604
  "name": "${publication.id}",
14604
14605
  "compatibility_date": "2026-01-01",
@@ -14616,9 +14617,9 @@ This folder is a website published by [EtherPK](https://etherpk.com) from the pu
14616
14617
  publish, so edit the notes, the publication page, its include pages and its theme in EtherPK,
14617
14618
  not here.
14618
14619
 
14619
- - Preview: open \`index.html\` in a browser; search works from disk too.
14620
+ - Preview: open \`index.html\` in a browser - search works from disk too.
14620
14621
  - GitHub Pages: push this folder to a repository and set Settings → Pages → Source to
14621
- "GitHub Actions"; \`.github/workflows/pages.yaml\` deploys every push to \`main\`.
14622
+ "GitHub Actions" - \`.github/workflows/pages.yaml\` deploys every push to \`main\`.
14622
14623
  - Cloudflare: \`npx wrangler deploy\` uses \`wrangler.jsonc\`, or connect the repository in the
14623
14624
  Workers dashboard with no build command.
14624
14625
  - Netlify, GitLab Pages, Vercel, S3 or your own server: the folder is plain files, so any of
@@ -14626,7 +14627,7 @@ not here.
14626
14627
  `],
14627
14628
  [GITIGNORE_PATH, `.DS_Store\nThumbs.db\n`],
14628
14629
  [ASSETS_IGNORE_PATH, `# Files in this folder that are not part of the website, kept out of the Worker's assets.
14629
- # Written once by EtherPK; yours to edit.
14630
+ # Written once by EtherPK - yours to edit.
14630
14631
  .git
14631
14632
  .github
14632
14633
  .gitignore
@@ -14650,15 +14651,15 @@ This folder is a website generated by [EtherPK](https://etherpk.com) from the pu
14650
14651
  \`robots.txt\` and the text outside this section of \`AGENTS.md\` is rewritten on each publish
14651
14652
  and files that are no longer produced are deleted.
14652
14653
 
14653
- - Do not edit the HTML, the assets or \`theme/\` here; the change is lost on the next publish.
14654
+ - Do not edit the HTML, the assets or \`theme/\` here - the change is lost on the next publish.
14654
14655
  - Content changes are made to the documents in EtherPK. A document is published when its
14655
14656
  frontmatter says \`public: true\` and names this publication in \`publications: [${publication.id}]\`
14656
14657
  (unless the publication takes every public document).
14657
- - The navigation is the outline of the publication page in EtherPK; the theme and the include
14658
+ - The navigation is the outline of the publication page in EtherPK - the theme and the include
14658
14659
  slots (head, logo, header, footer, before-content, after-content, scripts, styles) are chosen
14659
14660
  and edited there too, in the Publish settings and the Theme editor.
14660
14661
  - \`index.html\` opens from disk for a preview. \`.github/workflows/pages.yaml\` deploys the folder
14661
- to GitHub Pages; \`wrangler.jsonc\` deploys it to Cloudflare Workers; any static host serves it
14662
+ to GitHub Pages, \`wrangler.jsonc\` deploys it to Cloudflare Workers, and any static host serves it
14662
14663
  as it is (https://docs.etherpk.com/hosting-a-published-site).
14663
14664
 
14664
14665
  This section is rewritten by EtherPK on every publish. Put your own notes above or below it.
@@ -15544,7 +15545,7 @@ function buildNav(outline, resolver) {
15544
15545
  issues.push({
15545
15546
  level: "warning",
15546
15547
  code: "nav-entry-not-published",
15547
- message: `The navigation names "${whole.concept}", which is not in this publication; the entry is left out.`
15548
+ message: `The navigation names "${whole.concept}", which is not in this publication - the entry is left out.`
15548
15549
  });
15549
15550
  continue;
15550
15551
  }
@@ -15565,7 +15566,7 @@ function buildNav(outline, resolver) {
15565
15566
  issues.push({
15566
15567
  level: "warning",
15567
15568
  code: "nav-link-unsafe",
15568
- message: `The navigation links "${label}" to a target a published page does not carry (only http, https, mailto and site paths); the entry is shown without its link.`
15569
+ message: `The navigation links "${label}" to a target a published page does not carry (only http, https, mailto and site paths) - the entry is shown without its link.`
15569
15570
  });
15570
15571
  out.push({
15571
15572
  label,
@@ -15802,7 +15803,7 @@ async function publishPublication(source, publication, env, options = {}) {
15802
15803
  if (homeKey !== null && !homeDoc) warnings.push({
15803
15804
  level: "warning",
15804
15805
  code: "home-not-published",
15805
- message: `The home page "${publication.home}" is not in this publication; the front page lists the published pages instead.`
15806
+ message: `The home page "${publication.home}" is not in this publication - the front page lists the published pages instead.`
15806
15807
  });
15807
15808
  const allocation = allocateSlugs.withReport(included.filter((d) => d !== homeDoc));
15808
15809
  const slugs = new Map(allocation.slugs);
@@ -15811,13 +15812,13 @@ async function publishPublication(source, publication, env, options = {}) {
15811
15812
  for (const c of allocation.collisions) if (c.explicit) warnings.push({
15812
15813
  level: "warning",
15813
15814
  code: "slug-taken",
15814
- message: `"${c.concept}" asks for the address ${c.wanted}.html in its frontmatter, but another document has it; it is at ${c.slug}.html.`,
15815
+ message: `"${c.concept}" asks for the address ${c.wanted}.html in its frontmatter, but another document has it - it is at ${c.slug}.html.`,
15815
15816
  concept: c.concept
15816
15817
  });
15817
15818
  else info.push({
15818
15819
  level: "info",
15819
15820
  code: "slug-collision",
15820
- message: `"${c.concept}" wanted the address ${c.wanted}.html, which another document has; it is at ${c.slug}.html.`,
15821
+ message: `"${c.concept}" wanted the address ${c.wanted}.html, which another document has - it is at ${c.slug}.html.`,
15821
15822
  concept: c.concept
15822
15823
  });
15823
15824
  for (const ignoredSlug of allocation.ignored) {
@@ -15825,7 +15826,7 @@ async function publishPublication(source, publication, env, options = {}) {
15825
15826
  warnings.push({
15826
15827
  level: "warning",
15827
15828
  code: "slug-ignored",
15828
- message: `"${ignoredSlug.concept}" has \`slug: ${ignoredSlug.value}\`, which ${why}; its address is derived from its name instead.`,
15829
+ message: `"${ignoredSlug.concept}" has \`slug: ${ignoredSlug.value}\`, which ${why} - its address is derived from its name instead.`,
15829
15830
  concept: ignoredSlug.concept
15830
15831
  });
15831
15832
  }
@@ -15875,12 +15876,12 @@ async function publishPublication(source, publication, env, options = {}) {
15875
15876
  if (theme.manifest.contract < 1) warnings.push({
15876
15877
  level: "warning",
15877
15878
  code: "theme-older-contract",
15878
- message: `The theme "${theme.manifest.name}" was written for view contract ${theme.manifest.contract}; this EtherPK writes contract 1. It is rendered as is; newer view fields are simply absent to it.`
15879
+ message: `The theme "${theme.manifest.name}" was written for view contract ${theme.manifest.contract} - this EtherPK writes contract 1. It is rendered as is - newer view fields are simply absent to it.`
15879
15880
  });
15880
15881
  if (!theme.manifest.kinds.includes(publication.kind)) warnings.push({
15881
15882
  level: "warning",
15882
15883
  code: "theme-kind-mismatch",
15883
- message: `The theme "${theme.manifest.name}" is made for ${theme.manifest.kinds.join(" and ")} publications; this one is ${publication.kind}.`
15884
+ message: `The theme "${theme.manifest.name}" is made for ${theme.manifest.kinds.join(" and ")} publications - this one is ${publication.kind}.`
15884
15885
  });
15885
15886
  const includeDocs = /* @__PURE__ */ new Map();
15886
15887
  for (const [slot, concept] of Object.entries(publication.includes)) {
@@ -15890,7 +15891,7 @@ async function publishPublication(source, publication, env, options = {}) {
15890
15891
  warnings.push({
15891
15892
  level: "warning",
15892
15893
  code: "include-page-missing",
15893
- message: `The include "${slot}" names "${concept}", which is not a document in this graph; the theme's own ${slot} is used.`
15894
+ message: `The include "${slot}" names "${concept}", which is not a document in this graph - the theme's own ${slot} is used.`
15894
15895
  });
15895
15896
  continue;
15896
15897
  }
@@ -15905,7 +15906,7 @@ async function publishPublication(source, publication, env, options = {}) {
15905
15906
  warnings.push({
15906
15907
  level: "warning",
15907
15908
  code: `include-page-${status}`,
15908
- message: `The include "${slot}" names "${concept}", which ${why}; the theme's own ${slot} is used.`
15909
+ message: `The include "${slot}" names "${concept}", which ${why} - the theme's own ${slot} is used.`
15909
15910
  });
15910
15911
  continue;
15911
15912
  }
@@ -16019,7 +16020,7 @@ async function publishPublication(source, publication, env, options = {}) {
16019
16020
  warnings.push({
16020
16021
  level: "warning",
16021
16022
  code: "include-unknown-slot",
16022
- message: `The theme "${theme.manifest.name}" has no include slot "${slot}"; the page "${doc.concept}" is not used.`
16023
+ message: `The theme "${theme.manifest.name}" has no include slot "${slot}" - the page "${doc.concept}" is not used.`
16023
16024
  });
16024
16025
  continue;
16025
16026
  }
@@ -16030,7 +16031,7 @@ async function publishPublication(source, publication, env, options = {}) {
16030
16031
  warnings.push({
16031
16032
  level: "warning",
16032
16033
  code: "include-no-css",
16033
- message: `The include "${slot}" names "${doc.concept}", which has no \`css\` fence; nothing is appended to the stylesheet.`
16034
+ message: `The include "${slot}" names "${doc.concept}", which has no \`css\` fence - nothing is appended to the stylesheet.`
16034
16035
  });
16035
16036
  continue;
16036
16037
  }
@@ -16206,7 +16207,7 @@ async function publishPublication(source, publication, env, options = {}) {
16206
16207
  warnings.push({
16207
16208
  level: "warning",
16208
16209
  code: "asset-missing",
16209
- message: `"${from}" references ${name}, which the graph does not hold; the link is left as it is.`,
16210
+ message: `"${from}" references ${name}, which the graph does not hold - the link is left as it is.`,
16210
16211
  concept: from
16211
16212
  });
16212
16213
  continue;
@@ -16222,7 +16223,7 @@ async function publishPublication(source, publication, env, options = {}) {
16222
16223
  else warnings.push({
16223
16224
  level: "warning",
16224
16225
  code: "katex-assets-unavailable",
16225
- message: "The publication has maths but this host has no KaTeX stylesheet to copy in; formulas will render unstyled."
16226
+ message: "The publication has maths but this host has no KaTeX stylesheet to copy in - formulas will render unstyled."
16226
16227
  });
16227
16228
  }
16228
16229
  const search = pages.map((p) => ({
@@ -16256,7 +16257,7 @@ async function publishPublication(source, publication, env, options = {}) {
16256
16257
  } else info.push({
16257
16258
  level: "info",
16258
16259
  code: "no-site-url",
16259
- message: "The publication has no `url`, so no sitemap or feed is written; add one to the publication page to get both."
16260
+ message: "The publication has no `url`, so no sitemap or feed is written - add one to the publication page to get both."
16260
16261
  });
16261
16262
  bundle.set(".nojekyll", "");
16262
16263
  const external = /* @__PURE__ */ new Set();
@@ -16267,7 +16268,7 @@ async function publishPublication(source, publication, env, options = {}) {
16267
16268
  if (pages.some((p) => p.rendered.mermaidFallback)) warnings.push({
16268
16269
  level: "warning",
16269
16270
  code: "mermaid-not-prerendered",
16270
- message: "A diagram was not pre-rendered on this host and is left as its source; publish from the browser, or run `etherpk-mcp publish setup`, to get inline SVG."
16271
+ message: "A diagram was not pre-rendered on this host and is left as its source - publish from the browser, or run `etherpk-mcp diagrams setup`, to get inline SVG."
16271
16272
  });
16272
16273
  progress("writing", pages.length + 3, pages.length + 3);
16273
16274
  report.ok = errors.length === 0;
@@ -16360,7 +16361,7 @@ async function fetchTheme(url, fetchText) {
16360
16361
  const { manifest, errors } = parseThemeManifest(json);
16361
16362
  if (!manifest) throw new Error(`The theme at ${url} cannot be used: ${errors.join(" ")}`);
16362
16363
  if (manifest.files.length === 0) throw new Error(`The theme at ${url} lists no files in its manifest, so nothing can be fetched.`);
16363
- if (manifest.files.length > MAX_URL_THEME_FILES) throw new Error(`The theme at ${url} lists ${manifest.files.length} files; a theme may list at most ${MAX_URL_THEME_FILES}.`);
16364
+ if (manifest.files.length > MAX_URL_THEME_FILES) throw new Error(`The theme at ${url} lists ${manifest.files.length} files - a theme may list at most ${MAX_URL_THEME_FILES}.`);
16364
16365
  const base = new URL(url);
16365
16366
  const files = /* @__PURE__ */ new Map();
16366
16367
  for (const path of manifest.files) {
@@ -16456,7 +16457,7 @@ function publicationMapping(input) {
16456
16457
  * page whose purpose was anyone's guess (2026-09-19).
16457
16458
  */
16458
16459
  function publicationPageIntro(name) {
16459
- return `This page defines the publication "${name}". Its settings are the frontmatter above; edit them in Settings → Publish rather than here. The outline below is the site's navigation: one bullet per menu entry, written as a wikilink to the page; nested bullets are sub-entries; a plain bullet with children is a group; a markdown link is an outside link. A page that is public and in this publication but not listed here is still on the site, reachable by search and links, just not in the menu.`;
16460
+ return `This page defines the publication "${name}". Its settings are the frontmatter above - edit them in Settings → Publish rather than here. The outline below is the site's navigation: one bullet per menu entry, written as a wikilink to the page. Nested bullets are sub-entries, a plain bullet with children is a group, and a markdown link is an outside link. A page that is public and in this publication but not listed here is still on the site, reachable by search and links, just not in the menu.`;
16460
16461
  }
16461
16462
  /**
16462
16463
  * Create a publication page: a page whose frontmatter defines the publication and whose body
@@ -16610,7 +16611,7 @@ async function setupDiagrams(env, say) {
16610
16611
  }
16611
16612
  const cli = require$1.resolve("playwright-core/cli.js");
16612
16613
  const dir = env.PLAYWRIGHT_BROWSERS_PATH?.trim() || browsersDir(env);
16613
- say(`Installing Chromium into ${dir} (about 170 MB, once per computer; a diagram is rendered by a browser, and this is the one etherpk-mcp drives)…`);
16614
+ say(`Installing Chromium into ${dir} (about 170 MB, once per computer - a diagram is rendered by a browser, and this is the one etherpk-mcp drives)…`);
16614
16615
  await new Promise((resolve, reject) => {
16615
16616
  const child = spawn(process.execPath, [
16616
16617
  cli,
@@ -16636,7 +16637,7 @@ async function setupDiagrams(env, say) {
16636
16637
  child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`Playwright's installer exited with code ${code}.`)));
16637
16638
  });
16638
16639
  const after = await chromiumStatus(env);
16639
- if (!after.executable) throw new Error("The installer finished but no Chromium executable was found; set ETHERPK_CHROMIUM to a browser on this machine instead.");
16640
+ if (!after.executable) throw new Error("The installer finished but no Chromium executable was found - set ETHERPK_CHROMIUM to a browser on this machine instead.");
16640
16641
  say(`Chromium is set up: ${after.executable}`);
16641
16642
  return after;
16642
16643
  }
@@ -17267,7 +17268,7 @@ async function liveText(graph, identity) {
17267
17268
  return graph.store.open(identity.concept).getText();
17268
17269
  }
17269
17270
  function refuseIfProtected(concept, text) {
17270
- if (documentProtection(text).kind === "document") throw new ToolError("protected_document", `"${concept}" is a protected document. Its content is encrypted under a key this client never holds, so it cannot be read or changed here; the user can unlock it in EtherPK.`);
17271
+ if (documentProtection(text).kind === "document") throw new ToolError("protected_document", `"${concept}" is a protected document. Its content is encrypted under a key this client never holds, so it cannot be read or changed here - the user can unlock it in EtherPK.`);
17271
17272
  }
17272
17273
  function page$1(items, offset, limit) {
17273
17274
  return {
@@ -17470,7 +17471,7 @@ async function editDocument(graph, args) {
17470
17471
  const text = await liveText(graph, identity);
17471
17472
  refuseIfProtected(identity.concept, text);
17472
17473
  const first = text.indexOf(args.old);
17473
- if (first === -1) throw new ToolError("no_match", `"${identity.concept}" does not contain the text to replace. Read the document again; it may have changed.`);
17474
+ if (first === -1) throw new ToolError("no_match", `"${identity.concept}" does not contain the text to replace. Read the document again - it may have changed.`);
17474
17475
  if (text.indexOf(args.old, first + 1) !== -1) throw new ToolError("ambiguous_match", `The text to replace occurs more than once in "${identity.concept}". Include more surrounding text so it matches exactly once.`);
17475
17476
  const insert = normaliseIndentUnit(args.new);
17476
17477
  graph.store.open(identity.concept).applyChange({
@@ -17517,7 +17518,7 @@ async function appendDocument(graph, args) {
17517
17518
  async function createPage(graph, args) {
17518
17519
  const title = args.title.trim();
17519
17520
  if (title === "") throw new ToolError("invalid_argument", "title must not be empty.");
17520
- if (isJournalConcept(title)) throw new ToolError("invalid_argument", `"${title}" is a calendar day, so it names a journal entry; use append_document to write to it.`);
17521
+ if (isJournalConcept(title)) throw new ToolError("invalid_argument", `"${title}" is a calendar day, so it names a journal entry - use append_document to write to it.`);
17521
17522
  await graph.store.refresh();
17522
17523
  if (resolveIdentity(graph, title)) throw new ToolError("already_exists", `A document already answers to "${title}". Use edit_document or append_document instead.`);
17523
17524
  await graph.store.createPage(title, normaliseIndentUnit(args.text ?? ""));
@@ -17847,7 +17848,7 @@ async function listAssets(graph, args = {}) {
17847
17848
  */
17848
17849
  async function readDocuments(graph, args) {
17849
17850
  if (!Array.isArray(args.concepts) || args.concepts.length === 0) throw new ToolError("invalid_argument", "concepts must be a non-empty list of names.");
17850
- if (args.concepts.length > 20) throw new ToolError("too_many", `At most 20 documents per call; ask again for the rest.`);
17851
+ if (args.concepts.length > 20) throw new ToolError("too_many", `At most 20 documents per call - ask again for the rest.`);
17851
17852
  await graph.store.refresh();
17852
17853
  let budget = READ_MANY_TEXT_CAP;
17853
17854
  const documents = [];
@@ -17915,9 +17916,9 @@ async function setTask(graph, args) {
17915
17916
  const lines = text.split("\n");
17916
17917
  const current = lines[args.line];
17917
17918
  const parsed = current === void 0 ? null : parseTaskLine(current);
17918
- if (!parsed) throw new ToolError("task_moved", `Line ${args.line} of "${identity.concept}" is not a task now; read the document or list tasks again.`);
17919
+ if (!parsed) throw new ToolError("task_moved", `Line ${args.line} of "${identity.concept}" is not a task now - read the document or list tasks again.`);
17919
17920
  const afterCheckbox = current.replace(/^\s*-\s+\[[ xX]\]\s?/, "").trim();
17920
- if (args.expect !== void 0 && args.expect.trim() !== afterCheckbox && args.expect.trim() !== parsed.text.trim()) throw new ToolError("task_moved", `Line ${args.line} of "${identity.concept}" now reads "${parsed.text}", not "${args.expect}"; list tasks again before changing it.`);
17921
+ if (args.expect !== void 0 && args.expect.trim() !== afterCheckbox && args.expect.trim() !== parsed.text.trim()) throw new ToolError("task_moved", `Line ${args.line} of "${identity.concept}" now reads "${parsed.text}", not "${args.expect}" - list tasks again before changing it.`);
17921
17922
  const tags = { ...parsed };
17922
17923
  let done = parsed.done;
17923
17924
  switch (changes.status) {
@@ -17994,7 +17995,7 @@ async function graphInfo(graph) {
17994
17995
  complete: status.embedded >= status.total
17995
17996
  } : {
17996
17997
  state: "not-opened",
17997
- note: "The first search with mode \"semantic\" loads the model; if it is not set up on this computer, that search says so and names the command."
17998
+ note: "The first search with mode \"semantic\" loads the model - if it is not set up on this computer, that search says so and names the command."
17998
17999
  },
17999
18000
  assets: { available: graph.assets !== void 0 }
18000
18001
  };
@@ -18110,7 +18111,7 @@ async function createPublication(graph, args, host) {
18110
18111
  await graph.store.refresh();
18111
18112
  const before = summarisePublishing((await graph.publishing.readSource()).source);
18112
18113
  if (before.publications.some((p) => p.id === id)) throw new ToolError("already_exists", `A publication with the id "${id}" already exists (page "${before.publications.find((p) => p.id === id).concept}").`);
18113
- if (graph.store.listDocuments().some((d) => d.key === title.toLowerCase() || d.aliases.some((a) => a.toLowerCase() === title.toLowerCase()))) throw new ToolError("already_exists", `A document already answers to "${title}"; choose another title for the publication page.`);
18114
+ if (graph.store.listDocuments().some((d) => d.key === title.toLowerCase() || d.aliases.some((a) => a.toLowerCase() === title.toLowerCase()))) throw new ToolError("already_exists", `A document already answers to "${title}" - choose another title for the publication page.`);
18114
18115
  if (args.theme) await requireTheme(graph, args.theme);
18115
18116
  const store = frontmatterStore(graph);
18116
18117
  const concept = await createPublicationPage(store, {
@@ -18363,8 +18364,8 @@ var SAMPLE_SOURCE = {
18363
18364
  ""
18364
18365
  ].join("\n")),
18365
18366
  page("[[Getting Started]] Installing", "---\npublic: true\n---\n- A scoped concept: its heading links to its scope. Back to [[Getting Started]].\n"),
18366
- page("Wikilinks", "---\npublic: true\naliases: [Links]\n---\n- `[[Concept]]` links to a page; an alias like [[Links]] resolves to this one.\n", ["Links"]),
18367
- page("Private Notes", "- Not public, so never on the site; links to it are styled as missing.\n"),
18367
+ page("Wikilinks", "---\npublic: true\naliases: [Links]\n---\n- `[[Concept]]` links to a page - an alias like [[Links]] resolves to this one.\n", ["Links"]),
18368
+ page("Private Notes", "- Not public, so never on the site - links to it are styled as missing.\n"),
18368
18369
  {
18369
18370
  concept: "2026-06-02",
18370
18371
  kind: "journal",
@@ -18587,7 +18588,7 @@ async function customisePublicationTheme(graph, args) {
18587
18588
  await graph.store.refresh();
18588
18589
  const { source } = await graph.publishing.readSource();
18589
18590
  const publication = summarisePublishing(source).publications.find((p) => p.id === args.publication.trim());
18590
- if (!publication) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}"; list_publications shows them.`);
18591
+ if (!publication) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}" - list_publications shows them.`);
18591
18592
  const current = await graph.themes.get(publication.theme);
18592
18593
  if (current) return {
18593
18594
  created: false,
@@ -18623,7 +18624,7 @@ async function requireEditable(graph, id) {
18623
18624
  const own = await graph.themes.get(id);
18624
18625
  if (own) return own;
18625
18626
  if (bundledTheme(id)) throw new ToolError("theme_not_editable", `"${id}" is a bundled theme and cannot be edited in place. create_theme (or customise_publication_theme for the publication that uses it) makes a copy in the graph that can be.`);
18626
- throw new ToolError("theme_not_found", `No theme in this graph is called "${id}"; list_themes shows them.`);
18627
+ throw new ToolError("theme_not_found", `No theme in this graph is called "${id}" - list_themes shows them.`);
18627
18628
  }
18628
18629
  function requireThemePath(path) {
18629
18630
  const clean = path.trim().replace(/^\.?\//, "");
@@ -18723,7 +18724,7 @@ async function importThemeFolder(graph, args) {
18723
18724
  } catch (error) {
18724
18725
  throw new ToolError("invalid_argument", `Cannot read "${args.dir}": ${error instanceof Error ? error.message : String(error)}`);
18725
18726
  }
18726
- if (!files.has("theme.json")) throw new ToolError("invalid_argument", `"${args.dir}" has no theme.json; a theme folder has one at its top.`);
18727
+ if (!files.has("theme.json")) throw new ToolError("invalid_argument", `"${args.dir}" has no theme.json - a theme folder has one at its top.`);
18727
18728
  const next = {
18728
18729
  ...theme,
18729
18730
  files: Object.fromEntries(files),
@@ -18745,7 +18746,7 @@ async function deleteTheme(graph, args) {
18745
18746
  await graph.store.refresh();
18746
18747
  const theme = await requireEditable(graph, args.id.trim());
18747
18748
  const users = await publicationsUsing(graph, theme.id);
18748
- if (users.length > 0) throw new ToolError("theme_in_use", `"${theme.id}" is the theme of ${users.map((p) => `"${p.name}"`).join(", ")}; point ${users.length === 1 ? "that publication" : "those publications"} at another theme (update_publication) first.`);
18749
+ if (users.length > 0) throw new ToolError("theme_in_use", `"${theme.id}" is the theme of ${users.map((p) => `"${p.name}"`).join(", ")} - point ${users.length === 1 ? "that publication" : "those publications"} at another theme (update_publication) first.`);
18749
18750
  await graph.themes.remove(theme.id);
18750
18751
  await settle(graph);
18751
18752
  return {
@@ -18770,7 +18771,7 @@ async function previewTheme(graph, args, host) {
18770
18771
  if (args.publication?.trim()) {
18771
18772
  const read = await graph.publishing.readSource();
18772
18773
  const found = summarisePublishing(read.source).publications.find((p) => p.id === args.publication.trim());
18773
- if (!found) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}"; list_publications shows them.`);
18774
+ if (!found) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}" - list_publications shows them.`);
18774
18775
  publication = found;
18775
18776
  source = read.source;
18776
18777
  unsettled = read.unsettled;
@@ -18814,7 +18815,7 @@ async function previewTheme(graph, args, host) {
18814
18815
  ...screenshots ? { screenshots } : {},
18815
18816
  ...chromium ? {
18816
18817
  screenshots: null,
18817
- note: `No browser to photograph with; run ${chromium.setupCommand} or set ETHERPK_CHROMIUM.`
18818
+ note: `No browser to photograph with - run ${chromium.setupCommand} or set ETHERPK_CHROMIUM.`
18818
18819
  } : {}
18819
18820
  };
18820
18821
  } finally {
@@ -18928,25 +18929,25 @@ function createMcpServer(graph, info) {
18928
18929
  version: info.version
18929
18930
  }, { instructions: [
18930
18931
  `You are connected to the EtherPK knowledge graph "${info.graphName}": daily journal entries and titled pages in plain markdown, linked with [[wikilinks]].`,
18931
- "Concept names are case-insensitive. A protected document is listed with protected: true and cannot be read or written here; tell the user to unlock it in EtherPK if a task needs it.",
18932
- "Bullets are \"- \" with two-space nesting; tasks are \"- [ ]\" / \"- [x]\" with task tags (#P1 #P2 #P3, #W #D #C, #D-YYYY-MM-DD due, #S-YYYY-MM-DD scheduled) right after the checkbox.",
18933
- "read_document returns a document's body as text and its frontmatter block as data (the \"frontmatter\" object); the block is never in the text, so line numbers from search, backlinks and tasks match the text. Set frontmatter keys with set_frontmatter (public, publications, date, author, description and any other key); a document's title and aliases and a publication page's definition are not frontmatter here and have tools of their own.",
18934
- "Edit with edit_document (an exact, unique old→new replacement) or append_document; read first, then edit, and never paste a whole page back.",
18935
- "To rename a page or concept use plan_rename then rename, never a frontmatter edit: rename carries scoped concepts (\"[[Old]] Notes\") along and by default rewrites every [[Old]] link in the graph to the new name (strategy \"rewrite\"); strategy \"alias\" keeps the old name resolving instead. A rename onto a name that is taken merges two documents and needs confirm_merge: true. References inside protected documents cannot be seen or rewritten.",
18936
- "search has two kinds of matching: mode \"text\" (default) matches words and quoted phrases; mode \"semantic\" finds passages about what a question or description means even when no words match. Use semantic for questions and descriptions, text for names, identifiers and exact phrases; \"hybrid\" returns both groups. When you answer from a search result, cite the document and where in it (its breadcrumb or lines). Semantic mode needs a one-time \"semantic setup\" on this computer; if it is not set up, search says so and names the command.",
18937
- "Start with graph_info to see what you are connected to. read_documents reads several documents in one call; tasks lists tasks and set_task changes one (status, priority, due and scheduled dates) by document and line.",
18938
- "Publishing: list_publications shows the publications this graph defines (a publication is a page whose frontmatter defines it; its outline is the site navigation) and the public documents none takes; a document is on a site when its frontmatter has public: true and names the publication in publications. create_publication and update_publication change the settings; publish writes the site into the publish folder the user set for it on this machine with the etherpk-mcp publish command (the tool cannot choose a folder) and returns the report. Diagrams need a browser the user installs once with \"diagrams setup\".",
18939
- "Themes: a publication's look is a theme - Mustache templates, a stylesheet, a script and a manifest. list_themes shows the bundled ones (read-only) and the graph's own; read_theme writes a theme's files to a folder on this machine to read and edit; customise_publication_theme copies a publication's bundled theme into the graph and points the publication at the copy (create_theme copies any theme); write_theme_file, delete_theme_file and import_theme_folder change a graph theme; preview_theme renders a theme to a folder (with screenshots when a browser is set up) to check before publish. For a snippet such as an analytics script, an include slot (update_publication includes, e.g. head) filled by a page may be lighter than a theme copy.",
18940
- "Images and files are assets: upload_asset adds a file from this machine and returns the markdown to paste into a document; read_asset writes an asset to a local file you can open; list_assets shows the assets documents reference. An asset is available only where a document you can read references it (or you uploaded it this session). read_asset, read_theme and preview_theme write under the graph's downloads directory and return the path; name a folder relative to it, never elsewhere.",
18932
+ "Concept names are case-insensitive. A protected document is listed with protected: true and cannot be read or written here - tell the user to unlock it in EtherPK if a task needs it.",
18933
+ "Bullets are \"- \" with two-space nesting. Tasks are \"- [ ]\" / \"- [x]\" with task tags (#P1 #P2 #P3, #W #D #C, #D-YYYY-MM-DD due, #S-YYYY-MM-DD scheduled) right after the checkbox.",
18934
+ "read_document returns a document's body as text and its frontmatter block as data (the \"frontmatter\" object) - the block is never in the text, so line numbers from search, backlinks and tasks match the text. Set frontmatter keys with set_frontmatter (public, publications, date, author, description and any other key) - a document's title and aliases and a publication page's definition are not frontmatter here and have tools of their own.",
18935
+ "Edit with edit_document (an exact, unique old→new replacement) or append_document - read first, then edit, and never paste a whole page back.",
18936
+ "To rename a page or concept use plan_rename then rename, never a frontmatter edit: rename carries scoped concepts (\"[[Old]] Notes\") along and by default rewrites every [[Old]] link in the graph to the new name (strategy \"rewrite\") - strategy \"alias\" keeps the old name resolving instead. A rename onto a name that is taken merges two documents and needs confirm_merge: true. References inside protected documents cannot be seen or rewritten.",
18937
+ "search has two kinds of matching: mode \"text\" (default) matches words and quoted phrases, and mode \"semantic\" finds passages about what a question or description means even when no words match. Use semantic for questions and descriptions, text for names, identifiers and exact phrases. \"hybrid\" returns both groups. When you answer from a search result, cite the document and where in it (its breadcrumb or lines). Semantic mode needs a one-time \"semantic setup\" on this computer - if it is not set up, search says so and names the command.",
18938
+ "Start with graph_info to see what you are connected to. read_documents reads several documents in one call. tasks lists tasks and set_task changes one (status, priority, due and scheduled dates) by document and line.",
18939
+ "Publishing: list_publications shows the publications this graph defines (a publication is a page whose frontmatter defines it - its outline is the site navigation) and the public documents none takes. A document is on a site when its frontmatter has public: true and names the publication in publications. create_publication and update_publication change the settings, and publish writes the site into the publish folder the user set for it on this machine with the etherpk-mcp publish command (the tool cannot choose a folder) and returns the report. Diagrams need a browser the user installs once with \"diagrams setup\".",
18940
+ "Themes: a publication's look is a theme - Mustache templates, a stylesheet, a script and a manifest. list_themes shows the bundled ones (read-only) and the graph's own. read_theme writes a theme's files to a folder on this machine to read and edit. customise_publication_theme copies a publication's bundled theme into the graph and points the publication at the copy (create_theme copies any theme). write_theme_file, delete_theme_file and import_theme_folder change a graph theme. preview_theme renders a theme to a folder (with screenshots when a browser is set up) to check before publish. For a snippet such as an analytics script, an include slot (update_publication includes, e.g. head) filled by a page may be lighter than a theme copy.",
18941
+ "Images and files are assets: upload_asset adds a file from this machine and returns the markdown to paste into a document. read_asset writes an asset to a local file you can open, and list_assets shows the assets documents reference. An asset is available only where a document you can read references it (or you uploaded it this session). read_asset, read_theme and preview_theme write under the graph's downloads directory and return the path - name a folder relative to it, never elsewhere.",
18941
18942
  "The user documentation is at https://docs.etherpk.com."
18942
18943
  ].join("\n") });
18943
18944
  server.registerTool("list_documents", {
18944
18945
  title: "List documents",
18945
- description: `Every page and journal entry in the graph with its aliases; protected documents carry protected: true and have no readable body. from / to narrow to the journal entries of a range of days. Paged, at most 200 per call.`,
18946
+ description: `Every page and journal entry in the graph with its aliases - protected documents carry protected: true and have no readable body. from / to narrow to the journal entries of a range of days. Paged, at most 200 per call.`,
18946
18947
  inputSchema: {
18947
18948
  kind: z.enum(["page", "journal"]).optional().describe("Only pages, or only journal entries."),
18948
- from: z.string().optional().describe("Journal entries on or after this day (YYYY-MM-DD); implies kind journal."),
18949
- to: z.string().optional().describe("Journal entries on or before this day (YYYY-MM-DD); implies kind journal."),
18949
+ from: z.string().optional().describe("Journal entries on or after this day (YYYY-MM-DD) - implies kind journal."),
18950
+ to: z.string().optional().describe("Journal entries on or before this day (YYYY-MM-DD) - implies kind journal."),
18950
18951
  offset,
18951
18952
  limit: z.number().int().positive().max(200).optional()
18952
18953
  }
@@ -18963,7 +18964,7 @@ function createMcpServer(graph, info) {
18963
18964
  }, async (args) => run(() => readDocuments(graph, args)));
18964
18965
  server.registerTool("search", {
18965
18966
  title: "Search",
18966
- description: `Search every unprotected document. mode "text" (default): words match by prefix, "quoted phrases" match exactly; documents come most-matches-first with the matching lines. In text mode a term key:value is a property filter on the documents' frontmatter when some document has that key (keys and values ignore case; key:"two words", -key:value excludes, key:* means the key is set, key:va* matches the start of a value, publication.id:docs reaches a nested key, title: and aliases: match the document's names); filters alone list every matching document with the values they matched and no hits. mode "semantic": the passages closest in MEANING to the query, best first, each with its text, a similarity 0..1 and where it is (concept, breadcrumb of headings and parent bullets, start and end line) - finds a note that says "self-assessment is due 31 January" for "tax deadline". Line numbers are 0-based in every mode. Passage text is the note's own words with bullet markers and [[ ]] stripped: quote and cite it, but anchor edit_document on text from read_document. Use semantic for questions and descriptions, text for names, identifiers and exact phrases; mode "hybrid" returns both as separate groups. Semantic results carry complete: false while this computer is still embedding the graph (rerun later for more). If semantic mode answers error semantic_unavailable, it is not set up on this computer: tell the user the command in the message. At most 50 documents per call.`,
18967
+ description: `Search every unprotected document. mode "text" (default): words match by prefix, "quoted phrases" match exactly. Documents come most-matches-first with the matching lines. In text mode a term key:value is a property filter on the documents' frontmatter when some document has that key, and keys and values ignore case (key:"two words", -key:value excludes, key:* means the key is set, key:va* matches the start of a value, publication.id:docs reaches a nested key, title: and aliases: match the document's names). Filters alone list every matching document with the values they matched and no hits. mode "semantic": the passages closest in MEANING to the query, best first, each with its text, a similarity 0..1 and where it is (concept, breadcrumb of headings and parent bullets, start and end line) - finds a note that says "self-assessment is due 31 January" for "tax deadline". Line numbers are 0-based in every mode. Passage text is the note's own words with bullet markers and [[ ]] stripped: quote and cite it, but anchor edit_document on text from read_document. Use semantic for questions and descriptions, text for names, identifiers and exact phrases. mode "hybrid" returns both as separate groups. Semantic results carry complete: false while this computer is still embedding the graph (rerun later for more). If semantic mode answers error semantic_unavailable, it is not set up on this computer: tell the user the command in the message. At most 50 documents per call.`,
18967
18968
  inputSchema: {
18968
18969
  query: z.string().min(1),
18969
18970
  mode: z.enum([
@@ -19010,7 +19011,7 @@ function createMcpServer(graph, info) {
19010
19011
  }, async (args) => run(() => tasks(graph, args)));
19011
19012
  server.registerTool("set_task", {
19012
19013
  title: "Set a task",
19013
- description: "Change one task by document and 0-based line (as tasks and read_document count lines): its status (open, doing, waiting, done, cancelled - exclusive), priority (1, 2, 3 or null), due and scheduled days (YYYY-MM-DD or null). Pass expect with the task's text from tasks; if the line no longer holds that task the change is refused with error \"task_moved\" and nothing is edited.",
19014
+ description: "Change one task by document and 0-based line (as tasks and read_document count lines): its status (open, doing, waiting, done, cancelled - exclusive), priority (1, 2, 3 or null), due and scheduled days (YYYY-MM-DD or null). Pass expect with the task's text from tasks - if the line no longer holds that task the change is refused with error \"task_moved\" and nothing is edited.",
19014
19015
  inputSchema: {
19015
19016
  concept,
19016
19017
  line: z.number().int().nonnegative(),
@@ -19036,7 +19037,7 @@ function createMcpServer(graph, info) {
19036
19037
  }, async (args) => run(() => setTask(graph, args)));
19037
19038
  server.registerTool("edit_document", {
19038
19039
  title: "Edit a document",
19039
- description: "Replace one exact occurrence of \"old\" with \"new\" in a document. \"old\" must appear exactly once (include surrounding lines to disambiguate); the change is applied as one edit, so on a synced graph it merges with anyone typing elsewhere in the page, and on a folder it is in the file when this returns. Refused on a protected document.",
19040
+ description: "Replace one exact occurrence of \"old\" with \"new\" in a document. \"old\" must appear exactly once (include surrounding lines to disambiguate). The change is applied as one edit, so on a synced graph it merges with anyone typing elsewhere in the page, and on a folder it is in the file when this returns. Refused on a protected document.",
19040
19041
  inputSchema: {
19041
19042
  concept,
19042
19043
  old: z.string().min(1).describe("The exact text to replace, as read_document returned it."),
@@ -19070,7 +19071,7 @@ function createMcpServer(graph, info) {
19070
19071
  }, async (args) => run(() => planRename(graph, args)));
19071
19072
  server.registerTool("rename", {
19072
19073
  title: "Rename",
19073
- description: "Rename a page or concept. Its page (if any) and every scoped concept beneath it (\"[[Old]] Notes\" becomes \"[[New]] Notes\") move with it. strategy \"rewrite\" (default) rewrites every [[Old]] link in the graph to the new name; \"alias\" keeps the old name as an alias so existing links keep resolving. If the new name is already taken the two documents would merge, which is irreversible: refused with error \"merge_requires_confirmation\" unless confirm_merge is true. Refused with \"unconfirmed_documents\" if a document it must read has not finished syncing to this device (nothing changes; try again). Call plan_rename first to see what will happen.",
19074
+ description: "Rename a page or concept. Its page (if any) and every scoped concept beneath it (\"[[Old]] Notes\" becomes \"[[New]] Notes\") move with it. strategy \"rewrite\" (default) rewrites every [[Old]] link in the graph to the new name. \"alias\" keeps the old name as an alias so existing links keep resolving. If the new name is already taken the two documents would merge, which is irreversible: refused with error \"merge_requires_confirmation\" unless confirm_merge is true. Refused with \"unconfirmed_documents\" if a document it must read has not finished syncing to this device (nothing changes - try again). Call plan_rename first to see what will happen.",
19074
19075
  inputSchema: {
19075
19076
  from: z.string().min(1),
19076
19077
  to: z.string().min(1),
@@ -19083,7 +19084,7 @@ function createMcpServer(graph, info) {
19083
19084
  description: "Replace a document's aliases: other names it answers to in [[wikilinks]], Quick Find and on a published site. The list you pass is the complete list. A name that is already another document's title or alias is refused with error \"name_taken\".",
19084
19085
  inputSchema: {
19085
19086
  concept,
19086
- aliases: z.array(z.string()).describe("Every alias the document should have; an empty list removes them all.")
19087
+ aliases: z.array(z.string()).describe("Every alias the document should have - an empty list removes them all.")
19087
19088
  }
19088
19089
  }, async (args) => run(() => setAliases(graph, args)));
19089
19090
  server.registerTool("set_frontmatter", {
@@ -19091,23 +19092,23 @@ function createMcpServer(graph, info) {
19091
19092
  description: "Set keys of a document's frontmatter block, adding the block if there is none. A merge patch: the keys you name are set, null removes a key, every other key keeps its value. \"public\" (true/false) and \"publications\" (a list of publication ids: lower-case letters, digits and hyphens) decide what is published, and are validated. Refuses \"title\", \"aliases\" and \"publication\" with error \"identity_key\" (use rename, set_aliases and the publication tools), and a protected document. Returns the resulting frontmatter.",
19092
19093
  inputSchema: {
19093
19094
  concept,
19094
- patch: z.record(z.string(), frontmatterValue).describe("Keys to set; null removes a key. Example: {\"public\": true, \"publications\": [\"blog\"], \"date\": \"2026-01-31\", \"draft\": null}.")
19095
+ patch: z.record(z.string(), frontmatterValue).describe("Keys to set - null removes a key. Example: {\"public\": true, \"publications\": [\"blog\"], \"date\": \"2026-01-31\", \"draft\": null}.")
19095
19096
  }
19096
19097
  }, async (args) => run(() => setFrontmatter(graph, args)));
19097
19098
  server.registerTool("upload_asset", {
19098
19099
  title: "Upload an asset",
19099
- description: "Add a file on this machine (an image, a PDF, any file) to the graph as an asset. Returns the reference and the markdown to paste into a document (an image tag for an image, a link otherwise). Bytes the graph already holds are not stored twice: the result then says reused: true and points at the existing asset. The file is stored as it is, without image optimisation. Refused with error \"invalid_argument\" for a folder, a file over 100 MiB, a hidden file or one in a hidden folder (.ssh, .env), and the Headless Client's own config and cache; with error \"asset_refused\" when the server declines it on a quota.",
19100
+ description: "Add a file on this machine (an image, a PDF, any file) to the graph as an asset. Returns the reference and the markdown to paste into a document (an image tag for an image, a link otherwise). Bytes the graph already holds are not stored twice: the result then says reused: true and points at the existing asset. The file is stored as it is, without image optimisation. Refused with error \"invalid_argument\" for a folder, a file over 100 MiB, a hidden file or one in a hidden folder (.ssh, .env), and the Headless Client's own config and cache. Refused with error \"asset_refused\" when the server declines it on a quota.",
19100
19101
  inputSchema: {
19101
19102
  path: z.string().min(1).describe("Absolute path of an ordinary file on this machine."),
19102
- name: z.string().min(1).optional().describe("The name to store it under; the file's own name by default.")
19103
+ name: z.string().min(1).optional().describe("The name to store it under - the file's own name by default.")
19103
19104
  }
19104
19105
  }, async (args) => run(() => uploadAsset(graph, args)));
19105
19106
  server.registerTool("read_asset", {
19106
19107
  title: "Read an asset",
19107
- description: "Write an asset's bytes to a file on this machine and return the path, so you can open or view it. Pass the reference as a document shows it (\"../assets/<name>\") or the name alone. Available only where a document you can read references the asset (or you uploaded it this session); otherwise error \"asset_not_found\". The file is written under the graph's downloads directory, named after the asset, and never over an existing file.",
19108
+ description: "Write an asset's bytes to a file on this machine and return the path, so you can open or view it. Pass the reference as a document shows it (\"../assets/<name>\") or the name alone. Available only where a document you can read references the asset (or you uploaded it this session) - otherwise error \"asset_not_found\". The file is written under the graph's downloads directory, named after the asset, and never over an existing file.",
19108
19109
  inputSchema: {
19109
19110
  ref: z.string().min(1),
19110
- out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it; the downloads directory itself by default. A folder outside it is refused.")
19111
+ out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it - the downloads directory itself by default. A folder outside it is refused.")
19111
19112
  }
19112
19113
  }, async (args) => run(() => readAsset(graph, args)));
19113
19114
  server.registerTool("list_assets", {
@@ -19121,25 +19122,25 @@ function createMcpServer(graph, info) {
19121
19122
  };
19122
19123
  server.registerTool("list_publications", {
19123
19124
  title: "List publications",
19124
- description: "The publications this graph defines - id, name, the page that defines each, kind (docs or blog), selection (named: documents that list it in publications; all-public: every public document), home, url, theme, include slots, and the publish folder set for it on this machine (null when none) - with any issues in their pages and the public documents no publication takes.",
19125
+ description: "The publications this graph defines - id, name, the page that defines each, kind (docs or blog), selection (named: documents that list it in publications, all-public: every public document), home, url, theme, include slots, and the publish folder set for it on this machine (null when none) - with any issues in their pages and the public documents no publication takes.",
19125
19126
  inputSchema: {}
19126
19127
  }, async () => run(() => listPublications(graph, host)));
19127
19128
  server.registerTool("create_publication", {
19128
19129
  title: "Create a publication",
19129
- description: "Create a publication: a page titled with its name whose frontmatter defines it and whose outline (bullets of [[wikilinks]]) is the site navigation. id defaults to a slug of the title; kind is docs (default) or blog; selection is named (default: a document joins by listing the id in its publications) or all-public. theme is a bundled theme name (etherpk-docs, etherpk-blog), a theme in the graph, or a url.",
19130
+ description: "Create a publication: a page titled with its name whose frontmatter defines it and whose outline (bullets of [[wikilinks]]) is the site navigation. id defaults to a slug of the title. kind is docs (default) or blog. selection is named (default: a document joins by listing the id in its publications) or all-public. theme is a bundled theme name (etherpk-docs, etherpk-blog), a theme in the graph, or a url.",
19130
19131
  inputSchema: {
19131
19132
  title: z.string().min(1),
19132
19133
  id: z.string().min(1).optional(),
19133
19134
  kind: z.enum(["docs", "blog"]).optional(),
19134
19135
  selection: z.enum(["named", "all-public"]).optional(),
19135
- home: z.string().min(1).optional().describe("The page whose content is the front page; absent, an index is generated."),
19136
+ home: z.string().min(1).optional().describe("The page whose content is the front page - absent, an index is generated."),
19136
19137
  url: z.string().min(1).optional().describe("Where the site will be deployed, for its feed and sitemap."),
19137
19138
  theme: z.string().min(1).optional()
19138
19139
  }
19139
19140
  }, async (args) => run(() => createPublication(graph, args, host)));
19140
19141
  server.registerTool("update_publication", {
19141
19142
  title: "Update a publication",
19142
- description: "Change a publication's settings on its page. Each change is optional; null removes a field (back to its default) or clears an include slot. includes maps a theme's slot name (head, logo, header, footer, before, after, scripts, styles, or one the theme adds) to the page that fills it; that page must be public and in the publication.",
19143
+ description: "Change a publication's settings on its page. Each change is optional - null removes a field (back to its default) or clears an include slot. includes maps a theme's slot name (head, logo, header, footer, before, after, scripts, styles, or one the theme adds) to the page that fills it - that page must be public and in the publication.",
19143
19144
  inputSchema: {
19144
19145
  id: z.string().min(1),
19145
19146
  changes: z.object({
@@ -19148,27 +19149,27 @@ function createMcpServer(graph, info) {
19148
19149
  home: z.string().nullable().optional(),
19149
19150
  url: z.string().nullable().optional(),
19150
19151
  theme: z.string().nullable().optional(),
19151
- recent: z.number().int().nonnegative().nullable().optional().describe("Posts on a blog's front page; null for the default (10)."),
19152
+ recent: z.number().int().nonnegative().nullable().optional().describe("Posts on a blog's front page - null for the default (10)."),
19152
19153
  includes: z.record(z.string(), z.string().nullable()).optional()
19153
19154
  })
19154
19155
  }
19155
19156
  }, async (args) => run(() => updatePublication(graph, args, host)));
19156
19157
  server.registerTool("publish", {
19157
19158
  title: "Publish",
19158
- description: "Render a publication to its publish folder on this machine and return the report: what was included and why documents were left out, missing links, assets, warnings. The folder is the one the user set with \"etherpk-mcp publish --publication <id> --out <dir>\" (error \"no_publish_folder\" until then; the tool never chooses a folder). Pages with Mermaid diagrams need the browser from \"diagrams setup\" (error \"chromium_unavailable\"). The report is this result, trimmed to counts and first entries; nothing of it is written into the folder, so the site never names the documents it leaves out. Publishing writes files; it does not deploy them.",
19159
+ description: "Render a publication to its publish folder on this machine and return the report: what was included and why documents were left out, missing links, assets, warnings. The folder is the one the user set with \"etherpk-mcp publish --publication <id> --out <dir>\" (error \"no_publish_folder\" until then - the tool never chooses a folder). Pages with Mermaid diagrams need the browser from \"diagrams setup\" (error \"chromium_unavailable\"). The report is this result, trimmed to counts and first entries - nothing of it is written into the folder, so the site never names the documents it leaves out. Publishing writes files - it does not deploy them.",
19159
19160
  inputSchema: { id: z.string().min(1) }
19160
19161
  }, async (args) => run(() => publish(graph, args, host)));
19161
19162
  server.registerTool("list_themes", {
19162
19163
  title: "List themes",
19163
- description: "Every theme a publication here can use: the bundled ones (etherpk-docs, etherpk-blog; read-only, with their files and manifest) and the graph's own (editable; id, name, origin, files, manifest, validation errors, and which publications use each).",
19164
+ description: "Every theme a publication here can use: the bundled ones (etherpk-docs and etherpk-blog, read-only, with their files and manifest) and the graph's own (editable, with id, name, origin, files, manifest, validation errors, and which publications use each).",
19164
19165
  inputSchema: {}
19165
19166
  }, async () => run(() => listThemes(graph)));
19166
19167
  server.registerTool("read_theme", {
19167
19168
  title: "Read a theme",
19168
- description: "Write a theme's files (theme.json, layouts/, partials/, assets/) to a folder on this machine and return the path and file list, so you can read and edit them with your own tools. ref is a graph theme id, a bundled theme name or a url. Editing the folder changes nothing until write_theme_file or import_theme_folder brings it back; a bundled or url theme cannot be edited in place at all (create_theme copies it).",
19169
+ description: "Write a theme's files (theme.json, layouts/, partials/, assets/) to a folder on this machine and return the path and file list, so you can read and edit them with your own tools. ref is a graph theme id, a bundled theme name or a url. Editing the folder changes nothing until write_theme_file or import_theme_folder brings it back. A bundled or url theme cannot be edited in place at all (create_theme copies it).",
19169
19170
  inputSchema: {
19170
19171
  ref: z.string().min(1),
19171
- out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it; themes/<ref> there by default. A folder outside it is refused.")
19172
+ out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it - themes/<ref> there by default. A folder outside it is refused.")
19172
19173
  }
19173
19174
  }, async (args) => run(() => readTheme(graph, args)));
19174
19175
  server.registerTool("read_theme_file", {
@@ -19181,19 +19182,19 @@ function createMcpServer(graph, info) {
19181
19182
  }, async (args) => run(() => readThemeFile(graph, args)));
19182
19183
  server.registerTool("create_theme", {
19183
19184
  title: "Create a theme",
19184
- description: "Copy a theme into the graph as one of its own, ready to edit: from a bundled theme name, a url, or another theme in the graph. Returns the new theme; point a publication at it with update_publication (theme: <id>).",
19185
+ description: "Copy a theme into the graph as one of its own, ready to edit: from a bundled theme name, a url, or another theme in the graph. Returns the new theme - point a publication at it with update_publication (theme: <id>).",
19185
19186
  inputSchema: {
19186
19187
  from: z.string().min(1),
19187
- id: z.string().min(1).optional().describe("Lower-case letters, digits and hyphens; from the source by default."),
19188
+ id: z.string().min(1).optional().describe("Lower-case letters, digits and hyphens - from the source by default."),
19188
19189
  name: z.string().min(1).optional()
19189
19190
  }
19190
19191
  }, async (args) => run(() => createTheme(graph, args)));
19191
19192
  server.registerTool("customise_publication_theme", {
19192
19193
  title: "Customise a publication's theme",
19193
- description: "Make a publication's theme editable: if it uses a bundled or url theme, copy that theme into the graph and point the publication at the copy; if it already uses a graph theme, return it. The step before editing the look of a published site.",
19194
+ description: "Make a publication's theme editable: if it uses a bundled or url theme, copy that theme into the graph and point the publication at the copy. If it already uses a graph theme, return it. The step before editing the look of a published site.",
19194
19195
  inputSchema: {
19195
19196
  publication: z.string().min(1).describe("The publication id."),
19196
- id: z.string().min(1).optional().describe("The id for the copy; <publication>-theme by default."),
19197
+ id: z.string().min(1).optional().describe("The id for the copy - <publication>-theme by default."),
19197
19198
  name: z.string().min(1).optional()
19198
19199
  }
19199
19200
  }, async (args) => run(() => customisePublicationTheme(graph, args)));
@@ -19208,7 +19209,7 @@ function createMcpServer(graph, info) {
19208
19209
  }, async (args) => run(() => writeThemeFile(graph, args)));
19209
19210
  server.registerTool("delete_theme_file", {
19210
19211
  title: "Delete a theme file",
19211
- description: "Remove one file from a graph theme; the theme is validated afterwards and problems returned as errors.",
19212
+ description: "Remove one file from a graph theme - the theme is validated afterwards and problems returned as errors.",
19212
19213
  inputSchema: {
19213
19214
  id: z.string().min(1),
19214
19215
  path: z.string().min(1)
@@ -19216,7 +19217,7 @@ function createMcpServer(graph, info) {
19216
19217
  }, async (args) => run(() => deleteThemeFile(graph, args)));
19217
19218
  server.registerTool("import_theme_folder", {
19218
19219
  title: "Import a theme folder",
19219
- description: "Replace a graph theme's files with a folder's contents - the way back after editing what read_theme wrote. Files the folder no longer has are removed from the theme; the folder needs a theme.json at its top. dir is the folder read_theme returned, or another under the graph's downloads directory; a folder outside it is refused, and symbolic links in it are skipped. Validated afterwards.",
19220
+ description: "Replace a graph theme's files with a folder's contents - the way back after editing what read_theme wrote. Files the folder no longer has are removed from the theme - the folder needs a theme.json at its top. dir is the folder read_theme returned, or another under the graph's downloads directory - a folder outside it is refused, and symbolic links in it are skipped. Validated afterwards.",
19220
19221
  inputSchema: {
19221
19222
  id: z.string().min(1),
19222
19223
  dir: z.string().min(1)
@@ -19224,12 +19225,12 @@ function createMcpServer(graph, info) {
19224
19225
  }, async (args) => run(() => importThemeFolder(graph, args)));
19225
19226
  server.registerTool("delete_theme", {
19226
19227
  title: "Delete a theme",
19227
- description: "Remove a graph theme. Refused with error \"theme_in_use\" while a publication's saved settings name it; point the publication at another theme first.",
19228
+ description: "Remove a graph theme. Refused with error \"theme_in_use\" while a publication's saved settings name it - point the publication at another theme first.",
19228
19229
  inputSchema: { id: z.string().min(1) }
19229
19230
  }, async (args) => run(() => deleteTheme(graph, args)));
19230
19231
  server.registerTool("preview_theme", {
19231
19232
  title: "Preview a theme",
19232
- description: "Render a theme to a folder on this machine and return where: a publication's real pages when a publication is given (with its own theme, or the theme named), or a sample site covering every construct when only a theme is given. Open the HTML to check markup and styles. With screenshots: true and a browser set up (diagrams setup or ETHERPK_CHROMIUM), the front page and one content page are photographed at desktop and phone widths as PNGs you can view. A preview is scratch, not the publish folder. The page loads nothing from the network while it is photographed. out_dir is a folder under the graph's downloads directory (previews/<name> there by default); a folder outside it is refused.",
19233
+ description: "Render a theme to a folder on this machine and return where: a publication's real pages when a publication is given (with its own theme, or the theme named), or a sample site covering every construct when only a theme is given. Open the HTML to check markup and styles. With screenshots: true and a browser set up (diagrams setup or ETHERPK_CHROMIUM), the front page and one content page are photographed at desktop and phone widths as PNGs you can view. A preview is scratch, not the publish folder. The page loads nothing from the network while it is photographed. out_dir is a folder under the graph's downloads directory (previews/<name> there by default) - a folder outside it is refused.",
19233
19234
  inputSchema: {
19234
19235
  theme: z.string().min(1).optional(),
19235
19236
  publication: z.string().min(1).optional(),
@@ -19465,20 +19466,20 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
19465
19466
  Serve one synced graph to an agent over stdio. For Claude Code:
19466
19467
  claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --sync-server <url> --graph <id>
19467
19468
  Once "semantic setup" has run on this computer, serve also keeps a search-by-meaning
19468
- store of the graph current (the agent's search tool gains mode: semantic); pass
19469
+ store of the graph current (the agent's search tool gains mode: semantic) - pass
19469
19470
  --no-semantic to leave it off for this registration.
19470
19471
  ${CMD} serve --folder <path> [--no-semantic]
19471
19472
  Serve a local graph folder the same way: no sign-in, no server. The agent gets search,
19472
19473
  backlinks, tasks and format-safe edits over the folder's markdown, alongside the files
19473
19474
  themselves. Edits made in an editor or by the agent directly are picked up as they land.
19474
- The folder must already be a graph (open it in EtherPK once); its index is kept under
19475
+ The folder must already be a graph (open it in EtherPK once) - its index is kept under
19475
19476
  the cache directory, never in the folder.
19476
19477
  ${CMD} logout [--sync-server <url> | --all]
19477
19478
  Forget that server's token, keys and cached graphs on this machine.
19478
19479
  ${CMD} semantic setup
19479
19480
  Let the agent search by meaning. Installs a ~300 MB native runtime (onnxruntime-node,
19480
19481
  with your npm) and downloads a 23 MB embedding model into the cache directory, once
19481
- per computer. Nothing leaves this computer when the model runs; notes are never sent
19482
+ per computer. Nothing leaves this computer when the model runs - notes are never sent
19482
19483
  to a service to be embedded.
19483
19484
  ${CMD} semantic status | remove
19484
19485
  Whether it is set up here, or delete it (logout --all deletes it too).
@@ -19488,20 +19489,20 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
19488
19489
  then on this command without --out, and the agent's publish tool, write to the same
19489
19490
  folder (the agent cannot choose one). The folder's owned files (.html at the top,
19490
19491
  assets/, theme/, the search, feed and report files) are rewritten and their strays
19491
- removed; everything else in it is left alone. Publishing writes files; deploying them
19492
+ removed - everything else in it is left alone. Publishing writes files - deploying them
19492
19493
  (a git push, say) is yours. Run it from cron for a site that republishes itself.
19493
19494
  ${CMD} diagrams setup
19494
19495
  Let a publish draw Mermaid diagrams. Downloads a Chromium (about 170 MB) into the cache
19495
- directory with Playwright's installer, once per computer; a publish whose pages hold
19496
+ directory with Playwright's installer, once per computer - a publish whose pages hold
19496
19497
  diagrams refuses until this has run. Set ETHERPK_CHROMIUM=<path> to use a browser
19497
19498
  already on this machine instead (NixOS needs this).
19498
19499
  ${CMD} diagrams status
19499
19500
  Which browser a publish would use, if any.
19500
19501
 
19501
- This machine can hold logins for several Sync Servers at once; --sync-server says which one
19502
- a command means, and can be left out while there is only one. The config file is
19502
+ This machine can hold logins for several Sync Servers at once. The --sync-server flag says
19503
+ which one a command means, and can be left out while there is only one. The config file is
19503
19504
  ${defaultConfigPath()} (override with
19504
- ETHERPK_MCP_CONFIG); cached graphs live under ~/.cache/etherpk/mcp (override with
19505
+ ETHERPK_MCP_CONFIG) - cached graphs live under ~/.cache/etherpk/mcp (override with
19505
19506
  ETHERPK_MCP_CACHE_DIR).
19506
19507
  Docs: https://docs.etherpk.com/using-ai-agents-with-your-notes
19507
19508
  `;
@@ -19514,7 +19515,7 @@ function fail(message) {
19514
19515
  * an agent starting the process), since each question has its own option or variable.
19515
19516
  */
19516
19517
  async function ask(question, { secret = false, hint }) {
19517
- if (!process.stdin.isTTY) fail(`${question.replace(/:\s*$/, "")}: no terminal to ask on; ${hint}.`);
19518
+ if (!process.stdin.isTTY) fail(`${question.replace(/:\s*$/, "")}: no terminal to ask on - ${hint}.`);
19518
19519
  if (!secret) {
19519
19520
  const rl = createInterface({
19520
19521
  input: process.stdin,
@@ -19553,7 +19554,7 @@ async function ask(question, { secret = false, hint }) {
19553
19554
  }
19554
19555
  async function loadConfig(path) {
19555
19556
  const config = await readConfig(path);
19556
- if (!config) fail(`${path} is not a config file this version understands. Run: ${CMD} login --sync-server <url> (it will be rewritten; nothing else is affected).`);
19557
+ if (!config) fail(`${path} is not a config file this version understands. Run: ${CMD} login --sync-server <url> (it will be rewritten - nothing else is affected).`);
19557
19558
  return config;
19558
19559
  }
19559
19560
  /** The login a command means, or the reason there is none - in words the user can act on. */
@@ -19594,7 +19595,7 @@ async function login(args) {
19594
19595
  await writeConfig(path, config);
19595
19596
  console.log(`Keys unlocked and cached in ${path} (owner-only). Anyone who can read your files on this machine can read this account, as with a signed-in browser.`);
19596
19597
  const others = Object.keys(config.servers).filter((server) => server !== syncServer);
19597
- if (others.length > 0) console.log(`Also logged in to ${others.join(", ")}; commands now need --sync-server <url> to say which.`);
19598
+ if (others.length > 0) console.log(`Also logged in to ${others.join(", ")} - commands now need --sync-server <url> to say which.`);
19598
19599
  await listGraphs({
19599
19600
  syncServer,
19600
19601
  pat,
@@ -19650,7 +19651,7 @@ async function approveOrFallBack(account, byRecoveryCode) {
19650
19651
  process.exit(controls.exitCode);
19651
19652
  }
19652
19653
  if (!abandoned) throw new Error("unreachable");
19653
- console.log("Approval cancelled; unlocking with your Recovery Code instead.");
19654
+ console.log("Approval cancelled - unlocking with your Recovery Code instead.");
19654
19655
  return byRecoveryCode();
19655
19656
  }
19656
19657
  /**
@@ -19719,7 +19720,7 @@ async function semanticCommand(what) {
19719
19720
  const status = await semanticSetupStatus(process.env);
19720
19721
  console.log(`Runtime: ${status.runtime ? "installed" : "missing"} (${status.runtimeDir})`);
19721
19722
  console.log(`Model: ${status.model ? "present" : "missing"} (${status.modelDir})`);
19722
- console.log(status.runtime && status.model ? "Semantic search is set up; serve uses it unless started with --no-semantic." : `Not set up. Run: ${CMD} semantic setup`);
19723
+ console.log(status.runtime && status.model ? "Semantic search is set up - serve uses it unless started with --no-semantic." : `Not set up. Run: ${CMD} semantic setup`);
19723
19724
  const stores = await listGraphStores(process.env, MODEL.id);
19724
19725
  if (stores.length === 0) return;
19725
19726
  console.log("");
@@ -19778,7 +19779,7 @@ async function publishCommand(args) {
19778
19779
  const out = resolve(args.out.trim());
19779
19780
  await writePublishFolders(configPath, withPublishFolder(await readPublishFolders(configPath), key, publication, out));
19780
19781
  console.error(`etherpk-mcp: publish folder for "${publication}" of "${graphName}" set to ${out} (remembered in ${configPath}).`);
19781
- } else if (!publishFolderOf(await readPublishFolders(configPath), key, publication)) fail(`No publish folder is set for "${publication}" of "${graphName}" on this machine. Pass --out <dir> once; it is remembered.`);
19782
+ } else if (!publishFolderOf(await readPublishFolders(configPath), key, publication)) fail(`No publish folder is set for "${publication}" of "${graphName}" on this machine. Pass --out <dir> once - it is remembered.`);
19782
19783
  const result = await publish(graph, { id: publication }, host);
19783
19784
  console.log(JSON.stringify(cliPublishOutput(result), null, 2));
19784
19785
  if (!result.ok) process.exitCode = 1;
@@ -19851,7 +19852,7 @@ async function openFolderForServe(folder, args) {
19851
19852
  onSemanticProgress: reportSemanticProgress,
19852
19853
  onError: (error) => console.error(`etherpk-mcp: ${error.message}`),
19853
19854
  onWarning: (line) => console.error(`etherpk-mcp: ${line}`),
19854
- watch: watchFolder(path, (error) => console.error(`etherpk-mcp: not watching the folder for changes (${error.message}); edits made outside are still picked up before each tool call.`))
19855
+ watch: watchFolder(path, (error) => console.error(`etherpk-mcp: not watching the folder for changes (${error.message}) - edits made outside are still picked up before each tool call.`))
19855
19856
  }),
19856
19857
  graphName: name
19857
19858
  };
@@ -19923,7 +19924,7 @@ async function serveGraph(graph, graphName, args) {
19923
19924
  transport.onclose = listener;
19924
19925
  },
19925
19926
  async shutdown(end) {
19926
- console.error(`etherpk-mcp: ${end}; flushing and exiting.`);
19927
+ console.error(`etherpk-mcp: ${end} - flushing and exiting.`);
19927
19928
  await graph.settle().catch(() => {});
19928
19929
  await graph.dispose().catch(() => {});
19929
19930
  process.exit(0);
@@ -19933,17 +19934,17 @@ async function serveGraph(graph, graphName, args) {
19933
19934
  console.error(`etherpk-mcp: serving "${graphName}" over stdio as "Agent on ${hostname()}".`);
19934
19935
  chromiumStatus(process.env, CMD).then((chromium) => {
19935
19936
  if (chromium.executable) return;
19936
- console.error(`etherpk-mcp: no browser for diagrams (run: ${chromium.setupCommand}, or set ETHERPK_CHROMIUM); a publish or theme preview with Mermaid diagrams refuses until then.`);
19937
+ console.error(`etherpk-mcp: no browser for diagrams (run: ${chromium.setupCommand}, or set ETHERPK_CHROMIUM) - a publish or theme preview with Mermaid diagrams refuses until then.`);
19937
19938
  });
19938
19939
  const startSemantic = () => graph.semantic().then(async (semantic) => {
19939
19940
  const status = await semantic.status();
19940
- console.error(`etherpk-mcp: semantic search on (${semantic.model.id}); ${status.embedded} of ${status.total} passages embedded, building the rest in the background.`);
19941
+ console.error(`etherpk-mcp: semantic search on (${semantic.model.id}) - ${status.embedded} of ${status.total} passages embedded, building the rest in the background.`);
19941
19942
  }).catch((error) => console.error(`etherpk-mcp: semantic search unavailable: ${error instanceof Error ? error.message : String(error)}`));
19942
19943
  if (semanticOn) startSemantic();
19943
19944
  else if (!args["no-semantic"]) {
19944
- console.error(`etherpk-mcp: semantic search is not set up on this computer (run: ${CMD} semantic setup; this process will notice within half a minute, no restart needed); search answers by text only until then.`);
19945
+ console.error(`etherpk-mcp: semantic search is not set up on this computer (run: ${CMD} semantic setup - this process will notice within half a minute, no restart needed). Search answers by text only until then.`);
19945
19946
  whenSemanticSetUp(process.env, () => {
19946
- console.error("etherpk-mcp: semantic setup found; loading the model and building the store.");
19947
+ console.error("etherpk-mcp: semantic setup found - loading the model and building the store.");
19947
19948
  startSemantic();
19948
19949
  });
19949
19950
  }