@zosmaai/pi-llm-wiki 0.1.6 → 0.2.1

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.6",
3
+ "version": "0.2.1",
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,10 +27,14 @@
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
40
  "extensions": ["./extensions"],
@@ -49,6 +53,7 @@
49
53
  "@biomejs/biome": "^1.9.4",
50
54
  "@mariozechner/pi-coding-agent": "^0.70.2",
51
55
  "@mermaid-js/mermaid-cli": "^11.12.0",
56
+ "@vitest/coverage-v8": "^3.2.4",
52
57
  "typebox": "^1.1.34",
53
58
  "typescript": "^5.7.0",
54
59
  "vitest": "^3.0.0"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Generate a daily or weekly digest of wiki changes — new sources, pages, insights, and gaps.
3
- args: [--period daily|weekly]
3
+ argument-hint: "[--period daily|weekly]"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Generate a digest of recent wiki activity.
11
11
 
12
+ ## User Arguments
13
+
14
+ $ARGUMENTS
15
+
12
16
  ## Steps
13
17
 
14
18
  1. Read `wiki/LOG.md` — filter entries since last digest
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Auto-discover new sources from the web. Searches based on config topics and known knowledge gaps.
3
- args: [--topic <topic>]
3
+ argument-hint: "[--topic <topic>]"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Find new source material for the wiki by searching the web.
11
11
 
12
+ ## User Arguments
13
+
14
+ $ARGUMENTS
15
+
12
16
  Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first. Also read `config.yaml` for topics and feeds.
13
17
 
14
18
  ## Steps
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Process new source files in raw/ and update the wiki. Creates summaries, entities, concepts, and cross-references.
3
- args: [path]
3
+ argument-hint: "[path]"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Process new files in `raw/` and integrate them into the wiki.
11
11
 
12
+ ## User Arguments
13
+
14
+ $ARGUMENTS
15
+
12
16
  Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand the full schema, page formats, and conventions.
13
17
 
14
18
  ## Steps
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Initialize a new LLM Wiki in the current directory. Creates the full directory structure, config, and template files.
3
- args: <topic> [--mode personal|company]
3
+ argument-hint: "<topic> [--mode personal|company]"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Initialize a new LLM Wiki in the current directory.
11
11
 
12
+ ## User Arguments
13
+
14
+ $ARGUMENTS
15
+
12
16
  Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` (or wherever the skill is installed) first to understand the full schema and conventions.
13
17
 
14
18
  ## Steps
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Health check the wiki. Detects contradictions, orphans, missing pages, stale claims, and knowledge gaps.
3
- args: [--fix]
3
+ argument-hint: "[--fix]"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Run a comprehensive health check on the wiki.
11
11
 
12
+ ## User Arguments
13
+
14
+ $ARGUMENTS
15
+
12
16
  Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand the full schema and conventions.
13
17
 
14
18
  ## Steps
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Ask questions against the wiki. Synthesizes answers from wiki pages with cross-reference citations.
3
- args: <question>
3
+ argument-hint: "<question>"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Ask a question and get an answer synthesized from wiki content.
11
11
 
12
+ ## User Question
13
+
14
+ $ARGUMENTS
15
+
12
16
  Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand the full schema and conventions.
13
17
 
14
18
  ## Steps
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Run the full wiki cycle: discover → ingest → lint. Optionally schedule for auto-updates.
3
- args: [--schedule daily|weekly]
3
+ argument-hint: "[--schedule daily|weekly]"
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -9,6 +9,10 @@ topLevelCli: true
9
9
 
10
10
  Run the complete wiki maintenance cycle: discover new sources, ingest them, and lint for health.
11
11
 
12
+ ## User Arguments
13
+
14
+ $ARGUMENTS
15
+
12
16
  Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first.
13
17
 
14
18
  ## Steps
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Show wiki health overview — source count, page stats, orphan count, last activity dates.
3
- args: []
3
+ argument-hint: ""
4
4
  section: LLM Wiki
5
5
  topLevelCli: true
6
6
  ---
@@ -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.`);