@zosmaai/pi-llm-wiki 0.10.9 → 0.11.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.de.md +35 -4
  3. package/README.es.md +260 -170
  4. package/README.fr.md +35 -4
  5. package/README.hi.md +35 -4
  6. package/README.ja.md +35 -4
  7. package/README.ko.md +35 -4
  8. package/README.md +41 -12
  9. package/README.pt.md +35 -4
  10. package/README.ru.md +35 -4
  11. package/README.zh.md +260 -170
  12. package/assets/demo.gif +0 -0
  13. package/dist/extensions/llm-wiki/lib/bootstrap.js +74 -0
  14. package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
  15. package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
  16. package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
  17. package/dist/extensions/llm-wiki/lib/ingest-worker.js +410 -0
  18. package/dist/extensions/llm-wiki/lib/inject.js +65 -0
  19. package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
  20. package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
  21. package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
  22. package/dist/extensions/llm-wiki/lib/metadata.js +505 -0
  23. package/dist/extensions/llm-wiki/lib/model-command.js +85 -0
  24. package/dist/extensions/llm-wiki/lib/observation.js +283 -0
  25. package/dist/extensions/llm-wiki/lib/recall.js +875 -0
  26. package/dist/extensions/llm-wiki/lib/retro.js +158 -0
  27. package/dist/extensions/llm-wiki/lib/runtime.js +187 -0
  28. package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
  29. package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
  30. package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
  31. package/dist/extensions/llm-wiki/lib/task-config.js +201 -0
  32. package/dist/extensions/llm-wiki/lib/tools.js +1199 -0
  33. package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
  34. package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
  35. package/dist/extensions/llm-wiki/lib/utils.js +353 -0
  36. package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
  37. package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
  38. package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
  39. package/dist/mcp/exec.js +121 -0
  40. package/dist/mcp/index.js +229 -0
  41. package/dist/mcp/operations.js +130 -0
  42. package/dist/package.json +1 -0
  43. package/docs/api.md +5 -2
  44. package/docs/architecture.md +5 -2
  45. package/docs/configuration.md +25 -0
  46. package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
  47. package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
  48. package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
  49. package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
  50. package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
  51. package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +593 -0
  52. package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +542 -0
  53. package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
  54. package/extensions/llm-wiki/index.ts +22 -36
  55. package/extensions/llm-wiki/lib/bootstrap.ts +87 -0
  56. package/extensions/llm-wiki/lib/embeddings.ts +9 -3
  57. package/extensions/llm-wiki/lib/guardrails.ts +26 -18
  58. package/extensions/llm-wiki/lib/indexing.ts +2 -1
  59. package/extensions/llm-wiki/lib/ingest-worker.ts +304 -28
  60. package/extensions/llm-wiki/lib/knowledge-document.ts +663 -0
  61. package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
  62. package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
  63. package/extensions/llm-wiki/lib/metadata.ts +550 -128
  64. package/extensions/llm-wiki/lib/model-command.ts +0 -1
  65. package/extensions/llm-wiki/lib/observation.ts +37 -43
  66. package/extensions/llm-wiki/lib/recall.ts +61 -33
  67. package/extensions/llm-wiki/lib/retro.ts +65 -41
  68. package/extensions/llm-wiki/lib/runtime.ts +0 -3
  69. package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
  70. package/extensions/llm-wiki/lib/source-packet.ts +45 -32
  71. package/extensions/llm-wiki/lib/task-config.ts +36 -0
  72. package/extensions/llm-wiki/lib/tools.ts +413 -342
  73. package/extensions/llm-wiki/lib/trajectory.ts +15 -1
  74. package/extensions/llm-wiki/lib/utils.ts +127 -131
  75. package/extensions/llm-wiki/lib/vault-format.ts +363 -0
  76. package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
  77. package/mcp/exec.ts +122 -0
  78. package/mcp/index.ts +60 -250
  79. package/mcp/operations.ts +176 -0
  80. package/package.json +8 -2
  81. package/scripts/migrate-llm-wiki.js +801 -0
  82. package/skills/llm-wiki/SKILL.md +12 -8
@@ -0,0 +1,232 @@
1
+ import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
2
+ import { scheduleReindex } from "./indexing.js";
3
+ import { rebuildMetadataLight } from "./metadata.js";
4
+ import { isPathWithin, isProtectedPath, resolveVaultPaths } from "./utils.js";
5
+ import { inspectVaultFormat, isGeneratedOkfPath } from "./vault-format.js";
6
+ /**
7
+ * Guardrails and auto-rebuild hooks for the LLM Wiki extension.
8
+ */
9
+ let pendingRebuild = false;
10
+ const APPLY_PATCH_PATH_NOISE = /^\*{0,3}\s*(?:(?:update|add|delete|move)[^A-Za-z0-9]*(?:file|to)?[^A-Za-z0-9]*:)?\s*\*{0,3}\s*/i;
11
+ const PATCH_INPUT_KEYS = { input: true, _input: true, patch: true };
12
+ const DESTINATION_KEYS = {
13
+ rename: true,
14
+ move: true,
15
+ dest: true,
16
+ destination: true,
17
+ newPath: true,
18
+ };
19
+ function normalizeMutationPath(target) {
20
+ const trimmed = target.trim();
21
+ if (!trimmed)
22
+ return undefined;
23
+ const first = trimmed[0];
24
+ const last = trimmed[trimmed.length - 1];
25
+ const quoted = first === '"' || first === "'";
26
+ if (quoted !== (last === '"' || last === "'") || (quoted && first !== last)) {
27
+ return undefined;
28
+ }
29
+ const unquoted = quoted ? trimmed.slice(1, -1) : trimmed;
30
+ return unquoted.replace(APPLY_PATCH_PATH_NOISE, "") || undefined;
31
+ }
32
+ function parsePatchHeader(line) {
33
+ const trimmed = line.replace(/\r$/, "").trimEnd();
34
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
35
+ return undefined;
36
+ const body = trimmed.slice(1, -1).trim();
37
+ const tag = /#[0-9A-Fa-f]{4}\s*$/.exec(body);
38
+ const rawTarget = tag ? body.slice(0, tag.index) : body.replace(/\s+$/, "");
39
+ if (!rawTarget || rawTarget.includes("#"))
40
+ return undefined;
41
+ return normalizeMutationPath(rawTarget);
42
+ }
43
+ function parseMoveDestination(line) {
44
+ const rawDestination = line.trim().slice(2).trim();
45
+ if (!rawDestination)
46
+ return undefined;
47
+ const quote = rawDestination[0];
48
+ if (quote !== '"' && quote !== "'")
49
+ return normalizeMutationPath(rawDestination);
50
+ let cursor = 1;
51
+ while (cursor < rawDestination.length) {
52
+ if (rawDestination[cursor] === "\\" && cursor + 1 < rawDestination.length) {
53
+ cursor += 2;
54
+ continue;
55
+ }
56
+ if (rawDestination[cursor] === quote) {
57
+ return cursor === rawDestination.length - 1
58
+ ? normalizeMutationPath(rawDestination)
59
+ : undefined;
60
+ }
61
+ cursor++;
62
+ }
63
+ return undefined;
64
+ }
65
+ function scanPatchString(input) {
66
+ const paths = [];
67
+ let sawHeader = false;
68
+ let sectionHasMove = false;
69
+ let complete = true;
70
+ const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
71
+ for (const line of stripped.split("\n")) {
72
+ const trimmed = line.replace(/\r$/, "").trim();
73
+ if (trimmed.startsWith("[")) {
74
+ sawHeader = true;
75
+ sectionHasMove = false;
76
+ const path = parsePatchHeader(line);
77
+ if (path)
78
+ paths.push(path);
79
+ else
80
+ complete = false;
81
+ continue;
82
+ }
83
+ if (!/^MV(?:\s|$)/.test(trimmed))
84
+ continue;
85
+ const destination = parseMoveDestination(trimmed);
86
+ if (!sawHeader || sectionHasMove || !destination)
87
+ complete = false;
88
+ else {
89
+ paths.push(destination);
90
+ sectionHasMove = true;
91
+ }
92
+ }
93
+ return { paths, complete: sawHeader && complete };
94
+ }
95
+ function mergeMutationScans(target, source) {
96
+ target.paths.push(...source.paths);
97
+ target.complete &&= source.complete;
98
+ }
99
+ function addMutationPath(scan, target) {
100
+ const path = normalizeMutationPath(target);
101
+ if (path)
102
+ scan.paths.push(path);
103
+ else
104
+ scan.complete = false;
105
+ }
106
+ function collectMutationPaths(input, seen, stringsArePatches = false) {
107
+ if (typeof input === "string") {
108
+ return stringsArePatches ? scanPatchString(input) : { paths: [], complete: true };
109
+ }
110
+ if (!input || typeof input !== "object" || seen.has(input)) {
111
+ return { paths: [], complete: true };
112
+ }
113
+ seen.add(input);
114
+ const scan = { paths: [], complete: true };
115
+ if (Array.isArray(input)) {
116
+ for (const value of input) {
117
+ mergeMutationScans(scan, collectMutationPaths(value, seen, stringsArePatches));
118
+ }
119
+ return scan;
120
+ }
121
+ const record = input;
122
+ if (typeof record.path === "string" && record.path.length > 0) {
123
+ addMutationPath(scan, record.path);
124
+ }
125
+ const eventPaths = Array.isArray(record.paths) ? record.paths : [record.paths];
126
+ for (const path of eventPaths) {
127
+ if (typeof path === "string" && path.length > 0)
128
+ addMutationPath(scan, path);
129
+ }
130
+ for (const [key, value] of Object.entries(record)) {
131
+ if (key === "path" || key === "paths")
132
+ continue;
133
+ if (DESTINATION_KEYS[key] === true) {
134
+ if (typeof value === "string" && value.length > 0)
135
+ addMutationPath(scan, value);
136
+ else
137
+ scan.complete = false;
138
+ continue;
139
+ }
140
+ const childStringsArePatches = stringsArePatches || PATCH_INPUT_KEYS[key] === true;
141
+ if (typeof value !== "string" && (!value || typeof value !== "object"))
142
+ continue;
143
+ mergeMutationScans(scan, collectMutationPaths(value, seen, childStringsArePatches));
144
+ }
145
+ return scan;
146
+ }
147
+ function inspectMutationPaths(input) {
148
+ const stringsArePatches = typeof input === "string" || Array.isArray(input);
149
+ const scan = collectMutationPaths(input, new WeakSet(), stringsArePatches);
150
+ return { paths: [...new Set(scan.paths)], complete: scan.complete };
151
+ }
152
+ /** Return every file path targeted by a write or patch-shaped edit input. */
153
+ export function extractMutationPaths(input) {
154
+ return inspectMutationPaths(input).paths;
155
+ }
156
+ /** True when a write or patch-shaped edit targets a page in the wiki directory. */
157
+ export function hasWikiMutation(input, wikiPath) {
158
+ return extractMutationPaths(input).some((path) => isPathWithin(wikiPath, path));
159
+ }
160
+ export function mutationBlockReason(path, paths) {
161
+ const protectedPath = isProtectedPath(path, paths);
162
+ if (protectedPath.protected)
163
+ return protectedPath.reason;
164
+ if (isPathWithin(paths.dotWiki, path)) {
165
+ const state = inspectVaultFormat(paths);
166
+ if (state.blocking) {
167
+ return `Wiki vault configuration is invalid: ${state.diagnostics[0].message}`;
168
+ }
169
+ }
170
+ if (isGeneratedOkfPath(path, paths)) {
171
+ return "Generated OKF indexes and log are read-only. Use wiki_rebuild_meta or the page-producing tool that owns the source mutation.";
172
+ }
173
+ return undefined;
174
+ }
175
+ /** Install guardrails on the extension API. */
176
+ export function installGuardrails(pi, runtime) {
177
+ // Block direct edits to raw/ and meta/, plus OKF generated projections
178
+ pi.on("tool_call", async (event) => {
179
+ if (isToolCallEventType("write", event)) {
180
+ const path = event.input.path;
181
+ const paths = resolveVaultPaths(process.cwd());
182
+ const reason = mutationBlockReason(path, paths);
183
+ if (reason)
184
+ return { block: true, reason };
185
+ }
186
+ if (isToolCallEventType("edit", event)) {
187
+ const mutation = inspectMutationPaths(event.input);
188
+ const targetPaths = mutation.paths;
189
+ if (!mutation.complete || targetPaths.length === 0) {
190
+ return { block: true, reason: "Cannot determine the files targeted by this edit." };
191
+ }
192
+ const paths = resolveVaultPaths(process.cwd());
193
+ for (const path of targetPaths) {
194
+ const reason = mutationBlockReason(path, paths);
195
+ if (reason)
196
+ return { block: true, reason };
197
+ }
198
+ }
199
+ });
200
+ // Track wiki edits for auto-rebuild
201
+ pi.on("tool_result", async (event) => {
202
+ if (event.toolName === "write" || event.toolName === "edit") {
203
+ const paths = resolveVaultPaths(process.cwd());
204
+ if (hasWikiMutation(event.input, paths.wiki)) {
205
+ pendingRebuild = true;
206
+ }
207
+ }
208
+ });
209
+ // Rebuild metadata at end of turn if wiki was modified, then refresh
210
+ // semantic embeddings in the background (#66) so manual page edits get
211
+ // re-embedded. Both are best-effort no-ops when nothing is configured.
212
+ pi.on("turn_end", async (_event, ctx) => {
213
+ if (pendingRebuild) {
214
+ pendingRebuild = false;
215
+ try {
216
+ const paths = resolveVaultPaths(process.cwd());
217
+ // Manual page edits also rebuild off the critical path. Without a
218
+ // runtime (shouldn't happen in normal wiring) fall back to inline.
219
+ if (runtime) {
220
+ const launchCtx = ctx ? { hasUI: ctx.hasUI, ui: ctx.ui } : { hasUI: false };
221
+ scheduleReindex(runtime, launchCtx, paths);
222
+ }
223
+ else {
224
+ rebuildMetadataLight(paths);
225
+ }
226
+ }
227
+ catch {
228
+ // Silently fail — metadata rebuild is best-effort
229
+ }
230
+ }
231
+ });
232
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Non-blocking vault (re)indexing.
3
+ *
4
+ * Writing a wiki page (observe / retro / capture / ensure_page) and editing one
5
+ * by hand both require the derived metadata — `meta/registry.json`,
6
+ * `backlinks.json`, `index.md`, `log.md` — to be rebuilt, plus (optionally) the
7
+ * semantic embedding store to be refreshed. That rebuild is O(pages): it
8
+ * rescans every page in the vault. Doing it inline on the tool's / turn's
9
+ * critical path makes every write get slower as the vault grows.
10
+ *
11
+ * `scheduleReindex` moves that work off the caller's stack onto the shared
12
+ * background Runtime (#64) and coalesces a burst of writes into a single pass:
13
+ *
14
+ * - A leading micro-yield guarantees the caller (a tool's `execute`, or the
15
+ * turn_end handler) returns BEFORE the heavy rebuild runs.
16
+ * - A per-vault `dirty` flag + drain loop means writes that land while a pass
17
+ * is in flight are folded into a trailing rebuild instead of being lost —
18
+ * this also covers the async window of the embeddings refresh.
19
+ * - A per-vault `inflight` guard collapses concurrent schedule calls onto the
20
+ * same promise (single-flight), so N writes in a turn cost one rebuild.
21
+ *
22
+ * Errors are isolated by `Runtime.launchTask`; the promise never rejects. The
23
+ * embeddings step is a no-op unless an embedder is configured (#66/#67).
24
+ */
25
+ import { reindexEmbeddings, resolveEmbedder } from "./embeddings.js";
26
+ import { rebuildMetadataLight } from "./metadata.js";
27
+ /** Promise of the current background pass, keyed by vault root. */
28
+ const inflight = new Map();
29
+ /** Vault roots with writes awaiting a (re)build. */
30
+ const dirty = new Set();
31
+ /** Stable single-flight label for a vault's background index pass. */
32
+ export function indexLabel(root) {
33
+ return `index:${root}`;
34
+ }
35
+ /**
36
+ * Schedule a non-blocking metadata rebuild (+ embeddings refresh) for a vault.
37
+ * Returns the promise of the in-flight pass so callers/tests can await drainage
38
+ * (the agent loop itself never awaits it). Safe to call on every write.
39
+ */
40
+ export function scheduleReindex(runtime, ctx, paths) {
41
+ const root = paths.root;
42
+ dirty.add(root);
43
+ const active = inflight.get(root);
44
+ if (active)
45
+ return active;
46
+ const pass = runtime.launchTask(ctx, indexLabel(root), async () => {
47
+ // Yield once so the caller returns before the O(pages) rebuild runs. This
48
+ // is what makes the surrounding write non-blocking.
49
+ await Promise.resolve();
50
+ try {
51
+ // Drain: keep rebuilding until no new write arrived during the previous
52
+ // pass. The loop re-checks AFTER the awaited embeddings step, so writes
53
+ // that land during embedding are not lost.
54
+ while (dirty.has(root)) {
55
+ dirty.delete(root);
56
+ const projection = rebuildMetadataLight(paths);
57
+ if (!projection.ok)
58
+ continue;
59
+ // Refresh embeddings only after metadata is consistent. Stale-aware and
60
+ // a no-op unless an embedder is configured.
61
+ runtime.ensureConfig(root);
62
+ const embedder = resolveEmbedder(runtime.config);
63
+ if (embedder)
64
+ await reindexEmbeddings(paths, embedder);
65
+ }
66
+ }
67
+ finally {
68
+ inflight.delete(root);
69
+ }
70
+ });
71
+ inflight.set(root, pass);
72
+ return pass;
73
+ }
74
+ /** Test-only: clear coalescing state between cases. */
75
+ export function __resetIndexingState() {
76
+ inflight.clear();
77
+ dirty.clear();
78
+ }