@zosmaai/pi-llm-wiki 0.10.9 → 0.11.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/CHANGELOG.md +6 -0
- package/README.de.md +35 -4
- package/README.es.md +260 -170
- package/README.fr.md +35 -4
- package/README.hi.md +35 -4
- package/README.ja.md +35 -4
- package/README.ko.md +35 -4
- package/README.md +41 -12
- package/README.pt.md +35 -4
- package/README.ru.md +35 -4
- package/README.zh.md +260 -170
- package/assets/demo.gif +0 -0
- package/dist/extensions/llm-wiki/lib/bootstrap.js +74 -0
- package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +410 -0
- package/dist/extensions/llm-wiki/lib/inject.js +65 -0
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
- package/dist/extensions/llm-wiki/lib/metadata.js +505 -0
- package/dist/extensions/llm-wiki/lib/model-command.js +85 -0
- package/dist/extensions/llm-wiki/lib/observation.js +283 -0
- package/dist/extensions/llm-wiki/lib/recall.js +875 -0
- package/dist/extensions/llm-wiki/lib/retro.js +158 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +187 -0
- package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
- package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
- package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +201 -0
- package/dist/extensions/llm-wiki/lib/tools.js +1199 -0
- package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
- package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
- package/dist/extensions/llm-wiki/lib/utils.js +353 -0
- package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
- package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
- package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
- package/dist/mcp/exec.js +121 -0
- package/dist/mcp/index.js +229 -0
- package/dist/mcp/operations.js +130 -0
- package/dist/package.json +1 -0
- package/docs/api.md +5 -2
- package/docs/architecture.md +5 -2
- package/docs/configuration.md +25 -0
- package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
- package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
- package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
- package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
- package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +593 -0
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +542 -0
- package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
- package/extensions/llm-wiki/index.ts +22 -36
- package/extensions/llm-wiki/lib/bootstrap.ts +87 -0
- package/extensions/llm-wiki/lib/embeddings.ts +9 -3
- package/extensions/llm-wiki/lib/guardrails.ts +26 -18
- package/extensions/llm-wiki/lib/indexing.ts +2 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +304 -28
- package/extensions/llm-wiki/lib/knowledge-document.ts +663 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
- package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
- package/extensions/llm-wiki/lib/metadata.ts +550 -128
- package/extensions/llm-wiki/lib/model-command.ts +0 -1
- package/extensions/llm-wiki/lib/observation.ts +37 -43
- package/extensions/llm-wiki/lib/recall.ts +61 -33
- package/extensions/llm-wiki/lib/retro.ts +65 -41
- package/extensions/llm-wiki/lib/runtime.ts +0 -3
- package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
- package/extensions/llm-wiki/lib/source-packet.ts +45 -32
- package/extensions/llm-wiki/lib/task-config.ts +36 -0
- package/extensions/llm-wiki/lib/tools.ts +413 -342
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +127 -131
- package/extensions/llm-wiki/lib/vault-format.ts +363 -0
- package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
- package/mcp/exec.ts +122 -0
- package/mcp/index.ts +60 -250
- package/mcp/operations.ts +176 -0
- package/package.json +8 -2
- package/scripts/migrate-llm-wiki.js +801 -0
- package/skills/llm-wiki/SKILL.md +12 -8
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readJson } from "./utils.js";
|
|
4
|
+
import { compareCodePoint, discoverKnowledgeDocuments, inspectVaultFormat, } from "./vault-format.js";
|
|
5
|
+
/**
|
|
6
|
+
* Search the registry for matching concepts.
|
|
7
|
+
*
|
|
8
|
+
* Matches ID, semantic title, type, category, domain, tags, aliases, and recall triggers.
|
|
9
|
+
* Preserves unknown types as strings.
|
|
10
|
+
*/
|
|
11
|
+
export function searchRegistry(paths, query, typeFilter) {
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
const vaultState = inspectVaultFormat(paths);
|
|
14
|
+
diagnostics.push(...vaultState.diagnostics);
|
|
15
|
+
const registryPath = join(paths.meta, "registry.json");
|
|
16
|
+
if (!existsSync(registryPath)) {
|
|
17
|
+
return { matches: [], diagnostics };
|
|
18
|
+
}
|
|
19
|
+
const registry = readJson(registryPath, {
|
|
20
|
+
version: "1.0",
|
|
21
|
+
last_updated: "",
|
|
22
|
+
pages: {},
|
|
23
|
+
});
|
|
24
|
+
const normalizedQuery = query.toLowerCase();
|
|
25
|
+
const matches = [];
|
|
26
|
+
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
27
|
+
// Apply type filter
|
|
28
|
+
if (typeFilter && String(entry.type).toLowerCase() !== typeFilter.toLowerCase()) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// Check if query matches any searchable field
|
|
32
|
+
if (matchesField(id, entry, normalizedQuery)) {
|
|
33
|
+
matches.push({
|
|
34
|
+
id,
|
|
35
|
+
title: String(entry.title || id),
|
|
36
|
+
type: String(entry.type || "unknown"),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Sort by code point order for determinism
|
|
41
|
+
matches.sort((a, b) => compareCodePoint(a.id, b.id));
|
|
42
|
+
return { matches, diagnostics };
|
|
43
|
+
}
|
|
44
|
+
function matchesField(id, entry, query) {
|
|
45
|
+
// Match ID
|
|
46
|
+
if (id.toLowerCase().includes(query))
|
|
47
|
+
return true;
|
|
48
|
+
// Match title
|
|
49
|
+
if (String(entry.title || "")
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.includes(query))
|
|
52
|
+
return true;
|
|
53
|
+
// Match type
|
|
54
|
+
if (String(entry.type || "")
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.includes(query))
|
|
57
|
+
return true;
|
|
58
|
+
// Match category/domain
|
|
59
|
+
if (String(entry.category || "")
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.includes(query))
|
|
62
|
+
return true;
|
|
63
|
+
if (String(entry.domain || "")
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.includes(query))
|
|
66
|
+
return true;
|
|
67
|
+
// Match tags (array or string)
|
|
68
|
+
const tags = entry.tags;
|
|
69
|
+
if (Array.isArray(tags)) {
|
|
70
|
+
if (tags.some((t) => String(t).toLowerCase().includes(query)))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
else if (typeof tags === "string" && tags.toLowerCase().includes(query)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// Match aliases (array or string)
|
|
77
|
+
const aliases = entry.aliases;
|
|
78
|
+
if (Array.isArray(aliases)) {
|
|
79
|
+
if (aliases.some((a) => String(a).toLowerCase().includes(query)))
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
else if (typeof aliases === "string" && aliases.toLowerCase().includes(query)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Match recall_triggers (array or string)
|
|
86
|
+
const triggers = entry.recall_triggers;
|
|
87
|
+
if (Array.isArray(triggers)) {
|
|
88
|
+
if (triggers.some((t) => String(t).toLowerCase().includes(query)))
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
else if (typeof triggers === "string" && triggers.toLowerCase().includes(query)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get a status snapshot of the wiki.
|
|
98
|
+
*
|
|
99
|
+
* Reports resolved knowledge_format, page counts, and blocking diagnostics.
|
|
100
|
+
*/
|
|
101
|
+
export function getWikiStatus(paths) {
|
|
102
|
+
const vaultState = inspectVaultFormat(paths);
|
|
103
|
+
const diagnostics = [...vaultState.diagnostics];
|
|
104
|
+
// Also check discovery for current concept health
|
|
105
|
+
const discovery = discoverKnowledgeDocuments(paths);
|
|
106
|
+
diagnostics.push(...discovery.diagnostics);
|
|
107
|
+
const registryPath = join(paths.meta, "registry.json");
|
|
108
|
+
let registry;
|
|
109
|
+
if (existsSync(registryPath)) {
|
|
110
|
+
registry = readJson(registryPath, { version: "1.0", last_updated: "", pages: {} });
|
|
111
|
+
}
|
|
112
|
+
const byType = {};
|
|
113
|
+
let totalPages = 0;
|
|
114
|
+
if (registry) {
|
|
115
|
+
for (const entry of Object.values(registry.pages)) {
|
|
116
|
+
totalPages++;
|
|
117
|
+
const type = String(entry.type || "unknown");
|
|
118
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
knowledgeFormat: vaultState.knowledgeFormat,
|
|
123
|
+
totalPages,
|
|
124
|
+
byType,
|
|
125
|
+
blockingDiagnostics: diagnostics.filter((d) => d.severity === "error"),
|
|
126
|
+
lastUpdated: registry?.last_updated || "",
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/mcp/exec.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
3
|
+
export function createExecApi() {
|
|
4
|
+
return {
|
|
5
|
+
exec(command, args, options = {}) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let killed = false;
|
|
8
|
+
let settled = false;
|
|
9
|
+
let stdoutLimited = false;
|
|
10
|
+
let stderrLimited = false;
|
|
11
|
+
let forceTimer;
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
const child = spawn(command, args, {
|
|
15
|
+
cwd: options.cwd,
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
detached: process.platform !== "win32",
|
|
18
|
+
});
|
|
19
|
+
const sendSignal = (signal) => {
|
|
20
|
+
if (process.platform === "win32" && child.pid) {
|
|
21
|
+
execFile("taskkill", ["/pid", String(child.pid), "/T", "/F"], () => { });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (child.pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(-child.pid, signal);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Fall back to the direct child when process-group signalling fails.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
child.kill(signal);
|
|
34
|
+
};
|
|
35
|
+
const forceStop = () => {
|
|
36
|
+
if (forceTimer)
|
|
37
|
+
clearTimeout(forceTimer);
|
|
38
|
+
forceTimer = undefined;
|
|
39
|
+
sendSignal("SIGKILL");
|
|
40
|
+
};
|
|
41
|
+
const stop = () => {
|
|
42
|
+
if (killed)
|
|
43
|
+
return;
|
|
44
|
+
killed = true;
|
|
45
|
+
sendSignal("SIGTERM");
|
|
46
|
+
if (process.platform !== "win32")
|
|
47
|
+
forceTimer = setTimeout(forceStop, 100);
|
|
48
|
+
};
|
|
49
|
+
const appendOutput = (current, chunk, limited) => {
|
|
50
|
+
if (limited)
|
|
51
|
+
return [current, true];
|
|
52
|
+
const remaining = MAX_OUTPUT_BYTES - Buffer.byteLength(current);
|
|
53
|
+
if (remaining <= 0) {
|
|
54
|
+
stop();
|
|
55
|
+
return [current, true];
|
|
56
|
+
}
|
|
57
|
+
const bytes = Buffer.from(chunk);
|
|
58
|
+
if (bytes.byteLength <= remaining)
|
|
59
|
+
return [current + chunk, false];
|
|
60
|
+
stop();
|
|
61
|
+
let prefix = "";
|
|
62
|
+
let prefixBytes = 0;
|
|
63
|
+
for (const char of chunk) {
|
|
64
|
+
const charBytes = Buffer.byteLength(char);
|
|
65
|
+
if (prefixBytes + charBytes > remaining)
|
|
66
|
+
break;
|
|
67
|
+
prefix += char;
|
|
68
|
+
prefixBytes += charBytes;
|
|
69
|
+
}
|
|
70
|
+
return [current + prefix, true];
|
|
71
|
+
};
|
|
72
|
+
child.stdout?.setEncoding("utf8");
|
|
73
|
+
child.stderr?.setEncoding("utf8");
|
|
74
|
+
child.stdout?.on("data", (chunk) => {
|
|
75
|
+
[stdout, stdoutLimited] = appendOutput(stdout, chunk, stdoutLimited);
|
|
76
|
+
});
|
|
77
|
+
child.stderr?.on("data", (chunk) => {
|
|
78
|
+
[stderr, stderrLimited] = appendOutput(stderr, chunk, stderrLimited);
|
|
79
|
+
});
|
|
80
|
+
const timer = options.timeout ? setTimeout(stop, options.timeout) : undefined;
|
|
81
|
+
const abort = () => stop();
|
|
82
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
83
|
+
const finish = (code) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
if (forceTimer) {
|
|
87
|
+
clearTimeout(forceTimer);
|
|
88
|
+
forceTimer = undefined;
|
|
89
|
+
if (child.pid) {
|
|
90
|
+
try {
|
|
91
|
+
process.kill(-child.pid, "SIGKILL");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// The process group already exited; do not signal the closed child's stale PID.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve({
|
|
100
|
+
stdout,
|
|
101
|
+
stderr,
|
|
102
|
+
code: stdoutLimited || stderrLimited ? 1 : killed && code === 0 ? 1 : code,
|
|
103
|
+
killed,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
child.once("error", () => finish(1));
|
|
107
|
+
child.once("close", (code) => finish(typeof code === "number" ? code : 1));
|
|
108
|
+
function cleanup() {
|
|
109
|
+
settled = true;
|
|
110
|
+
if (timer)
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (forceTimer)
|
|
113
|
+
clearTimeout(forceTimer);
|
|
114
|
+
options.signal?.removeEventListener("abort", abort);
|
|
115
|
+
}
|
|
116
|
+
if (options.signal?.aborted)
|
|
117
|
+
stop();
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* LLM Wiki MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes wiki tools over the Model Context Protocol (MCP).
|
|
6
|
+
* Run: node dist/mcp/index.js
|
|
7
|
+
*
|
|
8
|
+
* Environment:
|
|
9
|
+
* WIKI_ROOT — path to wiki vault (default: auto-detect from cwd)
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { McpServer, StdioServerTransport } from "@modelcontextprotocol/server";
|
|
14
|
+
import * as z from "zod/v4";
|
|
15
|
+
import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
16
|
+
import { createExecApi } from "./exec.js";
|
|
17
|
+
import { captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
|
|
18
|
+
const execApi = createExecApi();
|
|
19
|
+
// ─── Vault Detection ────────────────────────────────────
|
|
20
|
+
/** Resolve vault paths, same as Pi extension. */
|
|
21
|
+
function getPaths() {
|
|
22
|
+
const root = process.env.WIKI_ROOT || process.cwd();
|
|
23
|
+
return resolveVaultPaths(root);
|
|
24
|
+
}
|
|
25
|
+
function hasVault() {
|
|
26
|
+
const paths = getPaths();
|
|
27
|
+
return existsSync(join(paths.dotWiki, "config.json"));
|
|
28
|
+
}
|
|
29
|
+
// ─── MCP Server ─────────────────────────────────────────
|
|
30
|
+
const server = new McpServer({
|
|
31
|
+
name: "llm-wiki",
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
});
|
|
34
|
+
// ---- wiki_recall ----
|
|
35
|
+
server.registerTool("wiki_recall", {
|
|
36
|
+
description: "Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
query: z.string().describe("Search query — use the user's full request or key terms"),
|
|
39
|
+
max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
|
|
40
|
+
}),
|
|
41
|
+
}, async ({ query, max_results }) => {
|
|
42
|
+
if (!hasVault()) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const paths = getPaths();
|
|
54
|
+
const result = await recallOperation(paths, query, Math.min(max_results ?? 5, 10));
|
|
55
|
+
const detailLines = result.diagnostics.map((d) => `⚠️ ${d.code}: ${d.message}`);
|
|
56
|
+
const detailText = detailLines.length > 0 ? `\n\n${detailLines.join("\n")}` : "";
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: JSON.stringify(result.results, null, 2) + detailText,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
// ---- wiki_search ----
|
|
67
|
+
server.registerTool("wiki_search", {
|
|
68
|
+
description: "Search the wiki registry for pages matching a query.",
|
|
69
|
+
inputSchema: z.object({
|
|
70
|
+
query: z.string().describe("Search term"),
|
|
71
|
+
type: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Filter by page type (source, entity, concept, synthesis, analysis)"),
|
|
75
|
+
}),
|
|
76
|
+
}, async ({ query, type }) => {
|
|
77
|
+
if (!hasVault()) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const paths = getPaths();
|
|
89
|
+
const result = await searchOperation(paths, query, type);
|
|
90
|
+
if (result.matches.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: `No pages found for "${query}"` }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: JSON.stringify(result.matches, null, 2),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
// ---- wiki_status ----
|
|
105
|
+
server.registerTool("wiki_status", {
|
|
106
|
+
description: "Show wiki health and stats: page counts, orphans, recent activity.",
|
|
107
|
+
inputSchema: z.object({}),
|
|
108
|
+
}, async () => {
|
|
109
|
+
if (!hasVault()) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const paths = getPaths();
|
|
121
|
+
const status = await statusOperation(paths);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: JSON.stringify(status, null, 2),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
// ---- wiki_retro ----
|
|
132
|
+
server.registerTool("wiki_retro", {
|
|
133
|
+
description: "Save an atomic insight from a completed task into the wiki. Creates a source page.",
|
|
134
|
+
inputSchema: z.object({
|
|
135
|
+
slug: z.string().describe("Unique kebab-case identifier (e.g. 'jwt-revocation-pattern')"),
|
|
136
|
+
title: z.string().describe("Short descriptive title (60 chars max)"),
|
|
137
|
+
body: z.string().describe("Markdown body explaining what was learned."),
|
|
138
|
+
category: z
|
|
139
|
+
.string()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Category (e.g. frontend, architecture, devops, bugfix)"),
|
|
142
|
+
}),
|
|
143
|
+
}, async ({ slug, title, body, category }) => {
|
|
144
|
+
if (!hasVault()) {
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const paths = getPaths();
|
|
156
|
+
const result = await retroOperation(paths, slug, title, body, category);
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: `Vault error: ${result.diagnostics[0].message}`,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: `Insight saved: ${result.slug} — ${title}`,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
// ---- wiki_capture_source ----
|
|
178
|
+
server.registerTool("wiki_capture_source", {
|
|
179
|
+
description: "Capture a URL, local file, or pasted text into an immutable source packet.",
|
|
180
|
+
inputSchema: z.object({
|
|
181
|
+
text: z.string().optional().describe("Text content to capture"),
|
|
182
|
+
url: z.string().optional().describe("URL to capture"),
|
|
183
|
+
file_path: z.string().optional().describe("Local file path to capture"),
|
|
184
|
+
title: z.string().optional().describe("Title for the captured source"),
|
|
185
|
+
}),
|
|
186
|
+
}, async ({ text, url, file_path, title }) => {
|
|
187
|
+
if (!hasVault()) {
|
|
188
|
+
return {
|
|
189
|
+
content: [
|
|
190
|
+
{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
isError: true,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const paths = getPaths();
|
|
199
|
+
const result = await captureSourceOperation(paths, { text, url, filePath: file_path, title }, execApi);
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
return {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: result.diagnostics[0].message,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
isError: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: `Source captured: ${result.sourceId}`,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
// ─── Main ───────────────────────────────────────────────
|
|
221
|
+
async function main() {
|
|
222
|
+
const transport = new StdioServerTransport();
|
|
223
|
+
await server.connect(transport);
|
|
224
|
+
console.error("🧠 LLM Wiki MCP Server running on stdio");
|
|
225
|
+
}
|
|
226
|
+
main().catch((err) => {
|
|
227
|
+
console.error("MCP Server error:", err);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP operation adapters over shared wiki services.
|
|
3
|
+
*
|
|
4
|
+
* Each operation is a thin, testable wrapper around the same services
|
|
5
|
+
* used by Pi tools. No operation parses YAML, scans files, scores
|
|
6
|
+
* registry entries, or builds page strings itself.
|
|
7
|
+
*/
|
|
8
|
+
import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
|
|
9
|
+
import { searchWiki } from "../extensions/llm-wiki/lib/recall.js";
|
|
10
|
+
import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
|
|
11
|
+
import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
|
|
12
|
+
import { VaultWriteError, inspectVaultFormat, inspectWritableVault, } from "../extensions/llm-wiki/lib/vault-format.js";
|
|
13
|
+
import { getWikiStatus, searchRegistry } from "../extensions/llm-wiki/lib/wiki-service.js";
|
|
14
|
+
function projectionOutcome(projection) {
|
|
15
|
+
return projection.ok
|
|
16
|
+
? { ok: true }
|
|
17
|
+
: {
|
|
18
|
+
ok: false,
|
|
19
|
+
diagnostics: projection.diagnostics.map(({ code, message }) => ({ code, message })),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Shared recall operation: calls searchWiki and appends vault diagnostics. */
|
|
23
|
+
export async function recallOperation(paths, query, maxResults = 5) {
|
|
24
|
+
const results = searchWiki(paths, query, maxResults);
|
|
25
|
+
const vaultState = inspectVaultFormat(paths);
|
|
26
|
+
return {
|
|
27
|
+
results,
|
|
28
|
+
diagnostics: vaultState.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Shared search operation: delegates directly to wiki-service. */
|
|
32
|
+
export async function searchOperation(paths, query, type) {
|
|
33
|
+
const result = searchRegistry(paths, query, type);
|
|
34
|
+
return {
|
|
35
|
+
matches: result.matches,
|
|
36
|
+
diagnostics: result.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Shared status operation: delegates directly to wiki-service. */
|
|
40
|
+
export async function statusOperation(paths) {
|
|
41
|
+
const status = getWikiStatus(paths);
|
|
42
|
+
return {
|
|
43
|
+
knowledgeFormat: status.knowledgeFormat,
|
|
44
|
+
totalPages: status.totalPages,
|
|
45
|
+
byType: status.byType,
|
|
46
|
+
blockingDiagnostics: status.blockingDiagnostics.map((d) => ({
|
|
47
|
+
code: d.code,
|
|
48
|
+
message: d.message,
|
|
49
|
+
})),
|
|
50
|
+
lastUpdated: status.lastUpdated,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Shared retro operation: validates vault then delegates to saveInsight. */
|
|
54
|
+
export async function retroOperation(paths, slug, title, body, category) {
|
|
55
|
+
const vaultCheck = inspectWritableVault(paths);
|
|
56
|
+
if (!vaultCheck.ok) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const result = saveInsight(paths, slug, title, body, category, { rebuild: false });
|
|
64
|
+
const projection = projectionOutcome(rebuildMetadata(paths));
|
|
65
|
+
if (!projection.ok)
|
|
66
|
+
return projection;
|
|
67
|
+
return { ok: true, slug: result.slug, sourcePagePath: result.sourcePagePath };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof VaultWriteError) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
diagnostics: error.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (error.message.startsWith("Invalid insight slug:")) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
diagnostics: [{ code: "invalid_insight_slug", message: error.message }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Shared capture operation: validates vault then delegates to capture functions. */
|
|
86
|
+
export async function captureSourceOperation(paths, input, execApi) {
|
|
87
|
+
const vaultCheck = inspectWritableVault(paths);
|
|
88
|
+
if (!vaultCheck.ok) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
let sourceId;
|
|
96
|
+
if (input.url) {
|
|
97
|
+
sourceId = (await captureUrl(execApi, paths, input.url)).sourceId;
|
|
98
|
+
}
|
|
99
|
+
else if (input.filePath) {
|
|
100
|
+
sourceId = (await captureFile(execApi, paths, input.filePath)).sourceId;
|
|
101
|
+
}
|
|
102
|
+
else if (input.text) {
|
|
103
|
+
sourceId = captureText(paths, input.text, input.title).sourceId;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
diagnostics: [
|
|
109
|
+
{
|
|
110
|
+
code: "event_missing_kind",
|
|
111
|
+
message: "Provide one of: text, url, or filePath",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const projection = projectionOutcome(rebuildMetadata(paths));
|
|
117
|
+
if (!projection.ok)
|
|
118
|
+
return projection;
|
|
119
|
+
return { ok: true, sourceId };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof VaultWriteError) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
diagnostics: error.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
package/docs/api.md
CHANGED
|
@@ -280,6 +280,8 @@ Health is `"⚠️ Warning"` when orphan count exceeds 5, `"🔴 Empty"` when th
|
|
|
280
280
|
Force a full synchronous rebuild of all generated metadata: `registry.json`, `backlinks.json`,
|
|
281
281
|
`index.md`, `log.md`. Use when metadata appears out of sync with actual wiki files.
|
|
282
282
|
|
|
283
|
+
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
|
+
|
|
283
285
|
**Parameters**
|
|
284
286
|
|
|
285
287
|
None.
|
|
@@ -294,8 +296,9 @@ details: { pageCount: number }
|
|
|
294
296
|
|
|
295
297
|
## wiki_log_event
|
|
296
298
|
|
|
297
|
-
Append a structured event to `meta/events.jsonl` and regenerate
|
|
298
|
-
|
|
299
|
+
Append a structured event to the authoritative, append-only `meta/events.jsonl` stream and regenerate available log projections. Every event is timestamped automatically. The event stream must be preserved in full-vault backups; generated Markdown logs cannot reconstruct it.
|
|
300
|
+
|
|
301
|
+
`details` is user-controlled and may appear in OKF-mode `wiki/log.md`. Do not include secrets, credentials, or private machine-local paths. Built-in local-file capture records its stable source ID and format in events, omitting the caller-supplied input path from the event stream and generated log projections. The exact path remains in the extension-owned raw manifest. Other wiki projections are not covered by this guarantee: extraction-failure text and source-page titles may still surface path-derived strings, so treat the source page area as potentially machine-local until a dedicated sanitization pass exists.
|
|
299
302
|
|
|
300
303
|
**Parameters**
|
|
301
304
|
|
package/docs/architecture.md
CHANGED
|
@@ -55,7 +55,7 @@ WIKI_ROOT/
|
|
|
55
55
|
│ ├── analyses/ # Durable query answers
|
|
56
56
|
│ ├── cases/ # One specific past task per trajectory
|
|
57
57
|
│ └── skills/ # Reusable patterns distilled from trajectories
|
|
58
|
-
├── meta/ #
|
|
58
|
+
├── meta/ # Durable event source + generated internal projections
|
|
59
59
|
│ ├── registry.json # Master page catalog
|
|
60
60
|
│ ├── backlinks.json # Inbound link map
|
|
61
61
|
│ ├── index.md # Human-readable catalog
|
|
@@ -73,9 +73,12 @@ WIKI_ROOT/
|
|
|
73
73
|
| --------------------- | ------------------------ | ------------------------ |
|
|
74
74
|
| `.llm-wiki/raw/**` | Extension | Immutable after capture |
|
|
75
75
|
| `.llm-wiki/wiki/**` | Model + user | Editable knowledge pages |
|
|
76
|
-
| `.llm-wiki/meta
|
|
76
|
+
| `.llm-wiki/meta/events.jsonl` | Extension tools | Authoritative, append-only; preserve in full-vault backups |
|
|
77
|
+
| `.llm-wiki/meta/**` except `events.jsonl` | Extension | Generated projections |
|
|
77
78
|
| `.llm-wiki/` | Human + explicit request | Operating rules |
|
|
78
79
|
|
|
80
|
+
`events.jsonl` records selected extension operations, not every filesystem edit. `meta/log.md` and OKF-mode `wiki/log.md` are one-way projections; neither can recover the event stream.
|
|
81
|
+
|
|
79
82
|
## Source Packet Format
|
|
80
83
|
|
|
81
84
|
Each captured source becomes a packet:
|
package/docs/configuration.md
CHANGED
|
@@ -32,6 +32,31 @@ The personal vault lives at `~/.llm-wiki/` (or `$WIKI_HOME`) and is always avail
|
|
|
32
32
|
| `WIKI_HOME` | `~/.llm-wiki` | Override the personal wiki vault location |
|
|
33
33
|
| `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
|
|
34
34
|
|
|
35
|
+
## Pi Agent Settings
|
|
36
|
+
|
|
37
|
+
Runtime settings for the wiki's background tasks live in `.pi/settings.json` under the `llm-wiki` namespace. These can be set globally (`~/.pi/agent/settings.json`) or per-project (`<cwd>/.pi/settings.json`).
|
|
38
|
+
|
|
39
|
+
| Setting | Default | Description |
|
|
40
|
+
| --------------------- | ------- | ------------------------------------------------------------ |
|
|
41
|
+
| `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
|
|
42
|
+
| `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
|
|
43
|
+
| `trajectories` | false | Enable agent-trajectory working-memory |
|
|
44
|
+
| `notices` | true | Show wiki activity notices in chat |
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"llm-wiki": {
|
|
51
|
+
"synthesisLanguage": "ru",
|
|
52
|
+
"taskModel": {
|
|
53
|
+
"provider": "openai",
|
|
54
|
+
"id": "gpt-4o"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
35
60
|
## Vault Resolution
|
|
36
61
|
|
|
37
62
|
The vault root is resolved in this priority order:
|