@appsoftwareltd/etherpk-mcp 0.8.3 → 0.8.5
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 +19 -16
- package/dist/main.js +789 -247
- package/dist/main.js.map +1 -1
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -34,7 +34,7 @@ import { request } from "node:https";
|
|
|
34
34
|
import { BlockList, isIP } from "node:net";
|
|
35
35
|
var package_default = {
|
|
36
36
|
name: "@appsoftwareltd/etherpk-mcp",
|
|
37
|
-
version: "0.8.
|
|
37
|
+
version: "0.8.5",
|
|
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";
|
|
@@ -2148,19 +2149,31 @@ function flatten(roots) {
|
|
|
2148
2149
|
roots.forEach((root, i) => walk(root, null, i));
|
|
2149
2150
|
return rows;
|
|
2150
2151
|
}
|
|
2151
|
-
/**
|
|
2152
|
-
|
|
2152
|
+
/**
|
|
2153
|
+
* A document's blocks and links, each link tagged with the block holding it, and which block
|
|
2154
|
+
* each source line belongs to (the deepest whose own range covers it). The one reading of a
|
|
2155
|
+
* document's outline that the index rows and {@link blockConceptChain} share.
|
|
2156
|
+
*/
|
|
2157
|
+
function blocksAndLinks(text) {
|
|
2153
2158
|
const blocks = flatten(parseBlocks(text));
|
|
2154
2159
|
const lineToBlock = /* @__PURE__ */ new Map();
|
|
2155
2160
|
for (const b of blocks) for (let line = b.startLine; line <= b.endLine; line++) lineToBlock.set(line, b.localId);
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2161
|
+
return {
|
|
2162
|
+
blocks,
|
|
2163
|
+
links: wikilinkOccurrencesInSource(text).map((occ) => ({
|
|
2164
|
+
concept: occ.concept,
|
|
2165
|
+
line: occ.line,
|
|
2166
|
+
lineText: occ.lineText,
|
|
2167
|
+
matchStart: occ.matchStart,
|
|
2168
|
+
matchEnd: occ.matchEnd,
|
|
2169
|
+
blockLocalId: lineToBlock.get(occ.line) ?? null
|
|
2170
|
+
})),
|
|
2171
|
+
lineToBlock
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
/** Derive the index rows for a single document's text. Pure. */
|
|
2175
|
+
function deriveDoc(text) {
|
|
2176
|
+
const { blocks, links } = blocksAndLinks(text);
|
|
2164
2177
|
const candidateTasks = blocks.filter((b) => b.kind === "task");
|
|
2165
2178
|
const fenced = candidateTasks.length === 0 ? () => false : fencedLines(text);
|
|
2166
2179
|
const taskBlocks = candidateTasks.filter((b) => !fenced(b.startLine));
|
|
@@ -2197,6 +2210,17 @@ function deriveDoc(text) {
|
|
|
2197
2210
|
*/
|
|
2198
2211
|
function deriveTaskConcepts(blocks, links, taskBlocks) {
|
|
2199
2212
|
if (taskBlocks.length === 0) return [];
|
|
2213
|
+
const byBlock = linksByBlock(links);
|
|
2214
|
+
if (byBlock.size === 0) return [];
|
|
2215
|
+
const rows = [];
|
|
2216
|
+
for (const task of taskBlocks) for (const { concept } of conceptChain(blocks, byBlock, task.localId)) rows.push({
|
|
2217
|
+
blockLocalId: task.localId,
|
|
2218
|
+
concept
|
|
2219
|
+
});
|
|
2220
|
+
return rows;
|
|
2221
|
+
}
|
|
2222
|
+
/** The concepts linked on each block, in the order they are written, by block. */
|
|
2223
|
+
function linksByBlock(links) {
|
|
2200
2224
|
const byBlock = /* @__PURE__ */ new Map();
|
|
2201
2225
|
for (const link of links) {
|
|
2202
2226
|
if (link.blockLocalId === null) continue;
|
|
@@ -2204,21 +2228,26 @@ function deriveTaskConcepts(blocks, links, taskBlocks) {
|
|
|
2204
2228
|
if (existing) existing.push(link.concept);
|
|
2205
2229
|
else byBlock.set(link.blockLocalId, [link.concept]);
|
|
2206
2230
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2231
|
+
return byBlock;
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* The walk under a [[Task Concept]] (ADR 0051): the concepts linked on block `from`, then on each
|
|
2235
|
+
* ancestor up to the root, each named once where it is nearest, whatever its case.
|
|
2236
|
+
*/
|
|
2237
|
+
function conceptChain(blocks, byBlock, from) {
|
|
2238
|
+
const out = [];
|
|
2239
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2240
|
+
let depth = 0;
|
|
2241
|
+
for (let id = from; id !== null; id = blocks[id]?.parentId ?? null, depth++) for (const concept of byBlock.get(id) ?? []) {
|
|
2242
|
+
const key = conceptKey$1(concept);
|
|
2243
|
+
if (seen.has(key)) continue;
|
|
2244
|
+
seen.add(key);
|
|
2245
|
+
out.push({
|
|
2246
|
+
concept,
|
|
2247
|
+
depth
|
|
2248
|
+
});
|
|
2220
2249
|
}
|
|
2221
|
-
return
|
|
2250
|
+
return out;
|
|
2222
2251
|
}
|
|
2223
2252
|
/**
|
|
2224
2253
|
* The references a document makes BY ITS NAME: every wikilink nested in the concept string, at
|
|
@@ -2243,6 +2272,14 @@ function deriveTitleLinks(concept) {
|
|
|
2243
2272
|
matchEnd: occ.matchEnd
|
|
2244
2273
|
}));
|
|
2245
2274
|
}
|
|
2275
|
+
/**
|
|
2276
|
+
* The [[Task Concept]]s every task in a document has, whatever its outline (ADR 0051): the
|
|
2277
|
+
* document's own concept, then the [[Scope]]s in its name, outermost first. The index writes these
|
|
2278
|
+
* for every task, and `/kanban` offers them after the block's own (ADR 0113).
|
|
2279
|
+
*/
|
|
2280
|
+
function documentTaskConcepts(concept) {
|
|
2281
|
+
return [concept, ...deriveTitleLinks(concept).map((link) => link.concept)];
|
|
2282
|
+
}
|
|
2246
2283
|
/** The ancestor labels of a block, root-first (the outline-chain breadcrumb). */
|
|
2247
2284
|
function ancestorChain(blocks, localId) {
|
|
2248
2285
|
const chain = [];
|
|
@@ -2742,7 +2779,7 @@ function createSchema(db) {
|
|
|
2742
2779
|
db.exec(SCHEMA$1);
|
|
2743
2780
|
db.run("INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('active_generation', 1)");
|
|
2744
2781
|
db.run("INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('revision', 0)");
|
|
2745
|
-
db.exec(`PRAGMA user_version =
|
|
2782
|
+
db.exec(`PRAGMA user_version = 15`);
|
|
2746
2783
|
}
|
|
2747
2784
|
function activeIndexGeneration(db) {
|
|
2748
2785
|
return db.all("SELECT value FROM index_metadata WHERE key = 'active_generation'")[0]?.value ?? 1;
|
|
@@ -2763,7 +2800,7 @@ function advanceIndexRevision(db) {
|
|
|
2763
2800
|
*/
|
|
2764
2801
|
function isUsableIndex(db) {
|
|
2765
2802
|
try {
|
|
2766
|
-
return db.all("PRAGMA user_version")[0]?.user_version ===
|
|
2803
|
+
return db.all("PRAGMA user_version")[0]?.user_version === 15;
|
|
2767
2804
|
} catch {
|
|
2768
2805
|
return false;
|
|
2769
2806
|
}
|
|
@@ -2940,13 +2977,17 @@ function insertProperties(db, pageId, doc) {
|
|
|
2940
2977
|
* the document's identity. Without that row, filtering to a project page returns nothing for
|
|
2941
2978
|
* the tasks written directly on it — the case the "Filter to <active tab>" shortcut exists for.
|
|
2942
2979
|
*
|
|
2943
|
-
*
|
|
2944
|
-
*
|
|
2945
|
-
*
|
|
2980
|
+
* The [[Scope]]s in the document's name sit on that root too (ADR 0051, amended 2026-09-29), so
|
|
2981
|
+
* the tasks on `[[Acme]] Website` answer to Acme. They come from the same scan that makes such a
|
|
2982
|
+
* page a Backlink of its scope (ADR 0083), so a page gives its tasks to exactly the concepts it
|
|
2983
|
+
* is a title reference of.
|
|
2984
|
+
*
|
|
2985
|
+
* Only the names the document is written with go in, never an alias of them: the query resolves
|
|
2986
|
+
* a filtered name to its page and matches on every name that page answers to, so an alias row
|
|
2987
|
+
* here would be redundant now and stale after a rename.
|
|
2946
2988
|
*/
|
|
2947
2989
|
function insertTaskConcepts(db, pageId, doc, tasks, taskConcepts) {
|
|
2948
2990
|
if (tasks.length === 0) return;
|
|
2949
|
-
const ownKey = conceptKey$1(doc.concept);
|
|
2950
2991
|
const written = /* @__PURE__ */ new Set();
|
|
2951
2992
|
const write = (blockLocalId, key) => {
|
|
2952
2993
|
const seen = `${blockLocalId} ${key}`;
|
|
@@ -2958,7 +2999,8 @@ function insertTaskConcepts(db, pageId, doc, tasks, taskConcepts) {
|
|
|
2958
2999
|
key
|
|
2959
3000
|
]);
|
|
2960
3001
|
};
|
|
2961
|
-
|
|
3002
|
+
const rootKeys = documentTaskConcepts(doc.concept).map(conceptKey$1);
|
|
3003
|
+
for (const t of tasks) for (const key of rootKeys) write(t.blockLocalId, key);
|
|
2962
3004
|
for (const tc of taskConcepts) write(tc.blockLocalId, conceptKey$1(tc.concept));
|
|
2963
3005
|
}
|
|
2964
3006
|
/**
|
|
@@ -3843,6 +3885,15 @@ var ORDER_SQL = {
|
|
|
3843
3885
|
due: `(t.due IS NULL), t.due ASC, (t.priority IS NULL), t.priority ASC`
|
|
3844
3886
|
};
|
|
3845
3887
|
/**
|
|
3888
|
+
* What decides a tie the grouping leaves: the document's name, then the task's line, which a
|
|
3889
|
+
* person can see and which survive a rebuild. A folder graph re-derives the whole graph with
|
|
3890
|
+
* fresh page ids on any file change, so ordering by page id let equal tasks swap places in the
|
|
3891
|
+
* [[Tasks View]] and on a [[Kanban Board]] (ADR 0113) between one refresh and the next.
|
|
3892
|
+
* `page_id, block_local_id` come last only to keep the order total, so paging cannot repeat or
|
|
3893
|
+
* skip.
|
|
3894
|
+
*/
|
|
3895
|
+
var TIE_BREAK_SQL = "p.concept_key ASC, t.line ASC, t.page_id ASC, t.block_local_id ASC";
|
|
3896
|
+
/**
|
|
3846
3897
|
* The WHERE clause and parameters shared by {@link tasksMatching} and its count, so the two
|
|
3847
3898
|
* can never drift into answering different questions about the same filter.
|
|
3848
3899
|
*
|
|
@@ -3911,7 +3962,7 @@ function tasksMatching(db, query, offset, limit) {
|
|
|
3911
3962
|
t.priority, t.waiting, t.doing, t.cancelled, t.due, t.completion, t.scheduled
|
|
3912
3963
|
FROM tasks t JOIN pages p ON p.id = t.page_id
|
|
3913
3964
|
WHERE ${predicate.where}
|
|
3914
|
-
ORDER BY ${ORDER_SQL[query.groupBy]},
|
|
3965
|
+
ORDER BY ${ORDER_SQL[query.groupBy]}, ${TIE_BREAK_SQL}
|
|
3915
3966
|
LIMIT ? OFFSET ?`, [
|
|
3916
3967
|
...predicate.params,
|
|
3917
3968
|
limit + 1,
|
|
@@ -3923,30 +3974,35 @@ function tasksMatching(db, query, offset, limit) {
|
|
|
3923
3974
|
hits: [],
|
|
3924
3975
|
hasMore: false
|
|
3925
3976
|
};
|
|
3926
|
-
const
|
|
3977
|
+
const ancestry = ancestryFor(db, page);
|
|
3927
3978
|
return {
|
|
3928
|
-
hits: page.map((row) =>
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3979
|
+
hits: page.map((row) => {
|
|
3980
|
+
const found = ancestry.get(row.page_id)?.get(row.block_local_id);
|
|
3981
|
+
return {
|
|
3982
|
+
...toHit(row),
|
|
3983
|
+
breadcrumb: found?.breadcrumb ?? [],
|
|
3984
|
+
parentTask: found?.parentTask ?? null
|
|
3985
|
+
};
|
|
3986
|
+
}),
|
|
3932
3987
|
hasMore
|
|
3933
3988
|
};
|
|
3934
3989
|
}
|
|
3935
3990
|
/**
|
|
3936
|
-
* Ancestor labels for a whole page of tasks
|
|
3937
|
-
* chains walked in JS. The same shape searchText uses, and for
|
|
3938
|
-
* SQL walk per row would be one query per task.
|
|
3991
|
+
* Ancestor labels for a whole page of tasks, and each task's nearest task ancestor: one read for
|
|
3992
|
+
* every document involved, then the chains walked in JS. The same shape searchText uses, and for
|
|
3993
|
+
* the same reason - a recursive SQL walk per row would be one query per task.
|
|
3939
3994
|
*/
|
|
3940
|
-
function
|
|
3995
|
+
function ancestryFor(db, rows) {
|
|
3941
3996
|
const ids = [...new Set(rows.map((row) => row.page_id))];
|
|
3942
3997
|
const placeholders = ids.map(() => "?").join(",");
|
|
3943
|
-
const blocks = db.all(`SELECT page_id, local_id, parent_local_id, label FROM blocks WHERE page_id IN (${placeholders})`, ids);
|
|
3998
|
+
const blocks = db.all(`SELECT page_id, local_id, parent_local_id, kind, label FROM blocks WHERE page_id IN (${placeholders})`, ids);
|
|
3944
3999
|
const byPage = /* @__PURE__ */ new Map();
|
|
3945
4000
|
for (const block of blocks) {
|
|
3946
4001
|
let page = byPage.get(block.page_id);
|
|
3947
4002
|
if (!page) byPage.set(block.page_id, page = /* @__PURE__ */ new Map());
|
|
3948
4003
|
page.set(block.local_id, {
|
|
3949
4004
|
parent_local_id: block.parent_local_id,
|
|
4005
|
+
kind: block.kind,
|
|
3950
4006
|
label: block.label
|
|
3951
4007
|
});
|
|
3952
4008
|
}
|
|
@@ -3956,10 +4012,24 @@ function breadcrumbsFor(db, rows) {
|
|
|
3956
4012
|
if (!page) continue;
|
|
3957
4013
|
let chains = out.get(row.page_id);
|
|
3958
4014
|
if (!chains) out.set(row.page_id, chains = /* @__PURE__ */ new Map());
|
|
3959
|
-
chains.set(row.block_local_id,
|
|
4015
|
+
chains.set(row.block_local_id, {
|
|
4016
|
+
breadcrumb: searchBreadcrumb(page, row.block_local_id),
|
|
4017
|
+
parentTask: nearestTask(page, row.block_local_id)
|
|
4018
|
+
});
|
|
3960
4019
|
}
|
|
3961
4020
|
return out;
|
|
3962
4021
|
}
|
|
4022
|
+
/** The label of the nearest ancestor of `localId` that is a task, or null. */
|
|
4023
|
+
function nearestTask(blocks, localId) {
|
|
4024
|
+
let parent = blocks.get(localId)?.parent_local_id ?? null;
|
|
4025
|
+
for (let guard = 0; parent !== null && guard <= blocks.size; guard++) {
|
|
4026
|
+
const block = blocks.get(parent);
|
|
4027
|
+
if (!block) break;
|
|
4028
|
+
if (block.kind === "task") return block.label;
|
|
4029
|
+
parent = block.parent_local_id;
|
|
4030
|
+
}
|
|
4031
|
+
return null;
|
|
4032
|
+
}
|
|
3963
4033
|
/** How many tasks match. Uncapped: this counts an indexed join, not a text index. */
|
|
3964
4034
|
function tasksMatchingCount(db, query) {
|
|
3965
4035
|
const predicate = taskPredicate(db, query);
|
|
@@ -4270,7 +4340,7 @@ function cacheRoot(env) {
|
|
|
4270
4340
|
* recovered from a joined path (see there).
|
|
4271
4341
|
*/
|
|
4272
4342
|
var CACHE_FILE_NAME = `local-cache.v${CACHE_DB_VERSION}.bin`;
|
|
4273
|
-
var INDEX_FILE_NAME = `index.
|
|
4343
|
+
var INDEX_FILE_NAME = `index.v15.sqlite`;
|
|
4274
4344
|
var VECTORS_FILE_NAME = `vectors.v1.sqlite`;
|
|
4275
4345
|
function cacheFile(dir) {
|
|
4276
4346
|
return join(dir, CACHE_FILE_NAME);
|
|
@@ -4299,7 +4369,7 @@ async function openCacheDb() {
|
|
|
4299
4369
|
const db = await request$1(indexedDB.open(CACHE_DB, CACHE_DB_VERSION));
|
|
4300
4370
|
for (const store of CACHE_STORES) if (!db.objectStoreNames.contains(store)) {
|
|
4301
4371
|
db.close();
|
|
4302
|
-
throw new Error(`Local Cache database has no "${store}" store
|
|
4372
|
+
throw new Error(`Local Cache database has no "${store}" store - open the graph cache before restoring it.`);
|
|
4303
4373
|
}
|
|
4304
4374
|
return db;
|
|
4305
4375
|
}
|
|
@@ -4587,7 +4657,7 @@ function describeGraphStore(store, now = Date.now()) {
|
|
|
4587
4657
|
if (total === 0) return `empty - no passages indexed here yet (snapshot ${age})`;
|
|
4588
4658
|
if (embedded >= total) return `up to date - ${count} embedded (snapshot ${age})`;
|
|
4589
4659
|
const percent = Math.floor(embedded / total * 100);
|
|
4590
|
-
return ageMs < 9e4 ? `building - ${count} (${percent}%) embedded, snapshot ${age}, refreshed every 30 s while it builds` : `paused - ${count} (${percent}%) embedded, last snapshot ${age}
|
|
4660
|
+
return ageMs < 9e4 ? `building - ${count} (${percent}%) embedded, snapshot ${age}, refreshed every 30 s while it builds` : `paused - ${count} (${percent}%) embedded, last snapshot ${age}. Nothing is building it now, it continues when serve next runs`;
|
|
4591
4661
|
}
|
|
4592
4662
|
/** Remove every persisted graph under the cache root (logout of the last server). */
|
|
4593
4663
|
async function removeCacheRoot(env) {
|
|
@@ -4784,7 +4854,7 @@ async function setupSemantic(io) {
|
|
|
4784
4854
|
const files = io.files ?? MODEL.files;
|
|
4785
4855
|
const status = await semanticSetupStatus(io.env, files);
|
|
4786
4856
|
if (!status.runtime) {
|
|
4787
|
-
io.say(`Installing ${RUNTIME.name} ${RUNTIME.version} into ${status.runtimeDir} (about 300 MB
|
|
4857
|
+
io.say(`Installing ${RUNTIME.name} ${RUNTIME.version} into ${status.runtimeDir} (about 300 MB - native, so it is not part of the etherpk-mcp package)…`);
|
|
4788
4858
|
await mkdir(status.runtimeDir, {
|
|
4789
4859
|
recursive: true,
|
|
4790
4860
|
mode: 448
|
|
@@ -4857,7 +4927,7 @@ async function download(fetchFn, file, path) {
|
|
|
4857
4927
|
const digest = hash.digest("hex");
|
|
4858
4928
|
if (digest !== file.sha256) {
|
|
4859
4929
|
await rm(temp, { force: true });
|
|
4860
|
-
throw new Error(`${file.name} downloaded with sha256 ${digest}, expected ${file.sha256}
|
|
4930
|
+
throw new Error(`${file.name} downloaded with sha256 ${digest}, expected ${file.sha256} - not kept.`);
|
|
4861
4931
|
}
|
|
4862
4932
|
await rename(temp, path);
|
|
4863
4933
|
}
|
|
@@ -4879,7 +4949,7 @@ var SemanticUnavailable = class extends Error {
|
|
|
4879
4949
|
this.name = "SemanticUnavailable";
|
|
4880
4950
|
}
|
|
4881
4951
|
};
|
|
4882
|
-
var SETUP_HINT = "run `npx @appsoftwareltd/etherpk-mcp semantic setup` on this computer once (it installs a ~300 MB runtime and a 23 MB model into the cache directory)
|
|
4952
|
+
var SETUP_HINT = "run `npx @appsoftwareltd/etherpk-mcp semantic setup` on this computer once (it installs a ~300 MB runtime and a 23 MB model into the cache directory) - the next semantic search will use it, no restart needed.";
|
|
4883
4953
|
/** The thread count `serve` uses: the option, else the environment, else the conservative default. */
|
|
4884
4954
|
function semanticThreads(env, requested) {
|
|
4885
4955
|
if (requested !== void 0) return Math.max(1, Math.floor(requested));
|
|
@@ -4900,7 +4970,7 @@ async function loadEmbeddingModel(env, options = {}) {
|
|
|
4900
4970
|
const loaded = await import(pathToFileURL(require.resolve(RUNTIME.name)).href);
|
|
4901
4971
|
ort = loaded.default?.InferenceSession ? loaded.default : loaded;
|
|
4902
4972
|
} catch (error) {
|
|
4903
|
-
throw new SemanticUnavailable(`The embedding runtime in ${status.runtimeDir} failed to load (${error instanceof Error ? error.message : String(error)})
|
|
4973
|
+
throw new SemanticUnavailable(`The embedding runtime in ${status.runtimeDir} failed to load (${error instanceof Error ? error.message : String(error)}): ${SETUP_HINT}`);
|
|
4904
4974
|
}
|
|
4905
4975
|
const tokenizer = new Tokenizer(JSON.parse(await readFile(join(status.modelDir, "tokenizer.json"), "utf8")), JSON.parse(await readFile(join(status.modelDir, "tokenizer_config.json"), "utf8")));
|
|
4906
4976
|
const threads = semanticThreads(env, options.threads);
|
|
@@ -6882,7 +6952,7 @@ function readPageDate(doc) {
|
|
|
6882
6952
|
return { issues: [{
|
|
6883
6953
|
level: "warning",
|
|
6884
6954
|
code: "invalid-date",
|
|
6885
|
-
message: `"${doc.concept}" has \`date: ${String(date)}\`, which is not a calendar day (YYYY-MM-DD)
|
|
6955
|
+
message: `"${doc.concept}" has \`date: ${String(date)}\`, which is not a calendar day (YYYY-MM-DD) - the document is undated.`,
|
|
6886
6956
|
concept: doc.concept
|
|
6887
6957
|
}] };
|
|
6888
6958
|
}
|
|
@@ -6911,7 +6981,7 @@ function readPublicationDefinition(doc) {
|
|
|
6911
6981
|
};
|
|
6912
6982
|
}
|
|
6913
6983
|
if (doc.kind === "journal") {
|
|
6914
|
-
issue("publication-on-journal", "A journal entry cannot define a publication
|
|
6984
|
+
issue("publication-on-journal", "A journal entry cannot define a publication - use a page.");
|
|
6915
6985
|
return {
|
|
6916
6986
|
publication: null,
|
|
6917
6987
|
issues
|
|
@@ -6944,7 +7014,7 @@ function readPublicationDefinition(doc) {
|
|
|
6944
7014
|
else issues.push({
|
|
6945
7015
|
level: "warning",
|
|
6946
7016
|
code: "publication-invalid-include",
|
|
6947
|
-
message: `Include \`${slot}\` must name the page that fills it
|
|
7017
|
+
message: `Include \`${slot}\` must name the page that fills it - it is ignored.`,
|
|
6948
7018
|
concept: doc.concept
|
|
6949
7019
|
});
|
|
6950
7020
|
}
|
|
@@ -7006,7 +7076,7 @@ function discoverPublications(documents) {
|
|
|
7006
7076
|
for (const publication of list) issues.push({
|
|
7007
7077
|
level: "error",
|
|
7008
7078
|
code: "publication-duplicate-id",
|
|
7009
|
-
message: `Two pages define the publication "${id}" (${list.map((p) => `"${p.concept}"`).join(" and ")})
|
|
7079
|
+
message: `Two pages define the publication "${id}" (${list.map((p) => `"${p.concept}"`).join(" and ")}) - neither is published until one is changed.`,
|
|
7010
7080
|
concept: publication.concept
|
|
7011
7081
|
});
|
|
7012
7082
|
}
|
|
@@ -7043,7 +7113,7 @@ function validateThemeManifest(raw) {
|
|
|
7043
7113
|
const version = typeof data.version === "string" && data.version.trim() !== "" ? data.version.trim() : "0.0.0";
|
|
7044
7114
|
const contract = typeof data.contract === "number" && Number.isInteger(data.contract) ? data.contract : null;
|
|
7045
7115
|
if (contract === null) errors.push("theme.json needs an integer `contract`, the view version the theme was written for.");
|
|
7046
|
-
else if (contract > 1) errors.push(`The theme was written for view contract ${contract}
|
|
7116
|
+
else if (contract > 1) errors.push(`The theme was written for view contract ${contract} - this EtherPK writes contract 1. Update EtherPK, or use an older theme.`);
|
|
7047
7117
|
const kinds = [];
|
|
7048
7118
|
if (Array.isArray(data.kinds)) {
|
|
7049
7119
|
for (const k of data.kinds) if (KINDS.has(k)) kinds.push(k);
|
|
@@ -10778,6 +10848,27 @@ function dayIsNotAPageName(day) {
|
|
|
10778
10848
|
return `“${day.trim()}” is a date, and a date is the name of that day's journal entry, so a page cannot be called that. Choose a different name.`;
|
|
10779
10849
|
}
|
|
10780
10850
|
//#endregion
|
|
10851
|
+
//#region ../client/src/lib/document/line-endings.ts
|
|
10852
|
+
/** `text` with every `\r\n` and every lone `\r` written as `\n`; `text` itself when it holds no `\r`. */
|
|
10853
|
+
function lineFeedsOnly(text) {
|
|
10854
|
+
return text.includes("\r") ? text.replace(/\r\n?/g, "\n") : text;
|
|
10855
|
+
}
|
|
10856
|
+
/**
|
|
10857
|
+
* The changes that turn `text` into {@link lineFeedsOnly}`(text)`, in document order and each
|
|
10858
|
+
* against `text` as it is: one per `\r`, deleting it before a `\n` and replacing it with `\n`
|
|
10859
|
+
* anywhere else. For a writer that must change a shared text in place rather than replace it,
|
|
10860
|
+
* applying them from the last to the first.
|
|
10861
|
+
*/
|
|
10862
|
+
function lineFeedChanges(text) {
|
|
10863
|
+
const changes = [];
|
|
10864
|
+
for (let at = text.indexOf("\r"); at !== -1; at = text.indexOf("\r", at + 1)) changes.push({
|
|
10865
|
+
from: at,
|
|
10866
|
+
to: at + 1,
|
|
10867
|
+
insert: text[at + 1] === "\n" ? "" : "\n"
|
|
10868
|
+
});
|
|
10869
|
+
return changes;
|
|
10870
|
+
}
|
|
10871
|
+
//#endregion
|
|
10781
10872
|
//#region ../client/src/lib/document/wikilink/rename.ts
|
|
10782
10873
|
/**
|
|
10783
10874
|
* Rewriting [[Wikilink]] targets across a document's source, for the rewrite arm of a page
|
|
@@ -11045,7 +11136,7 @@ var RenameUnconfirmedError = class extends Error {
|
|
|
11045
11136
|
name = "RenameUnconfirmedError";
|
|
11046
11137
|
constructor(concepts) {
|
|
11047
11138
|
const list = concepts.slice(0, 5).join(", ") + (concepts.length > 5 ? "…" : "");
|
|
11048
|
-
super(`${concepts.length === 1 ? "A document" : `${concepts.length} documents`} the rename would change ${concepts.length === 1 ? "has" : "have"} not finished syncing to this device: ${list}. Nothing was renamed
|
|
11139
|
+
super(`${concepts.length === 1 ? "A document" : `${concepts.length} documents`} the rename would change ${concepts.length === 1 ? "has" : "have"} not finished syncing to this device: ${list}. Nothing was renamed - try again once sync has caught up.`);
|
|
11049
11140
|
this.concepts = concepts;
|
|
11050
11141
|
}
|
|
11051
11142
|
};
|
|
@@ -11420,8 +11511,9 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11420
11511
|
}
|
|
11421
11512
|
return entries;
|
|
11422
11513
|
}
|
|
11423
|
-
|
|
11424
|
-
|
|
11514
|
+
/** Tell an open document's subscribers its text, all but `except` (the editor that made the edit). */
|
|
11515
|
+
function notify(doc, text, except) {
|
|
11516
|
+
for (const listener of doc.listeners) if (listener !== except) listener(text);
|
|
11425
11517
|
}
|
|
11426
11518
|
/** The frontmatter block of `text`, given its parsed body (`''` when there is none). */
|
|
11427
11519
|
function headOf(text, body) {
|
|
@@ -11659,7 +11751,7 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11659
11751
|
return doc.target;
|
|
11660
11752
|
},
|
|
11661
11753
|
getText: () => doc.buffer,
|
|
11662
|
-
applyChange(change, origin = "editor") {
|
|
11754
|
+
applyChange(change, origin = "editor", editor) {
|
|
11663
11755
|
doc.buffer = applyTextChange(doc.buffer, change);
|
|
11664
11756
|
doc.dirty = true;
|
|
11665
11757
|
if (doc.removed) {
|
|
@@ -11668,6 +11760,7 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11668
11760
|
}
|
|
11669
11761
|
doc.save.call();
|
|
11670
11762
|
if (origin === "external") notify(doc, doc.buffer);
|
|
11763
|
+
else if (editor) notify(doc, doc.buffer, editor);
|
|
11671
11764
|
},
|
|
11672
11765
|
subscribe(listener) {
|
|
11673
11766
|
doc.listeners.add(listener);
|
|
@@ -11675,12 +11768,12 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11675
11768
|
}
|
|
11676
11769
|
};
|
|
11677
11770
|
doc.ready = adapter.read(doc.subdir, doc.fileName).then((content) => {
|
|
11678
|
-
doc.buffer = content.text;
|
|
11771
|
+
doc.buffer = lineFeedsOnly(content.text);
|
|
11679
11772
|
doc.baseText = content.text;
|
|
11680
11773
|
doc.lastModified = content.lastModified;
|
|
11681
11774
|
doc.size = content.size;
|
|
11682
11775
|
doc.loaded = true;
|
|
11683
|
-
notify(doc,
|
|
11776
|
+
notify(doc, doc.buffer);
|
|
11684
11777
|
}).catch(async (error) => {
|
|
11685
11778
|
if (await adapter.exists(doc.subdir, doc.fileName).catch(() => true)) doc.loadError = error;
|
|
11686
11779
|
else doc.loaded = true;
|
|
@@ -11698,7 +11791,14 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11698
11791
|
return;
|
|
11699
11792
|
}
|
|
11700
11793
|
if (doc.loaded && entry.lastModified === doc.lastModified && entry.size === doc.size) return;
|
|
11701
|
-
|
|
11794
|
+
followDisk(doc, await adapter.read(doc.subdir, doc.fileName));
|
|
11795
|
+
}
|
|
11796
|
+
/**
|
|
11797
|
+
* Bring an open document in line with its file as just read (`reconcileDecision`): nothing
|
|
11798
|
+
* but the stamps when the file is what the buffer last synced with, a reload of a clean buffer,
|
|
11799
|
+
* a conflict over a dirty one.
|
|
11800
|
+
*/
|
|
11801
|
+
function followDisk(doc, { text: diskText, lastModified, size }) {
|
|
11702
11802
|
const decision = reconcileDecision({
|
|
11703
11803
|
dirty: doc.dirty,
|
|
11704
11804
|
baseText: doc.baseText,
|
|
@@ -11712,14 +11812,14 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11712
11812
|
return;
|
|
11713
11813
|
}
|
|
11714
11814
|
if (decision === "reload") {
|
|
11715
|
-
doc.buffer = diskText;
|
|
11815
|
+
doc.buffer = lineFeedsOnly(diskText);
|
|
11716
11816
|
doc.baseText = diskText;
|
|
11717
11817
|
doc.lastModified = lastModified;
|
|
11718
11818
|
doc.size = size;
|
|
11719
11819
|
doc.loaded = true;
|
|
11720
11820
|
doc.loadError = null;
|
|
11721
11821
|
doc.dirty = false;
|
|
11722
|
-
notify(doc,
|
|
11822
|
+
notify(doc, doc.buffer);
|
|
11723
11823
|
emitChange({ concept: conceptOf(doc) });
|
|
11724
11824
|
return;
|
|
11725
11825
|
}
|
|
@@ -11731,6 +11831,53 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11731
11831
|
doc.save.cancel();
|
|
11732
11832
|
onConflict?.(doc.conflict);
|
|
11733
11833
|
}
|
|
11834
|
+
/** A file's content, or null when it does not exist; any other failure rejects. */
|
|
11835
|
+
async function readIfPresent(subdir, fileName) {
|
|
11836
|
+
try {
|
|
11837
|
+
return await adapter.read(subdir, fileName);
|
|
11838
|
+
} catch (error) {
|
|
11839
|
+
if (await adapter.exists(subdir, fileName).catch(() => true)) throw error;
|
|
11840
|
+
return null;
|
|
11841
|
+
}
|
|
11842
|
+
}
|
|
11843
|
+
/**
|
|
11844
|
+
* A Formatting Scan's fix to an open document (ADR 0109): through the buffer, so the editor
|
|
11845
|
+
* showing it is told, then saved now. The file is read first: an edit made outside the app
|
|
11846
|
+
* since the buffer last synced is followed, a reload or a conflict over unsaved typing, and
|
|
11847
|
+
* the fix is refused rather than saved over it.
|
|
11848
|
+
*/
|
|
11849
|
+
async function spliceOpenDocument(doc, expected, splices) {
|
|
11850
|
+
await doc.ready;
|
|
11851
|
+
await settled(doc);
|
|
11852
|
+
if (doc.removed) return "gone";
|
|
11853
|
+
if (doc.conflict) return "changed";
|
|
11854
|
+
const disk = await readIfPresent(doc.subdir, doc.fileName);
|
|
11855
|
+
if (disk === null || doc.removed) return "gone";
|
|
11856
|
+
if (doc.saving || doc.conflict) return "changed";
|
|
11857
|
+
followDisk(doc, disk);
|
|
11858
|
+
if (doc.conflict || !doc.loaded) return "changed";
|
|
11859
|
+
if (containsCipherFence(doc.buffer)) return "protected";
|
|
11860
|
+
if (doc.buffer !== expected) return "changed";
|
|
11861
|
+
for (let i = splices.length - 1; i >= 0; i--) doc.handle.applyChange(splices[i], "external");
|
|
11862
|
+
await saveNow(doc);
|
|
11863
|
+
return "written";
|
|
11864
|
+
}
|
|
11865
|
+
/** A Formatting Scan's fix to a document nobody has open: read, compare, write (ADR 0109). */
|
|
11866
|
+
async function spliceClosedDocument(entry, expected, splices) {
|
|
11867
|
+
const read = await readIfPresent(entry.subdir, entry.fileName);
|
|
11868
|
+
if (read === null) return "gone";
|
|
11869
|
+
const opened = open.get(entry.key);
|
|
11870
|
+
if (opened) return spliceOpenDocument(opened, expected, splices);
|
|
11871
|
+
if (containsCipherFence(read.text)) return "protected";
|
|
11872
|
+
if (read.text !== expected) return "changed";
|
|
11873
|
+
let text = read.text;
|
|
11874
|
+
for (let i = splices.length - 1; i >= 0; i--) text = applyTextChange(text, splices[i]);
|
|
11875
|
+
const written = await adapter.write(entry.subdir, entry.fileName, text);
|
|
11876
|
+
entry.lastModified = written.lastModified;
|
|
11877
|
+
entry.size = written.size;
|
|
11878
|
+
emitChange({ concept: entry.concept });
|
|
11879
|
+
return "written";
|
|
11880
|
+
}
|
|
11734
11881
|
return {
|
|
11735
11882
|
open(target) {
|
|
11736
11883
|
return openDocNamed(target).handle;
|
|
@@ -11785,6 +11932,22 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11785
11932
|
emitRegistryOnly();
|
|
11786
11933
|
}
|
|
11787
11934
|
},
|
|
11935
|
+
async documentText(target) {
|
|
11936
|
+
const entry = registry.get(conceptKey(target));
|
|
11937
|
+
if (!entry) return null;
|
|
11938
|
+
const doc = open.get(entry.key);
|
|
11939
|
+
if (doc) {
|
|
11940
|
+
await doc.ready;
|
|
11941
|
+
if (doc.loaded) return doc.buffer;
|
|
11942
|
+
}
|
|
11943
|
+
return (await readIfPresent(entry.subdir, entry.fileName))?.text ?? null;
|
|
11944
|
+
},
|
|
11945
|
+
async spliceIfUnchanged(target, expected, splices) {
|
|
11946
|
+
const entry = registry.get(conceptKey(target));
|
|
11947
|
+
if (!entry) return "gone";
|
|
11948
|
+
const doc = open.get(entry.key);
|
|
11949
|
+
return doc ? spliceOpenDocument(doc, expected, splices) : spliceClosedDocument(entry, expected, splices);
|
|
11950
|
+
},
|
|
11788
11951
|
onChange(listener) {
|
|
11789
11952
|
changed.add(listener);
|
|
11790
11953
|
return () => changed.delete(listener);
|
|
@@ -11840,13 +12003,13 @@ function createFilesystemDocumentStore(adapter, options = {}) {
|
|
|
11840
12003
|
doc.loadError = null;
|
|
11841
12004
|
if (choice === "take-disk") {
|
|
11842
12005
|
const { text, lastModified, size } = await adapter.read(doc.subdir, doc.fileName);
|
|
11843
|
-
doc.buffer = text;
|
|
12006
|
+
doc.buffer = lineFeedsOnly(text);
|
|
11844
12007
|
doc.baseText = text;
|
|
11845
12008
|
doc.lastModified = lastModified;
|
|
11846
12009
|
doc.size = size;
|
|
11847
12010
|
doc.dirty = false;
|
|
11848
12011
|
doc.conflict = null;
|
|
11849
|
-
notify(doc,
|
|
12012
|
+
notify(doc, doc.buffer);
|
|
11850
12013
|
} else {
|
|
11851
12014
|
const res = await adapter.write(doc.subdir, doc.fileName, doc.buffer);
|
|
11852
12015
|
doc.baseText = doc.buffer;
|
|
@@ -12478,7 +12641,17 @@ function createServerDocumentStore(graph, options) {
|
|
|
12478
12641
|
},
|
|
12479
12642
|
getYText(target) {
|
|
12480
12643
|
const docId = docIdFor(target);
|
|
12481
|
-
|
|
12644
|
+
if (!docId) return void 0;
|
|
12645
|
+
const ytext = graph.docSync(docId).doc.getText("content");
|
|
12646
|
+
const changes = lineFeedChanges(ytext.toString());
|
|
12647
|
+
if (changes.length > 0) ytext.doc.transact(() => {
|
|
12648
|
+
for (let i = changes.length - 1; i >= 0; i--) {
|
|
12649
|
+
const change = changes[i];
|
|
12650
|
+
ytext.delete(change.from, change.to - change.from);
|
|
12651
|
+
if (change.insert) ytext.insert(change.from, change.insert);
|
|
12652
|
+
}
|
|
12653
|
+
}, STORE);
|
|
12654
|
+
return ytext;
|
|
12482
12655
|
},
|
|
12483
12656
|
async whenReady(target) {
|
|
12484
12657
|
const docId = docIdFor(target);
|
|
@@ -12780,6 +12953,28 @@ function createServerDocumentStore(graph, options) {
|
|
|
12780
12953
|
registry.set(docId, updated);
|
|
12781
12954
|
writeBackIdentity(docId);
|
|
12782
12955
|
},
|
|
12956
|
+
async spliceIfUnchanged(docId, expected, splices, spliceOptions) {
|
|
12957
|
+
if (!registry.has(docId)) return "gone";
|
|
12958
|
+
const materialised = await materialise([docId], spliceOptions?.timeoutMs ?? COLD_CONTENT_TIMEOUT_MS);
|
|
12959
|
+
try {
|
|
12960
|
+
if (!registry.has(docId)) return "gone";
|
|
12961
|
+
if (materialised.unconfirmed.length > 0) return "unconfirmed";
|
|
12962
|
+
const ytext = graph.docSync(docId).doc.getText("content");
|
|
12963
|
+
const text = ytext.toString();
|
|
12964
|
+
if (containsCipherFence(text)) return "protected";
|
|
12965
|
+
if (text !== expected) return "changed";
|
|
12966
|
+
ytext.doc.transact(() => {
|
|
12967
|
+
for (let i = splices.length - 1; i >= 0; i--) {
|
|
12968
|
+
const splice = splices[i];
|
|
12969
|
+
if (splice.to > splice.from) ytext.delete(splice.from, splice.to - splice.from);
|
|
12970
|
+
if (splice.insert) ytext.insert(splice.from, splice.insert);
|
|
12971
|
+
}
|
|
12972
|
+
}, STORE);
|
|
12973
|
+
return "written";
|
|
12974
|
+
} finally {
|
|
12975
|
+
materialised.release();
|
|
12976
|
+
}
|
|
12977
|
+
},
|
|
12783
12978
|
async reconcile() {},
|
|
12784
12979
|
async resolveConflict() {},
|
|
12785
12980
|
async createJournal(date, body = "") {
|
|
@@ -13506,6 +13701,7 @@ function assembleHeadlessGraph(parts) {
|
|
|
13506
13701
|
};
|
|
13507
13702
|
return {
|
|
13508
13703
|
graphId: parts.graphId,
|
|
13704
|
+
clientUrl: parts.clientUrl ?? null,
|
|
13509
13705
|
name: parts.name,
|
|
13510
13706
|
store: parts.store,
|
|
13511
13707
|
index,
|
|
@@ -13633,6 +13829,7 @@ async function openHeadlessGraph(deps) {
|
|
|
13633
13829
|
}
|
|
13634
13830
|
return assembleHeadlessGraph({
|
|
13635
13831
|
graphId: deps.graphId,
|
|
13832
|
+
clientUrl: deps.clientUrl ?? null,
|
|
13636
13833
|
name: sync.getMeta().name ?? deps.graphId,
|
|
13637
13834
|
accessLoss: () => sync.accessLoss(),
|
|
13638
13835
|
store: documents,
|
|
@@ -13790,7 +13987,7 @@ async function openHeadlessFolder(deps) {
|
|
|
13790
13987
|
for (const conflict of raised) {
|
|
13791
13988
|
await store.resolveConflict(conflict.target, "take-disk");
|
|
13792
13989
|
saveErrors.delete(conceptKey(conflict.target));
|
|
13793
|
-
warn(`"${conflict.target}" changed on disk while an edit to it could not be written
|
|
13990
|
+
warn(`"${conflict.target}" changed on disk while an edit to it could not be written - took the file on disk and discarded the edit.`);
|
|
13794
13991
|
}
|
|
13795
13992
|
})();
|
|
13796
13993
|
const pass = reconciling;
|
|
@@ -13969,7 +14166,7 @@ async function openHeadlessFolder(deps) {
|
|
|
13969
14166
|
/** The account has no vault yet, so there is nothing for a Recovery Code to open. */
|
|
13970
14167
|
var NoVaultError = class extends Error {
|
|
13971
14168
|
constructor() {
|
|
13972
|
-
super("This account has no encryption keys yet
|
|
14169
|
+
super("This account has no encryption keys yet - there is nothing for a Recovery Code to open.");
|
|
13973
14170
|
this.name = "NoVaultError";
|
|
13974
14171
|
}
|
|
13975
14172
|
};
|
|
@@ -13985,7 +14182,7 @@ async function openVaultWithRecoveryCode(api, code) {
|
|
|
13985
14182
|
try {
|
|
13986
14183
|
return (await openVault(fromBase64Url(stored.vault), wrapKey)).vaultKey;
|
|
13987
14184
|
} catch (error) {
|
|
13988
|
-
if (error instanceof EnvelopeError) throw new RecoveryCodeError("That Recovery Code does not open this account’s keys. Check it character by character
|
|
14185
|
+
if (error instanceof EnvelopeError) throw new RecoveryCodeError("That Recovery Code does not open this account’s keys. Check it character by character - only the most recently issued code works.");
|
|
13989
14186
|
throw error;
|
|
13990
14187
|
}
|
|
13991
14188
|
}
|
|
@@ -14114,7 +14311,7 @@ async function unlockByDeviceApproval(api, io) {
|
|
|
14114
14311
|
const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : "open EtherPK in a browser";
|
|
14115
14312
|
io.say("");
|
|
14116
14313
|
io.say(`To approve this device, ${where} (any page - it need not be a note), connected to this account`);
|
|
14117
|
-
io.say("with its keys unlocked. A prompt will show a code
|
|
14314
|
+
io.say("with its keys unlocked. A prompt will show a code - confirm it matches this one:");
|
|
14118
14315
|
io.say("");
|
|
14119
14316
|
io.say(` ${request.sas}`);
|
|
14120
14317
|
io.say("");
|
|
@@ -14337,7 +14534,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
14337
14534
|
"../etherpk-al-folio/assets/theme.js": "/*\n * The light / dark toggle: a `.theme-toggle` button flips the page between the two and the\n * choice is remembered in this browser; with no choice made the page follows the system. The\n * stylesheet keys on `data-theme` on <html>, and shell-top applies the remembered choice before\n * the first paint so a dark reader never sees a light flash.\n */\n(function () {\n 'use strict';\n var KEY = 'etherpk-theme';\n var root = document.documentElement;\n function isDark() {\n var chosen = root.getAttribute('data-theme');\n if (chosen === 'dark' || chosen === 'light') return chosen === 'dark';\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n function apply(theme) {\n if (theme === 'dark' || theme === 'light') root.setAttribute('data-theme', theme);\n else root.removeAttribute('data-theme');\n var buttons = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('aria-pressed', isDark() ? 'true' : 'false');\n }\n try { apply(localStorage.getItem(KEY)); } catch (_e) { apply(null); }\n var toggles = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < toggles.length; i++) {\n toggles[i].addEventListener('click', function () {\n var next = isDark() ? 'light' : 'dark';\n apply(next);\n try { localStorage.setItem(KEY, next); } catch (_e) { /* private mode: the choice lasts the page */ }\n });\n }\n})();\n",
|
|
14338
14535
|
"../etherpk-al-folio/layouts/404.html": "{{> shell-top}}\n <div class=\"post\">\n <header class=\"post-header\"><h1 class=\"post-title\">Page not found</h1></header>\n <div class=\"post-content\">\n <p>The page you followed a link to is not part of this site. <a href=\"index.html\">Back to {{site.title}}</a>.</p>\n </div>\n </div>\n{{> shell-bottom}}\n",
|
|
14339
14536
|
"../etherpk-al-folio/layouts/archive.html": "{{> shell-top}}\n <div class=\"post\">\n <header class=\"post-header\"><h1 class=\"post-title\">{{#page.isPostsArchive}}posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}journal{{/page.isJournalArchive}}</h1></header>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <h2 class=\"archive-year\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"post-list archive-list\">\n {{#posts}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a></li>{{/posts}}\n </ul>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <ul class=\"post-list\">\n {{#site.journals}}\n <li>\n <h3><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n <p class=\"post-meta\"><time datetime=\"{{date}}\">{{date}}</time></p>\n </li>\n {{/site.journals}}\n </ul>\n {{/page.isJournalArchive}}\n </div>\n{{> shell-bottom}}\n",
|
|
14340
|
-
"../etherpk-al-folio/layouts/home.html": "{{> shell-top}}\n <article class=\"post about\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{^page.isGeneratedIndex}}{{{page.titleHtml}}}{{/page.isGeneratedIndex}}{{#page.isGeneratedIndex}}{{site.title}}{{/page.isGeneratedIndex}}</h1>\n </header>\n {{^page.isGeneratedIndex}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}{{^site.isBlog}}\n <div class=\"post-content page-contents\">\n{{{page.content}}}\n </div>\n {{/site.isBlog}}{{/page.isGeneratedIndex}}\n {{#site.hasJournals}}\n <section class=\"news\">\n <h2><a href=\"journal.html\">news</a></h2>\n <table class=\"news-table\">\n {{#site.journals}}\n <tr>\n <th scope=\"row\"><time datetime=\"{{date}}\">{{date}}</time></th>\n <td><a href=\"{{url}}\">{{title}}</a>{{#excerpt}} <span class=\"news-excerpt\">{{excerpt}}</span>{{/excerpt}}</td>\n </tr>\n {{/site.journals}}\n </table>\n </section>\n {{/site.hasJournals}}\n {{#site.isBlog}}\n <section class=\"posts\">\n <h2>posts</h2>\n {{#site.hasPosts}}\n <ul class=\"post-list\">\n {{#site.recentPosts}}\n <li>\n <h3><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n <p class=\"post-meta\"><time datetime=\"{{date}}\">{{date}}</time></p>\n </li>\n {{/site.recentPosts}}\n </ul>\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day
|
|
14537
|
+
"../etherpk-al-folio/layouts/home.html": "{{> shell-top}}\n <article class=\"post about\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{^page.isGeneratedIndex}}{{{page.titleHtml}}}{{/page.isGeneratedIndex}}{{#page.isGeneratedIndex}}{{site.title}}{{/page.isGeneratedIndex}}</h1>\n </header>\n {{^page.isGeneratedIndex}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}{{^site.isBlog}}\n <div class=\"post-content page-contents\">\n{{{page.content}}}\n </div>\n {{/site.isBlog}}{{/page.isGeneratedIndex}}\n {{#site.hasJournals}}\n <section class=\"news\">\n <h2><a href=\"journal.html\">news</a></h2>\n <table class=\"news-table\">\n {{#site.journals}}\n <tr>\n <th scope=\"row\"><time datetime=\"{{date}}\">{{date}}</time></th>\n <td><a href=\"{{url}}\">{{title}}</a>{{#excerpt}} <span class=\"news-excerpt\">{{excerpt}}</span>{{/excerpt}}</td>\n </tr>\n {{/site.journals}}\n </table>\n </section>\n {{/site.hasJournals}}\n {{#site.isBlog}}\n <section class=\"posts\">\n <h2>posts</h2>\n {{#site.hasPosts}}\n <ul class=\"post-list\">\n {{#site.recentPosts}}\n <li>\n <h3><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n <p class=\"post-meta\"><time datetime=\"{{date}}\">{{date}}</time></p>\n </li>\n {{/site.recentPosts}}\n </ul>\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}\n </section>\n {{/site.isBlog}}\n </article>\n{{> shell-bottom}}\n",
|
|
14341
14538
|
"../etherpk-al-folio/layouts/journal.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <p class=\"post-meta\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}{{page.readingMinutes}} min read</p>\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"post-nav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"older\" href=\"{{url}}\">← {{title}}</a>{{/page.older}}\n {{#page.newer}}<a class=\"newer\" href=\"{{url}}\">{{title}} →</a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
|
|
14342
14539
|
"../etherpk-al-folio/layouts/page.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <p class=\"post-meta\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}{{page.readingMinutes}} min read</p>\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"post-nav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"older\" href=\"{{url}}\">← {{title}}</a>{{/page.older}}\n {{#page.newer}}<a class=\"newer\" href=\"{{url}}\">{{title}} →</a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
|
|
14343
14540
|
"../etherpk-al-folio/partials/after-content.html": "",
|
|
@@ -14358,7 +14555,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
14358
14555
|
"../etherpk-blog/assets/theme.css": "/* EtherPK blog theme. Tokens follow the app's own (agent blue, the gk surface scale); the font\n is the system stack so the site loads nothing from anywhere. Dark mode follows the reader's\n preference. Classes the publisher emits and this file styles: .wikilink, .wikilink-missing,\n .task, .task-tag, .toc, .backlinks, .site-nav-list, .diagram, .math, .tok-* (code). */\n\n:root {\n --font-sans: \"Inter\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n --agent-500: #007acc;\n --agent-600: #005f9e;\n --agent-100: #c2e3ff;\n --surface-0: #ffffff;\n --surface-1: #f9fafb;\n --surface-2: #e5e7eb;\n --text-strong: #030712;\n --text-default: #374151;\n --text-muted: #6b7280;\n --border-soft: rgba(3, 7, 18, 0.08);\n --border-strong: rgba(3, 7, 18, 0.16);\n --mark: #fff3a3;\n --missing: #b45309;\n --nav-width: 17rem;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --agent-500: #5ab3ff;\n --agent-600: #94ccff;\n --agent-100: #002e4f;\n --surface-0: #18181b;\n --surface-1: #1c1c20;\n --surface-2: #2a2a30;\n --text-strong: #fafafa;\n --text-default: #d4d4d8;\n --text-muted: #a1a1aa;\n --border-soft: rgba(255, 255, 255, 0.08);\n --border-strong: rgba(255, 255, 255, 0.16);\n --mark: #6b5a00;\n --missing: #f59e0b;\n }\n}\n\n*, *::before, *::after { box-sizing: border-box; }\nhtml { font-family: var(--font-sans); -webkit-font-smoothing: antialiased; }\nbody {\n margin: 0;\n color: var(--text-default);\n background: var(--surface-0);\n line-height: 1.6;\n font-size: 16px;\n}\na { color: var(--agent-500); }\na:hover { color: var(--agent-600); }\n.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.skip-link { position: absolute; left: -999px; top: 0; background: var(--surface-1); padding: 0.5rem 1rem; z-index: 100; }\n.skip-link:focus { left: 1rem; top: 1rem; }\n\n/* -- Header ----------------------------------------------------------------------------- */\n.site-header {\n display: flex;\n align-items: center;\n gap: 1rem;\n padding: 0.75rem 1.5rem;\n border-bottom: 1px solid var(--border-soft);\n background: var(--surface-1);\n position: sticky;\n top: 0;\n z-index: 20;\n}\n.site-title { display: inline-flex; align-items: center; gap: 0.5rem; min-width: 0; font-weight: 600; color: var(--text-strong); text-decoration: none; font-size: 1.05rem; }\n.site-title:hover { color: var(--agent-500); }\n.site-logo { flex-shrink: 0; }\n.site-title-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.site-search { position: relative; margin-left: auto; flex: 0 1 22rem; }\n.site-search input {\n width: 100%;\n padding: 0.45rem 0.75rem;\n border: 1px solid var(--border-strong);\n border-radius: 0.5rem;\n background: var(--surface-0);\n color: var(--text-default);\n font: inherit;\n}\n.site-search input:focus { outline: 2px solid var(--agent-500); outline-offset: 1px; }\n.site-search-results {\n position: absolute;\n top: calc(100% + 0.35rem);\n left: 0;\n right: 0;\n max-height: 24rem;\n overflow: auto;\n background: var(--surface-0);\n border: 1px solid var(--border-strong);\n border-radius: 0.5rem;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);\n z-index: 30;\n}\n.site-search-hit { display: block; padding: 0.55rem 0.75rem; text-decoration: none; color: var(--text-default); border-bottom: 1px solid var(--border-soft); }\n.site-search-hit:hover, .site-search-hit:focus { background: var(--surface-1); }\n.site-search-title { display: block; font-weight: 600; color: var(--text-strong); }\n.site-search-snippet { display: block; font-size: 0.9rem; color: var(--text-muted); }\n.site-search-empty { margin: 0; padding: 0.6rem 0.75rem; color: var(--text-muted); }\n\n/* -- Shell: top navigation, centred column --------------------------------------------- */\n.site-header { display: block; padding: 1rem 1.5rem 0; position: static; }\n.site-header-row { display: flex; align-items: center; gap: 1rem; max-width: 46rem; margin: 0 auto; }\n.site-title { font-size: 1.35rem; }\n.site-nav { max-width: 46rem; margin: 0.75rem auto 0; }\n.site-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; }\n.site-nav-list .site-nav-list { display: block; padding-left: 0.75rem; }\n.site-nav a, .site-nav .nav-group-label { color: var(--text-default); text-decoration: none; font-size: 0.95rem; }\n.site-nav a:hover { color: var(--text-strong); }\n.site-nav .nav-current > a { color: var(--agent-500); font-weight: 600; }\n.site-nav .nav-group-label { font-weight: 600; color: var(--text-strong); }\n.site-nav a .wikilink { color: inherit; text-decoration: none; }\n.nav-toggle, .nav-backdrop { display: none; }\n\n.content { padding: 2rem 1.5rem 4rem; max-width: 46rem; margin: 0 auto; }\n.post-header h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 0.5rem; color: var(--text-strong); }\n.post-header h1 .wikilink { color: inherit; text-decoration: none; }\n.post-header h1 .wikilink:hover { text-decoration: underline; }\n.post-date { color: var(--text-muted); font-size: 0.95rem; }\n.post-body h2 { margin-top: 2rem; font-size: 1.4rem; color: var(--text-strong); }\n.post-body h3 { margin-top: 1.5rem; font-size: 1.15rem; color: var(--text-strong); }\n.post-body ul { padding-left: 1.4rem; }\n.post-body li { margin: 0.2rem 0; }\n.post-body img { max-width: 100%; height: auto; border-radius: 0.35rem; }\n.post-body table { border-collapse: collapse; margin: 1rem 0; width: 100%; }\n.post-body th, .post-body td { border: 1px solid var(--border-strong); padding: 0.4rem 0.6rem; text-align: left; }\n.post-body th { background: var(--surface-1); }\n.post-body blockquote { border-left: 3px solid var(--border-strong); margin: 1rem 0; padding: 0.25rem 1rem; color: var(--text-muted); }\n.post-body code { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-1); padding: 0.1em 0.35em; border-radius: 0.25rem; }\n.post-body pre { background: var(--surface-1); border: 1px solid var(--border-soft); border-radius: 0.5rem; padding: 0.9rem 1rem; overflow-x: auto; }\n.post-body pre code { background: none; padding: 0; font-size: 0.88rem; }\nmark { background: var(--mark); color: inherit; padding: 0 0.15em; border-radius: 0.2em; }\n\n/* Wikilinks, as the editor colours them. A missing target is visibly missing. */\na.wikilink { text-decoration: none; border-bottom: 1px solid var(--agent-100); }\na.wikilink:hover { border-bottom-color: var(--agent-500); }\na.wikilink-missing { color: var(--missing); text-decoration: none; border-bottom: 1px dashed var(--missing); }\n\n/* Tasks and their badges. */\nli.task { list-style: none; margin-left: -1.4rem; padding-left: 0; }\nli.task input[type=\"checkbox\"] { margin-right: 0.35rem; vertical-align: -0.1em; }\nli.task.done { color: var(--text-muted); }\n.task-tag { display: inline-block; font-size: 0.8rem; font-weight: 600; padding: 0.05em 0.5em; border-radius: 999px; background: var(--surface-2); color: var(--text-strong); margin-right: 0.25rem; vertical-align: 0.1em; }\n.task-tag.priority-1 { background: #fee2e2; color: #991b1b; }\n.task-tag.priority-2 { background: #ffedd5; color: #9a3412; }\n.task-tag.priority-3 { background: #fef9c3; color: #854d0e; }\n.task-tag.waiting, .task-tag.doing { background: var(--agent-100); color: var(--agent-600); }\n.task-tag.cancelled { text-decoration: line-through; }\n\n/* Table of contents, diagrams, maths. */\n.toc { margin: 0 0 1.5rem; font-size: 0.95rem; }\n.toc summary { cursor: pointer; font-weight: 600; color: var(--text-strong); }\n.toc ul { list-style: none; padding-left: 0; margin: 0.4rem 0 0; }\n.toc li.toc-level-3 { padding-left: 1rem; }\n.toc li.toc-level-4 { padding-left: 2rem; }\n.diagram { margin: 1rem 0; text-align: center; }\n.diagram svg { max-width: 100%; height: auto; }\n.math-block { overflow-x: auto; margin: 1rem 0; }\n\n/* Linked references, beneath the content. */\n.backlinks { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border-soft); }\n.backlinks h2 { font-size: 1.1rem; color: var(--text-strong); margin: 0 0 0.75rem; }\n.backlink-list { list-style: none; padding: 0; margin: 0; }\n.backlink-source { margin: 0 0 0.9rem; }\n.backlink-source > a { font-weight: 600; }\n.backlink-source ul { list-style: none; padding-left: 1rem; margin: 0.25rem 0 0; border-left: 2px solid var(--border-soft); }\n.backlink-context { color: var(--text-muted); font-size: 0.95rem; margin: 0.15rem 0; }\n\n/* Archive and index lists. */\n.journal-archive, .page-index-list { list-style: none; padding: 0; }\n.journal-archive li { margin: 0 0 1rem; }\n.journal-archive time { color: var(--text-muted); margin-right: 0.5rem; font-variant-numeric: tabular-nums; }\n.archive-excerpt { margin: 0.2rem 0 0; color: var(--text-muted); }\n.archive-year { margin: 1.5rem 0 0.5rem; font-size: 1.1rem; }\n.all-posts { margin: 1.5rem 0 0; }\n\n.site-footer { border-top: 1px solid var(--border-soft); padding: 1rem 1.5rem; color: var(--text-muted); font-size: 0.9rem; text-align: center; }\n\n/* Code tokens (lezer classHighlighter names). */\n.tok-keyword { color: #0a7; }\n.tok-string, .tok-string2 { color: #690; }\n.tok-comment, .tok-lineComment, .tok-blockComment { color: #999; font-style: italic; }\n.tok-number, .tok-bool, .tok-null, .tok-atom { color: #905; }\n.tok-function, .tok-definition.tok-variableName { color: #c60; }\n.tok-typeName, .tok-className, .tok-namespace { color: #b58900; }\n.tok-operator, .tok-punctuation { color: #555; }\n.tok-propertyName, .tok-attributeName { color: #268bd2; }\n.tok-tagName { color: #22863a; }\n.tok-invalid { color: #d00; }\n@media (prefers-color-scheme: dark) {\n .tok-keyword { color: #4ec9b0; }\n .tok-string, .tok-string2 { color: #b5cea8; }\n .tok-comment, .tok-lineComment, .tok-blockComment { color: #7f848e; }\n .tok-number, .tok-bool, .tok-null, .tok-atom { color: #d19a66; }\n .tok-function, .tok-definition.tok-variableName { color: #dcdcaa; }\n .tok-typeName, .tok-className, .tok-namespace { color: #e5c07b; }\n .tok-operator, .tok-punctuation { color: #abb2bf; }\n .tok-propertyName, .tok-attributeName { color: #61afef; }\n .tok-tagName { color: #98c379; }\n}\n\n/* -- Posts ------------------------------------------------------------------------------ */\n.post-summary { margin: 0 0 2rem; }\n.post-summary h2 { margin: 0 0 0.2rem; font-size: 1.35rem; }\n.post-summary h2 a { color: var(--text-strong); text-decoration: none; }\n.post-summary h2 a:hover { color: var(--agent-500); }\n.post-summary time { color: var(--text-muted); font-size: 0.9rem; font-variant-numeric: tabular-nums; }\n.post-summary p { margin: 0.4rem 0 0; color: var(--text-default); }\n.home-intro { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-soft); }\n.post-list-empty { color: var(--text-muted); }\n@media (max-width: 40rem) {\n .site-header-row { flex-wrap: wrap; }\n .site-search { flex-basis: 100%; margin-left: 0; }\n .content { padding: 1.25rem 1rem 3rem; }\n}\n",
|
|
14359
14556
|
"../etherpk-blog/layouts/404.html": "{{> shell-top}}\n <article class=\"post not-published\">\n <header class=\"post-header\">\n <h1>Not published</h1>\n </header>\n <div class=\"post-body\">\n <p>The page you followed a link to is not part of this site.</p>\n <p><a href=\"index.html\">Back to {{site.title}}</a></p>\n </div>\n </article>\n{{> shell-bottom}}\n",
|
|
14360
14557
|
"../etherpk-blog/layouts/archive.html": "{{> shell-top}}\n <article class=\"post archive\">\n <header class=\"post-header\">\n <h1>{{#page.isPostsArchive}}Posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}Journal{{/page.isJournalArchive}}</h1>\n </header>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <h2 class=\"archive-year\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"journal-archive\">\n {{#posts}}\n <li>\n <time datetime=\"{{date}}\">{{date}}</time>\n <a href=\"{{url}}\">{{title}}</a>\n </li>\n {{/posts}}\n </ul>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <ul class=\"journal-archive\">\n {{#site.journals}}\n <li>\n <time datetime=\"{{date}}\">{{date}}</time>\n <a href=\"{{url}}\">{{title}}</a>\n {{#excerpt}}<p class=\"archive-excerpt\">{{excerpt}}</p>{{/excerpt}}\n </li>\n {{/site.journals}}\n </ul>\n {{/page.isJournalArchive}}\n </article>\n{{> shell-bottom}}\n",
|
|
14361
|
-
"../etherpk-blog/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <section class=\"home-intro\">\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </section>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"Posts\">\n {{#site.hasPosts}}\n {{#site.recentPosts}}\n <article class=\"post-summary\">\n <h2><a href=\"{{url}}\">{{title}}</a></h2>\n <time datetime=\"{{date}}\">{{date}}</time>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n </article>\n {{/site.recentPosts}}\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}\n <p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day
|
|
14558
|
+
"../etherpk-blog/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <section class=\"home-intro\">\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </section>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"Posts\">\n {{#site.hasPosts}}\n {{#site.recentPosts}}\n <article class=\"post-summary\">\n <h2><a href=\"{{url}}\">{{title}}</a></h2>\n <time datetime=\"{{date}}\">{{date}}</time>\n {{#excerpt}}<p>{{excerpt}}</p>{{/excerpt}}\n </article>\n {{/site.recentPosts}}\n <p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}\n {{^site.hasPosts}}\n <p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>\n {{/site.hasPosts}}\n </section>\n{{> shell-bottom}}\n",
|
|
14362
14559
|
"../etherpk-blog/layouts/journal.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1>{{{page.titleHtml}}}</h1>\n {{#page.hasDate}}<time class=\"post-date\" datetime=\"{{page.date}}\">{{page.date}}</time>{{/page.hasDate}}\n </header>\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </article>\n{{> shell-bottom}}\n",
|
|
14363
14560
|
"../etherpk-blog/layouts/page.html": "{{> shell-top}}\n <article class=\"post\">\n <header class=\"post-header\">\n <h1>{{{page.titleHtml}}}</h1>\n {{#page.hasDate}}<time class=\"post-date\" datetime=\"{{page.date}}\">{{page.date}}</time>{{/page.hasDate}}\n </header>\n <div class=\"post-body\">\n{{{page.content}}}\n </div>\n </article>\n{{> shell-bottom}}\n",
|
|
14364
14561
|
"../etherpk-blog/partials/after-content.html": "",
|
|
@@ -14386,7 +14583,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
14386
14583
|
"../etherpk-docs/partials/before-content.html": "",
|
|
14387
14584
|
"../etherpk-docs/partials/footer.html": " <footer class=\"site-footer\">\n <p>{{site.title}} · published with <a href=\"https://etherpk.com\" rel=\"noopener\">EtherPK</a></p>\n </footer>\n",
|
|
14388
14585
|
"../etherpk-docs/partials/head.html": "",
|
|
14389
|
-
"../etherpk-docs/partials/header.html": " <header class=\"site-header\">\n <a class=\"site-title\" href=\"index.html\">{{> logo}}<span class=\"site-title-text\">{{site.title}}</span></a>\n <form class=\"site-search\" role=\"search\" data-search-index=\"{{site.search.url}}\" data-search-script=\"{{site.search.scriptUrl}}\">\n <label class=\"visually-hidden\" for=\"site-search-input\">Search</label>\n <input id=\"site-search-input\" type=\"search\" name=\"q\" placeholder=\"Search\" autocomplete=\"off\" aria-describedby=\"site-search-hint\">\n <span id=\"site-search-hint\" class=\"visually-hidden\">Type to search the published pages
|
|
14586
|
+
"../etherpk-docs/partials/header.html": " <header class=\"site-header\">\n <a class=\"site-title\" href=\"index.html\">{{> logo}}<span class=\"site-title-text\">{{site.title}}</span></a>\n <form class=\"site-search\" role=\"search\" data-search-index=\"{{site.search.url}}\" data-search-script=\"{{site.search.scriptUrl}}\">\n <label class=\"visually-hidden\" for=\"site-search-input\">Search</label>\n <input id=\"site-search-input\" type=\"search\" name=\"q\" placeholder=\"Search\" autocomplete=\"off\" aria-describedby=\"site-search-hint\">\n <span id=\"site-search-hint\" class=\"visually-hidden\">Type to search the published pages - results appear below the box.</span>\n <div class=\"site-search-results\" role=\"listbox\" hidden></div>\n </form>\n </header>\n",
|
|
14390
14587
|
"../etherpk-docs/partials/logo.html": "<!-- The EtherPK icon (resources/brand/etherpk-icon-*.svg), inline so the page fetches nothing and it takes the text colour; aria-hidden because the title beside it is the name. Fill the \"logo\" include, or edit this partial, for your own. -->\n<svg class=\"site-logo\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 191.82 166.13\" width=\"24\" height=\"21\" aria-hidden=\"true\" focusable=\"false\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M67.09,132.92,47.93,166.13H9.59L0,149.52,86.33,0H105.5L19.17,149.52H38.34l19.17-33.2h76.8l9.59,16.6Zm48-83.12,9.65,16.71H143.9L115.08,16.6q-24,41.57-48,83.12H143.9q14.37,24.9,28.76,49.8h-96l-9.59,16.61H182.24l9.58-16.61-38.34-66.4H96C95.85,82.9,113.39,52.73,115.08,49.8Z\"/></svg>\n",
|
|
14391
14588
|
"../etherpk-docs/partials/nav-item.html": " <li class=\"{{#current}}nav-current {{/current}}{{#open}}nav-open {{/open}}{{^href}}nav-group{{/href}}\">\n {{#href}}<a href=\"{{href}}\"{{#current}} aria-current=\"page\"{{/current}}{{#external}} rel=\"noopener\"{{/external}}>{{label}}</a>{{/href}}\n {{^href}}<span class=\"nav-group-label\">{{{labelHtml}}}</span>{{/href}}\n {{#hasChildren}}<ul class=\"site-nav-list\">{{#children}}{{> nav-item}}{{/children}}</ul>{{/hasChildren}}\n </li>\n",
|
|
14392
14589
|
"../etherpk-docs/partials/nav.html": " <nav id=\"site-nav\" class=\"site-nav\" aria-label=\"Site\">\n <ul class=\"site-nav-list\">\n{{#nav}}{{> nav-item}}{{/nav}}\n </ul>\n </nav>\n",
|
|
@@ -14401,7 +14598,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
14401
14598
|
"../etherpk-papermod/assets/theme.js": "/*\n * The light / dark toggle: a `.theme-toggle` button flips the page between the two and the\n * choice is remembered in this browser; with no choice made the page follows the system. The\n * stylesheet keys on `data-theme` on <html>, and shell-top applies the remembered choice before\n * the first paint so a dark reader never sees a light flash.\n */\n(function () {\n 'use strict';\n var KEY = 'etherpk-theme';\n var root = document.documentElement;\n function isDark() {\n var chosen = root.getAttribute('data-theme');\n if (chosen === 'dark' || chosen === 'light') return chosen === 'dark';\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n function apply(theme) {\n if (theme === 'dark' || theme === 'light') root.setAttribute('data-theme', theme);\n else root.removeAttribute('data-theme');\n var buttons = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('aria-pressed', isDark() ? 'true' : 'false');\n }\n try { apply(localStorage.getItem(KEY)); } catch (_e) { apply(null); }\n var toggles = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < toggles.length; i++) {\n toggles[i].addEventListener('click', function () {\n var next = isDark() ? 'light' : 'dark';\n apply(next);\n try { localStorage.setItem(KEY, next); } catch (_e) { /* private mode: the choice lasts the page */ }\n });\n }\n})();\n",
|
|
14402
14599
|
"../etherpk-papermod/layouts/404.html": "{{> shell-top}}\n <div class=\"not-found\">\n <h1>404</h1>\n <p>This page is not published. <a href=\"index.html\">Back to {{site.title}}</a>.</p>\n </div>\n{{> shell-bottom}}\n",
|
|
14403
14600
|
"../etherpk-papermod/layouts/archive.html": "{{> shell-top}}\n <header class=\"page-header\"><h1>{{#page.isPostsArchive}}Posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}Journal{{/page.isJournalArchive}}</h1></header>\n <div class=\"archive-posts\">\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <div class=\"archive-year\">\n <h2 class=\"archive-year-header\" id=\"y{{year}}\">{{year}}</h2>\n {{#posts}}\n <div class=\"archive-entry\">\n <h3 class=\"archive-entry-title\"><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<div class=\"archive-entry-content\">{{excerpt}}</div>{{/excerpt}}\n <footer class=\"entry-footer\"><time datetime=\"{{date}}\">{{date}}</time></footer>\n </div>\n {{/posts}}\n </div>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n {{#site.journals}}\n <div class=\"archive-entry\">\n <h3 class=\"archive-entry-title\"><a href=\"{{url}}\">{{title}}</a></h3>\n {{#excerpt}}<div class=\"archive-entry-content\">{{excerpt}}</div>{{/excerpt}}\n <footer class=\"entry-footer\"><time datetime=\"{{date}}\">{{date}}</time></footer>\n </div>\n {{/site.journals}}\n {{/page.isJournalArchive}}\n </div>\n{{> shell-bottom}}\n",
|
|
14404
|
-
"../etherpk-papermod/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post-single home-info\">\n <header class=\"post-header\"><h1 class=\"post-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <header class=\"page-header\"><h1>{{site.title}}</h1></header>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day
|
|
14601
|
+
"../etherpk-papermod/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post-single home-info\">\n <header class=\"post-header\"><h1 class=\"post-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <header class=\"page-header\"><h1>{{site.title}}</h1></header>\n {{/page.isGeneratedIndex}}\n <section class=\"post-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<div class=\"post-content page-contents\">{{{page.content}}}</div>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </section>\n{{> shell-bottom}}\n",
|
|
14405
14602
|
"../etherpk-papermod/layouts/journal.html": "{{> shell-top}}\n <article class=\"post-single\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <footer class=\"post-footer\">\n <nav class=\"paginav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"prev\" href=\"{{url}}\"><span class=\"title\">« Prev</span><br><span>{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"next\" href=\"{{url}}\"><span class=\"title\">Next »</span><br><span>{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
|
|
14406
14603
|
"../etherpk-papermod/layouts/page.html": "{{> shell-top}}\n <article class=\"post-single\">\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n </header>\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <footer class=\"post-footer\">\n <nav class=\"paginav\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"prev\" href=\"{{url}}\"><span class=\"title\">« Prev</span><br><span>{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"next\" href=\"{{url}}\"><span class=\"title\">Next »</span><br><span>{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
|
|
14407
14604
|
"../etherpk-papermod/partials/after-content.html": "",
|
|
@@ -14425,7 +14622,7 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
14425
14622
|
"../etherpk-stack/assets/theme.js": "/*\n * The light / dark toggle: a `.theme-toggle` button flips the page between the two and the\n * choice is remembered in this browser; with no choice made the page follows the system. The\n * stylesheet keys on `data-theme` on <html>, and shell-top applies the remembered choice before\n * the first paint so a dark reader never sees a light flash.\n */\n(function () {\n 'use strict';\n var KEY = 'etherpk-theme';\n var root = document.documentElement;\n function isDark() {\n var chosen = root.getAttribute('data-theme');\n if (chosen === 'dark' || chosen === 'light') return chosen === 'dark';\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n function apply(theme) {\n if (theme === 'dark' || theme === 'light') root.setAttribute('data-theme', theme);\n else root.removeAttribute('data-theme');\n var buttons = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('aria-pressed', isDark() ? 'true' : 'false');\n }\n try { apply(localStorage.getItem(KEY)); } catch (_e) { apply(null); }\n var toggles = document.querySelectorAll('.theme-toggle');\n for (var i = 0; i < toggles.length; i++) {\n toggles[i].addEventListener('click', function () {\n var next = isDark() ? 'light' : 'dark';\n apply(next);\n try { localStorage.setItem(KEY, next); } catch (_e) { /* private mode: the choice lasts the page */ }\n });\n }\n})();\n",
|
|
14426
14623
|
"../etherpk-stack/layouts/404.html": "{{> shell-top}}\n <article class=\"article-page not-found\">\n <header class=\"article-header\"><h1 class=\"article-title\">Not published</h1></header>\n <div class=\"article-content\">\n <p>The page you followed a link to is not part of this site.</p>\n <p><a href=\"index.html\">Back to {{site.title}}</a></p>\n </div>\n </article>\n{{> shell-bottom}}\n",
|
|
14427
14624
|
"../etherpk-stack/layouts/archive.html": "{{> shell-top}}\n <header class=\"section-header\"><h1>{{#page.isPostsArchive}}Posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}Journal{{/page.isJournalArchive}}</h1></header>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <section class=\"archive-year\" aria-label=\"{{year}}\">\n <h2 class=\"archive-year-title\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"archive-list\">\n {{#posts}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a></li>{{/posts}}\n </ul>\n </section>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <section class=\"article-list\" aria-label=\"Journal\">\n {{#site.journals}}{{> article-card}}{{/site.journals}}\n </section>\n {{/page.isJournalArchive}}\n{{> shell-bottom}}\n",
|
|
14428
|
-
"../etherpk-stack/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"article-page home-intro\">\n <header class=\"article-header\"><h1 class=\"article-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n <section class=\"article-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> article-card}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"article-list-empty\">No dated documents are published yet. A journal entry is dated by its day
|
|
14625
|
+
"../etherpk-stack/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"article-page home-intro\">\n <header class=\"article-header\"><h1 class=\"article-title\">{{{page.titleHtml}}}</h1></header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n <section class=\"article-list\" aria-label=\"{{#site.isBlog}}Posts{{/site.isBlog}}{{^site.isBlog}}Pages{{/site.isBlog}}\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> article-card}}{{/site.recentPosts}}{{#site.hasPosts}}<p class=\"all-posts\"><a href=\"{{site.postsUrl}}\">All {{site.postCount}} posts →</a></p>{{/site.hasPosts}}{{^site.hasPosts}}<p class=\"article-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<article class=\"article-page page-contents\"><header class=\"article-header\"><h1 class=\"article-title\">{{site.title}}</h1></header><div class=\"article-content\">{{{page.content}}}</div></article>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </section>\n{{> shell-bottom}}\n",
|
|
14429
14626
|
"../etherpk-stack/layouts/journal.html": "{{> shell-top}}\n <article class=\"article-page\">\n <header class=\"article-header\">\n <span class=\"article-category\">{{page.kind}}</span>\n <h1 class=\"article-title\">{{{page.titleHtml}}}</h1>\n <div class=\"article-details\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}<span>{{page.readingMinutes}} minute read</span></div>\n </header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n <footer class=\"article-footer\">\n <nav class=\"article-pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"article-card pagination-card\" href=\"{{url}}\"><span class=\"pagination-label\">Previous</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"article-card pagination-card next\" href=\"{{url}}\"><span class=\"pagination-label\">Next</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
|
|
14430
14627
|
"../etherpk-stack/layouts/page.html": "{{> shell-top}}\n <article class=\"article-page\">\n <header class=\"article-header\">\n <span class=\"article-category\">{{page.kind}}</span>\n <h1 class=\"article-title\">{{{page.titleHtml}}}</h1>\n <div class=\"article-details\">{{#page.hasDate}}<time datetime=\"{{page.date}}\">{{page.date}}</time> · {{/page.hasDate}}<span>{{page.readingMinutes}} minute read</span></div>\n </header>\n <div class=\"article-content\">\n{{{page.content}}}\n </div>\n <footer class=\"article-footer\">\n <nav class=\"article-pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"article-card pagination-card\" href=\"{{url}}\"><span class=\"pagination-label\">Previous</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"article-card pagination-card next\" href=\"{{url}}\"><span class=\"pagination-label\">Next</span><span class=\"pagination-title\">{{title}}</span></a>{{/page.newer}}\n </nav>\n </footer>\n </article>\n{{> shell-bottom}}\n",
|
|
14431
14628
|
"../etherpk-stack/partials/after-content.html": "",
|
|
@@ -14443,12 +14640,12 @@ var files = /* @__PURE__ */ Object.assign({
|
|
|
14443
14640
|
"../etherpk-stack/partials/theme-scripts.html": " <script src=\"theme/search.js\" defer><\/script>\n <script src=\"theme/theme.js\" defer><\/script>\n",
|
|
14444
14641
|
"../etherpk-stack/partials/toc.html": " <nav class=\"toc\" aria-label=\"Table of contents\">\n <ul>\n {{#page.toc}}<li class=\"toc-level-{{level}}\"><a href=\"#{{id}}\">{{text}}</a></li>{{/page.toc}}\n </ul>\n </nav>\n",
|
|
14445
14642
|
"../etherpk-stack/partials/widgets.html": " <section class=\"widget search-widget\" aria-label=\"Search\">\n <form class=\"site-search\" role=\"search\" data-search-index=\"{{site.search.url}}\" data-search-script=\"{{site.search.scriptUrl}}\">\n <label class=\"visually-hidden\" for=\"site-search-input\">Search</label>\n <input id=\"site-search-input\" type=\"search\" name=\"q\" placeholder=\"Search\" autocomplete=\"off\">\n <div class=\"site-search-results\" role=\"listbox\" hidden></div>\n </form>\n </section>\n {{#page.hasToc}}\n <section class=\"widget toc-widget\">\n <h2 class=\"widget-title\">Table of contents</h2>\n{{> toc}}\n </section>\n {{/page.hasToc}}\n {{#site.hasPostsArchive}}\n <!-- Years with counts, as Stack's archives widget does: a list of every post would grow\n with the blog; the archive page carries them. -->\n <section class=\"widget archives\">\n <h2 class=\"widget-title\">Archives</h2>\n <ul class=\"widget-list\">\n {{#site.postsByYear}}<li><a href=\"{{site.postsUrl}}#y{{year}}\"><span>{{year}}</span><span class=\"widget-count\">{{count}}</span></a></li>{{/site.postsByYear}}\n </ul>\n <p class=\"widget-more\"><a href=\"{{site.postsUrl}}\">All posts</a></p>\n </section>\n {{/site.hasPostsArchive}}\n {{#site.hasJournals}}\n <section class=\"widget journal\">\n <h2 class=\"widget-title\">Journal</h2>\n <p class=\"widget-more\"><a href=\"journal.html\">Every entry, by day</a></p>\n </section>\n {{/site.hasJournals}}\n <!-- A blog's posts are in the archive above; only its other pages belong here. -->\n {{#site.isBlog}}{{#site.hasUndatedPages}}\n <section class=\"widget pages\">\n <h2 class=\"widget-title\">Pages</h2>\n <ul class=\"widget-list\">\n {{#site.undatedPages}}<li><a href=\"{{url}}\"><span>{{title}}</span></a></li>{{/site.undatedPages}}\n </ul>\n </section>\n {{/site.hasUndatedPages}}{{/site.isBlog}}\n {{^site.isBlog}}\n <section class=\"widget pages\">\n <h2 class=\"widget-title\">Pages</h2>\n <ul class=\"widget-list\">\n {{#site.pages}}<li><a href=\"{{url}}\"><span>{{title}}</span></a></li>{{/site.pages}}\n </ul>\n </section>\n {{/site.isBlog}}\n",
|
|
14446
|
-
"../etherpk-stack/theme.json": "{\n \"name\": \"etherpk-stack\",\n \"title\": \"EtherPK Stack\",\n \"version\": \"1.0.0\",\n \"contract\": 1,\n \"description\": \"In the mould of Hugo's Stack: a card-based blog with a left sidebar of site identity and menu, article cards in the middle, and widgets (search, contents, archive, pages) on the right.\",\n \"kinds\": [\"blog\", \"docs\"],\n \"includes\": [\n { \"name\": \"head\", \"description\": \"Extra markup inside <head>: analytics, fonts, meta tags. Raw HTML in the page body.\" },\n { \"name\": \"logo\", \"description\": \"The avatar above the site title in the sidebar: an image or inline SVG. Replaces the EtherPK icon.\" },\n { \"name\": \"header\", \"description\": \"The left sidebar's identity block (avatar, title, description). Replaces the theme's
|
|
14643
|
+
"../etherpk-stack/theme.json": "{\n \"name\": \"etherpk-stack\",\n \"title\": \"EtherPK Stack\",\n \"version\": \"1.0.0\",\n \"contract\": 1,\n \"description\": \"In the mould of Hugo's Stack: a card-based blog with a left sidebar of site identity and menu, article cards in the middle, and widgets (search, contents, archive, pages) on the right.\",\n \"kinds\": [\"blog\", \"docs\"],\n \"includes\": [\n { \"name\": \"head\", \"description\": \"Extra markup inside <head>: analytics, fonts, meta tags. Raw HTML in the page body.\" },\n { \"name\": \"logo\", \"description\": \"The avatar above the site title in the sidebar: an image or inline SVG. Replaces the EtherPK icon.\" },\n { \"name\": \"header\", \"description\": \"The left sidebar's identity block (avatar, title, description). Replaces the theme's - the menu stays.\" },\n { \"name\": \"before-content\", \"description\": \"Rendered above every page's content: a banner, a notice.\" },\n { \"name\": \"after-content\", \"description\": \"Rendered below every page's content, above the linked references: an author bio, a comments widget.\" },\n { \"name\": \"footer\", \"description\": \"The footer under the article column.\" },\n { \"name\": \"scripts\", \"description\": \"Scripts before </body>, added after the theme's own (search, the theme toggle): analytics, a widget. Raw HTML in the page body.\" },\n { \"name\": \"styles\", \"description\": \"CSS appended after the theme's stylesheet: the page's first css fence.\", \"kind\": \"css\" }\n ],\n \"files\": [\n \"layouts/page.html\", \"layouts/home.html\", \"layouts/journal.html\", \"layouts/archive.html\", \"layouts/404.html\",\n \"partials/shell-top.html\", \"partials/shell-bottom.html\", \"partials/head.html\", \"partials/logo.html\", \"partials/header.html\",\n \"partials/nav-item.html\", \"partials/toc.html\", \"partials/backlinks.html\", \"partials/article-card.html\", \"partials/widgets.html\",\n \"partials/before-content.html\", \"partials/after-content.html\", \"partials/footer.html\", \"partials/theme-scripts.html\", \"partials/scripts.html\",\n \"assets/theme.css\", \"assets/search.js\", \"assets/theme.js\"\n ]\n}\n",
|
|
14447
14644
|
"../etherpk-terminal/assets/search.js": "/*\n * Search for a Published Site: reads search.json (built over the publication's own documents\n * and nothing else) and scores documents by title and text matches. No dependencies, no\n * network beyond the one index file, and nothing runs until the search box is used. The same\n * script runs the sidebar toggle on small screens, so a page carries one script.\n */\n(function () {\n 'use strict';\n\n // -- Navigation toggle (small screens) -------------------------------------------------\n var toggle = document.querySelector('.nav-toggle');\n var backdrop = document.querySelector('.nav-backdrop');\n var nav = document.getElementById('site-nav');\n function setNavOpen(open) {\n var was = document.body.classList.contains('nav-open');\n document.body.classList.toggle('nav-open', open);\n if (toggle) toggle.setAttribute('aria-expanded', open ? 'true' : 'false');\n if (backdrop) backdrop.hidden = !open;\n // Focus follows the drawer: into its first link on open, back to the button on close,\n // so a keyboard or screen-reader user is never left on something now hidden.\n if (open && !was) {\n var first = nav.querySelector('a');\n if (first) first.focus();\n } else if (!open && was && toggle) {\n toggle.focus();\n }\n }\n if (toggle && nav) {\n toggle.addEventListener('click', function () { setNavOpen(!document.body.classList.contains('nav-open')); });\n if (backdrop) backdrop.addEventListener('click', function () { setNavOpen(false); });\n document.addEventListener('keydown', function (e) {\n if (e.key === 'Escape' && document.body.classList.contains('nav-open')) setNavOpen(false);\n });\n }\n\n // -- Search --------------------------------------------------------------------------\n var form = document.querySelector('.site-search');\n if (!form) return;\n var input = form.querySelector('input[type=\"search\"]');\n var results = form.querySelector('.site-search-results');\n var indexUrl = form.getAttribute('data-search-index') || 'search.json';\n var scriptUrl = form.getAttribute('data-search-script') || 'search-index.js';\n var index = null;\n var loading = null;\n /* True once neither route produced the index: the results then say so rather than \"nothing matches\". */\n var unavailable = false;\n\n /* The index as a script assigning window.etherpkSearchIndex: what a site opened from disk can\n load, since file:// refuses fetch but allows <script src>. */\n function loadScript() {\n return new Promise(function (resolve, reject) {\n if (window.etherpkSearchIndex) { resolve(window.etherpkSearchIndex); return; }\n var s = document.createElement('script');\n s.src = scriptUrl;\n s.onload = function () {\n if (window.etherpkSearchIndex) resolve(window.etherpkSearchIndex);\n else reject(new Error('no index'));\n };\n s.onerror = function () { reject(new Error('no script')); };\n document.head.appendChild(s);\n });\n }\n\n function load() {\n if (index) return Promise.resolve(index);\n if (!loading) {\n loading = fetch(indexUrl).then(function (r) {\n if (!r.ok) throw new Error(String(r.status));\n return r.json();\n }).catch(loadScript).then(function (data) {\n index = (data && data.documents) || [];\n return index;\n }).catch(function () { unavailable = true; index = []; return index; });\n }\n return loading;\n }\n\n function tokens(q) {\n return q.toLowerCase().split(/\\s+/).filter(function (t) { return t.length > 0; });\n }\n\n function score(doc, terms) {\n var title = (doc.title || '').toLowerCase();\n var text = (doc.text || '').toLowerCase();\n var s = 0;\n for (var i = 0; i < terms.length; i++) {\n var t = terms[i];\n if (title === t) s += 20;\n else if (title.indexOf(t) === 0) s += 10;\n else if (title.indexOf(t) !== -1) s += 6;\n var at = text.indexOf(t);\n if (at !== -1) s += 2;\n else if (title.indexOf(t) === -1) return 0; // every term must match somewhere\n }\n return s;\n }\n\n function snippet(doc, terms) {\n var text = doc.text || '';\n var lower = text.toLowerCase();\n var at = -1;\n for (var i = 0; i < terms.length && at === -1; i++) at = lower.indexOf(terms[i]);\n if (at === -1) return doc.excerpt || '';\n var start = Math.max(0, at - 60);\n var end = Math.min(text.length, at + 100);\n return (start > 0 ? '…' : '') + text.slice(start, end).replace(/\\s+/g, ' ') + (end < text.length ? '…' : '');\n }\n\n function escapeHtml(s) {\n return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n }\n\n function render(list, q) {\n if (!q) { results.hidden = true; results.innerHTML = ''; return; }\n if (unavailable) {\n results.innerHTML = '<p class=\"site-search-empty\">The search index could not be loaded.</p>';\n results.hidden = false;\n return;\n }\n if (list.length === 0) {\n results.innerHTML = '<p class=\"site-search-empty\">Nothing matches \"' + escapeHtml(q) + '\".</p>';\n results.hidden = false;\n return;\n }\n results.innerHTML = list.map(function (hit) {\n return '<a class=\"site-search-hit\" role=\"option\" href=\"' + escapeHtml(hit.doc.url) + '\">' +\n '<span class=\"site-search-title\">' + escapeHtml(hit.doc.title) + '</span>' +\n '<span class=\"site-search-snippet\">' + escapeHtml(hit.snippet) + '</span></a>';\n }).join('');\n results.hidden = false;\n }\n\n var timer = null;\n function run() {\n var q = input.value.trim();\n var terms = tokens(q);\n if (terms.length === 0) { render([], ''); return; }\n load().then(function (docs) {\n var hits = [];\n for (var i = 0; i < docs.length; i++) {\n var s = score(docs[i], terms);\n if (s > 0) hits.push({ doc: docs[i], score: s, snippet: snippet(docs[i], terms) });\n }\n hits.sort(function (a, b) { return b.score - a.score || a.doc.title.localeCompare(b.doc.title); });\n render(hits.slice(0, 12), q);\n });\n }\n\n input.addEventListener('input', function () {\n clearTimeout(timer);\n timer = setTimeout(run, 120);\n });\n input.addEventListener('focus', function () { load(); });\n form.addEventListener('submit', function (e) {\n e.preventDefault();\n var first = results.querySelector('a');\n if (first) window.location.href = first.getAttribute('href');\n });\n document.addEventListener('keydown', function (e) {\n if (e.key === '/' && document.activeElement !== input && !/input|textarea/i.test(document.activeElement.tagName)) {\n e.preventDefault();\n input.focus();\n }\n if (e.key === 'Escape' && !results.hidden) { render([], ''); }\n });\n document.addEventListener('click', function (e) {\n if (!form.contains(e.target)) render([], '');\n });\n})();\n",
|
|
14448
14645
|
"../etherpk-terminal/assets/theme.css": "/* EtherPK's Terminal: after Hugo's Terminal (panr, MIT), written afresh for EtherPK's view - one\n monospace face, a dark ground, no colour at all - the accent is a brighter shade of the text,\n so everything is drawn in tones of one ink - the block logo with its blinking cursor,\n underlined titles, framed boxes and bordered buttons. Uses Fira Code\n or the system monospace, so the site loads nothing from anywhere. Classes the publisher emits\n and this file styles: .wikilink, .wikilink-missing, .task, .task-tag, .toc, .backlinks,\n .site-nav-list, .diagram, .math, .tok-* (code). */\n\n:root {\n --accent: #f2f2f2;\n --background: #1d1e28;\n --color: #cfcfd6;\n --color-muted: #8f8f9a;\n --border-color: rgba(255, 255, 255, 0.1);\n --code-bg: rgba(255, 255, 255, 0.06);\n --font: \"Fira Code\", ui-monospace, Monaco, Consolas, \"Ubuntu Mono\", monospace;\n --container-width: 1080px;\n --mark: rgba(255, 255, 255, 0.22);\n --missing: var(--color-muted);\n}\n/* Terminal is dark by nature; a light system preference is respected, the ink inverted. */\n@media (prefers-color-scheme: light) {\n :root:not([data-theme=\"dark\"]) {\n --accent: #111111;\n --background: #f7f4ef;\n --color: #2b2b2b;\n --color-muted: #6a6a6a;\n --mark: rgba(0, 0, 0, 0.14);\n --border-color: rgba(0, 0, 0, 0.12);\n --code-bg: rgba(0, 0, 0, 0.05);\n }\n}\n\n*, *::before, *::after { box-sizing: border-box; }\nhtml { color-scheme: dark light; }\nbody {\n margin: 0;\n font-family: var(--font);\n font-size: 0.9375rem;\n line-height: 1.6;\n background: var(--background);\n color: var(--color);\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n font-feature-settings: \"liga\", \"tnum\", \"zero\", \"ss01\", \"locl\";\n}\na { color: var(--accent); }\na:hover { text-decoration: none; }\nimg { max-width: 100%; height: auto; display: block; }\n.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.skip-link { position: absolute; left: -999px; top: 0; background: var(--accent); color: var(--background); padding: 8px 16px; z-index: 100; }\n.skip-link:focus { left: 16px; top: 16px; }\n\n.container { max-width: var(--container-width); margin: 0 auto; padding: 40px; }\n\n/* -- Header: the prompt-style logo ------------------------------------------------------ */\n.header { margin-bottom: 40px; }\n.header__inner { display: flex; align-items: center; justify-content: space-between; gap: 20px; }\n/* The block logo: the mark, the title and a blinking cursor on one line, the mark's box sized\n to the text's cap height so the two read as one glyph run. */\n.logo {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n padding: 8px 16px;\n background: var(--accent);\n color: var(--background);\n font-size: 1.375rem;\n line-height: 1;\n font-weight: bold;\n text-decoration: none;\n}\n.logo:hover { text-decoration: none; }\n.logo__mark { display: inline-flex; align-items: center; line-height: 0; }\n.logo__mark .site-logo { display: block; width: 1.3em; height: 1.125em; }\n.logo__cursor {\n display: inline-block;\n width: 0.5em;\n height: 1em;\n background: var(--background);\n animation: cursor 1s infinite;\n}\n@keyframes cursor { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } }\n@media (prefers-reduced-motion: reduce) { .logo__cursor { animation: none; } }\n.menu { margin-top: 20px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 30px; }\n.menu__inner { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 30px; }\n.menu__inner li a, .menu__inner .nav-group-label { color: var(--color); text-decoration: none; }\n.menu__inner li a:hover { color: var(--accent); }\n.menu__inner .nav-current > a { color: var(--accent); }\n.menu__inner .nav-current > a::after { content: \" *\"; }\n.menu__inner .nav-group-label { color: var(--color-muted); }\n.menu__inner .nav-group-label::after { content: \":\"; }\n/* A nested outline level follows its parent on the same line, the way a path reads. */\n.menu__inner li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 12px; }\n.menu__inner .site-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0 12px; }\n.menu__inner .site-nav-list li::before { content: \"/\"; color: var(--color-muted); margin-right: 12px; }\n.nav-toggle { display: none; }\n.nav-backdrop { display: none; }\n.site-search { position: relative; margin-left: auto; flex: 0 1 16rem; }\n.site-search input {\n width: 100%;\n padding: 5px 10px;\n border: 1px solid var(--accent);\n background: transparent;\n color: var(--color);\n font: inherit;\n}\n.site-search input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }\n.site-search input::placeholder { color: var(--color-muted); }\n.site-search-results {\n position: absolute;\n right: 0;\n top: calc(100% + 6px);\n width: min(26rem, 90vw);\n max-height: 24rem;\n overflow: auto;\n background: var(--background);\n border: 1px solid var(--accent);\n z-index: 30;\n}\n.site-search-hit { display: block; padding: 8px 12px; color: var(--color); text-decoration: none; border-bottom: 1px solid var(--border-color); }\n.site-search-hit:hover, .site-search-hit:focus { background: var(--code-bg); }\n.site-search-title { display: block; color: var(--accent); }\n.site-search-snippet { display: block; color: var(--color-muted); font-size: 0.9rem; }\n.site-search-empty { margin: 0; padding: 8px 12px; color: var(--color-muted); }\n\n/* -- Posts ------------------------------------------------------------------------------ */\n.posts { width: 100%; }\n.post { width: 100%; }\n.post.on-list { margin-bottom: 40px; padding-bottom: 40px; border-bottom: 1px dotted var(--accent); }\n.post.on-list:last-child { border-bottom: 0; }\n.post-title {\n position: relative;\n margin: 0 0 15px;\n font-size: 1.6rem;\n line-height: 1.3;\n color: var(--accent);\n text-decoration: underline;\n text-decoration-color: var(--accent);\n text-decoration-thickness: 2px;\n text-underline-offset: 6px;\n}\n.post-title a { color: inherit; text-decoration: none; }\n.post-title .wikilink { color: inherit; text-decoration: none; }\n.index-title { text-decoration: none; }\n.index-title::before { content: \"# \"; }\n.post-meta { margin: 0 0 20px; font-size: 0.9rem; color: var(--color-muted); }\n.post-content { margin-top: 20px; }\n.post-content h1, .post-content h2, .post-content h3, .post-content h4 { font-weight: bold; line-height: 1.3; margin: 40px 0 20px; }\n.post-content h2 { font-size: 1.3rem; }\n.post-content h3 { font-size: 1.2rem; }\n.post-content h2::before { content: \"## \"; color: var(--accent); }\n.post-content h3::before { content: \"### \"; color: var(--accent); }\n.post-content p { margin: 0 0 20px; }\n.post-content ul, .post-content ol { margin: 0 0 20px; padding-left: 24px; }\n.post-content li { margin: 4px 0; }\n.post-content ul li::marker { color: var(--accent); }\n.post-content img { margin: 20px 0; border: 1px solid var(--border-color); }\n.post-content .diagram { margin: 20px 0; text-align: center; }\n.post-content .diagram svg { max-width: 100%; height: auto; }\n.post-content blockquote {\n position: relative;\n margin: 40px 0;\n padding: 25px;\n border-top: 1px solid var(--accent);\n border-bottom: 1px solid var(--accent);\n font-style: italic;\n}\n.post-content blockquote::before { content: \"”\"; position: absolute; top: -40px; left: 10px; font-size: 3.875rem; font-style: normal; color: var(--accent); }\n.post-content blockquote p:last-child { margin-bottom: 0; }\n.post-content hr { border: 0; border-top: 1px dashed var(--accent); margin: 40px 0; }\n.post-content table { width: 100%; border-collapse: collapse; margin: 20px 0; }\n.post-content th, .post-content td { padding: 8px 12px; border: 1px dashed var(--accent); text-align: left; }\n.post-content th { color: var(--accent); }\n.post-content code { padding: 1px 6px; background: var(--code-bg); font-family: inherit; font-size: 0.95em; }\n.post-content pre {\n margin: 20px 0;\n padding: 20px;\n background: var(--code-bg);\n border-left: 3px solid var(--accent);\n overflow-x: auto;\n font-family: inherit;\n font-size: 0.95rem;\n line-height: 1.5;\n}\n.post-content pre code { padding: 0; background: none; font-size: inherit; }\n.math-block { overflow-x: auto; margin: 20px 0; }\nmark { background: var(--mark); color: inherit; padding: 0 0.15em; }\n\n/* Wikilinks and tasks. */\na.wikilink { color: var(--accent); }\na.wikilink-missing { color: var(--missing); text-decoration-style: dotted; }\nli.task { list-style: none; margin-left: -24px; }\nli.task input[type=\"checkbox\"] { accent-color: var(--accent); margin-right: 6px; vertical-align: -0.1em; }\nli.task.done { color: var(--color-muted); text-decoration: line-through; }\n.task-tag { display: inline-block; padding: 0 6px; margin-right: 4px; border: 1px solid var(--accent); color: var(--accent); font-size: 0.8rem; }\n.task-tag.cancelled { text-decoration: line-through; }\n\n/* Buttons, frames, the table of contents and pagination. */\n.button {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 8px 18px;\n border: 1px solid var(--accent);\n color: var(--accent);\n font: inherit;\n text-decoration: none;\n background: transparent;\n cursor: pointer;\n}\n.button:hover { background: var(--accent); color: var(--background); }\n.button.inline { display: inline-flex; }\n.read-more { margin-top: 10px; }\n.all-posts { margin: 30px 0 0; }\n/* The contents page of a docs site: the outline as a tree, then any page it leaves out. */\n.page-index-list { list-style: none; margin: 0; padding: 0; }\n.page-index-list .page-index-list { padding-left: 24px; }\n.page-index-list li { margin: 6px 0; }\n.page-index-list li::before { content: \"- \"; color: var(--accent); }\n.page-index-list a { color: var(--color); }\n.page-index-list a:hover { color: var(--accent); }\n.page-index-group-label { color: var(--color-muted); }\n.page-index-group-label::after { content: \":\"; }\n.page-index-other { margin: 30px 0 10px; font-size: 1.1rem; }\n.page-index-other::before { content: \"## \"; color: var(--accent); }\n/* The archives: a year as a prompt-style heading, then one line per post. */\n.archive-year { margin: 30px 0 10px; font-size: 1.1rem; }\n.archive-year::before { content: \"## \"; color: var(--accent); }\n.archive-list { list-style: none; margin: 0; padding: 0; }\n.archive-list li { margin: 6px 0; }\n.archive-list time { color: var(--color-muted); margin-right: 12px; font-variant-numeric: tabular-nums; }\n.archive-list a { color: var(--color); }\n.archive-list a:hover { color: var(--accent); }\n.archive-excerpt { color: var(--color-muted); }\n.framed { border: 1px solid var(--accent); padding: 20px; }\n.toc { margin: 20px 0 40px; }\n.toc__title { margin: 0 0 10px; color: var(--accent); }\n.toc ul { list-style: none; margin: 0; padding: 0; }\n.toc li { margin: 4px 0; }\n.toc li::before { content: \"- \"; color: var(--accent); }\n.toc li.toc-level-3 { padding-left: 1.5rem; }\n.toc li.toc-level-4 { padding-left: 3rem; }\n.toc a { color: var(--color); text-decoration: none; }\n.toc a:hover { color: var(--accent); }\n.pagination { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; margin-top: 50px; }\n.pagination__next { margin-left: auto; }\n.button__text { max-width: 20rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n\n/* Linked references, framed like everything else worth a box. */\n.backlinks { margin-top: 40px; border: 1px solid var(--accent); padding: 20px; }\n.backlinks h2 { margin: 0 0 12px; font-size: 1rem; color: var(--accent); }\n.backlinks h2::before { content: \":: \"; }\n.backlink-list { list-style: none; margin: 0; padding: 0; }\n.backlink-source { margin: 0 0 12px; }\n.backlink-source > a { color: var(--color); }\n.backlink-source ul { list-style: none; margin: 4px 0 0; padding: 0 0 0 16px; border-left: 1px dashed var(--accent); }\n.backlink-context { color: var(--color-muted); font-size: 0.9rem; margin: 2px 0; }\n\n/* -- Footer ----------------------------------------------------------------------------- */\n.footer { margin-top: 60px; padding-top: 20px; border-top: 1px dotted var(--accent); font-size: 0.9rem; color: var(--color-muted); }\n.footer__inner { display: flex; flex-wrap: wrap; gap: 10px; }\n.copyright { display: flex; flex-wrap: wrap; gap: 0 8px; }\n.footer__separator { color: var(--accent); }\n\n/* Code tokens (lezer classHighlighter names). */\n/* Code tokens (lezer classHighlighter names), in tones of the one ink: weight and italics carry\n the distinction the usual colours would. */\n.tok-keyword { color: var(--accent); font-weight: bold; }\n.tok-string, .tok-string2 { color: var(--color); font-style: italic; }\n.tok-comment, .tok-lineComment, .tok-blockComment { color: var(--color-muted); font-style: italic; }\n.tok-number, .tok-bool, .tok-null, .tok-atom { color: var(--accent); }\n.tok-function, .tok-definition.tok-variableName { color: var(--accent); }\n.tok-typeName, .tok-className, .tok-namespace { color: var(--accent); font-weight: bold; }\n.tok-operator, .tok-punctuation { color: var(--color-muted); }\n.tok-propertyName, .tok-attributeName { color: var(--color); }\n.tok-tagName { color: var(--accent); }\n.tok-invalid { color: var(--missing); text-decoration: underline wavy; }\n\n/* -- Small screens: the menu behind a Terminal-style button ----------------------------- */\n@media (max-width: 684px) {\n .container { padding: 20px; }\n .nav-toggle {\n display: inline-block;\n padding: 5px 10px;\n border: 1px solid var(--accent);\n background: transparent;\n color: var(--accent);\n font: inherit;\n cursor: pointer;\n }\n .nav-toggle::before { content: \"$ \"; }\n .menu { display: none; flex-direction: column; align-items: stretch; gap: 12px; margin-top: 20px; padding: 20px; border: 1px solid var(--accent); }\n body.nav-open .menu { display: flex; }\n .menu__inner { flex-direction: column; gap: 8px; }\n .menu__inner li { flex-direction: column; align-items: flex-start; }\n .menu__inner .site-nav-list { flex-direction: column; padding-left: 12px; }\n .menu__inner .site-nav-list li::before { margin-right: 6px; }\n .site-search { margin-left: 0; flex-basis: auto; }\n .post-title { font-size: 1.3rem; }\n .pagination__prev, .pagination__next { flex-basis: 100%; }\n .pagination__next { justify-content: flex-end; }\n}\n",
|
|
14449
14646
|
"../etherpk-terminal/layouts/404.html": "{{> shell-top}}\n <article class=\"post\">\n <h1 class=\"post-title\">404 :: not published</h1>\n <div class=\"post-content\">\n <p>$ the page you followed a link to is not part of this site.</p>\n <p><a class=\"button inline\" href=\"index.html\">← {{site.title}}</a></p>\n </div>\n </article>\n{{> shell-bottom}}\n",
|
|
14450
14647
|
"../etherpk-terminal/layouts/archive.html": "{{> shell-top}}\n <article class=\"post archive\">\n <h1 class=\"post-title index-title\">{{#page.isPostsArchive}}posts{{/page.isPostsArchive}}{{#page.isJournalArchive}}journal{{/page.isJournalArchive}}</h1>\n {{#page.isPostsArchive}}\n {{#site.postsByYear}}\n <h2 class=\"archive-year\" id=\"y{{year}}\">{{year}}</h2>\n <ul class=\"archive-list\">\n {{#posts}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a></li>{{/posts}}\n </ul>\n {{/site.postsByYear}}\n {{/page.isPostsArchive}}\n {{#page.isJournalArchive}}\n <ul class=\"archive-list\">\n {{#site.journals}}<li><time datetime=\"{{date}}\">{{date}}</time> <a href=\"{{url}}\">{{title}}</a>{{#excerpt}} <span class=\"archive-excerpt\">{{excerpt}}</span>{{/excerpt}}</li>{{/site.journals}}\n </ul>\n {{/page.isJournalArchive}}\n </article>\n{{> shell-bottom}}\n",
|
|
14451
|
-
"../etherpk-terminal/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post home-info\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <h1 class=\"post-title index-title\">{{site.title}}</h1>\n {{/page.isGeneratedIndex}}\n <div class=\"posts\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day
|
|
14648
|
+
"../etherpk-terminal/layouts/home.html": "{{> shell-top}}\n {{^page.isGeneratedIndex}}\n <article class=\"post home-info\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n </article>\n {{/page.isGeneratedIndex}}\n {{#page.isGeneratedIndex}}\n <h1 class=\"post-title index-title\">{{site.title}}</h1>\n {{/page.isGeneratedIndex}}\n <div class=\"posts\">\n {{#site.isBlog}}{{#site.recentPosts}}{{> post-entry}}{{/site.recentPosts}}{{^site.hasPosts}}<p class=\"post-list-empty\">No dated documents are published yet. A journal entry is dated by its day, a page by a <code>date:</code> line in its frontmatter.</p>{{/site.hasPosts}}{{/site.isBlog}}\n {{^site.isBlog}}{{#page.isGeneratedIndex}}<div class=\"post-content\">{{{page.content}}}</div>{{/page.isGeneratedIndex}}{{/site.isBlog}}\n </div>\n {{#site.isBlog}}{{#site.hasPosts}}\n <p class=\"all-posts\"><a class=\"button inline\" href=\"{{site.postsUrl}}\">$ ls all {{site.postCount}} posts →</a></p>\n {{/site.hasPosts}}{{/site.isBlog}}\n{{> shell-bottom}}\n",
|
|
14452
14649
|
"../etherpk-terminal/layouts/journal.html": "{{> shell-top}}\n <article class=\"post\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"button pagination__prev\" href=\"{{url}}\"><span class=\"button__icon\">←</span><span class=\"button__text\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"button pagination__next\" href=\"{{url}}\"><span class=\"button__text\">{{title}}</span><span class=\"button__icon\">→</span></a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
|
|
14453
14650
|
"../etherpk-terminal/layouts/page.html": "{{> shell-top}}\n <article class=\"post\">\n <h1 class=\"post-title\">{{{page.titleHtml}}}</h1>\n{{> post-meta}}\n{{> toc}}\n <div class=\"post-content\">\n{{{page.content}}}\n </div>\n <nav class=\"pagination\" aria-label=\"Neighbouring posts\">\n {{#page.older}}<a class=\"button pagination__prev\" href=\"{{url}}\"><span class=\"button__icon\">←</span><span class=\"button__text\">{{title}}</span></a>{{/page.older}}\n {{#page.newer}}<a class=\"button pagination__next\" href=\"{{url}}\"><span class=\"button__text\">{{title}}</span><span class=\"button__icon\">→</span></a>{{/page.newer}}\n </nav>\n </article>\n{{> shell-bottom}}\n",
|
|
14454
14651
|
"../etherpk-terminal/partials/after-content.html": "",
|
|
@@ -14567,7 +14764,7 @@ function seededFiles(publication) {
|
|
|
14567
14764
|
return new Map([
|
|
14568
14765
|
[PAGES_WORKFLOW_PATH, `# Publishes this folder to GitHub Pages as it is: the site is already built by EtherPK,
|
|
14569
14766
|
# so there is no build step. In the repository's Settings → Pages, set the source to
|
|
14570
|
-
# "GitHub Actions" once
|
|
14767
|
+
# "GitHub Actions" once - every push to main then deploys.
|
|
14571
14768
|
name: Deploy to GitHub Pages
|
|
14572
14769
|
on:
|
|
14573
14770
|
push:
|
|
@@ -14598,7 +14795,7 @@ jobs:
|
|
|
14598
14795
|
[WRANGLER_PATH, `// Cloudflare Workers with static assets (Cloudflare's successor to Pages for new sites).
|
|
14599
14796
|
// Deploy with \`npx wrangler deploy\`, or connect the repository in the dashboard under
|
|
14600
14797
|
// Workers → Create → Import a repository and leave the build command empty. The Worker is
|
|
14601
|
-
// named after the publication
|
|
14798
|
+
// named after the publication - Cloudflare warns when this name and the Worker's differ.
|
|
14602
14799
|
{
|
|
14603
14800
|
"name": "${publication.id}",
|
|
14604
14801
|
"compatibility_date": "2026-01-01",
|
|
@@ -14616,9 +14813,9 @@ This folder is a website published by [EtherPK](https://etherpk.com) from the pu
|
|
|
14616
14813
|
publish, so edit the notes, the publication page, its include pages and its theme in EtherPK,
|
|
14617
14814
|
not here.
|
|
14618
14815
|
|
|
14619
|
-
- Preview: open \`index.html\` in a browser
|
|
14816
|
+
- Preview: open \`index.html\` in a browser - search works from disk too.
|
|
14620
14817
|
- GitHub Pages: push this folder to a repository and set Settings → Pages → Source to
|
|
14621
|
-
"GitHub Actions"
|
|
14818
|
+
"GitHub Actions" - \`.github/workflows/pages.yaml\` deploys every push to \`main\`.
|
|
14622
14819
|
- Cloudflare: \`npx wrangler deploy\` uses \`wrangler.jsonc\`, or connect the repository in the
|
|
14623
14820
|
Workers dashboard with no build command.
|
|
14624
14821
|
- Netlify, GitLab Pages, Vercel, S3 or your own server: the folder is plain files, so any of
|
|
@@ -14626,7 +14823,7 @@ not here.
|
|
|
14626
14823
|
`],
|
|
14627
14824
|
[GITIGNORE_PATH, `.DS_Store\nThumbs.db\n`],
|
|
14628
14825
|
[ASSETS_IGNORE_PATH, `# Files in this folder that are not part of the website, kept out of the Worker's assets.
|
|
14629
|
-
# Written once by EtherPK
|
|
14826
|
+
# Written once by EtherPK - yours to edit.
|
|
14630
14827
|
.git
|
|
14631
14828
|
.github
|
|
14632
14829
|
.gitignore
|
|
@@ -14650,15 +14847,15 @@ This folder is a website generated by [EtherPK](https://etherpk.com) from the pu
|
|
|
14650
14847
|
\`robots.txt\` and the text outside this section of \`AGENTS.md\` is rewritten on each publish
|
|
14651
14848
|
and files that are no longer produced are deleted.
|
|
14652
14849
|
|
|
14653
|
-
- Do not edit the HTML, the assets or \`theme/\` here
|
|
14850
|
+
- Do not edit the HTML, the assets or \`theme/\` here - the change is lost on the next publish.
|
|
14654
14851
|
- Content changes are made to the documents in EtherPK. A document is published when its
|
|
14655
14852
|
frontmatter says \`public: true\` and names this publication in \`publications: [${publication.id}]\`
|
|
14656
14853
|
(unless the publication takes every public document).
|
|
14657
|
-
- The navigation is the outline of the publication page in EtherPK
|
|
14854
|
+
- The navigation is the outline of the publication page in EtherPK - the theme and the include
|
|
14658
14855
|
slots (head, logo, header, footer, before-content, after-content, scripts, styles) are chosen
|
|
14659
14856
|
and edited there too, in the Publish settings and the Theme editor.
|
|
14660
14857
|
- \`index.html\` opens from disk for a preview. \`.github/workflows/pages.yaml\` deploys the folder
|
|
14661
|
-
to GitHub Pages
|
|
14858
|
+
to GitHub Pages, \`wrangler.jsonc\` deploys it to Cloudflare Workers, and any static host serves it
|
|
14662
14859
|
as it is (https://docs.etherpk.com/hosting-a-published-site).
|
|
14663
14860
|
|
|
14664
14861
|
This section is rewritten by EtherPK on every publish. Put your own notes above or below it.
|
|
@@ -15544,7 +15741,7 @@ function buildNav(outline, resolver) {
|
|
|
15544
15741
|
issues.push({
|
|
15545
15742
|
level: "warning",
|
|
15546
15743
|
code: "nav-entry-not-published",
|
|
15547
|
-
message: `The navigation names "${whole.concept}", which is not in this publication
|
|
15744
|
+
message: `The navigation names "${whole.concept}", which is not in this publication - the entry is left out.`
|
|
15548
15745
|
});
|
|
15549
15746
|
continue;
|
|
15550
15747
|
}
|
|
@@ -15565,7 +15762,7 @@ function buildNav(outline, resolver) {
|
|
|
15565
15762
|
issues.push({
|
|
15566
15763
|
level: "warning",
|
|
15567
15764
|
code: "nav-link-unsafe",
|
|
15568
|
-
message: `The navigation links "${label}" to a target a published page does not carry (only http, https, mailto and site paths)
|
|
15765
|
+
message: `The navigation links "${label}" to a target a published page does not carry (only http, https, mailto and site paths) - the entry is shown without its link.`
|
|
15569
15766
|
});
|
|
15570
15767
|
out.push({
|
|
15571
15768
|
label,
|
|
@@ -15802,7 +15999,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15802
15999
|
if (homeKey !== null && !homeDoc) warnings.push({
|
|
15803
16000
|
level: "warning",
|
|
15804
16001
|
code: "home-not-published",
|
|
15805
|
-
message: `The home page "${publication.home}" is not in this publication
|
|
16002
|
+
message: `The home page "${publication.home}" is not in this publication - the front page lists the published pages instead.`
|
|
15806
16003
|
});
|
|
15807
16004
|
const allocation = allocateSlugs.withReport(included.filter((d) => d !== homeDoc));
|
|
15808
16005
|
const slugs = new Map(allocation.slugs);
|
|
@@ -15811,13 +16008,13 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15811
16008
|
for (const c of allocation.collisions) if (c.explicit) warnings.push({
|
|
15812
16009
|
level: "warning",
|
|
15813
16010
|
code: "slug-taken",
|
|
15814
|
-
message: `"${c.concept}" asks for the address ${c.wanted}.html in its frontmatter, but another document has it
|
|
16011
|
+
message: `"${c.concept}" asks for the address ${c.wanted}.html in its frontmatter, but another document has it - it is at ${c.slug}.html.`,
|
|
15815
16012
|
concept: c.concept
|
|
15816
16013
|
});
|
|
15817
16014
|
else info.push({
|
|
15818
16015
|
level: "info",
|
|
15819
16016
|
code: "slug-collision",
|
|
15820
|
-
message: `"${c.concept}" wanted the address ${c.wanted}.html, which another document has
|
|
16017
|
+
message: `"${c.concept}" wanted the address ${c.wanted}.html, which another document has - it is at ${c.slug}.html.`,
|
|
15821
16018
|
concept: c.concept
|
|
15822
16019
|
});
|
|
15823
16020
|
for (const ignoredSlug of allocation.ignored) {
|
|
@@ -15825,7 +16022,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15825
16022
|
warnings.push({
|
|
15826
16023
|
level: "warning",
|
|
15827
16024
|
code: "slug-ignored",
|
|
15828
|
-
message: `"${ignoredSlug.concept}" has \`slug: ${ignoredSlug.value}\`, which ${why}
|
|
16025
|
+
message: `"${ignoredSlug.concept}" has \`slug: ${ignoredSlug.value}\`, which ${why} - its address is derived from its name instead.`,
|
|
15829
16026
|
concept: ignoredSlug.concept
|
|
15830
16027
|
});
|
|
15831
16028
|
}
|
|
@@ -15875,12 +16072,12 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15875
16072
|
if (theme.manifest.contract < 1) warnings.push({
|
|
15876
16073
|
level: "warning",
|
|
15877
16074
|
code: "theme-older-contract",
|
|
15878
|
-
message: `The theme "${theme.manifest.name}" was written for view contract ${theme.manifest.contract}
|
|
16075
|
+
message: `The theme "${theme.manifest.name}" was written for view contract ${theme.manifest.contract} - this EtherPK writes contract 1. It is rendered as is - newer view fields are simply absent to it.`
|
|
15879
16076
|
});
|
|
15880
16077
|
if (!theme.manifest.kinds.includes(publication.kind)) warnings.push({
|
|
15881
16078
|
level: "warning",
|
|
15882
16079
|
code: "theme-kind-mismatch",
|
|
15883
|
-
message: `The theme "${theme.manifest.name}" is made for ${theme.manifest.kinds.join(" and ")} publications
|
|
16080
|
+
message: `The theme "${theme.manifest.name}" is made for ${theme.manifest.kinds.join(" and ")} publications - this one is ${publication.kind}.`
|
|
15884
16081
|
});
|
|
15885
16082
|
const includeDocs = /* @__PURE__ */ new Map();
|
|
15886
16083
|
for (const [slot, concept] of Object.entries(publication.includes)) {
|
|
@@ -15890,7 +16087,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15890
16087
|
warnings.push({
|
|
15891
16088
|
level: "warning",
|
|
15892
16089
|
code: "include-page-missing",
|
|
15893
|
-
message: `The include "${slot}" names "${concept}", which is not a document in this graph
|
|
16090
|
+
message: `The include "${slot}" names "${concept}", which is not a document in this graph - the theme's own ${slot} is used.`
|
|
15894
16091
|
});
|
|
15895
16092
|
continue;
|
|
15896
16093
|
}
|
|
@@ -15905,7 +16102,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
15905
16102
|
warnings.push({
|
|
15906
16103
|
level: "warning",
|
|
15907
16104
|
code: `include-page-${status}`,
|
|
15908
|
-
message: `The include "${slot}" names "${concept}", which ${why}
|
|
16105
|
+
message: `The include "${slot}" names "${concept}", which ${why} - the theme's own ${slot} is used.`
|
|
15909
16106
|
});
|
|
15910
16107
|
continue;
|
|
15911
16108
|
}
|
|
@@ -16019,7 +16216,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
16019
16216
|
warnings.push({
|
|
16020
16217
|
level: "warning",
|
|
16021
16218
|
code: "include-unknown-slot",
|
|
16022
|
-
message: `The theme "${theme.manifest.name}" has no include slot "${slot}"
|
|
16219
|
+
message: `The theme "${theme.manifest.name}" has no include slot "${slot}" - the page "${doc.concept}" is not used.`
|
|
16023
16220
|
});
|
|
16024
16221
|
continue;
|
|
16025
16222
|
}
|
|
@@ -16030,7 +16227,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
16030
16227
|
warnings.push({
|
|
16031
16228
|
level: "warning",
|
|
16032
16229
|
code: "include-no-css",
|
|
16033
|
-
message: `The include "${slot}" names "${doc.concept}", which has no \`css\` fence
|
|
16230
|
+
message: `The include "${slot}" names "${doc.concept}", which has no \`css\` fence - nothing is appended to the stylesheet.`
|
|
16034
16231
|
});
|
|
16035
16232
|
continue;
|
|
16036
16233
|
}
|
|
@@ -16206,7 +16403,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
16206
16403
|
warnings.push({
|
|
16207
16404
|
level: "warning",
|
|
16208
16405
|
code: "asset-missing",
|
|
16209
|
-
message: `"${from}" references ${name}, which the graph does not hold
|
|
16406
|
+
message: `"${from}" references ${name}, which the graph does not hold - the link is left as it is.`,
|
|
16210
16407
|
concept: from
|
|
16211
16408
|
});
|
|
16212
16409
|
continue;
|
|
@@ -16222,7 +16419,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
16222
16419
|
else warnings.push({
|
|
16223
16420
|
level: "warning",
|
|
16224
16421
|
code: "katex-assets-unavailable",
|
|
16225
|
-
message: "The publication has maths but this host has no KaTeX stylesheet to copy in
|
|
16422
|
+
message: "The publication has maths but this host has no KaTeX stylesheet to copy in - formulas will render unstyled."
|
|
16226
16423
|
});
|
|
16227
16424
|
}
|
|
16228
16425
|
const search = pages.map((p) => ({
|
|
@@ -16256,7 +16453,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
16256
16453
|
} else info.push({
|
|
16257
16454
|
level: "info",
|
|
16258
16455
|
code: "no-site-url",
|
|
16259
|
-
message: "The publication has no `url`, so no sitemap or feed is written
|
|
16456
|
+
message: "The publication has no `url`, so no sitemap or feed is written - add one to the publication page to get both."
|
|
16260
16457
|
});
|
|
16261
16458
|
bundle.set(".nojekyll", "");
|
|
16262
16459
|
const external = /* @__PURE__ */ new Set();
|
|
@@ -16267,7 +16464,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
16267
16464
|
if (pages.some((p) => p.rendered.mermaidFallback)) warnings.push({
|
|
16268
16465
|
level: "warning",
|
|
16269
16466
|
code: "mermaid-not-prerendered",
|
|
16270
|
-
message: "A diagram was not pre-rendered on this host and is left as its source
|
|
16467
|
+
message: "A diagram was not pre-rendered on this host and is left as its source - publish from the browser, or run `etherpk-mcp diagrams setup`, to get inline SVG."
|
|
16271
16468
|
});
|
|
16272
16469
|
progress("writing", pages.length + 3, pages.length + 3);
|
|
16273
16470
|
report.ok = errors.length === 0;
|
|
@@ -16360,7 +16557,7 @@ async function fetchTheme(url, fetchText) {
|
|
|
16360
16557
|
const { manifest, errors } = parseThemeManifest(json);
|
|
16361
16558
|
if (!manifest) throw new Error(`The theme at ${url} cannot be used: ${errors.join(" ")}`);
|
|
16362
16559
|
if (manifest.files.length === 0) throw new Error(`The theme at ${url} lists no files in its manifest, so nothing can be fetched.`);
|
|
16363
|
-
if (manifest.files.length > MAX_URL_THEME_FILES) throw new Error(`The theme at ${url} lists ${manifest.files.length} files
|
|
16560
|
+
if (manifest.files.length > MAX_URL_THEME_FILES) throw new Error(`The theme at ${url} lists ${manifest.files.length} files - a theme may list at most ${MAX_URL_THEME_FILES}.`);
|
|
16364
16561
|
const base = new URL(url);
|
|
16365
16562
|
const files = /* @__PURE__ */ new Map();
|
|
16366
16563
|
for (const path of manifest.files) {
|
|
@@ -16456,7 +16653,7 @@ function publicationMapping(input) {
|
|
|
16456
16653
|
* page whose purpose was anyone's guess (2026-09-19).
|
|
16457
16654
|
*/
|
|
16458
16655
|
function publicationPageIntro(name) {
|
|
16459
|
-
return `This page defines the publication "${name}". Its settings are the frontmatter above
|
|
16656
|
+
return `This page defines the publication "${name}". Its settings are the frontmatter above - edit them in Settings → Publish rather than here. The outline below is the site's navigation: one bullet per menu entry, written as a wikilink to the page. Nested bullets are sub-entries, a plain bullet with children is a group, and a markdown link is an outside link. A page that is public and in this publication but not listed here is still on the site, reachable by search and links, just not in the menu.`;
|
|
16460
16657
|
}
|
|
16461
16658
|
/**
|
|
16462
16659
|
* Create a publication page: a page whose frontmatter defines the publication and whose body
|
|
@@ -16610,7 +16807,7 @@ async function setupDiagrams(env, say) {
|
|
|
16610
16807
|
}
|
|
16611
16808
|
const cli = require$1.resolve("playwright-core/cli.js");
|
|
16612
16809
|
const dir = env.PLAYWRIGHT_BROWSERS_PATH?.trim() || browsersDir(env);
|
|
16613
|
-
say(`Installing Chromium into ${dir} (about 170 MB, once per computer
|
|
16810
|
+
say(`Installing Chromium into ${dir} (about 170 MB, once per computer - a diagram is rendered by a browser, and this is the one etherpk-mcp drives)…`);
|
|
16614
16811
|
await new Promise((resolve, reject) => {
|
|
16615
16812
|
const child = spawn(process.execPath, [
|
|
16616
16813
|
cli,
|
|
@@ -16636,7 +16833,7 @@ async function setupDiagrams(env, say) {
|
|
|
16636
16833
|
child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`Playwright's installer exited with code ${code}.`)));
|
|
16637
16834
|
});
|
|
16638
16835
|
const after = await chromiumStatus(env);
|
|
16639
|
-
if (!after.executable) throw new Error("The installer finished but no Chromium executable was found
|
|
16836
|
+
if (!after.executable) throw new Error("The installer finished but no Chromium executable was found - set ETHERPK_CHROMIUM to a browser on this machine instead.");
|
|
16640
16837
|
say(`Chromium is set up: ${after.executable}`);
|
|
16641
16838
|
return after;
|
|
16642
16839
|
}
|
|
@@ -17166,6 +17363,243 @@ async function writeDownload(directory, name, bytes) {
|
|
|
17166
17363
|
}
|
|
17167
17364
|
throw new Error(`${directory} already holds a thousand files named like ${safe}.`);
|
|
17168
17365
|
}
|
|
17366
|
+
//#endregion
|
|
17367
|
+
//#region ../client/src/lib/navigation/document-url.ts
|
|
17368
|
+
/** Encode a concept for the `[...concept]` rest param: per-segment, `/` kept. */
|
|
17369
|
+
function encodeConceptPath(concept) {
|
|
17370
|
+
return concept.split("/").map(encodeURIComponent).join("/");
|
|
17371
|
+
}
|
|
17372
|
+
/** The Document URL for a concept within a graph. */
|
|
17373
|
+
function documentUrl(graphId, concept) {
|
|
17374
|
+
return `${graphUrl(graphId)}/d/${encodeConceptPath(concept)}`;
|
|
17375
|
+
}
|
|
17376
|
+
/** The bare workspace URL for a graph. */
|
|
17377
|
+
function graphUrl(graphId) {
|
|
17378
|
+
return `/g/${encodeURIComponent(graphId)}`;
|
|
17379
|
+
}
|
|
17380
|
+
/**
|
|
17381
|
+
* The words that identify a task while its tags change: the text after the checkbox and the tag
|
|
17382
|
+
* run, whitespace collapsed and Unicode NFC applied. A task written as tags alone is its tags.
|
|
17383
|
+
*/
|
|
17384
|
+
function taskWords(label) {
|
|
17385
|
+
return (parseTaskTags(label).text || label).normalize("NFC").replace(/\s+/g, " ").trim();
|
|
17386
|
+
}
|
|
17387
|
+
/** The fingerprint of a task's words, from its label (tag run included or not). */
|
|
17388
|
+
async function taskFingerprint(label) {
|
|
17389
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(taskWords(label)));
|
|
17390
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 12);
|
|
17391
|
+
}
|
|
17392
|
+
/** The reference's address: the Document URL with the task in its fragment, which a browser never sends. */
|
|
17393
|
+
function taskReferenceUrl(reference, origin = "") {
|
|
17394
|
+
return `${origin}${documentUrl(reference.graphId, reference.document)}#task=${reference.line}-${reference.fingerprint}`;
|
|
17395
|
+
}
|
|
17396
|
+
var REFERENCE = /\/g\/([^/?#\s]+)\/d\/([^?#\s]+?)(?:\?[^#\s]*)?#task=(\d+)-([0-9a-f]{12})(?![0-9a-f])/;
|
|
17397
|
+
/**
|
|
17398
|
+
* The reference in `input`: the two lines as copied, the address alone, with any origin or none,
|
|
17399
|
+
* or null when `input` holds none.
|
|
17400
|
+
*/
|
|
17401
|
+
function parseTaskReference(input) {
|
|
17402
|
+
const match = REFERENCE.exec(input);
|
|
17403
|
+
if (!match) return null;
|
|
17404
|
+
const [, graph, path, line, fingerprint] = match;
|
|
17405
|
+
try {
|
|
17406
|
+
return {
|
|
17407
|
+
graphId: decodeURIComponent(graph),
|
|
17408
|
+
document: path.split("/").map(decodeURIComponent).join("/"),
|
|
17409
|
+
line: Number(line),
|
|
17410
|
+
fingerprint
|
|
17411
|
+
};
|
|
17412
|
+
} catch {
|
|
17413
|
+
return null;
|
|
17414
|
+
}
|
|
17415
|
+
}
|
|
17416
|
+
/**
|
|
17417
|
+
* Find the task a reference names. First the named document's line, when the words there match;
|
|
17418
|
+
* then the one task in that document with those words; then the one task in the graph. None is
|
|
17419
|
+
* `task_not_found`, more than one `task_ambiguous`, since acting on a guess could change the
|
|
17420
|
+
* wrong task. A document that no longer answers to its name goes straight to the graph.
|
|
17421
|
+
*/
|
|
17422
|
+
async function resolveTaskReference(reference, sources) {
|
|
17423
|
+
if (sources.graphId !== null && sources.graphId !== reference.graphId) return {
|
|
17424
|
+
ok: false,
|
|
17425
|
+
code: "other_graph",
|
|
17426
|
+
servedGraphId: sources.graphId
|
|
17427
|
+
};
|
|
17428
|
+
const named = sources.resolveDocument(reference.document);
|
|
17429
|
+
if (named !== null) {
|
|
17430
|
+
const body = await sources.readBody(named);
|
|
17431
|
+
if (body !== null) {
|
|
17432
|
+
const matches = await taskLinesWith(body.split("\n"), reference.fingerprint);
|
|
17433
|
+
if (matches.includes(reference.line)) return {
|
|
17434
|
+
ok: true,
|
|
17435
|
+
document: named,
|
|
17436
|
+
line: reference.line,
|
|
17437
|
+
body,
|
|
17438
|
+
foundBy: "at_line"
|
|
17439
|
+
};
|
|
17440
|
+
if (matches.length === 1) return {
|
|
17441
|
+
ok: true,
|
|
17442
|
+
document: named,
|
|
17443
|
+
line: matches[0],
|
|
17444
|
+
body,
|
|
17445
|
+
foundBy: "moved"
|
|
17446
|
+
};
|
|
17447
|
+
if (matches.length > 1) return {
|
|
17448
|
+
ok: false,
|
|
17449
|
+
code: "task_ambiguous",
|
|
17450
|
+
candidates: matches.map((line) => ({
|
|
17451
|
+
document: named,
|
|
17452
|
+
line
|
|
17453
|
+
}))
|
|
17454
|
+
};
|
|
17455
|
+
}
|
|
17456
|
+
}
|
|
17457
|
+
const found = [];
|
|
17458
|
+
const looked = new Set(named === null ? [] : [named.toLowerCase()]);
|
|
17459
|
+
for (const task of await sources.allTasks()) {
|
|
17460
|
+
if (looked.has(task.document.toLowerCase())) continue;
|
|
17461
|
+
if (await taskFingerprint(task.label) !== reference.fingerprint) continue;
|
|
17462
|
+
looked.add(task.document.toLowerCase());
|
|
17463
|
+
const body = await sources.readBody(task.document);
|
|
17464
|
+
if (body === null) continue;
|
|
17465
|
+
for (const line of await taskLinesWith(body.split("\n"), reference.fingerprint)) found.push({
|
|
17466
|
+
document: task.document,
|
|
17467
|
+
line,
|
|
17468
|
+
body
|
|
17469
|
+
});
|
|
17470
|
+
}
|
|
17471
|
+
if (found.length === 1) return {
|
|
17472
|
+
ok: true,
|
|
17473
|
+
...found[0],
|
|
17474
|
+
foundBy: "other_document"
|
|
17475
|
+
};
|
|
17476
|
+
if (found.length > 1) return {
|
|
17477
|
+
ok: false,
|
|
17478
|
+
code: "task_ambiguous",
|
|
17479
|
+
candidates: found.map(({ document, line }) => ({
|
|
17480
|
+
document,
|
|
17481
|
+
line
|
|
17482
|
+
}))
|
|
17483
|
+
};
|
|
17484
|
+
return {
|
|
17485
|
+
ok: false,
|
|
17486
|
+
code: "task_not_found"
|
|
17487
|
+
};
|
|
17488
|
+
}
|
|
17489
|
+
/** How many tasks `everyIndexedTask` asks the index for at a time. */
|
|
17490
|
+
var INDEX_PAGE = 500;
|
|
17491
|
+
/** Every task the index knows, in pages, as {@link TaskReferenceSources.allTasks} wants them. */
|
|
17492
|
+
async function everyIndexedTask(index, today) {
|
|
17493
|
+
const query = {
|
|
17494
|
+
concept: null,
|
|
17495
|
+
statuses: TASK_STATUSES,
|
|
17496
|
+
priorities: TASK_PRIORITY_FILTERS,
|
|
17497
|
+
due: "any",
|
|
17498
|
+
groupBy: "document",
|
|
17499
|
+
today
|
|
17500
|
+
};
|
|
17501
|
+
const out = [];
|
|
17502
|
+
for (let offset = 0;; offset += INDEX_PAGE) {
|
|
17503
|
+
const page = await index.tasks(query, offset, INDEX_PAGE);
|
|
17504
|
+
for (const hit of page.hits) out.push({
|
|
17505
|
+
document: hit.concept,
|
|
17506
|
+
line: hit.line,
|
|
17507
|
+
label: hit.text
|
|
17508
|
+
});
|
|
17509
|
+
if (!page.hasMore) return out;
|
|
17510
|
+
}
|
|
17511
|
+
}
|
|
17512
|
+
/** The lines of `lines` that hold a task with this fingerprint, never one inside a code fence. */
|
|
17513
|
+
async function taskLinesWith(lines, fingerprint) {
|
|
17514
|
+
const inFence = /* @__PURE__ */ new Set();
|
|
17515
|
+
for (const block of fencedBlocks(lines)) for (let i = block.start; i <= block.end; i++) inFence.add(i);
|
|
17516
|
+
const out = [];
|
|
17517
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17518
|
+
if (inFence.has(i) || parseTaskLine(lines[i]) === null) continue;
|
|
17519
|
+
if (await taskFingerprint(bulletLabel(lines[i])) === fingerprint) out.push(i);
|
|
17520
|
+
}
|
|
17521
|
+
return out;
|
|
17522
|
+
}
|
|
17523
|
+
//#endregion
|
|
17524
|
+
//#region ../client/src/lib/document/view/minimal-replacement.ts
|
|
17525
|
+
/**
|
|
17526
|
+
* The smallest single replacement turning `text` into `next`: the shared prefix and suffix
|
|
17527
|
+
* trimmed away. Null when the two are equal.
|
|
17528
|
+
*
|
|
17529
|
+
* Used wherever a whole new text arrives for a document that is being edited - a heal, an
|
|
17530
|
+
* external write reaching an open editor - so that the change is the change and nothing more
|
|
17531
|
+
* (ADR 0066): on a synced graph a whole-document replace is one enormous CRDT operation that
|
|
17532
|
+
* clobbers everyone else's concurrent edits, and in any editor it throws the caret to the end.
|
|
17533
|
+
*/
|
|
17534
|
+
function minimalReplacement(text, next) {
|
|
17535
|
+
if (next === text) return null;
|
|
17536
|
+
let prefix = 0;
|
|
17537
|
+
while (prefix < text.length && prefix < next.length && text[prefix] === next[prefix]) prefix++;
|
|
17538
|
+
let suffix = 0;
|
|
17539
|
+
while (suffix < text.length - prefix && suffix < next.length - prefix && text[text.length - 1 - suffix] === next[next.length - 1 - suffix]) suffix++;
|
|
17540
|
+
return {
|
|
17541
|
+
from: prefix,
|
|
17542
|
+
to: text.length - suffix,
|
|
17543
|
+
insert: next.slice(prefix, next.length - suffix)
|
|
17544
|
+
};
|
|
17545
|
+
}
|
|
17546
|
+
//#endregion
|
|
17547
|
+
//#region ../client/src/lib/document/task-write.ts
|
|
17548
|
+
var MARKER = /^(\s*-\s+\[)[ xX](\])\s?/;
|
|
17549
|
+
/**
|
|
17550
|
+
* `line` with `changes` made, or null when `line` is not a task.
|
|
17551
|
+
*
|
|
17552
|
+
* A changed tag run is written in the fixed order the `#` helper writes (`serialiseTags`), for
|
|
17553
|
+
* the reason `applyTaskTag` gives: the run is a set, and a predictable order is worth more than
|
|
17554
|
+
* one that carries no meaning. A task already as asked comes back untouched, so a move that
|
|
17555
|
+
* changes nothing reorders nothing either.
|
|
17556
|
+
*/
|
|
17557
|
+
function taskLineWith(line, changes) {
|
|
17558
|
+
const parsed = parseTaskLine(line);
|
|
17559
|
+
const marker = MARKER.exec(line);
|
|
17560
|
+
if (!parsed || !marker) return null;
|
|
17561
|
+
const { done: wasDone, ...current } = parsed;
|
|
17562
|
+
const tags = { ...current };
|
|
17563
|
+
let done = wasDone;
|
|
17564
|
+
switch (changes.status) {
|
|
17565
|
+
case "open":
|
|
17566
|
+
done = false;
|
|
17567
|
+
tags.waiting = tags.doing = tags.cancelled = false;
|
|
17568
|
+
break;
|
|
17569
|
+
case "doing":
|
|
17570
|
+
done = false;
|
|
17571
|
+
tags.doing = true;
|
|
17572
|
+
tags.waiting = tags.cancelled = false;
|
|
17573
|
+
break;
|
|
17574
|
+
case "waiting":
|
|
17575
|
+
done = false;
|
|
17576
|
+
tags.waiting = true;
|
|
17577
|
+
tags.doing = tags.cancelled = false;
|
|
17578
|
+
break;
|
|
17579
|
+
case "done":
|
|
17580
|
+
done = true;
|
|
17581
|
+
tags.waiting = tags.doing = tags.cancelled = false;
|
|
17582
|
+
break;
|
|
17583
|
+
case "cancelled":
|
|
17584
|
+
done = true;
|
|
17585
|
+
tags.cancelled = true;
|
|
17586
|
+
tags.waiting = tags.doing = false;
|
|
17587
|
+
break;
|
|
17588
|
+
case void 0: break;
|
|
17589
|
+
}
|
|
17590
|
+
if (changes.priority !== void 0) tags.priority = changes.priority;
|
|
17591
|
+
if (changes.due !== void 0) tags.due = changes.due;
|
|
17592
|
+
if (changes.scheduled !== void 0) tags.scheduled = changes.scheduled;
|
|
17593
|
+
if (done === wasDone && sameTags(tags, current)) return line;
|
|
17594
|
+
return [
|
|
17595
|
+
`${marker[1]}${done ? "x" : " "}${marker[2]}`,
|
|
17596
|
+
...serialiseTags(tags),
|
|
17597
|
+
tags.text
|
|
17598
|
+
].filter((part) => part !== "").join(" ");
|
|
17599
|
+
}
|
|
17600
|
+
function sameTags(a, b) {
|
|
17601
|
+
return a.priority === b.priority && a.waiting === b.waiting && a.doing === b.doing && a.cancelled === b.cancelled && a.due === b.due && a.scheduled === b.scheduled && a.completion === b.completion;
|
|
17602
|
+
}
|
|
17169
17603
|
/** UTF-16 units of document text returned before `truncated` is set. */
|
|
17170
17604
|
var READ_TEXT_CAP = 2e5;
|
|
17171
17605
|
/** A refusal the agent can act on: a stable code first, a sentence second. */
|
|
@@ -17267,7 +17701,7 @@ async function liveText(graph, identity) {
|
|
|
17267
17701
|
return graph.store.open(identity.concept).getText();
|
|
17268
17702
|
}
|
|
17269
17703
|
function refuseIfProtected(concept, text) {
|
|
17270
|
-
if (documentProtection(text).kind === "document") throw new ToolError("protected_document", `"${concept}" is a protected document. Its content is encrypted under a key this client never holds, so it cannot be read or changed here
|
|
17704
|
+
if (documentProtection(text).kind === "document") throw new ToolError("protected_document", `"${concept}" is a protected document. Its content is encrypted under a key this client never holds, so it cannot be read or changed here - the user can unlock it in EtherPK.`);
|
|
17271
17705
|
}
|
|
17272
17706
|
function page$1(items, offset, limit) {
|
|
17273
17707
|
return {
|
|
@@ -17444,7 +17878,7 @@ async function tasks(graph, args = {}) {
|
|
|
17444
17878
|
today: todayISO()
|
|
17445
17879
|
}, offset, limit);
|
|
17446
17880
|
return {
|
|
17447
|
-
tasks: result.hits.map((hit) => ({
|
|
17881
|
+
tasks: await Promise.all(result.hits.map(async (hit) => ({
|
|
17448
17882
|
concept: hit.concept,
|
|
17449
17883
|
kind: hit.kind,
|
|
17450
17884
|
line: hit.line,
|
|
@@ -17457,12 +17891,20 @@ async function tasks(graph, args = {}) {
|
|
|
17457
17891
|
due: hit.due,
|
|
17458
17892
|
scheduled: hit.scheduled,
|
|
17459
17893
|
completion: hit.completion,
|
|
17460
|
-
breadcrumb: hit.breadcrumb
|
|
17461
|
-
|
|
17894
|
+
breadcrumb: hit.breadcrumb,
|
|
17895
|
+
reference: await referenceFor(graph, hit.concept, hit.line, hit.text)
|
|
17896
|
+
}))),
|
|
17462
17897
|
total: result.total,
|
|
17463
17898
|
hasMore: result.hasMore
|
|
17464
17899
|
};
|
|
17465
17900
|
}
|
|
17901
|
+
/**
|
|
17902
|
+
* Text an agent sent, in the form EtherPK writes: line feeds only (ADR 0112), then the
|
|
17903
|
+
* two-space Indent Unit (ADR 0067). Line endings go first so the normaliser reads whole lines.
|
|
17904
|
+
*/
|
|
17905
|
+
function asWritten(text) {
|
|
17906
|
+
return normaliseIndentUnit(lineFeedsOnly(text));
|
|
17907
|
+
}
|
|
17466
17908
|
async function editDocument(graph, args) {
|
|
17467
17909
|
if (args.old === "") throw new ToolError("invalid_argument", "old must not be empty.");
|
|
17468
17910
|
await graph.store.refresh();
|
|
@@ -17470,9 +17912,9 @@ async function editDocument(graph, args) {
|
|
|
17470
17912
|
const text = await liveText(graph, identity);
|
|
17471
17913
|
refuseIfProtected(identity.concept, text);
|
|
17472
17914
|
const first = text.indexOf(args.old);
|
|
17473
|
-
if (first === -1) throw new ToolError("no_match", `"${identity.concept}" does not contain the text to replace. Read the document again
|
|
17915
|
+
if (first === -1) throw new ToolError("no_match", `"${identity.concept}" does not contain the text to replace. Read the document again - it may have changed.`);
|
|
17474
17916
|
if (text.indexOf(args.old, first + 1) !== -1) throw new ToolError("ambiguous_match", `The text to replace occurs more than once in "${identity.concept}". Include more surrounding text so it matches exactly once.`);
|
|
17475
|
-
const insert =
|
|
17917
|
+
const insert = asWritten(args.new);
|
|
17476
17918
|
graph.store.open(identity.concept).applyChange({
|
|
17477
17919
|
from: first,
|
|
17478
17920
|
to: first + args.old.length,
|
|
@@ -17486,7 +17928,7 @@ async function editDocument(graph, args) {
|
|
|
17486
17928
|
};
|
|
17487
17929
|
}
|
|
17488
17930
|
async function appendDocument(graph, args) {
|
|
17489
|
-
const text =
|
|
17931
|
+
const text = asWritten(args.text);
|
|
17490
17932
|
if (text.trim() === "") throw new ToolError("invalid_argument", "text must not be empty.");
|
|
17491
17933
|
const target = args.concept.trim() === "today" ? todayISO() : args.concept.trim();
|
|
17492
17934
|
await graph.store.refresh();
|
|
@@ -17517,10 +17959,10 @@ async function appendDocument(graph, args) {
|
|
|
17517
17959
|
async function createPage(graph, args) {
|
|
17518
17960
|
const title = args.title.trim();
|
|
17519
17961
|
if (title === "") throw new ToolError("invalid_argument", "title must not be empty.");
|
|
17520
|
-
if (isJournalConcept(title)) throw new ToolError("invalid_argument", `"${title}" is a calendar day, so it names a journal entry
|
|
17962
|
+
if (isJournalConcept(title)) throw new ToolError("invalid_argument", `"${title}" is a calendar day, so it names a journal entry - use append_document to write to it.`);
|
|
17521
17963
|
await graph.store.refresh();
|
|
17522
17964
|
if (resolveIdentity(graph, title)) throw new ToolError("already_exists", `A document already answers to "${title}". Use edit_document or append_document instead.`);
|
|
17523
|
-
await graph.store.createPage(title,
|
|
17965
|
+
await graph.store.createPage(title, asWritten(args.text ?? ""));
|
|
17524
17966
|
if (args.frontmatter && Object.keys(args.frontmatter).length > 0) {
|
|
17525
17967
|
await graph.store.whenReady(title);
|
|
17526
17968
|
const raw = graph.store.openRaw(title).getText();
|
|
@@ -17847,7 +18289,7 @@ async function listAssets(graph, args = {}) {
|
|
|
17847
18289
|
*/
|
|
17848
18290
|
async function readDocuments(graph, args) {
|
|
17849
18291
|
if (!Array.isArray(args.concepts) || args.concepts.length === 0) throw new ToolError("invalid_argument", "concepts must be a non-empty list of names.");
|
|
17850
|
-
if (args.concepts.length > 20) throw new ToolError("too_many", `At most 20 documents per call
|
|
18292
|
+
if (args.concepts.length > 20) throw new ToolError("too_many", `At most 20 documents per call - ask again for the rest.`);
|
|
17851
18293
|
await graph.store.refresh();
|
|
17852
18294
|
let budget = READ_MANY_TEXT_CAP;
|
|
17853
18295
|
const documents = [];
|
|
@@ -17884,22 +18326,25 @@ async function readDocuments(graph, args) {
|
|
|
17884
18326
|
}
|
|
17885
18327
|
return { documents };
|
|
17886
18328
|
}
|
|
17887
|
-
/** The task line rebuilt with these tags and checkbox: indent and marker kept, the tag run in the fixed order. */
|
|
17888
|
-
function taskLineWith(line, done, tags) {
|
|
17889
|
-
const marker = /^(\s*-\s+\[)[ xX](\])\s?/.exec(line);
|
|
17890
|
-
if (!marker) throw new Error("not a task line");
|
|
17891
|
-
const run = serialiseTags(tags);
|
|
17892
|
-
return `${marker[1]}${done ? "x" : " "}${marker[2]} ${[...run, tags.text].join(" ")}`.replace(/\s+$/, "");
|
|
17893
|
-
}
|
|
17894
18329
|
/**
|
|
17895
18330
|
* Change one task's status, priority or dates, by document and line (ADR 0032's grammar).
|
|
17896
|
-
*
|
|
17897
|
-
*
|
|
17898
|
-
*
|
|
17899
|
-
* the
|
|
18331
|
+
* The line is rewritten by the writer a Kanban Board move uses (`task-write.ts`), so a status
|
|
18332
|
+
* here means exactly what a lane means: exclusive, `waiting` and `doing` unchecked states, `done`
|
|
18333
|
+
* and `cancelled` checked ones, `open` none of them, and no completion date written. `expect` is
|
|
18334
|
+
* the stale-line guard, made explicit for a caller working from a list.
|
|
17900
18335
|
*/
|
|
17901
18336
|
async function setTask(graph, args) {
|
|
17902
|
-
if (
|
|
18337
|
+
if (args.reference !== void 0) {
|
|
18338
|
+
const found = await requireReferencedTask(graph, args.reference);
|
|
18339
|
+
return setTask(graph, {
|
|
18340
|
+
concept: found.document,
|
|
18341
|
+
line: found.line,
|
|
18342
|
+
changes: args.changes
|
|
18343
|
+
});
|
|
18344
|
+
}
|
|
18345
|
+
if (args.concept === void 0) throw new ToolError("invalid_argument", "Name the task by reference, or by concept and line.");
|
|
18346
|
+
if (args.line === void 0 || !Number.isInteger(args.line) || args.line < 0) throw new ToolError("invalid_argument", "line must be a non-negative integer.");
|
|
18347
|
+
const line = args.line;
|
|
17903
18348
|
const changes = args.changes ?? {};
|
|
17904
18349
|
if (changes.status !== void 0 && !TASK_STATUSES.includes(changes.status)) throw new ToolError("invalid_argument", `Unknown task status "${String(changes.status)}".`);
|
|
17905
18350
|
if (changes.priority !== void 0 && changes.priority !== null && ![
|
|
@@ -17913,49 +18358,19 @@ async function setTask(graph, args) {
|
|
|
17913
18358
|
const text = await liveText(graph, identity);
|
|
17914
18359
|
refuseIfProtected(identity.concept, text);
|
|
17915
18360
|
const lines = text.split("\n");
|
|
17916
|
-
const current = lines[
|
|
18361
|
+
const current = lines[line];
|
|
17917
18362
|
const parsed = current === void 0 ? null : parseTaskLine(current);
|
|
17918
|
-
if (!parsed) throw new ToolError("task_moved", `Line ${
|
|
18363
|
+
if (!parsed) throw new ToolError("task_moved", `Line ${line} of "${identity.concept}" is not a task now - read the document or list tasks again.`);
|
|
17919
18364
|
const afterCheckbox = current.replace(/^\s*-\s+\[[ xX]\]\s?/, "").trim();
|
|
17920
|
-
if (args.expect !== void 0 && args.expect.trim() !== afterCheckbox && args.expect.trim() !== parsed.text.trim()) throw new ToolError("task_moved", `Line ${
|
|
17921
|
-
const
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
done = false;
|
|
17926
|
-
tags.waiting = tags.doing = tags.cancelled = false;
|
|
17927
|
-
break;
|
|
17928
|
-
case "doing":
|
|
17929
|
-
done = false;
|
|
17930
|
-
tags.doing = true;
|
|
17931
|
-
tags.waiting = tags.cancelled = false;
|
|
17932
|
-
break;
|
|
17933
|
-
case "waiting":
|
|
17934
|
-
done = false;
|
|
17935
|
-
tags.waiting = true;
|
|
17936
|
-
tags.doing = tags.cancelled = false;
|
|
17937
|
-
break;
|
|
17938
|
-
case "done":
|
|
17939
|
-
done = true;
|
|
17940
|
-
tags.waiting = tags.doing = tags.cancelled = false;
|
|
17941
|
-
break;
|
|
17942
|
-
case "cancelled":
|
|
17943
|
-
done = true;
|
|
17944
|
-
tags.cancelled = true;
|
|
17945
|
-
tags.waiting = tags.doing = false;
|
|
17946
|
-
break;
|
|
17947
|
-
case void 0: break;
|
|
17948
|
-
}
|
|
17949
|
-
if (changes.priority !== void 0) tags.priority = changes.priority;
|
|
17950
|
-
if (changes.due !== void 0) tags.due = changes.due;
|
|
17951
|
-
if (changes.scheduled !== void 0) tags.scheduled = changes.scheduled;
|
|
17952
|
-
const next = taskLineWith(current, done, tags);
|
|
17953
|
-
if (next !== current) {
|
|
17954
|
-
const from = lines.slice(0, args.line).reduce((n, l) => n + l.length + 1, 0);
|
|
18365
|
+
if (args.expect !== void 0 && args.expect.trim() !== afterCheckbox && args.expect.trim() !== parsed.text.trim()) throw new ToolError("task_moved", `Line ${line} of "${identity.concept}" now reads "${parsed.text}", not "${args.expect}" - list tasks again before changing it.`);
|
|
18366
|
+
const next = taskLineWith(current, changes) ?? current;
|
|
18367
|
+
const edit = minimalReplacement(current, next);
|
|
18368
|
+
if (edit) {
|
|
18369
|
+
const from = lines.slice(0, line).reduce((n, l) => n + l.length + 1, 0);
|
|
17955
18370
|
graph.store.open(identity.concept).applyChange({
|
|
17956
|
-
from,
|
|
17957
|
-
to: from +
|
|
17958
|
-
insert:
|
|
18371
|
+
from: from + edit.from,
|
|
18372
|
+
to: from + edit.to,
|
|
18373
|
+
insert: edit.insert
|
|
17959
18374
|
}, "external");
|
|
17960
18375
|
await settle$2(graph);
|
|
17961
18376
|
}
|
|
@@ -17963,13 +18378,124 @@ async function setTask(graph, args) {
|
|
|
17963
18378
|
const status = after.done ? after.cancelled ? "cancelled" : "done" : after.waiting ? "waiting" : after.doing ? "doing" : "open";
|
|
17964
18379
|
return {
|
|
17965
18380
|
concept: identity.concept,
|
|
17966
|
-
line
|
|
18381
|
+
line,
|
|
17967
18382
|
text: after.text,
|
|
17968
18383
|
status,
|
|
17969
18384
|
priority: after.priority,
|
|
17970
18385
|
due: after.due,
|
|
17971
18386
|
scheduled: after.scheduled,
|
|
17972
|
-
changed: next !== current
|
|
18387
|
+
changed: next !== current,
|
|
18388
|
+
reference: await referenceFor(graph, identity.concept, line, bulletLabel(next))
|
|
18389
|
+
};
|
|
18390
|
+
}
|
|
18391
|
+
/** A task's status from its parsed line, as the index derives it (`STATUS_SQL`). */
|
|
18392
|
+
function statusOf(task) {
|
|
18393
|
+
return task.done ? task.cancelled ? "cancelled" : "done" : task.waiting ? "waiting" : task.doing ? "doing" : "open";
|
|
18394
|
+
}
|
|
18395
|
+
/** Where a Task Reference is resolved in this graph: its documents, read live, and its index. */
|
|
18396
|
+
function referenceSources(graph) {
|
|
18397
|
+
return {
|
|
18398
|
+
graphId: graph.backend.kind === "synced" ? graph.graphId : null,
|
|
18399
|
+
resolveDocument: (name) => resolveIdentity(graph, name)?.concept ?? null,
|
|
18400
|
+
async readBody(document) {
|
|
18401
|
+
const identity = resolveIdentity(graph, document);
|
|
18402
|
+
if (!identity) return null;
|
|
18403
|
+
const text = await liveText(graph, identity);
|
|
18404
|
+
return documentProtection(text).kind === "document" ? null : text;
|
|
18405
|
+
},
|
|
18406
|
+
allTasks: () => everyIndexedTask(graph.index, todayISO())
|
|
18407
|
+
};
|
|
18408
|
+
}
|
|
18409
|
+
/** The Task Reference to the task at `line` of `concept`, whose label is `label`. */
|
|
18410
|
+
async function referenceFor(graph, concept, line, label) {
|
|
18411
|
+
const fingerprint = await taskFingerprint(label);
|
|
18412
|
+
return taskReferenceUrl({
|
|
18413
|
+
graphId: graph.graphId,
|
|
18414
|
+
document: concept,
|
|
18415
|
+
line,
|
|
18416
|
+
fingerprint
|
|
18417
|
+
}, graph.clientUrl ?? "");
|
|
18418
|
+
}
|
|
18419
|
+
/** The task a Task Reference names in this graph, or the error that says why there is none. */
|
|
18420
|
+
async function requireReferencedTask(graph, reference) {
|
|
18421
|
+
const parsed = parseTaskReference(reference);
|
|
18422
|
+
if (!parsed) throw new ToolError("invalid_argument", "That is not a task reference: one ends in #task=<line>-<fingerprint>, as Copy task reference in EtherPK writes it.");
|
|
18423
|
+
await graph.store.refresh();
|
|
18424
|
+
const found = await resolveTaskReference(parsed, referenceSources(graph));
|
|
18425
|
+
if (found.ok) return found;
|
|
18426
|
+
if (found.code === "other_graph") throw new ToolError("other_graph", `That task reference is for another graph. This server serves "${graph.name}" (${found.servedGraphId}); ask the user to connect the graph the reference is from.`);
|
|
18427
|
+
if (found.code === "task_ambiguous") throw new ToolError("task_ambiguous", `More than one task has those words (${found.candidates.map((candidate) => `"${candidate.document}" line ${candidate.line}`).join(", ")}), so none was chosen. Ask the user which one, or name it by concept and line.`);
|
|
18428
|
+
throw new ToolError("task_not_found", "No task in the graph has those words now: it was edited or deleted. Ask the user for a fresh reference.");
|
|
18429
|
+
}
|
|
18430
|
+
/**
|
|
18431
|
+
* The block's own lines and everything nested under it: in the index's rows, which run in
|
|
18432
|
+
* document order, the rows after it until one that is not its descendant.
|
|
18433
|
+
*/
|
|
18434
|
+
function subtreeEnd(blocks, localId) {
|
|
18435
|
+
let end = blocks[localId].endLine;
|
|
18436
|
+
const inside = new Set([localId]);
|
|
18437
|
+
for (let i = localId + 1; i < blocks.length; i++) {
|
|
18438
|
+
const parent = blocks[i].parentId;
|
|
18439
|
+
if (parent === null || !inside.has(parent)) break;
|
|
18440
|
+
inside.add(i);
|
|
18441
|
+
end = Math.max(end, blocks[i].endLine);
|
|
18442
|
+
}
|
|
18443
|
+
return end;
|
|
18444
|
+
}
|
|
18445
|
+
/**
|
|
18446
|
+
* The task a Task Reference names: its words and tags, where it is now, what is above it (the
|
|
18447
|
+
* headings and bullets its breadcrumb names) and its detail (its continuation lines and
|
|
18448
|
+
* everything nested under it), and how it was found. For the text around it, read the document.
|
|
18449
|
+
*/
|
|
18450
|
+
async function readTask(graph, args) {
|
|
18451
|
+
const found = await requireReferencedTask(graph, args.reference);
|
|
18452
|
+
const lines = found.body.split("\n");
|
|
18453
|
+
const line = lines[found.line];
|
|
18454
|
+
const task = parseTaskLine(line);
|
|
18455
|
+
const { blocks } = deriveDoc(found.body);
|
|
18456
|
+
const block = blocks.find((candidate) => candidate.startLine === found.line);
|
|
18457
|
+
const end = block ? subtreeEnd(blocks, block.localId) : found.line;
|
|
18458
|
+
return {
|
|
18459
|
+
concept: found.document,
|
|
18460
|
+
line: found.line,
|
|
18461
|
+
text: task.text,
|
|
18462
|
+
status: statusOf(task),
|
|
18463
|
+
priority: task.priority,
|
|
18464
|
+
due: task.due,
|
|
18465
|
+
scheduled: task.scheduled,
|
|
18466
|
+
breadcrumb: block ? ancestorChain(blocks, block.parentId) : [],
|
|
18467
|
+
detail: lines.slice(found.line + 1, end + 1).join("\n"),
|
|
18468
|
+
foundBy: found.foundBy,
|
|
18469
|
+
reference: await referenceFor(graph, found.document, found.line, bulletLabel(line))
|
|
18470
|
+
};
|
|
18471
|
+
}
|
|
18472
|
+
/**
|
|
18473
|
+
* Add a note under the task a Task Reference names: the text as a bullet one level under the
|
|
18474
|
+
* task, after its existing detail, so a Kanban Board's Task Detail shows it with the task. Text
|
|
18475
|
+
* that is already bullets is nested as it is.
|
|
18476
|
+
*/
|
|
18477
|
+
async function addTaskNote(graph, args) {
|
|
18478
|
+
if (typeof args.text !== "string" || args.text.trim() === "") throw new ToolError("invalid_argument", "text must not be empty.");
|
|
18479
|
+
const found = await requireReferencedTask(graph, args.reference);
|
|
18480
|
+
const lines = found.body.split("\n");
|
|
18481
|
+
const { blocks } = deriveDoc(found.body);
|
|
18482
|
+
const block = blocks.find((candidate) => candidate.startLine === found.line);
|
|
18483
|
+
const end = block ? subtreeEnd(blocks, block.localId) : found.line;
|
|
18484
|
+
const childIndent = `${/^\s*/.exec(lines[found.line])[0]} `;
|
|
18485
|
+
const written = asWritten(args.text.trim());
|
|
18486
|
+
const insert = (written.startsWith("- ") ? written : `- ${written.replace(/\n/g, "\n ")}`).split("\n").map((noteLine) => noteLine === "" ? noteLine : childIndent + noteLine).join("\n");
|
|
18487
|
+
const at = lines.slice(0, end + 1).reduce((n, l) => n + l.length + 1, 0) - 1;
|
|
18488
|
+
graph.store.open(found.document).applyChange({
|
|
18489
|
+
from: at,
|
|
18490
|
+
to: at,
|
|
18491
|
+
insert: `\n${insert}`
|
|
18492
|
+
}, "external");
|
|
18493
|
+
await settle$2(graph);
|
|
18494
|
+
return {
|
|
18495
|
+
concept: found.document,
|
|
18496
|
+
line: found.line,
|
|
18497
|
+
noteLine: end + 1,
|
|
18498
|
+
reference: await referenceFor(graph, found.document, found.line, bulletLabel(lines[found.line]))
|
|
17973
18499
|
};
|
|
17974
18500
|
}
|
|
17975
18501
|
/** What an agent should know before it starts: where it is, what is here, what is set up. */
|
|
@@ -17994,7 +18520,7 @@ async function graphInfo(graph) {
|
|
|
17994
18520
|
complete: status.embedded >= status.total
|
|
17995
18521
|
} : {
|
|
17996
18522
|
state: "not-opened",
|
|
17997
|
-
note: "The first search with mode \"semantic\" loads the model
|
|
18523
|
+
note: "The first search with mode \"semantic\" loads the model - if it is not set up on this computer, that search says so and names the command."
|
|
17998
18524
|
},
|
|
17999
18525
|
assets: { available: graph.assets !== void 0 }
|
|
18000
18526
|
};
|
|
@@ -18110,7 +18636,7 @@ async function createPublication(graph, args, host) {
|
|
|
18110
18636
|
await graph.store.refresh();
|
|
18111
18637
|
const before = summarisePublishing((await graph.publishing.readSource()).source);
|
|
18112
18638
|
if (before.publications.some((p) => p.id === id)) throw new ToolError("already_exists", `A publication with the id "${id}" already exists (page "${before.publications.find((p) => p.id === id).concept}").`);
|
|
18113
|
-
if (graph.store.listDocuments().some((d) => d.key === title.toLowerCase() || d.aliases.some((a) => a.toLowerCase() === title.toLowerCase()))) throw new ToolError("already_exists", `A document already answers to "${title}"
|
|
18639
|
+
if (graph.store.listDocuments().some((d) => d.key === title.toLowerCase() || d.aliases.some((a) => a.toLowerCase() === title.toLowerCase()))) throw new ToolError("already_exists", `A document already answers to "${title}" - choose another title for the publication page.`);
|
|
18114
18640
|
if (args.theme) await requireTheme(graph, args.theme);
|
|
18115
18641
|
const store = frontmatterStore(graph);
|
|
18116
18642
|
const concept = await createPublicationPage(store, {
|
|
@@ -18363,8 +18889,8 @@ var SAMPLE_SOURCE = {
|
|
|
18363
18889
|
""
|
|
18364
18890
|
].join("\n")),
|
|
18365
18891
|
page("[[Getting Started]] Installing", "---\npublic: true\n---\n- A scoped concept: its heading links to its scope. Back to [[Getting Started]].\n"),
|
|
18366
|
-
page("Wikilinks", "---\npublic: true\naliases: [Links]\n---\n- `[[Concept]]` links to a page
|
|
18367
|
-
page("Private Notes", "- Not public, so never on the site
|
|
18892
|
+
page("Wikilinks", "---\npublic: true\naliases: [Links]\n---\n- `[[Concept]]` links to a page - an alias like [[Links]] resolves to this one.\n", ["Links"]),
|
|
18893
|
+
page("Private Notes", "- Not public, so never on the site - links to it are styled as missing.\n"),
|
|
18368
18894
|
{
|
|
18369
18895
|
concept: "2026-06-02",
|
|
18370
18896
|
kind: "journal",
|
|
@@ -18587,7 +19113,7 @@ async function customisePublicationTheme(graph, args) {
|
|
|
18587
19113
|
await graph.store.refresh();
|
|
18588
19114
|
const { source } = await graph.publishing.readSource();
|
|
18589
19115
|
const publication = summarisePublishing(source).publications.find((p) => p.id === args.publication.trim());
|
|
18590
|
-
if (!publication) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}"
|
|
19116
|
+
if (!publication) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}" - list_publications shows them.`);
|
|
18591
19117
|
const current = await graph.themes.get(publication.theme);
|
|
18592
19118
|
if (current) return {
|
|
18593
19119
|
created: false,
|
|
@@ -18623,7 +19149,7 @@ async function requireEditable(graph, id) {
|
|
|
18623
19149
|
const own = await graph.themes.get(id);
|
|
18624
19150
|
if (own) return own;
|
|
18625
19151
|
if (bundledTheme(id)) throw new ToolError("theme_not_editable", `"${id}" is a bundled theme and cannot be edited in place. create_theme (or customise_publication_theme for the publication that uses it) makes a copy in the graph that can be.`);
|
|
18626
|
-
throw new ToolError("theme_not_found", `No theme in this graph is called "${id}"
|
|
19152
|
+
throw new ToolError("theme_not_found", `No theme in this graph is called "${id}" - list_themes shows them.`);
|
|
18627
19153
|
}
|
|
18628
19154
|
function requireThemePath(path) {
|
|
18629
19155
|
const clean = path.trim().replace(/^\.?\//, "");
|
|
@@ -18723,7 +19249,7 @@ async function importThemeFolder(graph, args) {
|
|
|
18723
19249
|
} catch (error) {
|
|
18724
19250
|
throw new ToolError("invalid_argument", `Cannot read "${args.dir}": ${error instanceof Error ? error.message : String(error)}`);
|
|
18725
19251
|
}
|
|
18726
|
-
if (!files.has("theme.json")) throw new ToolError("invalid_argument", `"${args.dir}" has no theme.json
|
|
19252
|
+
if (!files.has("theme.json")) throw new ToolError("invalid_argument", `"${args.dir}" has no theme.json - a theme folder has one at its top.`);
|
|
18727
19253
|
const next = {
|
|
18728
19254
|
...theme,
|
|
18729
19255
|
files: Object.fromEntries(files),
|
|
@@ -18745,7 +19271,7 @@ async function deleteTheme(graph, args) {
|
|
|
18745
19271
|
await graph.store.refresh();
|
|
18746
19272
|
const theme = await requireEditable(graph, args.id.trim());
|
|
18747
19273
|
const users = await publicationsUsing(graph, theme.id);
|
|
18748
|
-
if (users.length > 0) throw new ToolError("theme_in_use", `"${theme.id}" is the theme of ${users.map((p) => `"${p.name}"`).join(", ")}
|
|
19274
|
+
if (users.length > 0) throw new ToolError("theme_in_use", `"${theme.id}" is the theme of ${users.map((p) => `"${p.name}"`).join(", ")} - point ${users.length === 1 ? "that publication" : "those publications"} at another theme (update_publication) first.`);
|
|
18749
19275
|
await graph.themes.remove(theme.id);
|
|
18750
19276
|
await settle(graph);
|
|
18751
19277
|
return {
|
|
@@ -18770,7 +19296,7 @@ async function previewTheme(graph, args, host) {
|
|
|
18770
19296
|
if (args.publication?.trim()) {
|
|
18771
19297
|
const read = await graph.publishing.readSource();
|
|
18772
19298
|
const found = summarisePublishing(read.source).publications.find((p) => p.id === args.publication.trim());
|
|
18773
|
-
if (!found) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}"
|
|
19299
|
+
if (!found) throw new ToolError("publication_not_found", `No publication has the id "${args.publication}" - list_publications shows them.`);
|
|
18774
19300
|
publication = found;
|
|
18775
19301
|
source = read.source;
|
|
18776
19302
|
unsettled = read.unsettled;
|
|
@@ -18814,7 +19340,7 @@ async function previewTheme(graph, args, host) {
|
|
|
18814
19340
|
...screenshots ? { screenshots } : {},
|
|
18815
19341
|
...chromium ? {
|
|
18816
19342
|
screenshots: null,
|
|
18817
|
-
note: `No browser to photograph with
|
|
19343
|
+
note: `No browser to photograph with - run ${chromium.setupCommand} or set ETHERPK_CHROMIUM.`
|
|
18818
19344
|
} : {}
|
|
18819
19345
|
};
|
|
18820
19346
|
} finally {
|
|
@@ -18928,25 +19454,26 @@ function createMcpServer(graph, info) {
|
|
|
18928
19454
|
version: info.version
|
|
18929
19455
|
}, { instructions: [
|
|
18930
19456
|
`You are connected to the EtherPK knowledge graph "${info.graphName}": daily journal entries and titled pages in plain markdown, linked with [[wikilinks]].`,
|
|
18931
|
-
"Concept names are case-insensitive. A protected document is listed with protected: true and cannot be read or written here
|
|
18932
|
-
"Bullets are \"- \" with two-space nesting
|
|
18933
|
-
"read_document returns a document's body as text and its frontmatter block as data (the \"frontmatter\" object)
|
|
18934
|
-
"Edit with edit_document (an exact, unique old→new replacement) or append_document
|
|
18935
|
-
"To rename a page or concept use plan_rename then rename, never a frontmatter edit: rename carries scoped concepts (\"[[Old]] Notes\") along and by default rewrites every [[Old]] link in the graph to the new name (strategy \"rewrite\")
|
|
18936
|
-
"search has two kinds of matching: mode \"text\" (default) matches words and quoted phrases
|
|
18937
|
-
"Start with graph_info to see what you are connected to. read_documents reads several documents in one call
|
|
18938
|
-
"
|
|
18939
|
-
"
|
|
18940
|
-
"
|
|
19457
|
+
"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.",
|
|
19458
|
+
"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.",
|
|
19459
|
+
"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.",
|
|
19460
|
+
"Edit with edit_document (an exact, unique old→new replacement) or append_document - read first, then edit, and never paste a whole page back.",
|
|
19461
|
+
"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.",
|
|
19462
|
+
"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.",
|
|
19463
|
+
"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.",
|
|
19464
|
+
"A task reference is how a person hands you one task: its words, then an EtherPK address ending in #task=<line>-<fingerprint>. Pass the whole reference, or the address, to read_task for the task and its detail, to set_task (as reference) to change it, and to add_task_note to write what you did under it. It finds the task after lines are added above it or its tags change; if its words were edited it answers task_not_found, and you should ask the user for a fresh one.",
|
|
19465
|
+
"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\".",
|
|
19466
|
+
"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.",
|
|
19467
|
+
"Images and files are assets: upload_asset adds a file from this machine and returns the markdown to paste into a document. read_asset writes an asset to a local file you can open, and list_assets shows the assets documents reference. An asset is available only where a document you can read references it (or you uploaded it this session). read_asset, read_theme and preview_theme write under the graph's downloads directory and return the path - name a folder relative to it, never elsewhere.",
|
|
18941
19468
|
"The user documentation is at https://docs.etherpk.com."
|
|
18942
19469
|
].join("\n") });
|
|
18943
19470
|
server.registerTool("list_documents", {
|
|
18944
19471
|
title: "List documents",
|
|
18945
|
-
description: `Every page and journal entry in the graph with its aliases
|
|
19472
|
+
description: `Every page and journal entry in the graph with its aliases - protected documents carry protected: true and have no readable body. from / to narrow to the journal entries of a range of days. Paged, at most 200 per call.`,
|
|
18946
19473
|
inputSchema: {
|
|
18947
19474
|
kind: z.enum(["page", "journal"]).optional().describe("Only pages, or only journal entries."),
|
|
18948
|
-
from: z.string().optional().describe("Journal entries on or after this day (YYYY-MM-DD)
|
|
18949
|
-
to: z.string().optional().describe("Journal entries on or before this day (YYYY-MM-DD)
|
|
19475
|
+
from: z.string().optional().describe("Journal entries on or after this day (YYYY-MM-DD) - implies kind journal."),
|
|
19476
|
+
to: z.string().optional().describe("Journal entries on or before this day (YYYY-MM-DD) - implies kind journal."),
|
|
18950
19477
|
offset,
|
|
18951
19478
|
limit: z.number().int().positive().max(200).optional()
|
|
18952
19479
|
}
|
|
@@ -18963,7 +19490,7 @@ function createMcpServer(graph, info) {
|
|
|
18963
19490
|
}, async (args) => run(() => readDocuments(graph, args)));
|
|
18964
19491
|
server.registerTool("search", {
|
|
18965
19492
|
title: "Search",
|
|
18966
|
-
description: `Search every unprotected document. mode "text" (default): words match by prefix, "quoted phrases" match exactly
|
|
19493
|
+
description: `Search every unprotected document. mode "text" (default): words match by prefix, "quoted phrases" match exactly. Documents come most-matches-first with the matching lines. In text mode a term key:value is a property filter on the documents' frontmatter when some document has that key, and keys and values ignore case (key:"two words", -key:value excludes, key:* means the key is set, key:va* matches the start of a value, publication.id:docs reaches a nested key, title: and aliases: match the document's names). Filters alone list every matching document with the values they matched and no hits. mode "semantic": the passages closest in MEANING to the query, best first, each with its text, a similarity 0..1 and where it is (concept, breadcrumb of headings and parent bullets, start and end line) - finds a note that says "self-assessment is due 31 January" for "tax deadline". Line numbers are 0-based in every mode. Passage text is the note's own words with bullet markers and [[ ]] stripped: quote and cite it, but anchor edit_document on text from read_document. Use semantic for questions and descriptions, text for names, identifiers and exact phrases. mode "hybrid" returns both as separate groups. Semantic results carry complete: false while this computer is still embedding the graph (rerun later for more). If semantic mode answers error semantic_unavailable, it is not set up on this computer: tell the user the command in the message. At most 50 documents per call.`,
|
|
18967
19494
|
inputSchema: {
|
|
18968
19495
|
query: z.string().min(1),
|
|
18969
19496
|
mode: z.enum([
|
|
@@ -18982,7 +19509,7 @@ function createMcpServer(graph, info) {
|
|
|
18982
19509
|
}, async (args) => run(() => backlinks(graph, args.concept)));
|
|
18983
19510
|
server.registerTool("tasks", {
|
|
18984
19511
|
title: "Tasks",
|
|
18985
|
-
description: `Tasks ("- [ ]" bullets) across the graph with their tags. Defaults to every unfinished task. A concept filter matches tasks written on that page, linking it, or nested under a block or heading that links it. At most 200 per call.`,
|
|
19512
|
+
description: `Tasks ("- [ ]" bullets) across the graph with their tags. Defaults to every unfinished task. A concept filter matches tasks written on that page or on a page named for it ("[[Concept]] Notes"), linking it, or nested under a block or heading that links it. At most 200 per call.`,
|
|
18986
19513
|
inputSchema: {
|
|
18987
19514
|
concept: concept.optional(),
|
|
18988
19515
|
statuses: z.array(z.enum([
|
|
@@ -19008,12 +19535,26 @@ function createMcpServer(graph, info) {
|
|
|
19008
19535
|
limit: z.number().int().positive().max(200).optional()
|
|
19009
19536
|
}
|
|
19010
19537
|
}, async (args) => run(() => tasks(graph, args)));
|
|
19538
|
+
server.registerTool("read_task", {
|
|
19539
|
+
title: "Read a task",
|
|
19540
|
+
description: "The task a task reference names: its text, status, priority, due and scheduled days, its document and current 0-based line, its breadcrumb (the headings and bullets above it), its detail (its continuation lines and everything nested under it), foundBy (at_line, moved or other_document) and its current reference. Errors: task_not_found (its words were edited or it was deleted), task_ambiguous (two tasks have its words; the message names them), other_graph (the reference is from another graph).",
|
|
19541
|
+
inputSchema: { reference: z.string().min(1).describe("The task reference as the user gave it: the two lines, or the address alone.") }
|
|
19542
|
+
}, async (args) => run(() => readTask(graph, args)));
|
|
19543
|
+
server.registerTool("add_task_note", {
|
|
19544
|
+
title: "Add a note under a task",
|
|
19545
|
+
description: "Add text under the task a task reference names, one level in, after what is already nested there: a line of text becomes a bullet, and bullets are nested as written. Use it to record what you did about the task. Refuses as read_task does when the reference finds no single task.",
|
|
19546
|
+
inputSchema: {
|
|
19547
|
+
reference: z.string().min(1).describe("The task reference as the user gave it."),
|
|
19548
|
+
text: z.string().min(1).describe("Markdown: a line, or \"- \" bullets.")
|
|
19549
|
+
}
|
|
19550
|
+
}, async (args) => run(() => addTaskNote(graph, args)));
|
|
19011
19551
|
server.registerTool("set_task", {
|
|
19012
19552
|
title: "Set a task",
|
|
19013
|
-
description: "Change one task by document and 0-based line (as tasks and read_document count lines): its status (open, doing, waiting, done, cancelled - exclusive), priority (1, 2, 3 or null), due and scheduled days (YYYY-MM-DD or null).
|
|
19553
|
+
description: "Change one task, named by a task reference or 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). By line, 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. A reference finds the task wherever it is now.",
|
|
19014
19554
|
inputSchema: {
|
|
19015
|
-
concept,
|
|
19016
|
-
|
|
19555
|
+
reference: z.string().min(1).optional().describe("A task reference, in place of concept, line and expect."),
|
|
19556
|
+
concept: concept.optional(),
|
|
19557
|
+
line: z.number().int().nonnegative().optional(),
|
|
19017
19558
|
changes: z.object({
|
|
19018
19559
|
status: z.enum([
|
|
19019
19560
|
"open",
|
|
@@ -19036,7 +19577,7 @@ function createMcpServer(graph, info) {
|
|
|
19036
19577
|
}, async (args) => run(() => setTask(graph, args)));
|
|
19037
19578
|
server.registerTool("edit_document", {
|
|
19038
19579
|
title: "Edit a document",
|
|
19039
|
-
description: "Replace one exact occurrence of \"old\" with \"new\" in a document. \"old\" must appear exactly once (include surrounding lines to disambiguate)
|
|
19580
|
+
description: "Replace one exact occurrence of \"old\" with \"new\" in a document. \"old\" must appear exactly once (include surrounding lines to disambiguate). The change is applied as one edit, so on a synced graph it merges with anyone typing elsewhere in the page, and on a folder it is in the file when this returns. Refused on a protected document.",
|
|
19040
19581
|
inputSchema: {
|
|
19041
19582
|
concept,
|
|
19042
19583
|
old: z.string().min(1).describe("The exact text to replace, as read_document returned it."),
|
|
@@ -19070,7 +19611,7 @@ function createMcpServer(graph, info) {
|
|
|
19070
19611
|
}, async (args) => run(() => planRename(graph, args)));
|
|
19071
19612
|
server.registerTool("rename", {
|
|
19072
19613
|
title: "Rename",
|
|
19073
|
-
description: "Rename a page or concept. Its page (if any) and every scoped concept beneath it (\"[[Old]] Notes\" becomes \"[[New]] Notes\") move with it. strategy \"rewrite\" (default) rewrites every [[Old]] link in the graph to the new name
|
|
19614
|
+
description: "Rename a page or concept. Its page (if any) and every scoped concept beneath it (\"[[Old]] Notes\" becomes \"[[New]] Notes\") move with it. strategy \"rewrite\" (default) rewrites every [[Old]] link in the graph to the new name. \"alias\" keeps the old name as an alias so existing links keep resolving. If the new name is already taken the two documents would merge, which is irreversible: refused with error \"merge_requires_confirmation\" unless confirm_merge is true. Refused with \"unconfirmed_documents\" if a document it must read has not finished syncing to this device (nothing changes - try again). Call plan_rename first to see what will happen.",
|
|
19074
19615
|
inputSchema: {
|
|
19075
19616
|
from: z.string().min(1),
|
|
19076
19617
|
to: z.string().min(1),
|
|
@@ -19083,7 +19624,7 @@ function createMcpServer(graph, info) {
|
|
|
19083
19624
|
description: "Replace a document's aliases: other names it answers to in [[wikilinks]], Quick Find and on a published site. The list you pass is the complete list. A name that is already another document's title or alias is refused with error \"name_taken\".",
|
|
19084
19625
|
inputSchema: {
|
|
19085
19626
|
concept,
|
|
19086
|
-
aliases: z.array(z.string()).describe("Every alias the document should have
|
|
19627
|
+
aliases: z.array(z.string()).describe("Every alias the document should have - an empty list removes them all.")
|
|
19087
19628
|
}
|
|
19088
19629
|
}, async (args) => run(() => setAliases(graph, args)));
|
|
19089
19630
|
server.registerTool("set_frontmatter", {
|
|
@@ -19091,23 +19632,23 @@ function createMcpServer(graph, info) {
|
|
|
19091
19632
|
description: "Set keys of a document's frontmatter block, adding the block if there is none. A merge patch: the keys you name are set, null removes a key, every other key keeps its value. \"public\" (true/false) and \"publications\" (a list of publication ids: lower-case letters, digits and hyphens) decide what is published, and are validated. Refuses \"title\", \"aliases\" and \"publication\" with error \"identity_key\" (use rename, set_aliases and the publication tools), and a protected document. Returns the resulting frontmatter.",
|
|
19092
19633
|
inputSchema: {
|
|
19093
19634
|
concept,
|
|
19094
|
-
patch: z.record(z.string(), frontmatterValue).describe("Keys to set
|
|
19635
|
+
patch: z.record(z.string(), frontmatterValue).describe("Keys to set - null removes a key. Example: {\"public\": true, \"publications\": [\"blog\"], \"date\": \"2026-01-31\", \"draft\": null}.")
|
|
19095
19636
|
}
|
|
19096
19637
|
}, async (args) => run(() => setFrontmatter(graph, args)));
|
|
19097
19638
|
server.registerTool("upload_asset", {
|
|
19098
19639
|
title: "Upload an asset",
|
|
19099
|
-
description: "Add a file on this machine (an image, a PDF, any file) to the graph as an asset. Returns the reference and the markdown to paste into a document (an image tag for an image, a link otherwise). Bytes the graph already holds are not stored twice: the result then says reused: true and points at the existing asset. The file is stored as it is, without image optimisation. Refused with error \"invalid_argument\" for a folder, a file over 100 MiB, a hidden file or one in a hidden folder (.ssh, .env), and the Headless Client's own config and cache
|
|
19640
|
+
description: "Add a file on this machine (an image, a PDF, any file) to the graph as an asset. Returns the reference and the markdown to paste into a document (an image tag for an image, a link otherwise). Bytes the graph already holds are not stored twice: the result then says reused: true and points at the existing asset. The file is stored as it is, without image optimisation. Refused with error \"invalid_argument\" for a folder, a file over 100 MiB, a hidden file or one in a hidden folder (.ssh, .env), and the Headless Client's own config and cache. Refused with error \"asset_refused\" when the server declines it on a quota.",
|
|
19100
19641
|
inputSchema: {
|
|
19101
19642
|
path: z.string().min(1).describe("Absolute path of an ordinary file on this machine."),
|
|
19102
|
-
name: z.string().min(1).optional().describe("The name to store it under
|
|
19643
|
+
name: z.string().min(1).optional().describe("The name to store it under - the file's own name by default.")
|
|
19103
19644
|
}
|
|
19104
19645
|
}, async (args) => run(() => uploadAsset(graph, args)));
|
|
19105
19646
|
server.registerTool("read_asset", {
|
|
19106
19647
|
title: "Read an asset",
|
|
19107
|
-
description: "Write an asset's bytes to a file on this machine and return the path, so you can open or view it. Pass the reference as a document shows it (\"../assets/<name>\") or the name alone. Available only where a document you can read references the asset (or you uploaded it this session)
|
|
19648
|
+
description: "Write an asset's bytes to a file on this machine and return the path, so you can open or view it. Pass the reference as a document shows it (\"../assets/<name>\") or the name alone. Available only where a document you can read references the asset (or you uploaded it this session) - otherwise error \"asset_not_found\". The file is written under the graph's downloads directory, named after the asset, and never over an existing file.",
|
|
19108
19649
|
inputSchema: {
|
|
19109
19650
|
ref: z.string().min(1),
|
|
19110
|
-
out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it
|
|
19651
|
+
out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it - the downloads directory itself by default. A folder outside it is refused.")
|
|
19111
19652
|
}
|
|
19112
19653
|
}, async (args) => run(() => readAsset(graph, args)));
|
|
19113
19654
|
server.registerTool("list_assets", {
|
|
@@ -19121,25 +19662,25 @@ function createMcpServer(graph, info) {
|
|
|
19121
19662
|
};
|
|
19122
19663
|
server.registerTool("list_publications", {
|
|
19123
19664
|
title: "List publications",
|
|
19124
|
-
description: "The publications this graph defines - id, name, the page that defines each, kind (docs or blog), selection (named: documents that list it in publications
|
|
19665
|
+
description: "The publications this graph defines - id, name, the page that defines each, kind (docs or blog), selection (named: documents that list it in publications, all-public: every public document), home, url, theme, include slots, and the publish folder set for it on this machine (null when none) - with any issues in their pages and the public documents no publication takes.",
|
|
19125
19666
|
inputSchema: {}
|
|
19126
19667
|
}, async () => run(() => listPublications(graph, host)));
|
|
19127
19668
|
server.registerTool("create_publication", {
|
|
19128
19669
|
title: "Create a publication",
|
|
19129
|
-
description: "Create a publication: a page titled with its name whose frontmatter defines it and whose outline (bullets of [[wikilinks]]) is the site navigation. id defaults to a slug of the title
|
|
19670
|
+
description: "Create a publication: a page titled with its name whose frontmatter defines it and whose outline (bullets of [[wikilinks]]) is the site navigation. id defaults to a slug of the title. kind is docs (default) or blog. selection is named (default: a document joins by listing the id in its publications) or all-public. theme is a bundled theme name (etherpk-docs, etherpk-blog), a theme in the graph, or a url.",
|
|
19130
19671
|
inputSchema: {
|
|
19131
19672
|
title: z.string().min(1),
|
|
19132
19673
|
id: z.string().min(1).optional(),
|
|
19133
19674
|
kind: z.enum(["docs", "blog"]).optional(),
|
|
19134
19675
|
selection: z.enum(["named", "all-public"]).optional(),
|
|
19135
|
-
home: z.string().min(1).optional().describe("The page whose content is the front page
|
|
19676
|
+
home: z.string().min(1).optional().describe("The page whose content is the front page - absent, an index is generated."),
|
|
19136
19677
|
url: z.string().min(1).optional().describe("Where the site will be deployed, for its feed and sitemap."),
|
|
19137
19678
|
theme: z.string().min(1).optional()
|
|
19138
19679
|
}
|
|
19139
19680
|
}, async (args) => run(() => createPublication(graph, args, host)));
|
|
19140
19681
|
server.registerTool("update_publication", {
|
|
19141
19682
|
title: "Update a publication",
|
|
19142
|
-
description: "Change a publication's settings on its page. Each change is optional
|
|
19683
|
+
description: "Change a publication's settings on its page. Each change is optional - null removes a field (back to its default) or clears an include slot. includes maps a theme's slot name (head, logo, header, footer, before, after, scripts, styles, or one the theme adds) to the page that fills it - that page must be public and in the publication.",
|
|
19143
19684
|
inputSchema: {
|
|
19144
19685
|
id: z.string().min(1),
|
|
19145
19686
|
changes: z.object({
|
|
@@ -19148,27 +19689,27 @@ function createMcpServer(graph, info) {
|
|
|
19148
19689
|
home: z.string().nullable().optional(),
|
|
19149
19690
|
url: z.string().nullable().optional(),
|
|
19150
19691
|
theme: z.string().nullable().optional(),
|
|
19151
|
-
recent: z.number().int().nonnegative().nullable().optional().describe("Posts on a blog's front page
|
|
19692
|
+
recent: z.number().int().nonnegative().nullable().optional().describe("Posts on a blog's front page - null for the default (10)."),
|
|
19152
19693
|
includes: z.record(z.string(), z.string().nullable()).optional()
|
|
19153
19694
|
})
|
|
19154
19695
|
}
|
|
19155
19696
|
}, async (args) => run(() => updatePublication(graph, args, host)));
|
|
19156
19697
|
server.registerTool("publish", {
|
|
19157
19698
|
title: "Publish",
|
|
19158
|
-
description: "Render a publication to its publish folder on this machine and return the report: what was included and why documents were left out, missing links, assets, warnings. The folder is the one the user set with \"etherpk-mcp publish --publication <id> --out <dir>\" (error \"no_publish_folder\" until then
|
|
19699
|
+
description: "Render a publication to its publish folder on this machine and return the report: what was included and why documents were left out, missing links, assets, warnings. The folder is the one the user set with \"etherpk-mcp publish --publication <id> --out <dir>\" (error \"no_publish_folder\" until then - the tool never chooses a folder). Pages with Mermaid diagrams need the browser from \"diagrams setup\" (error \"chromium_unavailable\"). The report is this result, trimmed to counts and first entries - nothing of it is written into the folder, so the site never names the documents it leaves out. Publishing writes files - it does not deploy them.",
|
|
19159
19700
|
inputSchema: { id: z.string().min(1) }
|
|
19160
19701
|
}, async (args) => run(() => publish(graph, args, host)));
|
|
19161
19702
|
server.registerTool("list_themes", {
|
|
19162
19703
|
title: "List themes",
|
|
19163
|
-
description: "Every theme a publication here can use: the bundled ones (etherpk-docs
|
|
19704
|
+
description: "Every theme a publication here can use: the bundled ones (etherpk-docs and etherpk-blog, read-only, with their files and manifest) and the graph's own (editable, with id, name, origin, files, manifest, validation errors, and which publications use each).",
|
|
19164
19705
|
inputSchema: {}
|
|
19165
19706
|
}, async () => run(() => listThemes(graph)));
|
|
19166
19707
|
server.registerTool("read_theme", {
|
|
19167
19708
|
title: "Read a theme",
|
|
19168
|
-
description: "Write a theme's files (theme.json, layouts/, partials/, assets/) to a folder on this machine and return the path and file list, so you can read and edit them with your own tools. ref is a graph theme id, a bundled theme name or a url. Editing the folder changes nothing until write_theme_file or import_theme_folder brings it back
|
|
19709
|
+
description: "Write a theme's files (theme.json, layouts/, partials/, assets/) to a folder on this machine and return the path and file list, so you can read and edit them with your own tools. ref is a graph theme id, a bundled theme name or a url. Editing the folder changes nothing until write_theme_file or import_theme_folder brings it back. A bundled or url theme cannot be edited in place at all (create_theme copies it).",
|
|
19169
19710
|
inputSchema: {
|
|
19170
19711
|
ref: z.string().min(1),
|
|
19171
|
-
out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it
|
|
19712
|
+
out_dir: z.string().min(1).optional().describe("A folder under the graph's downloads directory, relative to it - themes/<ref> there by default. A folder outside it is refused.")
|
|
19172
19713
|
}
|
|
19173
19714
|
}, async (args) => run(() => readTheme(graph, args)));
|
|
19174
19715
|
server.registerTool("read_theme_file", {
|
|
@@ -19181,19 +19722,19 @@ function createMcpServer(graph, info) {
|
|
|
19181
19722
|
}, async (args) => run(() => readThemeFile(graph, args)));
|
|
19182
19723
|
server.registerTool("create_theme", {
|
|
19183
19724
|
title: "Create a theme",
|
|
19184
|
-
description: "Copy a theme into the graph as one of its own, ready to edit: from a bundled theme name, a url, or another theme in the graph. Returns the new theme
|
|
19725
|
+
description: "Copy a theme into the graph as one of its own, ready to edit: from a bundled theme name, a url, or another theme in the graph. Returns the new theme - point a publication at it with update_publication (theme: <id>).",
|
|
19185
19726
|
inputSchema: {
|
|
19186
19727
|
from: z.string().min(1),
|
|
19187
|
-
id: z.string().min(1).optional().describe("Lower-case letters, digits and hyphens
|
|
19728
|
+
id: z.string().min(1).optional().describe("Lower-case letters, digits and hyphens - from the source by default."),
|
|
19188
19729
|
name: z.string().min(1).optional()
|
|
19189
19730
|
}
|
|
19190
19731
|
}, async (args) => run(() => createTheme(graph, args)));
|
|
19191
19732
|
server.registerTool("customise_publication_theme", {
|
|
19192
19733
|
title: "Customise a publication's theme",
|
|
19193
|
-
description: "Make a publication's theme editable: if it uses a bundled or url theme, copy that theme into the graph and point the publication at the copy
|
|
19734
|
+
description: "Make a publication's theme editable: if it uses a bundled or url theme, copy that theme into the graph and point the publication at the copy. If it already uses a graph theme, return it. The step before editing the look of a published site.",
|
|
19194
19735
|
inputSchema: {
|
|
19195
19736
|
publication: z.string().min(1).describe("The publication id."),
|
|
19196
|
-
id: z.string().min(1).optional().describe("The id for the copy
|
|
19737
|
+
id: z.string().min(1).optional().describe("The id for the copy - <publication>-theme by default."),
|
|
19197
19738
|
name: z.string().min(1).optional()
|
|
19198
19739
|
}
|
|
19199
19740
|
}, async (args) => run(() => customisePublicationTheme(graph, args)));
|
|
@@ -19208,7 +19749,7 @@ function createMcpServer(graph, info) {
|
|
|
19208
19749
|
}, async (args) => run(() => writeThemeFile(graph, args)));
|
|
19209
19750
|
server.registerTool("delete_theme_file", {
|
|
19210
19751
|
title: "Delete a theme file",
|
|
19211
|
-
description: "Remove one file from a graph theme
|
|
19752
|
+
description: "Remove one file from a graph theme - the theme is validated afterwards and problems returned as errors.",
|
|
19212
19753
|
inputSchema: {
|
|
19213
19754
|
id: z.string().min(1),
|
|
19214
19755
|
path: z.string().min(1)
|
|
@@ -19216,7 +19757,7 @@ function createMcpServer(graph, info) {
|
|
|
19216
19757
|
}, async (args) => run(() => deleteThemeFile(graph, args)));
|
|
19217
19758
|
server.registerTool("import_theme_folder", {
|
|
19218
19759
|
title: "Import a theme folder",
|
|
19219
|
-
description: "Replace a graph theme's files with a folder's contents - the way back after editing what read_theme wrote. Files the folder no longer has are removed from the theme
|
|
19760
|
+
description: "Replace a graph theme's files with a folder's contents - the way back after editing what read_theme wrote. Files the folder no longer has are removed from the theme - the folder needs a theme.json at its top. dir is the folder read_theme returned, or another under the graph's downloads directory - a folder outside it is refused, and symbolic links in it are skipped. Validated afterwards.",
|
|
19220
19761
|
inputSchema: {
|
|
19221
19762
|
id: z.string().min(1),
|
|
19222
19763
|
dir: z.string().min(1)
|
|
@@ -19224,12 +19765,12 @@ function createMcpServer(graph, info) {
|
|
|
19224
19765
|
}, async (args) => run(() => importThemeFolder(graph, args)));
|
|
19225
19766
|
server.registerTool("delete_theme", {
|
|
19226
19767
|
title: "Delete a theme",
|
|
19227
|
-
description: "Remove a graph theme. Refused with error \"theme_in_use\" while a publication's saved settings name it
|
|
19768
|
+
description: "Remove a graph theme. Refused with error \"theme_in_use\" while a publication's saved settings name it - point the publication at another theme first.",
|
|
19228
19769
|
inputSchema: { id: z.string().min(1) }
|
|
19229
19770
|
}, async (args) => run(() => deleteTheme(graph, args)));
|
|
19230
19771
|
server.registerTool("preview_theme", {
|
|
19231
19772
|
title: "Preview a theme",
|
|
19232
|
-
description: "Render a theme to a folder on this machine and return where: a publication's real pages when a publication is given (with its own theme, or the theme named), or a sample site covering every construct when only a theme is given. Open the HTML to check markup and styles. With screenshots: true and a browser set up (diagrams setup or ETHERPK_CHROMIUM), the front page and one content page are photographed at desktop and phone widths as PNGs you can view. A preview is scratch, not the publish folder. The page loads nothing from the network while it is photographed. out_dir is a folder under the graph's downloads directory (previews/<name> there by default)
|
|
19773
|
+
description: "Render a theme to a folder on this machine and return where: a publication's real pages when a publication is given (with its own theme, or the theme named), or a sample site covering every construct when only a theme is given. Open the HTML to check markup and styles. With screenshots: true and a browser set up (diagrams setup or ETHERPK_CHROMIUM), the front page and one content page are photographed at desktop and phone widths as PNGs you can view. A preview is scratch, not the publish folder. The page loads nothing from the network while it is photographed. out_dir is a folder under the graph's downloads directory (previews/<name> there by default) - a folder outside it is refused.",
|
|
19233
19774
|
inputSchema: {
|
|
19234
19775
|
theme: z.string().min(1).optional(),
|
|
19235
19776
|
publication: z.string().min(1).optional(),
|
|
@@ -19465,20 +20006,20 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
19465
20006
|
Serve one synced graph to an agent over stdio. For Claude Code:
|
|
19466
20007
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --sync-server <url> --graph <id>
|
|
19467
20008
|
Once "semantic setup" has run on this computer, serve also keeps a search-by-meaning
|
|
19468
|
-
store of the graph current (the agent's search tool gains mode: semantic)
|
|
20009
|
+
store of the graph current (the agent's search tool gains mode: semantic) - pass
|
|
19469
20010
|
--no-semantic to leave it off for this registration.
|
|
19470
20011
|
${CMD} serve --folder <path> [--no-semantic]
|
|
19471
20012
|
Serve a local graph folder the same way: no sign-in, no server. The agent gets search,
|
|
19472
20013
|
backlinks, tasks and format-safe edits over the folder's markdown, alongside the files
|
|
19473
20014
|
themselves. Edits made in an editor or by the agent directly are picked up as they land.
|
|
19474
|
-
The folder must already be a graph (open it in EtherPK once)
|
|
20015
|
+
The folder must already be a graph (open it in EtherPK once) - its index is kept under
|
|
19475
20016
|
the cache directory, never in the folder.
|
|
19476
20017
|
${CMD} logout [--sync-server <url> | --all]
|
|
19477
20018
|
Forget that server's token, keys and cached graphs on this machine.
|
|
19478
20019
|
${CMD} semantic setup
|
|
19479
20020
|
Let the agent search by meaning. Installs a ~300 MB native runtime (onnxruntime-node,
|
|
19480
20021
|
with your npm) and downloads a 23 MB embedding model into the cache directory, once
|
|
19481
|
-
per computer. Nothing leaves this computer when the model runs
|
|
20022
|
+
per computer. Nothing leaves this computer when the model runs - notes are never sent
|
|
19482
20023
|
to a service to be embedded.
|
|
19483
20024
|
${CMD} semantic status | remove
|
|
19484
20025
|
Whether it is set up here, or delete it (logout --all deletes it too).
|
|
@@ -19488,20 +20029,20 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
19488
20029
|
then on this command without --out, and the agent's publish tool, write to the same
|
|
19489
20030
|
folder (the agent cannot choose one). The folder's owned files (.html at the top,
|
|
19490
20031
|
assets/, theme/, the search, feed and report files) are rewritten and their strays
|
|
19491
|
-
removed
|
|
20032
|
+
removed - everything else in it is left alone. Publishing writes files - deploying them
|
|
19492
20033
|
(a git push, say) is yours. Run it from cron for a site that republishes itself.
|
|
19493
20034
|
${CMD} diagrams setup
|
|
19494
20035
|
Let a publish draw Mermaid diagrams. Downloads a Chromium (about 170 MB) into the cache
|
|
19495
|
-
directory with Playwright's installer, once per computer
|
|
20036
|
+
directory with Playwright's installer, once per computer - a publish whose pages hold
|
|
19496
20037
|
diagrams refuses until this has run. Set ETHERPK_CHROMIUM=<path> to use a browser
|
|
19497
20038
|
already on this machine instead (NixOS needs this).
|
|
19498
20039
|
${CMD} diagrams status
|
|
19499
20040
|
Which browser a publish would use, if any.
|
|
19500
20041
|
|
|
19501
|
-
This machine can hold logins for several Sync Servers at once
|
|
19502
|
-
a command means, and can be left out while there is only one. The config file is
|
|
20042
|
+
This machine can hold logins for several Sync Servers at once. The --sync-server flag says
|
|
20043
|
+
which one a command means, and can be left out while there is only one. The config file is
|
|
19503
20044
|
${defaultConfigPath()} (override with
|
|
19504
|
-
ETHERPK_MCP_CONFIG)
|
|
20045
|
+
ETHERPK_MCP_CONFIG) - cached graphs live under ~/.cache/etherpk/mcp (override with
|
|
19505
20046
|
ETHERPK_MCP_CACHE_DIR).
|
|
19506
20047
|
Docs: https://docs.etherpk.com/using-ai-agents-with-your-notes
|
|
19507
20048
|
`;
|
|
@@ -19514,7 +20055,7 @@ function fail(message) {
|
|
|
19514
20055
|
* an agent starting the process), since each question has its own option or variable.
|
|
19515
20056
|
*/
|
|
19516
20057
|
async function ask(question, { secret = false, hint }) {
|
|
19517
|
-
if (!process.stdin.isTTY) fail(`${question.replace(/:\s*$/, "")}: no terminal to ask on
|
|
20058
|
+
if (!process.stdin.isTTY) fail(`${question.replace(/:\s*$/, "")}: no terminal to ask on - ${hint}.`);
|
|
19518
20059
|
if (!secret) {
|
|
19519
20060
|
const rl = createInterface({
|
|
19520
20061
|
input: process.stdin,
|
|
@@ -19553,7 +20094,7 @@ async function ask(question, { secret = false, hint }) {
|
|
|
19553
20094
|
}
|
|
19554
20095
|
async function loadConfig(path) {
|
|
19555
20096
|
const config = await readConfig(path);
|
|
19556
|
-
if (!config) fail(`${path} is not a config file this version understands. Run: ${CMD} login --sync-server <url> (it will be rewritten
|
|
20097
|
+
if (!config) fail(`${path} is not a config file this version understands. Run: ${CMD} login --sync-server <url> (it will be rewritten - nothing else is affected).`);
|
|
19557
20098
|
return config;
|
|
19558
20099
|
}
|
|
19559
20100
|
/** The login a command means, or the reason there is none - in words the user can act on. */
|
|
@@ -19594,7 +20135,7 @@ async function login(args) {
|
|
|
19594
20135
|
await writeConfig(path, config);
|
|
19595
20136
|
console.log(`Keys unlocked and cached in ${path} (owner-only). Anyone who can read your files on this machine can read this account, as with a signed-in browser.`);
|
|
19596
20137
|
const others = Object.keys(config.servers).filter((server) => server !== syncServer);
|
|
19597
|
-
if (others.length > 0) console.log(`Also logged in to ${others.join(", ")}
|
|
20138
|
+
if (others.length > 0) console.log(`Also logged in to ${others.join(", ")} - commands now need --sync-server <url> to say which.`);
|
|
19598
20139
|
await listGraphs({
|
|
19599
20140
|
syncServer,
|
|
19600
20141
|
pat,
|
|
@@ -19650,7 +20191,7 @@ async function approveOrFallBack(account, byRecoveryCode) {
|
|
|
19650
20191
|
process.exit(controls.exitCode);
|
|
19651
20192
|
}
|
|
19652
20193
|
if (!abandoned) throw new Error("unreachable");
|
|
19653
|
-
console.log("Approval cancelled
|
|
20194
|
+
console.log("Approval cancelled - unlocking with your Recovery Code instead.");
|
|
19654
20195
|
return byRecoveryCode();
|
|
19655
20196
|
}
|
|
19656
20197
|
/**
|
|
@@ -19719,7 +20260,7 @@ async function semanticCommand(what) {
|
|
|
19719
20260
|
const status = await semanticSetupStatus(process.env);
|
|
19720
20261
|
console.log(`Runtime: ${status.runtime ? "installed" : "missing"} (${status.runtimeDir})`);
|
|
19721
20262
|
console.log(`Model: ${status.model ? "present" : "missing"} (${status.modelDir})`);
|
|
19722
|
-
console.log(status.runtime && status.model ? "Semantic search is set up
|
|
20263
|
+
console.log(status.runtime && status.model ? "Semantic search is set up - serve uses it unless started with --no-semantic." : `Not set up. Run: ${CMD} semantic setup`);
|
|
19723
20264
|
const stores = await listGraphStores(process.env, MODEL.id);
|
|
19724
20265
|
if (stores.length === 0) return;
|
|
19725
20266
|
console.log("");
|
|
@@ -19778,7 +20319,7 @@ async function publishCommand(args) {
|
|
|
19778
20319
|
const out = resolve(args.out.trim());
|
|
19779
20320
|
await writePublishFolders(configPath, withPublishFolder(await readPublishFolders(configPath), key, publication, out));
|
|
19780
20321
|
console.error(`etherpk-mcp: publish folder for "${publication}" of "${graphName}" set to ${out} (remembered in ${configPath}).`);
|
|
19781
|
-
} else if (!publishFolderOf(await readPublishFolders(configPath), key, publication)) fail(`No publish folder is set for "${publication}" of "${graphName}" on this machine. Pass --out <dir> once
|
|
20322
|
+
} else if (!publishFolderOf(await readPublishFolders(configPath), key, publication)) fail(`No publish folder is set for "${publication}" of "${graphName}" on this machine. Pass --out <dir> once - it is remembered.`);
|
|
19782
20323
|
const result = await publish(graph, { id: publication }, host);
|
|
19783
20324
|
console.log(JSON.stringify(cliPublishOutput(result), null, 2));
|
|
19784
20325
|
if (!result.ok) process.exitCode = 1;
|
|
@@ -19851,7 +20392,7 @@ async function openFolderForServe(folder, args) {
|
|
|
19851
20392
|
onSemanticProgress: reportSemanticProgress,
|
|
19852
20393
|
onError: (error) => console.error(`etherpk-mcp: ${error.message}`),
|
|
19853
20394
|
onWarning: (line) => console.error(`etherpk-mcp: ${line}`),
|
|
19854
|
-
watch: watchFolder(path, (error) => console.error(`etherpk-mcp: not watching the folder for changes (${error.message})
|
|
20395
|
+
watch: watchFolder(path, (error) => console.error(`etherpk-mcp: not watching the folder for changes (${error.message}) - edits made outside are still picked up before each tool call.`))
|
|
19855
20396
|
}),
|
|
19856
20397
|
graphName: name
|
|
19857
20398
|
};
|
|
@@ -19887,6 +20428,7 @@ async function openSyncedForServe(wanted, args) {
|
|
|
19887
20428
|
keyring,
|
|
19888
20429
|
relayUrl: account.relayUrl,
|
|
19889
20430
|
token: account.tokenFor(graphId),
|
|
20431
|
+
clientUrl: account.clientUrl,
|
|
19890
20432
|
presenceName: `Agent on ${hostname()}`,
|
|
19891
20433
|
readyTimeoutMs: 2e4,
|
|
19892
20434
|
persistDir,
|
|
@@ -19923,7 +20465,7 @@ async function serveGraph(graph, graphName, args) {
|
|
|
19923
20465
|
transport.onclose = listener;
|
|
19924
20466
|
},
|
|
19925
20467
|
async shutdown(end) {
|
|
19926
|
-
console.error(`etherpk-mcp: ${end}
|
|
20468
|
+
console.error(`etherpk-mcp: ${end} - flushing and exiting.`);
|
|
19927
20469
|
await graph.settle().catch(() => {});
|
|
19928
20470
|
await graph.dispose().catch(() => {});
|
|
19929
20471
|
process.exit(0);
|
|
@@ -19933,17 +20475,17 @@ async function serveGraph(graph, graphName, args) {
|
|
|
19933
20475
|
console.error(`etherpk-mcp: serving "${graphName}" over stdio as "Agent on ${hostname()}".`);
|
|
19934
20476
|
chromiumStatus(process.env, CMD).then((chromium) => {
|
|
19935
20477
|
if (chromium.executable) return;
|
|
19936
|
-
console.error(`etherpk-mcp: no browser for diagrams (run: ${chromium.setupCommand}, or set ETHERPK_CHROMIUM)
|
|
20478
|
+
console.error(`etherpk-mcp: no browser for diagrams (run: ${chromium.setupCommand}, or set ETHERPK_CHROMIUM) - a publish or theme preview with Mermaid diagrams refuses until then.`);
|
|
19937
20479
|
});
|
|
19938
20480
|
const startSemantic = () => graph.semantic().then(async (semantic) => {
|
|
19939
20481
|
const status = await semantic.status();
|
|
19940
|
-
console.error(`etherpk-mcp: semantic search on (${semantic.model.id})
|
|
20482
|
+
console.error(`etherpk-mcp: semantic search on (${semantic.model.id}) - ${status.embedded} of ${status.total} passages embedded, building the rest in the background.`);
|
|
19941
20483
|
}).catch((error) => console.error(`etherpk-mcp: semantic search unavailable: ${error instanceof Error ? error.message : String(error)}`));
|
|
19942
20484
|
if (semanticOn) startSemantic();
|
|
19943
20485
|
else if (!args["no-semantic"]) {
|
|
19944
|
-
console.error(`etherpk-mcp: semantic search is not set up on this computer (run: ${CMD} semantic setup
|
|
20486
|
+
console.error(`etherpk-mcp: semantic search is not set up on this computer (run: ${CMD} semantic setup - this process will notice within half a minute, no restart needed). Search answers by text only until then.`);
|
|
19945
20487
|
whenSemanticSetUp(process.env, () => {
|
|
19946
|
-
console.error("etherpk-mcp: semantic setup found
|
|
20488
|
+
console.error("etherpk-mcp: semantic setup found - loading the model and building the store.");
|
|
19947
20489
|
startSemantic();
|
|
19948
20490
|
});
|
|
19949
20491
|
}
|