@zosmaai/pi-llm-wiki 0.10.6 → 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.
@@ -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
  }
@@ -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.6",
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)