@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
@@ -9,6 +9,7 @@ import {
9
9
  resolveGlobalSettingsPath,
10
10
  resolveProjectSettingsPath,
11
11
  } from "./host.js";
12
+ import type { WikilinkValidationMode } from "./knowledge-links.js";
12
13
 
13
14
  /**
14
15
  * Configuration for the background-task lane (issue #64, part of #63).
@@ -147,6 +148,27 @@ export interface TaskConfig {
147
148
  * response truncates before commit_synthesis can be called.
148
149
  */
149
150
  synthesisMaxTokens?: number;
151
+
152
+ /**
153
+ * Wikilink gate applied to the ingested source body before pages are
154
+ * written (issue #172, Layer 2). Reuses the Layer 1 resolver.
155
+ * - "off" : no-op.
156
+ * - "warn" : write proceeds; unresolved/ambiguous links are reported.
157
+ * - "strict" : block the write (commit returns ok:false) if any link is unresolvable.
158
+ * - "normalize" : rewrite resolvable links to their canonical id, then write.
159
+ * Default "warn". See `resolveWikilinkValidation`.
160
+ */
161
+ wikilinkValidation?: WikilinkValidationMode;
162
+
163
+ /**
164
+ * User-defined page types for wiki_ensure_page (issue #169). Merges with
165
+ * the 7 built-in types (entity, concept, synthesis, analysis, requirement,
166
+ * skill, case). Each key is the type name; each value is the folder name
167
+ * inside wiki/. Example: { "decision": "decisions", "metric": "metrics" }
168
+ * → wiki_ensure_page(type="decision") creates wiki/decisions/<slug>.md.
169
+ * Custom types use a generic page template (no per-type templates).
170
+ */
171
+ customTypes?: Record<string, string>;
150
172
  }
151
173
 
152
174
  export const TASK_DEFAULTS: TaskConfig = {};
@@ -159,6 +181,24 @@ export function noticesEnabled(config: TaskConfig | undefined): boolean {
159
181
  return config?.notices !== false;
160
182
  }
161
183
 
184
+ const WIKILINK_VALIDATION_MODES: readonly WikilinkValidationMode[] = [
185
+ "off",
186
+ "warn",
187
+ "strict",
188
+ "normalize",
189
+ ];
190
+
191
+ /**
192
+ * Resolve the wikilink write-gate mode (issue #172, Layer 2). Defaults to
193
+ * `warn` — ingest always writes and reports; only an explicit `strict` blocks.
194
+ * Unknown values fall back to `warn` rather than failing the ingest.
195
+ */
196
+ export function resolveWikilinkValidation(config: TaskConfig | undefined): WikilinkValidationMode {
197
+ const v = config?.wikilinkValidation;
198
+ if (v && (WIKILINK_VALIDATION_MODES as readonly string[]).includes(v)) return v;
199
+ return "warn";
200
+ }
201
+
162
202
  /**
163
203
  * Resolve whether the personal vault may serve as this project's ambient
164
204
  * vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
@@ -251,6 +291,23 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
251
291
  out.synthesisMaxTokens = Math.floor(maxTokens);
252
292
  }
253
293
 
294
+ const wl = section.wikilinkValidation;
295
+ if (typeof wl === "string" && (WIKILINK_VALIDATION_MODES as readonly string[]).includes(wl)) {
296
+ out.wikilinkValidation = wl as WikilinkValidationMode;
297
+ }
298
+
299
+ const ct = section.customTypes;
300
+ if (ct && typeof ct === "object" && !Array.isArray(ct)) {
301
+ const entries = Object.entries(ct as Record<string, unknown>);
302
+ const valid: Record<string, string> = {};
303
+ for (const [k, v] of entries) {
304
+ if (typeof k === "string" && typeof v === "string" && k && v) {
305
+ valid[k] = v;
306
+ }
307
+ }
308
+ if (Object.keys(valid).length) out.customTypes = valid;
309
+ }
310
+
254
311
  return out;
255
312
  } catch {
256
313
  return {};
@@ -438,6 +495,8 @@ const KNOWN_KEYS = [
438
495
  "trajectories",
439
496
  "synthesisLanguage",
440
497
  "synthesisMaxTokens",
498
+ "wikilinkValidation",
499
+ "customTypes",
441
500
  ] as const;
442
501
 
443
502
  export function loadTaskConfigSources(
@@ -11,12 +11,16 @@ import {
11
11
  serializeKnowledgeDocument,
12
12
  writeKnowledgeDocumentFile,
13
13
  } from "./knowledge-document.js";
14
- import { buildResolvedBacklinks } from "./knowledge-links.js";
14
+ import {
15
+ applyWikilinkGate,
16
+ buildResolvedBacklinks,
17
+ buildWikilinkIndex,
18
+ } from "./knowledge-links.js";
15
19
  import { repairLegacyKnowledgeDocuments } from "./legacy-repair.js";
16
20
  import { type Registry, appendEvent, rebuildMetadata, rebuildMetadataLight } from "./metadata.js";
17
21
  import type { Runtime } from "./runtime.js";
18
22
  import { captureFile, captureText, captureUrl } from "./source-packet.js";
19
- import { parseModelRef } from "./task-config.js";
23
+ import { loadTaskConfig, parseModelRef, resolveWikilinkValidation } from "./task-config.js";
20
24
  import {
21
25
  type VaultPaths,
22
26
  detectVaultFormat,
@@ -415,6 +419,7 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
415
419
  manifest: s.manifest,
416
420
  extracted: s.extracted,
417
421
  synthesisLanguage: runtime.config.synthesisLanguage,
422
+ wikilinkValidation: runtime.config.wikilinkValidation,
418
423
  });
419
424
  if (committed) {
420
425
  // Background semantic embeddings (#66): embed the pages this
@@ -428,8 +433,10 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
428
433
  ];
429
434
  launchEmbedPages(runtime, launchCtx, paths, pageIds, `embed:ingest:${s.id}`);
430
435
  }
436
+ const wl = committed?.wikilinkDiagnostics?.length ?? 0;
437
+ const wlNote = wl > 0 ? `, ${wl} wikilink issue${wl === 1 ? "" : "s"}` : "";
431
438
  const summary = committed
432
- ? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
439
+ ? `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}`
433
440
  : `LLM Wiki: ${s.id} produced no synthesis`;
434
441
  if (ctx.hasUI) {
435
442
  ctx.ui.notify(summary, committed ? "info" : "warning");
@@ -512,7 +519,7 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
512
519
  parameters: Type.Object({
513
520
  type: Type.String({
514
521
  description:
515
- "Page type: entity | concept | synthesis | analysis | requirement | skill | case",
522
+ "Page type: entity | concept | synthesis | analysis | requirement | skill | case (built-in) or any user-defined type from llm-wiki.customTypes config",
516
523
  }),
517
524
  title: Type.String({ description: "Page title" }),
518
525
  content: Type.Optional(
@@ -535,17 +542,8 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
535
542
  };
536
543
  }
537
544
 
538
- const type = params.type as
539
- | "entity"
540
- | "concept"
541
- | "synthesis"
542
- | "analysis"
543
- | "requirement"
544
- | "skill"
545
- | "case";
546
- const slug = slugify(params.title);
547
-
548
- const folderMap: Record<string, string> = {
545
+ const config = loadTaskConfig(ctx.cwd);
546
+ const builtInFolderMap: Record<string, string> = {
549
547
  entity: "entities",
550
548
  concept: "concepts",
551
549
  synthesis: "syntheses",
@@ -554,6 +552,9 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
554
552
  skill: "skills",
555
553
  case: "cases",
556
554
  };
555
+ const folderMap = { ...builtInFolderMap, ...config.customTypes };
556
+ const type = params.type as string;
557
+ const slug = slugify(params.title);
557
558
  const folder = folderMap[type] || "concepts";
558
559
  const pagePath = join(paths.wiki, folder, `${slug}.md`);
559
560
 
@@ -565,7 +566,42 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
565
566
  }
566
567
 
567
568
  const today = fmtDate();
568
- const body = params.content ?? buildPageBody(type, params.title);
569
+ let body = params.content ?? buildPageBody(type, params.title);
570
+
571
+ // Pre-write wikilink gate (#172): validate/normalize caller-supplied content.
572
+ const mode = resolveWikilinkValidation(loadTaskConfig(ctx.cwd));
573
+ let wikilinkIssues: string[] = [];
574
+ if (mode !== "off") {
575
+ const registry = readJson<{ pages: Record<string, unknown> }>(
576
+ join(paths.meta, "registry.json"),
577
+ { pages: {} },
578
+ );
579
+ const gate = applyWikilinkGate(
580
+ body,
581
+ buildWikilinkIndex(Object.keys(registry.pages)),
582
+ `${folder}/${slug}`,
583
+ mode,
584
+ );
585
+ wikilinkIssues = gate.diagnostics.map((d) => d.message);
586
+ if (!gate.ok) {
587
+ return {
588
+ content: [
589
+ {
590
+ type: "text",
591
+ text: `Rejected write — unresolved/ambiguous wikilinks:\n${wikilinkIssues
592
+ .map((m) => `- ${m}`)
593
+ .join("\n")}`,
594
+ },
595
+ ],
596
+ details: { error: "link_validation", issues: wikilinkIssues } as Record<
597
+ string,
598
+ unknown
599
+ >,
600
+ isError: true,
601
+ };
602
+ }
603
+ if (mode === "normalize") body = gate.body;
604
+ }
569
605
  const doc = createKnowledgeDocument(
570
606
  `${folder}/${slug}.md`,
571
607
  {
@@ -595,9 +631,16 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
595
631
  rebuildMetadataLight(paths);
596
632
  }
597
633
 
634
+ const gateNote = wikilinkIssues.length
635
+ ? `\n\n⚠️ ${wikilinkIssues.length} wikilink issue(s):\n${wikilinkIssues.map((m) => `- ${m}`).join("\n")}`
636
+ : "";
598
637
  return {
599
- content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`` }],
600
- details: { path: pagePath, created: true } as Record<string, unknown>,
638
+ content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`${gateNote}` }],
639
+ details: {
640
+ path: pagePath,
641
+ created: true,
642
+ wikilinkIssues,
643
+ } as Record<string, unknown>,
601
644
  };
602
645
  },
603
646
  });
@@ -871,7 +914,7 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
871
914
 
872
915
  const discovery = discoverKnowledgeDocuments(paths);
873
916
  const pages = discovery.documents;
874
- const knownIds = new Set(pages.map((page) => page.id));
917
+ const wikilinkIndex = buildWikilinkIndex(pages.map((page) => page.id));
875
918
  const inbound = Object.fromEntries(pages.map((page) => [page.id, 0]));
876
919
  const gapSources = new Map<string, Set<string>>();
877
920
  const findings: string[] = [];
@@ -879,7 +922,7 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
879
922
  let contradictions = 0;
880
923
 
881
924
  for (const page of pages) {
882
- const resolved = buildResolvedBacklinks(page.id, page.body, knownIds);
925
+ const resolved = buildResolvedBacklinks(page.id, page.body, wikilinkIndex);
883
926
  for (const target of resolved.targets) inbound[target]++;
884
927
  for (const unresolved of resolved.unresolved) {
885
928
  const sources = gapSources.get(unresolved.target) ?? new Set<string>();
@@ -888,6 +931,11 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
888
931
  missingPages++;
889
932
  findings.push(`Missing page: ${unresolved.target} (in ${page.id})`);
890
933
  }
934
+ for (const d of resolved.diagnostics) {
935
+ if (d.code === "link_ambiguous") {
936
+ findings.push(d.message.replace("Ambiguous wikilink: ", "Ambiguous: "));
937
+ }
938
+ }
891
939
  }
892
940
 
893
941
  let orphans = 0;
package/mcp/index.ts CHANGED
@@ -15,6 +15,10 @@ import { join } from "node:path";
15
15
  import { McpServer } from "@modelcontextprotocol/server";
16
16
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
17
17
  import * as z from "zod/v4";
18
+ import {
19
+ loadTaskConfig,
20
+ resolveWikilinkValidation,
21
+ } from "../extensions/llm-wiki/lib/task-config.js";
18
22
  import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
19
23
  import { createExecApi } from "./exec.js";
20
24
  import {
@@ -265,7 +269,14 @@ server.registerTool(
265
269
  }
266
270
 
267
271
  const paths = getPaths();
268
- const result = await retroOperation(paths, slug, title, body, category);
272
+ const result = await retroOperation(
273
+ paths,
274
+ slug,
275
+ title,
276
+ body,
277
+ category,
278
+ resolveWikilinkValidation(loadTaskConfig(process.cwd())),
279
+ );
269
280
 
270
281
  if (!result.ok) {
271
282
  return {
package/mcp/operations.ts CHANGED
@@ -6,13 +6,20 @@
6
6
  * registry entries, or builds page strings itself.
7
7
  */
8
8
 
9
+ import { join } from "node:path";
9
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
11
  import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
12
+ import {
13
+ type WikilinkValidationMode,
14
+ applyWikilinkGate,
15
+ buildWikilinkIndex,
16
+ } from "../extensions/llm-wiki/lib/knowledge-links.js";
11
17
  import { type ProjectionResult, rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
12
18
  import { type RecallResult, searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
13
19
  import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
14
20
  import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
15
- import type { VaultPaths } from "../extensions/llm-wiki/lib/utils.js";
21
+ import { resolveWikilinkValidation } from "../extensions/llm-wiki/lib/task-config.js";
22
+ import { type VaultPaths, readJson } from "../extensions/llm-wiki/lib/utils.js";
16
23
  import {
17
24
  VaultWriteError,
18
25
  inspectVaultFormat,
@@ -134,6 +141,7 @@ export async function retroOperation(
134
141
  title: string,
135
142
  body: string,
136
143
  category?: string,
144
+ wikilinkValidation?: WikilinkValidationMode,
137
145
  ): Promise<
138
146
  | { ok: true; slug: string; sourcePagePath: string }
139
147
  | { ok: false; diagnostics: Array<{ code: string; message: string }> }
@@ -145,8 +153,30 @@ export async function retroOperation(
145
153
  diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
146
154
  };
147
155
  }
156
+ // Pre-write wikilink gate (#172): validate/normalize caller-supplied body.
157
+ const mode = resolveWikilinkValidation({ wikilinkValidation });
158
+ let gateBody = body;
159
+ if (mode !== "off") {
160
+ const registry = readJson<{ pages: Record<string, unknown> }>(
161
+ join(paths.meta, "registry.json"),
162
+ { pages: {} },
163
+ );
164
+ const gate = applyWikilinkGate(
165
+ body,
166
+ buildWikilinkIndex(Object.keys(registry.pages)),
167
+ `sources/${slug}`,
168
+ mode,
169
+ );
170
+ if (!gate.ok) {
171
+ return {
172
+ ok: false,
173
+ diagnostics: gate.diagnostics.map((d) => ({ code: "link_validation", message: d.message })),
174
+ };
175
+ }
176
+ if (mode === "normalize") gateBody = gate.body;
177
+ }
148
178
  try {
149
- const result = saveInsight(paths, slug, title, body, category, { rebuild: false });
179
+ const result = saveInsight(paths, slug, title, gateBody, category, { rebuild: false });
150
180
  const projection = projectionOutcome(rebuildMetadata(paths));
151
181
  if (!projection.ok) return projection;
152
182
  return { ok: true, slug: result.slug, sourcePagePath: result.sourcePagePath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
4
4
  "description": "Self-maintaining LLM Wiki for Pi — Karpathy-pattern knowledge base with immutable source capture, automated ingestion, search, linting, and Obsidian-compatible vault. auto-updating personal & company wiki.",
5
5
  "keywords": [
6
6
  "pi",
@@ -91,6 +91,7 @@
91
91
  "zod": "^4.0"
92
92
  },
93
93
  "devDependencies": {
94
+ "@astrojs/starlight": "^0.41.10",
94
95
  "@biomejs/biome": "^1.9.4",
95
96
  "@mariozechner/pi-agent-core": "^0.70.2",
96
97
  "@mariozechner/pi-ai": "^0.70.2",
@@ -99,6 +100,7 @@
99
100
  "@types/mdast": "^4.0.4",
100
101
  "@types/node": "^22",
101
102
  "@vitest/coverage-v8": "^3.2.4",
103
+ "astro": "^7.2.9",
102
104
  "typescript": "^5.7.0",
103
105
  "vitest": "^3.0.0"
104
106
  },
@@ -110,10 +112,13 @@
110
112
  "build:mcp": "node scripts/build-mcp.js",
111
113
  "typecheck": "tsc --noEmit",
112
114
  "lint": "biome check .",
113
- "lint:fix": "biome check --apply .",
115
+ "lint:fix": "biome check --write .",
114
116
  "release:patch": "node scripts/release.js patch",
115
117
  "release:minor": "node scripts/release.js minor",
116
118
  "release:major": "node scripts/release.js major",
117
- "release:push": "git push origin main --tags"
119
+ "release:push": "git push origin main --tags",
120
+ "docs:sync": "node scripts/sync-docs.js",
121
+ "docs:dev": "astro dev",
122
+ "docs:build": "node scripts/sync-docs.js && astro build"
118
123
  }
119
124
  }