@zosmaai/pi-llm-wiki 0.12.1 → 0.12.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/CHANGELOG.md +1 -0
- package/README.md +16 -8
- package/dist/extensions/llm-wiki/lib/bootstrap.js +2 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +24 -1
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +3 -1
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +41 -6
- package/dist/extensions/llm-wiki/lib/model-command.js +45 -8
- package/dist/extensions/llm-wiki/lib/qmd-indexing.js +1024 -0
- package/dist/extensions/llm-wiki/lib/qmd-mirror.js +418 -0
- package/dist/extensions/llm-wiki/lib/qmd-store.js +112 -0
- package/dist/extensions/llm-wiki/lib/recall.js +77 -3
- package/dist/extensions/llm-wiki/lib/runtime.js +25 -1
- package/dist/extensions/llm-wiki/lib/subagent.js +47 -7
- package/dist/extensions/llm-wiki/lib/tools.js +165 -5
- package/dist/extensions/llm-wiki/lib/utils.js +16 -2
- package/dist/extensions/llm-wiki/lib/wiki-service.js +104 -5
- package/dist/mcp/index.js +66 -2
- package/dist/mcp/operations.js +26 -2
- package/docs/api.md +43 -1
- package/docs/architecture.md +28 -0
- package/docs/commands.md +1 -0
- package/docs/qmd-compatibility.md +47 -0
- package/docs/retrieval-benchmark.md +47 -0
- package/docs/superpowers/benchmarks/phase-1-current-baseline.json +53 -0
- package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-2-remediation.md +549 -0
- package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-2-validated-indexing.md +1493 -0
- package/docs/superpowers/plans/2026-08-11-qmd-retrieval-phase-3-retrieval-modes-and-recall-cutover.md +678 -0
- package/docs/superpowers/plans/2026-09-05-wikilink-alias-pipe-table-only.md +257 -0
- package/extensions/llm-wiki/index.ts +14 -1
- package/extensions/llm-wiki/lib/bootstrap.ts +2 -0
- package/extensions/llm-wiki/lib/indexing.ts +24 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +10 -2
- package/extensions/llm-wiki/lib/knowledge-document.ts +8 -1
- package/extensions/llm-wiki/lib/knowledge-links.ts +39 -7
- package/extensions/llm-wiki/lib/model-command.ts +57 -12
- package/extensions/llm-wiki/lib/qmd-indexing.ts +1304 -0
- package/extensions/llm-wiki/lib/qmd-mirror.ts +496 -0
- package/extensions/llm-wiki/lib/qmd-store.ts +222 -0
- package/extensions/llm-wiki/lib/recall.ts +77 -3
- package/extensions/llm-wiki/lib/runtime.ts +57 -5
- package/extensions/llm-wiki/lib/subagent.ts +73 -10
- package/extensions/llm-wiki/lib/tools.ts +188 -4
- package/extensions/llm-wiki/lib/utils.ts +21 -2
- package/extensions/llm-wiki/lib/wiki-service.ts +160 -4
- package/mcp/index.ts +78 -1
- package/mcp/operations.ts +41 -2
- package/package.json +9 -6
- package/skills/llm-wiki/SKILL.md +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
- **17 more tests** (`test/ambient-gate.test.ts`): project-vault resolution (own vault, ancestor walk, `WIKI_HOME`, personal-vault exclusion), the host-dependent default and its explicit override, the composed gate across four project states, and the reminder gate — including that a closed gate does not bank turns, so opening it later is not an instant nag, and that `display: false` is not a substitute because it still injects into model context.
|
|
14
14
|
|
|
15
15
|
### Fixed
|
|
16
|
+
- **`/wiki-model` picker ignored typing on long lists** (Issue #218): printable keys were forwarded to `SelectList`, which only handles arrows/enter/esc, so finding `anthropic/claude-haiku` still meant paging. A visible `Input` above the editor-dock list now filters with a case-insensitive substring on `label` and `value` (`SelectList.setFilter` is prefix-on-value and would miss `haiku`). Empty query restores the full list; no matches uses SelectList's no-match row; Esc still cancels immediately. Persist and scriptable args are unchanged.
|
|
16
17
|
- **MCP server failed to start on `@modelcontextprotocol/server` 2.0.0** (Issue #128): `mcp/index.ts` imported `StdioServerTransport` from the package root, but SDK 2.0.0 (published 2026-07-27) moved that export to the `./stdio` subpath, so `node dist/mcp/index.js` died with `SyntaxError: The requested module '@modelcontextprotocol/server' does not provide an export named 'StdioServerTransport'` before the transport ever connected. The declared range `^2.0.0-alpha.2` is a caret range over a prerelease, so it permits `2.0.0`: every fresh consumer install resolved the stable SDK against the pre-stable import, while `pnpm-lock.yaml` pinned the `2.0.0-alpha.2` floor — so `test/mcp-package.test.ts`, which does spawn the published command and complete the stdio handshake, only ever exercised the alpha where the root export still existed. Fixed by importing `StdioServerTransport` from `@modelcontextprotocol/server/stdio` (the `./stdio` subpath does not exist in `2.0.0-alpha.2`, so the range, the lockfile and the import move together), raising the range to `^2.0.0`, and refreshing the lockfile so the existing smoke test now runs against the SDK consumers actually get.
|
|
17
18
|
- **Packaged MCP server was never exercised with consumer-resolved dependencies**: a new `packaged-mcp-consumer` CI job packs the tarball, installs it into a directory with **no lockfile**, prints the resolved SDK version, and runs `scripts/mcp-smoke.mjs` — the stdio handshake (`initialize` → `tools/list`) — against that install. This closes the whole class of failure behind Issue #128: a dependency range that resolves differently for consumers than for this repo's lockfile can no longer break the published server undetected.
|
|
18
19
|
- **README documented a standalone MCP command that ships no JavaScript** (Issue #129): all ten READMEs pointed MCP clients at `node node_modules/@zosmaai/pi-llm-wiki/mcp/index.js`, but the published `mcp/` directory contains only TypeScript (`index.ts`, `exec.ts`, `operations.ts`), so the documented command failed with `MODULE_NOT_FOUND` — independently of any dependency-resolution problem. The runnable entry is `dist/mcp/index.js`, which the package's own `pi.mcpservers` manifest already declared correctly. Every README now documents that path and adds an `mcpServers` config block for `.mcp.json` / `claude_desktop_config.json` using **absolute** paths, because MCP clients spawn the command without a shell: a `~` in `args` or `env` is passed through literally and the server fails to start behind a generic connection error. A new `test/package-structure.test.ts` case derives the expected path from `pi.mcpservers` — the same command `test/mcp-package.test.ts` spawns and handshakes with — and asserts every documented `…/mcp/index.js` occurrence in every README resolves to it, so docs cannot drift from the runnable entry point again.
|
package/README.md
CHANGED
|
@@ -127,6 +127,7 @@ The result is a wiki that **compounds** as you capture sources, ask questions, a
|
|
|
127
127
|
| `wiki_status` | Show counts, source states, and recent activity |
|
|
128
128
|
| `wiki_observe` | Record timestamped, searchable observations from the current session (decisions, findings, constraints) — later distilled into pages |
|
|
129
129
|
| `wiki_rebuild_meta` | Force a full metadata rebuild (registry, backlinks, index, log) |
|
|
130
|
+
| `wiki_reindex` | Rebuild/repair the generated QMD search index at `meta/qmd` (lexical is model-free; vectors may download ~2 GB) |
|
|
130
131
|
| `wiki_reindex_embeddings` | Refresh semantic embeddings for new or stale pages (no-op if no embedding provider is configured) |
|
|
131
132
|
| `wiki_log_event` | Append a structured event to the wiki activity log |
|
|
132
133
|
| `wiki_watch` | Print a `crontab` line for automatic wiki updates (daily / weekly / hourly) — does not install it |
|
|
@@ -582,6 +583,13 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
|
|
|
582
583
|
<sub><b>PrestaLab</b></sub>
|
|
583
584
|
</a>
|
|
584
585
|
</td>
|
|
586
|
+
<td align="center">
|
|
587
|
+
<a href="https://github.com/wooksong">
|
|
588
|
+
<img src="https://avatars.githubusercontent.com/u/2772376?v=4" width="64;" alt="wooksong"/>
|
|
589
|
+
<br />
|
|
590
|
+
<sub><b>wooksong</b></sub>
|
|
591
|
+
</a>
|
|
592
|
+
</td>
|
|
585
593
|
<td align="center">
|
|
586
594
|
<a href="https://github.com/xcsf">
|
|
587
595
|
<img src="https://avatars.githubusercontent.com/u/43439835?v=4" width="64;" alt="xcsf"/>
|
|
@@ -589,6 +597,8 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
|
|
|
589
597
|
<sub><b>xcsf</b></sub>
|
|
590
598
|
</a>
|
|
591
599
|
</td>
|
|
600
|
+
</tr>
|
|
601
|
+
<tr>
|
|
592
602
|
<td align="center">
|
|
593
603
|
<a href="https://github.com/danielnaab">
|
|
594
604
|
<img src="https://avatars.githubusercontent.com/u/136512?v=4" width="64;" alt="danielnaab"/>
|
|
@@ -596,20 +606,18 @@ Thanks to everyone who has contributed! This list is regenerated automatically b
|
|
|
596
606
|
<sub><b>Daniel Naab</b></sub>
|
|
597
607
|
</a>
|
|
598
608
|
</td>
|
|
599
|
-
</tr>
|
|
600
|
-
<tr>
|
|
601
609
|
<td align="center">
|
|
602
|
-
<a href="https://github.com/
|
|
603
|
-
<img src="https://avatars.githubusercontent.com/u/
|
|
610
|
+
<a href="https://github.com/mdmayfield">
|
|
611
|
+
<img src="https://avatars.githubusercontent.com/u/26154258?v=4" width="64;" alt="mdmayfield"/>
|
|
604
612
|
<br />
|
|
605
|
-
<sub><b>
|
|
613
|
+
<sub><b>Matt Mayfield</b></sub>
|
|
606
614
|
</a>
|
|
607
615
|
</td>
|
|
608
616
|
<td align="center">
|
|
609
|
-
<a href="https://github.com/
|
|
610
|
-
<img src="https://avatars.githubusercontent.com/u/
|
|
617
|
+
<a href="https://github.com/deestax">
|
|
618
|
+
<img src="https://avatars.githubusercontent.com/u/152369481?v=4" width="64;" alt="deestax"/>
|
|
611
619
|
<br />
|
|
612
|
-
<sub><b>
|
|
620
|
+
<sub><b>Superdao</b></sub>
|
|
613
621
|
</a>
|
|
614
622
|
</td>
|
|
615
623
|
<td align="center">
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { existsSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { appendEvent, rebuildMetadata } from "./metadata.js";
|
|
@@ -64,6 +65,7 @@ export function bootstrapVault(paths, input) {
|
|
|
64
65
|
topic: input.topic,
|
|
65
66
|
created: existing.created ?? fmtDate(),
|
|
66
67
|
version: existing.version ?? "1.0",
|
|
68
|
+
vault_id: existing.vault_id ?? randomUUID(),
|
|
67
69
|
...(created ? { knowledge_format: "okf-0.2" } : {}),
|
|
68
70
|
};
|
|
69
71
|
ensureVaultStructure(paths);
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { reindexEmbeddings, resolveEmbedder } from "./embeddings.js";
|
|
26
26
|
import { rebuildMetadataLight } from "./metadata.js";
|
|
27
|
+
import { invalidateQmdAfterProjectionFailure, reindexQmdVault } from "./qmd-indexing.js";
|
|
27
28
|
/** Promise of the current background pass, keyed by vault root. */
|
|
28
29
|
const inflight = new Map();
|
|
29
30
|
/** Vault roots with writes awaiting a (re)build. */
|
|
@@ -54,8 +55,30 @@ export function scheduleReindex(runtime, ctx, paths) {
|
|
|
54
55
|
while (dirty.has(root)) {
|
|
55
56
|
dirty.delete(root);
|
|
56
57
|
const projection = rebuildMetadataLight(paths);
|
|
57
|
-
if (!projection.ok)
|
|
58
|
+
if (!projection.ok) {
|
|
59
|
+
// Generated QMD search state is repairable and must not fail the
|
|
60
|
+
// authoritative write. On a projection failure, only invalidate unsafe
|
|
61
|
+
// QMD entries (never index valid additions).
|
|
62
|
+
try {
|
|
63
|
+
await invalidateQmdAfterProjectionFailure(paths);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Best-effort safety pass; a busy/transient lock must not abort the
|
|
67
|
+
// metadata drain loop.
|
|
68
|
+
}
|
|
58
69
|
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// Post-projection lexical QMD pass. Model-free and repairable.
|
|
73
|
+
await reindexQmdVault(paths, {
|
|
74
|
+
scope: "changed",
|
|
75
|
+
components: ["lexical"],
|
|
76
|
+
force: false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Generated search indexing is repairable and must not fail the write.
|
|
81
|
+
}
|
|
59
82
|
// Refresh embeddings only after metadata is consistent. Stale-aware and
|
|
60
83
|
// a no-op unless an embedder is configured.
|
|
61
84
|
runtime.ensureConfig(root);
|
|
@@ -388,7 +388,7 @@ Rules:
|
|
|
388
388
|
* synthesis.
|
|
389
389
|
*/
|
|
390
390
|
export async function runIngestSynthesis(args) {
|
|
391
|
-
const { model, apiKey, headers, paths, sourceId, manifest, extracted, maxChars, signal, synthesisLanguage, synthesisMaxTokens, wikilinkValidation, } = args;
|
|
391
|
+
const { model, apiKey, headers, streamFn, env, paths, sourceId, manifest, extracted, maxChars, signal, synthesisLanguage, synthesisMaxTokens, wikilinkValidation, } = args;
|
|
392
392
|
const content = extracted.slice(0, maxChars ?? 24_000);
|
|
393
393
|
if (!content.trim())
|
|
394
394
|
return undefined;
|
|
@@ -422,6 +422,8 @@ export async function runIngestSynthesis(args) {
|
|
|
422
422
|
model,
|
|
423
423
|
apiKey,
|
|
424
424
|
headers,
|
|
425
|
+
streamFn,
|
|
426
|
+
env,
|
|
425
427
|
systemPrompt,
|
|
426
428
|
userPrompt,
|
|
427
429
|
tools: [commitTool],
|
|
@@ -7,21 +7,56 @@ function diag(severity, code, path, message) {
|
|
|
7
7
|
function normalizeWikilinkTarget(target) {
|
|
8
8
|
return target.trim().replace(/\\$/, "");
|
|
9
9
|
}
|
|
10
|
+
// Blank out code spans, fenced/indented code blocks, and raw HTML (the same
|
|
11
|
+
// node types the link walk skips), preserving length so offsets stay valid.
|
|
12
|
+
function maskCodeRegions(tree, body) {
|
|
13
|
+
if (!tree)
|
|
14
|
+
return body;
|
|
15
|
+
const ranges = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
const position = node
|
|
18
|
+
.position;
|
|
19
|
+
if (node.type === "inlineCode" || node.type === "code" || node.type === "html") {
|
|
20
|
+
if (position)
|
|
21
|
+
ranges.push([position.start.offset, position.end.offset]);
|
|
22
|
+
}
|
|
23
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
24
|
+
for (const child of node.children) {
|
|
25
|
+
visit(child);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
visit(tree);
|
|
30
|
+
if (ranges.length === 0)
|
|
31
|
+
return body;
|
|
32
|
+
const chars = body.split("");
|
|
33
|
+
for (const [start, end] of ranges) {
|
|
34
|
+
for (let i = start; i < end && i < chars.length; i++) {
|
|
35
|
+
if (i >= 0)
|
|
36
|
+
chars[i] = " ";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return chars.join("");
|
|
40
|
+
}
|
|
10
41
|
export function extractKnowledgeLinks(body) {
|
|
11
42
|
const markdown = [];
|
|
12
43
|
const wikilinks = [];
|
|
13
|
-
// Extract legacy wikilinks. A table-safe alias uses an escaped pipe: [[target\\|alias]].
|
|
14
|
-
for (const match of body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g)) {
|
|
15
|
-
wikilinks.push({ target: normalizeWikilinkTarget(match[1]), offset: match.index ?? 0 });
|
|
16
|
-
}
|
|
17
44
|
// Parse with CommonMark AST
|
|
18
|
-
let tree;
|
|
45
|
+
let tree = null;
|
|
19
46
|
try {
|
|
20
47
|
tree = fromMarkdown(body);
|
|
21
48
|
}
|
|
22
49
|
catch {
|
|
23
|
-
|
|
50
|
+
tree = null;
|
|
24
51
|
}
|
|
52
|
+
// Extract legacy wikilinks. A table-safe alias uses an escaped pipe: [[target\\|alias]].
|
|
53
|
+
// Scan the code-masked body so [[...]] inside code is not a real link.
|
|
54
|
+
const scanBody = maskCodeRegions(tree, body);
|
|
55
|
+
for (const match of scanBody.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g)) {
|
|
56
|
+
wikilinks.push({ target: normalizeWikilinkTarget(match[1]), offset: match.index ?? 0 });
|
|
57
|
+
}
|
|
58
|
+
if (!tree)
|
|
59
|
+
return { markdown, wikilinks };
|
|
25
60
|
// Build definition map (case-insensitive)
|
|
26
61
|
const defs = new Map();
|
|
27
62
|
function collectDefs(node) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Container,
|
|
1
|
+
import { Container, getKeybindings, Input, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
2
2
|
import { parseModelRef, persistTaskModel } from "./task-config.js";
|
|
3
3
|
/** Words that clear the override and revert to the session model. */
|
|
4
4
|
const CLEAR_WORDS = new Set(["session", "default", "reset", "clear", "none", "unset"]);
|
|
@@ -53,29 +53,66 @@ function buildPickerItems(pool, currentRef) {
|
|
|
53
53
|
}
|
|
54
54
|
return { items, selectedIndex };
|
|
55
55
|
}
|
|
56
|
+
/** Case-insensitive substring on label/value. Not SelectList.setFilter (prefix-on-value). */
|
|
57
|
+
function itemMatchesQuery(item, query) {
|
|
58
|
+
if (!query)
|
|
59
|
+
return true;
|
|
60
|
+
const needle = query.toLowerCase();
|
|
61
|
+
return item.label.toLowerCase().includes(needle) || item.value.toLowerCase().includes(needle);
|
|
62
|
+
}
|
|
56
63
|
/** Editor-dock picker; `ui.custom` without overlay replaces the input slot like `/model`. */
|
|
57
64
|
class ModelPickerScreen extends Container {
|
|
65
|
+
allItems;
|
|
66
|
+
listTheme;
|
|
67
|
+
search;
|
|
58
68
|
list;
|
|
59
69
|
doneFn;
|
|
60
70
|
closed = false;
|
|
61
71
|
constructor(title, items, selectedIndex, theme, done) {
|
|
62
72
|
super();
|
|
73
|
+
this.allItems = items;
|
|
74
|
+
this.listTheme = buildSelectTheme(theme);
|
|
63
75
|
this.doneFn = done;
|
|
64
76
|
this.addChild(new Text(title, 0, 0));
|
|
65
77
|
this.addChild(new Spacer(1));
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.list
|
|
78
|
+
this.search = new Input();
|
|
79
|
+
this.search.focused = true;
|
|
80
|
+
this.addChild(this.search);
|
|
81
|
+
this.list = this.makeList(items, selectedIndex);
|
|
70
82
|
this.addChild(this.list);
|
|
71
83
|
}
|
|
72
84
|
handleInput(data) {
|
|
73
|
-
|
|
74
|
-
if (
|
|
85
|
+
const kb = getKeybindings();
|
|
86
|
+
if (kb.matches(data, "tui.select.up") ||
|
|
87
|
+
kb.matches(data, "tui.select.down") ||
|
|
88
|
+
kb.matches(data, "tui.select.confirm")) {
|
|
89
|
+
this.list.handleInput(data);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// tui.select.cancel defaults to escape + ctrl+c and the kitty CSI-u Esc
|
|
93
|
+
// form, so one keybinding check covers every cancel keypath (no hardcoded Esc,
|
|
94
|
+
// so a remapped binding is honored).
|
|
95
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
75
96
|
this.finish(undefined);
|
|
76
97
|
return;
|
|
77
98
|
}
|
|
78
|
-
this.
|
|
99
|
+
this.search.handleInput(data);
|
|
100
|
+
this.applyFilter();
|
|
101
|
+
}
|
|
102
|
+
makeList(items, selectedIndex) {
|
|
103
|
+
const list = new SelectList(items, Math.min(MAX_VISIBLE, Math.max(items.length, 1)), this.listTheme);
|
|
104
|
+
list.setSelectedIndex(selectedIndex);
|
|
105
|
+
list.onSelect = (item) => this.finish(item.value);
|
|
106
|
+
list.onCancel = () => this.finish(undefined);
|
|
107
|
+
return list;
|
|
108
|
+
}
|
|
109
|
+
applyFilter() {
|
|
110
|
+
const filtered = this.allItems.filter((item) => itemMatchesQuery(item, this.search.getValue()));
|
|
111
|
+
const previous = this.list.getSelectedItem()?.value;
|
|
112
|
+
const selectedIndex = Math.max(0, filtered.findIndex((item) => item.value === previous));
|
|
113
|
+
this.removeChild(this.list);
|
|
114
|
+
this.list = this.makeList(filtered, selectedIndex);
|
|
115
|
+
this.addChild(this.list);
|
|
79
116
|
}
|
|
80
117
|
finish(result) {
|
|
81
118
|
if (this.closed)
|