@zosmaai/pi-llm-wiki 0.8.2 → 0.9.1
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 +41 -0
- package/extensions/llm-wiki/index.ts +81 -14
- package/extensions/llm-wiki/lib/embeddings.ts +420 -0
- package/extensions/llm-wiki/lib/guardrails.ts +15 -4
- package/extensions/llm-wiki/lib/indexing.ts +88 -0
- package/extensions/llm-wiki/lib/ingest-worker.ts +281 -0
- package/extensions/llm-wiki/lib/model-command.ts +128 -0
- package/extensions/llm-wiki/lib/observation.ts +84 -21
- package/extensions/llm-wiki/lib/recall.ts +331 -10
- package/extensions/llm-wiki/lib/retro.ts +13 -4
- package/extensions/llm-wiki/lib/runtime.ts +264 -0
- package/extensions/llm-wiki/lib/subagent.ts +82 -0
- package/extensions/llm-wiki/lib/task-config.ts +217 -0
- package/extensions/llm-wiki/lib/tools.ts +369 -149
- package/package.json +1 -1
- package/prompts/wiki-ingest.md +7 -4
- package/skills/llm-wiki/SKILL.md +30 -0
|
@@ -2,6 +2,9 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
+
import { launchEmbedPages, reindexEmbeddings, resolveEmbedder } from "./embeddings.js";
|
|
6
|
+
import { scheduleReindex } from "./indexing.js";
|
|
7
|
+
import { runIngestSynthesis } from "./ingest-worker.js";
|
|
5
8
|
import {
|
|
6
9
|
type Registry,
|
|
7
10
|
appendEvent,
|
|
@@ -10,7 +13,9 @@ import {
|
|
|
10
13
|
rebuildMetadata,
|
|
11
14
|
rebuildMetadataLight,
|
|
12
15
|
} from "./metadata.js";
|
|
16
|
+
import type { Runtime } from "./runtime.js";
|
|
13
17
|
import { captureFile, captureText, captureUrl } from "./source-packet.js";
|
|
18
|
+
import { parseModelRef } from "./task-config.js";
|
|
14
19
|
import {
|
|
15
20
|
type VaultPaths,
|
|
16
21
|
detectVaultFormat,
|
|
@@ -39,6 +44,55 @@ function requireVault(paths: VaultPaths): { ok: true } | { ok: false; reason: st
|
|
|
39
44
|
return { ok: true };
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
type WikiToolResult = {
|
|
48
|
+
content: { type: "text"; text: string }[];
|
|
49
|
+
details: Record<string, unknown>;
|
|
50
|
+
isError?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ToolCtx = {
|
|
54
|
+
cwd?: string;
|
|
55
|
+
hasUI: boolean;
|
|
56
|
+
ui?: { notify: (message: string, type?: string) => void };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dispatch a heavy mutating action to the background runtime and report its
|
|
61
|
+
* result (issue #77). The agent turn is never blocked: `work` runs off-thread
|
|
62
|
+
* and the returned one-line summary is surfaced to the user via
|
|
63
|
+
* `runtime.report()`. Returns an immediate, non-blocking tool result.
|
|
64
|
+
*
|
|
65
|
+
* When no runtime is available (unit tests / degraded mode), `work` runs
|
|
66
|
+
* synchronously and its summary is returned inline, preserving prior behavior.
|
|
67
|
+
* Retrieval tools (search/read/recall/status) never use this — the model needs
|
|
68
|
+
* their output inline.
|
|
69
|
+
*/
|
|
70
|
+
async function dispatchReported(
|
|
71
|
+
runtime: Runtime | undefined,
|
|
72
|
+
ctx: ToolCtx,
|
|
73
|
+
opts: {
|
|
74
|
+
label: string;
|
|
75
|
+
/** Immediate, non-blocking acknowledgement shown while work runs. */
|
|
76
|
+
started: string;
|
|
77
|
+
/** Off-thread work; resolves to the human-readable completion summary. */
|
|
78
|
+
work: () => Promise<string>;
|
|
79
|
+
details?: Record<string, unknown>;
|
|
80
|
+
},
|
|
81
|
+
): Promise<WikiToolResult> {
|
|
82
|
+
if (!runtime) {
|
|
83
|
+
const summary = await opts.work();
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: summary }],
|
|
86
|
+
details: { background: false, ...opts.details },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
runtime.launchReported({ hasUI: ctx.hasUI, ui: ctx.ui }, opts.label, opts.work);
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: opts.started }],
|
|
92
|
+
details: { background: true, ...opts.details },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
42
96
|
// ─── 1. wiki_bootstrap ──────────────────────────────────
|
|
43
97
|
|
|
44
98
|
export function registerWikiBootstrap(pi: ExtensionAPI): void {
|
|
@@ -142,7 +196,7 @@ export function registerWikiBootstrap(pi: ExtensionAPI): void {
|
|
|
142
196
|
|
|
143
197
|
// ─── 2. wiki_capture_source ─────────────────────────────
|
|
144
198
|
|
|
145
|
-
export function registerWikiCaptureSource(pi: ExtensionAPI): void {
|
|
199
|
+
export function registerWikiCaptureSource(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
146
200
|
pi.registerTool({
|
|
147
201
|
name: "wiki_capture_source",
|
|
148
202
|
label: "Wiki Capture Source",
|
|
@@ -191,7 +245,11 @@ export function registerWikiCaptureSource(pi: ExtensionAPI): void {
|
|
|
191
245
|
};
|
|
192
246
|
}
|
|
193
247
|
|
|
194
|
-
|
|
248
|
+
if (runtime) {
|
|
249
|
+
scheduleReindex(runtime, { hasUI: ctx.hasUI, ui: ctx.ui }, paths);
|
|
250
|
+
} else {
|
|
251
|
+
rebuildMetadataLight(paths);
|
|
252
|
+
}
|
|
195
253
|
|
|
196
254
|
return {
|
|
197
255
|
content: [
|
|
@@ -220,16 +278,17 @@ export function registerWikiCaptureSource(pi: ExtensionAPI): void {
|
|
|
220
278
|
|
|
221
279
|
// ─── 3. wiki_ingest ─────────────────────────────────────
|
|
222
280
|
|
|
223
|
-
export function registerWikiIngest(pi: ExtensionAPI): void {
|
|
281
|
+
export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
224
282
|
pi.registerTool({
|
|
225
283
|
name: "wiki_ingest",
|
|
226
284
|
label: "Wiki Ingest",
|
|
227
285
|
description:
|
|
228
|
-
"Process uningested source packets.
|
|
229
|
-
promptSnippet: "Ingest source packets
|
|
286
|
+
"Process uningested source packets. By default synthesis runs in the background (non-blocking) on the configured task model; pass background=false to return extracted content for the main agent to synthesize itself.",
|
|
287
|
+
promptSnippet: "Ingest source packets (background synthesis by default)",
|
|
230
288
|
promptGuidelines: [
|
|
231
289
|
"Use wiki_ingest when the user wants to process captured sources.",
|
|
232
|
-
"
|
|
290
|
+
"By default ingestion runs in the BACKGROUND — you'll get a notification, not extracted content. Do NOT synthesize those sources yourself.",
|
|
291
|
+
"If the tool returns extracted content (background unavailable, or background=false), then read each source's extracted.md, update its source page, create entity/concept pages, and cross-reference.",
|
|
233
292
|
"The extension auto-updates metadata — you do NOT need to edit meta/ files.",
|
|
234
293
|
],
|
|
235
294
|
parameters: Type.Object({
|
|
@@ -237,7 +296,20 @@ export function registerWikiIngest(pi: ExtensionAPI): void {
|
|
|
237
296
|
Type.String({ description: "Specific source ID to ingest. Leave empty for all new." }),
|
|
238
297
|
),
|
|
239
298
|
batch_size: Type.Optional(
|
|
240
|
-
Type.Number({ description: "Max sources to
|
|
299
|
+
Type.Number({ description: "Max sources to process (default: 3, max: 5)", default: 3 }),
|
|
300
|
+
),
|
|
301
|
+
background: Type.Optional(
|
|
302
|
+
Type.Boolean({
|
|
303
|
+
description:
|
|
304
|
+
"Synthesize in the background without blocking (default: true). Set false to return extracted content for the main agent to synthesize.",
|
|
305
|
+
default: true,
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
model: Type.Optional(
|
|
309
|
+
Type.String({
|
|
310
|
+
description:
|
|
311
|
+
"Per-call model override as 'provider/id' (e.g. anthropic/claude-haiku). Overrides the configured wiki taskModel for this call; defaults to the configured/session model.",
|
|
312
|
+
}),
|
|
241
313
|
),
|
|
242
314
|
}),
|
|
243
315
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -321,6 +393,76 @@ export function registerWikiIngest(pi: ExtensionAPI): void {
|
|
|
321
393
|
return { id, extracted, manifest };
|
|
322
394
|
});
|
|
323
395
|
|
|
396
|
+
// ── Background synthesis (issue #65) ──────────────────
|
|
397
|
+
// Default path: dispatch each source to a background sub-agent so the
|
|
398
|
+
// main agent is not blocked. Falls back to the synchronous return below
|
|
399
|
+
// when no runtime/model is available (resolveModel ok:false).
|
|
400
|
+
const wantBackground = params.background !== false;
|
|
401
|
+
if (wantBackground && runtime) {
|
|
402
|
+
runtime.ensureConfig(ctx.cwd);
|
|
403
|
+
// Per-call model override (issue #69): 'provider/id' beats the
|
|
404
|
+
// configured taskModel; a malformed/unknown ref degrades to the
|
|
405
|
+
// configured/session model inside resolveModel.
|
|
406
|
+
const override = params.model ? parseModelRef(params.model) : undefined;
|
|
407
|
+
const resolved = await runtime.resolveModel(ctx, override);
|
|
408
|
+
if (resolved.ok) {
|
|
409
|
+
const launchCtx = { hasUI: ctx.hasUI, ui: ctx.ui };
|
|
410
|
+
for (const s of sources) {
|
|
411
|
+
runtime.launchTask(launchCtx, `ingest:${s.id}`, async () => {
|
|
412
|
+
const committed = await runIngestSynthesis({
|
|
413
|
+
model: resolved.model as Parameters<typeof runIngestSynthesis>[0]["model"],
|
|
414
|
+
apiKey: resolved.apiKey,
|
|
415
|
+
headers: resolved.headers,
|
|
416
|
+
paths,
|
|
417
|
+
sourceId: s.id,
|
|
418
|
+
manifest: s.manifest,
|
|
419
|
+
extracted: s.extracted,
|
|
420
|
+
});
|
|
421
|
+
if (committed) {
|
|
422
|
+
// Background semantic embeddings (#66): embed the pages this
|
|
423
|
+
// ingest just wrote, off-thread. No-op when unconfigured.
|
|
424
|
+
const pageIds = [
|
|
425
|
+
`sources/${committed.sourceId}`,
|
|
426
|
+
...committed.entitiesCreated.map((e) => `entities/${e}`),
|
|
427
|
+
...committed.entitiesLinked.map((e) => `entities/${e}`),
|
|
428
|
+
...committed.conceptsCreated.map((c) => `concepts/${c}`),
|
|
429
|
+
...committed.conceptsLinked.map((c) => `concepts/${c}`),
|
|
430
|
+
];
|
|
431
|
+
launchEmbedPages(runtime, launchCtx, paths, pageIds, `embed:ingest:${s.id}`);
|
|
432
|
+
}
|
|
433
|
+
const summary = committed
|
|
434
|
+
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
|
|
435
|
+
: `LLM Wiki: ${s.id} produced no synthesis`;
|
|
436
|
+
if (ctx.hasUI) {
|
|
437
|
+
ctx.ui.notify(summary, committed ? "info" : "warning");
|
|
438
|
+
}
|
|
439
|
+
// Persistent, user-visible completion report (issue #77) in
|
|
440
|
+
// addition to the transient toast above. Notices-gated.
|
|
441
|
+
runtime.report(committed ? `✅ ${summary}` : `⚠️ ${summary}`);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
content: [
|
|
446
|
+
{
|
|
447
|
+
type: "text",
|
|
448
|
+
text: [
|
|
449
|
+
`🔄 **Ingesting ${sources.length} source(s) in the background** (${toProcess.length - batch.length} remaining).`,
|
|
450
|
+
"",
|
|
451
|
+
...sources.map((s) => `- **${s.id}**: ${s.manifest.title || s.id}`),
|
|
452
|
+
"",
|
|
453
|
+
"Synthesis runs on the configured task model without blocking. You'll be notified as each source completes — do NOT synthesize these yourself.",
|
|
454
|
+
].join("\n"),
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
details: {
|
|
458
|
+
background: true,
|
|
459
|
+
dispatched: sources.map((s) => s.id),
|
|
460
|
+
remaining: toProcess.length - batch.length,
|
|
461
|
+
} as Record<string, unknown>,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
324
466
|
return {
|
|
325
467
|
content: [
|
|
326
468
|
{
|
|
@@ -359,7 +501,7 @@ export function registerWikiIngest(pi: ExtensionAPI): void {
|
|
|
359
501
|
|
|
360
502
|
// ─── 4. wiki_ensure_page ────────────────────────────────
|
|
361
503
|
|
|
362
|
-
export function registerWikiEnsurePage(pi: ExtensionAPI): void {
|
|
504
|
+
export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
363
505
|
pi.registerTool({
|
|
364
506
|
name: "wiki_ensure_page",
|
|
365
507
|
label: "Wiki Ensure Page",
|
|
@@ -426,6 +568,15 @@ export function registerWikiEnsurePage(pi: ExtensionAPI): void {
|
|
|
426
568
|
path: `${folder}/${slug}`,
|
|
427
569
|
});
|
|
428
570
|
|
|
571
|
+
// Register the new page so retrieval + embeddings can see it. When a
|
|
572
|
+
// background runtime is available, the rebuild + embeddings run off the
|
|
573
|
+
// tool's critical path; otherwise fall back to a synchronous rebuild.
|
|
574
|
+
if (runtime) {
|
|
575
|
+
scheduleReindex(runtime, { hasUI: ctx.hasUI, ui: ctx.ui }, paths);
|
|
576
|
+
} else {
|
|
577
|
+
rebuildMetadataLight(paths);
|
|
578
|
+
}
|
|
579
|
+
|
|
429
580
|
return {
|
|
430
581
|
content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`` }],
|
|
431
582
|
details: { path: pagePath, created: true } as Record<string, unknown>,
|
|
@@ -571,7 +722,7 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
|
|
|
571
722
|
|
|
572
723
|
// ─── 6. wiki_lint ───────────────────────────────────────
|
|
573
724
|
|
|
574
|
-
export function registerWikiLint(pi: ExtensionAPI): void {
|
|
725
|
+
export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
575
726
|
pi.registerTool({
|
|
576
727
|
name: "wiki_lint",
|
|
577
728
|
label: "Wiki Lint",
|
|
@@ -598,138 +749,143 @@ export function registerWikiLint(pi: ExtensionAPI): void {
|
|
|
598
749
|
};
|
|
599
750
|
}
|
|
600
751
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const linkCounts: Record<string, number> = {};
|
|
613
|
-
|
|
614
|
-
for (const page of pages) {
|
|
615
|
-
const links = extractWikilinks(page.content);
|
|
616
|
-
for (const link of links) {
|
|
617
|
-
if (!allPageIds.has(link)) {
|
|
618
|
-
missingPages++;
|
|
619
|
-
findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
|
|
620
|
-
const existing = gaps.find((g) => g.topic === link);
|
|
621
|
-
if (existing) {
|
|
622
|
-
if (!existing.mentionedBy.includes(page.relative))
|
|
623
|
-
existing.mentionedBy.push(page.relative);
|
|
624
|
-
} else {
|
|
625
|
-
gaps.push({ topic: link, mentionedBy: [page.relative] });
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
linkCounts[link] = (linkCounts[link] || 0) + 1;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
752
|
+
// Full-vault scan (+ optional auto-fix writes + reindex) is O(pages):
|
|
753
|
+
// run it in the background and report the health summary (issue #77).
|
|
754
|
+
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
755
|
+
label: `lint:${paths.root}`,
|
|
756
|
+
started:
|
|
757
|
+
"\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted when it completes.",
|
|
758
|
+
work: async () => runWikiLint(paths, params.auto_fix === true),
|
|
759
|
+
});
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
}
|
|
632
763
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
764
|
+
/**
|
|
765
|
+
* Run the wiki health scan (issue #77 extracted it from the tool body so it can
|
|
766
|
+
* run off-thread via `dispatchReported`). Returns the human-readable summary.
|
|
767
|
+
*/
|
|
768
|
+
function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
769
|
+
const pages = findWikiPages(paths.wiki);
|
|
770
|
+
const registry = buildRegistry(paths);
|
|
771
|
+
buildBacklinks(paths, registry); // ensures backlinks.json is current
|
|
772
|
+
|
|
773
|
+
const findings: string[] = [];
|
|
774
|
+
let orphans = 0;
|
|
775
|
+
let missingPages = 0;
|
|
776
|
+
let contradictions = 0;
|
|
777
|
+
const gaps: Array<{ topic: string; mentionedBy: string[] }> = [];
|
|
778
|
+
|
|
779
|
+
const allPageIds = new Set(pages.map((p) => p.relative));
|
|
780
|
+
const linkCounts: Record<string, number> = {};
|
|
781
|
+
|
|
782
|
+
for (const page of pages) {
|
|
783
|
+
const links = extractWikilinks(page.content);
|
|
784
|
+
for (const link of links) {
|
|
785
|
+
if (!allPageIds.has(link)) {
|
|
786
|
+
missingPages++;
|
|
787
|
+
findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
|
|
788
|
+
const existing = gaps.find((g) => g.topic === link);
|
|
789
|
+
if (existing) {
|
|
790
|
+
if (!existing.mentionedBy.includes(page.relative))
|
|
791
|
+
existing.mentionedBy.push(page.relative);
|
|
792
|
+
} else {
|
|
793
|
+
gaps.push({ topic: link, mentionedBy: [page.relative] });
|
|
637
794
|
}
|
|
795
|
+
} else {
|
|
796
|
+
linkCounts[link] = (linkCounts[link] || 0) + 1;
|
|
638
797
|
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
639
800
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
801
|
+
for (const page of pages) {
|
|
802
|
+
if (!linkCounts[page.relative] || linkCounts[page.relative] === 0) {
|
|
803
|
+
orphans++;
|
|
804
|
+
findings.push(`Orphan: [[${page.relative}]] has no inbound links`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
646
807
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
808
|
+
for (const page of pages) {
|
|
809
|
+
if (page.content.includes("⚠️ **Contradiction")) {
|
|
810
|
+
contradictions++;
|
|
811
|
+
findings.push(`Contradiction flagged in [[${page.relative}]]`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let fixesApplied = 0;
|
|
816
|
+
if (autoFix) {
|
|
817
|
+
for (const gap of gaps) {
|
|
818
|
+
if (gap.mentionedBy.length >= 2) {
|
|
819
|
+
const folder = gap.topic.includes("/") ? gap.topic.split("/")[0] : "concepts";
|
|
820
|
+
const name = gap.topic.includes("/") ? gap.topic.split("/").pop()! : gap.topic;
|
|
821
|
+
const pagePath = join(paths.wiki, folder, `${name}.md`);
|
|
822
|
+
mkdirSync(join(paths.wiki, folder), { recursive: true });
|
|
823
|
+
try {
|
|
824
|
+
// Atomic create-if-absent: the `wx` flag fails with EEXIST instead of
|
|
825
|
+
// overwriting, avoiding the existsSync→write TOCTOU race (CodeQL).
|
|
826
|
+
writeFileSync(
|
|
827
|
+
pagePath,
|
|
828
|
+
`---\ntype: concept\ncreated: ${fmtDate()}\nupdated: ${fmtDate()}\nsources: []\nstatus: stub\n---\n\n# ${name.replace(/-/g, " ")}\n\n_Stub auto-created by lint. Expand with content from: ${gap.mentionedBy.map((r) => `[[${r}]]`).join(", ")}_\n`,
|
|
829
|
+
{ encoding: "utf-8", flag: "wx" },
|
|
830
|
+
);
|
|
831
|
+
fixesApplied++;
|
|
832
|
+
} catch (err) {
|
|
833
|
+
// Page already exists — nothing to fix. Re-throw anything else.
|
|
834
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
664
835
|
}
|
|
665
836
|
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
666
839
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const reportLines = [
|
|
673
|
-
"# Wiki Lint Report",
|
|
674
|
-
`Generated: ${fmtDate()}`,
|
|
675
|
-
"",
|
|
676
|
-
"## Summary",
|
|
677
|
-
`- Total pages: ${pages.length}`,
|
|
678
|
-
`- Orphans: ${orphans}`,
|
|
679
|
-
`- Missing pages: ${missingPages}`,
|
|
680
|
-
`- Contradictions: ${contradictions}`,
|
|
681
|
-
params.auto_fix ? `- Fixes applied: ${fixesApplied}` : "",
|
|
682
|
-
"",
|
|
683
|
-
"## Findings",
|
|
684
|
-
findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
|
|
685
|
-
"",
|
|
686
|
-
].filter(Boolean);
|
|
687
|
-
|
|
688
|
-
const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
|
|
689
|
-
mkdirSync(paths.outputs, { recursive: true });
|
|
690
|
-
writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
|
|
691
|
-
|
|
692
|
-
appendEvent(paths, {
|
|
693
|
-
kind: "lint",
|
|
694
|
-
orphans,
|
|
695
|
-
missing_pages: missingPages,
|
|
696
|
-
contradictions,
|
|
697
|
-
auto_fix: params.auto_fix ?? false,
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
rebuildMetadataLight(paths);
|
|
840
|
+
writeJson(join(paths.discoveries, "gaps.json"), {
|
|
841
|
+
gaps,
|
|
842
|
+
generated: new Date().toISOString(),
|
|
843
|
+
});
|
|
701
844
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
gaps: gaps.length,
|
|
729
|
-
} as Record<string, unknown>,
|
|
730
|
-
};
|
|
731
|
-
},
|
|
845
|
+
const reportLines = [
|
|
846
|
+
"# Wiki Lint Report",
|
|
847
|
+
`Generated: ${fmtDate()}`,
|
|
848
|
+
"",
|
|
849
|
+
"## Summary",
|
|
850
|
+
`- Total pages: ${pages.length}`,
|
|
851
|
+
`- Orphans: ${orphans}`,
|
|
852
|
+
`- Missing pages: ${missingPages}`,
|
|
853
|
+
`- Contradictions: ${contradictions}`,
|
|
854
|
+
autoFix ? `- Fixes applied: ${fixesApplied}` : "",
|
|
855
|
+
"",
|
|
856
|
+
"## Findings",
|
|
857
|
+
findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
|
|
858
|
+
"",
|
|
859
|
+
].filter(Boolean);
|
|
860
|
+
|
|
861
|
+
const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
|
|
862
|
+
mkdirSync(paths.outputs, { recursive: true });
|
|
863
|
+
writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
|
|
864
|
+
|
|
865
|
+
appendEvent(paths, {
|
|
866
|
+
kind: "lint",
|
|
867
|
+
orphans,
|
|
868
|
+
missing_pages: missingPages,
|
|
869
|
+
contradictions,
|
|
870
|
+
auto_fix: autoFix,
|
|
732
871
|
});
|
|
872
|
+
|
|
873
|
+
rebuildMetadataLight(paths);
|
|
874
|
+
|
|
875
|
+
return [
|
|
876
|
+
"🧹 **LLM Wiki lint complete**",
|
|
877
|
+
"",
|
|
878
|
+
`- Pages: ${pages.length}`,
|
|
879
|
+
`- Orphans: ${orphans}`,
|
|
880
|
+
`- Missing: ${missingPages}`,
|
|
881
|
+
`- Contradictions: ${contradictions}`,
|
|
882
|
+
autoFix ? `- Auto-fixes: ${fixesApplied}` : "",
|
|
883
|
+
"",
|
|
884
|
+
`📄 Report: \`${reportPath}\``,
|
|
885
|
+
gaps.length > 0 ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
|
|
886
|
+
]
|
|
887
|
+
.filter(Boolean)
|
|
888
|
+
.join("\n");
|
|
733
889
|
}
|
|
734
890
|
|
|
735
891
|
// ─── 7. wiki_status ─────────────────────────────────────
|
|
@@ -811,7 +967,7 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
|
|
|
811
967
|
|
|
812
968
|
// ─── 8. wiki_rebuild_meta ───────────────────────────────
|
|
813
969
|
|
|
814
|
-
export function registerWikiRebuildMeta(pi: ExtensionAPI): void {
|
|
970
|
+
export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
815
971
|
pi.registerTool({
|
|
816
972
|
name: "wiki_rebuild_meta",
|
|
817
973
|
label: "Wiki Rebuild Meta",
|
|
@@ -830,30 +986,94 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI): void {
|
|
|
830
986
|
};
|
|
831
987
|
}
|
|
832
988
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
989
|
+
// Heavy O(pages) rebuild — dispatch off the agent's critical path and
|
|
990
|
+
// report on completion (issue #77).
|
|
991
|
+
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
992
|
+
label: `rebuild_meta:${paths.root}`,
|
|
993
|
+
started:
|
|
994
|
+
"\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
|
|
995
|
+
work: async () => {
|
|
996
|
+
rebuildMetadata(paths);
|
|
997
|
+
appendEvent(paths, { kind: "rebuild_meta" });
|
|
998
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
999
|
+
version: "1.0",
|
|
1000
|
+
last_updated: "",
|
|
1001
|
+
pages: {},
|
|
1002
|
+
});
|
|
1003
|
+
return `✅ LLM Wiki: metadata rebuilt — ${Object.keys(registry.pages).length} pages indexed.`;
|
|
1004
|
+
},
|
|
840
1005
|
});
|
|
841
|
-
|
|
842
|
-
return {
|
|
843
|
-
content: [
|
|
844
|
-
{
|
|
845
|
-
type: "text",
|
|
846
|
-
text: `✅ Metadata rebuilt. ${Object.keys(registry.pages).length} pages indexed.`,
|
|
847
|
-
},
|
|
848
|
-
],
|
|
849
|
-
details: { pageCount: Object.keys(registry.pages).length } as Record<string, unknown>,
|
|
850
|
-
};
|
|
851
1006
|
},
|
|
852
1007
|
});
|
|
853
1008
|
}
|
|
854
1009
|
|
|
855
1010
|
// ─── 9. wiki_log_event ──────────────────────────────────
|
|
856
1011
|
|
|
1012
|
+
export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
1013
|
+
pi.registerTool({
|
|
1014
|
+
name: "wiki_reindex_embeddings",
|
|
1015
|
+
label: "Wiki Reindex Embeddings",
|
|
1016
|
+
description:
|
|
1017
|
+
"Backfill / refresh semantic embeddings for the vault. Embeds pages that " +
|
|
1018
|
+
"are new or stale (content changed); pass force to re-embed everything. " +
|
|
1019
|
+
"No-op when no embedding provider is configured.",
|
|
1020
|
+
promptSnippet: "Backfill semantic embeddings for the wiki",
|
|
1021
|
+
promptGuidelines: [
|
|
1022
|
+
"Use wiki_reindex_embeddings to embed an existing vault or refresh stale embeddings.",
|
|
1023
|
+
"Embeddings are optional: this no-ops cleanly when no embedding provider is configured.",
|
|
1024
|
+
],
|
|
1025
|
+
parameters: Type.Object({
|
|
1026
|
+
force: Type.Optional(
|
|
1027
|
+
Type.Boolean({ description: "Re-embed every page, ignoring staleness (default: false)" }),
|
|
1028
|
+
),
|
|
1029
|
+
}),
|
|
1030
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1031
|
+
const paths = getPaths(ctx.cwd);
|
|
1032
|
+
const vaultCheck = requireVault(paths);
|
|
1033
|
+
if (!vaultCheck.ok) {
|
|
1034
|
+
return {
|
|
1035
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
1036
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
1037
|
+
isError: true,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (runtime) runtime.ensureConfig(ctx.cwd ?? paths.root);
|
|
1042
|
+
const embedder = runtime ? resolveEmbedder(runtime.config) : undefined;
|
|
1043
|
+
if (!embedder) {
|
|
1044
|
+
return {
|
|
1045
|
+
content: [
|
|
1046
|
+
{
|
|
1047
|
+
type: "text",
|
|
1048
|
+
text: 'ℹ️ No embedding provider configured — semantic embeddings are disabled. Set `llm-wiki.embeddingProvider` (e.g. "openai") in settings to enable.',
|
|
1049
|
+
},
|
|
1050
|
+
],
|
|
1051
|
+
details: { enabled: false } as Record<string, unknown>,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Embedding is network-bound and O(pages) — run it in the background and
|
|
1056
|
+
// report the stats on completion (issue #77).
|
|
1057
|
+
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
1058
|
+
label: `reindex_embeddings:${paths.root}`,
|
|
1059
|
+
started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported when it completes.`,
|
|
1060
|
+
details: { enabled: true, model: embedder.model },
|
|
1061
|
+
work: async () => {
|
|
1062
|
+
const stats = await reindexEmbeddings(paths, embedder, { force: params.force === true });
|
|
1063
|
+
appendEvent(paths, {
|
|
1064
|
+
kind: "reindex_embeddings",
|
|
1065
|
+
embedded: stats.embedded,
|
|
1066
|
+
skipped: stats.skipped,
|
|
1067
|
+
pruned: stats.pruned,
|
|
1068
|
+
model: embedder.model,
|
|
1069
|
+
});
|
|
1070
|
+
return `✅ LLM Wiki: embeddings reindexed (${embedder.model}) — ${stats.embedded} embedded, ${stats.skipped} fresh, ${stats.pruned} pruned.`;
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
857
1077
|
export function registerWikiLogEvent(pi: ExtensionAPI): void {
|
|
858
1078
|
pi.registerTool({
|
|
859
1079
|
name: "wiki_log_event",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
package/prompts/wiki-ingest.md
CHANGED
|
@@ -15,17 +15,20 @@ $ARGUMENTS
|
|
|
15
15
|
|
|
16
16
|
## Steps
|
|
17
17
|
|
|
18
|
-
1. Call `wiki_ingest(source_id=<id if provided>, batch_size=3)
|
|
18
|
+
1. Call `wiki_ingest(source_id=<id if provided>, batch_size=3)`.
|
|
19
19
|
2. If the tool reports "All sources ingested", inform the user and stop.
|
|
20
|
-
3.
|
|
20
|
+
3. **If the tool reports it is ingesting in the background**, the synthesis sub-agent is handling those sources on the configured task model. Do NOT synthesize them yourself — just report which sources were dispatched and stop. (You'll be notified as each completes.)
|
|
21
|
+
4. **Otherwise** (the tool returned extracted content — background unavailable or `background=false`), for each source in the returned batch:
|
|
21
22
|
a. Read the extracted text from `raw/sources/<SOURCE_ID>/extracted.md`
|
|
22
23
|
b. Update the skeleton source page in `wiki/sources/` with a proper summary, key entities, and concepts
|
|
23
24
|
c. Use `wiki_ensure_page(type=entity, title=<name>)` for each new entity (people, orgs, tools, products)
|
|
24
25
|
d. Use `wiki_ensure_page(type=concept, title=<name>)` for each new concept (ideas, patterns, frameworks)
|
|
25
26
|
e. Add `[[wikilinks]]` cross-references between related pages
|
|
26
27
|
f. Flag any contradictions with existing wiki content using `⚠️ **Contradiction**` markers
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
5. After processing a synchronous batch, call `wiki_rebuild_meta` to update metadata.
|
|
29
|
+
6. Report: "Ingested [N] sources → [M] pages created/updated. [X] contradictions flagged."
|
|
30
|
+
|
|
31
|
+
> **Background vs synchronous:** ingestion runs in the background by default (non-blocking) when a task model is available, so the main agent is never stalled. It falls back to the synchronous main-agent flow above when no model/API key is configured, or when called with `background=false`.
|
|
29
32
|
|
|
30
33
|
**Rules:**
|
|
31
34
|
- Never modify files in `raw/` — source packets are immutable after capture.
|