@zosmaai/pi-llm-wiki 0.10.7 → 0.10.9

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
@@ -447,6 +447,20 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
447
447
  <sub><b>Daniel Naab</b></sub>
448
448
  </a>
449
449
  </td>
450
+ <td align="center">
451
+ <a href="https://github.com/deestax">
452
+ <img src="https://avatars.githubusercontent.com/u/152369481?v=4" width="64;" alt="deestax"/>
453
+ <br />
454
+ <sub><b>Superdao</b></sub>
455
+ </a>
456
+ </td>
457
+ <td align="center">
458
+ <a href="https://github.com/xcsf">
459
+ <img src="https://avatars.githubusercontent.com/u/43439835?v=4" width="64;" alt="xcsf"/>
460
+ <br />
461
+ <sub><b>xcsf</b></sub>
462
+ </a>
463
+ </td>
450
464
  <td align="center">
451
465
  <a href="https://github.com/mystery4f">
452
466
  <img src="https://avatars.githubusercontent.com/u/40482524?v=4" width="64;" alt="mystery4f"/>
@@ -12,26 +12,162 @@ import { isProtectedPath, resolveVaultPaths } from "./utils.js";
12
12
 
13
13
  let pendingRebuild = false;
14
14
 
15
- const PATCH_HEADER = /^\[([^#\r\n]+)#[0-9A-F]{4}\]$/gm;
15
+ const APPLY_PATCH_PATH_NOISE =
16
+ /^\*{0,3}\s*(?:(?:update|add|delete|move)[^A-Za-z0-9]*(?:file|to)?[^A-Za-z0-9]*:)?\s*\*{0,3}\s*/i;
17
+ const PATCH_INPUT_KEYS: Record<string, true> = { input: true, _input: true, patch: true };
18
+ const DESTINATION_KEYS: Record<string, true> = {
19
+ rename: true,
20
+ move: true,
21
+ dest: true,
22
+ destination: true,
23
+ newPath: true,
24
+ };
16
25
 
17
- function collectMutationPaths(input: unknown, seen: WeakSet<object>): string[] {
26
+ interface MutationScan {
27
+ paths: string[];
28
+ complete: boolean;
29
+ }
30
+
31
+ function normalizeMutationPath(target: string): string | undefined {
32
+ const trimmed = target.trim();
33
+ if (!trimmed) return undefined;
34
+ const first = trimmed[0];
35
+ const last = trimmed[trimmed.length - 1];
36
+ const quoted = first === '"' || first === "'";
37
+ if (quoted !== (last === '"' || last === "'") || (quoted && first !== last)) {
38
+ return undefined;
39
+ }
40
+ const unquoted = quoted ? trimmed.slice(1, -1) : trimmed;
41
+ return unquoted.replace(APPLY_PATCH_PATH_NOISE, "") || undefined;
42
+ }
43
+
44
+ function parsePatchHeader(line: string): string | undefined {
45
+ const trimmed = line.replace(/\r$/, "").trimEnd();
46
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return undefined;
47
+
48
+ const body = trimmed.slice(1, -1).trim();
49
+ const tag = /#[0-9A-Fa-f]{4}\s*$/.exec(body);
50
+ const rawTarget = tag ? body.slice(0, tag.index) : body.replace(/\s+$/, "");
51
+ if (!rawTarget || rawTarget.includes("#")) return undefined;
52
+
53
+ return normalizeMutationPath(rawTarget);
54
+ }
55
+
56
+ function parseMoveDestination(line: string): string | undefined {
57
+ const rawDestination = line.trim().slice(2).trim();
58
+ if (!rawDestination) return undefined;
59
+ const quote = rawDestination[0];
60
+ if (quote !== '"' && quote !== "'") return normalizeMutationPath(rawDestination);
61
+
62
+ let cursor = 1;
63
+ while (cursor < rawDestination.length) {
64
+ if (rawDestination[cursor] === "\\" && cursor + 1 < rawDestination.length) {
65
+ cursor += 2;
66
+ continue;
67
+ }
68
+ if (rawDestination[cursor] === quote) {
69
+ return cursor === rawDestination.length - 1
70
+ ? normalizeMutationPath(rawDestination)
71
+ : undefined;
72
+ }
73
+ cursor++;
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ function scanPatchString(input: string): MutationScan {
79
+ const paths: string[] = [];
80
+ let sawHeader = false;
81
+ let sectionHasMove = false;
82
+ let complete = true;
83
+ const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
84
+
85
+ for (const line of stripped.split("\n")) {
86
+ const trimmed = line.replace(/\r$/, "").trim();
87
+ if (trimmed.startsWith("[")) {
88
+ sawHeader = true;
89
+ sectionHasMove = false;
90
+ const path = parsePatchHeader(line);
91
+ if (path) paths.push(path);
92
+ else complete = false;
93
+ continue;
94
+ }
95
+ if (!/^MV(?:\s|$)/.test(trimmed)) continue;
96
+ const destination = parseMoveDestination(trimmed);
97
+ if (!sawHeader || sectionHasMove || !destination) complete = false;
98
+ else {
99
+ paths.push(destination);
100
+ sectionHasMove = true;
101
+ }
102
+ }
103
+
104
+ return { paths, complete: sawHeader && complete };
105
+ }
106
+
107
+ function mergeMutationScans(target: MutationScan, source: MutationScan): void {
108
+ target.paths.push(...source.paths);
109
+ target.complete &&= source.complete;
110
+ }
111
+
112
+ function addMutationPath(scan: MutationScan, target: string): void {
113
+ const path = normalizeMutationPath(target);
114
+ if (path) scan.paths.push(path);
115
+ else scan.complete = false;
116
+ }
117
+
118
+ function collectMutationPaths(
119
+ input: unknown,
120
+ seen: WeakSet<object>,
121
+ stringsArePatches = false,
122
+ ): MutationScan {
18
123
  if (typeof input === "string") {
19
- return Array.from(input.matchAll(PATCH_HEADER), ([, target]) => target);
124
+ return stringsArePatches ? scanPatchString(input) : { paths: [], complete: true };
125
+ }
126
+ if (!input || typeof input !== "object" || seen.has(input)) {
127
+ return { paths: [], complete: true };
20
128
  }
21
- if (!input || typeof input !== "object" || seen.has(input)) return [];
22
129
 
23
130
  seen.add(input);
24
- if (Array.isArray(input)) return input.flatMap((value) => collectMutationPaths(value, seen));
131
+ const scan: MutationScan = { paths: [], complete: true };
132
+ if (Array.isArray(input)) {
133
+ for (const value of input) {
134
+ mergeMutationScans(scan, collectMutationPaths(value, seen, stringsArePatches));
135
+ }
136
+ return scan;
137
+ }
138
+
139
+ const record = input as Record<string, unknown>;
140
+ if (typeof record.path === "string" && record.path.length > 0) {
141
+ addMutationPath(scan, record.path);
142
+ }
143
+ const eventPaths = Array.isArray(record.paths) ? record.paths : [record.paths];
144
+ for (const path of eventPaths) {
145
+ if (typeof path === "string" && path.length > 0) addMutationPath(scan, path);
146
+ }
25
147
 
26
- const { path, ...nested } = input as Record<string, unknown>;
27
- if (typeof path === "string" && path.length > 0) return [path];
148
+ for (const [key, value] of Object.entries(record)) {
149
+ if (key === "path" || key === "paths") continue;
150
+ if (DESTINATION_KEYS[key] === true) {
151
+ if (typeof value === "string" && value.length > 0) addMutationPath(scan, value);
152
+ else scan.complete = false;
153
+ continue;
154
+ }
155
+ const childStringsArePatches = stringsArePatches || PATCH_INPUT_KEYS[key] === true;
156
+ if (typeof value !== "string" && (!value || typeof value !== "object")) continue;
157
+ mergeMutationScans(scan, collectMutationPaths(value, seen, childStringsArePatches));
158
+ }
159
+ return scan;
160
+ }
28
161
 
29
- return Object.values(nested).flatMap((value) => collectMutationPaths(value, seen));
162
+ function inspectMutationPaths(input: unknown): MutationScan {
163
+ const stringsArePatches = typeof input === "string" || Array.isArray(input);
164
+ const scan = collectMutationPaths(input, new WeakSet(), stringsArePatches);
165
+ return { paths: [...new Set(scan.paths)], complete: scan.complete };
30
166
  }
31
167
 
32
168
  /** Return every file path targeted by a write or patch-shaped edit input. */
33
169
  export function extractMutationPaths(input: unknown): string[] {
34
- return [...new Set(collectMutationPaths(input, new WeakSet()))];
170
+ return inspectMutationPaths(input).paths;
35
171
  }
36
172
 
37
173
  /** True when a write or patch-shaped edit targets a page in the wiki directory. */
@@ -59,8 +195,9 @@ export function installGuardrails(pi: ExtensionAPI, runtime?: Runtime): void {
59
195
  }
60
196
 
61
197
  if (isToolCallEventType("edit", event)) {
62
- const targetPaths = extractMutationPaths(event.input);
63
- if (targetPaths.length === 0) {
198
+ const mutation = inspectMutationPaths(event.input);
199
+ const targetPaths = mutation.paths;
200
+ if (!mutation.complete || targetPaths.length === 0) {
64
201
  return { block: true, reason: "Cannot determine the files targeted by this edit." };
65
202
  }
66
203
 
@@ -26,6 +26,7 @@ import {
26
26
  getVaultPaths,
27
27
  readJson,
28
28
  resolveVaultPaths,
29
+ slugify,
29
30
  writeJson,
30
31
  } from "./utils.js";
31
32
 
@@ -544,12 +545,7 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
544
545
  | "requirement"
545
546
  | "skill"
546
547
  | "case";
547
- const slug = params.title
548
- .toLowerCase()
549
- .replace(/[^a-z0-9\s-]/g, "")
550
- .trim()
551
- .replace(/\s+/g, "-")
552
- .slice(0, 80);
548
+ const slug = slugify(params.title);
553
549
 
554
550
  const folderMap: Record<string, string> = {
555
551
  entity: "entities",
@@ -370,12 +370,14 @@ export function extractWikilinks(content: string): string[] {
370
370
 
371
371
  /** Slugify a title. */
372
372
  export function slugify(title: string): string {
373
- return title
374
- .toLowerCase()
375
- .replace(/[^a-z0-9\s-]/g, "")
376
- .trim()
377
- .replace(/\s+/g, "-")
378
- .slice(0, 80);
373
+ return (
374
+ title
375
+ .toLocaleLowerCase()
376
+ .replace(/[^\p{L}\p{N}\s-]/gu, "")
377
+ .trim()
378
+ .replace(/\s+/g, "-")
379
+ .slice(0, 80) || "untitled"
380
+ );
379
381
  }
380
382
 
381
383
  /** Format date as YYYY-MM-DD. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.10.7",
3
+ "version": "0.10.9",
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",