@appsoftwareltd/etherpk-mcp 0.9.6 → 0.10.1
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 +22 -5
- package/dist/main.js +361 -81
- package/dist/main.js.map +1 -1
- package/dist/map-picture-page.js +73 -72
- package/package.json +5 -5
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { availableParallelism, homedir, hostname, tmpdir } from "node:os";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import nodePath, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { access, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
|
7
7
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
@@ -717,7 +717,7 @@ function looksLikeAgentSetupCode(value) {
|
|
|
717
717
|
}
|
|
718
718
|
var package_default = {
|
|
719
719
|
name: "@appsoftwareltd/etherpk-mcp",
|
|
720
|
-
version: "0.
|
|
720
|
+
version: "0.10.1",
|
|
721
721
|
license: "Elastic-2.0",
|
|
722
722
|
description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
|
|
723
723
|
type: "module",
|
|
@@ -4162,9 +4162,49 @@ function parseBlocks(markdown) {
|
|
|
4162
4162
|
return roots;
|
|
4163
4163
|
}
|
|
4164
4164
|
//#endregion
|
|
4165
|
+
//#region ../client/src/lib/document/extension-fence.ts
|
|
4166
|
+
/**
|
|
4167
|
+
* [[Extension Fence]]s (ADR 0138): a fence whose info word is `<extension id>.<name>` holds an
|
|
4168
|
+
* extension's data, such as a drawing's elements. Every reader of a document decides from the info
|
|
4169
|
+
* word alone, with no extension running, so the editor, the index, Semantic Search, the publisher
|
|
4170
|
+
* and the Headless Client always agree: the body is never read as writing, never searched, and never
|
|
4171
|
+
* published as code.
|
|
4172
|
+
*
|
|
4173
|
+
* Framework-free, so the index worker, the publisher and the Headless Client read it as the editor
|
|
4174
|
+
* does.
|
|
4175
|
+
*/
|
|
4176
|
+
/** An extension's id or a name under it: lowercase letters, digits and hyphens, from a letter. */
|
|
4177
|
+
var NAME = "[a-z][a-z0-9-]*";
|
|
4178
|
+
var EXTENSION_FENCE_INFO = new RegExp(`^${NAME}\\.${NAME}$`);
|
|
4179
|
+
/** Whether an info word names an Extension Fence: an extension's id, a dot and a name. */
|
|
4180
|
+
function isExtensionFenceInfo(info) {
|
|
4181
|
+
return EXTENSION_FENCE_INFO.test(info);
|
|
4182
|
+
}
|
|
4183
|
+
/** The extension an Extension Fence's info word names: the part before the dot. */
|
|
4184
|
+
function extensionOfFence(info) {
|
|
4185
|
+
return info.slice(0, info.indexOf("."));
|
|
4186
|
+
}
|
|
4187
|
+
/** Each complete, outermost Extension Fence in `lines`, by its opener's and closer's 0-based lines. */
|
|
4188
|
+
function extensionFences(lines) {
|
|
4189
|
+
const out = [];
|
|
4190
|
+
const { sorted, parent } = fencedBlockTree(fencedBlocks(lines));
|
|
4191
|
+
sorted.forEach((block, i) => {
|
|
4192
|
+
if (parent[i] !== -1) return;
|
|
4193
|
+
const info = fenceLineInfo(lines[block.start])?.info ?? "";
|
|
4194
|
+
if (isExtensionFenceInfo(info)) out.push({
|
|
4195
|
+
start: block.start,
|
|
4196
|
+
end: block.end,
|
|
4197
|
+
info
|
|
4198
|
+
});
|
|
4199
|
+
});
|
|
4200
|
+
return out;
|
|
4201
|
+
}
|
|
4202
|
+
//#endregion
|
|
4165
4203
|
//#region ../client/src/lib/document/map-text.ts
|
|
4166
4204
|
var NUMBER = String.raw`[+-]?(?:\d+(?:\.\d*)?|\.\d+)`;
|
|
4167
4205
|
var POINT = new RegExp(String.raw`^\s*(${NUMBER})\s*,\s*(${NUMBER})\s*$`);
|
|
4206
|
+
/** How many of the last `@`s on a line may end its name (`readMapLine`). */
|
|
4207
|
+
var MAX_NAME_ENDS = 16;
|
|
4168
4208
|
function readPoint(text) {
|
|
4169
4209
|
const match = POINT.exec(text);
|
|
4170
4210
|
if (!match) return null;
|
|
@@ -4214,7 +4254,8 @@ function readLocation(text, name) {
|
|
|
4214
4254
|
function readMapLine(text) {
|
|
4215
4255
|
const trimmed = text.trim();
|
|
4216
4256
|
if (trimmed === "") return null;
|
|
4217
|
-
|
|
4257
|
+
let tries = 0;
|
|
4258
|
+
for (let at = trimmed.lastIndexOf("@"); at >= 0 && tries < MAX_NAME_ENDS; at = at === 0 ? -1 : trimmed.lastIndexOf("@", at - 1), tries++) {
|
|
4218
4259
|
const line = readLocation(trimmed.slice(at + 1), trimmed.slice(0, at).trim());
|
|
4219
4260
|
if (line) return line;
|
|
4220
4261
|
}
|
|
@@ -4528,30 +4569,57 @@ function deriveDoc(text) {
|
|
|
4528
4569
|
const candidateTasks = blocks.filter((b) => b.kind === "task");
|
|
4529
4570
|
const fenced = candidateTasks.length === 0 ? () => false : fencedLines(text);
|
|
4530
4571
|
const taskBlocks = candidateTasks.filter((b) => !fenced(b.startLine));
|
|
4572
|
+
const tasks = taskBlocks.map((b) => {
|
|
4573
|
+
const tags = parseTaskTags(b.label);
|
|
4574
|
+
return {
|
|
4575
|
+
blockLocalId: b.localId,
|
|
4576
|
+
done: b.done ?? false,
|
|
4577
|
+
text: b.label,
|
|
4578
|
+
line: b.startLine,
|
|
4579
|
+
priority: tags.priority,
|
|
4580
|
+
waiting: tags.waiting,
|
|
4581
|
+
doing: tags.doing,
|
|
4582
|
+
cancelled: tags.cancelled,
|
|
4583
|
+
due: tags.due,
|
|
4584
|
+
completion: tags.completion,
|
|
4585
|
+
scheduled: tags.scheduled
|
|
4586
|
+
};
|
|
4587
|
+
});
|
|
4588
|
+
const maps = deriveMaps(text, blocks, links, lineToBlock);
|
|
4531
4589
|
return {
|
|
4532
4590
|
blocks,
|
|
4533
4591
|
links,
|
|
4534
|
-
tasks
|
|
4535
|
-
const tags = parseTaskTags(b.label);
|
|
4536
|
-
return {
|
|
4537
|
-
blockLocalId: b.localId,
|
|
4538
|
-
done: b.done ?? false,
|
|
4539
|
-
text: b.label,
|
|
4540
|
-
line: b.startLine,
|
|
4541
|
-
priority: tags.priority,
|
|
4542
|
-
waiting: tags.waiting,
|
|
4543
|
-
doing: tags.doing,
|
|
4544
|
-
cancelled: tags.cancelled,
|
|
4545
|
-
due: tags.due,
|
|
4546
|
-
completion: tags.completion,
|
|
4547
|
-
scheduled: tags.scheduled
|
|
4548
|
-
};
|
|
4549
|
-
}),
|
|
4592
|
+
tasks,
|
|
4550
4593
|
taskConcepts: deriveTaskConcepts(blocks, links, taskBlocks),
|
|
4551
|
-
...
|
|
4594
|
+
...maps,
|
|
4595
|
+
searchText: withoutExtensionFences(text, blocks, lineToBlock, maps.searchText)
|
|
4552
4596
|
};
|
|
4553
4597
|
}
|
|
4554
4598
|
/**
|
|
4599
|
+
* What search and Semantic Search read for each block holding an [[Extension Fence]] (ADR 0138):
|
|
4600
|
+
* the block's text with the fence's lines left out, so an extension's data (a drawing's elements)
|
|
4601
|
+
* never fills either index. Added to what `deriveMaps` gave, block by block.
|
|
4602
|
+
*/
|
|
4603
|
+
function withoutExtensionFences(text, blocks, lineToBlock, searchText) {
|
|
4604
|
+
if (!/```[a-z][a-z0-9-]*\.[a-z]/.test(text)) return searchText;
|
|
4605
|
+
const fences = extensionFences(text.split("\n"));
|
|
4606
|
+
if (fences.length === 0) return searchText;
|
|
4607
|
+
const hidden = /* @__PURE__ */ new Map();
|
|
4608
|
+
for (const fence of fences) {
|
|
4609
|
+
const id = lineToBlock.get(fence.start);
|
|
4610
|
+
if (id === void 0) continue;
|
|
4611
|
+
const lines = hidden.get(id) ?? /* @__PURE__ */ new Set();
|
|
4612
|
+
for (let line = fence.start; line <= fence.end; line++) lines.add(line);
|
|
4613
|
+
hidden.set(id, lines);
|
|
4614
|
+
}
|
|
4615
|
+
for (const [id, lines] of hidden) {
|
|
4616
|
+
const block = blocks[id];
|
|
4617
|
+
const read = (searchText.get(id) ?? block.text).split("\n").map((line, n) => lines.has(block.startLine + n) ? "" : line);
|
|
4618
|
+
searchText.set(id, read.join("\n"));
|
|
4619
|
+
}
|
|
4620
|
+
return searchText;
|
|
4621
|
+
}
|
|
4622
|
+
/**
|
|
4555
4623
|
* The [[Place]]s and [[Route]]s a document's [[Map Block]]s hold (ADR 0118), the [[Block
|
|
4556
4624
|
* Concept]]s of the blocks they sit in, and what text search reads for those blocks instead of
|
|
4557
4625
|
* their coordinates.
|
|
@@ -12651,6 +12719,13 @@ function wikilinkRule(md, resolve) {
|
|
|
12651
12719
|
* grammars for code), so they are pre-rendered by the orchestrator and handed in as lookups;
|
|
12652
12720
|
* the renderer itself is synchronous and pure over its inputs.
|
|
12653
12721
|
*/
|
|
12722
|
+
/** A fence an extension published, as HTML the publisher writes: the extension never writes markup into a site. */
|
|
12723
|
+
function fenceOutputHtml(output) {
|
|
12724
|
+
if (output.kind === "picture") return `<figure class="diagram diagram-extension"><img src="${escapeHtml$3(output.src)}" alt="${escapeHtml$3(output.alt)}" width="${output.width}" height="${output.height}" loading="lazy"></figure>\n`;
|
|
12725
|
+
const size = (image) => image.width && image.height ? ` width="${Math.round(image.width)}" height="${Math.round(image.height)}"` : "";
|
|
12726
|
+
const image = output.image ? `<img src="${escapeHtml$3(output.image.src)}" alt="${escapeHtml$3(output.image.alt)}"${size(output.image)} loading="lazy">` : "";
|
|
12727
|
+
return `<figure class="diagram diagram-link"><a href="${escapeHtml$3(output.href)}">${image}</a><figcaption><a href="${escapeHtml$3(output.href)}">${escapeHtml$3(output.text)}</a></figcaption></figure>\n`;
|
|
12728
|
+
}
|
|
12654
12729
|
/** The names on a Map Block, one line for the search index and the excerpt; never its coordinates. */
|
|
12655
12730
|
function mapNames(source) {
|
|
12656
12731
|
return readMapBody(source.split("\n")).items.map((item) => item.name).filter((name) => name !== "").join(", ");
|
|
@@ -12698,7 +12773,7 @@ function plainText(tokens) {
|
|
|
12698
12773
|
if (info === "map") {
|
|
12699
12774
|
const names = mapNames(token.content);
|
|
12700
12775
|
if (names !== "") lines.push(names);
|
|
12701
|
-
} else if (info !== "mermaid" && info !== "math") lines.push(token.content.trim());
|
|
12776
|
+
} else if (info !== "mermaid" && info !== "math" && !isExtensionFenceInfo(info)) lines.push(token.content.trim());
|
|
12702
12777
|
}
|
|
12703
12778
|
return lines.join("\n");
|
|
12704
12779
|
}
|
|
@@ -12777,6 +12852,9 @@ function createDocumentRenderer(options) {
|
|
|
12777
12852
|
if (!picture) return "";
|
|
12778
12853
|
return `<figure class="map"><img src="${escapeHtml$3(picture.src)}" alt="${escapeHtml$3(picture.alt)}" width="${picture.width}" height="${picture.height}" loading="lazy"></figure>\n`;
|
|
12779
12854
|
}
|
|
12855
|
+
const output = options.fenceOutput?.(lang, code);
|
|
12856
|
+
if (output) return fenceOutputHtml(output);
|
|
12857
|
+
if (output === null || isExtensionFenceInfo(lang)) return "";
|
|
12780
12858
|
const highlighted = options.highlighted?.(lang, code);
|
|
12781
12859
|
return `<pre><code${lang === "" ? "" : ` class="language-${escapeHtml$3(lang)}"`}>${highlighted ?? escapeHtml$3(code)}</code></pre>\n`;
|
|
12782
12860
|
};
|
|
@@ -13229,6 +13307,80 @@ async function mapPictureName(source) {
|
|
|
13229
13307
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${MAP_PICTURE_VERSION}\n${source}`));
|
|
13230
13308
|
return [...new Uint8Array(digest)].slice(0, 8).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
13231
13309
|
}
|
|
13310
|
+
/** A fence's body as lines, as an extension's widget reads it: markdown-it's content ends with a line break. */
|
|
13311
|
+
function bodyLines(code) {
|
|
13312
|
+
const lines = code.split("\n");
|
|
13313
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
13314
|
+
return lines;
|
|
13315
|
+
}
|
|
13316
|
+
var PICTURE_EXTENSIONS = {
|
|
13317
|
+
"image/svg+xml": "svg",
|
|
13318
|
+
"image/png": "png",
|
|
13319
|
+
"image/webp": "webp"
|
|
13320
|
+
};
|
|
13321
|
+
/** An `https` address as written, or null for any other. */
|
|
13322
|
+
function httpsAddress(value) {
|
|
13323
|
+
if (typeof value !== "string") return null;
|
|
13324
|
+
try {
|
|
13325
|
+
return new URL(value).protocol === "https:" ? value : null;
|
|
13326
|
+
} catch {
|
|
13327
|
+
return null;
|
|
13328
|
+
}
|
|
13329
|
+
}
|
|
13330
|
+
/**
|
|
13331
|
+
* What an extension's `publish` answered for a fence, checked, its picture kept for the site under
|
|
13332
|
+
* a name taken from its bytes: a picture, a link (`https` only, as is any picture in it), null when
|
|
13333
|
+
* it is left out, or undefined when it could not be made, after which an ordinary fence is published
|
|
13334
|
+
* as code and an Extension Fence left out, with a warning either way.
|
|
13335
|
+
*/
|
|
13336
|
+
async function publishedFence(asked, info, concept, pictures, warnings) {
|
|
13337
|
+
try {
|
|
13338
|
+
const out = await asked;
|
|
13339
|
+
if (out === null) return null;
|
|
13340
|
+
if (out.kind === "picture") {
|
|
13341
|
+
const extension = PICTURE_EXTENSIONS[out.type];
|
|
13342
|
+
if (!(out.bytes instanceof Uint8Array) || !extension) throw new Error("its picture is not an SVG, PNG or WebP file");
|
|
13343
|
+
const digest = await crypto.subtle.digest("SHA-256", out.bytes);
|
|
13344
|
+
const path = `pictures/${[...new Uint8Array(digest)].slice(0, 8).map((byte) => byte.toString(16).padStart(2, "0")).join("")}.${extension}`;
|
|
13345
|
+
pictures.set(path, out.bytes);
|
|
13346
|
+
return {
|
|
13347
|
+
kind: "picture",
|
|
13348
|
+
src: path,
|
|
13349
|
+
alt: String(out.alt ?? ""),
|
|
13350
|
+
width: Math.round(out.width),
|
|
13351
|
+
height: Math.round(out.height)
|
|
13352
|
+
};
|
|
13353
|
+
}
|
|
13354
|
+
if (out.kind === "link") {
|
|
13355
|
+
const href = httpsAddress(out.href);
|
|
13356
|
+
if (!href) throw new Error("its link is not an https address");
|
|
13357
|
+
const src = httpsAddress(out.image?.src);
|
|
13358
|
+
const image = src && out.image ? {
|
|
13359
|
+
src,
|
|
13360
|
+
alt: String(out.image.alt ?? ""),
|
|
13361
|
+
width: out.image.width,
|
|
13362
|
+
height: out.image.height
|
|
13363
|
+
} : void 0;
|
|
13364
|
+
return {
|
|
13365
|
+
kind: "link",
|
|
13366
|
+
href,
|
|
13367
|
+
text: String(out.text ?? href),
|
|
13368
|
+
...image ? { image } : {}
|
|
13369
|
+
};
|
|
13370
|
+
}
|
|
13371
|
+
throw new Error("it answered with something that is neither a picture nor a link");
|
|
13372
|
+
} catch (error) {
|
|
13373
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
13374
|
+
const extensionFence = isExtensionFenceInfo(info);
|
|
13375
|
+
warnings.push({
|
|
13376
|
+
level: "warning",
|
|
13377
|
+
code: "extension-fence-failed",
|
|
13378
|
+
message: `A \`${info}\` block in "${concept}" could not be published: ${why}. ${extensionFence ? "It is left out." : "It is published as its text."}`,
|
|
13379
|
+
concept
|
|
13380
|
+
});
|
|
13381
|
+
return extensionFence ? null : void 0;
|
|
13382
|
+
}
|
|
13383
|
+
}
|
|
13232
13384
|
function assetHrefOf(name) {
|
|
13233
13385
|
return `assets/${encodeURIComponent(name)}`;
|
|
13234
13386
|
}
|
|
@@ -13407,6 +13559,10 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
13407
13559
|
}
|
|
13408
13560
|
const mermaidSvg = /* @__PURE__ */ new Map();
|
|
13409
13561
|
const mapPictures = /* @__PURE__ */ new Map();
|
|
13562
|
+
/** What each fence an extension publishes becomes, by its info word and body; null when it is left out. */
|
|
13563
|
+
const fenceOutputs = /* @__PURE__ */ new Map();
|
|
13564
|
+
/** The pictures extensions made of fences, by their path in the site. */
|
|
13565
|
+
const fencePictures = /* @__PURE__ */ new Map();
|
|
13410
13566
|
const highlighted = /* @__PURE__ */ new Map();
|
|
13411
13567
|
const highlight = env.highlightCode ?? highlightCode;
|
|
13412
13568
|
const probe = createDocumentRenderer({
|
|
@@ -13417,51 +13573,77 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
13417
13573
|
doc: d,
|
|
13418
13574
|
body: bodyOf$1(d)
|
|
13419
13575
|
}));
|
|
13420
|
-
for (const { doc, body } of bodies) for (const fence of probe.fences(body))
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13576
|
+
for (const { doc, body } of bodies) for (const fence of probe.fences(body)) {
|
|
13577
|
+
const fenceKey = `${fence.lang}\n${fence.code}`;
|
|
13578
|
+
const extensionFence = isExtensionFenceInfo(fence.lang);
|
|
13579
|
+
const asked = fence.lang === "" || fenceOutputs.has(fenceKey) ? void 0 : env.publishFence?.({
|
|
13580
|
+
info: fence.lang,
|
|
13581
|
+
body: bodyLines(fence.code),
|
|
13582
|
+
document: doc.concept
|
|
13583
|
+
});
|
|
13584
|
+
if (asked) {
|
|
13585
|
+
const output = await publishedFence(asked, fence.lang, doc.concept, fencePictures, warnings);
|
|
13586
|
+
if (output !== void 0) {
|
|
13587
|
+
fenceOutputs.set(fenceKey, output);
|
|
13588
|
+
continue;
|
|
13589
|
+
}
|
|
13590
|
+
} else if (fenceOutputs.has(fenceKey)) continue;
|
|
13591
|
+
if (extensionFence) {
|
|
13592
|
+
fenceOutputs.set(fenceKey, null);
|
|
13425
13593
|
warnings.push({
|
|
13426
13594
|
level: "warning",
|
|
13427
|
-
code: "
|
|
13428
|
-
message: `
|
|
13429
|
-
concept: doc.concept
|
|
13430
|
-
});
|
|
13431
|
-
}
|
|
13432
|
-
} else if (fence.lang === "map") {
|
|
13433
|
-
if (mapPictures.has(fence.code) || errors.some((e) => e.code === "map-not-drawn" && e.concept === doc.concept)) continue;
|
|
13434
|
-
if (!env.renderMap) {
|
|
13435
|
-
errors.push({
|
|
13436
|
-
level: "error",
|
|
13437
|
-
code: "map-not-drawn",
|
|
13438
|
-
message: `"${doc.concept}" holds a map, which this publisher can't draw. Publish the site from EtherPK in a browser, where maps are drawn.`,
|
|
13595
|
+
code: "extension-fence-left-out",
|
|
13596
|
+
message: `"${doc.concept}" holds content from the ${extensionOfFence(fence.lang)} extension, which this publisher can't draw, so it is left out. Publish from EtherPK in a browser, with that extension on, to include it.`,
|
|
13439
13597
|
concept: doc.concept
|
|
13440
13598
|
});
|
|
13441
13599
|
continue;
|
|
13442
13600
|
}
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13601
|
+
if (fence.lang === "mermaid") {
|
|
13602
|
+
if (!env.renderMermaid || mermaidSvg.has(fence.code)) continue;
|
|
13603
|
+
try {
|
|
13604
|
+
mermaidSvg.set(fence.code, await env.renderMermaid(fence.code));
|
|
13605
|
+
} catch (error) {
|
|
13606
|
+
warnings.push({
|
|
13607
|
+
level: "warning",
|
|
13608
|
+
code: "mermaid-render-failed",
|
|
13609
|
+
message: `A diagram in "${doc.concept}" could not be rendered: ${error instanceof Error ? error.message : String(error)}. It is left as its source for a script to draw.`,
|
|
13610
|
+
concept: doc.concept
|
|
13611
|
+
});
|
|
13612
|
+
}
|
|
13613
|
+
} else if (fence.lang === "map") {
|
|
13614
|
+
if (mapPictures.has(fence.code) || errors.some((e) => e.code === "map-not-drawn" && e.concept === doc.concept)) continue;
|
|
13615
|
+
if (!env.renderMap) {
|
|
13616
|
+
errors.push({
|
|
13617
|
+
level: "error",
|
|
13618
|
+
code: "map-not-drawn",
|
|
13619
|
+
message: `"${doc.concept}" holds a map, which this publisher can't draw. Publish the site from EtherPK in a browser, where maps are drawn.`,
|
|
13620
|
+
concept: doc.concept
|
|
13621
|
+
});
|
|
13622
|
+
continue;
|
|
13623
|
+
}
|
|
13624
|
+
try {
|
|
13625
|
+
const picture = await env.renderMap(fence.code);
|
|
13626
|
+
const path = `maps/${await mapPictureName(fence.code)}.${picture.type === "image/webp" ? "webp" : "png"}`;
|
|
13627
|
+
mapPictures.set(fence.code, {
|
|
13628
|
+
...picture,
|
|
13629
|
+
path,
|
|
13630
|
+
alt: mapAltText(fence.code.split("\n"))
|
|
13631
|
+
});
|
|
13632
|
+
} catch (error) {
|
|
13633
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
13634
|
+
errors.push({
|
|
13635
|
+
level: "error",
|
|
13636
|
+
code: "map-not-drawn",
|
|
13637
|
+
message: `The map in "${doc.concept}" could not be drawn: ${why}. Nothing was published, so try again once it can be.`,
|
|
13638
|
+
concept: doc.concept
|
|
13639
|
+
});
|
|
13640
|
+
}
|
|
13641
|
+
} else if (fence.lang !== "" && fence.lang !== "math" && fence.lang !== "etherpk-cipher") {
|
|
13642
|
+
const key = `${fence.lang}\n${fence.code}`;
|
|
13643
|
+
if (highlighted.has(key)) continue;
|
|
13644
|
+
const html = await highlight(fence.lang, fence.code);
|
|
13645
|
+
if (html !== null) highlighted.set(key, html);
|
|
13459
13646
|
}
|
|
13460
|
-
} else if (fence.lang !== "" && fence.lang !== "math" && fence.lang !== "etherpk-cipher") {
|
|
13461
|
-
const key = `${fence.lang}\n${fence.code}`;
|
|
13462
|
-
if (highlighted.has(key)) continue;
|
|
13463
|
-
const html = await highlight(fence.lang, fence.code);
|
|
13464
|
-
if (html !== null) highlighted.set(key, html);
|
|
13465
13647
|
}
|
|
13466
13648
|
if (errors.length > 0) return {
|
|
13467
13649
|
bundle,
|
|
@@ -13493,7 +13675,8 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
13493
13675
|
width: picture.width,
|
|
13494
13676
|
height: picture.height
|
|
13495
13677
|
};
|
|
13496
|
-
}
|
|
13678
|
+
},
|
|
13679
|
+
fenceOutput: (lang, code) => fenceOutputs.get(`${lang}\n${code}`)
|
|
13497
13680
|
});
|
|
13498
13681
|
const pages = [];
|
|
13499
13682
|
let done = 0;
|
|
@@ -13752,6 +13935,7 @@ async function publishPublication(source, publication, env, options = {}) {
|
|
|
13752
13935
|
report.assets.copied.push(name);
|
|
13753
13936
|
}
|
|
13754
13937
|
for (const picture of mapPictures.values()) bundle.set(picture.path, picture.bytes);
|
|
13938
|
+
for (const [path, bytes] of fencePictures) bundle.set(path, bytes);
|
|
13755
13939
|
for (const [path, text] of theme.files) if (path.startsWith("assets/")) bundle.set(`theme/${path.slice(7)}`, text);
|
|
13756
13940
|
if (customCss !== "") bundle.set("theme/custom.css", customCss.endsWith("\n") ? customCss : `${customCss}\n`);
|
|
13757
13941
|
if (hasMath) {
|
|
@@ -14663,6 +14847,45 @@ async function writePublishFolders(path, folders) {
|
|
|
14663
14847
|
function publishFolderOf(folders, graphKey, publicationId) {
|
|
14664
14848
|
return folders.folders[graphKey]?.[publicationId] ?? null;
|
|
14665
14849
|
}
|
|
14850
|
+
/**
|
|
14851
|
+
* How two absolute folder paths relate, by the platform's own `relative` rather than by splitting
|
|
14852
|
+
* on `/` (Windows paths use `\`, and a shared prefix such as `notes` and `notes-site` is not a
|
|
14853
|
+
* parent). Spelling only: {@link publishFolderOverlap} resolves symbolic links first.
|
|
14854
|
+
*/
|
|
14855
|
+
function relateFolders(graphFolder, publishFolder, options = {}) {
|
|
14856
|
+
const path = options.path ?? nodePath;
|
|
14857
|
+
const caseInsensitive = options.caseInsensitive ?? (process.platform === "darwin" || process.platform === "win32");
|
|
14858
|
+
const fold = (p) => caseInsensitive ? path.resolve(p).toLowerCase() : path.resolve(p);
|
|
14859
|
+
const graph = fold(graphFolder);
|
|
14860
|
+
const site = fold(publishFolder);
|
|
14861
|
+
const below = (from, to) => {
|
|
14862
|
+
const rel = path.relative(from, to);
|
|
14863
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
|
|
14864
|
+
};
|
|
14865
|
+
if (path.relative(graph, site) === "") return "same";
|
|
14866
|
+
if (below(graph, site)) return "inside";
|
|
14867
|
+
if (below(site, graph)) return "contains";
|
|
14868
|
+
return null;
|
|
14869
|
+
}
|
|
14870
|
+
/**
|
|
14871
|
+
* A path with its symbolic links resolved, for a folder that may not exist yet: the nearest
|
|
14872
|
+
* existing ancestor is resolved and the rest appended, so `--out ./site` under a linked parent is
|
|
14873
|
+
* compared where it will really be written.
|
|
14874
|
+
*/
|
|
14875
|
+
async function canonicalFolder(folder) {
|
|
14876
|
+
const absolute = resolve(folder);
|
|
14877
|
+
try {
|
|
14878
|
+
return await realpath(absolute);
|
|
14879
|
+
} catch {
|
|
14880
|
+
const parent = dirname(absolute);
|
|
14881
|
+
if (parent === absolute) return absolute;
|
|
14882
|
+
return join(await canonicalFolder(parent), basename(absolute));
|
|
14883
|
+
}
|
|
14884
|
+
}
|
|
14885
|
+
/** {@link relateFolders} over the folders as they are on disk, symbolic links followed. */
|
|
14886
|
+
async function publishFolderOverlap(graphFolder, publishFolder) {
|
|
14887
|
+
return relateFolders(await canonicalFolder(graphFolder), await canonicalFolder(publishFolder));
|
|
14888
|
+
}
|
|
14666
14889
|
/** The same file with one publication's folder set (made absolute) - the caller writes it. */
|
|
14667
14890
|
function withPublishFolder(folders, graphKey, publicationId, folder) {
|
|
14668
14891
|
return { folders: {
|
|
@@ -17395,6 +17618,26 @@ async function findPublication(graph, id, host) {
|
|
|
17395
17618
|
if (!found) throw publicationNotFound(id, publications, via);
|
|
17396
17619
|
return found;
|
|
17397
17620
|
}
|
|
17621
|
+
var OVERLAP_WORDS = {
|
|
17622
|
+
same: "is the folder of",
|
|
17623
|
+
inside: "is inside the folder of",
|
|
17624
|
+
contains: "holds the folder of"
|
|
17625
|
+
};
|
|
17626
|
+
/**
|
|
17627
|
+
* Refuse a Publish Folder that overlaps a served folder: the graph's own folder, one inside it, or
|
|
17628
|
+
* one holding it. A publish deletes the files it did not write from the folder it writes into
|
|
17629
|
+
* (`isOwnedPath`), which would take the graph's attachments, or mix the site's files into the
|
|
17630
|
+
* graph. Checked where a folder is set (`--out`) and again before every publish, since the
|
|
17631
|
+
* folder may have been set before this check existed. A synced graph has no folder here.
|
|
17632
|
+
*/
|
|
17633
|
+
async function checkPublishFolder(graph, publicationId, folder, host) {
|
|
17634
|
+
if (graph.backend.kind !== "folder") return;
|
|
17635
|
+
const overlap = await publishFolderOverlap(graph.backend.path, folder);
|
|
17636
|
+
if (!overlap) return;
|
|
17637
|
+
const { cmd, via } = hostOf$1(host);
|
|
17638
|
+
const command = `${cmd} publish --folder "${graph.backend.path}" --publication ${publicationId} --out <a folder outside the graph>`;
|
|
17639
|
+
throw new ToolError("publish_folder_overlaps_graph", `${folder} ${OVERLAP_WORDS[overlap]} the graph "${graph.name}" (${graph.backend.path}). A publish deletes the files it did not write from the folder it publishes into, so the site needs a folder of its own outside the graph. Nothing was published. ` + (via === "cli" ? `Run: ${command}` : `Ask the user to run: ${command}`));
|
|
17640
|
+
}
|
|
17398
17641
|
/** The publication page's mapping and body, through the raw handles: what `publish-service` writes to. */
|
|
17399
17642
|
function frontmatterStore(graph) {
|
|
17400
17643
|
return {
|
|
@@ -17526,7 +17769,7 @@ function head(items, n = 20) {
|
|
|
17526
17769
|
/**
|
|
17527
17770
|
* Publish one publication into its Publish Folder and report. The folder is the one a person
|
|
17528
17771
|
* set for this graph and publication on this machine (ADR 0086); a publish with Mermaid needs
|
|
17529
|
-
* the browser (ADR 0084).
|
|
17772
|
+
* the browser (ADR 0084). A dry run does everything but write, so it needs no folder.
|
|
17530
17773
|
*
|
|
17531
17774
|
* The result IS the report, trimmed to counts and first entries: nothing of it is written into
|
|
17532
17775
|
* the folder, because a static host serves whatever the folder holds and the report names every
|
|
@@ -17543,10 +17786,12 @@ async function publish(graph, args, host) {
|
|
|
17543
17786
|
const publication = summary.publications.find((p) => p.id === args.id.trim());
|
|
17544
17787
|
if (!publication) throw publicationNotFound(args.id, summary.publications, via);
|
|
17545
17788
|
const folder = (await foldersFor(graph, env))(publication.id);
|
|
17546
|
-
|
|
17789
|
+
const dryRun = args.dryRun === true;
|
|
17790
|
+
if (!folder && !dryRun) {
|
|
17547
17791
|
const where = graph.backend.kind === "folder" ? `--folder "${graph.backend.path}"` : `--graph "${graph.name}"`;
|
|
17548
17792
|
throw new ToolError("no_publish_folder", `No publish folder is set for "${publication.name}" on this machine. Ask the user to run: ${cmd} publish ${where} --publication ${publication.id} --out <dir> - that publishes once and remembers the folder for this tool.`);
|
|
17549
17793
|
}
|
|
17794
|
+
if (folder && !dryRun) await checkPublishFolder(graph, publication.id, folder, host);
|
|
17550
17795
|
const selected = selectDocuments(source.documents, publication, summary.publications);
|
|
17551
17796
|
const includeTexts = Object.values(publication.includes).map((concept) => source.documents.find((d) => d.concept.toLowerCase() === concept.toLowerCase())?.text ?? "");
|
|
17552
17797
|
const bodies = [...selected.included.map((d) => d.text), ...includeTexts];
|
|
@@ -17577,7 +17822,7 @@ async function publish(graph, args, host) {
|
|
|
17577
17822
|
}),
|
|
17578
17823
|
unsettled
|
|
17579
17824
|
});
|
|
17580
|
-
const written = run.report.ok ? await writeSite(nodeSiteFolder(folder), run.bundle, {
|
|
17825
|
+
const written = run.report.ok && folder && !dryRun ? await writeSite(nodeSiteFolder(folder), run.bundle, {
|
|
17581
17826
|
seeded: run.seeded,
|
|
17582
17827
|
agentsMd: run.agentsMd
|
|
17583
17828
|
}) : null;
|
|
@@ -17585,6 +17830,7 @@ async function publish(graph, args, host) {
|
|
|
17585
17830
|
for (const e of run.report.excluded) excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
|
|
17586
17831
|
return {
|
|
17587
17832
|
ok: run.report.ok,
|
|
17833
|
+
...dryRun ? { dryRun: true } : {},
|
|
17588
17834
|
publication: run.report.publication,
|
|
17589
17835
|
folder,
|
|
17590
17836
|
written: written ? {
|
|
@@ -18316,7 +18562,7 @@ function createMcpServer(source, info) {
|
|
|
18316
18562
|
"graph_insights says what the links show across the whole graph (the most-linked concepts, pages many documents ask for that do not exist yet, pages linked to nothing, groups of related concepts and the concepts bridging them), and graph_path finds how two concepts are connected. Use them to answer questions about the shape of the graph, or to suggest pages to write or link.",
|
|
18317
18563
|
"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.",
|
|
18318
18564
|
"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.",
|
|
18319
|
-
"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\".",
|
|
18565
|
+
"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. publish with dryRun: true renders and reports without writing and needs no folder: run it after editing a publication's pages and fix what it names. Diagrams need a browser the user installs once with \"diagrams setup\".",
|
|
18320
18566
|
"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.",
|
|
18321
18567
|
"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.",
|
|
18322
18568
|
"The user documentation is at https://docs.etherpk.com."
|
|
@@ -18498,7 +18744,7 @@ function createMcpServer(source, info) {
|
|
|
18498
18744
|
}, async (args) => run((graph) => rename$1(graph, args)));
|
|
18499
18745
|
server.registerTool("set_aliases", {
|
|
18500
18746
|
title: "Set aliases",
|
|
18501
|
-
description: "Replace a document's aliases: other names it answers to in [[wikilinks]],
|
|
18747
|
+
description: "Replace a document's aliases: other names it answers to in [[wikilinks]], Search and on a published site. The list you pass is the complete list, and it is shown in the document's frontmatter, in a block added if there is none. A name that is already another document's title or alias is refused with error \"name_taken\".",
|
|
18502
18748
|
inputSchema: {
|
|
18503
18749
|
concept,
|
|
18504
18750
|
aliases: z.array(z.string()).describe("Every alias the document should have - an empty list removes them all.")
|
|
@@ -18573,8 +18819,11 @@ function createMcpServer(source, info) {
|
|
|
18573
18819
|
}, async (args) => run((graph) => updatePublication(graph, args, host)));
|
|
18574
18820
|
server.registerTool("publish", {
|
|
18575
18821
|
title: "Publish",
|
|
18576
|
-
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 or maps 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.",
|
|
18577
|
-
inputSchema: {
|
|
18822
|
+
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), and a folder that is the graph's own folder, inside it or holding it is refused (error \"publish_folder_overlaps_graph\"). With dryRun: true it renders and reports but writes nothing and needs no folder: use it to check a publication after editing its pages, and fix what the report names. Pages with Mermaid diagrams or maps need the browser from \"diagrams setup\" (error \"chromium_unavailable\"), for a dry run too. 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.",
|
|
18823
|
+
inputSchema: {
|
|
18824
|
+
id: z.string().min(1),
|
|
18825
|
+
dryRun: z.boolean().optional().describe("Render and report without writing anything. No publish folder needed.")
|
|
18826
|
+
}
|
|
18578
18827
|
}, async (args) => run((graph) => publish(graph, args, host)));
|
|
18579
18828
|
server.registerTool("list_themes", {
|
|
18580
18829
|
title: "List themes",
|
|
@@ -19010,6 +19259,7 @@ async function runGraphHost(deps) {
|
|
|
19010
19259
|
if (stopping) return answer({ etherpk: "stopping" });
|
|
19011
19260
|
return startSession(socket, request, reply);
|
|
19012
19261
|
case "publish":
|
|
19262
|
+
case "publish-check":
|
|
19013
19263
|
if (stopping) return answer({ etherpk: "stopping" });
|
|
19014
19264
|
return runPublish(request, answer);
|
|
19015
19265
|
default: return answer({
|
|
@@ -25024,7 +25274,7 @@ function bindServeLifetime(deps) {
|
|
|
25024
25274
|
* etherpk-mcp stop [--graph <id or name> [--sync-server <url>] | --folder <path> | --all]
|
|
25025
25275
|
* etherpk-mcp logout [--sync-server <url> | --all]
|
|
25026
25276
|
* etherpk-mcp semantic setup | status | remove
|
|
25027
|
-
* etherpk-mcp publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir>]
|
|
25277
|
+
* etherpk-mcp publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --dry-run]
|
|
25028
25278
|
* etherpk-mcp diagrams setup | status
|
|
25029
25279
|
*
|
|
25030
25280
|
* One config file holds a login per Sync Server (ADR 0075). `--sync-server` names the one a
|
|
@@ -25075,8 +25325,9 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
25075
25325
|
Serve a local graph folder the same way: no sign-in, no server. The agent gets search,
|
|
25076
25326
|
backlinks, tasks and format-safe edits over the folder's markdown, alongside the files
|
|
25077
25327
|
themselves. Edits made in an editor or by the agent directly are picked up as they land.
|
|
25078
|
-
The folder must already be a graph
|
|
25079
|
-
the cache directory, never
|
|
25328
|
+
The folder must already be a graph, with pages/ and journals/ folders in it (EtherPK
|
|
25329
|
+
makes them when it opens a folder) - its index is kept under the cache directory, never
|
|
25330
|
+
in the folder.
|
|
25080
25331
|
${CMD} running
|
|
25081
25332
|
List the graphs served on this computer: each one's background process, its agents and
|
|
25082
25333
|
its log. A background process stops on its own once no agent has used it for five
|
|
@@ -25095,14 +25346,18 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
25095
25346
|
to a service to be embedded.
|
|
25096
25347
|
${CMD} semantic status | remove
|
|
25097
25348
|
Whether it is set up here, or delete it (logout --all deletes it too).
|
|
25098
|
-
${CMD} publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir>]
|
|
25349
|
+
${CMD} publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --dry-run]
|
|
25099
25350
|
Publish one publication of a graph to a folder on this machine and print the report.
|
|
25100
25351
|
--out sets the publish folder for that graph and publication and is remembered: from
|
|
25101
25352
|
then on this command without --out, and the agent's publish tool, write to the same
|
|
25102
|
-
folder (the agent cannot choose one).
|
|
25103
|
-
|
|
25104
|
-
removed - everything else in it is left
|
|
25105
|
-
(a git push, say) is yours. Run it from
|
|
25353
|
+
folder (the agent cannot choose one). It must be a folder of its own, outside the
|
|
25354
|
+
graph's folder: the folder's owned files (.html at the top, assets/, theme/, the search
|
|
25355
|
+
and feed files) are rewritten and their strays removed - everything else in it is left
|
|
25356
|
+
alone. Publishing writes files - deploying them (a git push, say) is yours. Run it from
|
|
25357
|
+
cron for a site that republishes itself.
|
|
25358
|
+
--dry-run renders and checks the publication and prints the whole report, with the
|
|
25359
|
+
names of the pages it mentions, but writes nothing and needs no folder. Keep its output
|
|
25360
|
+
out of public logs.
|
|
25106
25361
|
${CMD} diagrams setup
|
|
25107
25362
|
Let a publish draw Mermaid diagrams and maps. Downloads a Chromium (about 170 MB) into the
|
|
25108
25363
|
cache directory with Playwright's installer, once per computer - a publish whose pages hold
|
|
@@ -25510,14 +25765,22 @@ async function publishCommand(args) {
|
|
|
25510
25765
|
const request = graphRequest("publish", args);
|
|
25511
25766
|
const publication = args.publication?.trim();
|
|
25512
25767
|
if (!publication) fail("publish needs --publication <id>. The agent's list_publications tool, or Settings → Publish in EtherPK, shows the ids.");
|
|
25513
|
-
const
|
|
25768
|
+
const dryRun = args["dry-run"] === true;
|
|
25514
25769
|
const out = args.out?.trim();
|
|
25515
|
-
|
|
25770
|
+
if (dryRun && out) fail("publish --dry-run writes nothing, so it takes no --out. Leave --out off, or drop --dry-run to publish.");
|
|
25771
|
+
const found = await findGraph(request).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
|
|
25772
|
+
const env = publishSettings(process.env);
|
|
25773
|
+
const { reply, socket } = await requestHost(launcherFor(found), dryRun ? {
|
|
25774
|
+
etherpk: "publish-check",
|
|
25775
|
+
protocol: 1,
|
|
25776
|
+
publication,
|
|
25777
|
+
env
|
|
25778
|
+
} : {
|
|
25516
25779
|
etherpk: "publish",
|
|
25517
25780
|
protocol: 1,
|
|
25518
25781
|
publication,
|
|
25519
25782
|
...out ? { out: resolve(out) } : {},
|
|
25520
|
-
env
|
|
25783
|
+
env
|
|
25521
25784
|
}).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
|
|
25522
25785
|
socket.destroy();
|
|
25523
25786
|
switch (reply.etherpk) {
|
|
@@ -25527,15 +25790,18 @@ async function publishCommand(args) {
|
|
|
25527
25790
|
process.exitCode = reply.exitCode;
|
|
25528
25791
|
return;
|
|
25529
25792
|
case "refused": return fail(reply.message);
|
|
25530
|
-
case "failed":
|
|
25793
|
+
case "failed":
|
|
25794
|
+
if (dryRun && reply.message === "The request was not understood.") return fail(`etherpk-mcp: this graph is served on this computer by an older etherpk-mcp, which cannot run a dry run. Run: ${CMD} stop, then try again.`);
|
|
25795
|
+
return fail(`etherpk-mcp: ${reply.message}`);
|
|
25531
25796
|
case "unsupported": return fail(`etherpk-mcp: this graph is served on this computer by etherpk-mcp ${reply.version}, which cannot run a publish for this version (${VERSION}). Run: ${CMD} stop, then publish again.`);
|
|
25532
25797
|
default: return fail(`etherpk-mcp: the graph's background process gave an answer this version does not know ("${reply.etherpk}").`);
|
|
25533
25798
|
}
|
|
25534
25799
|
}
|
|
25535
25800
|
/**
|
|
25536
25801
|
* A command-line publish, as the graph's host runs it: the publication first, so a mistyped id is
|
|
25537
|
-
* never remembered as a publish folder, then `--out`
|
|
25538
|
-
* tool runs. The answer is what the command prints, in
|
|
25802
|
+
* never remembered as a publish folder, then `--out` checked against the graph's folder and
|
|
25803
|
+
* remembered, then the publish the agent's tool runs. The answer is what the command prints, in
|
|
25804
|
+
* its own words.
|
|
25539
25805
|
*/
|
|
25540
25806
|
async function publishInHost(graph, request) {
|
|
25541
25807
|
const env = withPublishSettings(process.env, request.env ?? {});
|
|
@@ -25546,10 +25812,23 @@ async function publishInHost(graph, request) {
|
|
|
25546
25812
|
};
|
|
25547
25813
|
const notes = [];
|
|
25548
25814
|
try {
|
|
25815
|
+
if (request.etherpk === "publish-check") {
|
|
25816
|
+
const result = await publish(graph, {
|
|
25817
|
+
id: request.publication,
|
|
25818
|
+
dryRun: true
|
|
25819
|
+
}, host);
|
|
25820
|
+
return {
|
|
25821
|
+
etherpk: "published",
|
|
25822
|
+
output: result,
|
|
25823
|
+
notes: [],
|
|
25824
|
+
exitCode: result.ok ? 0 : 1
|
|
25825
|
+
};
|
|
25826
|
+
}
|
|
25549
25827
|
await findPublication(graph, request.publication, host);
|
|
25550
25828
|
const configPath = defaultPublishFoldersPath(env);
|
|
25551
25829
|
const key = publishGraphKey(graph.backend, graph.graphId);
|
|
25552
25830
|
if (request.out) {
|
|
25831
|
+
await checkPublishFolder(graph, request.publication, request.out, host);
|
|
25553
25832
|
await writePublishFolders(configPath, withPublishFolder(await readPublishFolders(configPath), key, request.publication, request.out));
|
|
25554
25833
|
notes.push(`etherpk-mcp: publish folder for "${request.publication}" of "${graph.name}" set to ${request.out} (remembered in ${configPath}).`);
|
|
25555
25834
|
} else if (!publishFolderOf(await readPublishFolders(configPath), key, request.publication)) return {
|
|
@@ -26072,6 +26351,7 @@ async function main() {
|
|
|
26072
26351
|
"no-semantic": { type: "boolean" },
|
|
26073
26352
|
publication: { type: "string" },
|
|
26074
26353
|
out: { type: "string" },
|
|
26354
|
+
"dry-run": { type: "boolean" },
|
|
26075
26355
|
all: { type: "boolean" },
|
|
26076
26356
|
name: { type: "string" },
|
|
26077
26357
|
help: {
|