@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.
- package/README.de.md +1 -0
- package/README.es.md +1 -0
- package/README.fr.md +1 -0
- package/README.hi.md +1 -0
- package/README.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/README.pt.md +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +43 -20
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +127 -24
- package/dist/extensions/llm-wiki/lib/metadata.js +5 -5
- package/dist/extensions/llm-wiki/lib/retro.js +38 -4
- package/dist/extensions/llm-wiki/lib/runtime.js +2 -2
- package/dist/extensions/llm-wiki/lib/task-config.js +35 -0
- package/dist/extensions/llm-wiki/lib/tools.js +53 -12
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/operations.js +21 -2
- package/docs/configuration.md +1 -0
- package/docs/obsidian.md +6 -6
- package/docs/superpowers/plans/2026-08-27-wikilink-resolver-normalization.md +735 -0
- package/docs/superpowers/plans/2026-08-29-wikilink-gate-ensure-page-retro.md +642 -0
- package/docs/superpowers/plans/2026-08-29-wikilink-write-validation.md +695 -0
- package/extensions/llm-wiki/lib/ingest-worker.ts +60 -27
- package/extensions/llm-wiki/lib/knowledge-document.ts +1 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +201 -32
- package/extensions/llm-wiki/lib/metadata.ts +9 -5
- package/extensions/llm-wiki/lib/retro.ts +48 -4
- package/extensions/llm-wiki/lib/runtime.ts +2 -2
- package/extensions/llm-wiki/lib/task-config.ts +59 -0
- package/extensions/llm-wiki/lib/tools.ts +68 -20
- package/mcp/index.ts +12 -1
- package/mcp/operations.ts +32 -2
- 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
|
|
443
|
-
const
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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,
|
|
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: [
|
package/dist/mcp/operations.js
CHANGED
|
@@ -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,
|
|
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;
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
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
|
|
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
|
-
|
|
21
|
+
For custom dashboards, use Dataview queries against frontmatter fields like `type`, `domain`, `category`, `sources`. Query the `wiki/` directory for knowledge pages.
|