@zosmaai/pi-llm-wiki 0.1.2 → 0.2.0

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.
@@ -0,0 +1,222 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+
5
+ /**
6
+ * Vault utility functions for the LLM Wiki extension.
7
+ */
8
+
9
+ export interface VaultPaths {
10
+ root: string;
11
+ raw: string;
12
+ rawSources: string;
13
+ wiki: string;
14
+ meta: string;
15
+ dotWiki: string;
16
+ outputs: string;
17
+ discoveries: string;
18
+ }
19
+
20
+ /** Resolve vault root from cwd or find nearest wiki root. */
21
+ export function resolveVaultRoot(cwd: string): string {
22
+ // If cwd has .wiki/config.json, it's the root
23
+ if (existsSync(join(cwd, ".wiki", "config.json"))) return cwd;
24
+
25
+ // Walk up looking for .wiki/config.json
26
+ let dir = cwd;
27
+ while (dir !== dirname(dir)) {
28
+ if (existsSync(join(dir, ".wiki", "config.json"))) return dir;
29
+ dir = dirname(dir);
30
+ }
31
+
32
+ // Fallback: cwd itself
33
+ return cwd;
34
+ }
35
+
36
+ /** Get all vault paths. */
37
+ export function getVaultPaths(root: string): VaultPaths {
38
+ return {
39
+ root,
40
+ raw: join(root, "raw"),
41
+ rawSources: join(root, "raw", "sources"),
42
+ wiki: join(root, "wiki"),
43
+ meta: join(root, "meta"),
44
+ dotWiki: join(root, ".wiki"),
45
+ outputs: join(root, "outputs"),
46
+ discoveries: join(root, ".discoveries"),
47
+ };
48
+ }
49
+
50
+ /** Ensure all vault directories exist. */
51
+ export function ensureVaultStructure(paths: VaultPaths): void {
52
+ const dirs = [
53
+ paths.rawSources,
54
+ join(paths.raw, "assets"),
55
+ join(paths.wiki, "sources"),
56
+ join(paths.wiki, "entities"),
57
+ join(paths.wiki, "concepts"),
58
+ join(paths.wiki, "syntheses"),
59
+ join(paths.wiki, "analyses"),
60
+ paths.meta,
61
+ paths.dotWiki,
62
+ paths.outputs,
63
+ paths.discoveries,
64
+ join(paths.dotWiki, "templates"),
65
+ join(paths.dotWiki, "templates", "pages"),
66
+ ];
67
+ for (const d of dirs) mkdirSync(d, { recursive: true });
68
+ }
69
+
70
+ /** Read JSON file or return default. */
71
+ export function readJson<T>(path: string, defaultValue: T): T {
72
+ try {
73
+ if (!existsSync(path)) return defaultValue;
74
+ return JSON.parse(readFileSync(path, "utf-8")) as T;
75
+ } catch {
76
+ return defaultValue;
77
+ }
78
+ }
79
+
80
+ /** Write JSON file atomically. */
81
+ export function writeJson(path: string, data: unknown): void {
82
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
83
+ }
84
+
85
+ /** Read text file or return empty string. */
86
+ export function readText(path: string): string {
87
+ try {
88
+ if (!existsSync(path)) return "";
89
+ return readFileSync(path, "utf-8");
90
+ } catch {
91
+ return "";
92
+ }
93
+ }
94
+
95
+ /** Generate the next source ID. */
96
+ export function nextSourceId(paths: VaultPaths): string {
97
+ const today = new Date().toISOString().split("T")[0];
98
+ const prefix = `SRC-${today}`;
99
+
100
+ if (!existsSync(paths.rawSources)) return `${prefix}-001`;
101
+
102
+ const dirs = readdirSync(paths.rawSources)
103
+ .filter((d) => d.startsWith(prefix))
104
+ .sort();
105
+
106
+ if (dirs.length === 0) return `${prefix}-001`;
107
+
108
+ const last = dirs[dirs.length - 1];
109
+ const num = Number.parseInt(last.slice(-3), 10);
110
+ return `${prefix}-${String(num + 1).padStart(3, "0")}`;
111
+ }
112
+
113
+ /** Extract frontmatter from markdown. */
114
+ export function parseFrontmatter(content: string): {
115
+ frontmatter: Record<string, unknown>;
116
+ body: string;
117
+ } {
118
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
119
+ if (!match) return { frontmatter: {}, body: content };
120
+
121
+ const frontmatter: Record<string, unknown> = {};
122
+ const lines = match[1].split("\n");
123
+ for (const line of lines) {
124
+ const idx = line.indexOf(":");
125
+ if (idx > 0) {
126
+ const key = line.slice(0, idx).trim();
127
+ const val = line.slice(idx + 1).trim();
128
+ frontmatter[key] = val;
129
+ }
130
+ }
131
+ return { frontmatter, body: match[2] };
132
+ }
133
+
134
+ /** Find all wiki pages recursively. */
135
+ export function findWikiPages(
136
+ wikiDir: string,
137
+ ): Array<{ path: string; relative: string; content: string }> {
138
+ const results: Array<{ path: string; relative: string; content: string }> = [];
139
+
140
+ function walk(dir: string, rel: string) {
141
+ if (!existsSync(dir)) return;
142
+ for (const entry of readdirSync(dir)) {
143
+ const full = join(dir, entry);
144
+ const stat = statSync(full);
145
+ if (stat.isDirectory()) {
146
+ walk(full, rel ? `${rel}/${entry}` : entry);
147
+ } else if (entry.endsWith(".md")) {
148
+ results.push({
149
+ path: full,
150
+ relative: rel ? `${rel}/${entry.slice(0, -3)}` : entry.slice(0, -3),
151
+ content: readFileSync(full, "utf-8"),
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ walk(wikiDir, "");
158
+ return results;
159
+ }
160
+
161
+ /** Extract all [[wikilinks]] from content. */
162
+ export function extractWikilinks(content: string): string[] {
163
+ const links: string[] = [];
164
+ const regex = /\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g;
165
+ let m: RegExpExecArray | null = regex.exec(content);
166
+ while (m !== null) {
167
+ links.push(m[1]);
168
+ m = regex.exec(content);
169
+ }
170
+ return links;
171
+ }
172
+
173
+ /** Slugify a title. */
174
+ export function slugify(title: string): string {
175
+ return title
176
+ .toLowerCase()
177
+ .replace(/[^a-z0-9\s-]/g, "")
178
+ .trim()
179
+ .replace(/\s+/g, "-")
180
+ .slice(0, 80);
181
+ }
182
+
183
+ /** Format date as YYYY-MM-DD. */
184
+ export function fmtDate(d = new Date()): string {
185
+ return d.toISOString().split("T")[0];
186
+ }
187
+
188
+ /** Run a shell command via pi.exec. */
189
+ export async function exec(
190
+ pi: ExtensionAPI,
191
+ command: string,
192
+ args: string[],
193
+ options?: { signal?: AbortSignal; timeout?: number; cwd?: string },
194
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
195
+ const result = await pi.exec(command, args, options ?? {});
196
+ return result;
197
+ }
198
+
199
+ /** Check if a path is inside a protected directory. */
200
+ export function isProtectedPath(
201
+ absPath: string,
202
+ root: string,
203
+ ): { protected: boolean; reason?: string } {
204
+ const rawPath = resolve(root, "raw");
205
+ const metaPath = resolve(root, "meta");
206
+ const norm = resolve(absPath);
207
+
208
+ if (norm.startsWith(`${rawPath}/`) || norm === rawPath) {
209
+ return {
210
+ protected: true,
211
+ reason: "Raw sources are immutable. Use wiki_capture_source to add sources.",
212
+ };
213
+ }
214
+ if (norm.startsWith(`${metaPath}/`) || norm === metaPath) {
215
+ return {
216
+ protected: true,
217
+ reason: "Metadata is auto-generated. Use wiki_rebuild_meta or wiki_log_event instead.",
218
+ };
219
+ }
220
+
221
+ return { protected: false };
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "LLM Wiki for Pi — self-maintaining knowledge base following Karpathy's pattern. Obsidian-friendly, auto-updating, personal & company wiki.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -27,22 +27,20 @@
27
27
  "scripts": {
28
28
  "test": "vitest run",
29
29
  "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage",
30
31
  "typecheck": "tsc --noEmit",
31
32
  "lint": "biome check .",
32
33
  "lint:fix": "biome check --apply .",
33
- "prepare": "node -e \"try { require('husky').install() } catch {}\""
34
+ "release:patch": "node scripts/release.js patch",
35
+ "release:minor": "node scripts/release.js minor",
36
+ "release:major": "node scripts/release.js major",
37
+ "release:push": "git push origin main --tags"
34
38
  },
35
39
  "pi": {
36
- "extensions": [
37
- "./extensions"
38
- ],
39
- "skills": [
40
- "./skills"
41
- ],
42
- "prompts": [
43
- "./prompts"
44
- ],
45
- "image": "https://raw.githubusercontent.com/zosmaai/pi-llm-wiki/master/assets/screenshot.png"
40
+ "extensions": ["./extensions"],
41
+ "skills": ["./skills"],
42
+ "prompts": ["./prompts"],
43
+ "image": "https://raw.githubusercontent.com/zosmaai/pi-llm-wiki/main/assets/screenshot.png"
46
44
  },
47
45
  "peerDependencies": {
48
46
  "@mariozechner/pi-coding-agent": "*",
@@ -55,6 +53,7 @@
55
53
  "@biomejs/biome": "^1.9.4",
56
54
  "@mariozechner/pi-coding-agent": "^0.70.2",
57
55
  "@mermaid-js/mermaid-cli": "^11.12.0",
56
+ "@vitest/coverage-v8": "^3.2.4",
58
57
  "typebox": "^1.1.34",
59
58
  "typescript": "^5.7.0",
60
59
  "vitest": "^3.0.0"
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:digest
8
+ # /wiki-digest
9
9
 
10
10
  Generate a digest of recent wiki activity.
11
11
 
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:discover
8
+ # /wiki-discover
9
9
 
10
10
  Find new source material for the wiki by searching the web.
11
11
 
@@ -24,6 +24,6 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first. Also read `conf
24
24
  a. Fetch full content
25
25
  b. Save to `raw/articles/YYYY-MM-DD-slug.md` with frontmatter (title, url, discovered, topic)
26
26
  6. Update `.discoveries/history.json`
27
- 7. Report: "Discovered [N] new sources. Run `/wiki:ingest` to process them."
27
+ 7. Report: "Discovered [N] new sources. Run `/wiki-ingest` to process them."
28
28
 
29
29
  **Rules:** Max 5-10 sources. Skip ads, listicles, duplicates. Prefer in-depth analysis.
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:ingest
8
+ # /wiki-ingest
9
9
 
10
10
  Process new files in `raw/` and integrate them into the wiki.
11
11
 
@@ -14,7 +14,7 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
14
14
  ## Steps
15
15
 
16
16
  1. Read `config.yaml` and `.discoveries/history.json`
17
- 2. If a specific path is given (e.g., `/wiki:ingest raw/articles/my-file.md`), process just that file
17
+ 2. If a specific path is given (e.g., `/wiki-ingest raw/articles/my-file.md`), process just that file
18
18
  3. If no path given, scan all files in `raw/` and find ones not in history
19
19
  4. For each new source:
20
20
  a. Read the full content
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:init
8
+ # /wiki-init
9
9
 
10
10
  Initialize a new LLM Wiki in the current directory.
11
11
 
@@ -25,6 +25,6 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` (or wherever the skill
25
25
  6. Create `wiki/DASHBOARD.md` with Dataview queries for Obsidian
26
26
  7. Create `.gitignore` to exclude `outputs/` from version control if desired
27
27
  8. Initialize git repo if not already present
28
- 9. Report the structure and suggest first steps: "Drop sources into `raw/` and run `/wiki:ingest`"
28
+ 9. Report the structure and suggest first steps: "Drop sources into `raw/` and run `/wiki-ingest`"
29
29
 
30
30
  If `--mode company`, add the `change_detection: true` flag to config.yaml and add a `wiki/decisions/` folder.
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:lint
8
+ # /wiki-lint
9
9
 
10
10
  Run a comprehensive health check on the wiki.
11
11
 
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:query
8
+ # /wiki-query
9
9
 
10
10
  Ask a question and get an answer synthesized from wiki content.
11
11
 
@@ -24,7 +24,7 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
24
24
  **Example:**
25
25
 
26
26
  ```
27
- /wiki:query What are the key differences between RAG and LLM Wiki?
27
+ /wiki-query What are the key differences between RAG and LLM Wiki?
28
28
  → Reads INDEX.md, finds pages on RAG and LLM Wiki patterns
29
29
  → Reads both pages
30
30
  → Synthesizes a comparison table with [[wikilink]] citations
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:run
8
+ # /wiki-run
9
9
 
10
10
  Run the complete wiki maintenance cycle: discover new sources, ingest them, and lint for health.
11
11
 
@@ -13,9 +13,9 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first.
13
13
 
14
14
  ## Steps
15
15
 
16
- 1. Run `/wiki:discover` → find new sources
17
- 2. Run `/wiki:ingest` → process all new files
18
- 3. Run `/wiki:lint` → health check
16
+ 1. Run `/wiki-discover` → find new sources
17
+ 2. Run `/wiki-ingest` → process all new files
18
+ 3. Run `/wiki-lint` → health check
19
19
  4. If critical gaps found → optionally one more discover+ingest cycle
20
20
  5. Save summary → `outputs/run-YYYY-MM-DD.md`
21
21
  6. Report final summary
@@ -25,11 +25,11 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first.
25
25
  If `--schedule daily` is used, use `schedule_prompt` to set up daily runs:
26
26
 
27
27
  ```
28
- schedule_prompt action=add schedule="0 0 8 * * *" prompt="Run /wiki:run for the LLM Wiki"
28
+ schedule_prompt action=add schedule="0 0 8 * * *" prompt="Run /wiki-run for the LLM Wiki"
29
29
  ```
30
30
 
31
31
  If `--schedule weekly` is used:
32
32
 
33
33
  ```
34
- schedule_prompt action=add schedule="0 0 9 * * 1" prompt="Run /wiki:run for the LLM Wiki"
34
+ schedule_prompt action=add schedule="0 0 9 * * 1" prompt="Run /wiki-run for the LLM Wiki"
35
35
  ```
@@ -5,7 +5,7 @@ section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
7
7
 
8
- # /wiki:status
8
+ # /wiki-status
9
9
 
10
10
  Show a quick overview of wiki health and statistics.
11
11
 
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Semantic version release script.
4
+ * Usage: node scripts/release.js [patch|minor|major]
5
+ */
6
+
7
+ const { execSync } = require("node:child_process");
8
+ const fs = require("node:fs");
9
+ const path = require("node:path");
10
+
11
+ const bump = process.argv[2];
12
+ if (!["patch", "minor", "major"].includes(bump)) {
13
+ console.error("Usage: node scripts/release.js [patch|minor|major]");
14
+ process.exit(1);
15
+ }
16
+
17
+ // Verify clean tree
18
+ const status = execSync("git status --porcelain", { encoding: "utf-8" }).trim();
19
+ if (status) {
20
+ console.error("Error: working tree is not clean");
21
+ process.exit(1);
22
+ }
23
+
24
+ // Verify main branch
25
+ const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
26
+ if (branch !== "main") {
27
+ console.error("Error: not on main branch");
28
+ process.exit(1);
29
+ }
30
+
31
+ // Run checks
32
+ execSync("npm run typecheck", { stdio: "inherit" });
33
+ execSync("npm run lint", { stdio: "inherit" });
34
+ execSync("npm test", { stdio: "inherit" });
35
+
36
+ // Read current version
37
+ const pkgPath = path.join(__dirname, "..", "package.json");
38
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
39
+ const current = pkg.version;
40
+ const [major, minor, patch] = current.split(".").map(Number);
41
+
42
+ let next;
43
+ if (bump === "major") next = `${major + 1}.0.0`;
44
+ else if (bump === "minor") next = `${major}.${minor + 1}.0`;
45
+ else next = `${major}.${minor}.${patch + 1}`;
46
+
47
+ // Update package.json
48
+ pkg.version = next;
49
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
50
+
51
+ // Update CHANGELOG
52
+ const changelogPath = path.join(__dirname, "..", "CHANGELOG.md");
53
+ let changelog = "";
54
+ if (fs.existsSync(changelogPath)) {
55
+ changelog = fs.readFileSync(changelogPath, "utf-8");
56
+ }
57
+ const today = new Date().toISOString().split("T")[0];
58
+ const newSection = `## [${next}] - ${today}\n\n### Added\n- Release ${next}\n`;
59
+ if (changelog.includes("## [Unreleased]")) {
60
+ changelog = changelog.replace("## [Unreleased]", `## [Unreleased]\n\n${newSection}`);
61
+ } else {
62
+ changelog = `# Changelog\n\n## [Unreleased]\n\n${newSection}\n${changelog.replace("# Changelog\n\n", "")}`;
63
+ }
64
+ fs.writeFileSync(changelogPath, changelog, "utf-8");
65
+
66
+ // Commit and tag
67
+ execSync("git add package.json CHANGELOG.md", { stdio: "inherit" });
68
+ execSync(`git commit -m "chore(release): v${next}"`, { stdio: "inherit" });
69
+ execSync(`git tag v${next}`, { stdio: "inherit" });
70
+
71
+ console.log(`\n✅ Released v${next}`);
72
+ console.log(`Run "npm run release:push" to publish.`);