@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,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readJson } from "./utils.js";
4
+ import { compareCodePoint, discoverKnowledgeDocuments, inspectVaultFormat, } from "./vault-format.js";
5
+ /**
6
+ * Search the registry for matching concepts.
7
+ *
8
+ * Matches ID, semantic title, type, category, domain, tags, aliases, and recall triggers.
9
+ * Preserves unknown types as strings.
10
+ */
11
+ export function searchRegistry(paths, query, typeFilter) {
12
+ const diagnostics = [];
13
+ const vaultState = inspectVaultFormat(paths);
14
+ diagnostics.push(...vaultState.diagnostics);
15
+ const registryPath = join(paths.meta, "registry.json");
16
+ if (!existsSync(registryPath)) {
17
+ return { matches: [], diagnostics };
18
+ }
19
+ const registry = readJson(registryPath, {
20
+ version: "1.0",
21
+ last_updated: "",
22
+ pages: {},
23
+ });
24
+ const normalizedQuery = query.toLowerCase();
25
+ const matches = [];
26
+ for (const [id, entry] of Object.entries(registry.pages)) {
27
+ // Apply type filter
28
+ if (typeFilter && String(entry.type).toLowerCase() !== typeFilter.toLowerCase()) {
29
+ continue;
30
+ }
31
+ // Check if query matches any searchable field
32
+ if (matchesField(id, entry, normalizedQuery)) {
33
+ matches.push({
34
+ id,
35
+ title: String(entry.title || id),
36
+ type: String(entry.type || "unknown"),
37
+ });
38
+ }
39
+ }
40
+ // Sort by code point order for determinism
41
+ matches.sort((a, b) => compareCodePoint(a.id, b.id));
42
+ return { matches, diagnostics };
43
+ }
44
+ function matchesField(id, entry, query) {
45
+ // Match ID
46
+ if (id.toLowerCase().includes(query))
47
+ return true;
48
+ // Match title
49
+ if (String(entry.title || "")
50
+ .toLowerCase()
51
+ .includes(query))
52
+ return true;
53
+ // Match type
54
+ if (String(entry.type || "")
55
+ .toLowerCase()
56
+ .includes(query))
57
+ return true;
58
+ // Match category/domain
59
+ if (String(entry.category || "")
60
+ .toLowerCase()
61
+ .includes(query))
62
+ return true;
63
+ if (String(entry.domain || "")
64
+ .toLowerCase()
65
+ .includes(query))
66
+ return true;
67
+ // Match tags (array or string)
68
+ const tags = entry.tags;
69
+ if (Array.isArray(tags)) {
70
+ if (tags.some((t) => String(t).toLowerCase().includes(query)))
71
+ return true;
72
+ }
73
+ else if (typeof tags === "string" && tags.toLowerCase().includes(query)) {
74
+ return true;
75
+ }
76
+ // Match aliases (array or string)
77
+ const aliases = entry.aliases;
78
+ if (Array.isArray(aliases)) {
79
+ if (aliases.some((a) => String(a).toLowerCase().includes(query)))
80
+ return true;
81
+ }
82
+ else if (typeof aliases === "string" && aliases.toLowerCase().includes(query)) {
83
+ return true;
84
+ }
85
+ // Match recall_triggers (array or string)
86
+ const triggers = entry.recall_triggers;
87
+ if (Array.isArray(triggers)) {
88
+ if (triggers.some((t) => String(t).toLowerCase().includes(query)))
89
+ return true;
90
+ }
91
+ else if (typeof triggers === "string" && triggers.toLowerCase().includes(query)) {
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+ /**
97
+ * Get a status snapshot of the wiki.
98
+ *
99
+ * Reports resolved knowledge_format, page counts, and blocking diagnostics.
100
+ */
101
+ export function getWikiStatus(paths) {
102
+ const vaultState = inspectVaultFormat(paths);
103
+ const diagnostics = [...vaultState.diagnostics];
104
+ // Also check discovery for current concept health
105
+ const discovery = discoverKnowledgeDocuments(paths);
106
+ diagnostics.push(...discovery.diagnostics);
107
+ const registryPath = join(paths.meta, "registry.json");
108
+ let registry;
109
+ if (existsSync(registryPath)) {
110
+ registry = readJson(registryPath, { version: "1.0", last_updated: "", pages: {} });
111
+ }
112
+ const byType = {};
113
+ let totalPages = 0;
114
+ if (registry) {
115
+ for (const entry of Object.values(registry.pages)) {
116
+ totalPages++;
117
+ const type = String(entry.type || "unknown");
118
+ byType[type] = (byType[type] || 0) + 1;
119
+ }
120
+ }
121
+ return {
122
+ knowledgeFormat: vaultState.knowledgeFormat,
123
+ totalPages,
124
+ byType,
125
+ blockingDiagnostics: diagnostics.filter((d) => d.severity === "error"),
126
+ lastUpdated: registry?.last_updated || "",
127
+ };
128
+ }
@@ -0,0 +1,121 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
3
+ export function createExecApi() {
4
+ return {
5
+ exec(command, args, options = {}) {
6
+ return new Promise((resolve) => {
7
+ let killed = false;
8
+ let settled = false;
9
+ let stdoutLimited = false;
10
+ let stderrLimited = false;
11
+ let forceTimer;
12
+ let stdout = "";
13
+ let stderr = "";
14
+ const child = spawn(command, args, {
15
+ cwd: options.cwd,
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ detached: process.platform !== "win32",
18
+ });
19
+ const sendSignal = (signal) => {
20
+ if (process.platform === "win32" && child.pid) {
21
+ execFile("taskkill", ["/pid", String(child.pid), "/T", "/F"], () => { });
22
+ return;
23
+ }
24
+ if (child.pid) {
25
+ try {
26
+ process.kill(-child.pid, signal);
27
+ return;
28
+ }
29
+ catch {
30
+ // Fall back to the direct child when process-group signalling fails.
31
+ }
32
+ }
33
+ child.kill(signal);
34
+ };
35
+ const forceStop = () => {
36
+ if (forceTimer)
37
+ clearTimeout(forceTimer);
38
+ forceTimer = undefined;
39
+ sendSignal("SIGKILL");
40
+ };
41
+ const stop = () => {
42
+ if (killed)
43
+ return;
44
+ killed = true;
45
+ sendSignal("SIGTERM");
46
+ if (process.platform !== "win32")
47
+ forceTimer = setTimeout(forceStop, 100);
48
+ };
49
+ const appendOutput = (current, chunk, limited) => {
50
+ if (limited)
51
+ return [current, true];
52
+ const remaining = MAX_OUTPUT_BYTES - Buffer.byteLength(current);
53
+ if (remaining <= 0) {
54
+ stop();
55
+ return [current, true];
56
+ }
57
+ const bytes = Buffer.from(chunk);
58
+ if (bytes.byteLength <= remaining)
59
+ return [current + chunk, false];
60
+ stop();
61
+ let prefix = "";
62
+ let prefixBytes = 0;
63
+ for (const char of chunk) {
64
+ const charBytes = Buffer.byteLength(char);
65
+ if (prefixBytes + charBytes > remaining)
66
+ break;
67
+ prefix += char;
68
+ prefixBytes += charBytes;
69
+ }
70
+ return [current + prefix, true];
71
+ };
72
+ child.stdout?.setEncoding("utf8");
73
+ child.stderr?.setEncoding("utf8");
74
+ child.stdout?.on("data", (chunk) => {
75
+ [stdout, stdoutLimited] = appendOutput(stdout, chunk, stdoutLimited);
76
+ });
77
+ child.stderr?.on("data", (chunk) => {
78
+ [stderr, stderrLimited] = appendOutput(stderr, chunk, stderrLimited);
79
+ });
80
+ const timer = options.timeout ? setTimeout(stop, options.timeout) : undefined;
81
+ const abort = () => stop();
82
+ options.signal?.addEventListener("abort", abort, { once: true });
83
+ const finish = (code) => {
84
+ if (settled)
85
+ return;
86
+ if (forceTimer) {
87
+ clearTimeout(forceTimer);
88
+ forceTimer = undefined;
89
+ if (child.pid) {
90
+ try {
91
+ process.kill(-child.pid, "SIGKILL");
92
+ }
93
+ catch {
94
+ // The process group already exited; do not signal the closed child's stale PID.
95
+ }
96
+ }
97
+ }
98
+ cleanup();
99
+ resolve({
100
+ stdout,
101
+ stderr,
102
+ code: stdoutLimited || stderrLimited ? 1 : killed && code === 0 ? 1 : code,
103
+ killed,
104
+ });
105
+ };
106
+ child.once("error", () => finish(1));
107
+ child.once("close", (code) => finish(typeof code === "number" ? code : 1));
108
+ function cleanup() {
109
+ settled = true;
110
+ if (timer)
111
+ clearTimeout(timer);
112
+ if (forceTimer)
113
+ clearTimeout(forceTimer);
114
+ options.signal?.removeEventListener("abort", abort);
115
+ }
116
+ if (options.signal?.aborted)
117
+ stop();
118
+ });
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LLM Wiki MCP Server
4
+ *
5
+ * Exposes wiki tools over the Model Context Protocol (MCP).
6
+ * Run: node dist/mcp/index.js
7
+ *
8
+ * Environment:
9
+ * WIKI_ROOT — path to wiki vault (default: auto-detect from cwd)
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { McpServer, StdioServerTransport } from "@modelcontextprotocol/server";
14
+ import * as z from "zod/v4";
15
+ import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
16
+ import { createExecApi } from "./exec.js";
17
+ import { captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
18
+ const execApi = createExecApi();
19
+ // ─── Vault Detection ────────────────────────────────────
20
+ /** Resolve vault paths, same as Pi extension. */
21
+ function getPaths() {
22
+ const root = process.env.WIKI_ROOT || process.cwd();
23
+ return resolveVaultPaths(root);
24
+ }
25
+ function hasVault() {
26
+ const paths = getPaths();
27
+ return existsSync(join(paths.dotWiki, "config.json"));
28
+ }
29
+ // ─── MCP Server ─────────────────────────────────────────
30
+ const server = new McpServer({
31
+ name: "llm-wiki",
32
+ version: "1.0.0",
33
+ });
34
+ // ---- wiki_recall ----
35
+ server.registerTool("wiki_recall", {
36
+ description: "Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
37
+ inputSchema: z.object({
38
+ query: z.string().describe("Search query — use the user's full request or key terms"),
39
+ max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
40
+ }),
41
+ }, async ({ query, max_results }) => {
42
+ if (!hasVault()) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
48
+ },
49
+ ],
50
+ isError: true,
51
+ };
52
+ }
53
+ const paths = getPaths();
54
+ const result = await recallOperation(paths, query, Math.min(max_results ?? 5, 10));
55
+ const detailLines = result.diagnostics.map((d) => `⚠️ ${d.code}: ${d.message}`);
56
+ const detailText = detailLines.length > 0 ? `\n\n${detailLines.join("\n")}` : "";
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: JSON.stringify(result.results, null, 2) + detailText,
62
+ },
63
+ ],
64
+ };
65
+ });
66
+ // ---- wiki_search ----
67
+ server.registerTool("wiki_search", {
68
+ description: "Search the wiki registry for pages matching a query.",
69
+ inputSchema: z.object({
70
+ query: z.string().describe("Search term"),
71
+ type: z
72
+ .string()
73
+ .optional()
74
+ .describe("Filter by page type (source, entity, concept, synthesis, analysis)"),
75
+ }),
76
+ }, async ({ query, type }) => {
77
+ if (!hasVault()) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
83
+ },
84
+ ],
85
+ isError: true,
86
+ };
87
+ }
88
+ const paths = getPaths();
89
+ const result = await searchOperation(paths, query, type);
90
+ if (result.matches.length === 0) {
91
+ return {
92
+ content: [{ type: "text", text: `No pages found for "${query}"` }],
93
+ };
94
+ }
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: JSON.stringify(result.matches, null, 2),
100
+ },
101
+ ],
102
+ };
103
+ });
104
+ // ---- wiki_status ----
105
+ server.registerTool("wiki_status", {
106
+ description: "Show wiki health and stats: page counts, orphans, recent activity.",
107
+ inputSchema: z.object({}),
108
+ }, async () => {
109
+ if (!hasVault()) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+ const paths = getPaths();
121
+ const status = await statusOperation(paths);
122
+ return {
123
+ content: [
124
+ {
125
+ type: "text",
126
+ text: JSON.stringify(status, null, 2),
127
+ },
128
+ ],
129
+ };
130
+ });
131
+ // ---- wiki_retro ----
132
+ server.registerTool("wiki_retro", {
133
+ description: "Save an atomic insight from a completed task into the wiki. Creates a source page.",
134
+ inputSchema: z.object({
135
+ slug: z.string().describe("Unique kebab-case identifier (e.g. 'jwt-revocation-pattern')"),
136
+ title: z.string().describe("Short descriptive title (60 chars max)"),
137
+ body: z.string().describe("Markdown body explaining what was learned."),
138
+ category: z
139
+ .string()
140
+ .optional()
141
+ .describe("Category (e.g. frontend, architecture, devops, bugfix)"),
142
+ }),
143
+ }, async ({ slug, title, body, category }) => {
144
+ if (!hasVault()) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
150
+ },
151
+ ],
152
+ isError: true,
153
+ };
154
+ }
155
+ const paths = getPaths();
156
+ const result = await retroOperation(paths, slug, title, body, category);
157
+ if (!result.ok) {
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `Vault error: ${result.diagnostics[0].message}`,
163
+ },
164
+ ],
165
+ isError: true,
166
+ };
167
+ }
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: `Insight saved: ${result.slug} — ${title}`,
173
+ },
174
+ ],
175
+ };
176
+ });
177
+ // ---- wiki_capture_source ----
178
+ server.registerTool("wiki_capture_source", {
179
+ description: "Capture a URL, local file, or pasted text into an immutable source packet.",
180
+ inputSchema: z.object({
181
+ text: z.string().optional().describe("Text content to capture"),
182
+ url: z.string().optional().describe("URL to capture"),
183
+ file_path: z.string().optional().describe("Local file path to capture"),
184
+ title: z.string().optional().describe("Title for the captured source"),
185
+ }),
186
+ }, async ({ text, url, file_path, title }) => {
187
+ if (!hasVault()) {
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
193
+ },
194
+ ],
195
+ isError: true,
196
+ };
197
+ }
198
+ const paths = getPaths();
199
+ const result = await captureSourceOperation(paths, { text, url, filePath: file_path, title }, execApi);
200
+ if (!result.ok) {
201
+ return {
202
+ content: [
203
+ {
204
+ type: "text",
205
+ text: result.diagnostics[0].message,
206
+ },
207
+ ],
208
+ isError: true,
209
+ };
210
+ }
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: `Source captured: ${result.sourceId}`,
216
+ },
217
+ ],
218
+ };
219
+ });
220
+ // ─── Main ───────────────────────────────────────────────
221
+ async function main() {
222
+ const transport = new StdioServerTransport();
223
+ await server.connect(transport);
224
+ console.error("🧠 LLM Wiki MCP Server running on stdio");
225
+ }
226
+ main().catch((err) => {
227
+ console.error("MCP Server error:", err);
228
+ process.exit(1);
229
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * MCP operation adapters over shared wiki services.
3
+ *
4
+ * Each operation is a thin, testable wrapper around the same services
5
+ * used by Pi tools. No operation parses YAML, scans files, scores
6
+ * registry entries, or builds page strings itself.
7
+ */
8
+ import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
9
+ import { searchWiki } from "../extensions/llm-wiki/lib/recall.js";
10
+ import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
11
+ import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
12
+ import { VaultWriteError, inspectVaultFormat, inspectWritableVault, } from "../extensions/llm-wiki/lib/vault-format.js";
13
+ import { getWikiStatus, searchRegistry } from "../extensions/llm-wiki/lib/wiki-service.js";
14
+ function projectionOutcome(projection) {
15
+ return projection.ok
16
+ ? { ok: true }
17
+ : {
18
+ ok: false,
19
+ diagnostics: projection.diagnostics.map(({ code, message }) => ({ code, message })),
20
+ };
21
+ }
22
+ /** Shared recall operation: calls searchWiki and appends vault diagnostics. */
23
+ export async function recallOperation(paths, query, maxResults = 5) {
24
+ const results = searchWiki(paths, query, maxResults);
25
+ const vaultState = inspectVaultFormat(paths);
26
+ return {
27
+ results,
28
+ diagnostics: vaultState.diagnostics.map((d) => ({ code: d.code, message: d.message })),
29
+ };
30
+ }
31
+ /** Shared search operation: delegates directly to wiki-service. */
32
+ export async function searchOperation(paths, query, type) {
33
+ const result = searchRegistry(paths, query, type);
34
+ return {
35
+ matches: result.matches,
36
+ diagnostics: result.diagnostics.map((d) => ({ code: d.code, message: d.message })),
37
+ };
38
+ }
39
+ /** Shared status operation: delegates directly to wiki-service. */
40
+ export async function statusOperation(paths) {
41
+ const status = getWikiStatus(paths);
42
+ return {
43
+ knowledgeFormat: status.knowledgeFormat,
44
+ totalPages: status.totalPages,
45
+ byType: status.byType,
46
+ blockingDiagnostics: status.blockingDiagnostics.map((d) => ({
47
+ code: d.code,
48
+ message: d.message,
49
+ })),
50
+ lastUpdated: status.lastUpdated,
51
+ };
52
+ }
53
+ /** Shared retro operation: validates vault then delegates to saveInsight. */
54
+ export async function retroOperation(paths, slug, title, body, category) {
55
+ const vaultCheck = inspectWritableVault(paths);
56
+ if (!vaultCheck.ok) {
57
+ return {
58
+ ok: false,
59
+ diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
60
+ };
61
+ }
62
+ try {
63
+ const result = saveInsight(paths, slug, title, body, category, { rebuild: false });
64
+ const projection = projectionOutcome(rebuildMetadata(paths));
65
+ if (!projection.ok)
66
+ return projection;
67
+ return { ok: true, slug: result.slug, sourcePagePath: result.sourcePagePath };
68
+ }
69
+ catch (error) {
70
+ if (error instanceof VaultWriteError) {
71
+ return {
72
+ ok: false,
73
+ diagnostics: error.diagnostics.map((d) => ({ code: d.code, message: d.message })),
74
+ };
75
+ }
76
+ if (error.message.startsWith("Invalid insight slug:")) {
77
+ return {
78
+ ok: false,
79
+ diagnostics: [{ code: "invalid_insight_slug", message: error.message }],
80
+ };
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ /** Shared capture operation: validates vault then delegates to capture functions. */
86
+ export async function captureSourceOperation(paths, input, execApi) {
87
+ const vaultCheck = inspectWritableVault(paths);
88
+ if (!vaultCheck.ok) {
89
+ return {
90
+ ok: false,
91
+ diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
92
+ };
93
+ }
94
+ try {
95
+ let sourceId;
96
+ if (input.url) {
97
+ sourceId = (await captureUrl(execApi, paths, input.url)).sourceId;
98
+ }
99
+ else if (input.filePath) {
100
+ sourceId = (await captureFile(execApi, paths, input.filePath)).sourceId;
101
+ }
102
+ else if (input.text) {
103
+ sourceId = captureText(paths, input.text, input.title).sourceId;
104
+ }
105
+ else {
106
+ return {
107
+ ok: false,
108
+ diagnostics: [
109
+ {
110
+ code: "event_missing_kind",
111
+ message: "Provide one of: text, url, or filePath",
112
+ },
113
+ ],
114
+ };
115
+ }
116
+ const projection = projectionOutcome(rebuildMetadata(paths));
117
+ if (!projection.ok)
118
+ return projection;
119
+ return { ok: true, sourceId };
120
+ }
121
+ catch (error) {
122
+ if (error instanceof VaultWriteError) {
123
+ return {
124
+ ok: false,
125
+ diagnostics: error.diagnostics.map((d) => ({ code: d.code, message: d.message })),
126
+ };
127
+ }
128
+ throw error;
129
+ }
130
+ }
@@ -0,0 +1 @@
1
+ {"type":"module"}
package/docs/api.md CHANGED
@@ -280,6 +280,8 @@ Health is `"⚠️ Warning"` when orphan count exceeds 5, `"🔴 Empty"` when th
280
280
  Force a full synchronous rebuild of all generated metadata: `registry.json`, `backlinks.json`,
281
281
  `index.md`, `log.md`. Use when metadata appears out of sync with actual wiki files.
282
282
 
283
+ If `meta/events.jsonl` is missing or unreadable, rebuild reports a warning and preserves existing log projections while continuing to rebuild registry, backlinks, and indexes. A present zero-byte event file is an intentional empty history.
284
+
283
285
  **Parameters**
284
286
 
285
287
  None.
@@ -294,8 +296,9 @@ details: { pageCount: number }
294
296
 
295
297
  ## wiki_log_event
296
298
 
297
- Append a structured event to `meta/events.jsonl` and regenerate `meta/log.md`. Every event is
298
- timestamped automatically.
299
+ Append a structured event to the authoritative, append-only `meta/events.jsonl` stream and regenerate available log projections. Every event is timestamped automatically. The event stream must be preserved in full-vault backups; generated Markdown logs cannot reconstruct it.
300
+
301
+ `details` is user-controlled and may appear in OKF-mode `wiki/log.md`. Do not include secrets, credentials, or private machine-local paths. Built-in local-file capture records its stable source ID and format in events, omitting the caller-supplied input path from the event stream and generated log projections. The exact path remains in the extension-owned raw manifest. Other wiki projections are not covered by this guarantee: extraction-failure text and source-page titles may still surface path-derived strings, so treat the source page area as potentially machine-local until a dedicated sanitization pass exists.
299
302
 
300
303
  **Parameters**
301
304
 
@@ -55,7 +55,7 @@ WIKI_ROOT/
55
55
  │ ├── analyses/ # Durable query answers
56
56
  │ ├── cases/ # One specific past task per trajectory
57
57
  │ └── skills/ # Reusable patterns distilled from trajectories
58
- ├── meta/ # Auto-generated (extension-owned)
58
+ ├── meta/ # Durable event source + generated internal projections
59
59
  │ ├── registry.json # Master page catalog
60
60
  │ ├── backlinks.json # Inbound link map
61
61
  │ ├── index.md # Human-readable catalog
@@ -73,9 +73,12 @@ WIKI_ROOT/
73
73
  | --------------------- | ------------------------ | ------------------------ |
74
74
  | `.llm-wiki/raw/**` | Extension | Immutable after capture |
75
75
  | `.llm-wiki/wiki/**` | Model + user | Editable knowledge pages |
76
- | `.llm-wiki/meta/**` | Extension | Auto-generated |
76
+ | `.llm-wiki/meta/events.jsonl` | Extension tools | Authoritative, append-only; preserve in full-vault backups |
77
+ | `.llm-wiki/meta/**` except `events.jsonl` | Extension | Generated projections |
77
78
  | `.llm-wiki/` | Human + explicit request | Operating rules |
78
79
 
80
+ `events.jsonl` records selected extension operations, not every filesystem edit. `meta/log.md` and OKF-mode `wiki/log.md` are one-way projections; neither can recover the event stream.
81
+
79
82
  ## Source Packet Format
80
83
 
81
84
  Each captured source becomes a packet:
@@ -32,6 +32,31 @@ The personal vault lives at `~/.llm-wiki/` (or `$WIKI_HOME`) and is always avail
32
32
  | `WIKI_HOME` | `~/.llm-wiki` | Override the personal wiki vault location |
33
33
  | `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
34
34
 
35
+ ## Pi Agent Settings
36
+
37
+ Runtime settings for the wiki's background tasks live in `.pi/settings.json` under the `llm-wiki` namespace. These can be set globally (`~/.pi/agent/settings.json`) or per-project (`<cwd>/.pi/settings.json`).
38
+
39
+ | Setting | Default | Description |
40
+ | --------------------- | ------- | ------------------------------------------------------------ |
41
+ | `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
42
+ | `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
43
+ | `trajectories` | false | Enable agent-trajectory working-memory |
44
+ | `notices` | true | Show wiki activity notices in chat |
45
+
46
+ Example:
47
+
48
+ ```json
49
+ {
50
+ "llm-wiki": {
51
+ "synthesisLanguage": "ru",
52
+ "taskModel": {
53
+ "provider": "openai",
54
+ "id": "gpt-4o"
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
35
60
  ## Vault Resolution
36
61
 
37
62
  The vault root is resolved in this priority order: