@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/README.md +16 -16
- package/dist/main.js +132 -131
- package/dist/main.js.map +1 -1
- package/package.json +3 -3
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.
|
|
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-
|
|
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
|
|
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}
|
|
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
|
|
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}
|
|
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)
|
|
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)})
|
|
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)
|
|
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
|
|
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
|
|
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 ")})
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
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}
|
|
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}"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}"
|
|
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
|
|
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}"
|
|
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
|
|
18367
|
-
page("Private Notes", "- Not public, so never on the site
|
|
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}"
|
|
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}"
|
|
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
|
|
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(", ")}
|
|
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}"
|
|
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
|
|
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
|
|
18932
|
-
"Bullets are \"- \" with two-space nesting
|
|
18933
|
-
"read_document returns a document's body as text and its frontmatter block as data (the \"frontmatter\" object)
|
|
18934
|
-
"Edit with edit_document (an exact, unique old→new replacement) or append_document
|
|
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\")
|
|
18936
|
-
"search has two kinds of matching: mode \"text\" (default) matches words and quoted phrases
|
|
18937
|
-
"Start with graph_info to see what you are connected to. read_documents reads several documents in one call
|
|
18938
|
-
"Publishing: list_publications shows the publications this graph defines (a publication is a page whose frontmatter defines it
|
|
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
|
|
18940
|
-
"Images and files are assets: upload_asset adds a file from this machine and returns the markdown to paste into a document
|
|
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
|
|
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)
|
|
18949
|
-
to: z.string().optional().describe("Journal entries on or before this day (YYYY-MM-DD)
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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(", ")}
|
|
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
|
|
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
|
|
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
|
|
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})
|
|
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}
|
|
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)
|
|
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})
|
|
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
|
|
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
|
|
19947
|
+
console.error("etherpk-mcp: semantic setup found - loading the model and building the store.");
|
|
19947
19948
|
startSemantic();
|
|
19948
19949
|
});
|
|
19949
19950
|
}
|