@zosmaai/pi-llm-wiki 0.11.2 → 0.11.3

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/CHANGELOG.md CHANGED
@@ -18,6 +18,7 @@
18
18
  - **Personal wiki created at doubled path `~/.llm-wiki/.llm-wiki/…`**: `getPersonalWikiRoot()` returned the dot-dir itself (`~/.llm-wiki`) while `getVaultPaths()` then appended another `.llm-wiki/` segment, so the personal vault was written to `~/.llm-wiki/.llm-wiki/wiki/…`. Fixed by aligning `getPersonalWikiRoot()` with the same "root = parent of `.llm-wiki/`" contract used by project vaults. `WIKI_HOME` continues to override the parent.
19
19
 
20
20
  ### Added
21
+ - **`wiki_bootstrap` over MCP, so MCP-only clients can create a vault** (Issue #130): the README aims the standalone MCP server at Claude Code, Cursor and Windsurf — clients with no access to the Pi extension — yet all five MCP tools failed closed with `No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.`, naming a tool those clients did not have. Short of installing pi or hand-building the `.llm-wiki/` tree, there was no path to a working vault. `bootstrapVault()` is pure Node (`node:fs`, `node:path`, sibling lib modules) with no model and no credentials in play, so it is now exposed as a sixth MCP tool through a thin `bootstrapOperation` adapter, keeping Pi and MCP over one implementation. Bootstrap resolves its target with `getVaultPaths(WIKI_ROOT)` rather than the usual `resolveVaultPaths()`: resolution deliberately walks up to a parent vault and then falls back to the personal vault, which is right for reading and writing pages but would create the vault somewhere other than where the client pointed the server. There is no `root` parameter, so the server stays confined to its configured root, and `mode` accepts the same free-form `personal`/`company` string the Pi tool takes. Re-running is safe: an existing vault has its config updated and metadata rebuilt, and pages are untouched. The two guards on the MCP surface (`test/mcp-parity.test.ts` and `test/mcp-package.test.ts`) were deliberately widened from five tools to six, `scripts/mcp-smoke.mjs` now requires the tool on the packed artifact, and every README's MCP table lists it.
21
22
  - **Visible wiki activity + background/reported mutations** (Issue #77): the wiki was effectively invisible — recall was appended only to the **system prompt**, the observe/retro reminder was sent with `display: false`, and the lone user-facing cue was a static status line. The wiki now surfaces what it does, and mutating work is pushed off the agent's critical path.
22
23
  - **Visible surfaces**: a one-time **session notice** (`buildSessionNotice`) announces the full loop — retrieval (recall → `wiki_search` → `read`, all synchronous because the LLM consumes their output) and capture (`wiki_observe` → `wiki_retro`, background + reported); the periodic reminder is now `display: true` and names **both** capture tools (`buildReminderText`); and the status line becomes **recall-aware** (`🧠 LLM Wiki — recalled N page(s) for this task`) when auto-recall matches.
23
24
  - **Background + reported principle**: only `wiki_search` / `read` / `wiki_recall` stay synchronous. Heavy mutations — `wiki_rebuild_meta`, `wiki_reindex_embeddings`, `wiki_lint` — now dispatch to the background runtime and **report a visible completion message** instead of blocking the turn; `wiki_ingest` gained a persistent completion report alongside its toast. New `Runtime.report()` / `Runtime.launchReported()` primitives and a `dispatchReported()` tool helper (with a synchronous fallback when no runtime is available, preserving prior behavior and unit tests).
package/README.de.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.es.md CHANGED
@@ -330,10 +330,11 @@ Esto preserva tanto el **artefacto original** como una **vista extraída normali
330
330
 
331
331
  Usa el wiki desde **cualquier herramienta compatible con MCP** — Claude Code, Cursor, Windsurf y otras.
332
332
 
333
- El paquete incluye un servidor MCP independiente que expone 5 herramientas de wiki por stdio:
333
+ El paquete incluye un servidor MCP independiente que expone 6 herramientas de wiki por stdio:
334
334
 
335
335
  | Herramienta | Descripción |
336
336
  |-------------|-------------|
337
+ | `wiki_bootstrap` | Inicializa un nuevo vault de wiki con configuración, plantillas, esquema y metadatos |
337
338
  | `wiki_recall` | Busca en el wiki páginas relevantes para la tarea |
338
339
  | `wiki_search` | Búsqueda completa del registro |
339
340
  | `wiki_status` | Estadísticas del wiki (conteos de páginas, desglose por tipo) |
package/README.fr.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.hi.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.ja.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.ko.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.md CHANGED
@@ -342,10 +342,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
342
342
 
343
343
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
344
344
 
345
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
345
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
346
346
 
347
347
  | Tool | Description |
348
348
  |------|-------------|
349
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
349
350
  | `wiki_recall` | Search wiki for task-relevant pages |
350
351
  | `wiki_search` | Full registry search |
351
352
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
@@ -480,6 +481,13 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
480
481
  <sub><b>James Fraser</b></sub>
481
482
  </a>
482
483
  </td>
484
+ <td align="center">
485
+ <a href="https://github.com/mkuhl">
486
+ <img src="https://avatars.githubusercontent.com/u/61073?v=4" width="64;" alt="mkuhl"/>
487
+ <br />
488
+ <sub><b>Mike P. Kuhl</b></sub>
489
+ </a>
490
+ </td>
483
491
  <td align="center">
484
492
  <a href="https://github.com/CelestialCreator">
485
493
  <img src="https://avatars.githubusercontent.com/u/177931942?v=4" width="64;" alt="CelestialCreator"/>
@@ -501,13 +509,6 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
501
509
  <sub><b>Daniel Naab</b></sub>
502
510
  </a>
503
511
  </td>
504
- <td align="center">
505
- <a href="https://github.com/mkuhl">
506
- <img src="https://avatars.githubusercontent.com/u/61073?v=4" width="64;" alt="mkuhl"/>
507
- <br />
508
- <sub><b>Mike P. Kuhl</b></sub>
509
- </a>
510
- </td>
511
512
  <td align="center">
512
513
  <a href="https://github.com/deestax">
513
514
  <img src="https://avatars.githubusercontent.com/u/152369481?v=4" width="64;" alt="deestax"/>
package/README.pt.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.ru.md CHANGED
@@ -346,10 +346,11 @@ This preserves both the **original artifact** and a **normalized extracted view*
346
346
 
347
347
  Use the wiki from **any MCP-compatible tool** — Claude Code, Cursor, Windsurf, and others.
348
348
 
349
- The package ships a standalone MCP server exposing 5 wiki tools over stdio:
349
+ The package ships a standalone MCP server exposing 6 wiki tools over stdio:
350
350
 
351
351
  | Tool | Description |
352
352
  |------|-------------|
353
+ | `wiki_bootstrap` | Initialize a new wiki vault with config, templates, schema, and metadata |
353
354
  | `wiki_recall` | Search wiki for task-relevant pages |
354
355
  | `wiki_search` | Full registry search |
355
356
  | `wiki_status` | Wiki stats (page counts, type breakdown) |
package/README.zh.md CHANGED
@@ -330,10 +330,11 @@ my-wiki/
330
330
 
331
331
  从 **任何 MCP 兼容工具** 使用 wiki——Claude Code、Cursor、Windsurf 等。
332
332
 
333
- 包附带独立 MCP 服务器,通过 stdio 暴露 5 个 wiki 工具:
333
+ 包附带独立 MCP 服务器,通过 stdio 暴露 6 个 wiki 工具:
334
334
 
335
335
  | 工具 | 描述 |
336
336
  |------|------|
337
+ | `wiki_bootstrap` | 用配置、模板、模式和元数据初始化新的 wiki vault |
337
338
  | `wiki_recall` | 搜索 wiki 中与任务相关的页面 |
338
339
  | `wiki_search` | 完整注册表搜索 |
339
340
  | `wiki_status` | Wiki 统计(页面计数、类型分解) |
@@ -143,9 +143,21 @@ export class Runtime {
143
143
  */
144
144
  launchReported(ctx, label, work) {
145
145
  return this.launchTask(ctx, label, async () => {
146
+ // Capture synchronously — after `await work()` the extension ctx may be
147
+ // a stale proxy (newSession/fork/switchSession/reload) and accessing
148
+ // ctx.hasUI or ctx.ui on it throws (see launchTask).
149
+ const hasUI = ctx.hasUI;
150
+ const ui = ctx.ui;
146
151
  const summary = await work();
147
- if (summary)
152
+ if (summary) {
153
+ // Instant completion feedback: the nextTurn report below is queued for
154
+ // the next user prompt, so without a toast a background task looks
155
+ // stuck. Mirrors the failure notification in launchTask and the
156
+ // success toast already used by wiki_ingest.
157
+ if (hasUI && ui)
158
+ ui.notify(summary.split("\n")[0].replace(/\*\*/g, ""), "info");
148
159
  this.report(summary);
160
+ }
149
161
  });
150
162
  }
151
163
  /**
@@ -694,7 +694,7 @@ export function registerWikiLint(pi, runtime) {
694
694
  // run it in the background and report the health summary (issue #77).
695
695
  return dispatchReported(runtime, ctx, {
696
696
  label: `lint:${paths.root}`,
697
- started: "\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted when it completes.",
697
+ started: "\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted with your next message.",
698
698
  work: async () => runWikiLint(paths, params.auto_fix === true),
699
699
  });
700
700
  },
@@ -827,11 +827,14 @@ function runWikiLint(paths, autoFix) {
827
827
  "",
828
828
  ].filter(Boolean);
829
829
  const reportPath = autoFix ? join(paths.outputs, `lint-${fmtDate()}.md`) : undefined;
830
+ // The gap snapshot is generated discovery metadata consumed by wiki_status:
831
+ // persist it on every successful lint so status never reports a stale count.
832
+ // Corrective actions below (report, event, meta rebuild) stay autoFix-only.
833
+ writeJson(join(paths.discoveries, "gaps.json"), {
834
+ gaps,
835
+ generated: new Date().toISOString(),
836
+ });
830
837
  if (autoFix && reportPath) {
831
- writeJson(join(paths.discoveries, "gaps.json"), {
832
- gaps,
833
- generated: new Date().toISOString(),
834
- });
835
838
  mkdirSync(paths.outputs, { recursive: true });
836
839
  writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf8");
837
840
  appendEvent(paths, {
@@ -954,7 +957,7 @@ export function registerWikiRebuildMeta(pi, runtime) {
954
957
  // report on completion (issue #77).
955
958
  return dispatchReported(runtime, ctx, {
956
959
  label: `rebuild_meta:${paths.root}`,
957
- started: "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
960
+ started: "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported with your next message.",
958
961
  work: async () => {
959
962
  const result = rebuildMetadata(paths);
960
963
  // No rebuild_meta event — rebuild is a projection, not an authoritative mutation
@@ -1027,7 +1030,7 @@ export function registerWikiReindexEmbeddings(pi, runtime) {
1027
1030
  // report the stats on completion (issue #77).
1028
1031
  return dispatchReported(runtime, ctx, {
1029
1032
  label: `reindex_embeddings:${paths.root}`,
1030
- started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported when it completes.`,
1033
+ started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported with your next message.`,
1031
1034
  details: { enabled: true, model: embedder.model },
1032
1035
  work: async () => {
1033
1036
  const stats = await reindexEmbeddings(paths, embedder, { force: params.force === true });
package/dist/mcp/index.js CHANGED
@@ -13,9 +13,9 @@ import { join } from "node:path";
13
13
  import { McpServer } from "@modelcontextprotocol/server";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
15
15
  import * as z from "zod/v4";
16
- import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
16
+ import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
17
17
  import { createExecApi } from "./exec.js";
18
- import { captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
18
+ import { bootstrapOperation, captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
19
19
  const execApi = createExecApi();
20
20
  // ─── Vault Detection ────────────────────────────────────
21
21
  /** Resolve vault paths, same as Pi extension. */
@@ -23,6 +23,18 @@ function getPaths() {
23
23
  const root = process.env.WIKI_ROOT || process.cwd();
24
24
  return resolveVaultPaths(root);
25
25
  }
26
+ /**
27
+ * The vault root this server was configured with, without resolution.
28
+ *
29
+ * `getPaths()` RESOLVES an existing vault: on a root that has none it walks up
30
+ * to a parent vault and then falls back to the personal vault. That is right
31
+ * for reading and writing pages, and wrong for creating one — bootstrap must
32
+ * create the vault where the client pointed the server, not wherever
33
+ * resolution lands. The Pi tool draws the same distinction.
34
+ */
35
+ function getConfiguredPaths() {
36
+ return getVaultPaths(process.env.WIKI_ROOT || process.cwd());
37
+ }
26
38
  function hasVault() {
27
39
  const paths = getPaths();
28
40
  return existsSync(join(paths.dotWiki, "config.json"));
@@ -32,9 +44,49 @@ const server = new McpServer({
32
44
  name: "llm-wiki",
33
45
  version: "1.0.0",
34
46
  });
47
+ // ---- wiki_bootstrap ----
48
+ //
49
+ // Registered first, and the only tool not gated on an existing vault: the
50
+ // other five fail closed with a message naming this one, which an MCP-only
51
+ // client could not act on while it was extension-only (issue #130).
52
+ server.registerTool("wiki_bootstrap", {
53
+ description: "Create an LLM Wiki vault at this server's wiki root (WIKI_ROOT, or the working directory). Writes config, schema, templates and metadata scaffolding. Run this first when no vault exists; safe to re-run on an existing vault, where it updates the config and rebuilds metadata without touching pages.",
54
+ inputSchema: z.object({
55
+ topic: z.string().describe("Main topic of the wiki"),
56
+ mode: z.string().optional().describe("personal or company (default: personal)"),
57
+ }),
58
+ }, async ({ topic, mode }) => {
59
+ const paths = getConfiguredPaths();
60
+ const result = await bootstrapOperation(paths, { topic, mode });
61
+ if (!result.ok) {
62
+ return {
63
+ content: [
64
+ {
65
+ type: "text",
66
+ text: `Vault error: ${result.diagnostics[0].message}`,
67
+ },
68
+ ],
69
+ isError: true,
70
+ };
71
+ }
72
+ const warnings = result.diagnostics.map((d) => `⚠️ ${d.code}: ${d.message}`);
73
+ return {
74
+ content: [
75
+ {
76
+ type: "text",
77
+ text: [
78
+ `${result.created ? "Wiki vault created" : "Wiki vault updated"} at ${paths.root}`,
79
+ "Structure: .llm-wiki/{raw,wiki,meta} plus config and WIKI_SCHEMA.md",
80
+ "Next: capture a source with wiki_capture_source, or save an insight with wiki_retro.",
81
+ ...warnings,
82
+ ].join("\n"),
83
+ },
84
+ ],
85
+ };
86
+ });
35
87
  // ---- wiki_recall ----
36
88
  server.registerTool("wiki_recall", {
37
- description: "Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
89
+ description: "Search the wiki for pages relevant to a query. Searches the resolved vault and the personal vault (~/.llm-wiki) together, deduplicated, with personal hits labelled. Returns matching page IDs, titles, types, and content previews.",
38
90
  inputSchema: z.object({
39
91
  query: z.string().describe("Search query — use the user's full request or key terms"),
40
92
  max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
@@ -5,8 +5,9 @@
5
5
  * used by Pi tools. No operation parses YAML, scans files, scores
6
6
  * registry entries, or builds page strings itself.
7
7
  */
8
+ import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
8
9
  import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
9
- import { searchWiki } from "../extensions/llm-wiki/lib/recall.js";
10
+ import { searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
10
11
  import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
11
12
  import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
12
13
  import { VaultWriteError, inspectVaultFormat, inspectWritableVault, } from "../extensions/llm-wiki/lib/vault-format.js";
@@ -19,9 +20,44 @@ function projectionOutcome(projection) {
19
20
  diagnostics: projection.diagnostics.map(({ code, message }) => ({ code, message })),
20
21
  };
21
22
  }
22
- /** Shared recall operation: calls searchWiki and appends vault diagnostics. */
23
+ /**
24
+ * Shared bootstrap operation: create (or update) the vault at `paths`.
25
+ *
26
+ * This is the one operation that must work when no vault exists — every other
27
+ * one fails closed naming it. `bootstrapVault` is pure Node (`node:fs`,
28
+ * `node:path` and sibling lib modules), so it needs no model and no
29
+ * credentials, which is what makes it fit the MCP surface.
30
+ *
31
+ * A failed projection rebuild is reported as diagnostics alongside `ok: true`:
32
+ * the vault has been written to disk by then, and `wiki_lint` is the repair
33
+ * path, so failing the call outright would misreport what happened.
34
+ */
35
+ export async function bootstrapOperation(paths, input) {
36
+ const result = bootstrapVault(paths, { topic: input.topic, mode: input.mode ?? "personal" });
37
+ if (!result.ok) {
38
+ return {
39
+ ok: false,
40
+ diagnostics: result.diagnostics.map(({ code, message }) => ({ code, message })),
41
+ };
42
+ }
43
+ const projection = projectionOutcome(result.projection);
44
+ return {
45
+ ok: true,
46
+ created: result.created,
47
+ diagnostics: projection.ok ? [] : projection.diagnostics,
48
+ };
49
+ }
50
+ /**
51
+ * Shared recall operation: layered search plus vault diagnostics.
52
+ *
53
+ * Layering is the shared contract, not an extension-only feature: MCP clients
54
+ * get the same personal + project merge the Pi `wiki_recall` tool does.
55
+ * `searchWikiLayered` appends personal-vault hits, deduplicates by page ID and
56
+ * tags personal results with `vaultLabel`. It is a no-op when no personal vault
57
+ * exists, or when the resolved vault IS the personal vault.
58
+ */
23
59
  export async function recallOperation(paths, query, maxResults = 5) {
24
- const results = searchWiki(paths, query, maxResults);
60
+ const results = searchWikiLayered(paths, query, maxResults);
25
61
  const vaultState = inspectVaultFormat(paths);
26
62
  return {
27
63
  results,
package/docs/api.md CHANGED
@@ -223,6 +223,10 @@ Deterministic health check of the wiki. Scans for orphan pages (no inbound links
223
223
  (linked but not created), and contradiction markers. Optionally auto-creates stub pages for
224
224
  knowledge gaps cited in two or more pages.
225
225
 
226
+ Runs **asynchronously**: the tool acknowledges immediately and scans off-thread. On completion a
227
+ UI toast fires instantly (when a UI is available) and the full health report is delivered with
228
+ the next user message.
229
+
226
230
  **Parameters**
227
231
 
228
232
  | Name | Type | Required | Description |
@@ -277,9 +281,13 @@ Health is `"⚠️ Warning"` when orphan count exceeds 5, `"🔴 Empty"` when th
277
281
 
278
282
  ## wiki_rebuild_meta
279
283
 
280
- Force a full synchronous rebuild of all generated metadata: `registry.json`, `backlinks.json`,
284
+ Force a full rebuild of all generated metadata: `registry.json`, `backlinks.json`,
281
285
  `index.md`, `log.md`. Use when metadata appears out of sync with actual wiki files.
282
286
 
287
+ Runs **asynchronously**: the tool acknowledges immediately and rebuilds off-thread. On completion
288
+ a UI toast fires instantly (when a UI is available) and the result is delivered with the next
289
+ user message.
290
+
283
291
  If `meta/events.jsonl` is missing or unreadable, rebuild reports a warning and preserves existing log projections while continuing to rebuild registry, backlinks, and indexes. A present zero-byte event file is an intentional empty history.
284
292
 
285
293
  **Parameters**
@@ -213,8 +213,20 @@ export class Runtime {
213
213
  */
214
214
  launchReported(ctx: LaunchCtx, label: string, work: () => Promise<string | null>): Promise<void> {
215
215
  return this.launchTask(ctx, label, async () => {
216
+ // Capture synchronously — after `await work()` the extension ctx may be
217
+ // a stale proxy (newSession/fork/switchSession/reload) and accessing
218
+ // ctx.hasUI or ctx.ui on it throws (see launchTask).
219
+ const hasUI = ctx.hasUI;
220
+ const ui = ctx.ui;
216
221
  const summary = await work();
217
- if (summary) this.report(summary);
222
+ if (summary) {
223
+ // Instant completion feedback: the nextTurn report below is queued for
224
+ // the next user prompt, so without a toast a background task looks
225
+ // stuck. Mirrors the failure notification in launchTask and the
226
+ // success toast already used by wiki_ingest.
227
+ if (hasUI && ui) ui.notify(summary.split("\n")[0].replace(/\*\*/g, ""), "info");
228
+ this.report(summary);
229
+ }
218
230
  });
219
231
  }
220
232
 
@@ -821,7 +821,7 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
821
821
  return dispatchReported(runtime, ctx as ToolCtx, {
822
822
  label: `lint:${paths.root}`,
823
823
  started:
824
- "\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted when it completes.",
824
+ "\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted with your next message.",
825
825
  work: async () => runWikiLint(paths, params.auto_fix === true),
826
826
  });
827
827
  },
@@ -962,11 +962,14 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
962
962
  "",
963
963
  ].filter(Boolean);
964
964
  const reportPath = autoFix ? join(paths.outputs, `lint-${fmtDate()}.md`) : undefined;
965
+ // The gap snapshot is generated discovery metadata consumed by wiki_status:
966
+ // persist it on every successful lint so status never reports a stale count.
967
+ // Corrective actions below (report, event, meta rebuild) stay autoFix-only.
968
+ writeJson(join(paths.discoveries, "gaps.json"), {
969
+ gaps,
970
+ generated: new Date().toISOString(),
971
+ });
965
972
  if (autoFix && reportPath) {
966
- writeJson(join(paths.discoveries, "gaps.json"), {
967
- gaps,
968
- generated: new Date().toISOString(),
969
- });
970
973
  mkdirSync(paths.outputs, { recursive: true });
971
974
  writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf8");
972
975
  appendEvent(paths, {
@@ -1106,7 +1109,7 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1106
1109
  return dispatchReported(runtime, ctx as ToolCtx, {
1107
1110
  label: `rebuild_meta:${paths.root}`,
1108
1111
  started:
1109
- "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
1112
+ "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported with your next message.",
1110
1113
  work: async () => {
1111
1114
  const result = rebuildMetadata(paths);
1112
1115
  // No rebuild_meta event — rebuild is a projection, not an authoritative mutation
@@ -1187,7 +1190,7 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
1187
1190
  // report the stats on completion (issue #77).
1188
1191
  return dispatchReported(runtime, ctx as ToolCtx, {
1189
1192
  label: `reindex_embeddings:${paths.root}`,
1190
- started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported when it completes.`,
1193
+ started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported with your next message.`,
1191
1194
  details: { enabled: true, model: embedder.model },
1192
1195
  work: async () => {
1193
1196
  const stats = await reindexEmbeddings(paths, embedder, { force: params.force === true });
package/mcp/index.ts CHANGED
@@ -15,9 +15,10 @@ import { join } from "node:path";
15
15
  import { McpServer } from "@modelcontextprotocol/server";
16
16
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
17
17
  import * as z from "zod/v4";
18
- import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
18
+ import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
19
19
  import { createExecApi } from "./exec.js";
20
20
  import {
21
+ bootstrapOperation,
21
22
  captureSourceOperation,
22
23
  recallOperation,
23
24
  retroOperation,
@@ -35,6 +36,19 @@ function getPaths(): ReturnType<typeof resolveVaultPaths> {
35
36
  return resolveVaultPaths(root);
36
37
  }
37
38
 
39
+ /**
40
+ * The vault root this server was configured with, without resolution.
41
+ *
42
+ * `getPaths()` RESOLVES an existing vault: on a root that has none it walks up
43
+ * to a parent vault and then falls back to the personal vault. That is right
44
+ * for reading and writing pages, and wrong for creating one — bootstrap must
45
+ * create the vault where the client pointed the server, not wherever
46
+ * resolution lands. The Pi tool draws the same distinction.
47
+ */
48
+ function getConfiguredPaths(): ReturnType<typeof getVaultPaths> {
49
+ return getVaultPaths(process.env.WIKI_ROOT || process.cwd());
50
+ }
51
+
38
52
  function hasVault(): boolean {
39
53
  const paths = getPaths();
40
54
  return existsSync(join(paths.dotWiki, "config.json"));
@@ -47,13 +61,62 @@ const server = new McpServer({
47
61
  version: "1.0.0",
48
62
  });
49
63
 
64
+ // ---- wiki_bootstrap ----
65
+ //
66
+ // Registered first, and the only tool not gated on an existing vault: the
67
+ // other five fail closed with a message naming this one, which an MCP-only
68
+ // client could not act on while it was extension-only (issue #130).
69
+
70
+ server.registerTool(
71
+ "wiki_bootstrap",
72
+ {
73
+ description:
74
+ "Create an LLM Wiki vault at this server's wiki root (WIKI_ROOT, or the working directory). Writes config, schema, templates and metadata scaffolding. Run this first when no vault exists; safe to re-run on an existing vault, where it updates the config and rebuilds metadata without touching pages.",
75
+ inputSchema: z.object({
76
+ topic: z.string().describe("Main topic of the wiki"),
77
+ mode: z.string().optional().describe("personal or company (default: personal)"),
78
+ }),
79
+ },
80
+ async ({ topic, mode }) => {
81
+ const paths = getConfiguredPaths();
82
+ const result = await bootstrapOperation(paths, { topic, mode });
83
+
84
+ if (!result.ok) {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text" as const,
89
+ text: `Vault error: ${result.diagnostics[0].message}`,
90
+ },
91
+ ],
92
+ isError: true,
93
+ };
94
+ }
95
+
96
+ const warnings = result.diagnostics.map((d) => `⚠️ ${d.code}: ${d.message}`);
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text" as const,
101
+ text: [
102
+ `${result.created ? "Wiki vault created" : "Wiki vault updated"} at ${paths.root}`,
103
+ "Structure: .llm-wiki/{raw,wiki,meta} plus config and WIKI_SCHEMA.md",
104
+ "Next: capture a source with wiki_capture_source, or save an insight with wiki_retro.",
105
+ ...warnings,
106
+ ].join("\n"),
107
+ },
108
+ ],
109
+ };
110
+ },
111
+ );
112
+
50
113
  // ---- wiki_recall ----
51
114
 
52
115
  server.registerTool(
53
116
  "wiki_recall",
54
117
  {
55
118
  description:
56
- "Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
119
+ "Search the wiki for pages relevant to a query. Searches the resolved vault and the personal vault (~/.llm-wiki) together, deduplicated, with personal hits labelled. Returns matching page IDs, titles, types, and content previews.",
57
120
  inputSchema: z.object({
58
121
  query: z.string().describe("Search query — use the user's full request or key terms"),
59
122
  max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
package/mcp/operations.ts CHANGED
@@ -7,8 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+ import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
10
11
  import { type ProjectionResult, rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
11
- import { searchWiki } from "../extensions/llm-wiki/lib/recall.js";
12
+ import { type RecallResult, searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
12
13
  import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
13
14
  import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
14
15
  import type { VaultPaths } from "../extensions/llm-wiki/lib/utils.js";
@@ -30,16 +31,58 @@ function projectionOutcome(
30
31
  };
31
32
  }
32
33
 
33
- /** Shared recall operation: calls searchWiki and appends vault diagnostics. */
34
+ /**
35
+ * Shared bootstrap operation: create (or update) the vault at `paths`.
36
+ *
37
+ * This is the one operation that must work when no vault exists — every other
38
+ * one fails closed naming it. `bootstrapVault` is pure Node (`node:fs`,
39
+ * `node:path` and sibling lib modules), so it needs no model and no
40
+ * credentials, which is what makes it fit the MCP surface.
41
+ *
42
+ * A failed projection rebuild is reported as diagnostics alongside `ok: true`:
43
+ * the vault has been written to disk by then, and `wiki_lint` is the repair
44
+ * path, so failing the call outright would misreport what happened.
45
+ */
46
+ export async function bootstrapOperation(
47
+ paths: VaultPaths,
48
+ input: { topic: string; mode?: string },
49
+ ): Promise<
50
+ | { ok: true; created: boolean; diagnostics: Array<{ code: string; message: string }> }
51
+ | { ok: false; diagnostics: Array<{ code: string; message: string }> }
52
+ > {
53
+ const result = bootstrapVault(paths, { topic: input.topic, mode: input.mode ?? "personal" });
54
+ if (!result.ok) {
55
+ return {
56
+ ok: false,
57
+ diagnostics: result.diagnostics.map(({ code, message }) => ({ code, message })),
58
+ };
59
+ }
60
+ const projection = projectionOutcome(result.projection);
61
+ return {
62
+ ok: true,
63
+ created: result.created,
64
+ diagnostics: projection.ok ? [] : projection.diagnostics,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Shared recall operation: layered search plus vault diagnostics.
70
+ *
71
+ * Layering is the shared contract, not an extension-only feature: MCP clients
72
+ * get the same personal + project merge the Pi `wiki_recall` tool does.
73
+ * `searchWikiLayered` appends personal-vault hits, deduplicates by page ID and
74
+ * tags personal results with `vaultLabel`. It is a no-op when no personal vault
75
+ * exists, or when the resolved vault IS the personal vault.
76
+ */
34
77
  export async function recallOperation(
35
78
  paths: VaultPaths,
36
79
  query: string,
37
80
  maxResults = 5,
38
81
  ): Promise<{
39
- results: Array<{ id: string; title: string; type: string; preview?: string }>;
82
+ results: RecallResult[];
40
83
  diagnostics: Array<{ code: string; message: string }>;
41
84
  }> {
42
- const results = searchWiki(paths, query, maxResults);
85
+ const results = searchWikiLayered(paths, query, maxResults);
43
86
  const vaultState = inspectVaultFormat(paths);
44
87
  return {
45
88
  results,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
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",