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