@zosmaai/pi-llm-wiki 0.7.0 → 0.7.2
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 +33 -4
- package/docs/architecture.md +32 -1
- package/docs/commands.md +1 -1
- package/docs/configuration.md +15 -3
- package/extensions/llm-wiki/index.ts +19 -12
- package/extensions/llm-wiki/lib/recall.ts +67 -8
- package/extensions/llm-wiki/lib/retro.ts +23 -59
- package/extensions/llm-wiki/lib/utils.ts +36 -3
- package/package.json +1 -1
- package/prompts/wiki-retro.md +3 -3
- package/skills/llm-wiki/SKILL.md +32 -35
package/README.md
CHANGED
|
@@ -49,15 +49,16 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
|
|
|
49
49
|
|
|
50
50
|
| Capability | Description |
|
|
51
51
|
|------------|-------------|
|
|
52
|
+
| 🏠 **Personal fallback** | Always-on `~/.llm-wiki/` vault — knowledge compounds across projects even when no project wiki exists |
|
|
52
53
|
| 🔗 **Immutable source capture** | URLs, local files (PDF/md/txt/html/XML/JSON), or pasted text → structured source packets |
|
|
53
54
|
| 🧠 **Automated ingestion** | `wiki_ingest` batch-processes sources into concept, entity, synthesis & analysis pages |
|
|
54
55
|
| 🔍 **Full-text search** | Generated registry with keyword lookup across all pages and sources |
|
|
55
56
|
| 🩺 **Mechanical linting** | Orphans, broken links, duplicate aliases, coverage gaps, stale captures |
|
|
56
57
|
| 📊 **Dashboard** | `wiki_status` — counts, source states, recent activity |
|
|
57
58
|
| 🤖 **Auto-update watch** | `wiki_watch` — schedule periodic discovery + ingest |
|
|
58
|
-
| 🧠 **
|
|
59
|
+
| 🧠 **Layered recall** | Searches both personal (`~/.llm-wiki/`) and project (`.llm-wiki/`) vaults — personal knowledge follows you everywhere |
|
|
59
60
|
| 📝 **Auto-bootstrap** | Extension suggests creating a wiki when none exists in the current directory |
|
|
60
|
-
| 💾 **
|
|
61
|
+
| 💾 **Lightweight capture** | `wiki_retro` — save atomic insights as a single markdown file; full 4-layer pipeline also available via `wiki_capture_source` |
|
|
61
62
|
| 🌐 **MCP Server** | Use with Claude Code, Cursor, Windsurf via stdio MCP transport |
|
|
62
63
|
| 📝 **Obsidian-friendly** | Folder-qualified wikilinks, stable source-ID citations, compatible vault |
|
|
63
64
|
| 🛡️ **Guardrails** | Blocks direct edits to raw sources and generated metadata |
|
|
@@ -72,7 +73,7 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
|
|
|
72
73
|
|------|-------------|
|
|
73
74
|
| `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
|
|
74
75
|
| `wiki_capture_source` | Capture a URL, local file, or pasted text into an immutable source packet |
|
|
75
|
-
| `wiki_recall` |
|
|
76
|
+
| `wiki_recall` | Search wiki for task-relevant pages — searches both personal (`~/.llm-wiki/`) and project (`.llm-wiki/`) vaults, deduplicated |
|
|
76
77
|
| `wiki_retro` | Save atomic insights from completed tasks into the wiki |
|
|
77
78
|
| `wiki_ingest` | Process uningested source packets into wiki pages (batch) |
|
|
78
79
|
| `wiki_ensure_page` | Resolve or safely create entity / concept / synthesis / analysis pages |
|
|
@@ -99,6 +100,28 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
|
|
|
99
100
|
|
|
100
101
|
---
|
|
101
102
|
|
|
103
|
+
## Layered Vault Architecture
|
|
104
|
+
|
|
105
|
+
Knowledge follows you everywhere. pi-llm-wiki uses a layered vault system:
|
|
106
|
+
|
|
107
|
+
| Layer | Location | Purpose |
|
|
108
|
+
|-------|----------|---------|
|
|
109
|
+
| 🏠 **Personal** | `~/.llm-wiki/` | Always active. Zero setup. Knowledge compounds across all your sessions — regardless of which project you're in. |
|
|
110
|
+
| 📁 **Project** | `{project}/.llm-wiki/` | Explicit opt-in. Dedicated wiki per project, sharing personal knowledge when relevant. |
|
|
111
|
+
| 🏢 **Company** (future) | git-tracked | Shared wiki across a team. `wiki_publish` promotes personal/project pages to the company wiki. |
|
|
112
|
+
|
|
113
|
+
**How it works:**
|
|
114
|
+
|
|
115
|
+
1. `resolveVaultRoot()` checks: cwd → walk up for `.llm-wiki/` → `~/.llm-wiki/`
|
|
116
|
+
2. `wiki_recall` (layered) searches **both** personal and project vaults, merging results with vault labels
|
|
117
|
+
3. Personal results are shown first in recall output, tagged as "📓 personal"
|
|
118
|
+
4. `wiki_retro` writes to whichever vault is active (project takes priority)
|
|
119
|
+
5. Set `WIKI_HOME` env var to override the personal wiki location
|
|
120
|
+
|
|
121
|
+
This means: you can have a project wiki for team documentation **and** a personal wiki for your own notes, and recall searches both simultaneously.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
102
125
|
## Quick Start (Detailed)
|
|
103
126
|
|
|
104
127
|
### 1) Create a new wiki
|
|
@@ -309,7 +332,13 @@ The bundled `llm-wiki` skill teaches the model to:
|
|
|
309
332
|
|
|
310
333
|
## Architecture
|
|
311
334
|
|
|
312
|
-
|
|
335
|
+
### Vault Layers
|
|
336
|
+
|
|
337
|
+
See the [Layered Vault Architecture](#layered-vault-architecture) section above for the personal/project/company layering.
|
|
338
|
+
|
|
339
|
+
### Four-Layer Page Model
|
|
340
|
+
|
|
341
|
+
Each wiki vault has four layers with clear ownership:
|
|
313
342
|
|
|
314
343
|
```
|
|
315
344
|
.llm-wiki/raw/sources/SRC-*/ # Immutable source packets (extension-owned)
|
package/docs/architecture.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Layered Vault Architecture
|
|
4
|
+
|
|
5
|
+
pi-llm-wiki supports multiple vault layers that are searched together:
|
|
6
|
+
|
|
7
|
+
| Layer | Location | Resolution | Searched by recall |
|
|
8
|
+
|-------|----------|------------|-------------------|
|
|
9
|
+
| **Personal** | `~/.llm-wiki/` | Fallback when no project wiki found | ✅ Always |
|
|
10
|
+
| **Project** | `{project}/.llm-wiki/` | Walk up from cwd | ✅ When present |
|
|
11
|
+
|
|
12
|
+
### Resolution Order
|
|
13
|
+
|
|
14
|
+
1. Check current directory for `.llm-wiki/` → use as project wiki
|
|
15
|
+
2. Walk up parent directories looking for `.llm-wiki/` → use as project wiki
|
|
16
|
+
3. Check `WIKI_HOME` env var → use as personal wiki
|
|
17
|
+
4. Fall back to `~/.llm-wiki/` → create if doesn't exist
|
|
18
|
+
|
|
19
|
+
This means a project wiki is always preferred when you're inside a project that has one, but your personal wiki is always available as the fallback.
|
|
20
|
+
|
|
21
|
+
### Dual-Vault Recall
|
|
22
|
+
|
|
23
|
+
`wiki_recall` uses `searchWikiLayered()` which:
|
|
24
|
+
1. Searches the **project vault** (if one exists in cwd)
|
|
25
|
+
2. Searches the **personal vault** (`~/.llm-wiki/` or `WIKI_HOME`)
|
|
26
|
+
3. Deduplicates results by page ID (project takes priority on duplicates)
|
|
27
|
+
4. Tags personal results with "📓 personal" label
|
|
28
|
+
5. Merges results: personal first, then project
|
|
29
|
+
|
|
30
|
+
Results are injected into the context with vault source tags so the model can distinguish between personal and project knowledge.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Four-Layer Page Model (within each vault)
|
|
4
35
|
|
|
5
36
|
```
|
|
6
37
|
WIKI_ROOT/
|
package/docs/commands.md
CHANGED
|
@@ -22,7 +22,7 @@ The extension registers 12 tools the LLM can call directly:
|
|
|
22
22
|
| --------------------- | ------------------------------------------- |
|
|
23
23
|
| `wiki_bootstrap` | Initialize a new vault |
|
|
24
24
|
| `wiki_capture_source` | Capture URL/file/text into immutable packet |
|
|
25
|
-
| `wiki_recall` |
|
|
25
|
+
| `wiki_recall` | Search personal + project wikis for task-relevant pages (layered) |
|
|
26
26
|
| `wiki_retro` | Save atomic insights from completed tasks |
|
|
27
27
|
| `wiki_ingest` | Get batch of uningested sources |
|
|
28
28
|
| `wiki_ensure_page` | Create canonical page from template |
|
package/docs/configuration.md
CHANGED
|
@@ -6,6 +6,8 @@ Wiki configuration lives in `.llm-wiki/config.json`.
|
|
|
6
6
|
|
|
7
7
|
### Personal
|
|
8
8
|
|
|
9
|
+
The personal vault lives at `~/.llm-wiki/` (or `$WIKI_HOME`) and is always available as a fallback when no project wiki exists. It accumulates knowledge across all your sessions.
|
|
10
|
+
|
|
9
11
|
- Extra folders: `wiki/journal/`, `wiki/goals/`
|
|
10
12
|
- Track: learning, books, health, reflections
|
|
11
13
|
|
|
@@ -25,9 +27,19 @@ Wiki configuration lives in `.llm-wiki/config.json`.
|
|
|
25
27
|
|
|
26
28
|
## Environment Variables
|
|
27
29
|
|
|
28
|
-
| Variable | Default
|
|
29
|
-
| ----------------------------- |
|
|
30
|
-
| `
|
|
30
|
+
| Variable | Default | Description |
|
|
31
|
+
| ----------------------------- | ----------- | ----------------------------------------------- |
|
|
32
|
+
| `WIKI_HOME` | `~/.llm-wiki` | Override the personal wiki vault location |
|
|
33
|
+
| `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
|
|
34
|
+
|
|
35
|
+
## Vault Resolution
|
|
36
|
+
|
|
37
|
+
The vault root is resolved in this priority order:
|
|
38
|
+
|
|
39
|
+
1. **Project vault**: walk up from current directory looking for `.llm-wiki/`
|
|
40
|
+
2. **Personal vault**: fall back to `$WIKI_HOME` or `~/.llm-wiki/`
|
|
41
|
+
|
|
42
|
+
This means when you're in a project with its own `.llm-wiki/`, that project wiki is active. When you're outside any project wiki, your personal `~/.llm-wiki/` takes over automatically.
|
|
31
43
|
|
|
32
44
|
## Page Frontmatter
|
|
33
45
|
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { installGuardrails } from "./lib/guardrails.js";
|
|
5
|
-
import { formatRecallContext, registerWikiRecall,
|
|
5
|
+
import { formatRecallContext, registerWikiRecall, searchWikiLayered } from "./lib/recall.js";
|
|
6
6
|
import { registerWikiRetro } from "./lib/retro.js";
|
|
7
7
|
import {
|
|
8
8
|
registerWikiBootstrap,
|
|
@@ -27,17 +27,19 @@ import {
|
|
|
27
27
|
/**
|
|
28
28
|
* @zosmaai/pi-llm-wiki — LLM Wiki extension for Pi
|
|
29
29
|
*
|
|
30
|
-
* Registers
|
|
31
|
-
*
|
|
30
|
+
* Registers 12 custom tools and installs guardrails:
|
|
31
|
+
* - wiki_recall (layered: personal + project vaults)
|
|
32
|
+
* - wiki_retro (lightweight: single markdown file)
|
|
33
|
+
* - wiki_capture_source (full 4-layer pipeline)
|
|
32
34
|
*
|
|
33
35
|
* Guardrails:
|
|
34
36
|
* - Blocks direct edits to raw/** and meta/**
|
|
35
37
|
* - Auto-rebuilds metadata after wiki/** edits
|
|
36
38
|
*
|
|
37
|
-
*
|
|
38
|
-
* - before_agent_start hook searches
|
|
39
|
-
* - Injects matching knowledge as system context
|
|
40
|
-
* - wiki_recall tool available for explicit
|
|
39
|
+
* Layered recall:
|
|
40
|
+
* - before_agent_start hook searches personal + project vaults
|
|
41
|
+
* - Injects matching knowledge as system context with vault labels
|
|
42
|
+
* - wiki_recall tool available for explicit task-specific searches
|
|
41
43
|
*/
|
|
42
44
|
|
|
43
45
|
export default function (pi: ExtensionAPI) {
|
|
@@ -95,14 +97,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
95
97
|
return;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (12 tools,
|
|
100
|
+
ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (12 tools, layered recall active)");
|
|
99
101
|
});
|
|
100
102
|
|
|
101
|
-
// ───
|
|
103
|
+
// ─── Layered recall + topic inference hook ──────────
|
|
102
104
|
// Before each agent turn:
|
|
103
105
|
// 1. If wiki was just auto-created, inject a directive to infer topic/mode
|
|
104
106
|
// from the user's first prompt and update config via wiki_bootstrap.
|
|
105
|
-
// 2. Search
|
|
107
|
+
// 2. Search both personal + project vaults for relevant pages.
|
|
106
108
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
107
109
|
const paths = resolveVaultPaths(process.cwd());
|
|
108
110
|
if (!existsSync(join(paths.dotWiki, "config.json"))) {
|
|
@@ -145,9 +147,9 @@ ${projectHints}
|
|
|
145
147
|
Then call wiki_bootstrap with the inferred topic and mode to finalize the setup. This is a one-time step.`;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
//
|
|
150
|
+
// Layered recall: search personal + project vaults for relevant pages
|
|
149
151
|
if (prompt.trim()) {
|
|
150
|
-
const results =
|
|
152
|
+
const results = searchWikiLayered(paths, prompt);
|
|
151
153
|
if (results.length > 0) {
|
|
152
154
|
const recallContext = formatRecallContext(results);
|
|
153
155
|
if (recallContext) {
|
|
@@ -156,6 +158,11 @@ Then call wiki_bootstrap with the inferred topic and mode to finalize the setup.
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
// Always inject a visible wiki status footer, even when empty
|
|
162
|
+
// This ensures the model knows the wiki is active and can use it
|
|
163
|
+
injectedContext +=
|
|
164
|
+
"\n\n<wiki_status>LLM Wiki active — use wiki_recall for deeper search, wiki_retro to save new knowledge.</wiki_status>";
|
|
165
|
+
|
|
159
166
|
if (injectedContext === event.systemPrompt) return;
|
|
160
167
|
return { systemPrompt: injectedContext };
|
|
161
168
|
});
|
|
@@ -3,7 +3,13 @@ import { join } from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import type { Registry } from "./metadata.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type VaultPaths,
|
|
8
|
+
getPersonalWikiPaths,
|
|
9
|
+
isPersonalVault,
|
|
10
|
+
readJson,
|
|
11
|
+
resolveVaultPaths,
|
|
12
|
+
} from "./utils.js";
|
|
7
13
|
|
|
8
14
|
// ─── Public API ────────────────────────────────────────
|
|
9
15
|
|
|
@@ -18,10 +24,12 @@ export interface RecallResult {
|
|
|
18
24
|
preview: string;
|
|
19
25
|
/** Relative path from wiki root */
|
|
20
26
|
path: string;
|
|
27
|
+
/** Vault source label for dual-vault results */
|
|
28
|
+
vaultLabel?: string;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
|
-
* Search
|
|
32
|
+
* Search a single vault's registry for pages matching a query.
|
|
25
33
|
* Returns up to `maxResults` matches, each with a content preview.
|
|
26
34
|
*/
|
|
27
35
|
export function searchWiki(paths: VaultPaths, query: string, maxResults = 5): RecallResult[] {
|
|
@@ -88,21 +96,68 @@ export function searchWiki(paths: VaultPaths, query: string, maxResults = 5): Re
|
|
|
88
96
|
});
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Format recall results as a compact system-prompt section.
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* Search both project/primary vault and personal vault, merging results.
|
|
104
|
+
* Personal results are appended after primary results, deduplicated by page ID.
|
|
105
|
+
*/
|
|
106
|
+
export function searchWikiLayered(
|
|
107
|
+
primaryPaths: VaultPaths,
|
|
108
|
+
query: string,
|
|
109
|
+
maxResults = 5,
|
|
110
|
+
): RecallResult[] {
|
|
111
|
+
// Search primary vault
|
|
112
|
+
const primaryResults = searchWiki(primaryPaths, query, maxResults);
|
|
113
|
+
|
|
114
|
+
// If primary is already the personal vault, no layered search needed
|
|
115
|
+
if (isPersonalVault(primaryPaths)) return primaryResults;
|
|
116
|
+
|
|
117
|
+
// Search personal vault as secondary layer
|
|
118
|
+
const personalPaths = getPersonalWikiPaths();
|
|
119
|
+
if (!existsSync(join(personalPaths.dotWiki, "config.json"))) return primaryResults;
|
|
120
|
+
|
|
121
|
+
const personalResults = searchWiki(personalPaths, query, maxResults);
|
|
122
|
+
|
|
123
|
+
// Merge: personal results first (they're the user's accumulated knowledge),
|
|
124
|
+
// then primary results (project-specific). Deduplicate by page ID.
|
|
125
|
+
const seen = new Set<string>();
|
|
126
|
+
const merged: RecallResult[] = [];
|
|
127
|
+
|
|
128
|
+
for (const r of [...personalResults, ...primaryResults]) {
|
|
129
|
+
if (seen.has(r.id)) continue;
|
|
130
|
+
seen.add(r.id);
|
|
131
|
+
// If it's from personal vault, tag it
|
|
132
|
+
if (personalResults.includes(r)) {
|
|
133
|
+
merged.push({ ...r, vaultLabel: "📓 personal" });
|
|
134
|
+
} else {
|
|
135
|
+
merged.push(r);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return merged.slice(0, maxResults);
|
|
140
|
+
}
|
|
141
|
+
|
|
91
142
|
/**
|
|
92
143
|
* Format recall results as a compact system-prompt section.
|
|
93
144
|
*/
|
|
94
145
|
export function formatRecallContext(results: RecallResult[]): string {
|
|
95
146
|
if (results.length === 0) return "";
|
|
96
147
|
|
|
148
|
+
const hasLayered = results.some((r) => r.vaultLabel);
|
|
149
|
+
const label = hasLayered ? " (personal + project)" : "";
|
|
150
|
+
|
|
97
151
|
const lines: string[] = [
|
|
98
152
|
"## Relevant Wiki Knowledge",
|
|
99
153
|
"",
|
|
100
|
-
`_${results.length} page(s) matched your query
|
|
154
|
+
`_${results.length} page(s) matched your query${label}._`,
|
|
101
155
|
"",
|
|
102
156
|
];
|
|
103
157
|
|
|
104
158
|
for (const r of results) {
|
|
105
|
-
|
|
159
|
+
const vaultTag = r.vaultLabel ? ` ${r.vaultLabel}` : "";
|
|
160
|
+
lines.push(`- **[[${r.id}]]** — *${r.type}* — ${r.title}${vaultTag}`);
|
|
106
161
|
if (r.preview) {
|
|
107
162
|
// Truncate preview to one line
|
|
108
163
|
const preview = r.preview.length > 120 ? `${r.preview.slice(0, 120)}…` : r.preview;
|
|
@@ -164,30 +219,34 @@ export function registerWikiRecall(pi: ExtensionAPI): void {
|
|
|
164
219
|
}
|
|
165
220
|
|
|
166
221
|
const maxResults = Math.min(params.max_results ?? 5, 10);
|
|
167
|
-
|
|
222
|
+
// Use layered search: personal vault + project vault
|
|
223
|
+
const results = searchWikiLayered(paths, params.query, maxResults);
|
|
168
224
|
|
|
169
225
|
if (results.length === 0) {
|
|
170
226
|
return {
|
|
171
227
|
content: [
|
|
172
228
|
{
|
|
173
229
|
type: "text",
|
|
174
|
-
text: `No wiki pages found matching "${params.query}".
|
|
230
|
+
text: `No wiki pages found matching "${params.query}". The wiki is empty — use wiki_retro to start building knowledge.`,
|
|
175
231
|
},
|
|
176
232
|
],
|
|
177
233
|
details: { query: params.query, matches: [] } as Record<string, unknown>,
|
|
178
234
|
};
|
|
179
235
|
}
|
|
180
236
|
|
|
237
|
+
const hasPersonal = results.some((r) => r.vaultLabel);
|
|
238
|
+
const layerTag = hasPersonal ? " (personal + project)" : "";
|
|
239
|
+
|
|
181
240
|
return {
|
|
182
241
|
content: [
|
|
183
242
|
{
|
|
184
243
|
type: "text",
|
|
185
244
|
text: [
|
|
186
|
-
`🧠 **${results.length} wiki page(s) relevant** to "${params.query}":`,
|
|
245
|
+
`🧠 **${results.length} wiki page(s) relevant** to "${params.query}"${layerTag}:`,
|
|
187
246
|
"",
|
|
188
247
|
...results.map(
|
|
189
248
|
(r) =>
|
|
190
|
-
`- [[${r.id}]] — *${r.type}* — ${r.title}${
|
|
249
|
+
`- ${r.vaultLabel || "📁"} [[${r.id}]] — *${r.type}* — ${r.title}${
|
|
191
250
|
r.preview ? `\n > ${r.preview.slice(0, 150)}` : ""
|
|
192
251
|
}`,
|
|
193
252
|
),
|
|
@@ -3,19 +3,24 @@ import { join } from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { appendEvent, rebuildMetadataLight } from "./metadata.js";
|
|
6
|
-
import { type VaultPaths, fmtDate,
|
|
6
|
+
import { type VaultPaths, fmtDate, resolveVaultPaths } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
// ─── Public API ────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
export interface RetroResult {
|
|
11
|
-
|
|
12
|
-
packetPath: string;
|
|
11
|
+
slug: string;
|
|
13
12
|
sourcePagePath: string;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
* Save an atomic insight into the wiki as a
|
|
18
|
-
*
|
|
16
|
+
* Save an atomic insight into the wiki as a single markdown file.
|
|
17
|
+
*
|
|
18
|
+
* Unlike wiki_capture_source (which creates a full source packet with
|
|
19
|
+
* manifest.json, extracted.md, and attachments), this is a lightweight
|
|
20
|
+
* path for quick knowledge capture — one file, one call.
|
|
21
|
+
*
|
|
22
|
+
* The 4-layer pipeline (raw → source pages → canonical pages → metadata)
|
|
23
|
+
* is still available via wiki_capture_source → wiki_ingest for deep research.
|
|
19
24
|
*/
|
|
20
25
|
export function saveInsight(
|
|
21
26
|
paths: VaultPaths,
|
|
@@ -24,92 +29,54 @@ export function saveInsight(
|
|
|
24
29
|
body: string,
|
|
25
30
|
category?: string,
|
|
26
31
|
): RetroResult {
|
|
27
|
-
const sourceId = nextSourceId(paths);
|
|
28
|
-
const packetPath = join(paths.rawSources, sourceId);
|
|
29
|
-
mkdirSync(packetPath, { recursive: true });
|
|
30
|
-
mkdirSync(join(packetPath, "attachments"), { recursive: true });
|
|
31
|
-
|
|
32
32
|
const today = fmtDate();
|
|
33
33
|
|
|
34
|
-
// Write
|
|
35
|
-
const manifest = {
|
|
36
|
-
id: sourceId,
|
|
37
|
-
title,
|
|
38
|
-
slug,
|
|
39
|
-
category: category || "uncategorized",
|
|
40
|
-
captured: today,
|
|
41
|
-
format: "insight",
|
|
42
|
-
packet_version: "1.0",
|
|
43
|
-
};
|
|
44
|
-
writeFileSync(
|
|
45
|
-
join(packetPath, "manifest.json"),
|
|
46
|
-
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
47
|
-
"utf-8",
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// Write extracted text (the insight body in markdown)
|
|
51
|
-
const extracted = [
|
|
52
|
-
`# ${title}`,
|
|
53
|
-
"",
|
|
54
|
-
body,
|
|
55
|
-
"",
|
|
56
|
-
"---",
|
|
57
|
-
`*Captured: ${today}*`,
|
|
58
|
-
category ? `*Category: ${category}*` : "",
|
|
59
|
-
]
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.join("\n");
|
|
62
|
-
writeFileSync(join(packetPath, "extracted.md"), extracted, "utf-8");
|
|
63
|
-
|
|
64
|
-
// Create source page
|
|
34
|
+
// Write a single markdown file to wiki/sources/{slug}.md
|
|
65
35
|
const sourcePageDir = join(paths.wiki, "sources");
|
|
66
36
|
mkdirSync(sourcePageDir, { recursive: true });
|
|
67
|
-
const sourcePagePath = join(sourcePageDir, `${
|
|
37
|
+
const sourcePagePath = join(sourcePageDir, `${slug}.md`);
|
|
68
38
|
|
|
69
|
-
const
|
|
70
|
-
const sourcePageContent = [
|
|
39
|
+
const pageContent = [
|
|
71
40
|
"---",
|
|
72
41
|
"type: source",
|
|
73
42
|
`title: "${title}"`,
|
|
74
|
-
`
|
|
43
|
+
`slug: ${slug}`,
|
|
75
44
|
"status: insight",
|
|
76
45
|
`created: ${today}`,
|
|
77
46
|
`updated: ${today}`,
|
|
78
|
-
|
|
47
|
+
category ? `category: ${category}` : "",
|
|
79
48
|
"---",
|
|
80
49
|
"",
|
|
81
50
|
`# ${title}`,
|
|
82
51
|
"",
|
|
83
52
|
body,
|
|
84
53
|
"",
|
|
85
|
-
|
|
54
|
+
category ? `*Category: ${category}*` : "",
|
|
86
55
|
"",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
category ? `- **Category:** ${category}` : "",
|
|
56
|
+
"---",
|
|
57
|
+
`*Captured: ${today}*`,
|
|
90
58
|
"",
|
|
91
59
|
"## Related",
|
|
92
60
|
"",
|
|
93
|
-
"
|
|
61
|
+
"_Add links to related pages._",
|
|
94
62
|
"",
|
|
95
63
|
]
|
|
96
64
|
.filter((l) => l !== "")
|
|
97
65
|
.join("\n");
|
|
98
|
-
writeFileSync(sourcePagePath,
|
|
66
|
+
writeFileSync(sourcePagePath, pageContent, "utf-8");
|
|
99
67
|
|
|
100
68
|
// Log event
|
|
101
69
|
appendEvent(paths, {
|
|
102
70
|
kind: "retro",
|
|
103
|
-
source_id: sourceId,
|
|
104
|
-
title,
|
|
105
71
|
slug,
|
|
72
|
+
title,
|
|
106
73
|
category: category || "uncategorized",
|
|
107
74
|
});
|
|
108
75
|
|
|
109
|
-
// Rebuild metadata
|
|
76
|
+
// Rebuild metadata so the insight is immediately searchable
|
|
110
77
|
rebuildMetadataLight(paths);
|
|
111
78
|
|
|
112
|
-
return {
|
|
79
|
+
return { slug, sourcePagePath };
|
|
113
80
|
}
|
|
114
81
|
|
|
115
82
|
// ─── Tool Registration ──────────────────────────────────
|
|
@@ -176,8 +143,6 @@ export function registerWikiRetro(pi: ExtensionAPI): void {
|
|
|
176
143
|
text: [
|
|
177
144
|
`🧠 **Insight saved**: ${params.title}`,
|
|
178
145
|
"",
|
|
179
|
-
`- Source: \`${result.sourceId}\``,
|
|
180
|
-
`- Packet: \`${result.packetPath}\``,
|
|
181
146
|
`- Page: \`${result.sourcePagePath}\``,
|
|
182
147
|
"",
|
|
183
148
|
"This insight will be auto-surfaced by wiki_recall in future sessions.",
|
|
@@ -185,7 +150,6 @@ export function registerWikiRetro(pi: ExtensionAPI): void {
|
|
|
185
150
|
},
|
|
186
151
|
],
|
|
187
152
|
details: {
|
|
188
|
-
sourceId: result.sourceId,
|
|
189
153
|
slug: params.slug,
|
|
190
154
|
title: params.title,
|
|
191
155
|
category: params.category || null,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
5
|
|
|
@@ -32,7 +33,35 @@ export function detectVaultFormat(dir: string): VaultFormat {
|
|
|
32
33
|
return "none";
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
+
/** Get the personal wiki root directory (~/.llm-wiki/). */
|
|
37
|
+
export function getPersonalWikiRoot(): string {
|
|
38
|
+
const envWiki = process.env.WIKI_HOME;
|
|
39
|
+
if (envWiki) return envWiki;
|
|
40
|
+
return join(homedir(), ".llm-wiki");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Get VaultPaths for the personal wiki. */
|
|
44
|
+
export function getPersonalWikiPaths(): VaultPaths {
|
|
45
|
+
return getVaultPaths(getPersonalWikiRoot());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a vault is the personal wiki location.
|
|
50
|
+
* Used in layered recall to avoid double-counting.
|
|
51
|
+
*/
|
|
52
|
+
export function isPersonalVault(paths: VaultPaths): boolean {
|
|
53
|
+
return paths.root === getPersonalWikiRoot();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve vault root from cwd with personal fallback.
|
|
58
|
+
*
|
|
59
|
+
* Priority:
|
|
60
|
+
* 1. cwd has .llm-wiki/ → project wiki (explicit)
|
|
61
|
+
* 2. Walk up from cwd → parent project wiki
|
|
62
|
+
* 3. ~/.llm-wiki/ exists → personal wiki
|
|
63
|
+
* 4. Fallback: ~/.llm-wiki/ (create personal wiki)
|
|
64
|
+
*/
|
|
36
65
|
export function resolveVaultRoot(cwd: string): string {
|
|
37
66
|
// Check for any vault format at cwd
|
|
38
67
|
if (detectVaultFormat(cwd) !== "none") return cwd;
|
|
@@ -44,8 +73,12 @@ export function resolveVaultRoot(cwd: string): string {
|
|
|
44
73
|
if (detectVaultFormat(dir) !== "none") return dir;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
|
-
//
|
|
48
|
-
|
|
76
|
+
// Check personal wiki at ~/.llm-wiki/
|
|
77
|
+
const personalRoot = getPersonalWikiRoot();
|
|
78
|
+
if (detectVaultFormat(personalRoot) !== "none") return personalRoot;
|
|
79
|
+
|
|
80
|
+
// Fallback: personal wiki
|
|
81
|
+
return personalRoot;
|
|
49
82
|
}
|
|
50
83
|
|
|
51
84
|
/** Get all vault paths for the new (.llm-wiki) layout. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|
package/prompts/wiki-retro.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Save an atomic insight from the current task into the wiki. Creates a
|
|
2
|
+
description: Save an atomic insight from the current task into the wiki. Creates a single markdown file that layered recall surfaces in future sessions.
|
|
3
3
|
argument-hint: "<title> [--category <category>]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
@@ -9,7 +9,7 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Save an atomic insight from a completed task into the wiki.
|
|
11
11
|
|
|
12
|
-
Captures what you learned as
|
|
12
|
+
Captures what you learned as a single markdown file so that layered recall surfaces it in future sessions.
|
|
13
13
|
|
|
14
14
|
## User Arguments
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
25
25
|
- `title`: short descriptive phrase, ≤60 chars, noun phrase not a sentence
|
|
26
26
|
- `body`: markdown explanation with `[[wikilinks]]` to related wiki pages
|
|
27
27
|
- `category`: optional (frontend, architecture, devops, bugfix, design, etc.)
|
|
28
|
-
3. Confirm the insight was saved and will be
|
|
28
|
+
3. Confirm the insight was saved and will be surfaced by layered recall in future sessions
|
|
29
29
|
4. If the insight relates to existing wiki pages, update those pages with cross-references
|
|
30
30
|
|
|
31
31
|
**Rules:**
|
package/skills/llm-wiki/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: llm-wiki
|
|
3
3
|
description: Build and maintain a persistent, interlinked Obsidian-compatible markdown wiki using Karpathy's LLM Wiki pattern. Extension-backed with auto-generated metadata, guardrails, and 12 custom tools.
|
|
4
|
-
whenToUse:
|
|
4
|
+
whenToUse: Call wiki_recall at task start to find relevant wiki pages. Call wiki_retro at task end to save new insights. The extension injects a brief status line, but explicit wiki_recall calls with task-specific terms get better results.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# LLM Wiki for Pi
|
|
@@ -56,25 +56,24 @@ WIKI_ROOT/
|
|
|
56
56
|
| Find orphans | Shell `grep` scans | Instant from `backlinks.json` |
|
|
57
57
|
| Block raw edits | Skill says "don't" | Extension **enforces** immutability |
|
|
58
58
|
| Create source page | 8 tool calls | `wiki_capture_source` + LLM synthesis |
|
|
59
|
-
| **Recall wiki knowledge** | Never happens | **
|
|
59
|
+
| **Recall wiki knowledge** | Never happens | **Layered search before every turn (personal + project)** |
|
|
60
60
|
| **Save task insights** | Manual capture | `wiki_retro` — one tool call |
|
|
61
61
|
|
|
62
|
-
## 🔄
|
|
62
|
+
## 🔄 Wiki Usage
|
|
63
63
|
|
|
64
|
-
### At
|
|
64
|
+
### At Start — Call wiki_recall
|
|
65
65
|
|
|
66
|
-
**
|
|
66
|
+
**Call `wiki_recall` at the START of every task** to find relevant wiki pages:
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
3. Injects matching page titles + summaries into context
|
|
72
|
-
4. You see this as "Relevant Wiki Knowledge" in your system prompt
|
|
68
|
+
```
|
|
69
|
+
wiki_recall(query="key terms from the user's request", max_results=5)
|
|
70
|
+
```
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
You don't need to remember to search — relevant knowledge is surfaced automatically.
|
|
72
|
+
This searches both your **personal wiki** (`~/.llm-wiki/`) and the **project wiki** (`.llm-wiki/` in the current directory), merging results.
|
|
76
73
|
|
|
77
|
-
|
|
74
|
+
The extension also briefly searches automatically, but explicit calls with task-specific terms get better results.
|
|
75
|
+
|
|
76
|
+
### At End — Save Insights with wiki_retro
|
|
78
77
|
|
|
79
78
|
After completing any meaningful task, call `wiki_retro` to save key insights:
|
|
80
79
|
- Non-obvious bug fixes or workarounds
|
|
@@ -84,29 +83,26 @@ After completing any meaningful task, call `wiki_retro` to save key insights:
|
|
|
84
83
|
|
|
85
84
|
**Do not wait for the user to ask.** Save insights proactively — one atomic insight per call.
|
|
86
85
|
|
|
87
|
-
```
|
|
86
|
+
```
|
|
87
|
+
wiki_retro(slug="kebab-case-slug", title="Brief descriptive title", body="Insight in your own words with [[wikilinks]]")
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
###
|
|
90
|
+
### Deeper Searches
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
For thorough research, also use `wiki_search` to browse the full registry:
|
|
93
93
|
|
|
94
94
|
```
|
|
95
|
-
|
|
95
|
+
wiki_search(query="broad topic")
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
### Auto-Bootstrap (One-Time)
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
When you start in a directory without a wiki, the extension silently creates `.llm-wiki/` with placeholder config. On your first turn, it injects a directive asking you to:
|
|
105
|
-
1. Analyze the user's prompt and project context
|
|
106
|
-
2. Infer a topic (e.g. "React app", "startup finances")
|
|
107
|
-
3. Call `wiki_bootstrap(topic="...", mode="personal|company")` to finalize setup
|
|
100
|
+
The extension creates the wiki vault automatically on startup. On the first turn, it injects a directive asking you to infer topic and mode, then call:
|
|
101
|
+
```
|
|
102
|
+
wiki_bootstrap(topic="...", mode="personal|company")
|
|
103
|
+
```
|
|
108
104
|
|
|
109
|
-
This is a one-time step
|
|
105
|
+
This is a one-time step.
|
|
110
106
|
|
|
111
107
|
## Available Tools
|
|
112
108
|
|
|
@@ -114,7 +110,7 @@ Use these directly — they handle scaffolding, bookkeeping, recall, and capture
|
|
|
114
110
|
|
|
115
111
|
- `wiki_bootstrap` — Initialize a new vault
|
|
116
112
|
- `wiki_capture_source` — Capture URL/file/text into immutable packet + skeleton page
|
|
117
|
-
- `wiki_recall` —
|
|
113
|
+
- `wiki_recall` — Search both personal + project wikis for task-relevant pages
|
|
118
114
|
- `wiki_retro` — Save an atomic insight from a completed task into the wiki
|
|
119
115
|
- `wiki_ingest` — Get batch of uningested sources with extracted text
|
|
120
116
|
- `wiki_ensure_page` — Create entity/concept/synthesis/analysis page from template
|
|
@@ -139,19 +135,20 @@ Use these directly — they handle scaffolding, bookkeeping, recall, and capture
|
|
|
139
135
|
|
|
140
136
|
### Query → Answer → File
|
|
141
137
|
|
|
142
|
-
1. **
|
|
143
|
-
2.
|
|
144
|
-
3.
|
|
145
|
-
4.
|
|
146
|
-
5.
|
|
138
|
+
1. **Layered recall**: Extension searches personal + project vaults, injects matching pages with vault labels
|
|
139
|
+
2. For better results: call `wiki_recall` explicitly with task-specific terms
|
|
140
|
+
3. Read those pages
|
|
141
|
+
4. Synthesize answer with `[[wikilink]]` citations
|
|
142
|
+
5. If novel: create analysis page via `wiki_ensure_page(type="analysis")`
|
|
143
|
+
6. Extension auto-updates metadata
|
|
147
144
|
|
|
148
145
|
### Task → Capture → Retro
|
|
149
146
|
|
|
150
147
|
1. Complete a meaningful task
|
|
151
148
|
2. Call `wiki_retro` to save key insights
|
|
152
|
-
3. The insight is
|
|
149
|
+
3. The insight is saved as a single markdown file
|
|
153
150
|
4. Extension auto-updates metadata
|
|
154
|
-
5. Next time,
|
|
151
|
+
5. Next time, layered recall surfaces your saved insight
|
|
155
152
|
|
|
156
153
|
## Page Conventions
|
|
157
154
|
|