@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.
package/docs/api.md ADDED
@@ -0,0 +1,105 @@
1
+ # API Reference
2
+
3
+ ## Extension Tools
4
+
5
+ ### wiki_bootstrap
6
+
7
+ Initialize a new LLM Wiki vault.
8
+
9
+ ```
10
+ wiki_bootstrap(topic: string, mode?: "personal" | "company", root?: string)
11
+ ```
12
+
13
+ ### wiki_capture_source
14
+
15
+ Capture a URL, file, or text into an immutable source packet.
16
+
17
+ ```
18
+ wiki_capture_source(
19
+ inputType: "url" | "file" | "text",
20
+ value: string,
21
+ title?: string,
22
+ kind?: string,
23
+ tags?: string[],
24
+ createSourcePage?: boolean
25
+ )
26
+ ```
27
+
28
+ ### wiki_ingest
29
+
30
+ Get a batch of sources needing synthesis.
31
+
32
+ ```
33
+ wiki_ingest(batch_size?: number)
34
+ ```
35
+
36
+ ### wiki_ensure_page
37
+
38
+ Create or resolve a canonical page.
39
+
40
+ ```
41
+ wiki_ensure_page(
42
+ type: "concept" | "entity" | "synthesis" | "analysis",
43
+ title: string,
44
+ aliases?: string[],
45
+ tags?: string[],
46
+ summary?: string,
47
+ createIfMissing?: boolean
48
+ )
49
+ ```
50
+
51
+ ### wiki_search
52
+
53
+ Search the wiki registry.
54
+
55
+ ```
56
+ wiki_search(query: string, type?: string, limit?: number)
57
+ ```
58
+
59
+ ### wiki_lint
60
+
61
+ Health check the wiki.
62
+
63
+ ```
64
+ wiki_lint(mode?: string, writeReport?: boolean, limit?: number)
65
+ ```
66
+
67
+ ### wiki_status
68
+
69
+ Show wiki statistics.
70
+
71
+ ```
72
+ wiki_status()
73
+ ```
74
+
75
+ ### wiki_rebuild_meta
76
+
77
+ Force metadata rebuild.
78
+
79
+ ```
80
+ wiki_rebuild_meta()
81
+ ```
82
+
83
+ ### wiki_log_event
84
+
85
+ Record a structured event.
86
+
87
+ ```
88
+ wiki_log_event(
89
+ kind: string,
90
+ title: string,
91
+ summary?: string,
92
+ sourceIds?: string[],
93
+ pagePaths?: string[],
94
+ notes?: string[],
95
+ actor?: "agent" | "user" | "extension"
96
+ )
97
+ ```
98
+
99
+ ### wiki_watch
100
+
101
+ Schedule auto-updates.
102
+
103
+ ```
104
+ wiki_watch(schedule: string, command: string)
105
+ ```
@@ -0,0 +1,65 @@
1
+ # Architecture
2
+
3
+ ## Four Layers
4
+
5
+ ```
6
+ WIKI_ROOT/
7
+ ├── raw/sources/SRC-*/ # Immutable source packets (extension-owned)
8
+ │ ├── manifest.json # Capture metadata
9
+ │ ├── original/ # Original artifact
10
+ │ ├── extracted.md # Normalized markdown
11
+ │ └── attachments/ # Downloaded images, PDFs
12
+ ├── wiki/ # Editable knowledge pages (you + LLM)
13
+ │ ├── sources/ # One summary per source
14
+ │ ├── entities/ # People, orgs, tools, products
15
+ │ ├── concepts/ # Ideas, patterns, frameworks
16
+ │ ├── syntheses/ # Cross-cutting analyses
17
+ │ └── analyses/ # Durable query answers
18
+ ├── meta/ # Auto-generated (extension-owned)
19
+ │ ├── registry.json # Master page catalog
20
+ │ ├── backlinks.json # Inbound link map
21
+ │ ├── index.md # Human-readable catalog
22
+ │ ├── log.md # Activity log
23
+ │ └── events.jsonl # Structured event stream
24
+ └── .wiki/ # Config and templates
25
+ ├── config.json
26
+ └── templates/
27
+ ```
28
+
29
+ ## Ownership Rules
30
+
31
+ | Path | Owner | Rule |
32
+ | --------- | ------------------------ | ------------------------ |
33
+ | `raw/**` | Extension | Immutable after capture |
34
+ | `wiki/**` | Model + user | Editable knowledge pages |
35
+ | `meta/*` | Extension | Auto-generated |
36
+ | `.wiki/*` | Human + explicit request | Operating rules |
37
+
38
+ ## Source Packet Format
39
+
40
+ Each captured source becomes a packet:
41
+
42
+ ```
43
+ raw/sources/SRC-YYYY-MM-DD-NNN/
44
+ manifest.json
45
+ original/
46
+ extracted.md
47
+ attachments/
48
+ ```
49
+
50
+ ## Page Types
51
+
52
+ - **source** — what this specific source says
53
+ - **entity** — people, orgs, tools, products
54
+ - **concept** — ideas, patterns, frameworks
55
+ - **synthesis** — cross-source theses and tensions
56
+ - **analysis** — durable filed answers from queries
57
+
58
+ ## Linking Style
59
+
60
+ - Internal: `[[folder/page-name]]`
61
+ - Citation: `[[sources/SRC-YYYY-MM-DD-NNN]]`
62
+
63
+ ## Guardrails
64
+
65
+ The extension blocks direct edits to `raw/**` and `meta/**`. Metadata rebuilds automatically after `wiki/**` edits.
@@ -0,0 +1,51 @@
1
+ # Commands
2
+
3
+ ## Slash Commands
4
+
5
+ | Command | Description |
6
+ | ---------------- | ------------------------------------- |
7
+ | `/wiki-init` | Create a new wiki vault |
8
+ | `/wiki-ingest` | Process new sources |
9
+ | `/wiki-query` | Ask questions against the wiki |
10
+ | `/wiki-lint` | Health check |
11
+ | `/wiki-discover` | Auto-discover sources |
12
+ | `/wiki-run` | Full cycle (discover → ingest → lint) |
13
+ | `/wiki-status` | Show wiki health |
14
+ | `/wiki-digest` | Daily/weekly summary |
15
+
16
+ ## Extension Tools
17
+
18
+ The extension registers 10 tools the LLM can call directly:
19
+
20
+ | Tool | Purpose |
21
+ | --------------------- | ------------------------------------------- |
22
+ | `wiki_bootstrap` | Initialize a new vault |
23
+ | `wiki_capture_source` | Capture URL/file/text into immutable packet |
24
+ | `wiki_ingest` | Get batch of uningested sources |
25
+ | `wiki_ensure_page` | Create canonical page from template |
26
+ | `wiki_search` | Search the wiki registry |
27
+ | `wiki_lint` | Health check with auto-fix |
28
+ | `wiki_status` | Instant stats |
29
+ | `wiki_rebuild_meta` | Force metadata rebuild |
30
+ | `wiki_log_event` | Record custom event |
31
+ | `wiki_watch` | Schedule auto-updates |
32
+
33
+ ## Workflows
34
+
35
+ ### Capture → Ingest → Synthesize
36
+
37
+ 1. `wiki_capture_source(url="...")` — creates packet + skeleton
38
+ 2. `wiki_ingest()` — get batch of sources needing synthesis
39
+ 3. Read `raw/sources/SRC-*/extracted.md`
40
+ 4. Update skeleton source page with summary, entities, concepts
41
+ 5. `wiki_ensure_page(type="entity", title="...")` for each entity
42
+ 6. Add `[[wikilinks]]` between related pages
43
+ 7. Extension auto-rebuilds metadata
44
+
45
+ ### Query → Answer → File
46
+
47
+ 1. `wiki_search(query="...")` to find relevant pages
48
+ 2. Read those pages
49
+ 3. Synthesize answer with `[[wikilink]]` citations
50
+ 4. If novel: create analysis page via `wiki_ensure_page(type="analysis")`
51
+ 5. Extension auto-updates metadata
@@ -0,0 +1,38 @@
1
+ # Configuration
2
+
3
+ Wiki configuration lives in `.wiki/config.json`.
4
+
5
+ ## Modes
6
+
7
+ ### Personal
8
+
9
+ - Extra folders: `wiki/journal/`, `wiki/goals/`
10
+ - Track: learning, books, health, reflections
11
+
12
+ ### Company
13
+
14
+ - Extra folders: `wiki/changes/`, `wiki/decisions/`
15
+ - Track: competitors, market, strategy
16
+ - Frontmatter: `confidence: high | medium | low`
17
+
18
+ ## Settings
19
+
20
+ | Setting | Default | Description |
21
+ | -------------------------- | ------- | ---------------------------------- |
22
+ | `max_sources_per_discover` | 8 | Sources fetched per discovery run |
23
+ | `auto_fix_lint` | false | Auto-fix lint issues |
24
+ | `batch_ingest_size` | 3 | Sources processed per ingest batch |
25
+
26
+ ## Page Frontmatter
27
+
28
+ ```yaml
29
+ ---
30
+ type: entity | concept | source | synthesis | analysis
31
+ created: YYYY-MM-DD
32
+ updated: YYYY-MM-DD
33
+ sources: [sources/SRC-YYYY-MM-DD-NNN]
34
+ ---
35
+ ```
36
+
37
+ Entity: add `category: person | organization | tool | project | product`
38
+ Concept: add `domain: ai | engineering | business | product | design | personal`
@@ -0,0 +1,21 @@
1
+ # Obsidian Integration
2
+
3
+ ## Setup
4
+
5
+ 1. Open `wiki/` as an Obsidian vault
6
+ 2. The extension generates `meta/index.md` as a browsable catalog
7
+ 3. `meta/backlinks.json` is available for graph plugins
8
+
9
+ ## Recommended Plugins
10
+
11
+ - [Dataview](https://github.com/blacksmithgu/obsidian-dataview) — Query pages by frontmatter
12
+ - [Graph View](https://obsidian.md) (built-in) — Visualize `[[wikilink]]` connections
13
+ - [Backlinks](https://obsidian.md) (built-in) — See inbound links
14
+
15
+ ## Web Clipper
16
+
17
+ Use [Obsidian Web Clipper](https://obsidian.md/clipper) to save articles directly into `raw/articles/`.
18
+
19
+ ## Dataview Dashboard
20
+
21
+ The extension creates `meta/index.md` with page listings. For custom dashboards, use Dataview queries against frontmatter fields like `type`, `domain`, `category`, `sources`.
@@ -0,0 +1,53 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { installGuardrails } from "./lib/guardrails.js";
3
+ import {
4
+ registerWikiBootstrap,
5
+ registerWikiCaptureSource,
6
+ registerWikiEnsurePage,
7
+ registerWikiIngest,
8
+ registerWikiLint,
9
+ registerWikiLogEvent,
10
+ registerWikiRebuildMeta,
11
+ registerWikiSearch,
12
+ registerWikiStatus,
13
+ registerWikiWatch,
14
+ } from "./lib/tools.js";
15
+
16
+ /**
17
+ * @zosmaai/pi-llm-wiki — LLM Wiki extension for Pi
18
+ *
19
+ * Registers 10 custom tools and installs guardrails:
20
+ * - wiki_bootstrap Initialize a new vault
21
+ * - wiki_capture_source Capture URL/file/text into source packet
22
+ * - wiki_ingest Get batch of sources needing synthesis
23
+ * - wiki_ensure_page Create canonical page from template
24
+ * - wiki_search Search generated registry
25
+ * - wiki_lint Health check with auto-fix
26
+ * - wiki_status Instant stats from registry
27
+ * - wiki_rebuild_meta Force metadata rebuild
28
+ * - wiki_log_event Append event and regenerate log
29
+ * - wiki_watch Schedule auto-updates
30
+ *
31
+ * Guardrails:
32
+ * - Blocks direct edits to raw/** and meta/**
33
+ * - Auto-rebuilds metadata after wiki/** edits
34
+ */
35
+
36
+ export default function (pi: ExtensionAPI) {
37
+ registerWikiBootstrap(pi);
38
+ registerWikiCaptureSource(pi);
39
+ registerWikiIngest(pi);
40
+ registerWikiEnsurePage(pi);
41
+ registerWikiSearch(pi);
42
+ registerWikiLint(pi);
43
+ registerWikiStatus(pi);
44
+ registerWikiRebuildMeta(pi);
45
+ registerWikiLogEvent(pi);
46
+ registerWikiWatch(pi);
47
+
48
+ installGuardrails(pi);
49
+
50
+ pi.on("session_start", async (_event, ctx) => {
51
+ ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (10 tools, guardrails active)");
52
+ });
53
+ }
@@ -0,0 +1,69 @@
1
+ import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import { rebuildMetadataLight } from "./metadata.js";
4
+ import { isProtectedPath, resolveVaultRoot } from "./utils.js";
5
+
6
+ /**
7
+ * Guardrails and auto-rebuild hooks for the LLM Wiki extension.
8
+ */
9
+
10
+ let pendingRebuild = false;
11
+
12
+ /** Install guardrails on the extension API. */
13
+ export function installGuardrails(pi: ExtensionAPI): void {
14
+ // Block direct edits to raw/ and meta/
15
+ pi.on("tool_call", async (event) => {
16
+ if (isToolCallEventType("write", event)) {
17
+ const path = event.input.path as string;
18
+ const root = resolveVaultRoot(process.cwd());
19
+ const check = isProtectedPath(path, root);
20
+ if (check.protected) {
21
+ return { block: true, reason: check.reason };
22
+ }
23
+ }
24
+
25
+ if (isToolCallEventType("edit", event)) {
26
+ const path = event.input.path as string;
27
+ const root = resolveVaultRoot(process.cwd());
28
+ const check = isProtectedPath(path, root);
29
+ if (check.protected) {
30
+ return { block: true, reason: check.reason };
31
+ }
32
+ }
33
+ });
34
+
35
+ // Track wiki edits for auto-rebuild
36
+ pi.on("tool_result", async (event) => {
37
+ if (event.toolName === "write" || event.toolName === "edit") {
38
+ const path = event.input.path as string;
39
+ const root = resolveVaultRoot(process.cwd());
40
+ const wikiPath = `${root}/wiki/`;
41
+ if (path?.startsWith(wikiPath)) {
42
+ pendingRebuild = true;
43
+ }
44
+ }
45
+ });
46
+
47
+ // Rebuild metadata at end of turn if wiki was modified
48
+ pi.on("turn_end", async (_event) => {
49
+ if (pendingRebuild) {
50
+ pendingRebuild = false;
51
+ try {
52
+ const root = resolveVaultRoot(process.cwd());
53
+ const paths = {
54
+ root,
55
+ raw: `${root}/raw`,
56
+ rawSources: `${root}/raw/sources`,
57
+ wiki: `${root}/wiki`,
58
+ meta: `${root}/meta`,
59
+ dotWiki: `${root}/.wiki`,
60
+ outputs: `${root}/outputs`,
61
+ discoveries: `${root}/.discoveries`,
62
+ };
63
+ rebuildMetadataLight(paths);
64
+ } catch {
65
+ // Silently fail — metadata rebuild is best-effort
66
+ }
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,218 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import {
4
+ type VaultPaths,
5
+ extractWikilinks,
6
+ findWikiPages,
7
+ fmtDate,
8
+ parseFrontmatter,
9
+ readJson,
10
+ readText,
11
+ writeJson,
12
+ } from "./utils.js";
13
+
14
+ /**
15
+ * Metadata generation for the LLM Wiki.
16
+ *
17
+ * Rebuilds registry.json, backlinks.json, index.md, log.md, and lint-report.md
18
+ * deterministically from the current state of raw/ and wiki/.
19
+ */
20
+
21
+ export interface RegistryEntry {
22
+ type: "source" | "entity" | "concept" | "synthesis" | "analysis";
23
+ title: string;
24
+ created: string;
25
+ updated: string;
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ export interface Registry {
30
+ version: string;
31
+ last_updated: string;
32
+ pages: Record<string, RegistryEntry>;
33
+ }
34
+
35
+ export interface Backlinks {
36
+ [pageId: string]: string[];
37
+ }
38
+
39
+ export interface WikiEvent {
40
+ timestamp: string;
41
+ kind: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /** Rebuild the complete metadata layer. */
46
+ export function rebuildMetadata(paths: VaultPaths): void {
47
+ mkdirSync(paths.meta, { recursive: true });
48
+
49
+ const registry = buildRegistry(paths);
50
+ const backlinks = buildBacklinks(paths, registry);
51
+
52
+ writeJson(join(paths.meta, "registry.json"), registry);
53
+ writeJson(join(paths.meta, "backlinks.json"), backlinks);
54
+ writeFileSync(join(paths.meta, "index.md"), buildIndexMarkdown(registry), "utf-8");
55
+
56
+ const log = buildLogMarkdown(paths);
57
+ writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
58
+ }
59
+
60
+ /** Build registry from wiki/ and raw/ state. */
61
+ export function buildRegistry(paths: VaultPaths): Registry {
62
+ const pages: Record<string, RegistryEntry> = {};
63
+
64
+ // Scan wiki pages
65
+ for (const page of findWikiPages(paths.wiki)) {
66
+ const { frontmatter } = parseFrontmatter(page.content);
67
+ const type = String(frontmatter.type || "page") as RegistryEntry["type"];
68
+ const title = String(frontmatter.title || page.relative.split("/").pop() || "Untitled");
69
+
70
+ pages[page.relative] = {
71
+ type,
72
+ title,
73
+ created: String(frontmatter.created || fmtDate()),
74
+ updated: String(frontmatter.updated || frontmatter.created || fmtDate()),
75
+ ...frontmatter,
76
+ };
77
+ }
78
+
79
+ // Scan raw source packets
80
+ if (existsSync(paths.rawSources)) {
81
+ for (const entry of readdirSync(paths.rawSources)) {
82
+ const manifestPath = join(paths.rawSources, entry, "manifest.json");
83
+ if (!existsSync(manifestPath)) continue;
84
+
85
+ const manifest = readJson<Record<string, unknown>>(manifestPath, {});
86
+ const id = String(manifest.id || entry);
87
+ const sourcePage = `sources/${id}`;
88
+
89
+ if (!pages[sourcePage]) {
90
+ pages[sourcePage] = {
91
+ type: "source",
92
+ title: String(manifest.title || id),
93
+ created: String(manifest.captured || fmtDate()),
94
+ updated: String(manifest.captured || fmtDate()),
95
+ ...manifest,
96
+ };
97
+ }
98
+ }
99
+ }
100
+
101
+ return {
102
+ version: "1.0",
103
+ last_updated: new Date().toISOString(),
104
+ pages,
105
+ };
106
+ }
107
+
108
+ /** Build backlinks map from all wiki pages. */
109
+ export function buildBacklinks(paths: VaultPaths, registry: Registry): Backlinks {
110
+ const inbound: Backlinks = {};
111
+
112
+ // Initialize all pages with empty arrays
113
+ for (const id of Object.keys(registry.pages)) {
114
+ inbound[id] = [];
115
+ }
116
+
117
+ // Count inbound links
118
+ for (const page of findWikiPages(paths.wiki)) {
119
+ const links = extractWikilinks(page.content);
120
+ for (const link of links) {
121
+ if (inbound[link] && !inbound[link].includes(page.relative)) {
122
+ inbound[link].push(page.relative);
123
+ }
124
+ }
125
+ }
126
+
127
+ return inbound;
128
+ }
129
+
130
+ /** Build index markdown from registry. */
131
+ export function buildIndexMarkdown(registry: Registry): string {
132
+ const byType: Record<string, Array<{ id: string; entry: RegistryEntry }>> = {};
133
+
134
+ for (const [id, entry] of Object.entries(registry.pages)) {
135
+ const t = entry.type;
136
+ if (!byType[t]) byType[t] = [];
137
+ byType[t].push({ id, entry });
138
+ }
139
+
140
+ const sections: string[] = [];
141
+ sections.push(
142
+ "# Wiki Index\n\n> Auto-generated from meta/registry.json. Do not edit manually.\n",
143
+ );
144
+
145
+ for (const [type, items] of Object.entries(byType).sort()) {
146
+ const label = `${type.charAt(0).toUpperCase() + type.slice(1)}s`;
147
+ sections.push(`## ${label}\n`);
148
+ for (const { id, entry } of items.sort((a, b) => a.id.localeCompare(b.id))) {
149
+ sections.push(`- [[${id}]] — ${entry.title} *(created: ${entry.created})*`);
150
+ }
151
+ sections.push("");
152
+ }
153
+
154
+ sections.push(
155
+ `---\n*Last updated: ${registry.last_updated}* | *Total pages: ${Object.keys(registry.pages).length}*`,
156
+ );
157
+ return `${sections.join("\n")}\n`;
158
+ }
159
+
160
+ /** Build log markdown from events.jsonl. */
161
+ export function buildLogMarkdown(paths: VaultPaths): string {
162
+ const eventsPath = join(paths.meta, "events.jsonl");
163
+ const events: WikiEvent[] = [];
164
+
165
+ if (existsSync(eventsPath)) {
166
+ const raw = readFileSync(eventsPath, "utf-8").trim();
167
+ for (const line of raw.split("\n")) {
168
+ if (!line.trim()) continue;
169
+ try {
170
+ events.push(JSON.parse(line) as WikiEvent);
171
+ } catch {
172
+ // skip malformed
173
+ }
174
+ }
175
+ }
176
+
177
+ const lines: string[] = [];
178
+ lines.push("# Activity Log\n\n> Auto-generated from meta/events.jsonl. Do not edit manually.\n");
179
+
180
+ for (const ev of events) {
181
+ const ts = ev.timestamp || "unknown";
182
+ const kind = ev.kind || "event";
183
+ const details = Object.entries(ev)
184
+ .filter(([k]) => k !== "timestamp" && k !== "kind")
185
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
186
+ .join(", ");
187
+
188
+ lines.push(`## [${ts}] ${kind}`);
189
+ if (details) lines.push(`- ${details}`);
190
+ lines.push("");
191
+ }
192
+
193
+ if (events.length === 0) {
194
+ lines.push("_No events recorded yet._\n");
195
+ }
196
+
197
+ return `${lines.join("\n")}\n`;
198
+ }
199
+
200
+ /** Append an event to events.jsonl. */
201
+ export function appendEvent(paths: VaultPaths, event: Omit<WikiEvent, "timestamp">): void {
202
+ mkdirSync(paths.meta, { recursive: true });
203
+ const eventsPath = join(paths.meta, "events.jsonl");
204
+ const line = JSON.stringify({ timestamp: new Date().toISOString(), ...event });
205
+ writeFileSync(eventsPath, `${line}\n`, { flag: "a", encoding: "utf-8" });
206
+ }
207
+
208
+ /** Quick lightweight metadata rebuild (backlinks + index + log only). */
209
+ export function rebuildMetadataLight(paths: VaultPaths): void {
210
+ const registry = buildRegistry(paths);
211
+ const backlinks = buildBacklinks(paths, registry);
212
+ writeJson(join(paths.meta, "registry.json"), registry);
213
+ writeJson(join(paths.meta, "backlinks.json"), backlinks);
214
+ writeFileSync(join(paths.meta, "index.md"), buildIndexMarkdown(registry), "utf-8");
215
+
216
+ const log = buildLogMarkdown(paths);
217
+ writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
218
+ }