@zosmaai/pi-llm-wiki 0.6.6 → 0.7.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/README.md CHANGED
@@ -20,6 +20,8 @@ Turn raw sources (URLs, PDFs, markdown, JSON, XML) into a durable, interlinked,
20
20
  pi install npm:@zosmaai/pi-llm-wiki
21
21
  ```
22
22
 
23
+ The extension will proactively suggest creating a wiki on your first session. Alternatively:
24
+
23
25
  ```
24
26
  /wiki-init "AI Engineering"
25
27
  /wiki-ingest
@@ -47,14 +49,16 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
47
49
 
48
50
  | Capability | Description |
49
51
  |------------|-------------|
52
+ | 🏠 **Personal fallback** | Always-on `~/.llm-wiki/` vault — knowledge compounds across projects even when no project wiki exists |
50
53
  | 🔗 **Immutable source capture** | URLs, local files (PDF/md/txt/html/XML/JSON), or pasted text → structured source packets |
51
54
  | 🧠 **Automated ingestion** | `wiki_ingest` batch-processes sources into concept, entity, synthesis & analysis pages |
52
55
  | 🔍 **Full-text search** | Generated registry with keyword lookup across all pages and sources |
53
56
  | 🩺 **Mechanical linting** | Orphans, broken links, duplicate aliases, coverage gaps, stale captures |
54
57
  | 📊 **Dashboard** | `wiki_status` — counts, source states, recent activity |
55
58
  | 🤖 **Auto-update watch** | `wiki_watch` — schedule periodic discovery + ingest |
56
- | 🧠 **Auto-recall** | Wiki searched automatically before every turnrelevant pages injected into context |
57
- | 💾 **Auto-capture** | `wiki_retro` save atomic insights from completed tasks with one call |
59
+ | 🧠 **Layered recall** | Searches both personal (`~/.llm-wiki/`) and project (`.llm-wiki/`) vaults personal knowledge follows you everywhere |
60
+ | 📝 **Auto-bootstrap** | Extension suggests creating a wiki when none exists in the current directory |
61
+ | 💾 **Lightweight capture** | `wiki_retro` — save atomic insights as a single markdown file; full 4-layer pipeline also available via `wiki_capture_source` |
58
62
  | 🌐 **MCP Server** | Use with Claude Code, Cursor, Windsurf via stdio MCP transport |
59
63
  | 📝 **Obsidian-friendly** | Folder-qualified wikilinks, stable source-ID citations, compatible vault |
60
64
  | 🛡️ **Guardrails** | Blocks direct edits to raw sources and generated metadata |
@@ -69,7 +73,7 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
69
73
  |------|-------------|
70
74
  | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
71
75
  | `wiki_capture_source` | Capture a URL, local file, or pasted text into an immutable source packet |
72
- | `wiki_recall` | 🔄 **Auto-called at turn start**search wiki for task-relevant pages |
76
+ | `wiki_recall` | Search wiki for task-relevant pagessearches both personal (`~/.llm-wiki/`) and project (`.llm-wiki/`) vaults, deduplicated |
73
77
  | `wiki_retro` | Save atomic insights from completed tasks into the wiki |
74
78
  | `wiki_ingest` | Process uningested source packets into wiki pages (batch) |
75
79
  | `wiki_ensure_page` | Resolve or safely create entity / concept / synthesis / analysis pages |
@@ -96,6 +100,28 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
96
100
 
97
101
  ---
98
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
+
99
125
  ## Quick Start (Detailed)
100
126
 
101
127
  ### 1) Create a new wiki
@@ -306,7 +332,13 @@ The bundled `llm-wiki` skill teaches the model to:
306
332
 
307
333
  ## Architecture
308
334
 
309
- Four layers with clear ownership:
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:
310
342
 
311
343
  ```
312
344
  .llm-wiki/raw/sources/SRC-*/ # Immutable source packets (extension-owned)
@@ -1,6 +1,37 @@
1
1
  # Architecture
2
2
 
3
- ## Four Layers
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` | 🔄 Auto-called at turn start search wiki |
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 |
@@ -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 | Description |
29
- | ----------------------------- | ------- | ----------------------------------------------- |
30
- | `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
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
 
@@ -1,8 +1,8 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
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, searchWiki } from "./lib/recall.js";
5
+ import { formatRecallContext, registerWikiRecall, searchWikiLayered } from "./lib/recall.js";
6
6
  import { registerWikiRetro } from "./lib/retro.js";
7
7
  import {
8
8
  registerWikiBootstrap,
@@ -16,27 +16,32 @@ import {
16
16
  registerWikiStatus,
17
17
  registerWikiWatch,
18
18
  } from "./lib/tools.js";
19
- import { resolveVaultPaths } from "./lib/utils.js";
19
+ import {
20
+ ensureVaultStructure,
21
+ fmtDate,
22
+ getVaultPaths,
23
+ resolveVaultPaths,
24
+ writeJson,
25
+ } from "./lib/utils.js";
20
26
 
21
27
  /**
22
28
  * @zosmaai/pi-llm-wiki — LLM Wiki extension for Pi
23
29
  *
24
- * Registers 11 custom tools and installs guardrails:
25
- * All 10 original tools + wiki_recall (auto-recall at session start)
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)
26
34
  *
27
35
  * Guardrails:
28
36
  * - Blocks direct edits to raw/** and meta/**
29
37
  * - Auto-rebuilds metadata after wiki/** edits
30
38
  *
31
- * Auto-recall:
32
- * - before_agent_start hook searches wiki for pages relevant to user prompt
33
- * - Injects matching knowledge as system context
34
- * - wiki_recall tool available for explicit deep searches
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
35
43
  */
36
44
 
37
- // Track whether we already suggested bootstrapping this session
38
- let bootstrapSuggested = false;
39
-
40
45
  export default function (pi: ExtensionAPI) {
41
46
  registerWikiBootstrap(pi);
42
47
  registerWikiCaptureSource(pi);
@@ -53,43 +58,112 @@ export default function (pi: ExtensionAPI) {
53
58
 
54
59
  installGuardrails(pi);
55
60
 
61
+ // Track if wiki was just auto-created and needs topic inference
62
+ let needsTopicInference = false;
63
+
56
64
  pi.on("session_start", async (_event, ctx) => {
57
- bootstrapSuggested = false;
58
65
  const paths = resolveVaultPaths(process.cwd());
59
66
  if (!existsSync(join(paths.dotWiki, "config.json"))) {
60
- ctx.ui.setStatus("llm-wiki", "📝 No wiki — call wiki_bootstrap to enable");
67
+ // Silently create the wiki vault no UI prompts
68
+ // Topic/mode will be inferred from user's first prompt via before_agent_start
69
+ const root = paths.root;
70
+ const vaultPaths = getVaultPaths(root);
71
+ ensureVaultStructure(vaultPaths);
72
+
73
+ writeJson(join(vaultPaths.dotWiki, "config.json"), {
74
+ name: "pending",
75
+ mode: "personal",
76
+ topic: "pending",
77
+ created: fmtDate(),
78
+ version: "1.0",
79
+ });
80
+
81
+ const schema = [
82
+ "# LLM Wiki Schema",
83
+ "",
84
+ "## Ownership Rules",
85
+ "",
86
+ "| Path | Owner | Rule |",
87
+ "|------|-------|------|",
88
+ "| raw/** | extension | immutable after capture |",
89
+ "| wiki/** | model + user | editable knowledge pages |",
90
+ "| meta/* | extension | auto-generated |",
91
+ "| . | human + explicit request | operating rules |",
92
+ ].join("\n");
93
+ writeFileSync(join(vaultPaths.dotWiki, "WIKI_SCHEMA.md"), schema, "utf-8");
94
+
95
+ needsTopicInference = true;
96
+ ctx.ui.setStatus("llm-wiki", "🧠 Wiki created (inferring topic from first prompt…)");
61
97
  return;
62
98
  }
63
- ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (12 tools, auto-recall active)");
99
+
100
+ ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (12 tools, layered recall active)");
64
101
  });
65
102
 
66
- // ─── Auto-recall hook ──────────────────────────────
67
- // Before each agent turn, search the wiki for pages relevant
68
- // to the user's prompt and inject them as system context.
103
+ // ─── Layered recall + topic inference hook ──────────
104
+ // Before each agent turn:
105
+ // 1. If wiki was just auto-created, inject a directive to infer topic/mode
106
+ // from the user's first prompt and update config via wiki_bootstrap.
107
+ // 2. Search both personal + project vaults for relevant pages.
69
108
  pi.on("before_agent_start", async (event, _ctx) => {
70
109
  const paths = resolveVaultPaths(process.cwd());
71
110
  if (!existsSync(join(paths.dotWiki, "config.json"))) {
72
- // No wiki — suggest bootstrap on first turn only
73
- if (!bootstrapSuggested) {
74
- bootstrapSuggested = true;
75
- return {
76
- systemPrompt: `${event.systemPrompt}\n\n📝 No LLM Wiki found in this directory. On your first response, use ask_user to offer the user creating one via wiki_bootstrap. After suggesting once, do not repeat.`,
77
- };
78
- }
79
111
  return;
80
112
  }
81
113
 
82
114
  const prompt = event.prompt || "";
83
- if (!prompt.trim()) return;
115
+ let injectedContext = event.systemPrompt || "";
116
+
117
+ // Topic inference on first turn after auto-creation
118
+ if (needsTopicInference && prompt.trim()) {
119
+ needsTopicInference = false;
120
+
121
+ // Gather project context clues for topic inference
122
+ const cwd = process.cwd();
123
+ const dirName = basename(cwd);
124
+ let projectHints = `Project directory: "${dirName}" (path: ${cwd})`;
84
125
 
85
- const results = searchWiki(paths, prompt);
86
- if (results.length === 0) return;
126
+ try {
127
+ const pkgPath = join(cwd, "package.json");
128
+ if (existsSync(pkgPath)) {
129
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
130
+ projectHints += `\nPackage: ${pkg.name || "unknown"} v${pkg.version || "?"}`;
131
+ if (pkg.description) projectHints += `\nDescription: ${pkg.description}`;
132
+ }
133
+ } catch {
134
+ // ignore
135
+ }
136
+
137
+ injectedContext += `
138
+
139
+ ## Wiki Setup Required
140
+ The LLM Wiki was just auto-created but needs its topic and mode configured. Before responding to the user, analyze their prompt and this project's context to infer:
141
+ - **topic**: What is this wiki about? (e.g. "React app", "personal notes", "startup finances")
142
+ - **mode**: "personal" or "company" based on whether this looks like work or personal use
143
+
144
+ Project context hints:
145
+ ${projectHints}
146
+
147
+ Then call wiki_bootstrap with the inferred topic and mode to finalize the setup. This is a one-time step.`;
148
+ }
149
+
150
+ // Layered recall: search personal + project vaults for relevant pages
151
+ if (prompt.trim()) {
152
+ const results = searchWikiLayered(paths, prompt);
153
+ if (results.length > 0) {
154
+ const recallContext = formatRecallContext(results);
155
+ if (recallContext) {
156
+ injectedContext += `\n\n${recallContext}`;
157
+ }
158
+ }
159
+ }
87
160
 
88
- const context = formatRecallContext(results);
89
- if (!context) return;
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>";
90
165
 
91
- return {
92
- systemPrompt: `${event.systemPrompt}\n\n${context}`,
93
- };
166
+ if (injectedContext === event.systemPrompt) return;
167
+ return { systemPrompt: injectedContext };
94
168
  });
95
169
  }
@@ -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 { type VaultPaths, readJson, resolveVaultPaths } from "./utils.js";
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 the wiki registry for pages matching a query.
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 — reviewed automatically by LLM Wiki._`,
154
+ `_${results.length} page(s) matched your query${label}._`,
101
155
  "",
102
156
  ];
103
157
 
104
158
  for (const r of results) {
105
- lines.push(`- **[[${r.id}]]** *${r.type}* ${r.title}`);
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;
@@ -147,8 +202,8 @@ export function registerWikiRecall(pi: ExtensionAPI): void {
147
202
  Type.Number({ description: "Max results (default: 5, max: 10)", default: 5 }),
148
203
  ),
149
204
  }),
150
- async execute(_toolCallId, params) {
151
- const paths = resolveVaultPaths(process.cwd());
205
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
206
+ const paths = resolveVaultPaths(ctx.cwd ?? process.cwd());
152
207
 
153
208
  if (!existsSync(join(paths.dotWiki, "config.json"))) {
154
209
  return {
@@ -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
- const results = searchWiki(paths, params.query, maxResults);
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}". Use wiki_search for broader results.`,
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, nextSourceId, resolveVaultPaths } from "./utils.js";
6
+ import { type VaultPaths, fmtDate, resolveVaultPaths } from "./utils.js";
7
7
 
8
8
  // ─── Public API ────────────────────────────────────────
9
9
 
10
10
  export interface RetroResult {
11
- sourceId: string;
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 source packet + source page.
18
- * Returns the source ID, packet path, and source page path.
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,69 +29,32 @@ 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 manifest
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, `${sourceId}.md`);
37
+ const sourcePagePath = join(sourcePageDir, `${slug}.md`);
68
38
 
69
- const tagLine = category ? `category: ${category}` : "";
70
- const sourcePageContent = [
39
+ const pageContent = [
71
40
  "---",
72
41
  "type: source",
73
42
  `title: "${title}"`,
74
- `source_id: ${sourceId}`,
43
+ `slug: ${slug}`,
75
44
  "status: insight",
76
45
  `created: ${today}`,
77
46
  `updated: ${today}`,
78
- tagLine,
47
+ category ? `category: ${category}` : "",
79
48
  "---",
80
49
  "",
81
50
  `# ${title}`,
82
51
  "",
83
52
  body,
84
53
  "",
85
- "## Source",
54
+ category ? `*Category: ${category}*` : "",
86
55
  "",
87
- `- **Packet:** \`${packetPath}\``,
88
- `- **Captured:** ${today}`,
89
- category ? `- **Category:** ${category}` : "",
56
+ "---",
57
+ `*Captured: ${today}*`,
90
58
  "",
91
59
  "## Related",
92
60
  "",
@@ -95,21 +63,20 @@ export function saveInsight(
95
63
  ]
96
64
  .filter((l) => l !== "")
97
65
  .join("\n");
98
- writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
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 { sourceId, packetPath, sourcePagePath };
79
+ return { slug, sourcePagePath };
113
80
  }
114
81
 
115
82
  // ─── Tool Registration ──────────────────────────────────
@@ -151,8 +118,8 @@ export function registerWikiRetro(pi: ExtensionAPI): void {
151
118
  }),
152
119
  ),
153
120
  }),
154
- async execute(_toolCallId, params) {
155
- const paths = resolveVaultPaths(process.cwd());
121
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
122
+ const paths = resolveVaultPaths(ctx.cwd ?? process.cwd());
156
123
 
157
124
  if (!existsSync(join(paths.dotWiki, "config.json"))) {
158
125
  return {
@@ -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,