@zosmaai/pi-llm-wiki 0.11.0 → 0.11.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.de.md +19 -1
  3. package/README.es.md +19 -1
  4. package/README.fr.md +19 -1
  5. package/README.hi.md +19 -1
  6. package/README.ja.md +19 -1
  7. package/README.ko.md +19 -1
  8. package/README.md +38 -3
  9. package/README.pt.md +19 -1
  10. package/README.ru.md +19 -1
  11. package/README.zh.md +19 -1
  12. package/dist/extensions/llm-wiki/lib/bootstrap.js +4 -1
  13. package/dist/extensions/llm-wiki/lib/ingest-worker.js +129 -29
  14. package/dist/extensions/llm-wiki/lib/metadata.js +37 -31
  15. package/dist/extensions/llm-wiki/lib/model-command.js +0 -1
  16. package/dist/extensions/llm-wiki/lib/runtime.js +0 -4
  17. package/dist/extensions/llm-wiki/lib/source-packet.js +1 -1
  18. package/dist/extensions/llm-wiki/lib/task-config.js +29 -0
  19. package/dist/extensions/llm-wiki/lib/tools.js +7 -0
  20. package/dist/extensions/llm-wiki/lib/utils.js +6 -0
  21. package/dist/mcp/index.js +2 -1
  22. package/docs/api.md +5 -2
  23. package/docs/architecture.md +5 -2
  24. package/docs/configuration.md +25 -0
  25. package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
  26. package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
  27. package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +17 -2
  28. package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +6 -2
  29. package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
  30. package/extensions/llm-wiki/lib/bootstrap.ts +4 -1
  31. package/extensions/llm-wiki/lib/ingest-worker.ts +161 -26
  32. package/extensions/llm-wiki/lib/knowledge-document.ts +2 -0
  33. package/extensions/llm-wiki/lib/metadata.ts +38 -31
  34. package/extensions/llm-wiki/lib/model-command.ts +0 -1
  35. package/extensions/llm-wiki/lib/runtime.ts +0 -3
  36. package/extensions/llm-wiki/lib/source-packet.ts +1 -1
  37. package/extensions/llm-wiki/lib/task-config.ts +36 -0
  38. package/extensions/llm-wiki/lib/tools.ts +9 -0
  39. package/extensions/llm-wiki/lib/utils.ts +7 -0
  40. package/mcp/index.ts +2 -1
  41. package/package.json +2 -2
  42. package/skills/llm-wiki/SKILL.md +4 -2
@@ -89,14 +89,16 @@ export function rebuildMetadata(paths: VaultPaths): ProjectionResult {
89
89
  const knownIds = new Set(documents.map((d) => d.id));
90
90
  const backlinks = buildBacklinks(documents, knownIds, allDiagnostics);
91
91
 
92
- const eventLogResult = buildOkfLog(readText(join(paths.meta, "events.jsonl")));
93
- allDiagnostics.push(...eventLogResult.diagnostics);
92
+ const eventSource = readEventSource(join(paths.meta, "events.jsonl"));
93
+ const eventLogResult = eventSource.available ? buildOkfLog(eventSource.content) : undefined;
94
+ if (eventLogResult) allDiagnostics.push(...eventLogResult.diagnostics);
95
+ else if (!eventSource.available) allDiagnostics.push(eventSource.diagnostic);
94
96
 
95
97
  // Step 5: Build meta/index.md
96
98
  const metaIndex = buildIndexMarkdown(registry);
97
99
 
98
100
  // Step 6: Build meta/log.md (existing rich format)
99
- const metaLog = buildLogMarkdown(paths);
101
+ const metaLog = eventSource.available ? buildLogMarkdown(eventSource.content) : undefined;
100
102
 
101
103
  // Step 7: Build OKF projections if in okf-0.2 mode
102
104
  const okfIndexes: Map<string, string> | null =
@@ -104,8 +106,8 @@ export function rebuildMetadata(paths: VaultPaths): ProjectionResult {
104
106
  ? buildDirectoryIndexes(documents, readJson(join(paths.dotWiki, "config.json"), {}))
105
107
  : null;
106
108
 
107
- const okfLog: string | null =
108
- vaultState.knowledgeFormat === "okf-0.2" ? eventLogResult.markdown : null;
109
+ const okfLog: string | undefined =
110
+ vaultState.knowledgeFormat === "okf-0.2" ? eventLogResult?.markdown : undefined;
109
111
 
110
112
  // Step 8: Atomic write all projections
111
113
  mkdirSync(paths.meta, { recursive: true });
@@ -116,7 +118,7 @@ export function rebuildMetadata(paths: VaultPaths): ProjectionResult {
116
118
  atomicWriteFile(join(paths.meta, "registry.json"), registryJson);
117
119
  atomicWriteFile(join(paths.meta, "backlinks.json"), backlinksJson);
118
120
  atomicWriteFile(join(paths.meta, "index.md"), metaIndex);
119
- atomicWriteFile(join(paths.meta, "log.md"), metaLog);
121
+ if (metaLog !== undefined) atomicWriteFile(join(paths.meta, "log.md"), metaLog);
120
122
 
121
123
  // Step 9: Write OKF projections if applicable
122
124
  if (okfIndexes && okfIndexes.size > 0) {
@@ -128,7 +130,7 @@ export function rebuildMetadata(paths: VaultPaths): ProjectionResult {
128
130
  pruneObsoleteIndexes(paths, okfIndexes);
129
131
  }
130
132
 
131
- if (okfLog !== null) {
133
+ if (okfLog !== undefined) {
132
134
  mkdirSync(paths.wiki, { recursive: true });
133
135
  atomicWriteFile(join(paths.wiki, "log.md"), okfLog);
134
136
  }
@@ -302,23 +304,19 @@ function buildIndexMarkdown(registry: Registry): string {
302
304
  return `${sections.join("\n")}\n`;
303
305
  }
304
306
 
305
- /** Build meta/log.md in existing rich format. */
306
- function buildLogMarkdown(paths: VaultPaths): string {
307
- const eventsPath = join(paths.meta, "events.jsonl");
307
+ function buildLogMarkdown(eventsJsonl: string): string {
308
308
  const events: WikiEvent[] = [];
309
+ const raw = eventsJsonl.trim();
309
310
 
310
- if (existsSync(eventsPath)) {
311
- const raw = readText(eventsPath).trim();
312
- for (const line of raw.split("\n")) {
313
- if (!line.trim()) continue;
314
- try {
315
- const candidate: unknown = JSON.parse(line);
316
- if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
317
- events.push(candidate as WikiEvent);
318
- }
319
- } catch {
320
- // skip malformed
311
+ for (const line of raw.split("\n")) {
312
+ if (!line.trim()) continue;
313
+ try {
314
+ const candidate: unknown = JSON.parse(line);
315
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
316
+ events.push(candidate as WikiEvent);
321
317
  }
318
+ } catch {
319
+ // Keep backward-compatible rich-log behavior: malformed lines are omitted.
322
320
  }
323
321
  }
324
322
 
@@ -338,10 +336,7 @@ function buildLogMarkdown(paths: VaultPaths): string {
338
336
  lines.push("");
339
337
  }
340
338
 
341
- if (events.length === 0) {
342
- lines.push("_No events recorded yet._\n");
343
- }
344
-
339
+ if (events.length === 0) lines.push("_No events recorded yet._\n");
345
340
  return `${lines.join("\n")}\n`;
346
341
  }
347
342
 
@@ -365,13 +360,25 @@ function atomicWriteFile(path: string, content: string): void {
365
360
  renameSync(temporary, path);
366
361
  }
367
362
 
368
- /** Read text file or return empty string. */
369
- function readText(path: string): string {
363
+ type EventSourceRead =
364
+ | { available: true; content: string }
365
+ | { available: false; diagnostic: KnowledgeDiagnostic };
366
+
367
+ function readEventSource(filePath: string, diagnosticPath = "meta/events.jsonl"): EventSourceRead {
370
368
  try {
371
- if (!existsSync(path)) return "";
372
- return readFileSync(path, "utf8");
373
- } catch {
374
- return "";
369
+ return { available: true, content: readFileSync(filePath, "utf8") };
370
+ } catch (error) {
371
+ const code = (error as NodeJS.ErrnoException).code;
372
+ // ENOENT = missing; ENOTDIR/EISDIR = not a regular file (FreeBSD dir hazard); else unreadable
373
+ const diagnosticCode = code === "ENOENT" ? "event_source_missing" : "event_source_unreadable";
374
+ const message =
375
+ diagnosticCode === "event_source_missing"
376
+ ? "Authoritative event source is missing; existing log projections were preserved"
377
+ : "Authoritative event source is unreadable; existing log projections were preserved";
378
+ return {
379
+ available: false,
380
+ diagnostic: okfDiag("warning", diagnosticCode, diagnosticPath, message),
381
+ };
375
382
  }
376
383
  }
377
384
 
@@ -63,7 +63,6 @@ export function registerWikiModelCommand(pi: ExtensionAPI, runtime: Runtime): vo
63
63
  const apply = (model: { provider: string; id: string } | undefined): void => {
64
64
  persistTaskModel(ctx.cwd, model);
65
65
  runtime.config = { ...runtime.config, taskModel: model };
66
- runtime.configLoaded = true;
67
66
  const label = formatActiveModelLabel(runtime.config, sessionId);
68
67
  ctx.ui.setStatus(MODEL_STATUS_KEY, `🧠 wiki model: ${label}`);
69
68
  ctx.ui.notify(`LLM Wiki: background tasks now use ${label}`, "info");
@@ -51,7 +51,6 @@ export interface LaunchCtx {
51
51
 
52
52
  export class Runtime {
53
53
  config: TaskConfig = { ...TASK_DEFAULTS };
54
- configLoaded = false;
55
54
 
56
55
  /**
57
56
  * Extension API handle, attached at registration. Used by `report()` to emit
@@ -68,9 +67,7 @@ export class Runtime {
68
67
  resolveFailureNotified = false;
69
68
 
70
69
  ensureConfig(cwd: string): void {
71
- if (this.configLoaded) return;
72
70
  this.config = loadTaskConfig(cwd);
73
- this.configLoaded = true;
74
71
  }
75
72
 
76
73
  /** True if a task with this label is currently running. */
@@ -147,7 +147,7 @@ function fileCaptureSource(pi: ExecApi, filePath: string, signal?: AbortSignal):
147
147
  file_path: filePath,
148
148
  format: extractor.format,
149
149
  }),
150
- event: () => ({ file_path: filePath, format: extractor.format }),
150
+ event: () => ({ format: extractor.format }),
151
151
  };
152
152
  }
153
153
 
@@ -100,6 +100,14 @@ export interface TaskConfig {
100
100
  * they cost nothing in the system prompt for the ~95% who don't use them.
101
101
  */
102
102
  trajectories?: boolean;
103
+
104
+ /**
105
+ * Language for background ingest synthesis narrative content (issue #124).
106
+ * BCP 47 language tag (e.g. "ru", "fr"). When unset, synthesis defaults to
107
+ * English. Applies to titles, summaries, takeaways, descriptions, etc.; raw
108
+ * source content and technical identifiers remain unchanged.
109
+ */
110
+ synthesisLanguage?: string;
103
111
  }
104
112
 
105
113
  export const TASK_DEFAULTS: TaskConfig = {};
@@ -175,6 +183,13 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
175
183
  if (typeof section.trajectories === "boolean") {
176
184
  out.trajectories = section.trajectories;
177
185
  }
186
+
187
+ const lang = section.synthesisLanguage;
188
+ if (typeof lang === "string" && lang.trim()) {
189
+ const canonical = validateSynthesisLanguage(lang.trim());
190
+ if (canonical) out.synthesisLanguage = canonical;
191
+ }
192
+
178
193
  return out;
179
194
  } catch {
180
195
  return {};
@@ -198,6 +213,27 @@ export function parseModelRef(ref: string): { provider: string; id: string } | u
198
213
  return { provider, id };
199
214
  }
200
215
 
216
+ /**
217
+ * Validate and canonicalize a BCP 47 language tag for synthesisLanguage (issue #124).
218
+ * Returns the canonical tag, or undefined if invalid or suspicious.
219
+ * Uses Intl.getCanonicalLocales for validation; rejects tags containing
220
+ * newlines, quotes, or other prompt-injection candidates.
221
+ */
222
+ export function validateSynthesisLanguage(tag: string): string | undefined {
223
+ // Reject obvious injection attempts: newlines, quotes, braces, angle brackets
224
+ if (/\n|\r|"|'|<|>|{|}/.test(tag)) return undefined;
225
+ // Reject if it looks like an instruction (contains "write", "ignore", "translate", etc.)
226
+ const lower = tag.toLowerCase();
227
+ if (/\b(write|ignore|translate|system|prompt|instruction)\b/.test(lower)) return undefined;
228
+ // Basic BCP 47 pattern: language[-script][-region][-variant]*
229
+ // Must start with 2-3 letter language code
230
+ if (!/^[a-z]{2,3}(?:-[A-Za-z]{1,8})*$/.test(tag)) return undefined;
231
+
232
+ const canonical = Intl.getCanonicalLocales(tag);
233
+ if (canonical.length === 0) return undefined;
234
+ return canonical[0];
235
+ }
236
+
201
237
  /**
202
238
  * Read a settings JSON file as a plain object, or `{}` when it is absent or
203
239
  * corrupt. Reads directly (no `existsSync` pre-check) so there is no
@@ -414,6 +414,7 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
414
414
  sourceId: s.id,
415
415
  manifest: s.manifest,
416
416
  extracted: s.extracted,
417
+ synthesisLanguage: runtime.config.synthesisLanguage,
417
418
  });
418
419
  if (committed) {
419
420
  // Background semantic embeddings (#66): embed the pages this
@@ -1112,6 +1113,14 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1112
1113
  if (!result.ok) {
1113
1114
  return `⚠️ LLM Wiki: rebuild had issues — ${result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("; ")}`;
1114
1115
  }
1116
+ const warnings = result.diagnostics.filter(
1117
+ (diagnostic) => diagnostic.severity === "warning",
1118
+ );
1119
+ if (warnings.length > 0) {
1120
+ return `⚠️ LLM Wiki: metadata rebuilt with warnings — ${warnings
1121
+ .map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
1122
+ .join("; ")}`;
1123
+ }
1115
1124
  const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
1116
1125
  version: "1.0",
1117
1126
  last_updated: "",
@@ -397,6 +397,13 @@ export function isProtectedPath(
397
397
  reason: "Raw sources are immutable. Use wiki_capture_source to add sources.",
398
398
  };
399
399
  }
400
+ if (relativePhysicalPath(paths.meta, absPath) === "events.jsonl") {
401
+ return {
402
+ protected: true,
403
+ reason:
404
+ "Event history is append-only authoritative state. Use wiki_log_event or an owning wiki operation instead.",
405
+ };
406
+ }
400
407
  if (isPathWithin(paths.meta, absPath)) {
401
408
  return {
402
409
  protected: true,
package/mcp/index.ts CHANGED
@@ -12,7 +12,8 @@
12
12
 
13
13
  import { existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import { McpServer, StdioServerTransport } from "@modelcontextprotocol/server";
15
+ import { McpServer } from "@modelcontextprotocol/server";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
16
17
  import * as z from "zod/v4";
17
18
  import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
18
19
  import { createExecApi } from "./exec.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  "dependencies": {
75
75
  "@cfworker/json-schema": "^4.1.1",
76
- "@modelcontextprotocol/server": "^2.0.0-alpha.2",
76
+ "@modelcontextprotocol/server": "^2.0.0",
77
77
  "mdast-util-from-markdown": "^2.0.3",
78
78
  "node-html-markdown": "^2.0.0",
79
79
  "yaml": "^2.9.0",
@@ -32,7 +32,7 @@ WIKI_ROOT/
32
32
  │ ├── analyses/ # Durable query answers
33
33
  │ ├── cases/ # One specific past task per trajectory
34
34
  │ └── skills/ # Reusable patterns distilled from trajectories
35
- ├── meta/ # Auto-generated (extension-owned)
35
+ ├── meta/ # Durable events + generated projections (extension-owned)
36
36
  │ ├── registry.json # Master page catalog
37
37
  │ ├── backlinks.json # Inbound link map
38
38
  │ ├── index.md # Human-readable catalog
@@ -45,13 +45,15 @@ WIKI_ROOT/
45
45
  ## Golden Rules
46
46
 
47
47
  1. **RAW IS IMMUTABLE.** Never edit `raw/`. Use `wiki_capture_source` to add sources.
48
- 2. **META IS AUTO-GENERATED.** Never edit `meta/`. The extension rebuilds it automatically.
48
+ 2. **META IS EXTENSION-OWNED.** Never edit `meta/` directly. `events.jsonl` is append-only authoritative activity state; other metadata files are generated projections.
49
49
  3. **YOU OWN THE WIKI.** Create, update, and cross-reference everything in `wiki/`.
50
50
  4. **ONE FILE PER THING.** Each entity, concept, source gets its own `.md` file.
51
51
  5. **CROSS-REFERENCE EVERYTHING.** Every page needs at least 2 links. Prefer standard Markdown: `[label](/folder/page.md)`. Legacy wikilinks `[[folder/page]]` remain readable.
52
52
  6. **CITE SOURCES.** Every claim links back to its raw source packet.
53
53
  7. **FLAG CONTRADICTIONS.** When sources disagree, document both sides.
54
54
 
55
+ > Preserve `meta/events.jsonl` in full-vault backups. `meta/log.md` and `wiki/log.md` cannot reconstruct it. Do not place secrets or private machine paths in manual event details.
56
+
55
57
  ## Agent Working-Memory (Trajectories)
56
58
 
57
59
  The wiki captures not only what you *read* (sources) but what you *do*