@appsoftwareltd/etherpk-mcp 0.9.5 → 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.5",
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.
@@ -6056,7 +6124,8 @@ function backlinksFor(db, concept) {
6056
6124
  sourceConcept: h.sourceConcept,
6057
6125
  sourceKind: h.sourceKind,
6058
6126
  refs: [],
6059
- bodyNames: []
6127
+ bodyNames: [],
6128
+ sourceIsTarget: h.page_id === pageId
6060
6129
  };
6061
6130
  groups.set(h.sourceConcept, group);
6062
6131
  }
@@ -12650,6 +12719,13 @@ function wikilinkRule(md, resolve) {
12650
12719
  * grammars for code), so they are pre-rendered by the orchestrator and handed in as lookups;
12651
12720
  * the renderer itself is synchronous and pure over its inputs.
12652
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
+ }
12653
12729
  /** The names on a Map Block, one line for the search index and the excerpt; never its coordinates. */
12654
12730
  function mapNames(source) {
12655
12731
  return readMapBody(source.split("\n")).items.map((item) => item.name).filter((name) => name !== "").join(", ");
@@ -12697,7 +12773,7 @@ function plainText(tokens) {
12697
12773
  if (info === "map") {
12698
12774
  const names = mapNames(token.content);
12699
12775
  if (names !== "") lines.push(names);
12700
- } 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());
12701
12777
  }
12702
12778
  return lines.join("\n");
12703
12779
  }
@@ -12776,6 +12852,9 @@ function createDocumentRenderer(options) {
12776
12852
  if (!picture) return "";
12777
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`;
12778
12854
  }
12855
+ const output = options.fenceOutput?.(lang, code);
12856
+ if (output) return fenceOutputHtml(output);
12857
+ if (output === null || isExtensionFenceInfo(lang)) return "";
12779
12858
  const highlighted = options.highlighted?.(lang, code);
12780
12859
  return `<pre><code${lang === "" ? "" : ` class="language-${escapeHtml$3(lang)}"`}>${highlighted ?? escapeHtml$3(code)}</code></pre>\n`;
12781
12860
  };
@@ -13219,14 +13298,89 @@ function createThemeRenderer(theme, includes) {
13219
13298
  */
13220
13299
  /**
13221
13300
  * Bumped when the way a map is drawn changes, so a site published again holds the new picture
13222
- * under a new name rather than a cached one under the old.
13301
+ * under a new name rather than a cached one under the old. 2: a name that would run past the
13302
+ * picture's edge is shortened with an ellipsis.
13223
13303
  */
13224
- var MAP_PICTURE_VERSION = 1;
13304
+ var MAP_PICTURE_VERSION = 2;
13225
13305
  /** A picture's file name: from what the map holds and how it is drawn, so an unchanged map publishes the same file. */
13226
13306
  async function mapPictureName(source) {
13227
13307
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${MAP_PICTURE_VERSION}\n${source}`));
13228
13308
  return [...new Uint8Array(digest)].slice(0, 8).map((byte) => byte.toString(16).padStart(2, "0")).join("");
13229
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
+ }
13230
13384
  function assetHrefOf(name) {
13231
13385
  return `assets/${encodeURIComponent(name)}`;
13232
13386
  }
@@ -13405,6 +13559,10 @@ async function publishPublication(source, publication, env, options = {}) {
13405
13559
  }
13406
13560
  const mermaidSvg = /* @__PURE__ */ new Map();
13407
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();
13408
13566
  const highlighted = /* @__PURE__ */ new Map();
13409
13567
  const highlight = env.highlightCode ?? highlightCode;
13410
13568
  const probe = createDocumentRenderer({
@@ -13415,51 +13573,77 @@ async function publishPublication(source, publication, env, options = {}) {
13415
13573
  doc: d,
13416
13574
  body: bodyOf$1(d)
13417
13575
  }));
13418
- for (const { doc, body } of bodies) for (const fence of probe.fences(body)) if (fence.lang === "mermaid") {
13419
- if (!env.renderMermaid || mermaidSvg.has(fence.code)) continue;
13420
- try {
13421
- mermaidSvg.set(fence.code, await env.renderMermaid(fence.code));
13422
- } 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);
13423
13593
  warnings.push({
13424
13594
  level: "warning",
13425
- code: "mermaid-render-failed",
13426
- 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.`,
13427
- concept: doc.concept
13428
- });
13429
- }
13430
- } else if (fence.lang === "map") {
13431
- if (mapPictures.has(fence.code) || errors.some((e) => e.code === "map-not-drawn" && e.concept === doc.concept)) continue;
13432
- if (!env.renderMap) {
13433
- errors.push({
13434
- level: "error",
13435
- code: "map-not-drawn",
13436
- 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.`,
13437
13597
  concept: doc.concept
13438
13598
  });
13439
13599
  continue;
13440
13600
  }
13441
- try {
13442
- const picture = await env.renderMap(fence.code);
13443
- const path = `maps/${await mapPictureName(fence.code)}.${picture.type === "image/webp" ? "webp" : "png"}`;
13444
- mapPictures.set(fence.code, {
13445
- ...picture,
13446
- path,
13447
- alt: mapAltText(fence.code.split("\n"))
13448
- });
13449
- } catch (error) {
13450
- const why = error instanceof Error ? error.message : String(error);
13451
- errors.push({
13452
- level: "error",
13453
- code: "map-not-drawn",
13454
- message: `The map in "${doc.concept}" could not be drawn: ${why}. Nothing was published, so try again once it can be.`,
13455
- concept: doc.concept
13456
- });
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);
13457
13646
  }
13458
- } else if (fence.lang !== "" && fence.lang !== "math" && fence.lang !== "etherpk-cipher") {
13459
- const key = `${fence.lang}\n${fence.code}`;
13460
- if (highlighted.has(key)) continue;
13461
- const html = await highlight(fence.lang, fence.code);
13462
- if (html !== null) highlighted.set(key, html);
13463
13647
  }
13464
13648
  if (errors.length > 0) return {
13465
13649
  bundle,
@@ -13491,7 +13675,8 @@ async function publishPublication(source, publication, env, options = {}) {
13491
13675
  width: picture.width,
13492
13676
  height: picture.height
13493
13677
  };
13494
- }
13678
+ },
13679
+ fenceOutput: (lang, code) => fenceOutputs.get(`${lang}\n${code}`)
13495
13680
  });
13496
13681
  const pages = [];
13497
13682
  let done = 0;
@@ -13750,6 +13935,7 @@ async function publishPublication(source, publication, env, options = {}) {
13750
13935
  report.assets.copied.push(name);
13751
13936
  }
13752
13937
  for (const picture of mapPictures.values()) bundle.set(picture.path, picture.bytes);
13938
+ for (const [path, bytes] of fencePictures) bundle.set(path, bytes);
13753
13939
  for (const [path, text] of theme.files) if (path.startsWith("assets/")) bundle.set(`theme/${path.slice(7)}`, text);
13754
13940
  if (customCss !== "") bundle.set("theme/custom.css", customCss.endsWith("\n") ? customCss : `${customCss}\n`);
13755
13941
  if (hasMath) {
@@ -18496,7 +18682,7 @@ function createMcpServer(source, info) {
18496
18682
  }, async (args) => run((graph) => rename$1(graph, args)));
18497
18683
  server.registerTool("set_aliases", {
18498
18684
  title: "Set aliases",
18499
- 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\".",
18500
18686
  inputSchema: {
18501
18687
  concept,
18502
18688
  aliases: z.array(z.string()).describe("Every alias the document should have - an empty list removes them all.")