@appsoftwareltd/etherpk-mcp 0.9.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -717,7 +717,7 @@ function looksLikeAgentSetupCode(value) {
717
717
  }
718
718
  var package_default = {
719
719
  name: "@appsoftwareltd/etherpk-mcp",
720
- version: "0.9.6",
720
+ version: "0.10.0",
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
- for (let at = trimmed.lastIndexOf("@"); at >= 0; at = at === 0 ? -1 : trimmed.lastIndexOf("@", at - 1)) {
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: taskBlocks.map((b) => {
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
- ...deriveMaps(text, blocks, links, lineToBlock)
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)) if (fence.lang === "mermaid") {
13421
- if (!env.renderMermaid || mermaidSvg.has(fence.code)) continue;
13422
- try {
13423
- mermaidSvg.set(fence.code, await env.renderMermaid(fence.code));
13424
- } catch (error) {
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: "mermaid-render-failed",
13428
- 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.`,
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
- try {
13444
- const picture = await env.renderMap(fence.code);
13445
- const path = `maps/${await mapPictureName(fence.code)}.${picture.type === "image/webp" ? "webp" : "png"}`;
13446
- mapPictures.set(fence.code, {
13447
- ...picture,
13448
- path,
13449
- alt: mapAltText(fence.code.split("\n"))
13450
- });
13451
- } catch (error) {
13452
- const why = error instanceof Error ? error.message : String(error);
13453
- errors.push({
13454
- level: "error",
13455
- code: "map-not-drawn",
13456
- message: `The map in "${doc.concept}" could not be drawn: ${why}. Nothing was published, so try again once it can be.`,
13457
- concept: doc.concept
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) {
@@ -18498,7 +18682,7 @@ function createMcpServer(source, info) {
18498
18682
  }, async (args) => run((graph) => rename$1(graph, args)));
18499
18683
  server.registerTool("set_aliases", {
18500
18684
  title: "Set aliases",
18501
- 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, 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\".",
18685
+ 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
18686
  inputSchema: {
18503
18687
  concept,
18504
18688
  aliases: z.array(z.string()).describe("Every alias the document should have - an empty list removes them all.")