@zosmaai/pi-llm-wiki 0.11.5 → 0.11.7

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.
Files changed (35) hide show
  1. package/README.de.md +1 -0
  2. package/README.es.md +1 -0
  3. package/README.fr.md +1 -0
  4. package/README.hi.md +1 -0
  5. package/README.ja.md +1 -0
  6. package/README.ko.md +1 -0
  7. package/README.md +1 -0
  8. package/README.pt.md +1 -0
  9. package/README.ru.md +1 -0
  10. package/README.zh.md +1 -0
  11. package/dist/extensions/llm-wiki/lib/ingest-worker.js +43 -20
  12. package/dist/extensions/llm-wiki/lib/knowledge-links.js +127 -24
  13. package/dist/extensions/llm-wiki/lib/metadata.js +5 -5
  14. package/dist/extensions/llm-wiki/lib/retro.js +38 -4
  15. package/dist/extensions/llm-wiki/lib/runtime.js +2 -2
  16. package/dist/extensions/llm-wiki/lib/task-config.js +35 -0
  17. package/dist/extensions/llm-wiki/lib/tools.js +53 -12
  18. package/dist/mcp/index.js +2 -1
  19. package/dist/mcp/operations.js +21 -2
  20. package/docs/configuration.md +1 -0
  21. package/docs/obsidian.md +6 -6
  22. package/docs/superpowers/plans/2026-08-27-wikilink-resolver-normalization.md +735 -0
  23. package/docs/superpowers/plans/2026-08-29-wikilink-gate-ensure-page-retro.md +642 -0
  24. package/docs/superpowers/plans/2026-08-29-wikilink-write-validation.md +695 -0
  25. package/extensions/llm-wiki/lib/ingest-worker.ts +60 -27
  26. package/extensions/llm-wiki/lib/knowledge-document.ts +1 -0
  27. package/extensions/llm-wiki/lib/knowledge-links.ts +201 -32
  28. package/extensions/llm-wiki/lib/metadata.ts +9 -5
  29. package/extensions/llm-wiki/lib/retro.ts +48 -4
  30. package/extensions/llm-wiki/lib/runtime.ts +2 -2
  31. package/extensions/llm-wiki/lib/task-config.ts +59 -0
  32. package/extensions/llm-wiki/lib/tools.ts +68 -20
  33. package/mcp/index.ts +12 -1
  34. package/mcp/operations.ts +32 -2
  35. package/package.json +8 -3
@@ -6,11 +6,11 @@ import { launchEmbedPages, reindexEmbeddings, resolveEmbedder } from "./embeddin
6
6
  import { scheduleReindex } from "./indexing.js";
7
7
  import { runIngestSynthesis } from "./ingest-worker.js";
8
8
  import { createKnowledgeDocument, serializeKnowledgeDocument, writeKnowledgeDocumentFile, } from "./knowledge-document.js";
9
- import { buildResolvedBacklinks } from "./knowledge-links.js";
9
+ import { applyWikilinkGate, buildResolvedBacklinks, buildWikilinkIndex, } from "./knowledge-links.js";
10
10
  import { repairLegacyKnowledgeDocuments } from "./legacy-repair.js";
11
11
  import { appendEvent, rebuildMetadata, rebuildMetadataLight } from "./metadata.js";
12
12
  import { captureFile, captureText, captureUrl } from "./source-packet.js";
13
- import { parseModelRef } from "./task-config.js";
13
+ import { loadTaskConfig, parseModelRef, resolveWikilinkValidation } from "./task-config.js";
14
14
  import { detectVaultFormat, fmtDate, getVaultPaths, readJson, resolveVaultPaths, slugify, writeJson, } from "./utils.js";
15
15
  import { assertWritableVault, compareCodePoint, discoverKnowledgeDocuments, inspectVaultFormat, inspectWritableVault, } from "./vault-format.js";
16
16
  import { getWikiStatus, searchRegistry } from "./wiki-service.js";
@@ -328,6 +328,7 @@ export function registerWikiIngest(pi, runtime) {
328
328
  manifest: s.manifest,
329
329
  extracted: s.extracted,
330
330
  synthesisLanguage: runtime.config.synthesisLanguage,
331
+ wikilinkValidation: runtime.config.wikilinkValidation,
331
332
  });
332
333
  if (committed) {
333
334
  // Background semantic embeddings (#66): embed the pages this
@@ -341,8 +342,10 @@ export function registerWikiIngest(pi, runtime) {
341
342
  ];
342
343
  launchEmbedPages(runtime, launchCtx, paths, pageIds, `embed:ingest:${s.id}`);
343
344
  }
345
+ const wl = committed?.wikilinkDiagnostics?.length ?? 0;
346
+ const wlNote = wl > 0 ? `, ${wl} wikilink issue${wl === 1 ? "" : "s"}` : "";
344
347
  const summary = committed
345
- ? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
348
+ ? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}${wlNote}`
346
349
  : `LLM Wiki: ${s.id} produced no synthesis`;
347
350
  if (ctx.hasUI) {
348
351
  ctx.ui.notify(summary, committed ? "info" : "warning");
@@ -419,7 +422,7 @@ export function registerWikiEnsurePage(pi, runtime) {
419
422
  ],
420
423
  parameters: Type.Object({
421
424
  type: Type.String({
422
- description: "Page type: entity | concept | synthesis | analysis | requirement | skill | case",
425
+ description: "Page type: entity | concept | synthesis | analysis | requirement | skill | case (built-in) or any user-defined type from llm-wiki.customTypes config",
423
426
  }),
424
427
  title: Type.String({ description: "Page title" }),
425
428
  content: Type.Optional(Type.String({ description: "Optional initial content (otherwise uses template)" })),
@@ -439,9 +442,8 @@ export function registerWikiEnsurePage(pi, runtime) {
439
442
  isError: true,
440
443
  };
441
444
  }
442
- const type = params.type;
443
- const slug = slugify(params.title);
444
- const folderMap = {
445
+ const config = loadTaskConfig(ctx.cwd);
446
+ const builtInFolderMap = {
445
447
  entity: "entities",
446
448
  concept: "concepts",
447
449
  synthesis: "syntheses",
@@ -450,6 +452,9 @@ export function registerWikiEnsurePage(pi, runtime) {
450
452
  skill: "skills",
451
453
  case: "cases",
452
454
  };
455
+ const folderMap = { ...builtInFolderMap, ...config.customTypes };
456
+ const type = params.type;
457
+ const slug = slugify(params.title);
453
458
  const folder = folderMap[type] || "concepts";
454
459
  const pagePath = join(paths.wiki, folder, `${slug}.md`);
455
460
  if (existsSync(pagePath)) {
@@ -459,7 +464,31 @@ export function registerWikiEnsurePage(pi, runtime) {
459
464
  };
460
465
  }
461
466
  const today = fmtDate();
462
- const body = params.content ?? buildPageBody(type, params.title);
467
+ let body = params.content ?? buildPageBody(type, params.title);
468
+ // Pre-write wikilink gate (#172): validate/normalize caller-supplied content.
469
+ const mode = resolveWikilinkValidation(loadTaskConfig(ctx.cwd));
470
+ let wikilinkIssues = [];
471
+ if (mode !== "off") {
472
+ const registry = readJson(join(paths.meta, "registry.json"), { pages: {} });
473
+ const gate = applyWikilinkGate(body, buildWikilinkIndex(Object.keys(registry.pages)), `${folder}/${slug}`, mode);
474
+ wikilinkIssues = gate.diagnostics.map((d) => d.message);
475
+ if (!gate.ok) {
476
+ return {
477
+ content: [
478
+ {
479
+ type: "text",
480
+ text: `Rejected write — unresolved/ambiguous wikilinks:\n${wikilinkIssues
481
+ .map((m) => `- ${m}`)
482
+ .join("\n")}`,
483
+ },
484
+ ],
485
+ details: { error: "link_validation", issues: wikilinkIssues },
486
+ isError: true,
487
+ };
488
+ }
489
+ if (mode === "normalize")
490
+ body = gate.body;
491
+ }
463
492
  const doc = createKnowledgeDocument(`${folder}/${slug}.md`, {
464
493
  type,
465
494
  title: params.title,
@@ -483,9 +512,16 @@ export function registerWikiEnsurePage(pi, runtime) {
483
512
  else {
484
513
  rebuildMetadataLight(paths);
485
514
  }
515
+ const gateNote = wikilinkIssues.length
516
+ ? `\n\n⚠️ ${wikilinkIssues.length} wikilink issue(s):\n${wikilinkIssues.map((m) => `- ${m}`).join("\n")}`
517
+ : "";
486
518
  return {
487
- content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`` }],
488
- details: { path: pagePath, created: true },
519
+ content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`${gateNote}` }],
520
+ details: {
521
+ path: pagePath,
522
+ created: true,
523
+ wikilinkIssues,
524
+ },
489
525
  };
490
526
  },
491
527
  });
@@ -740,14 +776,14 @@ function runWikiLint(paths, autoFix) {
740
776
  }
741
777
  const discovery = discoverKnowledgeDocuments(paths);
742
778
  const pages = discovery.documents;
743
- const knownIds = new Set(pages.map((page) => page.id));
779
+ const wikilinkIndex = buildWikilinkIndex(pages.map((page) => page.id));
744
780
  const inbound = Object.fromEntries(pages.map((page) => [page.id, 0]));
745
781
  const gapSources = new Map();
746
782
  const findings = [];
747
783
  let missingPages = 0;
748
784
  let contradictions = 0;
749
785
  for (const page of pages) {
750
- const resolved = buildResolvedBacklinks(page.id, page.body, knownIds);
786
+ const resolved = buildResolvedBacklinks(page.id, page.body, wikilinkIndex);
751
787
  for (const target of resolved.targets)
752
788
  inbound[target]++;
753
789
  for (const unresolved of resolved.unresolved) {
@@ -757,6 +793,11 @@ function runWikiLint(paths, autoFix) {
757
793
  missingPages++;
758
794
  findings.push(`Missing page: ${unresolved.target} (in ${page.id})`);
759
795
  }
796
+ for (const d of resolved.diagnostics) {
797
+ if (d.code === "link_ambiguous") {
798
+ findings.push(d.message.replace("Ambiguous wikilink: ", "Ambiguous: "));
799
+ }
800
+ }
760
801
  }
761
802
  let orphans = 0;
762
803
  for (const page of pages) {
package/dist/mcp/index.js CHANGED
@@ -13,6 +13,7 @@ import { join } from "node:path";
13
13
  import { McpServer } from "@modelcontextprotocol/server";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
15
15
  import * as z from "zod/v4";
16
+ import { loadTaskConfig, resolveWikilinkValidation, } from "../extensions/llm-wiki/lib/task-config.js";
16
17
  import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
17
18
  import { createExecApi } from "./exec.js";
18
19
  import { bootstrapOperation, captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
@@ -206,7 +207,7 @@ server.registerTool("wiki_retro", {
206
207
  };
207
208
  }
208
209
  const paths = getPaths();
209
- const result = await retroOperation(paths, slug, title, body, category);
210
+ const result = await retroOperation(paths, slug, title, body, category, resolveWikilinkValidation(loadTaskConfig(process.cwd())));
210
211
  if (!result.ok) {
211
212
  return {
212
213
  content: [
@@ -5,11 +5,15 @@
5
5
  * used by Pi tools. No operation parses YAML, scans files, scores
6
6
  * registry entries, or builds page strings itself.
7
7
  */
8
+ import { join } from "node:path";
8
9
  import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
10
+ import { applyWikilinkGate, buildWikilinkIndex, } from "../extensions/llm-wiki/lib/knowledge-links.js";
9
11
  import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
10
12
  import { searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
11
13
  import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
12
14
  import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
15
+ import { resolveWikilinkValidation } from "../extensions/llm-wiki/lib/task-config.js";
16
+ import { readJson } from "../extensions/llm-wiki/lib/utils.js";
13
17
  import { VaultWriteError, inspectVaultFormat, inspectWritableVault, } from "../extensions/llm-wiki/lib/vault-format.js";
14
18
  import { getWikiStatus, searchRegistry } from "../extensions/llm-wiki/lib/wiki-service.js";
15
19
  function projectionOutcome(projection) {
@@ -87,7 +91,7 @@ export async function statusOperation(paths) {
87
91
  };
88
92
  }
89
93
  /** Shared retro operation: validates vault then delegates to saveInsight. */
90
- export async function retroOperation(paths, slug, title, body, category) {
94
+ export async function retroOperation(paths, slug, title, body, category, wikilinkValidation) {
91
95
  const vaultCheck = inspectWritableVault(paths);
92
96
  if (!vaultCheck.ok) {
93
97
  return {
@@ -95,8 +99,23 @@ export async function retroOperation(paths, slug, title, body, category) {
95
99
  diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
96
100
  };
97
101
  }
102
+ // Pre-write wikilink gate (#172): validate/normalize caller-supplied body.
103
+ const mode = resolveWikilinkValidation({ wikilinkValidation });
104
+ let gateBody = body;
105
+ if (mode !== "off") {
106
+ const registry = readJson(join(paths.meta, "registry.json"), { pages: {} });
107
+ const gate = applyWikilinkGate(body, buildWikilinkIndex(Object.keys(registry.pages)), `sources/${slug}`, mode);
108
+ if (!gate.ok) {
109
+ return {
110
+ ok: false,
111
+ diagnostics: gate.diagnostics.map((d) => ({ code: "link_validation", message: d.message })),
112
+ };
113
+ }
114
+ if (mode === "normalize")
115
+ gateBody = gate.body;
116
+ }
98
117
  try {
99
- const result = saveInsight(paths, slug, title, body, category, { rebuild: false });
118
+ const result = saveInsight(paths, slug, title, gateBody, category, { rebuild: false });
100
119
  const projection = projectionOutcome(rebuildMetadata(paths));
101
120
  if (!projection.ok)
102
121
  return projection;
@@ -58,6 +58,7 @@ All of the above are viewable and editable in the `/wiki-settings` TUI (persists
58
58
  | `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
59
59
  | `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
60
60
  | `synthesisMaxTokens` | 16384 | Max output tokens for ingest/synthesis runs (stored as a plain number) |
61
+ | `wikilinkValidation` | warn | Pre-write wikilink gate for page writes (ingest, `wiki_ensure_page`, `wiki_retro`, MCP `wiki_retro`). `off` ignore, `warn` report, `normalize` rewrite resolvable links, `strict` block writes with unresolved/ambiguous links |
61
62
  | `trajectories` | false | Enable agent-trajectory working-memory |
62
63
  | `notices` | true | Show wiki activity notices in chat |
63
64
  | `ambientPersonalVault` | host-dependent | Let the personal vault act as the ambient vault in projects that have no wiki. `true` under pi, `false` under oh-my-pi — see below. |
package/docs/obsidian.md CHANGED
@@ -2,20 +2,20 @@
2
2
 
3
3
  ## Setup
4
4
 
5
- 1. Open `.llm-wiki/wiki/` as an Obsidian vault
6
- 2. The extension generates `.llm-wiki/meta/index.md` as a browsable catalog
7
- 3. `.llm-wiki/meta/backlinks.json` is available for graph plugins
5
+ 1. Open `.llm-wiki/` as an Obsidian vault
6
+ 2. Your wiki pages live in `wiki/` Graph View and Backlinks work on these automatically
7
+ 3. The extension generates `meta/index.md` (browsable catalog) and `meta/backlinks.json` (link map) both are read-only, regenerated on each rebuild
8
8
 
9
9
  ## Recommended Plugins
10
10
 
11
11
  - [Dataview](https://github.com/blacksmithgu/obsidian-dataview) — Query pages by frontmatter
12
- - [Graph View](https://obsidian.md) (built-in) — Visualize `[[wikilink]]` connections
12
+ - [Graph View](https://obsidian.md) (built-in) — Visualize `[[wikilink]]` connections in `wiki/`
13
13
  - [Backlinks](https://obsidian.md) (built-in) — See inbound links
14
14
 
15
15
  ## Web Clipper
16
16
 
17
- Use [Obsidian Web Clipper](https://obsidian.md/clipper) to save articles directly into `.llm-wiki/raw/articles/`.
17
+ Use [Obsidian Web Clipper](https://obsidian.md/clipper) to save articles directly into `raw/articles/`.
18
18
 
19
19
  ## Dataview Dashboard
20
20
 
21
- The extension creates `.llm-wiki/meta/index.md` with page listings. For custom dashboards, use Dataview queries against frontmatter fields like `type`, `domain`, `category`, `sources`.
21
+ For custom dashboards, use Dataview queries against frontmatter fields like `type`, `domain`, `category`, `sources`. Query the `wiki/` directory for knowledge pages.