@zosmaai/pi-llm-wiki 0.12.1 → 0.12.2
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/CHANGELOG.md +1 -0
- package/README.md +16 -8
- package/dist/extensions/llm-wiki/lib/bootstrap.js +2 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +24 -1
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +3 -1
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +41 -6
- package/dist/extensions/llm-wiki/lib/model-command.js +45 -8
- package/dist/extensions/llm-wiki/lib/qmd-indexing.js +1024 -0
- package/dist/extensions/llm-wiki/lib/qmd-mirror.js +418 -0
- package/dist/extensions/llm-wiki/lib/qmd-store.js +112 -0
- package/dist/extensions/llm-wiki/lib/recall.js +77 -3
- package/dist/extensions/llm-wiki/lib/runtime.js +25 -1
- package/dist/extensions/llm-wiki/lib/subagent.js +47 -7
- package/dist/extensions/llm-wiki/lib/tools.js +165 -5
- package/dist/extensions/llm-wiki/lib/utils.js +16 -2
- package/dist/extensions/llm-wiki/lib/wiki-service.js +104 -5
- package/dist/mcp/index.js +66 -2
- package/dist/mcp/operations.js +26 -2
- package/docs/api.md +43 -1
- package/docs/architecture.md +28 -0
- package/docs/commands.md +1 -0
- package/docs/qmd-compatibility.md +47 -0
- package/docs/retrieval-benchmark.md +47 -0
- package/docs/superpowers/benchmarks/phase-1-current-baseline.json +53 -0
- package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-2-remediation.md +549 -0
- package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-2-validated-indexing.md +1493 -0
- package/docs/superpowers/plans/2026-08-11-qmd-retrieval-phase-3-retrieval-modes-and-recall-cutover.md +678 -0
- package/docs/superpowers/plans/2026-09-05-wikilink-alias-pipe-table-only.md +257 -0
- package/extensions/llm-wiki/index.ts +14 -1
- package/extensions/llm-wiki/lib/bootstrap.ts +2 -0
- package/extensions/llm-wiki/lib/indexing.ts +24 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +10 -2
- package/extensions/llm-wiki/lib/knowledge-document.ts +8 -1
- package/extensions/llm-wiki/lib/knowledge-links.ts +39 -7
- package/extensions/llm-wiki/lib/model-command.ts +57 -12
- package/extensions/llm-wiki/lib/qmd-indexing.ts +1304 -0
- package/extensions/llm-wiki/lib/qmd-mirror.ts +496 -0
- package/extensions/llm-wiki/lib/qmd-store.ts +222 -0
- package/extensions/llm-wiki/lib/recall.ts +77 -3
- package/extensions/llm-wiki/lib/runtime.ts +57 -5
- package/extensions/llm-wiki/lib/subagent.ts +73 -10
- package/extensions/llm-wiki/lib/tools.ts +188 -4
- package/extensions/llm-wiki/lib/utils.ts +21 -2
- package/extensions/llm-wiki/lib/wiki-service.ts +160 -4
- package/mcp/index.ts +78 -1
- package/mcp/operations.ts +41 -2
- package/package.json +9 -6
- package/skills/llm-wiki/SKILL.md +7 -1
|
@@ -8,6 +8,8 @@ import { scheduleReindex } from "./indexing.js";
|
|
|
8
8
|
import { runIngestSynthesis } from "./ingest-worker.js";
|
|
9
9
|
import {
|
|
10
10
|
createKnowledgeDocument,
|
|
11
|
+
type KnowledgeValue,
|
|
12
|
+
parseMarkdownFrontmatter,
|
|
11
13
|
serializeKnowledgeDocument,
|
|
12
14
|
writeKnowledgeDocumentFile,
|
|
13
15
|
} from "./knowledge-document.js";
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
} from "./knowledge-links.js";
|
|
19
21
|
import { repairLegacyKnowledgeDocuments } from "./legacy-repair.js";
|
|
20
22
|
import { appendEvent, type Registry, rebuildMetadata, rebuildMetadataLight } from "./metadata.js";
|
|
23
|
+
import { readQmdIndexStatus, reindexQmdVault } from "./qmd-indexing.js";
|
|
21
24
|
import type { Runtime } from "./runtime.js";
|
|
22
25
|
import { captureFile, captureText, captureUrl } from "./source-packet.js";
|
|
23
26
|
import { loadTaskConfig, parseModelRef, resolveWikilinkValidation } from "./task-config.js";
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
38
41
|
inspectVaultFormat,
|
|
39
42
|
inspectWritableVault,
|
|
40
43
|
} from "./vault-format.js";
|
|
41
|
-
import { getWikiStatus, searchRegistry } from "./wiki-service.js";
|
|
44
|
+
import { getWikiStatus, reindexWiki, searchRegistry } from "./wiki-service.js";
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
47
|
* All LLM Wiki custom tools.
|
|
@@ -414,6 +417,8 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
414
417
|
model: resolved.model as Parameters<typeof runIngestSynthesis>[0]["model"],
|
|
415
418
|
apiKey: resolved.apiKey,
|
|
416
419
|
headers: resolved.headers,
|
|
420
|
+
streamFn: resolved.streamFn as Parameters<typeof runIngestSynthesis>[0]["streamFn"],
|
|
421
|
+
env: resolved.env,
|
|
417
422
|
paths,
|
|
418
423
|
sourceId: s.id,
|
|
419
424
|
manifest: s.manifest,
|
|
@@ -506,6 +511,11 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
506
511
|
|
|
507
512
|
// ─── 4. wiki_ensure_page ────────────────────────────────
|
|
508
513
|
|
|
514
|
+
// Frontmatter fields wiki_ensure_page generates or derives itself (title also
|
|
515
|
+
// determines the filename). Model-supplied values for these are ignored rather
|
|
516
|
+
// than merged (issue #241).
|
|
517
|
+
const RESERVED_FRONTMATTER = new Set(["type", "title", "created", "updated", "sources", "id"]);
|
|
518
|
+
|
|
509
519
|
export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
510
520
|
pi.registerTool({
|
|
511
521
|
name: "wiki_ensure_page",
|
|
@@ -515,6 +525,7 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
515
525
|
promptGuidelines: [
|
|
516
526
|
"Use wiki_ensure_page before creating pages to avoid duplicates.",
|
|
517
527
|
"Search existing pages first with wiki_search.",
|
|
528
|
+
"Content may begin with a YAML frontmatter block; its fields are merged into the page frontmatter, and generated fields are reserved.",
|
|
518
529
|
],
|
|
519
530
|
parameters: Type.Object({
|
|
520
531
|
type: Type.String({
|
|
@@ -523,7 +534,10 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
523
534
|
}),
|
|
524
535
|
title: Type.String({ description: "Page title" }),
|
|
525
536
|
content: Type.Optional(
|
|
526
|
-
Type.String({
|
|
537
|
+
Type.String({
|
|
538
|
+
description:
|
|
539
|
+
"Optional Markdown body. May begin with a YAML frontmatter block, whose fields are merged into the page frontmatter; generated fields (type, title, created, updated, sources) are reserved and ignored.",
|
|
540
|
+
}),
|
|
527
541
|
),
|
|
528
542
|
}),
|
|
529
543
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -568,6 +582,25 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
568
582
|
const today = fmtDate();
|
|
569
583
|
let body = params.content ?? buildPageBody(type, params.title);
|
|
570
584
|
|
|
585
|
+
// #241: models sometimes pass a YAML frontmatter block inside content.
|
|
586
|
+
// Consume it (with the same hardened parser used for page reads) and merge
|
|
587
|
+
// its fields into the generated frontmatter, instead of writing the block
|
|
588
|
+
// verbatim into the body (duplicate frontmatter, and the model's fields
|
|
589
|
+
// silently never took effect). Generated fields are reserved; a
|
|
590
|
+
// frontmatter-only content falls back to the template body.
|
|
591
|
+
const extraFrontmatter: Record<string, KnowledgeValue> = {};
|
|
592
|
+
if (body.trimStart().startsWith("---")) {
|
|
593
|
+
const parsed = parseMarkdownFrontmatter(body, `${folder}/${slug}.md`);
|
|
594
|
+
if (parsed.ok) {
|
|
595
|
+
for (const [key, value] of Object.entries(parsed.mapping)) {
|
|
596
|
+
if (!RESERVED_FRONTMATTER.has(key)) {
|
|
597
|
+
extraFrontmatter[key] = value;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
body = parsed.body.trim() ? parsed.body : buildPageBody(type, params.title);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
571
604
|
// Pre-write wikilink gate (#172): validate/normalize caller-supplied content.
|
|
572
605
|
const mode = resolveWikilinkValidation(loadTaskConfig(ctx.cwd));
|
|
573
606
|
let wikilinkIssues: string[] = [];
|
|
@@ -609,6 +642,7 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
609
642
|
title: params.title,
|
|
610
643
|
created: today,
|
|
611
644
|
updated: today,
|
|
645
|
+
...extraFrontmatter,
|
|
612
646
|
},
|
|
613
647
|
body,
|
|
614
648
|
);
|
|
@@ -875,8 +909,9 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
875
909
|
* Run the wiki health scan (issue #77 extracted it from the tool body so it can
|
|
876
910
|
* run off-thread via `dispatchReported`). Returns the human-readable summary.
|
|
877
911
|
*/
|
|
878
|
-
function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
912
|
+
async function runWikiLint(paths: VaultPaths, autoFix: boolean): Promise<string> {
|
|
879
913
|
assertWritableVault(paths);
|
|
914
|
+
const qmdStatus = await readQmdIndexStatus(paths);
|
|
880
915
|
let repair: ReturnType<typeof repairLegacyKnowledgeDocuments> | undefined;
|
|
881
916
|
if (autoFix) {
|
|
882
917
|
let projection = rebuildMetadata(paths);
|
|
@@ -1034,6 +1069,26 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
|
1034
1069
|
rebuildMetadataLight(paths);
|
|
1035
1070
|
}
|
|
1036
1071
|
|
|
1072
|
+
const qmdFindings: string[] = [];
|
|
1073
|
+
if (qmdStatus.state === "stale") {
|
|
1074
|
+
const components = JSON.stringify(
|
|
1075
|
+
qmdStatus.repairComponents.length > 0 ? qmdStatus.repairComponents : ["lexical"],
|
|
1076
|
+
);
|
|
1077
|
+
qmdFindings.push(
|
|
1078
|
+
`- QMD index stale (${qmdStatus.indexedManifestHash ? "manifest or model changed" : ""}): repair with \`wiki_reindex(scope="changed", components=${components}, vault="active")\``,
|
|
1079
|
+
);
|
|
1080
|
+
} else if (qmdStatus.state === "recovering") {
|
|
1081
|
+
qmdFindings.push(
|
|
1082
|
+
`- QMD swap interrupted (${qmdStatus.swapPhase ?? ""}): restart recovery via \`wiki_reindex(vault="active")\``,
|
|
1083
|
+
);
|
|
1084
|
+
} else if (qmdStatus.state === "error") {
|
|
1085
|
+
qmdFindings.push(
|
|
1086
|
+
`- QMD index error: ${qmdStatus.issues[0]?.message ?? "repair with wiki_reindex"} — \`wiki_reindex(scope="changed", components=${JSON.stringify(qmdStatus.repairComponents.length > 0 ? qmdStatus.repairComponents : ["lexical"])}, vault="active")\``,
|
|
1087
|
+
);
|
|
1088
|
+
} else if (qmdStatus.state === "missing") {
|
|
1089
|
+
qmdFindings.push("- QMD index not built yet (informational): run wiki_reindex to build it");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1037
1092
|
return [
|
|
1038
1093
|
"🧹 **LLM Wiki lint complete**",
|
|
1039
1094
|
"",
|
|
@@ -1047,6 +1102,10 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
|
1047
1102
|
reportPath ? `📄 Report: \`${reportPath}\`` : "",
|
|
1048
1103
|
repair?.manifestPath ? `🛟 Repair manifest: \`${repair.manifestPath}\`` : "",
|
|
1049
1104
|
gaps.length ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
|
|
1105
|
+
"",
|
|
1106
|
+
"## QMD Index",
|
|
1107
|
+
`- State: ${qmdStatus.state}`,
|
|
1108
|
+
...qmdFindings,
|
|
1050
1109
|
]
|
|
1051
1110
|
.filter(Boolean)
|
|
1052
1111
|
.join("\n");
|
|
@@ -1073,7 +1132,7 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
|
|
|
1073
1132
|
};
|
|
1074
1133
|
}
|
|
1075
1134
|
|
|
1076
|
-
const status = getWikiStatus(paths);
|
|
1135
|
+
const status = await getWikiStatus(paths);
|
|
1077
1136
|
const config = readJson<Record<string, unknown>>(join(paths.dotWiki, "config.json"), {});
|
|
1078
1137
|
const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
|
|
1079
1138
|
|
|
@@ -1108,6 +1167,10 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
|
|
|
1108
1167
|
`Gaps: ${gaps.gaps?.length || 0}`,
|
|
1109
1168
|
`Health: ${health}`,
|
|
1110
1169
|
`Last updated: ${status.lastUpdated || "Never"}`,
|
|
1170
|
+
`QMD index: ${status.qmd.state}`,
|
|
1171
|
+
`QMD documents: ${status.qmd.totalDocuments} (${status.qmd.canonicalDocuments} canonical, ${status.qmd.evidenceDocuments} evidence)`,
|
|
1172
|
+
`QMD embeddings pending: ${status.qmd.needsEmbedding}`,
|
|
1173
|
+
`QMD package: ${status.qmd.qmdVersion}`,
|
|
1111
1174
|
...diagLines,
|
|
1112
1175
|
];
|
|
1113
1176
|
|
|
@@ -1123,6 +1186,7 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
|
|
|
1123
1186
|
gaps: gaps.gaps?.length || 0,
|
|
1124
1187
|
health,
|
|
1125
1188
|
blockingDiagnostics: status.blockingDiagnostics,
|
|
1189
|
+
qmd: status.qmd,
|
|
1126
1190
|
} as Record<string, unknown>,
|
|
1127
1191
|
};
|
|
1128
1192
|
},
|
|
@@ -1170,6 +1234,22 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
|
|
|
1170
1234
|
const warnings = result.diagnostics.filter(
|
|
1171
1235
|
(diagnostic) => diagnostic.severity === "warning",
|
|
1172
1236
|
);
|
|
1237
|
+
// After a successful projection, keep the generated QMD index in sync
|
|
1238
|
+
// (model-free lexical pass). A QMD failure is a warning, never a
|
|
1239
|
+
// projection failure.
|
|
1240
|
+
const qmdResult = await reindexQmdVault(paths, {
|
|
1241
|
+
scope: "changed",
|
|
1242
|
+
components: ["lexical"],
|
|
1243
|
+
force: false,
|
|
1244
|
+
});
|
|
1245
|
+
if (!qmdResult.ok) {
|
|
1246
|
+
warnings.push({
|
|
1247
|
+
severity: "warning" as const,
|
|
1248
|
+
code: "qmd_index_error" as const,
|
|
1249
|
+
path: paths.qmd,
|
|
1250
|
+
message: qmdResult.errors[0]?.message ?? "QMD indexing failed",
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1173
1253
|
if (warnings.length > 0) {
|
|
1174
1254
|
return `⚠️ LLM Wiki: metadata rebuilt with warnings — ${warnings
|
|
1175
1255
|
.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
|
|
@@ -1259,6 +1339,110 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
|
|
|
1259
1339
|
});
|
|
1260
1340
|
}
|
|
1261
1341
|
|
|
1342
|
+
export function registerWikiReindex(pi: ExtensionAPI): void {
|
|
1343
|
+
pi.registerTool({
|
|
1344
|
+
name: "wiki_reindex",
|
|
1345
|
+
label: "Wiki Reindex QMD",
|
|
1346
|
+
description:
|
|
1347
|
+
"Rebuild or repair the generated QMD index (meta/qmd) for the vault. " +
|
|
1348
|
+
"Lexical indexing is model-free; selecting vectors may download " +
|
|
1349
|
+
"approximately 2 GB of models on first use. Repair stale/error state. " +
|
|
1350
|
+
"Active recall still uses the legacy heuristic until Phase 3.",
|
|
1351
|
+
promptSnippet: "Rebuild the QMD search index",
|
|
1352
|
+
promptGuidelines: [
|
|
1353
|
+
"Use wiki_reindex to repair a stale, error, or recovering QMD index.",
|
|
1354
|
+
"Lexical-only reindexing never loads a model.",
|
|
1355
|
+
"Vector reindexing may download approximately 2 GB of models on first use.",
|
|
1356
|
+
],
|
|
1357
|
+
parameters: Type.Object({
|
|
1358
|
+
scope: Type.Optional(
|
|
1359
|
+
Type.Union([Type.Literal("changed"), Type.Literal("all")], { default: "changed" }),
|
|
1360
|
+
),
|
|
1361
|
+
components: Type.Optional(
|
|
1362
|
+
Type.Array(Type.Union([Type.Literal("lexical"), Type.Literal("vectors")]), {
|
|
1363
|
+
minItems: 1,
|
|
1364
|
+
uniqueItems: true,
|
|
1365
|
+
default: ["lexical", "vectors"],
|
|
1366
|
+
}),
|
|
1367
|
+
),
|
|
1368
|
+
force: Type.Optional(Type.Boolean({ default: false })),
|
|
1369
|
+
vault: Type.Optional(
|
|
1370
|
+
Type.Union(
|
|
1371
|
+
[
|
|
1372
|
+
Type.Literal("active"),
|
|
1373
|
+
Type.Literal("personal"),
|
|
1374
|
+
Type.Literal("project"),
|
|
1375
|
+
Type.Literal("all"),
|
|
1376
|
+
],
|
|
1377
|
+
{ default: "active" },
|
|
1378
|
+
),
|
|
1379
|
+
),
|
|
1380
|
+
}),
|
|
1381
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1382
|
+
const paths = getPaths(ctx.cwd);
|
|
1383
|
+
const vaultCheck = inspectWritableVault(paths);
|
|
1384
|
+
if (!vaultCheck.ok) {
|
|
1385
|
+
return {
|
|
1386
|
+
content: [
|
|
1387
|
+
{ type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
|
|
1388
|
+
],
|
|
1389
|
+
details: {
|
|
1390
|
+
error: vaultCheck.diagnostics[0].code,
|
|
1391
|
+
diagnostics: vaultCheck.diagnostics,
|
|
1392
|
+
} as Record<string, unknown>,
|
|
1393
|
+
isError: true,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const scope = params.scope ?? "changed";
|
|
1398
|
+
const components = params.components ?? ["lexical", "vectors"];
|
|
1399
|
+
const force = params.force === true;
|
|
1400
|
+
const vault = params.vault ?? "active";
|
|
1401
|
+
const lexicalOnly = components.length === 1 && components[0] === "lexical";
|
|
1402
|
+
|
|
1403
|
+
// Warn before any vector work so the operator expects a large download.
|
|
1404
|
+
if (components.includes("vectors") && signal && signal.aborted) {
|
|
1405
|
+
return {
|
|
1406
|
+
content: [{ type: "text", text: "QMD reindex cancelled before it started." }],
|
|
1407
|
+
details: { cancelled: true } as Record<string, unknown>,
|
|
1408
|
+
isError: true,
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const result = await reindexWiki(paths, {
|
|
1413
|
+
scope,
|
|
1414
|
+
components,
|
|
1415
|
+
force,
|
|
1416
|
+
vault,
|
|
1417
|
+
signal,
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
const ok = result.results.every((r) => r.result.ok);
|
|
1421
|
+
const lines = [
|
|
1422
|
+
ok ? "✅ QMD indexing complete" : "⚠️ QMD indexing completed with errors",
|
|
1423
|
+
...result.results.map((r) => {
|
|
1424
|
+
const st = r.result.status;
|
|
1425
|
+
return `- ${r.label} (${r.root}): state=${st.state}, documents=${st.totalDocuments}, indexed=${r.result.documents.indexed}, updated=${r.result.documents.updated}, removed=${r.result.documents.removed}, vectors=${r.result.vectors.generated}`;
|
|
1426
|
+
}),
|
|
1427
|
+
];
|
|
1428
|
+
if (lexicalOnly) lines.push("Model-free lexical indexing — no model was downloaded.");
|
|
1429
|
+
if (components.includes("vectors")) {
|
|
1430
|
+
lines.push("⚠️ Vector indexing may download approximately 2 GB of models on first use.");
|
|
1431
|
+
}
|
|
1432
|
+
for (const r of result.results) {
|
|
1433
|
+
for (const e of r.result.errors) lines.push(`- [${r.label}] ${e.code}: ${e.message}`);
|
|
1434
|
+
for (const w of r.result.warnings) lines.push(`- [${r.label}] ${w.code}: ${w.message}`);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return {
|
|
1438
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1439
|
+
details: { scope, components, ...result } as Record<string, unknown>,
|
|
1440
|
+
...(ok ? {} : { isError: true }),
|
|
1441
|
+
};
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1262
1446
|
export function registerWikiLogEvent(pi: ExtensionAPI): void {
|
|
1263
1447
|
pi.registerTool({
|
|
1264
1448
|
name: "wiki_log_event",
|
|
@@ -28,6 +28,11 @@ export interface VaultPaths {
|
|
|
28
28
|
dotWiki: string;
|
|
29
29
|
outputs: string;
|
|
30
30
|
discoveries: string;
|
|
31
|
+
qmd: string;
|
|
32
|
+
qmdCurrent: string;
|
|
33
|
+
qmdDocuments: string;
|
|
34
|
+
qmdManifest: string;
|
|
35
|
+
qmdSwap: string;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/** Detect whether a vault root uses new (.llm-wiki) or legacy (.wiki) layout. */
|
|
@@ -200,31 +205,45 @@ export function resolveVaultRoot(cwd: string): string {
|
|
|
200
205
|
|
|
201
206
|
/** Get all vault paths for the new (.llm-wiki) layout. */
|
|
202
207
|
export function getVaultPaths(root: string): VaultPaths {
|
|
208
|
+
const meta = join(root, ".llm-wiki", "meta");
|
|
209
|
+
const qmd = join(meta, "qmd");
|
|
203
210
|
return {
|
|
204
211
|
root,
|
|
205
212
|
raw: join(root, ".llm-wiki", "raw"),
|
|
206
213
|
rawSources: join(root, ".llm-wiki", "raw", "sources"),
|
|
207
214
|
rawTrajectories: join(root, ".llm-wiki", "raw", "trajectories"),
|
|
208
215
|
wiki: join(root, ".llm-wiki", "wiki"),
|
|
209
|
-
meta
|
|
216
|
+
meta,
|
|
210
217
|
dotWiki: join(root, ".llm-wiki"),
|
|
211
218
|
outputs: join(root, ".llm-wiki", "outputs"),
|
|
212
219
|
discoveries: join(root, ".llm-wiki", ".discoveries"),
|
|
220
|
+
qmd,
|
|
221
|
+
qmdCurrent: join(qmd, "current"),
|
|
222
|
+
qmdDocuments: join(qmd, "documents"),
|
|
223
|
+
qmdManifest: join(qmd, "manifest.json"),
|
|
224
|
+
qmdSwap: join(qmd, "swap.json"),
|
|
213
225
|
};
|
|
214
226
|
}
|
|
215
227
|
|
|
216
228
|
/** Get all vault paths for the legacy (.wiki) layout. */
|
|
217
229
|
export function getLegacyVaultPaths(root: string): VaultPaths {
|
|
230
|
+
const meta = join(root, "meta");
|
|
231
|
+
const qmd = join(meta, "qmd");
|
|
218
232
|
return {
|
|
219
233
|
root,
|
|
220
234
|
raw: join(root, "raw"),
|
|
221
235
|
rawSources: join(root, "raw", "sources"),
|
|
222
236
|
rawTrajectories: join(root, "raw", "trajectories"),
|
|
223
237
|
wiki: join(root, "wiki"),
|
|
224
|
-
meta
|
|
238
|
+
meta,
|
|
225
239
|
dotWiki: join(root, ".wiki"),
|
|
226
240
|
outputs: join(root, "outputs"),
|
|
227
241
|
discoveries: join(root, ".discoveries"),
|
|
242
|
+
qmd,
|
|
243
|
+
qmdCurrent: join(qmd, "current"),
|
|
244
|
+
qmdDocuments: join(qmd, "documents"),
|
|
245
|
+
qmdManifest: join(qmd, "manifest.json"),
|
|
246
|
+
qmdSwap: join(qmd, "swap.json"),
|
|
228
247
|
};
|
|
229
248
|
}
|
|
230
249
|
|
|
@@ -2,13 +2,24 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { KnowledgeDiagnostic } from "./knowledge-document.js";
|
|
4
4
|
import type { Registry } from "./metadata.js";
|
|
5
|
+
import {
|
|
6
|
+
awaitQmdIndexQueue,
|
|
7
|
+
type QmdGeneratedStatus,
|
|
8
|
+
type QmdIndexProgress,
|
|
9
|
+
type QmdIndexState,
|
|
10
|
+
type QmdReindexResult,
|
|
11
|
+
readQmdIndexStatus,
|
|
12
|
+
reindexQmdVault,
|
|
13
|
+
} from "./qmd-indexing.js";
|
|
14
|
+
import { QMD_PACKAGE_VERSION, resolveQmdModels } from "./qmd-store.js";
|
|
5
15
|
import type { VaultPaths } from "./utils.js";
|
|
6
|
-
import { readJson } from "./utils.js";
|
|
16
|
+
import { getPersonalWikiPaths, isPersonalVault, readJson } from "./utils.js";
|
|
7
17
|
import type { KnowledgeFormat } from "./vault-format.js";
|
|
8
18
|
import {
|
|
9
19
|
compareCodePoint,
|
|
10
20
|
discoverKnowledgeDocuments,
|
|
11
21
|
inspectVaultFormat,
|
|
22
|
+
inspectWritableVault,
|
|
12
23
|
} from "./vault-format.js";
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -29,12 +40,13 @@ export interface WikiStatusSnapshot {
|
|
|
29
40
|
byType: Record<string, number>;
|
|
30
41
|
blockingDiagnostics: KnowledgeDiagnostic[];
|
|
31
42
|
lastUpdated: string;
|
|
43
|
+
qmd: QmdGeneratedStatus;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/**
|
|
35
47
|
* Search the registry for matching concepts.
|
|
36
48
|
*
|
|
37
|
-
* Matches ID,
|
|
49
|
+
* Matches ID, title, type, state, status, category, domain, tags, aliases, and recall triggers.
|
|
38
50
|
* Preserves unknown types as strings.
|
|
39
51
|
*/
|
|
40
52
|
export function searchRegistry(
|
|
@@ -102,6 +114,22 @@ function matchesField(id: string, entry: Record<string, unknown>, query: string)
|
|
|
102
114
|
)
|
|
103
115
|
return true;
|
|
104
116
|
|
|
117
|
+
// Match state
|
|
118
|
+
if (
|
|
119
|
+
String(entry.state || "")
|
|
120
|
+
.toLowerCase()
|
|
121
|
+
.includes(query)
|
|
122
|
+
)
|
|
123
|
+
return true;
|
|
124
|
+
|
|
125
|
+
// Match status
|
|
126
|
+
if (
|
|
127
|
+
String(entry.status || "")
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.includes(query)
|
|
130
|
+
)
|
|
131
|
+
return true;
|
|
132
|
+
|
|
105
133
|
// Match category/domain
|
|
106
134
|
if (
|
|
107
135
|
String(entry.category || "")
|
|
@@ -146,9 +174,10 @@ function matchesField(id: string, entry: Record<string, unknown>, query: string)
|
|
|
146
174
|
/**
|
|
147
175
|
* Get a status snapshot of the wiki.
|
|
148
176
|
*
|
|
149
|
-
* Reports resolved knowledge_format, page counts,
|
|
177
|
+
* Reports resolved knowledge_format, page counts, blocking diagnostics, and
|
|
178
|
+
* generated QMD index status (read without opening any QMD store).
|
|
150
179
|
*/
|
|
151
|
-
export function getWikiStatus(paths: VaultPaths): WikiStatusSnapshot {
|
|
180
|
+
export async function getWikiStatus(paths: VaultPaths): Promise<WikiStatusSnapshot> {
|
|
152
181
|
const vaultState = inspectVaultFormat(paths);
|
|
153
182
|
const diagnostics = [...vaultState.diagnostics];
|
|
154
183
|
|
|
@@ -179,5 +208,132 @@ export function getWikiStatus(paths: VaultPaths): WikiStatusSnapshot {
|
|
|
179
208
|
byType,
|
|
180
209
|
blockingDiagnostics: diagnostics.filter((d) => d.severity === "error"),
|
|
181
210
|
lastUpdated: registry?.last_updated || "",
|
|
211
|
+
qmd: await readQmdIndexStatus(paths),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Shared QMD reindex operation ─────────────────────────
|
|
216
|
+
|
|
217
|
+
export type WikiReindexVault = "active" | "personal" | "project" | "all";
|
|
218
|
+
|
|
219
|
+
export interface WikiReindexInput {
|
|
220
|
+
scope?: "changed" | "all";
|
|
221
|
+
components?: Array<"lexical" | "vectors">;
|
|
222
|
+
force?: boolean;
|
|
223
|
+
vault?: WikiReindexVault;
|
|
224
|
+
signal?: AbortSignal;
|
|
225
|
+
onProgress?: (progress: { vault: string; progress: QmdIndexProgress }) => void;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface WikiReindexResult {
|
|
229
|
+
vault: WikiReindexVault;
|
|
230
|
+
results: Array<{
|
|
231
|
+
root: string;
|
|
232
|
+
label: "active" | "personal" | "project";
|
|
233
|
+
result: QmdReindexResult;
|
|
234
|
+
}>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function blockedReindexResult(
|
|
238
|
+
scope: "changed" | "all",
|
|
239
|
+
components: Array<"lexical" | "vectors">,
|
|
240
|
+
code: string,
|
|
241
|
+
message: string,
|
|
242
|
+
): QmdReindexResult {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
scope,
|
|
246
|
+
components,
|
|
247
|
+
documents: { indexed: 0, updated: 0, unchanged: 0, removed: 0 },
|
|
248
|
+
vectors: { generated: 0, skipped: 0, errors: 0 },
|
|
249
|
+
elapsedMs: 0,
|
|
250
|
+
status: {
|
|
251
|
+
state: "error" as QmdIndexState,
|
|
252
|
+
qmdVersion: QMD_PACKAGE_VERSION,
|
|
253
|
+
models: resolveQmdModels(),
|
|
254
|
+
totalDocuments: 0,
|
|
255
|
+
canonicalDocuments: 0,
|
|
256
|
+
evidenceDocuments: 0,
|
|
257
|
+
needsEmbedding: 0,
|
|
258
|
+
hasVectorIndex: false,
|
|
259
|
+
repairComponents: [],
|
|
260
|
+
issues: [{ code, message }],
|
|
261
|
+
},
|
|
262
|
+
warnings: [],
|
|
263
|
+
errors: [{ code, message }],
|
|
182
264
|
};
|
|
183
265
|
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reindex QMD stores for selected vaults, sequentially. Validates each vault
|
|
269
|
+
* immediately before work. One vault's failure does not prevent the others.
|
|
270
|
+
*/
|
|
271
|
+
export async function reindexWiki(
|
|
272
|
+
activePaths: VaultPaths,
|
|
273
|
+
input: WikiReindexInput,
|
|
274
|
+
): Promise<WikiReindexResult> {
|
|
275
|
+
const scope = input.scope ?? "changed";
|
|
276
|
+
const components = input.components ?? ["lexical", "vectors"];
|
|
277
|
+
const force = input.force ?? false;
|
|
278
|
+
const vault = input.vault ?? "active";
|
|
279
|
+
const signal = input.signal;
|
|
280
|
+
const onProgress = input.onProgress;
|
|
281
|
+
|
|
282
|
+
const personalPaths = getPersonalWikiPaths();
|
|
283
|
+
const activeIsPersonal = isPersonalVault(activePaths);
|
|
284
|
+
|
|
285
|
+
const targets: Array<{ paths: VaultPaths; label: "active" | "personal" | "project" }> = [];
|
|
286
|
+
switch (vault) {
|
|
287
|
+
case "active":
|
|
288
|
+
targets.push({ paths: activePaths, label: activeIsPersonal ? "personal" : "active" });
|
|
289
|
+
break;
|
|
290
|
+
case "personal":
|
|
291
|
+
targets.push({ paths: personalPaths, label: "personal" });
|
|
292
|
+
break;
|
|
293
|
+
case "project":
|
|
294
|
+
if (!activeIsPersonal) targets.push({ paths: activePaths, label: "project" });
|
|
295
|
+
break;
|
|
296
|
+
case "all":
|
|
297
|
+
if (!activeIsPersonal) targets.push({ paths: activePaths, label: "project" });
|
|
298
|
+
targets.push({ paths: personalPaths, label: "personal" });
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const seen = new Set<string>();
|
|
303
|
+
const results: WikiReindexResult["results"] = [];
|
|
304
|
+
for (const target of targets) {
|
|
305
|
+
if (seen.has(target.paths.root)) continue;
|
|
306
|
+
seen.add(target.paths.root);
|
|
307
|
+
|
|
308
|
+
const check = inspectWritableVault(target.paths);
|
|
309
|
+
if (!check.ok) {
|
|
310
|
+
results.push({
|
|
311
|
+
root: target.paths.root,
|
|
312
|
+
label: target.label,
|
|
313
|
+
result: blockedReindexResult(
|
|
314
|
+
scope,
|
|
315
|
+
components,
|
|
316
|
+
check.diagnostics[0]?.code ?? "config_invalid_knowledge_format",
|
|
317
|
+
check.diagnostics[0]?.message ?? "Vault is not writable",
|
|
318
|
+
),
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const result = await reindexQmdVault(target.paths, {
|
|
324
|
+
scope,
|
|
325
|
+
components,
|
|
326
|
+
force,
|
|
327
|
+
signal,
|
|
328
|
+
onProgress: (p) => onProgress?.({ vault: target.paths.root, progress: p }),
|
|
329
|
+
});
|
|
330
|
+
results.push({ root: target.paths.root, label: target.label, result });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { vault, results };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Test-only: drain/await in-process QMD reindex queue work for a vault root. */
|
|
337
|
+
export function awaitWikiQmdIndexQueue(root: string): Promise<unknown> {
|
|
338
|
+
return awaitQmdIndexQueue(root);
|
|
339
|
+
}
|
package/mcp/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ 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 { recoverQmdIndex } from "../extensions/llm-wiki/lib/qmd-indexing.js";
|
|
18
19
|
import {
|
|
19
20
|
loadTaskConfig,
|
|
20
21
|
resolveWikilinkValidation,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
bootstrapOperation,
|
|
26
27
|
captureSourceOperation,
|
|
27
28
|
recallOperation,
|
|
29
|
+
reindexOperation,
|
|
28
30
|
retroOperation,
|
|
29
31
|
searchOperation,
|
|
30
32
|
statusOperation,
|
|
@@ -68,7 +70,7 @@ const server = new McpServer({
|
|
|
68
70
|
// ---- wiki_bootstrap ----
|
|
69
71
|
//
|
|
70
72
|
// Registered first, and the only tool not gated on an existing vault: the
|
|
71
|
-
// other
|
|
73
|
+
// other tools fail closed with a message naming this one, which an MCP-only
|
|
72
74
|
// client could not act on while it was extension-only (issue #130).
|
|
73
75
|
|
|
74
76
|
server.registerTool(
|
|
@@ -238,6 +240,65 @@ server.registerTool(
|
|
|
238
240
|
},
|
|
239
241
|
);
|
|
240
242
|
|
|
243
|
+
// ---- wiki_reindex ----
|
|
244
|
+
|
|
245
|
+
server.registerTool(
|
|
246
|
+
"wiki_reindex",
|
|
247
|
+
{
|
|
248
|
+
description:
|
|
249
|
+
"Rebuild or repair the generated QMD search index (meta/qmd) for the vault. " +
|
|
250
|
+
"Lexical indexing is model-free; selecting vectors may download approximately 2 GB " +
|
|
251
|
+
"of models on first use. Options: scope (changed|all), components (lexical|vectors), " +
|
|
252
|
+
"force, vault (active|personal|project|all).",
|
|
253
|
+
inputSchema: z.object({
|
|
254
|
+
scope: z.enum(["changed", "all"]).optional().default("changed").describe("changed or all"),
|
|
255
|
+
components: z
|
|
256
|
+
.array(z.enum(["lexical", "vectors"]))
|
|
257
|
+
.min(1)
|
|
258
|
+
.optional()
|
|
259
|
+
.describe("Index components (lexical|vectors)"),
|
|
260
|
+
force: z.boolean().optional().describe("Force full rebuild of selected components"),
|
|
261
|
+
vault: z
|
|
262
|
+
.enum(["active", "personal", "project", "all"])
|
|
263
|
+
.optional()
|
|
264
|
+
.default("active")
|
|
265
|
+
.describe("Which vaults to reindex"),
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
async ({ scope, components, force, vault }) => {
|
|
269
|
+
if (!hasVault()) {
|
|
270
|
+
return {
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "text" as const,
|
|
274
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
isError: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const paths = getPaths();
|
|
282
|
+
const result = await reindexOperation(paths, {
|
|
283
|
+
scope,
|
|
284
|
+
components,
|
|
285
|
+
force,
|
|
286
|
+
vault,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const ok = result.results.every((r) => r.result.ok);
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "text" as const,
|
|
294
|
+
text: JSON.stringify(result, null, 2),
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
...(ok ? {} : { isError: true as const }),
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
241
302
|
// ---- wiki_retro ----
|
|
242
303
|
|
|
243
304
|
server.registerTool(
|
|
@@ -363,6 +424,22 @@ async function main() {
|
|
|
363
424
|
const transport = new StdioServerTransport();
|
|
364
425
|
await server.connect(transport);
|
|
365
426
|
console.error("🧠 LLM Wiki MCP Server running on stdio");
|
|
427
|
+
|
|
428
|
+
// Fire-and-forget QMD index recovery AFTER the transport is connected so
|
|
429
|
+
// clients are never blocked. A busy/live lock just logs a warning and MCP
|
|
430
|
+
// continues with current state untouched.
|
|
431
|
+
if (hasVault()) {
|
|
432
|
+
const paths = getPaths();
|
|
433
|
+
recoverQmdIndex(paths)
|
|
434
|
+
.then((result) => {
|
|
435
|
+
if (!result.ok) {
|
|
436
|
+
console.error(
|
|
437
|
+
`[llm-wiki] QMD recovery skipped: ${result.diagnostics[0]?.message ?? "locked"}`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
.catch((err) => console.error(`[llm-wiki] QMD recovery failed: ${(err as Error).message}`));
|
|
442
|
+
}
|
|
366
443
|
}
|
|
367
444
|
|
|
368
445
|
main().catch((err) => {
|