@zosmaai/pi-llm-wiki 0.10.5 → 0.10.7

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/README.md CHANGED
@@ -440,6 +440,13 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
440
440
  <sub><b>Akshay</b></sub>
441
441
  </a>
442
442
  </td>
443
+ <td align="center">
444
+ <a href="https://github.com/danielnaab">
445
+ <img src="https://avatars.githubusercontent.com/u/136512?v=4" width="64;" alt="danielnaab"/>
446
+ <br />
447
+ <sub><b>Daniel Naab</b></sub>
448
+ </a>
449
+ </td>
443
450
  <td align="center">
444
451
  <a href="https://github.com/mystery4f">
445
452
  <img src="https://avatars.githubusercontent.com/u/40482524?v=4" width="64;" alt="mystery4f"/>
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, join } from "node:path";
3
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
4
  import { installGuardrails } from "./lib/guardrails.js";
5
- import { buildAgentStartInjection } from "./lib/inject.js";
5
+ import { buildAgentStartInjection, normalizeSystemPrompt } from "./lib/inject.js";
6
6
  import { registerWikiModelCommand } from "./lib/model-command.js";
7
7
  import {
8
8
  buildSessionNotice,
@@ -303,13 +303,12 @@ Then call wiki_bootstrap with the inferred topic and mode to finalize the setup.
303
303
 
304
304
  // Split into a cache-stable system prompt (static footer only) and a
305
305
  // volatile tail message (issue #92). See lib/inject.ts for the contract.
306
- const { systemPrompt, message } = buildAgentStartInjection(event.systemPrompt || "", [
307
- dynamicContext,
308
- ]);
306
+ const priorSystemPrompt = normalizeSystemPrompt(event.systemPrompt);
307
+ const { systemPrompt, message } = buildAgentStartInjection(priorSystemPrompt, [dynamicContext]);
309
308
 
310
309
  // Only claim a systemPrompt change when the footer actually altered the
311
310
  // string (a carry-forward turn already carries it, so this no-ops).
312
- const systemPromptChanged = systemPrompt !== event.systemPrompt;
311
+ const systemPromptChanged = systemPrompt !== priorSystemPrompt;
313
312
  if (!systemPromptChanged && !message) return;
314
313
  return {
315
314
  ...(systemPromptChanged ? { systemPrompt } : {}),
@@ -1,3 +1,4 @@
1
+ import { resolve, sep } from "node:path";
1
2
  import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
2
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
4
  import { scheduleReindex } from "./indexing.js";
@@ -11,6 +12,39 @@ import { isProtectedPath, resolveVaultPaths } from "./utils.js";
11
12
 
12
13
  let pendingRebuild = false;
13
14
 
15
+ const PATCH_HEADER = /^\[([^#\r\n]+)#[0-9A-F]{4}\]$/gm;
16
+
17
+ function collectMutationPaths(input: unknown, seen: WeakSet<object>): string[] {
18
+ if (typeof input === "string") {
19
+ return Array.from(input.matchAll(PATCH_HEADER), ([, target]) => target);
20
+ }
21
+ if (!input || typeof input !== "object" || seen.has(input)) return [];
22
+
23
+ seen.add(input);
24
+ if (Array.isArray(input)) return input.flatMap((value) => collectMutationPaths(value, seen));
25
+
26
+ const { path, ...nested } = input as Record<string, unknown>;
27
+ if (typeof path === "string" && path.length > 0) return [path];
28
+
29
+ return Object.values(nested).flatMap((value) => collectMutationPaths(value, seen));
30
+ }
31
+
32
+ /** Return every file path targeted by a write or patch-shaped edit input. */
33
+ export function extractMutationPaths(input: unknown): string[] {
34
+ return [...new Set(collectMutationPaths(input, new WeakSet()))];
35
+ }
36
+
37
+ /** True when a write or patch-shaped edit targets a page in the wiki directory. */
38
+ export function hasWikiMutation(input: unknown, wikiPath: string): boolean {
39
+ const resolvedWikiPath = resolve(wikiPath);
40
+ return extractMutationPaths(input).some((path) => {
41
+ const resolvedPath = resolve(path);
42
+ return (
43
+ resolvedPath === resolvedWikiPath || resolvedPath.startsWith(`${resolvedWikiPath}${sep}`)
44
+ );
45
+ });
46
+ }
47
+
14
48
  /** Install guardrails on the extension API. */
15
49
  export function installGuardrails(pi: ExtensionAPI, runtime?: Runtime): void {
16
50
  // Block direct edits to raw/ and meta/
@@ -25,11 +59,17 @@ export function installGuardrails(pi: ExtensionAPI, runtime?: Runtime): void {
25
59
  }
26
60
 
27
61
  if (isToolCallEventType("edit", event)) {
28
- const path = event.input.path as string;
62
+ const targetPaths = extractMutationPaths(event.input);
63
+ if (targetPaths.length === 0) {
64
+ return { block: true, reason: "Cannot determine the files targeted by this edit." };
65
+ }
66
+
29
67
  const paths = resolveVaultPaths(process.cwd());
30
- const check = isProtectedPath(path, paths);
31
- if (check.protected) {
32
- return { block: true, reason: check.reason };
68
+ for (const path of targetPaths) {
69
+ const check = isProtectedPath(path, paths);
70
+ if (check.protected) {
71
+ return { block: true, reason: check.reason };
72
+ }
33
73
  }
34
74
  }
35
75
  });
@@ -37,10 +77,8 @@ export function installGuardrails(pi: ExtensionAPI, runtime?: Runtime): void {
37
77
  // Track wiki edits for auto-rebuild
38
78
  pi.on("tool_result", async (event) => {
39
79
  if (event.toolName === "write" || event.toolName === "edit") {
40
- const path = event.input.path as string;
41
80
  const paths = resolveVaultPaths(process.cwd());
42
- const wikiPath = `${paths.wiki}/`;
43
- if (path?.startsWith(wikiPath)) {
81
+ if (hasWikiMutation(event.input, paths.wiki)) {
44
82
  pendingRebuild = true;
45
83
  }
46
84
  }
@@ -33,6 +33,14 @@ export function appendWikiStatus(systemPrompt: string): string {
33
33
  return `${base}\n\n${WIKI_STATUS_BLOCK}`;
34
34
  }
35
35
 
36
+ /** Normalize upstream Pi and OMP system-prompt representations. */
37
+ export function normalizeSystemPrompt(
38
+ systemPrompt: string | readonly string[] | null | undefined,
39
+ ): string {
40
+ if (Array.isArray(systemPrompt)) return systemPrompt.join("\n\n");
41
+ return typeof systemPrompt === "string" ? systemPrompt : "";
42
+ }
43
+
36
44
  /** customType of the hidden tail message carrying volatile per-turn context. */
37
45
  export const WIKI_RECALL_MESSAGE_TYPE = "wiki-recall-context";
38
46
 
@@ -69,10 +77,10 @@ export interface AgentStartInjection {
69
77
  * Pure and side-effect free — see test/agent-start-injection.test.ts.
70
78
  */
71
79
  export function buildAgentStartInjection(
72
- baseSystemPrompt: string,
80
+ baseSystemPrompt: string | readonly string[] | null | undefined,
73
81
  dynamicBlocks: Array<string | undefined>,
74
82
  ): AgentStartInjection {
75
- const systemPrompt = appendWikiStatus(baseSystemPrompt);
83
+ const systemPrompt = appendWikiStatus(normalizeSystemPrompt(baseSystemPrompt));
76
84
  const content = dynamicBlocks
77
85
  .map((b) => b?.trim())
78
86
  .filter((b): b is string => Boolean(b))
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { join, relative } from "node:path";
3
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
5
  import { launchEmbedPages, reindexEmbeddings, resolveEmbedder } from "./embeddings.js";
@@ -390,7 +390,11 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
390
390
  const manifestPath = join(paths.rawSources, id, "manifest.json");
391
391
  const extracted = existsSync(extractedPath) ? readFileSync(extractedPath, "utf-8") : "";
392
392
  const manifest = readJson<Record<string, unknown>>(manifestPath, {});
393
- return { id, extracted, manifest };
393
+ // Vault-relative path used in tool messages so the read tool can open
394
+ // the file from the vault root (fix #101: agent previously got
395
+ // "raw/sources/..." and failed on new-layout vaults).
396
+ const relRaw = relative(paths.root, paths.rawSources);
397
+ return { id, extracted, manifest, relRaw };
394
398
  });
395
399
 
396
400
  // ── Background synthesis (issue #65) ──────────────────
@@ -474,7 +478,7 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
474
478
  [
475
479
  `- **${s.id}**: ${s.manifest.title || s.id}`,
476
480
  ` - Extracted: ${s.extracted.length} chars`,
477
- ` - Read: \`raw/sources/${s.id}/extracted.md\``,
481
+ ` - Read: \`${s.relRaw}/${s.id}/extracted.md\``,
478
482
  ].join("\n"),
479
483
  ),
480
484
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
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",
@@ -19,7 +19,7 @@ $ARGUMENTS
19
19
  2. If the tool reports "All sources ingested", inform the user and stop.
20
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
21
  4. **Otherwise** (the tool returned extracted content — background unavailable or `background=false`), for each source in the returned batch:
22
- a. Read the extracted text from `raw/sources/<SOURCE_ID>/extracted.md`
22
+ a. Read the extracted text from `.llm-wiki/raw/sources/<SOURCE_ID>/extracted.md`
23
23
  b. Update the skeleton source page in `wiki/sources/` with a proper summary, key entities, and concepts
24
24
  c. Use `wiki_ensure_page(type=entity, title=<name>)` for each new entity (people, orgs, tools, products)
25
25
  d. Use `wiki_ensure_page(type=concept, title=<name>)` for each new concept (ideas, patterns, frameworks)