@appsoftwareltd/etherpk-mcp 0.8.2 → 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 +806 -360
- package/dist/main.js.map +1 -1
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as Y from "yjs";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from "y-protocols/awareness";
|
|
22
22
|
import * as encoding from "lib0/encoding";
|
|
23
|
-
import { parse,
|
|
23
|
+
import { Document, Scalar, YAMLMap, isMap, isScalar, isSeq, parse, parseDocument, visit } from "yaml";
|
|
24
24
|
import "fake-indexeddb/auto";
|
|
25
25
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26
26
|
import { classHighlighter, highlightTree } from "@lezer/highlight";
|
|
@@ -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";
|
|
@@ -2511,6 +2512,119 @@ function snippetSegments(snippet) {
|
|
|
2511
2512
|
}
|
|
2512
2513
|
return segments.filter((s) => s.text !== "");
|
|
2513
2514
|
}
|
|
2515
|
+
/** Letters, digits, `_`, `.` and `-`: a key a person can type, dot paths included. */
|
|
2516
|
+
var PROPERTY_KEY_PATTERN = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
|
|
2517
|
+
/**
|
|
2518
|
+
* Whitespace-separated tokens. A quote opens a run that spaces do not end, whether it starts the
|
|
2519
|
+
* token (`"a phrase"`) or follows a key (`status:"in progress"`); an unclosed one runs to the end.
|
|
2520
|
+
*/
|
|
2521
|
+
function searchTokens(input) {
|
|
2522
|
+
const out = [];
|
|
2523
|
+
let index = 0;
|
|
2524
|
+
while (index < input.length) {
|
|
2525
|
+
if (/\s/.test(input[index])) {
|
|
2526
|
+
index++;
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const from = index;
|
|
2530
|
+
while (index < input.length && !/\s/.test(input[index])) {
|
|
2531
|
+
if (input[index] === "\"") {
|
|
2532
|
+
const close = input.indexOf("\"", index + 1);
|
|
2533
|
+
index = close === -1 ? input.length : close + 1;
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
index++;
|
|
2537
|
+
}
|
|
2538
|
+
out.push({
|
|
2539
|
+
text: input.slice(from, index),
|
|
2540
|
+
from,
|
|
2541
|
+
to: index
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
return out;
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* The filters in `input`, and the words left for name and text matching.
|
|
2548
|
+
*
|
|
2549
|
+
* A term is a filter only when its key is one at least one searchable document carries
|
|
2550
|
+
* (`knownKeys`, lower-cased): anything else shaped like `x:y` stays words, which keeps a URL, a
|
|
2551
|
+
* time or `note:` in prose as text. A fully quoted term is always words. A known key with no
|
|
2552
|
+
* value yet (`status:` mid-typing) is dropped from both, so the key is never searched as a word
|
|
2553
|
+
* a moment before the value arrives.
|
|
2554
|
+
*/
|
|
2555
|
+
function parseSearchQuery(input, knownKeys) {
|
|
2556
|
+
const filters = [];
|
|
2557
|
+
const terms = [];
|
|
2558
|
+
const cut = [];
|
|
2559
|
+
if (knownKeys.size > 0) for (const token of searchTokens(input)) {
|
|
2560
|
+
if (token.text.startsWith("\"")) continue;
|
|
2561
|
+
const colon = token.text.indexOf(":");
|
|
2562
|
+
if (colon <= 0) continue;
|
|
2563
|
+
const negated = token.text.startsWith("-");
|
|
2564
|
+
const key = token.text.slice(negated ? 1 : 0, colon);
|
|
2565
|
+
if (!PROPERTY_KEY_PATTERN.test(key) || !knownKeys.has(key.toLowerCase())) continue;
|
|
2566
|
+
const filter = filterValue(key, token.text.slice(colon + 1), negated);
|
|
2567
|
+
cut.push({
|
|
2568
|
+
from: token.from,
|
|
2569
|
+
to: token.to
|
|
2570
|
+
});
|
|
2571
|
+
if (filter === null) continue;
|
|
2572
|
+
filters.push(filter);
|
|
2573
|
+
terms.push({
|
|
2574
|
+
filter,
|
|
2575
|
+
from: token.from,
|
|
2576
|
+
to: token.to
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
words: withoutRanges(input, cut),
|
|
2581
|
+
filters,
|
|
2582
|
+
terms
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
/** The filter for a value as typed, or null when there is no value yet. */
|
|
2586
|
+
function filterValue(key, raw, negated) {
|
|
2587
|
+
if (raw === "*") return {
|
|
2588
|
+
key,
|
|
2589
|
+
value: null,
|
|
2590
|
+
prefix: false,
|
|
2591
|
+
negated
|
|
2592
|
+
};
|
|
2593
|
+
if (raw.startsWith("\"")) {
|
|
2594
|
+
const close = raw.indexOf("\"", 1);
|
|
2595
|
+
const value = (close === -1 ? raw.slice(1) : raw.slice(1, close)).trim();
|
|
2596
|
+
return value === "" ? null : {
|
|
2597
|
+
key,
|
|
2598
|
+
value,
|
|
2599
|
+
prefix: false,
|
|
2600
|
+
negated
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
if (raw.length > 1 && raw.endsWith("*")) return {
|
|
2604
|
+
key,
|
|
2605
|
+
value: raw.slice(0, -1),
|
|
2606
|
+
prefix: true,
|
|
2607
|
+
negated
|
|
2608
|
+
};
|
|
2609
|
+
return raw === "" ? null : {
|
|
2610
|
+
key,
|
|
2611
|
+
value: raw,
|
|
2612
|
+
prefix: false,
|
|
2613
|
+
negated
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
/** `input` with the ranges removed and the gaps they leave closed to single spaces. */
|
|
2617
|
+
function withoutRanges(input, ranges) {
|
|
2618
|
+
if (ranges.length === 0) return input;
|
|
2619
|
+
const parts = [];
|
|
2620
|
+
let at = 0;
|
|
2621
|
+
for (const range of ranges) {
|
|
2622
|
+
parts.push(input.slice(at, range.from));
|
|
2623
|
+
at = range.to;
|
|
2624
|
+
}
|
|
2625
|
+
parts.push(input.slice(at));
|
|
2626
|
+
return parts.map((part) => part.trim()).filter((part) => part !== "").join(" ");
|
|
2627
|
+
}
|
|
2514
2628
|
//#endregion
|
|
2515
2629
|
//#region ../client/src/lib/document/index-db.ts
|
|
2516
2630
|
/**
|
|
@@ -2572,6 +2686,15 @@ CREATE TABLE IF NOT EXISTS passages (
|
|
|
2572
2686
|
page_id INTEGER NOT NULL, ord INTEGER NOT NULL, start_line INTEGER NOT NULL,
|
|
2573
2687
|
end_line INTEGER NOT NULL, first_block_local_id INTEGER NOT NULL,
|
|
2574
2688
|
content_hash TEXT NOT NULL, text TEXT NOT NULL);
|
|
2689
|
+
-- [[Property]] rows for [[Property Filter]]s (ADR 0107): one per value, keys as dot paths. key and
|
|
2690
|
+
-- value keep their spelling for display; key_lc and value_lc are lower-cased in JS, because
|
|
2691
|
+
-- SQLite's lower() folds ASCII only and a filter matches ignoring case in any script. value is
|
|
2692
|
+
-- NULL on a mapping's presence row. title and aliases rows come from the graph's names. A
|
|
2693
|
+
-- protected document has none.
|
|
2694
|
+
CREATE TABLE IF NOT EXISTS properties (
|
|
2695
|
+
page_id INTEGER NOT NULL, key TEXT NOT NULL, key_lc TEXT NOT NULL,
|
|
2696
|
+
value TEXT, value_lc TEXT);
|
|
2697
|
+
CREATE INDEX IF NOT EXISTS properties_key ON properties(key_lc, value_lc);
|
|
2575
2698
|
CREATE INDEX IF NOT EXISTS passages_hash ON passages(content_hash);
|
|
2576
2699
|
CREATE INDEX IF NOT EXISTS links_concept_key ON links(concept_key);
|
|
2577
2700
|
CREATE INDEX IF NOT EXISTS blocks_page ON blocks(page_id);
|
|
@@ -2620,7 +2743,7 @@ function createSchema(db) {
|
|
|
2620
2743
|
db.exec(SCHEMA$1);
|
|
2621
2744
|
db.run("INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('active_generation', 1)");
|
|
2622
2745
|
db.run("INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('revision', 0)");
|
|
2623
|
-
db.exec(`PRAGMA user_version =
|
|
2746
|
+
db.exec(`PRAGMA user_version = 14`);
|
|
2624
2747
|
}
|
|
2625
2748
|
function activeIndexGeneration(db) {
|
|
2626
2749
|
return db.all("SELECT value FROM index_metadata WHERE key = 'active_generation'")[0]?.value ?? 1;
|
|
@@ -2641,7 +2764,7 @@ function advanceIndexRevision(db) {
|
|
|
2641
2764
|
*/
|
|
2642
2765
|
function isUsableIndex(db) {
|
|
2643
2766
|
try {
|
|
2644
|
-
return db.all("PRAGMA user_version")[0]?.user_version ===
|
|
2767
|
+
return db.all("PRAGMA user_version")[0]?.user_version === 14;
|
|
2645
2768
|
} catch {
|
|
2646
2769
|
return false;
|
|
2647
2770
|
}
|
|
@@ -2661,8 +2784,11 @@ function documentHash(db, key) {
|
|
|
2661
2784
|
*/
|
|
2662
2785
|
function indexDocHash(doc) {
|
|
2663
2786
|
const includes = doc.includes ?? [];
|
|
2664
|
-
|
|
2665
|
-
|
|
2787
|
+
const properties = doc.properties ?? [];
|
|
2788
|
+
if (includes.length === 0 && properties.length === 0) return hashText(doc.text);
|
|
2789
|
+
const includeText = includes.map((i) => `${i.publication}\u0001${i.slot}\u0001${i.concept}`).join("\0");
|
|
2790
|
+
const propertyText = properties.map((p) => `${p.key}\u0001${p.value ?? ""}`).join("\0");
|
|
2791
|
+
return hashText(`${doc.text}\u0000${includeText}\u0003${propertyText}`);
|
|
2666
2792
|
}
|
|
2667
2793
|
function hashText(text) {
|
|
2668
2794
|
let h1 = 3735928559;
|
|
@@ -2703,6 +2829,7 @@ function insertDerived(db, pageId, doc) {
|
|
|
2703
2829
|
conceptKey$1(alias),
|
|
2704
2830
|
alias
|
|
2705
2831
|
]);
|
|
2832
|
+
insertProperties(db, pageId, doc);
|
|
2706
2833
|
for (const include of doc.includes ?? []) db.run("INSERT INTO publication_includes (page_id, publication_id, slot, concept_key, concept) VALUES (?,?,?,?,?)", [
|
|
2707
2834
|
pageId,
|
|
2708
2835
|
include.publication,
|
|
@@ -2779,12 +2906,40 @@ function insertDerived(db, pageId, doc) {
|
|
|
2779
2906
|
return searchable;
|
|
2780
2907
|
}
|
|
2781
2908
|
/**
|
|
2909
|
+
* Write one document's [[Property]] rows (ADR 0107): its name as `title`, each alias as
|
|
2910
|
+
* `aliases`, then what its Frontmatter carries. A [[Protected Document]] writes none. Its
|
|
2911
|
+
* Frontmatter is stored in plaintext, but no kind of matching in Search reaches protected
|
|
2912
|
+
* content, and a document with no rows cannot match a filter, not even a negated one, because
|
|
2913
|
+
* every filtered query also requires `protected = 0`.
|
|
2914
|
+
*/
|
|
2915
|
+
function insertProperties(db, pageId, doc) {
|
|
2916
|
+
if (protectedFlag(doc) === 1) return;
|
|
2917
|
+
const rows = [
|
|
2918
|
+
{
|
|
2919
|
+
key: "title",
|
|
2920
|
+
value: doc.concept
|
|
2921
|
+
},
|
|
2922
|
+
...doc.aliases.map((alias) => ({
|
|
2923
|
+
key: "aliases",
|
|
2924
|
+
value: alias
|
|
2925
|
+
})),
|
|
2926
|
+
...doc.properties ?? []
|
|
2927
|
+
];
|
|
2928
|
+
for (const row of rows) db.run("INSERT INTO properties (page_id, key, key_lc, value, value_lc) VALUES (?,?,?,?,?)", [
|
|
2929
|
+
pageId,
|
|
2930
|
+
row.key,
|
|
2931
|
+
row.key.toLowerCase(),
|
|
2932
|
+
row.value,
|
|
2933
|
+
row.value === null ? null : row.value.toLowerCase()
|
|
2934
|
+
]);
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2782
2937
|
* Write one document's [[Task Concept]] rows (ADR 0051).
|
|
2783
2938
|
*
|
|
2784
2939
|
* Derivation supplies the wikilinked half; the document's own concept is added HERE, as the
|
|
2785
2940
|
* virtual root of every task's ancestry chain, because derivation reads text and never learns
|
|
2786
2941
|
* the document's identity. Without that row, filtering to a project page returns nothing for
|
|
2787
|
-
* the tasks written directly on it — the case the "
|
|
2942
|
+
* the tasks written directly on it — the case the "Filter to <active tab>" shortcut exists for.
|
|
2788
2943
|
*
|
|
2789
2944
|
* Only the document's own concept goes in, never its aliases: the query resolves a filtered
|
|
2790
2945
|
* name to its page and matches on every name that page answers to, so an alias row here would
|
|
@@ -2879,7 +3034,8 @@ function deleteGeneration(db, generation) {
|
|
|
2879
3034
|
"links",
|
|
2880
3035
|
"tasks",
|
|
2881
3036
|
"task_concepts",
|
|
2882
|
-
"passages"
|
|
3037
|
+
"passages",
|
|
3038
|
+
"properties"
|
|
2883
3039
|
]) db.run(`DELETE FROM ${table} WHERE page_id IN (SELECT id FROM pages WHERE generation = ?)`, [generation]);
|
|
2884
3040
|
db.run("DELETE FROM pages WHERE generation = ?", [generation]);
|
|
2885
3041
|
}
|
|
@@ -2954,6 +3110,7 @@ function ingestOne(db, doc) {
|
|
|
2954
3110
|
db.run("DELETE FROM tasks WHERE page_id = ?", [pageId]);
|
|
2955
3111
|
db.run("DELETE FROM task_concepts WHERE page_id = ?", [pageId]);
|
|
2956
3112
|
db.run("DELETE FROM passages WHERE page_id = ?", [pageId]);
|
|
3113
|
+
db.run("DELETE FROM properties WHERE page_id = ?", [pageId]);
|
|
2957
3114
|
db.run("UPDATE pages SET concept = ?, kind = ?, text_hash = ?, protected = ? WHERE id = ?", [
|
|
2958
3115
|
doc.concept,
|
|
2959
3116
|
doc.kind,
|
|
@@ -3366,25 +3523,27 @@ var SNIPPET_TOKENS = 24;
|
|
|
3366
3523
|
* scores merely reintroduces the long-document bias bm25 exists to remove. Counting is cruder,
|
|
3367
3524
|
* robust, and explainable in one sentence - and it orders by a number the row displays.
|
|
3368
3525
|
*/
|
|
3369
|
-
function searchText$1(db, query, offset, limit) {
|
|
3526
|
+
function searchText$1(db, query, offset, limit, filters = []) {
|
|
3370
3527
|
const match = buildFtsMatch(query);
|
|
3371
3528
|
if (match === null) return {
|
|
3372
3529
|
groups: [],
|
|
3373
3530
|
hasMore: false
|
|
3374
3531
|
};
|
|
3375
3532
|
const generation = activeIndexGeneration(db);
|
|
3533
|
+
const narrow = propertyFilterSql(filters);
|
|
3376
3534
|
const ranked = db.all(`WITH hits AS MATERIALIZED (
|
|
3377
3535
|
SELECT rowid / ${BLOCK_FTS_STRIDE} AS page_id, bm25(block_fts) AS score
|
|
3378
3536
|
FROM block_fts WHERE block_fts MATCH ?
|
|
3379
3537
|
)
|
|
3380
3538
|
SELECT h.page_id AS page_id, COUNT(*) AS matches, MIN(h.score) AS best
|
|
3381
3539
|
FROM hits h
|
|
3382
|
-
JOIN pages p ON p.id = h.page_id AND p.generation =
|
|
3540
|
+
JOIN pages p ON p.id = h.page_id AND p.generation = ?${narrow.sql}
|
|
3383
3541
|
GROUP BY h.page_id
|
|
3384
3542
|
ORDER BY matches DESC, best ASC, h.page_id ASC
|
|
3385
3543
|
LIMIT ? OFFSET ?`, [
|
|
3386
3544
|
match,
|
|
3387
3545
|
generation,
|
|
3546
|
+
...narrow.params,
|
|
3388
3547
|
limit + 1,
|
|
3389
3548
|
offset
|
|
3390
3549
|
]);
|
|
@@ -3462,21 +3621,23 @@ function searchBreadcrumb(blocks, localId) {
|
|
|
3462
3621
|
* Capped rather than exact because a prefix query - which is every query, mid-typing - can
|
|
3463
3622
|
* match most of the graph, and this runs beside the rows it must never delay.
|
|
3464
3623
|
*/
|
|
3465
|
-
function searchTextCount(db, query) {
|
|
3624
|
+
function searchTextCount(db, query, filters = []) {
|
|
3466
3625
|
const match = buildFtsMatch(query);
|
|
3467
3626
|
if (match === null) return {
|
|
3468
3627
|
total: 0,
|
|
3469
3628
|
capped: false
|
|
3470
3629
|
};
|
|
3630
|
+
const narrow = propertyFilterSql(filters);
|
|
3471
3631
|
const n = db.all(`SELECT COUNT(*) AS n FROM (
|
|
3472
3632
|
SELECT m.page_id FROM (
|
|
3473
3633
|
SELECT DISTINCT rowid / 1048576 AS page_id FROM block_fts
|
|
3474
3634
|
WHERE block_fts MATCH ?
|
|
3475
3635
|
) m
|
|
3476
|
-
JOIN pages p ON p.id = m.page_id AND p.generation =
|
|
3636
|
+
JOIN pages p ON p.id = m.page_id AND p.generation = ?${narrow.sql}
|
|
3477
3637
|
LIMIT ?)`, [
|
|
3478
3638
|
match,
|
|
3479
3639
|
activeIndexGeneration(db),
|
|
3640
|
+
...narrow.params,
|
|
3480
3641
|
1001
|
|
3481
3642
|
])[0]?.n ?? 0;
|
|
3482
3643
|
return n > 1e3 ? {
|
|
@@ -3487,6 +3648,106 @@ function searchTextCount(db, query) {
|
|
|
3487
3648
|
capped: false
|
|
3488
3649
|
};
|
|
3489
3650
|
}
|
|
3651
|
+
/** Documents a filtered name listing returns at most: well past any graph Search serves. */
|
|
3652
|
+
var PROPERTY_MATCH_CAP = 5e3;
|
|
3653
|
+
/**
|
|
3654
|
+
* The SQL that narrows a query over `pages AS p` to the documents passing every filter, as an
|
|
3655
|
+
* `AND …` suffix, with its parameters in order. Empty when there are no filters. A filtered
|
|
3656
|
+
* query never returns a [[Protected Document]], which has no property rows; the explicit
|
|
3657
|
+
* `protected = 0` is what keeps a NEGATED filter from matching one.
|
|
3658
|
+
*/
|
|
3659
|
+
function propertyFilterSql(filters) {
|
|
3660
|
+
if (filters.length === 0) return {
|
|
3661
|
+
sql: "",
|
|
3662
|
+
params: []
|
|
3663
|
+
};
|
|
3664
|
+
const clauses = ["p.protected = 0"];
|
|
3665
|
+
const params = [];
|
|
3666
|
+
for (const filter of filters) {
|
|
3667
|
+
let test = "x.key_lc = ?";
|
|
3668
|
+
params.push(filter.key.toLowerCase());
|
|
3669
|
+
if (filter.value !== null) {
|
|
3670
|
+
const value = filter.value.toLowerCase();
|
|
3671
|
+
if (filter.prefix) {
|
|
3672
|
+
test += " AND substr(x.value_lc, 1, ?) = ?";
|
|
3673
|
+
params.push(Array.from(value).length, value);
|
|
3674
|
+
} else {
|
|
3675
|
+
test += " AND x.value_lc = ?";
|
|
3676
|
+
params.push(value);
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
clauses.push(`${filter.negated ? "NOT " : ""}EXISTS (SELECT 1 FROM properties x WHERE x.page_id = p.id AND ${test})`);
|
|
3680
|
+
}
|
|
3681
|
+
return {
|
|
3682
|
+
sql: ` AND ${clauses.join(" AND ")}`,
|
|
3683
|
+
params
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Every key a searchable document carries, once per spelling, most used first: what decides
|
|
3688
|
+
* whether a typed `x:y` is a filter, and what the key suggestions offer.
|
|
3689
|
+
*/
|
|
3690
|
+
function propertyKeys(db) {
|
|
3691
|
+
return db.all(`SELECT pr.key AS key, COUNT(DISTINCT pr.page_id) AS documents
|
|
3692
|
+
FROM properties pr JOIN pages p ON p.id = pr.page_id
|
|
3693
|
+
WHERE p.generation = ?
|
|
3694
|
+
GROUP BY pr.key
|
|
3695
|
+
ORDER BY documents DESC, pr.key_lc, pr.key`, [activeIndexGeneration(db)]);
|
|
3696
|
+
}
|
|
3697
|
+
/** The values `key` has across the graph, grouped ignoring case, most used first. */
|
|
3698
|
+
function propertyValues(db, key, limit = 50) {
|
|
3699
|
+
return db.all(`SELECT MIN(pr.value) AS value, COUNT(DISTINCT pr.page_id) AS documents
|
|
3700
|
+
FROM properties pr JOIN pages p ON p.id = pr.page_id
|
|
3701
|
+
WHERE p.generation = ? AND pr.key_lc = ? AND pr.value IS NOT NULL
|
|
3702
|
+
GROUP BY pr.value_lc
|
|
3703
|
+
ORDER BY documents DESC, pr.value_lc
|
|
3704
|
+
LIMIT ?`, [
|
|
3705
|
+
activeIndexGeneration(db),
|
|
3706
|
+
key.toLowerCase(),
|
|
3707
|
+
limit
|
|
3708
|
+
]);
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Every document that passes all `filters`, in name order, with the values of the keys the
|
|
3712
|
+
* positive filters name (not `title`, which the row already shows). Empty without a filter:
|
|
3713
|
+
* listing the whole graph is [[All Documents]]' job.
|
|
3714
|
+
*/
|
|
3715
|
+
function documentsMatchingProperties(db, filters, limit = PROPERTY_MATCH_CAP) {
|
|
3716
|
+
if (filters.length === 0) return [];
|
|
3717
|
+
const narrow = propertyFilterSql(filters);
|
|
3718
|
+
const pages = db.all(`SELECT p.id AS id, p.concept AS concept, p.kind AS kind FROM pages p
|
|
3719
|
+
WHERE p.generation = ?${narrow.sql}
|
|
3720
|
+
ORDER BY p.concept_key
|
|
3721
|
+
LIMIT ?`, [
|
|
3722
|
+
activeIndexGeneration(db),
|
|
3723
|
+
...narrow.params,
|
|
3724
|
+
limit
|
|
3725
|
+
]);
|
|
3726
|
+
if (pages.length === 0) return [];
|
|
3727
|
+
const shown = [...new Set(filters.filter((f) => !f.negated).map((f) => f.key.toLowerCase()))].filter((key) => key !== "title");
|
|
3728
|
+
const values = /* @__PURE__ */ new Map();
|
|
3729
|
+
if (shown.length > 0) {
|
|
3730
|
+
const rows = db.all(`SELECT page_id, key, value FROM properties
|
|
3731
|
+
WHERE page_id IN (SELECT value FROM json_each(?))
|
|
3732
|
+
AND key_lc IN (SELECT value FROM json_each(?))
|
|
3733
|
+
AND value IS NOT NULL
|
|
3734
|
+
ORDER BY rowid`, [JSON.stringify(pages.map((page) => page.id)), JSON.stringify(shown)]);
|
|
3735
|
+
for (const row of rows) {
|
|
3736
|
+
const list = values.get(row.page_id);
|
|
3737
|
+
const entry = {
|
|
3738
|
+
key: row.key,
|
|
3739
|
+
value: row.value
|
|
3740
|
+
};
|
|
3741
|
+
if (list) list.push(entry);
|
|
3742
|
+
else values.set(row.page_id, [entry]);
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
return pages.map((page) => ({
|
|
3746
|
+
concept: page.concept,
|
|
3747
|
+
kind: page.kind,
|
|
3748
|
+
properties: values.get(page.id) ?? []
|
|
3749
|
+
}));
|
|
3750
|
+
}
|
|
3490
3751
|
/** Escape a literal for a LIKE pattern, so an asset name can never behave as a wildcard. */
|
|
3491
3752
|
function likeLiteral(needle) {
|
|
3492
3753
|
return needle.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
@@ -4010,7 +4271,7 @@ function cacheRoot(env) {
|
|
|
4010
4271
|
* recovered from a joined path (see there).
|
|
4011
4272
|
*/
|
|
4012
4273
|
var CACHE_FILE_NAME = `local-cache.v${CACHE_DB_VERSION}.bin`;
|
|
4013
|
-
var INDEX_FILE_NAME = `index.
|
|
4274
|
+
var INDEX_FILE_NAME = `index.v14.sqlite`;
|
|
4014
4275
|
var VECTORS_FILE_NAME = `vectors.v1.sqlite`;
|
|
4015
4276
|
function cacheFile(dir) {
|
|
4016
4277
|
return join(dir, CACHE_FILE_NAME);
|
|
@@ -4039,7 +4300,7 @@ async function openCacheDb() {
|
|
|
4039
4300
|
const db = await request$1(indexedDB.open(CACHE_DB, CACHE_DB_VERSION));
|
|
4040
4301
|
for (const store of CACHE_STORES) if (!db.objectStoreNames.contains(store)) {
|
|
4041
4302
|
db.close();
|
|
4042
|
-
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.`);
|
|
4043
4304
|
}
|
|
4044
4305
|
return db;
|
|
4045
4306
|
}
|
|
@@ -4327,7 +4588,7 @@ function describeGraphStore(store, now = Date.now()) {
|
|
|
4327
4588
|
if (total === 0) return `empty - no passages indexed here yet (snapshot ${age})`;
|
|
4328
4589
|
if (embedded >= total) return `up to date - ${count} embedded (snapshot ${age})`;
|
|
4329
4590
|
const percent = Math.floor(embedded / total * 100);
|
|
4330
|
-
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`;
|
|
4331
4592
|
}
|
|
4332
4593
|
/** Remove every persisted graph under the cache root (logout of the last server). */
|
|
4333
4594
|
async function removeCacheRoot(env) {
|
|
@@ -4524,7 +4785,7 @@ async function setupSemantic(io) {
|
|
|
4524
4785
|
const files = io.files ?? MODEL.files;
|
|
4525
4786
|
const status = await semanticSetupStatus(io.env, files);
|
|
4526
4787
|
if (!status.runtime) {
|
|
4527
|
-
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)…`);
|
|
4528
4789
|
await mkdir(status.runtimeDir, {
|
|
4529
4790
|
recursive: true,
|
|
4530
4791
|
mode: 448
|
|
@@ -4597,7 +4858,7 @@ async function download(fetchFn, file, path) {
|
|
|
4597
4858
|
const digest = hash.digest("hex");
|
|
4598
4859
|
if (digest !== file.sha256) {
|
|
4599
4860
|
await rm(temp, { force: true });
|
|
4600
|
-
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.`);
|
|
4601
4862
|
}
|
|
4602
4863
|
await rename(temp, path);
|
|
4603
4864
|
}
|
|
@@ -4619,7 +4880,7 @@ var SemanticUnavailable = class extends Error {
|
|
|
4619
4880
|
this.name = "SemanticUnavailable";
|
|
4620
4881
|
}
|
|
4621
4882
|
};
|
|
4622
|
-
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.";
|
|
4623
4884
|
/** The thread count `serve` uses: the option, else the environment, else the conservative default. */
|
|
4624
4885
|
function semanticThreads(env, requested) {
|
|
4625
4886
|
if (requested !== void 0) return Math.max(1, Math.floor(requested));
|
|
@@ -4640,7 +4901,7 @@ async function loadEmbeddingModel(env, options = {}) {
|
|
|
4640
4901
|
const loaded = await import(pathToFileURL(require.resolve(RUNTIME.name)).href);
|
|
4641
4902
|
ort = loaded.default?.InferenceSession ? loaded.default : loaded;
|
|
4642
4903
|
} catch (error) {
|
|
4643
|
-
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}`);
|
|
4644
4905
|
}
|
|
4645
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")));
|
|
4646
4907
|
const threads = semanticThreads(env, options.threads);
|
|
@@ -6396,6 +6657,172 @@ function parseFrontmatter(text) {
|
|
|
6396
6657
|
}
|
|
6397
6658
|
}
|
|
6398
6659
|
//#endregion
|
|
6660
|
+
//#region ../client/src/lib/document/frontmatter/frontmatter-yaml.ts
|
|
6661
|
+
/**
|
|
6662
|
+
* Reading, editing and writing a [[Frontmatter]] block's YAML in the one style EtherPK writes
|
|
6663
|
+
* (ADR 0108).
|
|
6664
|
+
*
|
|
6665
|
+
* Every writer goes through here: the identity write-back, publishing, the arbitrary-key patch,
|
|
6666
|
+
* the importers, Add frontmatter and the editor's tidy when an editing episode ends. Edits go
|
|
6667
|
+
* through the `yaml` library's document model rather than a plain object, so a comment, and any
|
|
6668
|
+
* key the edit does not touch, survives. The block is then written in the EtherPK style:
|
|
6669
|
+
*
|
|
6670
|
+
* - two-space indentation, a list under its key as ` - item`, one item per line;
|
|
6671
|
+
* - an inline list (`[a, b]`) written as a block list; an empty `[]` written bare on a key EtherPK
|
|
6672
|
+
* reads (`publications:`), where empty means not set, and kept on any other key;
|
|
6673
|
+
* - quotes only where a value needs them (`"true"` stays quoted, `"hello"` does not);
|
|
6674
|
+
* - no line wrapping, no blank lines, key order as found, comments kept;
|
|
6675
|
+
* - an empty value written bare (`slug:`), which reads as "fill this in".
|
|
6676
|
+
*
|
|
6677
|
+
* Restyling never changes what the block says: the value is compared before and after, and a
|
|
6678
|
+
* rewrite that would change it is dropped. A block that does not parse is never rewritten.
|
|
6679
|
+
*
|
|
6680
|
+
* Pure: no store, no editor. What counts as a block comes from `frontmatter-span.ts`.
|
|
6681
|
+
*/
|
|
6682
|
+
/** The `yaml` serialiser options that make up the EtherPK style. */
|
|
6683
|
+
var STYLE = {
|
|
6684
|
+
indent: 2,
|
|
6685
|
+
indentSeq: true,
|
|
6686
|
+
lineWidth: 0,
|
|
6687
|
+
minContentWidth: 0,
|
|
6688
|
+
nullStr: ""
|
|
6689
|
+
};
|
|
6690
|
+
/**
|
|
6691
|
+
* The block's YAML parsed as a document, or null when it does not parse or is not a mapping.
|
|
6692
|
+
* An empty block (or one holding only comments) is an empty mapping.
|
|
6693
|
+
*/
|
|
6694
|
+
function parseBlock(body) {
|
|
6695
|
+
const doc = parseDocument(body);
|
|
6696
|
+
if (doc.errors.length > 0) return null;
|
|
6697
|
+
if (doc.contents === null) {
|
|
6698
|
+
doc.contents = new YAMLMap();
|
|
6699
|
+
return doc;
|
|
6700
|
+
}
|
|
6701
|
+
return isMap(doc.contents) ? doc : null;
|
|
6702
|
+
}
|
|
6703
|
+
/** The block's YAML as a plain object, or null when it does not parse to a mapping. */
|
|
6704
|
+
function frontmatterData(body) {
|
|
6705
|
+
const doc = parseBlock(body);
|
|
6706
|
+
return doc === null ? null : doc.toJS();
|
|
6707
|
+
}
|
|
6708
|
+
/**
|
|
6709
|
+
* The keys EtherPK reads, on which an empty value means the key is not set (ADR 0108, point 5).
|
|
6710
|
+
* On these an empty `[]` and a bare key say the same thing, so the style writes it bare. Another
|
|
6711
|
+
* key's `[]` is kept: to a tool that reads it, an empty list and no value may differ.
|
|
6712
|
+
*/
|
|
6713
|
+
var NOT_SET_WHEN_EMPTY = new Set([
|
|
6714
|
+
"title",
|
|
6715
|
+
"aliases",
|
|
6716
|
+
"public",
|
|
6717
|
+
"publications",
|
|
6718
|
+
"slug",
|
|
6719
|
+
"date",
|
|
6720
|
+
"publication"
|
|
6721
|
+
]);
|
|
6722
|
+
/** Apply the EtherPK style to a document in place. */
|
|
6723
|
+
function applyStyle(doc) {
|
|
6724
|
+
visit(doc, {
|
|
6725
|
+
Pair(_, pair, path) {
|
|
6726
|
+
if (path.length !== 2) return;
|
|
6727
|
+
const key = isScalar(pair.key) ? pair.key.value : pair.key;
|
|
6728
|
+
if (typeof key === "string" && NOT_SET_WHEN_EMPTY.has(key) && isSeq(pair.value) && pair.value.items.length === 0) pair.value = new Scalar(null);
|
|
6729
|
+
},
|
|
6730
|
+
Scalar(_, node) {
|
|
6731
|
+
node.spaceBefore = false;
|
|
6732
|
+
if (typeof node.value === "string" && !node.value.includes("\n") && (node.type === "QUOTE_DOUBLE" || node.type === "QUOTE_SINGLE")) node.type = "PLAIN";
|
|
6733
|
+
},
|
|
6734
|
+
Map(_, node) {
|
|
6735
|
+
node.spaceBefore = false;
|
|
6736
|
+
if (node.items.length > 0) node.flow = false;
|
|
6737
|
+
},
|
|
6738
|
+
Seq(_, node) {
|
|
6739
|
+
node.spaceBefore = false;
|
|
6740
|
+
if (node.items.length > 0) node.flow = false;
|
|
6741
|
+
}
|
|
6742
|
+
});
|
|
6743
|
+
}
|
|
6744
|
+
/** The styled YAML of a document, ending in a newline, or '' when it has no keys and no comment. */
|
|
6745
|
+
function styledYaml(doc) {
|
|
6746
|
+
applyStyle(doc);
|
|
6747
|
+
const map = doc.contents;
|
|
6748
|
+
if (isMap(map) && map.items.length === 0) return doc.commentBefore ? `${doc.commentBefore.replace(/^/gm, "#")}\n` : "";
|
|
6749
|
+
return doc.toString(STYLE);
|
|
6750
|
+
}
|
|
6751
|
+
/** Deep equality over the plain values a block parses to. */
|
|
6752
|
+
function sameValue(a, b) {
|
|
6753
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
6754
|
+
}
|
|
6755
|
+
/**
|
|
6756
|
+
* A new block holding `data`, in the EtherPK style: `---\n…---\n`, or '' when `data` has no keys.
|
|
6757
|
+
* For a document that has no block yet; to change a block that exists use {@link editFrontmatter},
|
|
6758
|
+
* which keeps what the block already says.
|
|
6759
|
+
*/
|
|
6760
|
+
function renderFrontmatter(data) {
|
|
6761
|
+
if (Object.keys(data).length === 0) return "";
|
|
6762
|
+
return `---\n${styledYaml(new Document(data))}---\n`;
|
|
6763
|
+
}
|
|
6764
|
+
/** The line terminator a block uses, so a rewrite does not mix endings in a CRLF file. */
|
|
6765
|
+
function eolOf(text, end) {
|
|
6766
|
+
return text.slice(0, end).includes("\r\n") ? "\r\n" : "\n";
|
|
6767
|
+
}
|
|
6768
|
+
function withEol(yaml, eol) {
|
|
6769
|
+
return eol === "\n" ? yaml : yaml.replace(/\n/g, eol);
|
|
6770
|
+
}
|
|
6771
|
+
function editorFor(doc) {
|
|
6772
|
+
const map = doc.contents;
|
|
6773
|
+
const keyOf = (item) => String(isScalar(item.key) ? item.key.value : item.key);
|
|
6774
|
+
return {
|
|
6775
|
+
has: (key) => map.has(key),
|
|
6776
|
+
get: (key) => map.has(key) ? doc.toJS()[key] : void 0,
|
|
6777
|
+
set(key, value, where = "last") {
|
|
6778
|
+
if (map.has(key)) {
|
|
6779
|
+
const current = doc.toJS()[key];
|
|
6780
|
+
if (sameValue(current, value)) return;
|
|
6781
|
+
map.set(key, doc.createNode(value));
|
|
6782
|
+
return;
|
|
6783
|
+
}
|
|
6784
|
+
const pair = doc.createPair(key, value);
|
|
6785
|
+
if (where === "first") map.items.unshift(pair);
|
|
6786
|
+
else map.items.push(pair);
|
|
6787
|
+
},
|
|
6788
|
+
delete: (key) => {
|
|
6789
|
+
map.delete(key);
|
|
6790
|
+
},
|
|
6791
|
+
keys: () => map.items.map(keyOf)
|
|
6792
|
+
};
|
|
6793
|
+
}
|
|
6794
|
+
/**
|
|
6795
|
+
* The text with its block changed by `edit`, then written in the EtherPK style. The same string
|
|
6796
|
+
* comes back when the edit changes nothing the block says, so a writer that runs on every save
|
|
6797
|
+
* never restyles a block it agrees with. A block whose YAML does not parse is left alone: rewriting
|
|
6798
|
+
* it would destroy whatever the person was typing. A block the edit empties of every key is
|
|
6799
|
+
* removed.
|
|
6800
|
+
*/
|
|
6801
|
+
function editFrontmatter(text, edit, options = {}) {
|
|
6802
|
+
const span = frontmatterSpan(text);
|
|
6803
|
+
if (!span && !options.addBlock) return text;
|
|
6804
|
+
const doc = span ? parseBlock(span.body) : parseBlock("");
|
|
6805
|
+
if (doc === null) return text;
|
|
6806
|
+
const before = doc.toJS();
|
|
6807
|
+
edit(editorFor(doc));
|
|
6808
|
+
if (sameValue(doc.toJS(), before)) return text;
|
|
6809
|
+
const body = span ? text.slice(span.end) : text;
|
|
6810
|
+
if (doc.contents.items.length === 0) return body;
|
|
6811
|
+
const eol = span ? eolOf(text, span.end) : "\n";
|
|
6812
|
+
return `---${eol}${withEol(styledYaml(doc), eol)}---${eol}${body}`;
|
|
6813
|
+
}
|
|
6814
|
+
/**
|
|
6815
|
+
* Whether a value counts as not set (ADR 0108): nothing, a blank string, an empty list, or a
|
|
6816
|
+
* mapping whose every value is itself empty. `false` and `0` are values.
|
|
6817
|
+
*/
|
|
6818
|
+
function isEmptyValue(value) {
|
|
6819
|
+
if (value === null || value === void 0) return true;
|
|
6820
|
+
if (typeof value === "string") return value.trim() === "";
|
|
6821
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
6822
|
+
if (typeof value === "object" && !(value instanceof Date)) return Object.values(value).every(isEmptyValue);
|
|
6823
|
+
return false;
|
|
6824
|
+
}
|
|
6825
|
+
//#endregion
|
|
6399
6826
|
//#region ../client/src/lib/document/publish/publication.ts
|
|
6400
6827
|
/** A publication id: kebab-case, what `publications:` entries and `theme:` graph ids look like. */
|
|
6401
6828
|
var PUBLICATION_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -6411,13 +6838,19 @@ function readMembership(text, concept) {
|
|
|
6411
6838
|
const { data } = parseFrontmatter(text);
|
|
6412
6839
|
const issues = [];
|
|
6413
6840
|
const isPublic = data.public === true;
|
|
6841
|
+
if (!isEmptyValue(data.public) && typeof data.public !== "boolean") issues.push({
|
|
6842
|
+
level: "warning",
|
|
6843
|
+
code: "public-not-a-boolean",
|
|
6844
|
+
message: `\`public\` is the text "${String(data.public)}", not \`true\` or \`false\`, so the document is not public. Write \`public: true\` without quotes to publish it.`,
|
|
6845
|
+
concept
|
|
6846
|
+
});
|
|
6414
6847
|
const publications = [];
|
|
6415
|
-
const raw = data.publications;
|
|
6416
|
-
const entries = raw === void 0 ? [] : Array.isArray(raw) ? raw :
|
|
6848
|
+
const raw = isEmptyValue(data.publications) ? void 0 : data.publications;
|
|
6849
|
+
const entries = raw === void 0 ? [] : Array.isArray(raw) ? raw : null;
|
|
6417
6850
|
if (entries === null) issues.push({
|
|
6418
6851
|
level: "warning",
|
|
6419
6852
|
code: "publications-not-a-list",
|
|
6420
|
-
message: "`publications` must be a list of publication ids,
|
|
6853
|
+
message: "`publications` must be a list of publication ids, each on its own line under the key as `- docs`, even when there is one.",
|
|
6421
6854
|
concept
|
|
6422
6855
|
});
|
|
6423
6856
|
else for (const entry of entries) if (isPublicationId(entry)) {
|
|
@@ -6434,13 +6867,33 @@ function readMembership(text, concept) {
|
|
|
6434
6867
|
issues
|
|
6435
6868
|
};
|
|
6436
6869
|
}
|
|
6870
|
+
var DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
6871
|
+
/**
|
|
6872
|
+
* The day a page is dated on a blog, from its `date:` key, and a warning when the key holds
|
|
6873
|
+
* something that is not a calendar day. A journal entry is dated by its name, never by this key.
|
|
6874
|
+
*/
|
|
6875
|
+
function readPageDate(doc) {
|
|
6876
|
+
const date = parseFrontmatter(doc.text).data.date;
|
|
6877
|
+
if (isEmptyValue(date)) return { issues: [] };
|
|
6878
|
+
const text = date instanceof Date ? date.toISOString().slice(0, 10) : String(date);
|
|
6879
|
+
if (DATE.test(text)) return {
|
|
6880
|
+
date: text,
|
|
6881
|
+
issues: []
|
|
6882
|
+
};
|
|
6883
|
+
return { issues: [{
|
|
6884
|
+
level: "warning",
|
|
6885
|
+
code: "invalid-date",
|
|
6886
|
+
message: `"${doc.concept}" has \`date: ${String(date)}\`, which is not a calendar day (YYYY-MM-DD) - the document is undated.`,
|
|
6887
|
+
concept: doc.concept
|
|
6888
|
+
}] };
|
|
6889
|
+
}
|
|
6437
6890
|
var KINDS$1 = ["docs", "blog"];
|
|
6438
6891
|
var SELECTIONS = ["named", "all-public"];
|
|
6439
6892
|
/** The publication a page defines through its `publication:` mapping, validated. */
|
|
6440
6893
|
function readPublicationDefinition(doc) {
|
|
6441
6894
|
const { data, body } = parseFrontmatter(doc.text);
|
|
6442
6895
|
const raw = data.publication;
|
|
6443
|
-
if (raw
|
|
6896
|
+
if (isEmptyValue(raw)) return {
|
|
6444
6897
|
publication: null,
|
|
6445
6898
|
issues: []
|
|
6446
6899
|
};
|
|
@@ -6459,13 +6912,13 @@ function readPublicationDefinition(doc) {
|
|
|
6459
6912
|
};
|
|
6460
6913
|
}
|
|
6461
6914
|
if (doc.kind === "journal") {
|
|
6462
|
-
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.");
|
|
6463
6916
|
return {
|
|
6464
6917
|
publication: null,
|
|
6465
6918
|
issues
|
|
6466
6919
|
};
|
|
6467
6920
|
}
|
|
6468
|
-
const def = raw;
|
|
6921
|
+
const def = Object.fromEntries(Object.entries(raw).filter(([, value]) => !isEmptyValue(value)));
|
|
6469
6922
|
const id = def.id;
|
|
6470
6923
|
if (id === void 0 || id === null || id === "") issue("publication-missing-id", "`publication` needs an `id`: lower-case letters, digits and hyphens, like `docs`.");
|
|
6471
6924
|
else if (!isPublicationId(id)) issue("publication-invalid-id", `"${String(id)}" is not a publication id: use lower-case letters, digits and hyphens, like \`docs\`.`);
|
|
@@ -6486,13 +6939,16 @@ function readPublicationDefinition(doc) {
|
|
|
6486
6939
|
if (def.recent !== void 0) if (typeof def.recent === "number" && Number.isInteger(def.recent) && def.recent > 0) recent = def.recent;
|
|
6487
6940
|
else issue("publication-invalid-recent", "`recent` must be a whole number above zero: how many posts the front page lists (10 unless set).");
|
|
6488
6941
|
const includes = {};
|
|
6489
|
-
if (def.includes !== void 0) if (typeof def.includes === "object" && def.includes !== null && !Array.isArray(def.includes)) for (const [slot, value] of Object.entries(def.includes))
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6942
|
+
if (def.includes !== void 0) if (typeof def.includes === "object" && def.includes !== null && !Array.isArray(def.includes)) for (const [slot, value] of Object.entries(def.includes)) {
|
|
6943
|
+
if (isEmptyValue(value)) continue;
|
|
6944
|
+
if (typeof value === "string" && value.trim() !== "") includes[slot] = value.trim();
|
|
6945
|
+
else issues.push({
|
|
6946
|
+
level: "warning",
|
|
6947
|
+
code: "publication-invalid-include",
|
|
6948
|
+
message: `Include \`${slot}\` must name the page that fills it - it is ignored.`,
|
|
6949
|
+
concept: doc.concept
|
|
6950
|
+
});
|
|
6951
|
+
}
|
|
6496
6952
|
else issue("publication-invalid-includes", "`includes` must be a mapping of include slot to the page that fills it.");
|
|
6497
6953
|
if (issues.some((i) => i.level === "error")) return {
|
|
6498
6954
|
publication: null,
|
|
@@ -6551,7 +7007,7 @@ function discoverPublications(documents) {
|
|
|
6551
7007
|
for (const publication of list) issues.push({
|
|
6552
7008
|
level: "error",
|
|
6553
7009
|
code: "publication-duplicate-id",
|
|
6554
|
-
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.`,
|
|
6555
7011
|
concept: publication.concept
|
|
6556
7012
|
});
|
|
6557
7013
|
}
|
|
@@ -6588,7 +7044,7 @@ function validateThemeManifest(raw) {
|
|
|
6588
7044
|
const version = typeof data.version === "string" && data.version.trim() !== "" ? data.version.trim() : "0.0.0";
|
|
6589
7045
|
const contract = typeof data.contract === "number" && Number.isInteger(data.contract) ? data.contract : null;
|
|
6590
7046
|
if (contract === null) errors.push("theme.json needs an integer `contract`, the view version the theme was written for.");
|
|
6591
|
-
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.`);
|
|
6592
7048
|
const kinds = [];
|
|
6593
7049
|
if (Array.isArray(data.kinds)) {
|
|
6594
7050
|
for (const k of data.kinds) if (KINDS.has(k)) kinds.push(k);
|
|
@@ -8557,7 +9013,7 @@ function createIndexCore(host) {
|
|
|
8557
9013
|
}];
|
|
8558
9014
|
}
|
|
8559
9015
|
case "search-text": {
|
|
8560
|
-
const page = db ? searchText$1(db, request.query, request.offset, request.limit) : {
|
|
9016
|
+
const page = db ? searchText$1(db, request.query, request.offset, request.limit, request.filters ?? []) : {
|
|
8561
9017
|
groups: [],
|
|
8562
9018
|
hasMore: false
|
|
8563
9019
|
};
|
|
@@ -8569,7 +9025,7 @@ function createIndexCore(host) {
|
|
|
8569
9025
|
}];
|
|
8570
9026
|
}
|
|
8571
9027
|
case "search-count": {
|
|
8572
|
-
const count = db ? searchTextCount(db, request.query) : {
|
|
9028
|
+
const count = db ? searchTextCount(db, request.query, request.filters ?? []) : {
|
|
8573
9029
|
total: 0,
|
|
8574
9030
|
capped: false
|
|
8575
9031
|
};
|
|
@@ -8580,6 +9036,21 @@ function createIndexCore(host) {
|
|
|
8580
9036
|
capped: count.capped
|
|
8581
9037
|
}];
|
|
8582
9038
|
}
|
|
9039
|
+
case "property-keys": return [{
|
|
9040
|
+
type: "property-keys",
|
|
9041
|
+
id: request.id,
|
|
9042
|
+
keys: db ? propertyKeys(db) : []
|
|
9043
|
+
}];
|
|
9044
|
+
case "property-values": return [{
|
|
9045
|
+
type: "property-values",
|
|
9046
|
+
id: request.id,
|
|
9047
|
+
values: db ? propertyValues(db, request.key) : []
|
|
9048
|
+
}];
|
|
9049
|
+
case "property-match": return [{
|
|
9050
|
+
type: "property-match",
|
|
9051
|
+
id: request.id,
|
|
9052
|
+
documents: db ? documentsMatchingProperties(db, request.filters) : []
|
|
9053
|
+
}];
|
|
8583
9054
|
case "tasks": {
|
|
8584
9055
|
const page = db ? tasksMatching(db, request.query, request.offset, request.limit) : {
|
|
8585
9056
|
hits: [],
|
|
@@ -9297,30 +9768,52 @@ function createRemoteGraphIndex(source, transport, options) {
|
|
|
9297
9768
|
needles: [...needles]
|
|
9298
9769
|
}))).usage;
|
|
9299
9770
|
},
|
|
9300
|
-
async searchText(query, offset, limit) {
|
|
9771
|
+
async searchText(query, offset, limit, filters) {
|
|
9301
9772
|
const response = await request((id) => ({
|
|
9302
9773
|
type: "search-text",
|
|
9303
9774
|
id,
|
|
9304
9775
|
query,
|
|
9305
9776
|
offset,
|
|
9306
|
-
limit
|
|
9777
|
+
limit,
|
|
9778
|
+
...filters?.length ? { filters: [...filters] } : {}
|
|
9307
9779
|
}));
|
|
9308
9780
|
return {
|
|
9309
9781
|
groups: response.groups,
|
|
9310
9782
|
hasMore: response.hasMore
|
|
9311
9783
|
};
|
|
9312
9784
|
},
|
|
9313
|
-
async searchTextCount(query) {
|
|
9785
|
+
async searchTextCount(query, filters) {
|
|
9314
9786
|
const response = await request((id) => ({
|
|
9315
9787
|
type: "search-count",
|
|
9316
9788
|
id,
|
|
9317
|
-
query
|
|
9789
|
+
query,
|
|
9790
|
+
...filters?.length ? { filters: [...filters] } : {}
|
|
9318
9791
|
}));
|
|
9319
9792
|
return {
|
|
9320
9793
|
total: response.total,
|
|
9321
9794
|
capped: response.capped
|
|
9322
9795
|
};
|
|
9323
9796
|
},
|
|
9797
|
+
async propertyKeys() {
|
|
9798
|
+
return (await request((id) => ({
|
|
9799
|
+
type: "property-keys",
|
|
9800
|
+
id
|
|
9801
|
+
}))).keys;
|
|
9802
|
+
},
|
|
9803
|
+
async propertyValues(key) {
|
|
9804
|
+
return (await request((id) => ({
|
|
9805
|
+
type: "property-values",
|
|
9806
|
+
id,
|
|
9807
|
+
key
|
|
9808
|
+
}))).values;
|
|
9809
|
+
},
|
|
9810
|
+
async propertyMatch(filters) {
|
|
9811
|
+
return (await request((id) => ({
|
|
9812
|
+
type: "property-match",
|
|
9813
|
+
id,
|
|
9814
|
+
filters: [...filters]
|
|
9815
|
+
}))).documents;
|
|
9816
|
+
},
|
|
9324
9817
|
async tasks(query, offset, limit) {
|
|
9325
9818
|
const response = await request((id) => ({
|
|
9326
9819
|
type: "tasks",
|
|
@@ -10067,7 +10560,7 @@ async function deleteGraphTheme(adapter, id) {
|
|
|
10067
10560
|
/** What the block claims. An unterminated block is not a block, as everywhere else. */
|
|
10068
10561
|
function frontmatterIdentity(text) {
|
|
10069
10562
|
const span = frontmatterSpan(text);
|
|
10070
|
-
const data = span ?
|
|
10563
|
+
const data = span ? frontmatterData(span.body) : {};
|
|
10071
10564
|
if (data === null) return {
|
|
10072
10565
|
title: null,
|
|
10073
10566
|
aliases: [],
|
|
@@ -10119,37 +10612,19 @@ function sameAliases(a, b) {
|
|
|
10119
10612
|
*/
|
|
10120
10613
|
function withFrontmatterIdentity(text, patch, options = {}) {
|
|
10121
10614
|
const span = frontmatterSpan(text);
|
|
10122
|
-
if (
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
const
|
|
10127
|
-
if (
|
|
10128
|
-
if (Object.keys(data).length === 0) return text;
|
|
10129
|
-
return `---\n${stringify(data)}---\n${text}`;
|
|
10130
|
-
}
|
|
10131
|
-
const data = parseBlock$2(span.body);
|
|
10132
|
-
if (data === null) return text;
|
|
10133
|
-
const currentTitle = titleOf(data);
|
|
10134
|
-
const currentAliases = aliasesOf({ data });
|
|
10135
|
-
const wantTitle = patch.title === void 0 ? currentTitle : patch.title;
|
|
10136
|
-
const wantAliases = patch.aliases === void 0 ? currentAliases : [...patch.aliases];
|
|
10137
|
-
if (wantTitle === currentTitle && sameAliases(wantAliases, currentAliases)) return text;
|
|
10138
|
-
const next = {};
|
|
10139
|
-
if (wantTitle !== null && !("title" in data)) next.title = wantTitle;
|
|
10140
|
-
for (const [key, value] of Object.entries(data)) {
|
|
10141
|
-
if (key === "title") {
|
|
10142
|
-
if (wantTitle !== null) next.title = wantTitle;
|
|
10143
|
-
continue;
|
|
10144
|
-
}
|
|
10145
|
-
if (key === "aliases") {
|
|
10146
|
-
if (wantAliases.length > 0) next.aliases = wantAliases;
|
|
10147
|
-
continue;
|
|
10148
|
-
}
|
|
10149
|
-
next[key] = value;
|
|
10615
|
+
if (span) {
|
|
10616
|
+
const data = frontmatterData(span.body);
|
|
10617
|
+
if (data === null) return text;
|
|
10618
|
+
const wantTitle = patch.title === void 0 ? titleOf(data) : patch.title;
|
|
10619
|
+
const wantAliases = patch.aliases === void 0 ? aliasesOf({ data }) : patch.aliases;
|
|
10620
|
+
if (wantTitle === titleOf(data) && sameAliases(wantAliases, aliasesOf({ data }))) return text;
|
|
10150
10621
|
}
|
|
10151
|
-
|
|
10152
|
-
|
|
10622
|
+
return editFrontmatter(text, (block) => {
|
|
10623
|
+
if (patch.title !== void 0) if (patch.title === null || patch.title.trim() === "") block.delete("title");
|
|
10624
|
+
else block.set("title", patch.title, "first");
|
|
10625
|
+
if (patch.aliases !== void 0) if (patch.aliases.length > 0) block.set("aliases", [...patch.aliases]);
|
|
10626
|
+
else block.delete("aliases");
|
|
10627
|
+
}, options);
|
|
10153
10628
|
}
|
|
10154
10629
|
/**
|
|
10155
10630
|
* `after` - a writer's rewrite of `before` - with the document's aliases carried into the block
|
|
@@ -10164,19 +10639,83 @@ function withAliasesInAddedBlock(before, after, aliases) {
|
|
|
10164
10639
|
if (aliases.length === 0 || frontmatterSpan(before) !== null || frontmatterSpan(after) === null) return after;
|
|
10165
10640
|
return withFrontmatterIdentity(after, { aliases });
|
|
10166
10641
|
}
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10642
|
+
//#endregion
|
|
10643
|
+
//#region ../client/src/lib/document/properties.ts
|
|
10644
|
+
/**
|
|
10645
|
+
* Keys a Property Filter answers from the graph's names rather than the block: the registry on
|
|
10646
|
+
* a Server Backend holds them, and a page created in the app there has no block to read.
|
|
10647
|
+
*/
|
|
10648
|
+
var NAME_PROPERTY_KEYS = new Set(["title", "aliases"]);
|
|
10649
|
+
/** Rows per document at most. Frontmatter is metadata; a block past this is not a label set. */
|
|
10650
|
+
var MAX_ROWS = 256;
|
|
10651
|
+
/** Characters of one value kept. Longer text is prose, which Search's text group covers. */
|
|
10652
|
+
var MAX_VALUE_LENGTH = 1e3;
|
|
10653
|
+
/**
|
|
10654
|
+
* Whether a value counts as not set (ADR 0108): null, blank text, a list with no set item, or a
|
|
10655
|
+
* mapping with no set value. `false` and `0` are values.
|
|
10656
|
+
*/
|
|
10657
|
+
function isEmptyPropertyValue(value) {
|
|
10658
|
+
if (value === null || value === void 0) return true;
|
|
10659
|
+
if (typeof value === "string") return value.trim() === "";
|
|
10660
|
+
if (Array.isArray(value)) return value.every(isEmptyPropertyValue);
|
|
10661
|
+
if (isMapping(value)) return Object.values(value).every(isEmptyPropertyValue);
|
|
10662
|
+
return false;
|
|
10663
|
+
}
|
|
10664
|
+
/**
|
|
10665
|
+
* The rows for one document's parsed Frontmatter, in the block's order. Empty values are left
|
|
10666
|
+
* out, and so are `title` and `aliases` in any spelling: those filters read the graph's names.
|
|
10667
|
+
*/
|
|
10668
|
+
function propertiesOf(data) {
|
|
10669
|
+
const out = [];
|
|
10670
|
+
for (const [key, value] of Object.entries(data)) {
|
|
10671
|
+
if (NAME_PROPERTY_KEYS.has(key.toLowerCase())) continue;
|
|
10672
|
+
collect(key, value, out);
|
|
10673
|
+
if (out.length >= MAX_ROWS) break;
|
|
10674
|
+
}
|
|
10675
|
+
return out.slice(0, MAX_ROWS);
|
|
10676
|
+
}
|
|
10677
|
+
/** Append the rows for `value` under `path`; a mapping adds its presence row before its values. */
|
|
10678
|
+
function collect(path, value, out) {
|
|
10679
|
+
if (isEmptyPropertyValue(value)) return;
|
|
10680
|
+
if (Array.isArray(value)) {
|
|
10681
|
+
let presence = false;
|
|
10682
|
+
for (const item of value) if (isMapping(item)) {
|
|
10683
|
+
if (!presence && !isEmptyPropertyValue(item)) {
|
|
10684
|
+
out.push({
|
|
10685
|
+
key: path,
|
|
10686
|
+
value: null
|
|
10687
|
+
});
|
|
10688
|
+
presence = true;
|
|
10689
|
+
}
|
|
10690
|
+
for (const [key, child] of Object.entries(item)) collect(`${path}.${key}`, child, out);
|
|
10691
|
+
} else collect(path, item, out);
|
|
10692
|
+
return;
|
|
10693
|
+
}
|
|
10694
|
+
if (isMapping(value)) {
|
|
10695
|
+
out.push({
|
|
10696
|
+
key: path,
|
|
10697
|
+
value: null
|
|
10698
|
+
});
|
|
10699
|
+
for (const [key, child] of Object.entries(value)) collect(`${path}.${key}`, child, out);
|
|
10700
|
+
return;
|
|
10176
10701
|
}
|
|
10702
|
+
out.push({
|
|
10703
|
+
key: path,
|
|
10704
|
+
value: textOf(value).slice(0, MAX_VALUE_LENGTH)
|
|
10705
|
+
});
|
|
10177
10706
|
}
|
|
10178
|
-
function
|
|
10179
|
-
return
|
|
10707
|
+
function isMapping(value) {
|
|
10708
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
10709
|
+
}
|
|
10710
|
+
/** A scalar as the text a person would type to find it. */
|
|
10711
|
+
function textOf(value) {
|
|
10712
|
+
if (value instanceof Date) {
|
|
10713
|
+
const iso = value.toISOString();
|
|
10714
|
+
return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
|
|
10715
|
+
}
|
|
10716
|
+
if (typeof value === "string") return value;
|
|
10717
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
10718
|
+
return JSON.stringify(value) ?? "";
|
|
10180
10719
|
}
|
|
10181
10720
|
//#endregion
|
|
10182
10721
|
//#region ../client/src/lib/document/calendar/month-grid-core.ts
|
|
@@ -10507,7 +11046,7 @@ var RenameUnconfirmedError = class extends Error {
|
|
|
10507
11046
|
name = "RenameUnconfirmedError";
|
|
10508
11047
|
constructor(concepts) {
|
|
10509
11048
|
const list = concepts.slice(0, 5).join(", ") + (concepts.length > 5 ? "…" : "");
|
|
10510
|
-
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.`);
|
|
10511
11050
|
this.concepts = concepts;
|
|
10512
11051
|
}
|
|
10513
11052
|
};
|
|
@@ -10757,23 +11296,6 @@ async function scanGraph(adapter, options = {}) {
|
|
|
10757
11296
|
//#endregion
|
|
10758
11297
|
//#region ../client/src/lib/storage/fs/filesystem-store.ts
|
|
10759
11298
|
/**
|
|
10760
|
-
* A {@link DocumentStore} over a real directory (via a {@link DirectoryAdapter}):
|
|
10761
|
-
* the Filesystem Backend. The files *are* the state — this holds only a live
|
|
10762
|
-
* buffer per open document plus a derived registry, and reconciles external
|
|
10763
|
-
* changes (git checkout, Syncthing) the way DESIGN.md → The git workflow requires.
|
|
10764
|
-
*
|
|
10765
|
-
* Assembled entirely from the pure pieces (scan, identity, reconcileDecision,
|
|
10766
|
-
* debounce) so it is exercised in Node over createMemoryDirectoryAdapter before
|
|
10767
|
-
* any browser code exists.
|
|
10768
|
-
*
|
|
10769
|
-
* Async-seam note: the seam's
|
|
10770
|
-
* `getText()` is synchronous but disk reads are async, so `open()` returns a
|
|
10771
|
-
* handle whose buffer is empty on first open and is hydrated by an internal
|
|
10772
|
-
* awaited read that then notifies subscribers the *external* way — which is safe
|
|
10773
|
-
* because DocumentView registers its `subscribe` listener in the same synchronous
|
|
10774
|
-
* onMount tick as its `getText()` seed, before the read resolves.
|
|
10775
|
-
*/
|
|
10776
|
-
/**
|
|
10777
11299
|
* The snapshot for one document's full text: identity from the registry, aliases and include
|
|
10778
11300
|
* facts from the frontmatter, the body for the index. Both backends build theirs this way, so
|
|
10779
11301
|
* the index sees one shape whatever holds the document.
|
|
@@ -10786,12 +11308,14 @@ function indexSnapshotOf(identity, text) {
|
|
|
10786
11308
|
text,
|
|
10787
11309
|
aliases: []
|
|
10788
11310
|
}) : [];
|
|
11311
|
+
const properties = propertiesOf(fm.data);
|
|
10789
11312
|
return {
|
|
10790
11313
|
concept: identity.concept,
|
|
10791
11314
|
kind: identity.kind,
|
|
10792
11315
|
aliases: aliasesOf(fm),
|
|
10793
11316
|
text: fm.body,
|
|
10794
|
-
...includes.length > 0 ? { includes } : {}
|
|
11317
|
+
...includes.length > 0 ? { includes } : {},
|
|
11318
|
+
...properties.length > 0 ? { properties } : {}
|
|
10795
11319
|
};
|
|
10796
11320
|
}
|
|
10797
11321
|
function applyTextChange(text, change) {
|
|
@@ -10964,7 +11488,7 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
10964
11488
|
const fm = parseFrontmatter(text);
|
|
10965
11489
|
let aliases = aliasesOf(fm);
|
|
10966
11490
|
let body = fm.body;
|
|
10967
|
-
let
|
|
11491
|
+
let blockSource = text;
|
|
10968
11492
|
if (strategy === "alias") {
|
|
10969
11493
|
if (!aliases.some((a) => conceptKey(a) === conceptKey(step.from))) aliases = [...aliases, step.from];
|
|
10970
11494
|
}
|
|
@@ -10983,15 +11507,17 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
10983
11507
|
});
|
|
10984
11508
|
body = merged.body;
|
|
10985
11509
|
aliases = merged.aliases;
|
|
10986
|
-
|
|
11510
|
+
blockSource = existing.text;
|
|
10987
11511
|
}
|
|
10988
11512
|
aliases = normaliseAliases(aliases, step.into);
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
11513
|
+
const blockSpan = frontmatterSpan(blockSource);
|
|
11514
|
+
const content = withFrontmatterIdentity((blockSpan ? blockSource.slice(0, blockSpan.end) : "") + body, {
|
|
11515
|
+
title: step.into,
|
|
11516
|
+
aliases
|
|
11517
|
+
}, { addBlock: true });
|
|
10992
11518
|
const subdir = survivor?.subdir ?? entry.subdir;
|
|
10993
11519
|
const fileName = survivor?.fileName ?? await allocateFileName(subdir, step.into, entry);
|
|
10994
|
-
const written = await adapter.write(subdir, fileName,
|
|
11520
|
+
const written = await adapter.write(subdir, fileName, content);
|
|
10995
11521
|
if (!(subdir === entry.subdir && fileName === entry.fileName)) await adapter.remove(entry.subdir, entry.fileName);
|
|
10996
11522
|
registry.delete(entry.key);
|
|
10997
11523
|
if (survivor) registry.delete(survivor.key);
|
|
@@ -11354,7 +11880,7 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11354
11880
|
if (registry.get(key)) throw new Error(`A document for "${concept}" already exists.`);
|
|
11355
11881
|
await adapter.ensureSkeleton();
|
|
11356
11882
|
const fileName = await allocateFileName("pages", concept);
|
|
11357
|
-
const content =
|
|
11883
|
+
const content = renderFrontmatter({ title: concept }) + body;
|
|
11358
11884
|
await adapter.write("pages", fileName, content);
|
|
11359
11885
|
await refreshRegistry();
|
|
11360
11886
|
return concept;
|
|
@@ -11847,12 +12373,14 @@ function createServerDocumentStore(graph, options) {
|
|
|
11847
12373
|
text,
|
|
11848
12374
|
aliases: []
|
|
11849
12375
|
}) : [];
|
|
12376
|
+
const properties = propertiesOf(parseFrontmatter(text).data);
|
|
11850
12377
|
return {
|
|
11851
12378
|
concept,
|
|
11852
12379
|
kind: entry.kind,
|
|
11853
12380
|
aliases: entry.aliases ?? [],
|
|
11854
12381
|
text: bodyForIndex(text),
|
|
11855
|
-
...includes.length > 0 ? { includes } : {}
|
|
12382
|
+
...includes.length > 0 ? { includes } : {},
|
|
12383
|
+
...properties.length > 0 ? { properties } : {}
|
|
11856
12384
|
};
|
|
11857
12385
|
}
|
|
11858
12386
|
/**
|
|
@@ -13005,8 +13533,9 @@ function assembleHeadlessGraph(parts) {
|
|
|
13005
13533
|
}
|
|
13006
13534
|
/** Open a synced graph, scan its registry and build the index; resolves once tools can answer. */
|
|
13007
13535
|
async function openHeadlessGraph(deps) {
|
|
13008
|
-
const
|
|
13009
|
-
|
|
13536
|
+
const cacheName = deps.localCacheName ?? deps.graphId;
|
|
13537
|
+
const cache = await openGraphCache(cacheName);
|
|
13538
|
+
if (deps.persistDir) await loadLocalCache(deps.persistDir, cacheName);
|
|
13010
13539
|
const indexHost = deps.persistDir ? nodeIndexHost(deps.persistDir) : void 0;
|
|
13011
13540
|
const sync = createGraphSync({
|
|
13012
13541
|
graphId: deps.graphId,
|
|
@@ -13135,7 +13664,7 @@ async function openHeadlessGraph(deps) {
|
|
|
13135
13664
|
embeddingModel: deps.embeddingModel,
|
|
13136
13665
|
onSemanticProgress: deps.onSemanticProgress,
|
|
13137
13666
|
onError: deps.onError,
|
|
13138
|
-
persistBackend: deps.persistDir ? () => persistLocalCache(deps.persistDir,
|
|
13667
|
+
persistBackend: deps.persistDir ? () => persistLocalCache(deps.persistDir, cacheName) : void 0,
|
|
13139
13668
|
onChange: (schedule) => sync.onDocUpdate(schedule),
|
|
13140
13669
|
async settle(schedulePersist) {
|
|
13141
13670
|
await sync.flushAll();
|
|
@@ -13262,7 +13791,7 @@ async function openHeadlessFolder(deps) {
|
|
|
13262
13791
|
for (const conflict of raised) {
|
|
13263
13792
|
await store.resolveConflict(conflict.target, "take-disk");
|
|
13264
13793
|
saveErrors.delete(conceptKey(conflict.target));
|
|
13265
|
-
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.`);
|
|
13266
13795
|
}
|
|
13267
13796
|
})();
|
|
13268
13797
|
const pass = reconciling;
|
|
@@ -13441,7 +13970,7 @@ async function openHeadlessFolder(deps) {
|
|
|
13441
13970
|
/** The account has no vault yet, so there is nothing for a Recovery Code to open. */
|
|
13442
13971
|
var NoVaultError = class extends Error {
|
|
13443
13972
|
constructor() {
|
|
13444
|
-
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.");
|
|
13445
13974
|
this.name = "NoVaultError";
|
|
13446
13975
|
}
|
|
13447
13976
|
};
|
|
@@ -13457,7 +13986,7 @@ async function openVaultWithRecoveryCode(api, code) {
|
|
|
13457
13986
|
try {
|
|
13458
13987
|
return (await openVault(fromBase64Url(stored.vault), wrapKey)).vaultKey;
|
|
13459
13988
|
} catch (error) {
|
|
13460
|
-
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.");
|
|
13461
13990
|
throw error;
|
|
13462
13991
|
}
|
|
13463
13992
|
}
|
|
@@ -13586,7 +14115,7 @@ async function unlockByDeviceApproval(api, io) {
|
|
|
13586
14115
|
const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : "open EtherPK in a browser";
|
|
13587
14116
|
io.say("");
|
|
13588
14117
|
io.say(`To approve this device, ${where} (any page - it need not be a note), connected to this account`);
|
|
13589
|
-
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:");
|
|
13590
14119
|
io.say("");
|
|
13591
14120
|
io.say(` ${request.sas}`);
|
|
13592
14121
|
io.say("");
|
|
@@ -13809,7 +14338,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
13809
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",
|
|
13810
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",
|
|
13811
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",
|
|
13812
|
-
"../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",
|
|
13813
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",
|
|
13814
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",
|
|
13815
14344
|
"../etherpk-al-folio/partials/after-content.html": "",
|
|
@@ -13830,7 +14359,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
13830
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",
|
|
13831
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",
|
|
13832
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",
|
|
13833
|
-
"../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",
|
|
13834
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",
|
|
13835
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",
|
|
13836
14365
|
"../etherpk-blog/partials/after-content.html": "",
|
|
@@ -13858,7 +14387,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
13858
14387
|
"../etherpk-docs/partials/before-content.html": "",
|
|
13859
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",
|
|
13860
14389
|
"../etherpk-docs/partials/head.html": "",
|
|
13861
|
-
"../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",
|
|
13862
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",
|
|
13863
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",
|
|
13864
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",
|
|
@@ -13873,7 +14402,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
13873
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",
|
|
13874
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",
|
|
13875
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",
|
|
13876
|
-
"../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",
|
|
13877
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",
|
|
13878
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",
|
|
13879
14408
|
"../etherpk-papermod/partials/after-content.html": "",
|
|
@@ -13897,7 +14426,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
13897
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",
|
|
13898
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",
|
|
13899
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",
|
|
13900
|
-
"../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",
|
|
13901
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",
|
|
13902
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",
|
|
13903
14432
|
"../etherpk-stack/partials/after-content.html": "",
|
|
@@ -13915,12 +14444,12 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
13915
14444
|
"../etherpk-stack/partials/theme-scripts.html": " <script src=\"theme/search.js\" defer><\/script>\n <script src=\"theme/theme.js\" defer><\/script>\n",
|
|
13916
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",
|
|
13917
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",
|
|
13918
|
-
"../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",
|
|
13919
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",
|
|
13920
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",
|
|
13921
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",
|
|
13922
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",
|
|
13923
|
-
"../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",
|
|
13924
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",
|
|
13925
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",
|
|
13926
14455
|
"../etherpk-terminal/partials/after-content.html": "",
|
|
@@ -13989,134 +14518,43 @@ function bundledThemeList() {
|
|
|
13989
14518
|
//#region ../client/src/lib/document/frontmatter/patch.ts
|
|
13990
14519
|
/**
|
|
13991
14520
|
* Rewriting arbitrary keys of a document's [[Frontmatter]] block, the way `withFrontmatterIdentity`
|
|
13992
|
-
* rewrites identity: other keys keep their values and
|
|
13993
|
-
* nothing would change, and a block whose YAML does not parse is left alone rather than
|
|
14521
|
+
* rewrites identity: other keys keep their values, order and comments, the same string comes back
|
|
14522
|
+
* when nothing would change, and a block whose YAML does not parse is left alone rather than
|
|
13994
14523
|
* destroyed. `null` removes a key. A block emptied of every key is removed with it. Identity keys
|
|
13995
14524
|
* are not this function's business: pass them through `withFrontmatterIdentity` so the registry
|
|
13996
14525
|
* rules (ADR 0061) apply.
|
|
13997
14526
|
*/
|
|
13998
|
-
function parseBlock$1(yaml) {
|
|
13999
|
-
try {
|
|
14000
|
-
const parsed = parse(yaml);
|
|
14001
|
-
if (parsed === null || parsed === void 0) return {};
|
|
14002
|
-
if (typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
14003
|
-
return parsed;
|
|
14004
|
-
} catch {
|
|
14005
|
-
return null;
|
|
14006
|
-
}
|
|
14007
|
-
}
|
|
14008
|
-
function same(a, b) {
|
|
14009
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
14010
|
-
}
|
|
14011
14527
|
function withFrontmatterPatch(text, patch, options = {}) {
|
|
14012
|
-
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
for (const [key, value] of Object.entries(patch)) if (value !== null && value !== void 0) data[key] = value;
|
|
14017
|
-
if (Object.keys(data).length === 0) return text;
|
|
14018
|
-
return `---\n${stringify(data)}---\n${text}`;
|
|
14019
|
-
}
|
|
14020
|
-
const data = parseBlock$1(span.body);
|
|
14021
|
-
if (data === null) return text;
|
|
14022
|
-
const next = {};
|
|
14023
|
-
let changed = false;
|
|
14024
|
-
for (const [key, value] of Object.entries(data)) {
|
|
14025
|
-
if (key in patch) {
|
|
14026
|
-
const wanted = patch[key];
|
|
14027
|
-
if (wanted === null || wanted === void 0) {
|
|
14028
|
-
changed = true;
|
|
14029
|
-
continue;
|
|
14030
|
-
}
|
|
14031
|
-
next[key] = wanted;
|
|
14032
|
-
if (!same(wanted, value)) changed = true;
|
|
14033
|
-
continue;
|
|
14034
|
-
}
|
|
14035
|
-
next[key] = value;
|
|
14036
|
-
}
|
|
14037
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
14038
|
-
if (key in data || value === null || value === void 0) continue;
|
|
14039
|
-
next[key] = value;
|
|
14040
|
-
changed = true;
|
|
14041
|
-
}
|
|
14042
|
-
if (!changed) return text;
|
|
14043
|
-
if (Object.keys(next).length === 0) return text.slice(span.end);
|
|
14044
|
-
return `---\n${stringify(next)}---\n${text.slice(span.end)}`;
|
|
14528
|
+
return editFrontmatter(text, (block) => {
|
|
14529
|
+
for (const [key, value] of Object.entries(patch)) if (value === null || value === void 0) block.delete(key);
|
|
14530
|
+
else block.set(key, value);
|
|
14531
|
+
}, options);
|
|
14045
14532
|
}
|
|
14046
14533
|
//#endregion
|
|
14047
14534
|
//#region ../client/src/lib/document/frontmatter/publishing.ts
|
|
14048
14535
|
/**
|
|
14049
14536
|
* The publishing keys of a document's [[Frontmatter]] (ADR 0082): `public: true` is the consent
|
|
14050
|
-
* switch, `publications
|
|
14051
|
-
* key
|
|
14052
|
-
* key is that prompt to whoever edits the file by hand.
|
|
14053
|
-
*
|
|
14054
|
-
*
|
|
14055
|
-
*
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
if (parsed === null || parsed === void 0) return {};
|
|
14061
|
-
if (typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
14062
|
-
return parsed;
|
|
14063
|
-
} catch {
|
|
14064
|
-
return null;
|
|
14065
|
-
}
|
|
14066
|
-
}
|
|
14067
|
-
function sameList(a, b) {
|
|
14068
|
-
return Array.isArray(a) && a.length === b.length && a.every((v, i) => v === b[i]);
|
|
14069
|
-
}
|
|
14537
|
+
* switch, and `publications`, a list of publication ids, routes. A public document with no
|
|
14538
|
+
* publication keeps the key, written bare (`publications:`): public with nowhere to go is a state
|
|
14539
|
+
* worth a prompt, and the empty key is that prompt to whoever edits the file by hand. Bare rather
|
|
14540
|
+
* than `[]` because that is how the EtherPK style writes every empty value (ADR 0108), and because
|
|
14541
|
+
* Enter after a bare key opens the next line one level in, ready for the first `- id`. Not public, the empty key is
|
|
14542
|
+
* removed. Rewritten the way `withFrontmatterIdentity` rewrites identity: other keys keep their
|
|
14543
|
+
* values and order, the same string comes back when nothing would change, and a block whose YAML
|
|
14544
|
+
* does not parse is left alone rather than destroyed. Read through `readMembership` in
|
|
14545
|
+
* `publish/publication.ts`, which reads a bare key and `[]` alike; this file only writes.
|
|
14546
|
+
*/
|
|
14070
14547
|
/** The text with its publishing keys rewritten. `addBlock` adds a block to a document without one. */
|
|
14071
14548
|
function withPublishing(text, patch, options = {}) {
|
|
14072
14549
|
const publications = patch.publications === void 0 || patch.publications === null ? patch.publications : [...new Set(patch.publications.filter(isPublicationId))];
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
if (
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
if (publications
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
}
|
|
14082
|
-
const data = parseBlock(span.body);
|
|
14083
|
-
if (data === null) return text;
|
|
14084
|
-
const next = {};
|
|
14085
|
-
let changed = false;
|
|
14086
|
-
const wantPublic = patch.public === void 0 ? data.public : patch.public;
|
|
14087
|
-
const keepEmpty = wantPublic === true;
|
|
14088
|
-
const wantPublications = publications === void 0 ? data.publications : publications && (publications.length > 0 || keepEmpty) ? publications : null;
|
|
14089
|
-
for (const [key, value] of Object.entries(data)) {
|
|
14090
|
-
if (key === "public") {
|
|
14091
|
-
if (wantPublic === null || wantPublic === void 0) changed = true;
|
|
14092
|
-
else {
|
|
14093
|
-
next.public = wantPublic;
|
|
14094
|
-
if (wantPublic !== value) changed = true;
|
|
14095
|
-
}
|
|
14096
|
-
continue;
|
|
14097
|
-
}
|
|
14098
|
-
if (key === "publications") {
|
|
14099
|
-
if (wantPublications === null || wantPublications === void 0) changed = true;
|
|
14100
|
-
else {
|
|
14101
|
-
next.publications = wantPublications;
|
|
14102
|
-
if (!sameList(value, wantPublications)) changed = true;
|
|
14103
|
-
}
|
|
14104
|
-
continue;
|
|
14105
|
-
}
|
|
14106
|
-
next[key] = value;
|
|
14107
|
-
}
|
|
14108
|
-
if (!("public" in data) && (wantPublic === true || wantPublic === false)) {
|
|
14109
|
-
next.public = wantPublic;
|
|
14110
|
-
changed = true;
|
|
14111
|
-
}
|
|
14112
|
-
if (!("publications" in data) && Array.isArray(wantPublications) && (wantPublications.length > 0 || keepEmpty)) {
|
|
14113
|
-
next.publications = wantPublications;
|
|
14114
|
-
changed = true;
|
|
14115
|
-
}
|
|
14116
|
-
if (!changed) return text;
|
|
14117
|
-
const yaml = Object.keys(next).length === 0 ? "" : stringify(next);
|
|
14118
|
-
if (yaml === "") return text.slice(span.end);
|
|
14119
|
-
return `---\n${yaml}---\n${text.slice(span.end)}`;
|
|
14550
|
+
return editFrontmatter(text, (block) => {
|
|
14551
|
+
if (patch.public === null) block.delete("public");
|
|
14552
|
+
else if (patch.public !== void 0) block.set("public", patch.public);
|
|
14553
|
+
if (publications === void 0) return;
|
|
14554
|
+
const keepEmpty = (patch.public === void 0 ? block.get("public") : patch.public) === true;
|
|
14555
|
+
if (publications === null || publications.length === 0 && !keepEmpty) block.delete("publications");
|
|
14556
|
+
else block.set("publications", publications.length === 0 ? null : publications);
|
|
14557
|
+
}, options);
|
|
14120
14558
|
}
|
|
14121
14559
|
//#endregion
|
|
14122
14560
|
//#region ../client/src/lib/document/publish/seeded.ts
|
|
@@ -14130,7 +14568,7 @@ function seededFiles(publication) {
|
|
|
14130
14568
|
return new Map([
|
|
14131
14569
|
[PAGES_WORKFLOW_PATH, `# Publishes this folder to GitHub Pages as it is: the site is already built by EtherPK,
|
|
14132
14570
|
# so there is no build step. In the repository's Settings → Pages, set the source to
|
|
14133
|
-
# "GitHub Actions" once
|
|
14571
|
+
# "GitHub Actions" once - every push to main then deploys.
|
|
14134
14572
|
name: Deploy to GitHub Pages
|
|
14135
14573
|
on:
|
|
14136
14574
|
push:
|
|
@@ -14161,7 +14599,7 @@ jobs:
|
|
|
14161
14599
|
[WRANGLER_PATH, `// Cloudflare Workers with static assets (Cloudflare's successor to Pages for new sites).
|
|
14162
14600
|
// Deploy with \`npx wrangler deploy\`, or connect the repository in the dashboard under
|
|
14163
14601
|
// Workers → Create → Import a repository and leave the build command empty. The Worker is
|
|
14164
|
-
// named after the publication
|
|
14602
|
+
// named after the publication - Cloudflare warns when this name and the Worker's differ.
|
|
14165
14603
|
{
|
|
14166
14604
|
"name": "${publication.id}",
|
|
14167
14605
|
"compatibility_date": "2026-01-01",
|
|
@@ -14179,9 +14617,9 @@ This folder is a website published by [EtherPK](https://etherpk.com) from the pu
|
|
|
14179
14617
|
publish, so edit the notes, the publication page, its include pages and its theme in EtherPK,
|
|
14180
14618
|
not here.
|
|
14181
14619
|
|
|
14182
|
-
- Preview: open \`index.html\` in a browser
|
|
14620
|
+
- Preview: open \`index.html\` in a browser - search works from disk too.
|
|
14183
14621
|
- GitHub Pages: push this folder to a repository and set Settings → Pages → Source to
|
|
14184
|
-
"GitHub Actions"
|
|
14622
|
+
"GitHub Actions" - \`.github/workflows/pages.yaml\` deploys every push to \`main\`.
|
|
14185
14623
|
- Cloudflare: \`npx wrangler deploy\` uses \`wrangler.jsonc\`, or connect the repository in the
|
|
14186
14624
|
Workers dashboard with no build command.
|
|
14187
14625
|
- Netlify, GitLab Pages, Vercel, S3 or your own server: the folder is plain files, so any of
|
|
@@ -14189,7 +14627,7 @@ not here.
|
|
|
14189
14627
|
`],
|
|
14190
14628
|
[GITIGNORE_PATH, `.DS_Store\nThumbs.db\n`],
|
|
14191
14629
|
[ASSETS_IGNORE_PATH, `# Files in this folder that are not part of the website, kept out of the Worker's assets.
|
|
14192
|
-
# Written once by EtherPK
|
|
14630
|
+
# Written once by EtherPK - yours to edit.
|
|
14193
14631
|
.git
|
|
14194
14632
|
.github
|
|
14195
14633
|
.gitignore
|
|
@@ -14213,15 +14651,15 @@ This folder is a website generated by [EtherPK](https://etherpk.com) from the pu
|
|
|
14213
14651
|
\`robots.txt\` and the text outside this section of \`AGENTS.md\` is rewritten on each publish
|
|
14214
14652
|
and files that are no longer produced are deleted.
|
|
14215
14653
|
|
|
14216
|
-
- 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.
|
|
14217
14655
|
- Content changes are made to the documents in EtherPK. A document is published when its
|
|
14218
14656
|
frontmatter says \`public: true\` and names this publication in \`publications: [${publication.id}]\`
|
|
14219
14657
|
(unless the publication takes every public document).
|
|
14220
|
-
- 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
|
|
14221
14659
|
slots (head, logo, header, footer, before-content, after-content, scripts, styles) are chosen
|
|
14222
14660
|
and edited there too, in the Publish settings and the Theme editor.
|
|
14223
14661
|
- \`index.html\` opens from disk for a preview. \`.github/workflows/pages.yaml\` deploys the folder
|
|
14224
|
-
to GitHub Pages
|
|
14662
|
+
to GitHub Pages, \`wrangler.jsonc\` deploys it to Cloudflare Workers, and any static host serves it
|
|
14225
14663
|
as it is (https://docs.etherpk.com/hosting-a-published-site).
|
|
14226
14664
|
|
|
14227
14665
|
This section is rewritten by EtherPK on every publish. Put your own notes above or below it.
|
|
@@ -15107,7 +15545,7 @@ function buildNav(outline, resolver) {
|
|
|
15107
15545
|
issues.push({
|
|
15108
15546
|
level: "warning",
|
|
15109
15547
|
code: "nav-entry-not-published",
|
|
15110
|
-
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.`
|
|
15111
15549
|
});
|
|
15112
15550
|
continue;
|
|
15113
15551
|
}
|
|
@@ -15128,7 +15566,7 @@ function buildNav(outline, resolver) {
|
|
|
15128
15566
|
issues.push({
|
|
15129
15567
|
level: "warning",
|
|
15130
15568
|
code: "nav-link-unsafe",
|
|
15131
|
-
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.`
|
|
15132
15570
|
});
|
|
15133
15571
|
out.push({
|
|
15134
15572
|
label,
|
|
@@ -15211,7 +15649,7 @@ function allocate(documents) {
|
|
|
15211
15649
|
const derived = [];
|
|
15212
15650
|
for (const doc of documents) {
|
|
15213
15651
|
const value = parseFrontmatter(doc.text).data.slug;
|
|
15214
|
-
if (value
|
|
15652
|
+
if (isEmptyValue(value)) {
|
|
15215
15653
|
derived.push(doc);
|
|
15216
15654
|
continue;
|
|
15217
15655
|
}
|
|
@@ -15298,7 +15736,6 @@ function createThemeRenderer(theme, includes) {
|
|
|
15298
15736
|
* a pre-render pass for the asynchronous bits (diagrams, code); then rendering, includes, the
|
|
15299
15737
|
* theme, the derived files. Everything derived is computed from the included documents alone.
|
|
15300
15738
|
*/
|
|
15301
|
-
var DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
15302
15739
|
function assetHrefOf(name) {
|
|
15303
15740
|
return `assets/${encodeURIComponent(name)}`;
|
|
15304
15741
|
}
|
|
@@ -15366,7 +15803,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15366
15803
|
if (homeKey !== null && !homeDoc) warnings.push({
|
|
15367
15804
|
level: "warning",
|
|
15368
15805
|
code: "home-not-published",
|
|
15369
|
-
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.`
|
|
15370
15807
|
});
|
|
15371
15808
|
const allocation = allocateSlugs.withReport(included.filter((d) => d !== homeDoc));
|
|
15372
15809
|
const slugs = new Map(allocation.slugs);
|
|
@@ -15375,13 +15812,13 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15375
15812
|
for (const c of allocation.collisions) if (c.explicit) warnings.push({
|
|
15376
15813
|
level: "warning",
|
|
15377
15814
|
code: "slug-taken",
|
|
15378
|
-
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.`,
|
|
15379
15816
|
concept: c.concept
|
|
15380
15817
|
});
|
|
15381
15818
|
else info.push({
|
|
15382
15819
|
level: "info",
|
|
15383
15820
|
code: "slug-collision",
|
|
15384
|
-
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.`,
|
|
15385
15822
|
concept: c.concept
|
|
15386
15823
|
});
|
|
15387
15824
|
for (const ignoredSlug of allocation.ignored) {
|
|
@@ -15389,7 +15826,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15389
15826
|
warnings.push({
|
|
15390
15827
|
level: "warning",
|
|
15391
15828
|
code: "slug-ignored",
|
|
15392
|
-
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.`,
|
|
15393
15830
|
concept: ignoredSlug.concept
|
|
15394
15831
|
});
|
|
15395
15832
|
}
|
|
@@ -15439,12 +15876,12 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15439
15876
|
if (theme.manifest.contract < 1) warnings.push({
|
|
15440
15877
|
level: "warning",
|
|
15441
15878
|
code: "theme-older-contract",
|
|
15442
|
-
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.`
|
|
15443
15880
|
});
|
|
15444
15881
|
if (!theme.manifest.kinds.includes(publication.kind)) warnings.push({
|
|
15445
15882
|
level: "warning",
|
|
15446
15883
|
code: "theme-kind-mismatch",
|
|
15447
|
-
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}.`
|
|
15448
15885
|
});
|
|
15449
15886
|
const includeDocs = /* @__PURE__ */ new Map();
|
|
15450
15887
|
for (const [slot, concept] of Object.entries(publication.includes)) {
|
|
@@ -15454,7 +15891,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15454
15891
|
warnings.push({
|
|
15455
15892
|
level: "warning",
|
|
15456
15893
|
code: "include-page-missing",
|
|
15457
|
-
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.`
|
|
15458
15895
|
});
|
|
15459
15896
|
continue;
|
|
15460
15897
|
}
|
|
@@ -15469,7 +15906,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15469
15906
|
warnings.push({
|
|
15470
15907
|
level: "warning",
|
|
15471
15908
|
code: `include-page-${status}`,
|
|
15472
|
-
message: `The include "${slot}" names "${concept}", which ${why}
|
|
15909
|
+
message: `The include "${slot}" names "${concept}", which ${why} - the theme's own ${slot} is used.`
|
|
15473
15910
|
});
|
|
15474
15911
|
continue;
|
|
15475
15912
|
}
|
|
@@ -15542,17 +15979,9 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15542
15979
|
};
|
|
15543
15980
|
if (doc.kind === "journal") page.date = doc.concept;
|
|
15544
15981
|
else {
|
|
15545
|
-
const
|
|
15546
|
-
if (date !== void 0)
|
|
15547
|
-
|
|
15548
|
-
if (DATE.test(text)) page.date = text;
|
|
15549
|
-
else warnings.push({
|
|
15550
|
-
level: "warning",
|
|
15551
|
-
code: "invalid-date",
|
|
15552
|
-
message: `"${doc.concept}" has \`date: ${String(date)}\`, which is not a calendar day (YYYY-MM-DD); the document is undated.`,
|
|
15553
|
-
concept: doc.concept
|
|
15554
|
-
});
|
|
15555
|
-
}
|
|
15982
|
+
const dated = readPageDate(doc);
|
|
15983
|
+
if (dated.date !== void 0) page.date = dated.date;
|
|
15984
|
+
take(dated.issues);
|
|
15556
15985
|
}
|
|
15557
15986
|
for (const link of linksOf(bodyOf(doc), resolver, doc)) report.missingLinks.push(link);
|
|
15558
15987
|
pages.push(page);
|
|
@@ -15591,7 +16020,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15591
16020
|
warnings.push({
|
|
15592
16021
|
level: "warning",
|
|
15593
16022
|
code: "include-unknown-slot",
|
|
15594
|
-
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.`
|
|
15595
16024
|
});
|
|
15596
16025
|
continue;
|
|
15597
16026
|
}
|
|
@@ -15602,7 +16031,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15602
16031
|
warnings.push({
|
|
15603
16032
|
level: "warning",
|
|
15604
16033
|
code: "include-no-css",
|
|
15605
|
-
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.`
|
|
15606
16035
|
});
|
|
15607
16036
|
continue;
|
|
15608
16037
|
}
|
|
@@ -15778,7 +16207,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15778
16207
|
warnings.push({
|
|
15779
16208
|
level: "warning",
|
|
15780
16209
|
code: "asset-missing",
|
|
15781
|
-
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.`,
|
|
15782
16211
|
concept: from
|
|
15783
16212
|
});
|
|
15784
16213
|
continue;
|
|
@@ -15794,7 +16223,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15794
16223
|
else warnings.push({
|
|
15795
16224
|
level: "warning",
|
|
15796
16225
|
code: "katex-assets-unavailable",
|
|
15797
|
-
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."
|
|
15798
16227
|
});
|
|
15799
16228
|
}
|
|
15800
16229
|
const search = pages.map((p) => ({
|
|
@@ -15828,7 +16257,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15828
16257
|
} else info.push({
|
|
15829
16258
|
level: "info",
|
|
15830
16259
|
code: "no-site-url",
|
|
15831
|
-
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."
|
|
15832
16261
|
});
|
|
15833
16262
|
bundle.set(".nojekyll", "");
|
|
15834
16263
|
const external = /* @__PURE__ */ new Set();
|
|
@@ -15839,7 +16268,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15839
16268
|
if (pages.some((p) => p.rendered.mermaidFallback)) warnings.push({
|
|
15840
16269
|
level: "warning",
|
|
15841
16270
|
code: "mermaid-not-prerendered",
|
|
15842
|
-
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."
|
|
15843
16272
|
});
|
|
15844
16273
|
progress("writing", pages.length + 3, pages.length + 3);
|
|
15845
16274
|
report.ok = errors.length === 0;
|
|
@@ -15932,7 +16361,7 @@ async function fetchTheme(url, fetchText) {
|
|
|
15932
16361
|
const { manifest, errors } = parseThemeManifest(json);
|
|
15933
16362
|
if (!manifest) throw new Error(`The theme at ${url} cannot be used: ${errors.join(" ")}`);
|
|
15934
16363
|
if (manifest.files.length === 0) throw new Error(`The theme at ${url} lists no files in its manifest, so nothing can be fetched.`);
|
|
15935
|
-
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}.`);
|
|
15936
16365
|
const base = new URL(url);
|
|
15937
16366
|
const files = /* @__PURE__ */ new Map();
|
|
15938
16367
|
for (const path of manifest.files) {
|
|
@@ -16028,7 +16457,7 @@ function publicationMapping(input) {
|
|
|
16028
16457
|
* page whose purpose was anyone's guess (2026-09-19).
|
|
16029
16458
|
*/
|
|
16030
16459
|
function publicationPageIntro(name) {
|
|
16031
|
-
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.`;
|
|
16032
16461
|
}
|
|
16033
16462
|
/**
|
|
16034
16463
|
* Create a publication page: a page whose frontmatter defines the publication and whose body
|
|
@@ -16182,7 +16611,7 @@ async function setupDiagrams(env, say) {
|
|
|
16182
16611
|
}
|
|
16183
16612
|
const cli = require$1.resolve("playwright-core/cli.js");
|
|
16184
16613
|
const dir = env.PLAYWRIGHT_BROWSERS_PATH?.trim() || browsersDir(env);
|
|
16185
|
-
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)…`);
|
|
16186
16615
|
await new Promise((resolve, reject) => {
|
|
16187
16616
|
const child = spawn(process.execPath, [
|
|
16188
16617
|
cli,
|
|
@@ -16208,7 +16637,7 @@ async function setupDiagrams(env, say) {
|
|
|
16208
16637
|
child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`Playwright's installer exited with code ${code}.`)));
|
|
16209
16638
|
});
|
|
16210
16639
|
const after = await chromiumStatus(env);
|
|
16211
|
-
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.");
|
|
16212
16641
|
say(`Chromium is set up: ${after.executable}`);
|
|
16213
16642
|
return after;
|
|
16214
16643
|
}
|
|
@@ -16765,7 +17194,7 @@ function agentFrontmatter(rawText) {
|
|
|
16765
17194
|
/**
|
|
16766
17195
|
* The block after a patch. `public` and `publications` go through the publishing writer, so its
|
|
16767
17196
|
* rules hold whoever writes them: an id is lower-case letters, digits and hyphens, and a public
|
|
16768
|
-
* document with an empty list keeps
|
|
17197
|
+
* document with an empty list keeps the key, written bare, as the prompt it is. Everything else is a
|
|
16769
17198
|
* plain key. An invalid publication id is refused rather than dropped, which is what the
|
|
16770
17199
|
* writer would do: an agent that misspells an id must hear about it.
|
|
16771
17200
|
*/
|
|
@@ -16839,7 +17268,7 @@ async function liveText(graph, identity) {
|
|
|
16839
17268
|
return graph.store.open(identity.concept).getText();
|
|
16840
17269
|
}
|
|
16841
17270
|
function refuseIfProtected(concept, text) {
|
|
16842
|
-
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.`);
|
|
16843
17272
|
}
|
|
16844
17273
|
function page$1(items, offset, limit) {
|
|
16845
17274
|
return {
|
|
@@ -16948,7 +17377,24 @@ function round(similarity) {
|
|
|
16948
17377
|
return Math.round(similarity * 1e3) / 1e3;
|
|
16949
17378
|
}
|
|
16950
17379
|
async function searchText(graph, query, offset, limit) {
|
|
16951
|
-
const
|
|
17380
|
+
const keys = await graph.index.propertyKeys();
|
|
17381
|
+
const { words, filters } = parseSearchQuery(query, new Set(keys.map((info) => info.key.toLowerCase())));
|
|
17382
|
+
if (filters.length > 0 && words.trim() === "") {
|
|
17383
|
+
const documents = await graph.index.propertyMatch(filters);
|
|
17384
|
+
return {
|
|
17385
|
+
results: documents.slice(offset, offset + limit).map((doc) => ({
|
|
17386
|
+
concept: doc.concept,
|
|
17387
|
+
kind: doc.kind,
|
|
17388
|
+
matches: 0,
|
|
17389
|
+
hits: [],
|
|
17390
|
+
properties: doc.properties
|
|
17391
|
+
})),
|
|
17392
|
+
total: documents.length,
|
|
17393
|
+
totalCapped: false,
|
|
17394
|
+
hasMore: offset + limit < documents.length
|
|
17395
|
+
};
|
|
17396
|
+
}
|
|
17397
|
+
const [result, count] = await Promise.all([graph.index.searchText(words, offset, limit, filters), graph.index.searchTextCount(words, filters)]);
|
|
16952
17398
|
return {
|
|
16953
17399
|
results: result.groups.map((group) => ({
|
|
16954
17400
|
concept: group.concept,
|
|
@@ -17025,7 +17471,7 @@ async function editDocument(graph, args) {
|
|
|
17025
17471
|
const text = await liveText(graph, identity);
|
|
17026
17472
|
refuseIfProtected(identity.concept, text);
|
|
17027
17473
|
const first = text.indexOf(args.old);
|
|
17028
|
-
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.`);
|
|
17029
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.`);
|
|
17030
17476
|
const insert = normaliseIndentUnit(args.new);
|
|
17031
17477
|
graph.store.open(identity.concept).applyChange({
|
|
@@ -17072,7 +17518,7 @@ async function appendDocument(graph, args) {
|
|
|
17072
17518
|
async function createPage(graph, args) {
|
|
17073
17519
|
const title = args.title.trim();
|
|
17074
17520
|
if (title === "") throw new ToolError("invalid_argument", "title must not be empty.");
|
|
17075
|
-
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.`);
|
|
17076
17522
|
await graph.store.refresh();
|
|
17077
17523
|
if (resolveIdentity(graph, title)) throw new ToolError("already_exists", `A document already answers to "${title}". Use edit_document or append_document instead.`);
|
|
17078
17524
|
await graph.store.createPage(title, normaliseIndentUnit(args.text ?? ""));
|
|
@@ -17402,7 +17848,7 @@ async function listAssets(graph, args = {}) {
|
|
|
17402
17848
|
*/
|
|
17403
17849
|
async function readDocuments(graph, args) {
|
|
17404
17850
|
if (!Array.isArray(args.concepts) || args.concepts.length === 0) throw new ToolError("invalid_argument", "concepts must be a non-empty list of names.");
|
|
17405
|
-
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.`);
|
|
17406
17852
|
await graph.store.refresh();
|
|
17407
17853
|
let budget = READ_MANY_TEXT_CAP;
|
|
17408
17854
|
const documents = [];
|
|
@@ -17470,9 +17916,9 @@ async function setTask(graph, args) {
|
|
|
17470
17916
|
const lines = text.split("\n");
|
|
17471
17917
|
const current = lines[args.line];
|
|
17472
17918
|
const parsed = current === void 0 ? null : parseTaskLine(current);
|
|
17473
|
-
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.`);
|
|
17474
17920
|
const afterCheckbox = current.replace(/^\s*-\s+\[[ xX]\]\s?/, "").trim();
|
|
17475
|
-
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.`);
|
|
17476
17922
|
const tags = { ...parsed };
|
|
17477
17923
|
let done = parsed.done;
|
|
17478
17924
|
switch (changes.status) {
|
|
@@ -17549,7 +17995,7 @@ async function graphInfo(graph) {
|
|
|
17549
17995
|
complete: status.embedded >= status.total
|
|
17550
17996
|
} : {
|
|
17551
17997
|
state: "not-opened",
|
|
17552
|
-
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."
|
|
17553
17999
|
},
|
|
17554
18000
|
assets: { available: graph.assets !== void 0 }
|
|
17555
18001
|
};
|
|
@@ -17665,7 +18111,7 @@ async function createPublication(graph, args, host) {
|
|
|
17665
18111
|
await graph.store.refresh();
|
|
17666
18112
|
const before = summarisePublishing((await graph.publishing.readSource()).source);
|
|
17667
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}").`);
|
|
17668
|
-
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.`);
|
|
17669
18115
|
if (args.theme) await requireTheme(graph, args.theme);
|
|
17670
18116
|
const store = frontmatterStore(graph);
|
|
17671
18117
|
const concept = await createPublicationPage(store, {
|
|
@@ -17918,8 +18364,8 @@ var SAMPLE_SOURCE = {
|
|
|
17918
18364
|
""
|
|
17919
18365
|
].join("\n")),
|
|
17920
18366
|
page("[[Getting Started]] Installing", "---\npublic: true\n---\n- A scoped concept: its heading links to its scope. Back to [[Getting Started]].\n"),
|
|
17921
|
-
page("Wikilinks", "---\npublic: true\naliases: [Links]\n---\n- `[[Concept]]` links to a page
|
|
17922
|
-
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"),
|
|
17923
18369
|
{
|
|
17924
18370
|
concept: "2026-06-02",
|
|
17925
18371
|
kind: "journal",
|
|
@@ -18142,7 +18588,7 @@ async function customisePublicationTheme(graph, args) {
|
|
|
18142
18588
|
await graph.store.refresh();
|
|
18143
18589
|
const { source } = await graph.publishing.readSource();
|
|
18144
18590
|
const publication = summarisePublishing(source).publications.find((p) => p.id === args.publication.trim());
|
|
18145
|
-
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.`);
|
|
18146
18592
|
const current = await graph.themes.get(publication.theme);
|
|
18147
18593
|
if (current) return {
|
|
18148
18594
|
created: false,
|
|
@@ -18178,7 +18624,7 @@ async function requireEditable(graph, id) {
|
|
|
18178
18624
|
const own = await graph.themes.get(id);
|
|
18179
18625
|
if (own) return own;
|
|
18180
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.`);
|
|
18181
|
-
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.`);
|
|
18182
18628
|
}
|
|
18183
18629
|
function requireThemePath(path) {
|
|
18184
18630
|
const clean = path.trim().replace(/^\.?\//, "");
|
|
@@ -18278,7 +18724,7 @@ async function importThemeFolder(graph, args) {
|
|
|
18278
18724
|
} catch (error) {
|
|
18279
18725
|
throw new ToolError("invalid_argument", `Cannot read "${args.dir}": ${error instanceof Error ? error.message : String(error)}`);
|
|
18280
18726
|
}
|
|
18281
|
-
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.`);
|
|
18282
18728
|
const next = {
|
|
18283
18729
|
...theme,
|
|
18284
18730
|
files: Object.fromEntries(files),
|
|
@@ -18300,7 +18746,7 @@ async function deleteTheme(graph, args) {
|
|
|
18300
18746
|
await graph.store.refresh();
|
|
18301
18747
|
const theme = await requireEditable(graph, args.id.trim());
|
|
18302
18748
|
const users = await publicationsUsing(graph, theme.id);
|
|
18303
|
-
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.`);
|
|
18304
18750
|
await graph.themes.remove(theme.id);
|
|
18305
18751
|
await settle(graph);
|
|
18306
18752
|
return {
|
|
@@ -18325,7 +18771,7 @@ async function previewTheme(graph, args, host) {
|
|
|
18325
18771
|
if (args.publication?.trim()) {
|
|
18326
18772
|
const read = await graph.publishing.readSource();
|
|
18327
18773
|
const found = summarisePublishing(read.source).publications.find((p) => p.id === args.publication.trim());
|
|
18328
|
-
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.`);
|
|
18329
18775
|
publication = found;
|
|
18330
18776
|
source = read.source;
|
|
18331
18777
|
unsettled = read.unsettled;
|
|
@@ -18369,7 +18815,7 @@ async function previewTheme(graph, args, host) {
|
|
|
18369
18815
|
...screenshots ? { screenshots } : {},
|
|
18370
18816
|
...chromium ? {
|
|
18371
18817
|
screenshots: null,
|
|
18372
|
-
note: `No browser to photograph with
|
|
18818
|
+
note: `No browser to photograph with - run ${chromium.setupCommand} or set ETHERPK_CHROMIUM.`
|
|
18373
18819
|
} : {}
|
|
18374
18820
|
};
|
|
18375
18821
|
} finally {
|
|
@@ -18483,25 +18929,25 @@ function createMcpServer(graph, info) {
|
|
|
18483
18929
|
version: info.version
|
|
18484
18930
|
}, { instructions: [
|
|
18485
18931
|
`You are connected to the EtherPK knowledge graph "${info.graphName}": daily journal entries and titled pages in plain markdown, linked with [[wikilinks]].`,
|
|
18486
|
-
"Concept names are case-insensitive. A protected document is listed with protected: true and cannot be read or written here
|
|
18487
|
-
"Bullets are \"- \" with two-space nesting
|
|
18488
|
-
"read_document returns a document's body as text and its frontmatter block as data (the \"frontmatter\" object)
|
|
18489
|
-
"Edit with edit_document (an exact, unique old→new replacement) or append_document
|
|
18490
|
-
"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\")
|
|
18491
|
-
"search has two kinds of matching: mode \"text\" (default) matches words and quoted phrases
|
|
18492
|
-
"Start with graph_info to see what you are connected to. read_documents reads several documents in one call
|
|
18493
|
-
"Publishing: list_publications shows the publications this graph defines (a publication is a page whose frontmatter defines it
|
|
18494
|
-
"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
|
|
18495
|
-
"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.",
|
|
18496
18942
|
"The user documentation is at https://docs.etherpk.com."
|
|
18497
18943
|
].join("\n") });
|
|
18498
18944
|
server.registerTool("list_documents", {
|
|
18499
18945
|
title: "List documents",
|
|
18500
|
-
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.`,
|
|
18501
18947
|
inputSchema: {
|
|
18502
18948
|
kind: z.enum(["page", "journal"]).optional().describe("Only pages, or only journal entries."),
|
|
18503
|
-
from: z.string().optional().describe("Journal entries on or after this day (YYYY-MM-DD)
|
|
18504
|
-
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."),
|
|
18505
18951
|
offset,
|
|
18506
18952
|
limit: z.number().int().positive().max(200).optional()
|
|
18507
18953
|
}
|
|
@@ -18518,7 +18964,7 @@ function createMcpServer(graph, info) {
|
|
|
18518
18964
|
}, async (args) => run(() => readDocuments(graph, args)));
|
|
18519
18965
|
server.registerTool("search", {
|
|
18520
18966
|
title: "Search",
|
|
18521
|
-
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.`,
|
|
18522
18968
|
inputSchema: {
|
|
18523
18969
|
query: z.string().min(1),
|
|
18524
18970
|
mode: z.enum([
|
|
@@ -18565,7 +19011,7 @@ function createMcpServer(graph, info) {
|
|
|
18565
19011
|
}, async (args) => run(() => tasks(graph, args)));
|
|
18566
19012
|
server.registerTool("set_task", {
|
|
18567
19013
|
title: "Set a task",
|
|
18568
|
-
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.",
|
|
18569
19015
|
inputSchema: {
|
|
18570
19016
|
concept,
|
|
18571
19017
|
line: z.number().int().nonnegative(),
|
|
@@ -18591,7 +19037,7 @@ function createMcpServer(graph, info) {
|
|
|
18591
19037
|
}, async (args) => run(() => setTask(graph, args)));
|
|
18592
19038
|
server.registerTool("edit_document", {
|
|
18593
19039
|
title: "Edit a document",
|
|
18594
|
-
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.",
|
|
18595
19041
|
inputSchema: {
|
|
18596
19042
|
concept,
|
|
18597
19043
|
old: z.string().min(1).describe("The exact text to replace, as read_document returned it."),
|
|
@@ -18625,7 +19071,7 @@ function createMcpServer(graph, info) {
|
|
|
18625
19071
|
}, async (args) => run(() => planRename(graph, args)));
|
|
18626
19072
|
server.registerTool("rename", {
|
|
18627
19073
|
title: "Rename",
|
|
18628
|
-
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.",
|
|
18629
19075
|
inputSchema: {
|
|
18630
19076
|
from: z.string().min(1),
|
|
18631
19077
|
to: z.string().min(1),
|
|
@@ -18638,7 +19084,7 @@ function createMcpServer(graph, info) {
|
|
|
18638
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\".",
|
|
18639
19085
|
inputSchema: {
|
|
18640
19086
|
concept,
|
|
18641
|
-
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.")
|
|
18642
19088
|
}
|
|
18643
19089
|
}, async (args) => run(() => setAliases(graph, args)));
|
|
18644
19090
|
server.registerTool("set_frontmatter", {
|
|
@@ -18646,23 +19092,23 @@ function createMcpServer(graph, info) {
|
|
|
18646
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.",
|
|
18647
19093
|
inputSchema: {
|
|
18648
19094
|
concept,
|
|
18649
|
-
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}.")
|
|
18650
19096
|
}
|
|
18651
19097
|
}, async (args) => run(() => setFrontmatter(graph, args)));
|
|
18652
19098
|
server.registerTool("upload_asset", {
|
|
18653
19099
|
title: "Upload an asset",
|
|
18654
|
-
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.",
|
|
18655
19101
|
inputSchema: {
|
|
18656
19102
|
path: z.string().min(1).describe("Absolute path of an ordinary file on this machine."),
|
|
18657
|
-
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.")
|
|
18658
19104
|
}
|
|
18659
19105
|
}, async (args) => run(() => uploadAsset(graph, args)));
|
|
18660
19106
|
server.registerTool("read_asset", {
|
|
18661
19107
|
title: "Read an asset",
|
|
18662
|
-
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.",
|
|
18663
19109
|
inputSchema: {
|
|
18664
19110
|
ref: z.string().min(1),
|
|
18665
|
-
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.")
|
|
18666
19112
|
}
|
|
18667
19113
|
}, async (args) => run(() => readAsset(graph, args)));
|
|
18668
19114
|
server.registerTool("list_assets", {
|
|
@@ -18676,25 +19122,25 @@ function createMcpServer(graph, info) {
|
|
|
18676
19122
|
};
|
|
18677
19123
|
server.registerTool("list_publications", {
|
|
18678
19124
|
title: "List publications",
|
|
18679
|
-
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.",
|
|
18680
19126
|
inputSchema: {}
|
|
18681
19127
|
}, async () => run(() => listPublications(graph, host)));
|
|
18682
19128
|
server.registerTool("create_publication", {
|
|
18683
19129
|
title: "Create a publication",
|
|
18684
|
-
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.",
|
|
18685
19131
|
inputSchema: {
|
|
18686
19132
|
title: z.string().min(1),
|
|
18687
19133
|
id: z.string().min(1).optional(),
|
|
18688
19134
|
kind: z.enum(["docs", "blog"]).optional(),
|
|
18689
19135
|
selection: z.enum(["named", "all-public"]).optional(),
|
|
18690
|
-
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."),
|
|
18691
19137
|
url: z.string().min(1).optional().describe("Where the site will be deployed, for its feed and sitemap."),
|
|
18692
19138
|
theme: z.string().min(1).optional()
|
|
18693
19139
|
}
|
|
18694
19140
|
}, async (args) => run(() => createPublication(graph, args, host)));
|
|
18695
19141
|
server.registerTool("update_publication", {
|
|
18696
19142
|
title: "Update a publication",
|
|
18697
|
-
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.",
|
|
18698
19144
|
inputSchema: {
|
|
18699
19145
|
id: z.string().min(1),
|
|
18700
19146
|
changes: z.object({
|
|
@@ -18703,27 +19149,27 @@ function createMcpServer(graph, info) {
|
|
|
18703
19149
|
home: z.string().nullable().optional(),
|
|
18704
19150
|
url: z.string().nullable().optional(),
|
|
18705
19151
|
theme: z.string().nullable().optional(),
|
|
18706
|
-
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)."),
|
|
18707
19153
|
includes: z.record(z.string(), z.string().nullable()).optional()
|
|
18708
19154
|
})
|
|
18709
19155
|
}
|
|
18710
19156
|
}, async (args) => run(() => updatePublication(graph, args, host)));
|
|
18711
19157
|
server.registerTool("publish", {
|
|
18712
19158
|
title: "Publish",
|
|
18713
|
-
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.",
|
|
18714
19160
|
inputSchema: { id: z.string().min(1) }
|
|
18715
19161
|
}, async (args) => run(() => publish(graph, args, host)));
|
|
18716
19162
|
server.registerTool("list_themes", {
|
|
18717
19163
|
title: "List themes",
|
|
18718
|
-
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).",
|
|
18719
19165
|
inputSchema: {}
|
|
18720
19166
|
}, async () => run(() => listThemes(graph)));
|
|
18721
19167
|
server.registerTool("read_theme", {
|
|
18722
19168
|
title: "Read a theme",
|
|
18723
|
-
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).",
|
|
18724
19170
|
inputSchema: {
|
|
18725
19171
|
ref: z.string().min(1),
|
|
18726
|
-
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.")
|
|
18727
19173
|
}
|
|
18728
19174
|
}, async (args) => run(() => readTheme(graph, args)));
|
|
18729
19175
|
server.registerTool("read_theme_file", {
|
|
@@ -18736,19 +19182,19 @@ function createMcpServer(graph, info) {
|
|
|
18736
19182
|
}, async (args) => run(() => readThemeFile(graph, args)));
|
|
18737
19183
|
server.registerTool("create_theme", {
|
|
18738
19184
|
title: "Create a theme",
|
|
18739
|
-
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>).",
|
|
18740
19186
|
inputSchema: {
|
|
18741
19187
|
from: z.string().min(1),
|
|
18742
|
-
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."),
|
|
18743
19189
|
name: z.string().min(1).optional()
|
|
18744
19190
|
}
|
|
18745
19191
|
}, async (args) => run(() => createTheme(graph, args)));
|
|
18746
19192
|
server.registerTool("customise_publication_theme", {
|
|
18747
19193
|
title: "Customise a publication's theme",
|
|
18748
|
-
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.",
|
|
18749
19195
|
inputSchema: {
|
|
18750
19196
|
publication: z.string().min(1).describe("The publication id."),
|
|
18751
|
-
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."),
|
|
18752
19198
|
name: z.string().min(1).optional()
|
|
18753
19199
|
}
|
|
18754
19200
|
}, async (args) => run(() => customisePublicationTheme(graph, args)));
|
|
@@ -18763,7 +19209,7 @@ function createMcpServer(graph, info) {
|
|
|
18763
19209
|
}, async (args) => run(() => writeThemeFile(graph, args)));
|
|
18764
19210
|
server.registerTool("delete_theme_file", {
|
|
18765
19211
|
title: "Delete a theme file",
|
|
18766
|
-
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.",
|
|
18767
19213
|
inputSchema: {
|
|
18768
19214
|
id: z.string().min(1),
|
|
18769
19215
|
path: z.string().min(1)
|
|
@@ -18771,7 +19217,7 @@ function createMcpServer(graph, info) {
|
|
|
18771
19217
|
}, async (args) => run(() => deleteThemeFile(graph, args)));
|
|
18772
19218
|
server.registerTool("import_theme_folder", {
|
|
18773
19219
|
title: "Import a theme folder",
|
|
18774
|
-
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.",
|
|
18775
19221
|
inputSchema: {
|
|
18776
19222
|
id: z.string().min(1),
|
|
18777
19223
|
dir: z.string().min(1)
|
|
@@ -18779,12 +19225,12 @@ function createMcpServer(graph, info) {
|
|
|
18779
19225
|
}, async (args) => run(() => importThemeFolder(graph, args)));
|
|
18780
19226
|
server.registerTool("delete_theme", {
|
|
18781
19227
|
title: "Delete a theme",
|
|
18782
|
-
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.",
|
|
18783
19229
|
inputSchema: { id: z.string().min(1) }
|
|
18784
19230
|
}, async (args) => run(() => deleteTheme(graph, args)));
|
|
18785
19231
|
server.registerTool("preview_theme", {
|
|
18786
19232
|
title: "Preview a theme",
|
|
18787
|
-
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.",
|
|
18788
19234
|
inputSchema: {
|
|
18789
19235
|
theme: z.string().min(1).optional(),
|
|
18790
19236
|
publication: z.string().min(1).optional(),
|
|
@@ -19020,20 +19466,20 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
19020
19466
|
Serve one synced graph to an agent over stdio. For Claude Code:
|
|
19021
19467
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --sync-server <url> --graph <id>
|
|
19022
19468
|
Once "semantic setup" has run on this computer, serve also keeps a search-by-meaning
|
|
19023
|
-
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
|
|
19024
19470
|
--no-semantic to leave it off for this registration.
|
|
19025
19471
|
${CMD} serve --folder <path> [--no-semantic]
|
|
19026
19472
|
Serve a local graph folder the same way: no sign-in, no server. The agent gets search,
|
|
19027
19473
|
backlinks, tasks and format-safe edits over the folder's markdown, alongside the files
|
|
19028
19474
|
themselves. Edits made in an editor or by the agent directly are picked up as they land.
|
|
19029
|
-
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
|
|
19030
19476
|
the cache directory, never in the folder.
|
|
19031
19477
|
${CMD} logout [--sync-server <url> | --all]
|
|
19032
19478
|
Forget that server's token, keys and cached graphs on this machine.
|
|
19033
19479
|
${CMD} semantic setup
|
|
19034
19480
|
Let the agent search by meaning. Installs a ~300 MB native runtime (onnxruntime-node,
|
|
19035
19481
|
with your npm) and downloads a 23 MB embedding model into the cache directory, once
|
|
19036
|
-
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
|
|
19037
19483
|
to a service to be embedded.
|
|
19038
19484
|
${CMD} semantic status | remove
|
|
19039
19485
|
Whether it is set up here, or delete it (logout --all deletes it too).
|
|
@@ -19043,20 +19489,20 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
19043
19489
|
then on this command without --out, and the agent's publish tool, write to the same
|
|
19044
19490
|
folder (the agent cannot choose one). The folder's owned files (.html at the top,
|
|
19045
19491
|
assets/, theme/, the search, feed and report files) are rewritten and their strays
|
|
19046
|
-
removed
|
|
19492
|
+
removed - everything else in it is left alone. Publishing writes files - deploying them
|
|
19047
19493
|
(a git push, say) is yours. Run it from cron for a site that republishes itself.
|
|
19048
19494
|
${CMD} diagrams setup
|
|
19049
19495
|
Let a publish draw Mermaid diagrams. Downloads a Chromium (about 170 MB) into the cache
|
|
19050
|
-
directory with Playwright's installer, once per computer
|
|
19496
|
+
directory with Playwright's installer, once per computer - a publish whose pages hold
|
|
19051
19497
|
diagrams refuses until this has run. Set ETHERPK_CHROMIUM=<path> to use a browser
|
|
19052
19498
|
already on this machine instead (NixOS needs this).
|
|
19053
19499
|
${CMD} diagrams status
|
|
19054
19500
|
Which browser a publish would use, if any.
|
|
19055
19501
|
|
|
19056
|
-
This machine can hold logins for several Sync Servers at once
|
|
19057
|
-
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
|
|
19058
19504
|
${defaultConfigPath()} (override with
|
|
19059
|
-
ETHERPK_MCP_CONFIG)
|
|
19505
|
+
ETHERPK_MCP_CONFIG) - cached graphs live under ~/.cache/etherpk/mcp (override with
|
|
19060
19506
|
ETHERPK_MCP_CACHE_DIR).
|
|
19061
19507
|
Docs: https://docs.etherpk.com/using-ai-agents-with-your-notes
|
|
19062
19508
|
`;
|
|
@@ -19069,7 +19515,7 @@ function fail(message) {
|
|
|
19069
19515
|
* an agent starting the process), since each question has its own option or variable.
|
|
19070
19516
|
*/
|
|
19071
19517
|
async function ask(question, { secret = false, hint }) {
|
|
19072
|
-
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}.`);
|
|
19073
19519
|
if (!secret) {
|
|
19074
19520
|
const rl = createInterface({
|
|
19075
19521
|
input: process.stdin,
|
|
@@ -19108,7 +19554,7 @@ async function ask(question, { secret = false, hint }) {
|
|
|
19108
19554
|
}
|
|
19109
19555
|
async function loadConfig(path) {
|
|
19110
19556
|
const config = await readConfig(path);
|
|
19111
|
-
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).`);
|
|
19112
19558
|
return config;
|
|
19113
19559
|
}
|
|
19114
19560
|
/** The login a command means, or the reason there is none - in words the user can act on. */
|
|
@@ -19149,7 +19595,7 @@ async function login(args) {
|
|
|
19149
19595
|
await writeConfig(path, config);
|
|
19150
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.`);
|
|
19151
19597
|
const others = Object.keys(config.servers).filter((server) => server !== syncServer);
|
|
19152
|
-
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.`);
|
|
19153
19599
|
await listGraphs({
|
|
19154
19600
|
syncServer,
|
|
19155
19601
|
pat,
|
|
@@ -19205,7 +19651,7 @@ async function approveOrFallBack(account, byRecoveryCode) {
|
|
|
19205
19651
|
process.exit(controls.exitCode);
|
|
19206
19652
|
}
|
|
19207
19653
|
if (!abandoned) throw new Error("unreachable");
|
|
19208
|
-
console.log("Approval cancelled
|
|
19654
|
+
console.log("Approval cancelled - unlocking with your Recovery Code instead.");
|
|
19209
19655
|
return byRecoveryCode();
|
|
19210
19656
|
}
|
|
19211
19657
|
/**
|
|
@@ -19274,7 +19720,7 @@ async function semanticCommand(what) {
|
|
|
19274
19720
|
const status = await semanticSetupStatus(process.env);
|
|
19275
19721
|
console.log(`Runtime: ${status.runtime ? "installed" : "missing"} (${status.runtimeDir})`);
|
|
19276
19722
|
console.log(`Model: ${status.model ? "present" : "missing"} (${status.modelDir})`);
|
|
19277
|
-
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`);
|
|
19278
19724
|
const stores = await listGraphStores(process.env, MODEL.id);
|
|
19279
19725
|
if (stores.length === 0) return;
|
|
19280
19726
|
console.log("");
|
|
@@ -19333,7 +19779,7 @@ async function publishCommand(args) {
|
|
|
19333
19779
|
const out = resolve(args.out.trim());
|
|
19334
19780
|
await writePublishFolders(configPath, withPublishFolder(await readPublishFolders(configPath), key, publication, out));
|
|
19335
19781
|
console.error(`etherpk-mcp: publish folder for "${publication}" of "${graphName}" set to ${out} (remembered in ${configPath}).`);
|
|
19336
|
-
} 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.`);
|
|
19337
19783
|
const result = await publish(graph, { id: publication }, host);
|
|
19338
19784
|
console.log(JSON.stringify(cliPublishOutput(result), null, 2));
|
|
19339
19785
|
if (!result.ok) process.exitCode = 1;
|
|
@@ -19406,7 +19852,7 @@ async function openFolderForServe(folder, args) {
|
|
|
19406
19852
|
onSemanticProgress: reportSemanticProgress,
|
|
19407
19853
|
onError: (error) => console.error(`etherpk-mcp: ${error.message}`),
|
|
19408
19854
|
onWarning: (line) => console.error(`etherpk-mcp: ${line}`),
|
|
19409
|
-
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.`))
|
|
19410
19856
|
}),
|
|
19411
19857
|
graphName: name
|
|
19412
19858
|
};
|
|
@@ -19478,7 +19924,7 @@ async function serveGraph(graph, graphName, args) {
|
|
|
19478
19924
|
transport.onclose = listener;
|
|
19479
19925
|
},
|
|
19480
19926
|
async shutdown(end) {
|
|
19481
|
-
console.error(`etherpk-mcp: ${end}
|
|
19927
|
+
console.error(`etherpk-mcp: ${end} - flushing and exiting.`);
|
|
19482
19928
|
await graph.settle().catch(() => {});
|
|
19483
19929
|
await graph.dispose().catch(() => {});
|
|
19484
19930
|
process.exit(0);
|
|
@@ -19488,17 +19934,17 @@ async function serveGraph(graph, graphName, args) {
|
|
|
19488
19934
|
console.error(`etherpk-mcp: serving "${graphName}" over stdio as "Agent on ${hostname()}".`);
|
|
19489
19935
|
chromiumStatus(process.env, CMD).then((chromium) => {
|
|
19490
19936
|
if (chromium.executable) return;
|
|
19491
|
-
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.`);
|
|
19492
19938
|
});
|
|
19493
19939
|
const startSemantic = () => graph.semantic().then(async (semantic) => {
|
|
19494
19940
|
const status = await semantic.status();
|
|
19495
|
-
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.`);
|
|
19496
19942
|
}).catch((error) => console.error(`etherpk-mcp: semantic search unavailable: ${error instanceof Error ? error.message : String(error)}`));
|
|
19497
19943
|
if (semanticOn) startSemantic();
|
|
19498
19944
|
else if (!args["no-semantic"]) {
|
|
19499
|
-
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.`);
|
|
19500
19946
|
whenSemanticSetUp(process.env, () => {
|
|
19501
|
-
console.error("etherpk-mcp: semantic setup found
|
|
19947
|
+
console.error("etherpk-mcp: semantic setup found - loading the model and building the store.");
|
|
19502
19948
|
startSemantic();
|
|
19503
19949
|
});
|
|
19504
19950
|
}
|