@zosmaai/pi-llm-wiki 0.5.0 → 0.6.0
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 +13 -0
- package/extensions/llm-wiki/index.ts +39 -12
- package/extensions/llm-wiki/lib/recall.ts +208 -0
- package/extensions/llm-wiki/lib/retro.ts +203 -0
- package/mcp/index.ts +463 -0
- package/package.json +9 -2
- package/skills/llm-wiki/SKILL.md +45 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-05-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Phase 1 — Auto-recall** (PR #19 by @arjun-zosma): New `wiki_recall` tool for explicit searches. Extension now auto-searches wiki before every user turn via `before_agent_start` hook. Matching pages injected as "Relevant Wiki Knowledge" into system prompt. 8 new tests.
|
|
9
|
+
- **Phase 2 — Auto-capture** (PR #20 by @arjun-zosma): New `wiki_retro` tool for saving atomic insights from completed tasks. Creates source packets with manifest, extracted text, and source page. 4 new tests.
|
|
10
|
+
- **Phase 3 — MCP Server** (PR #21 by @arjun-zosma): Standalone MCP server using `@modelcontextprotocol/server` (v2 SDK) with stdio transport. Exposes 5 tools: wiki_recall, wiki_search, wiki_status, wiki_retro, wiki_capture_source. Cross-platform reach to Claude Code, Cursor, Windsurf.
|
|
11
|
+
- **12 extension tools** (up from 10): wiki_recall (auto at turn start) and wiki_retro (manual at task end)
|
|
12
|
+
- **SKILL.md**: Auto-Recall section, wiki_recall + wiki_retro tool docs, "Task → Capture → Retro" workflow
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Extension registers 12 tools instead of 10
|
|
16
|
+
- Status bar now shows "12 tools, auto-recall active"
|
|
17
|
+
|
|
5
18
|
## [0.5.0] - 2026-05-11
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
4
|
import { installGuardrails } from "./lib/guardrails.js";
|
|
5
|
+
import { formatRecallContext, registerWikiRecall, searchWiki } from "./lib/recall.js";
|
|
6
|
+
import { registerWikiRetro } from "./lib/retro.js";
|
|
3
7
|
import {
|
|
4
8
|
registerWikiBootstrap,
|
|
5
9
|
registerWikiCaptureSource,
|
|
@@ -12,25 +16,22 @@ import {
|
|
|
12
16
|
registerWikiStatus,
|
|
13
17
|
registerWikiWatch,
|
|
14
18
|
} from "./lib/tools.js";
|
|
19
|
+
import { getVaultPaths, resolveVaultRoot } from "./lib/utils.js";
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* @zosmaai/pi-llm-wiki — LLM Wiki extension for Pi
|
|
18
23
|
*
|
|
19
|
-
* Registers
|
|
20
|
-
* -
|
|
21
|
-
* - wiki_capture_source Capture URL/file/text into source packet
|
|
22
|
-
* - wiki_ingest Get batch of sources needing synthesis
|
|
23
|
-
* - wiki_ensure_page Create canonical page from template
|
|
24
|
-
* - wiki_search Search generated registry
|
|
25
|
-
* - wiki_lint Health check with auto-fix
|
|
26
|
-
* - wiki_status Instant stats from registry
|
|
27
|
-
* - wiki_rebuild_meta Force metadata rebuild
|
|
28
|
-
* - wiki_log_event Append event and regenerate log
|
|
29
|
-
* - wiki_watch Schedule auto-updates
|
|
24
|
+
* Registers 11 custom tools and installs guardrails:
|
|
25
|
+
* All 10 original tools + wiki_recall (auto-recall at session start)
|
|
30
26
|
*
|
|
31
27
|
* Guardrails:
|
|
32
28
|
* - Blocks direct edits to raw/** and meta/**
|
|
33
29
|
* - Auto-rebuilds metadata after wiki/** edits
|
|
30
|
+
*
|
|
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
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
export default function (pi: ExtensionAPI) {
|
|
@@ -44,10 +45,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
44
45
|
registerWikiRebuildMeta(pi);
|
|
45
46
|
registerWikiLogEvent(pi);
|
|
46
47
|
registerWikiWatch(pi);
|
|
48
|
+
registerWikiRecall(pi);
|
|
49
|
+
registerWikiRetro(pi);
|
|
47
50
|
|
|
48
51
|
installGuardrails(pi);
|
|
49
52
|
|
|
50
53
|
pi.on("session_start", async (_event, ctx) => {
|
|
51
|
-
ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (
|
|
54
|
+
ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (11 tools, auto-recall active)");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── Auto-recall hook ──────────────────────────────
|
|
58
|
+
// Before each agent turn, search the wiki for pages relevant
|
|
59
|
+
// to the user's prompt and inject them as system context.
|
|
60
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
61
|
+
const root = resolveVaultRoot(process.cwd());
|
|
62
|
+
if (!existsSync(join(root, ".wiki", "config.json"))) {
|
|
63
|
+
return; // No wiki vault — nothing to recall
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const paths = getVaultPaths(root);
|
|
67
|
+
const prompt = event.prompt || "";
|
|
68
|
+
if (!prompt.trim()) return;
|
|
69
|
+
|
|
70
|
+
const results = searchWiki(paths, prompt);
|
|
71
|
+
if (results.length === 0) return;
|
|
72
|
+
|
|
73
|
+
const context = formatRecallContext(results);
|
|
74
|
+
if (!context) return;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
systemPrompt: `${event.systemPrompt}\n\n${context}`,
|
|
78
|
+
};
|
|
52
79
|
});
|
|
53
80
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import type { Registry } from "./metadata.js";
|
|
6
|
+
import { type VaultPaths, getVaultPaths, readJson, readText, resolveVaultRoot } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
// ─── Public API ────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface RecallResult {
|
|
11
|
+
/** Page identifier (folder-qualified, e.g. "concepts/rag") */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Page title */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Page type: source, entity, concept, synthesis, analysis */
|
|
16
|
+
type: string;
|
|
17
|
+
/** First N chars of page content for context */
|
|
18
|
+
preview: string;
|
|
19
|
+
/** Relative path from wiki root */
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Search the wiki registry for pages matching a query.
|
|
25
|
+
* Returns up to `maxResults` matches, each with a content preview.
|
|
26
|
+
*/
|
|
27
|
+
export function searchWiki(paths: VaultPaths, query: string, maxResults = 5): RecallResult[] {
|
|
28
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
29
|
+
version: "1.0",
|
|
30
|
+
last_updated: "",
|
|
31
|
+
pages: {},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const q = query.toLowerCase();
|
|
35
|
+
const terms = q
|
|
36
|
+
.split(/\s+/)
|
|
37
|
+
.filter((t) => t.length > 2)
|
|
38
|
+
.slice(0, 10);
|
|
39
|
+
|
|
40
|
+
if (terms.length === 0) return [];
|
|
41
|
+
|
|
42
|
+
type Scored = { id: string; entry: Registry["pages"][string]; score: number };
|
|
43
|
+
const scored: Scored[] = [];
|
|
44
|
+
|
|
45
|
+
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
46
|
+
let score = 0;
|
|
47
|
+
const title = String(entry.title || "").toLowerCase();
|
|
48
|
+
const type = String(entry.type || "").toLowerCase();
|
|
49
|
+
|
|
50
|
+
for (const term of terms) {
|
|
51
|
+
if (id.toLowerCase().includes(term)) score += 3;
|
|
52
|
+
if (title.includes(term)) score += 4;
|
|
53
|
+
if (type.includes(term)) score += 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Boost if query terms appear in tags/categories
|
|
57
|
+
const tags = String(entry.tags || entry.category || entry.domain || "");
|
|
58
|
+
for (const term of terms) {
|
|
59
|
+
if (tags.toLowerCase().includes(term)) score += 2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (score > 0) {
|
|
63
|
+
scored.push({ id, entry, score });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
scored.sort((a, b) => b.score - a.score);
|
|
68
|
+
const top = scored.slice(0, maxResults);
|
|
69
|
+
|
|
70
|
+
return top.map(({ id, entry }) => {
|
|
71
|
+
// Try to read page content for preview
|
|
72
|
+
let preview = "";
|
|
73
|
+
const pagePath = join(paths.wiki, `${id}.md`);
|
|
74
|
+
if (existsSync(pagePath)) {
|
|
75
|
+
const content = readFileSync(pagePath, "utf-8");
|
|
76
|
+
// Strip frontmatter
|
|
77
|
+
const body = content.replace(/^---[\s\S]*?---\n/, "").trim();
|
|
78
|
+
preview = body.slice(0, 200).replace(/\n/g, " ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
title: String(entry.title || id),
|
|
84
|
+
type: String(entry.type || "page"),
|
|
85
|
+
preview,
|
|
86
|
+
path: pagePath,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format recall results as a compact system-prompt section.
|
|
93
|
+
*/
|
|
94
|
+
export function formatRecallContext(results: RecallResult[]): string {
|
|
95
|
+
if (results.length === 0) return "";
|
|
96
|
+
|
|
97
|
+
const lines: string[] = [
|
|
98
|
+
"## Relevant Wiki Knowledge",
|
|
99
|
+
"",
|
|
100
|
+
`_${results.length} page(s) matched your query — reviewed automatically by LLM Wiki._`,
|
|
101
|
+
"",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const r of results) {
|
|
105
|
+
lines.push(`- **[[${r.id}]]** — *${r.type}* — ${r.title}`);
|
|
106
|
+
if (r.preview) {
|
|
107
|
+
// Truncate preview to one line
|
|
108
|
+
const preview = r.preview.length > 120 ? `${r.preview.slice(0, 120)}…` : r.preview;
|
|
109
|
+
lines.push(` ${preview}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
lines.push(
|
|
115
|
+
"Use `read` to view full pages. Add new findings via wiki_ensure_page or wiki_retro.",
|
|
116
|
+
"",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Tool Registration ──────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Register the `wiki_recall` tool.
|
|
126
|
+
* The model can call this explicitly to search the wiki.
|
|
127
|
+
* It is also called automatically via before_agent_start hook.
|
|
128
|
+
*/
|
|
129
|
+
export function registerWikiRecall(pi: ExtensionAPI): void {
|
|
130
|
+
pi.registerTool({
|
|
131
|
+
name: "wiki_recall",
|
|
132
|
+
label: "Wiki Recall",
|
|
133
|
+
description:
|
|
134
|
+
"Search the wiki for pages relevant to a query. " +
|
|
135
|
+
"Returns matching page IDs, titles, types, and content previews. " +
|
|
136
|
+
"Called automatically at session start — use explicitly to dig deeper.",
|
|
137
|
+
promptSnippet: "Recall wiki knowledge relevant to the current task",
|
|
138
|
+
promptGuidelines: [
|
|
139
|
+
"Use wiki_recall at the START of every task to find relevant wiki knowledge.",
|
|
140
|
+
"The extension auto-calls wiki_recall — but calling it explicitly with specific terms gets better results.",
|
|
141
|
+
],
|
|
142
|
+
parameters: Type.Object({
|
|
143
|
+
query: Type.String({
|
|
144
|
+
description: "Search query — use the user's full request or key terms",
|
|
145
|
+
}),
|
|
146
|
+
max_results: Type.Optional(
|
|
147
|
+
Type.Number({ description: "Max results (default: 5, max: 10)", default: 5 }),
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
async execute(_toolCallId, params) {
|
|
151
|
+
const root = resolveVaultRoot(process.cwd());
|
|
152
|
+
const paths = getVaultPaths(root);
|
|
153
|
+
|
|
154
|
+
if (!existsSync(join(root, ".wiki", "config.json"))) {
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: "No wiki vault found at this location. Initialize one with wiki_bootstrap first.",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
details: { error: "no_vault" } as Record<string, unknown>,
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const maxResults = Math.min(params.max_results ?? 5, 10);
|
|
168
|
+
const results = searchWiki(paths, params.query, maxResults);
|
|
169
|
+
|
|
170
|
+
if (results.length === 0) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: `No wiki pages found matching "${params.query}". Use wiki_search for broader results.`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
details: { query: params.query, matches: [] } as Record<string, unknown>,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: [
|
|
187
|
+
`🧠 **${results.length} wiki page(s) relevant** to "${params.query}":`,
|
|
188
|
+
"",
|
|
189
|
+
...results.map(
|
|
190
|
+
(r) =>
|
|
191
|
+
`- [[${r.id}]] — *${r.type}* — ${r.title}${
|
|
192
|
+
r.preview ? `\n > ${r.preview.slice(0, 150)}` : ""
|
|
193
|
+
}`,
|
|
194
|
+
),
|
|
195
|
+
"",
|
|
196
|
+
"Use `read` on any page for full content.",
|
|
197
|
+
"Use `wiki_retro` to save new insights from this task.",
|
|
198
|
+
].join("\n"),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
details: { query: params.query, matches: results.map((r) => r.id) } as Record<
|
|
202
|
+
string,
|
|
203
|
+
unknown
|
|
204
|
+
>,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { appendEvent, rebuildMetadataLight } from "./metadata.js";
|
|
6
|
+
import {
|
|
7
|
+
type VaultPaths,
|
|
8
|
+
fmtDate,
|
|
9
|
+
getVaultPaths,
|
|
10
|
+
nextSourceId,
|
|
11
|
+
resolveVaultRoot,
|
|
12
|
+
} from "./utils.js";
|
|
13
|
+
|
|
14
|
+
// ─── Public API ────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface RetroResult {
|
|
17
|
+
sourceId: string;
|
|
18
|
+
packetPath: string;
|
|
19
|
+
sourcePagePath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save an atomic insight into the wiki as a source packet + source page.
|
|
24
|
+
* Returns the source ID, packet path, and source page path.
|
|
25
|
+
*/
|
|
26
|
+
export function saveInsight(
|
|
27
|
+
paths: VaultPaths,
|
|
28
|
+
slug: string,
|
|
29
|
+
title: string,
|
|
30
|
+
body: string,
|
|
31
|
+
category?: string,
|
|
32
|
+
): RetroResult {
|
|
33
|
+
const sourceId = nextSourceId(paths);
|
|
34
|
+
const packetPath = join(paths.rawSources, sourceId);
|
|
35
|
+
mkdirSync(packetPath, { recursive: true });
|
|
36
|
+
mkdirSync(join(packetPath, "attachments"), { recursive: true });
|
|
37
|
+
|
|
38
|
+
const today = fmtDate();
|
|
39
|
+
|
|
40
|
+
// Write manifest
|
|
41
|
+
const manifest = {
|
|
42
|
+
id: sourceId,
|
|
43
|
+
title,
|
|
44
|
+
slug,
|
|
45
|
+
category: category || "uncategorized",
|
|
46
|
+
captured: today,
|
|
47
|
+
format: "insight",
|
|
48
|
+
packet_version: "1.0",
|
|
49
|
+
};
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(packetPath, "manifest.json"),
|
|
52
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
53
|
+
"utf-8",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Write extracted text (the insight body in markdown)
|
|
57
|
+
const extracted = [
|
|
58
|
+
`# ${title}`,
|
|
59
|
+
"",
|
|
60
|
+
body,
|
|
61
|
+
"",
|
|
62
|
+
"---",
|
|
63
|
+
`*Captured: ${today}*`,
|
|
64
|
+
category ? `*Category: ${category}*` : "",
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join("\n");
|
|
68
|
+
writeFileSync(join(packetPath, "extracted.md"), extracted, "utf-8");
|
|
69
|
+
|
|
70
|
+
// Create source page
|
|
71
|
+
const sourcePageDir = join(paths.wiki, "sources");
|
|
72
|
+
mkdirSync(sourcePageDir, { recursive: true });
|
|
73
|
+
const sourcePagePath = join(sourcePageDir, `${sourceId}.md`);
|
|
74
|
+
|
|
75
|
+
const tagLine = category ? `category: ${category}` : "";
|
|
76
|
+
const sourcePageContent = [
|
|
77
|
+
"---",
|
|
78
|
+
"type: source",
|
|
79
|
+
`title: "${title}"`,
|
|
80
|
+
`source_id: ${sourceId}`,
|
|
81
|
+
"status: insight",
|
|
82
|
+
`created: ${today}`,
|
|
83
|
+
`updated: ${today}`,
|
|
84
|
+
tagLine,
|
|
85
|
+
"---",
|
|
86
|
+
"",
|
|
87
|
+
`# ${title}`,
|
|
88
|
+
"",
|
|
89
|
+
body,
|
|
90
|
+
"",
|
|
91
|
+
"## Source",
|
|
92
|
+
"",
|
|
93
|
+
`- **Packet:** \`${packetPath}\``,
|
|
94
|
+
`- **Captured:** ${today}`,
|
|
95
|
+
category ? `- **Category:** ${category}` : "",
|
|
96
|
+
"",
|
|
97
|
+
"## Related",
|
|
98
|
+
"",
|
|
99
|
+
"_(Add [[wikilinks]] to related pages)_",
|
|
100
|
+
"",
|
|
101
|
+
]
|
|
102
|
+
.filter((l) => l !== "")
|
|
103
|
+
.join("\n");
|
|
104
|
+
writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
|
|
105
|
+
|
|
106
|
+
// Log event
|
|
107
|
+
appendEvent(paths, {
|
|
108
|
+
kind: "retro",
|
|
109
|
+
source_id: sourceId,
|
|
110
|
+
title,
|
|
111
|
+
slug,
|
|
112
|
+
category: category || "uncategorized",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Rebuild metadata
|
|
116
|
+
rebuildMetadataLight(paths);
|
|
117
|
+
|
|
118
|
+
return { sourceId, packetPath, sourcePagePath };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Tool Registration ──────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register the `wiki_retro` tool.
|
|
125
|
+
* The model calls this to save an atomic insight from a completed task.
|
|
126
|
+
* Inspired by the memex_retro pattern.
|
|
127
|
+
*/
|
|
128
|
+
export function registerWikiRetro(pi: ExtensionAPI): void {
|
|
129
|
+
pi.registerTool({
|
|
130
|
+
name: "wiki_retro",
|
|
131
|
+
label: "Wiki Retro",
|
|
132
|
+
description:
|
|
133
|
+
"Save an atomic insight from a completed task into the wiki. " +
|
|
134
|
+
"Creates a source packet and source page. The insight will be " +
|
|
135
|
+
"surfaced automatically by wiki_recall in future sessions.",
|
|
136
|
+
promptSnippet: "Save atomic insights from completed tasks into the wiki",
|
|
137
|
+
promptGuidelines: [
|
|
138
|
+
"Use wiki_retro at the END of every meaningful task to save what you learned.",
|
|
139
|
+
"Write atomic insights — one insight per call. Use multiple calls for multiple insights.",
|
|
140
|
+
"The insight will be auto-surfaced by wiki_recall in future sessions.",
|
|
141
|
+
],
|
|
142
|
+
parameters: Type.Object({
|
|
143
|
+
slug: Type.String({
|
|
144
|
+
description:
|
|
145
|
+
"Unique kebab-case identifier (e.g. 'jwt-revocation-pattern'). Used for lookups.",
|
|
146
|
+
}),
|
|
147
|
+
title: Type.String({
|
|
148
|
+
description: "Short descriptive title (60 chars max). Noun phrase, not a sentence.",
|
|
149
|
+
}),
|
|
150
|
+
body: Type.String({
|
|
151
|
+
description:
|
|
152
|
+
"Markdown body with [[wikilinks]] to related wiki pages. Explain what was learned.",
|
|
153
|
+
}),
|
|
154
|
+
category: Type.Optional(
|
|
155
|
+
Type.String({
|
|
156
|
+
description: "Optional category (e.g. frontend, architecture, devops, bugfix, design)",
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
}),
|
|
160
|
+
async execute(_toolCallId, params) {
|
|
161
|
+
const root = resolveVaultRoot(process.cwd());
|
|
162
|
+
const paths = getVaultPaths(root);
|
|
163
|
+
|
|
164
|
+
if (!existsSync(join(root, ".wiki", "config.json"))) {
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: "No wiki vault found at this location. Initialize one with wiki_bootstrap first.",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
details: { error: "no_vault" } as Record<string, unknown>,
|
|
173
|
+
isError: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = saveInsight(paths, params.slug, params.title, params.body, params.category);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: [
|
|
184
|
+
`🧠 **Insight saved**: ${params.title}`,
|
|
185
|
+
"",
|
|
186
|
+
`- Source: \`${result.sourceId}\``,
|
|
187
|
+
`- Packet: \`${result.packetPath}\``,
|
|
188
|
+
`- Page: \`${result.sourcePagePath}\``,
|
|
189
|
+
"",
|
|
190
|
+
"This insight will be auto-surfaced by wiki_recall in future sessions.",
|
|
191
|
+
].join("\n"),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
details: {
|
|
195
|
+
sourceId: result.sourceId,
|
|
196
|
+
slug: params.slug,
|
|
197
|
+
title: params.title,
|
|
198
|
+
category: params.category || null,
|
|
199
|
+
} as Record<string, unknown>,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
package/mcp/index.ts
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LLM Wiki MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes wiki tools over the Model Context Protocol (MCP).
|
|
7
|
+
* Run: node mcp/index.js
|
|
8
|
+
* Or via package.json: pi install npm:@zosmaai/pi-llm-wiki && node mcp/index.js
|
|
9
|
+
*
|
|
10
|
+
* Environment:
|
|
11
|
+
* WIKI_ROOT — path to wiki vault (default: auto-detect from cwd)
|
|
12
|
+
* WIKI_MARKITDOWN_TIMEOUT_MS — PDF extraction timeout (default: 180000)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
19
|
+
import * as z from "zod/v4";
|
|
20
|
+
|
|
21
|
+
// ─── Wiki Vault Detection ──────────────────────────────
|
|
22
|
+
|
|
23
|
+
interface VaultPaths {
|
|
24
|
+
root: string;
|
|
25
|
+
rawSources: string;
|
|
26
|
+
wiki: string;
|
|
27
|
+
meta: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveVaultRoot(cwd: string): string | null {
|
|
31
|
+
if (existsSync(join(cwd, ".wiki", "config.json"))) return cwd;
|
|
32
|
+
const parts = cwd.split("/");
|
|
33
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
34
|
+
const dir = parts.slice(0, i + 1).join("/") || "/";
|
|
35
|
+
if (existsSync(join(dir, ".wiki", "config.json"))) return dir;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getPaths(): VaultPaths {
|
|
41
|
+
const root = process.env.WIKI_ROOT || resolveVaultRoot(process.cwd()) || process.cwd();
|
|
42
|
+
return {
|
|
43
|
+
root,
|
|
44
|
+
rawSources: join(root, "raw", "sources"),
|
|
45
|
+
wiki: join(root, "wiki"),
|
|
46
|
+
meta: join(root, "meta"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasVault(): boolean {
|
|
51
|
+
return existsSync(join(getPaths().root, ".wiki", "config.json"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function readJson<T>(path: string, defaultVal: T): T {
|
|
57
|
+
try {
|
|
58
|
+
if (!existsSync(path)) return defaultVal;
|
|
59
|
+
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
60
|
+
} catch {
|
|
61
|
+
return defaultVal;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── MCP Server ─────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const server = new McpServer({
|
|
68
|
+
name: "llm-wiki",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ---- wiki_recall ----
|
|
73
|
+
|
|
74
|
+
server.registerTool(
|
|
75
|
+
"wiki_recall",
|
|
76
|
+
{
|
|
77
|
+
description:
|
|
78
|
+
"Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
|
|
79
|
+
inputSchema: z.object({
|
|
80
|
+
query: z.string().describe("Search query — use the user's full request or key terms"),
|
|
81
|
+
max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
async ({ query, max_results }) => {
|
|
85
|
+
if (!hasVault()) {
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text" as const,
|
|
90
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const paths = getPaths();
|
|
98
|
+
const registry = readJson<{
|
|
99
|
+
pages: Record<string, { type: string; title: string; [key: string]: unknown }>;
|
|
100
|
+
}>(join(paths.meta, "registry.json"), { pages: {} });
|
|
101
|
+
|
|
102
|
+
const terms = query
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.split(/\s+/)
|
|
105
|
+
.filter((t) => t.length > 2)
|
|
106
|
+
.slice(0, 10);
|
|
107
|
+
|
|
108
|
+
if (terms.length === 0) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text" as const, text: "Query too short." }],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type Scored = { id: string; score: number };
|
|
115
|
+
const scored: Scored[] = [];
|
|
116
|
+
|
|
117
|
+
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
118
|
+
let score = 0;
|
|
119
|
+
const title = String(entry.title || "").toLowerCase();
|
|
120
|
+
const type = String(entry.type || "").toLowerCase();
|
|
121
|
+
|
|
122
|
+
for (const term of terms) {
|
|
123
|
+
if (id.toLowerCase().includes(term)) score += 3;
|
|
124
|
+
if (title.includes(term)) score += 4;
|
|
125
|
+
if (type.includes(term)) score += 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const tags = String(entry.tags || entry.category || entry.domain || "").toLowerCase();
|
|
129
|
+
for (const term of terms) {
|
|
130
|
+
if (tags.includes(term)) score += 2;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (score > 0) scored.push({ id, score });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
scored.sort((a, b) => b.score - a.score);
|
|
137
|
+
const top = scored.slice(0, Math.min(max_results ?? 5, 10));
|
|
138
|
+
|
|
139
|
+
const results = top.map(({ id }) => {
|
|
140
|
+
const entry = registry.pages[id];
|
|
141
|
+
let preview = "";
|
|
142
|
+
const pagePath = join(paths.wiki, `${id}.md`);
|
|
143
|
+
if (existsSync(pagePath)) {
|
|
144
|
+
const content = readFileSync(pagePath, "utf-8");
|
|
145
|
+
preview = content
|
|
146
|
+
.replace(/^---[\s\S]*?---\n/, "")
|
|
147
|
+
.trim()
|
|
148
|
+
.slice(0, 200)
|
|
149
|
+
.replace(/\n/g, " ");
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
title: String(entry?.title || id),
|
|
154
|
+
type: String(entry?.type || "page"),
|
|
155
|
+
preview,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text" as const,
|
|
163
|
+
text: JSON.stringify(results, null, 2),
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// ---- wiki_search ----
|
|
171
|
+
|
|
172
|
+
server.registerTool(
|
|
173
|
+
"wiki_search",
|
|
174
|
+
{
|
|
175
|
+
description: "Search the wiki registry for pages matching a query.",
|
|
176
|
+
inputSchema: z.object({
|
|
177
|
+
query: z.string().describe("Search term"),
|
|
178
|
+
type: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Filter by page type (source, entity, concept, synthesis, analysis)"),
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
async ({ query, type }) => {
|
|
185
|
+
if (!hasVault()) {
|
|
186
|
+
return {
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: "text" as const,
|
|
190
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
isError: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const paths = getPaths();
|
|
198
|
+
const registry = readJson<{
|
|
199
|
+
pages: Record<string, { type: string; title: string; [key: string]: unknown }>;
|
|
200
|
+
}>(join(paths.meta, "registry.json"), { pages: {} });
|
|
201
|
+
|
|
202
|
+
const q = query.toLowerCase();
|
|
203
|
+
const matches = Object.entries(registry.pages)
|
|
204
|
+
.filter(([id, entry]) => {
|
|
205
|
+
const matchesQuery =
|
|
206
|
+
id.toLowerCase().includes(q) ||
|
|
207
|
+
String(entry.title).toLowerCase().includes(q) ||
|
|
208
|
+
String(entry.type).toLowerCase().includes(q);
|
|
209
|
+
const matchesType = !type || String(entry.type).toLowerCase() === type.toLowerCase();
|
|
210
|
+
return matchesQuery && matchesType;
|
|
211
|
+
})
|
|
212
|
+
.map(([id, entry]) => ({
|
|
213
|
+
id,
|
|
214
|
+
title: entry.title,
|
|
215
|
+
type: entry.type,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "text" as const,
|
|
222
|
+
text:
|
|
223
|
+
matches.length > 0 ? JSON.stringify(matches, null, 2) : `No pages found for "${query}"`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// ---- wiki_status ----
|
|
231
|
+
|
|
232
|
+
server.registerTool(
|
|
233
|
+
"wiki_status",
|
|
234
|
+
{
|
|
235
|
+
description: "Show wiki health and stats: page counts, orphans, recent activity.",
|
|
236
|
+
inputSchema: z.object({}),
|
|
237
|
+
},
|
|
238
|
+
async () => {
|
|
239
|
+
if (!hasVault()) {
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text" as const,
|
|
244
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
isError: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const paths = getPaths();
|
|
252
|
+
const registry = readJson<{
|
|
253
|
+
version: string;
|
|
254
|
+
last_updated: string;
|
|
255
|
+
pages: Record<string, { type: string; title: string; [key: string]: unknown }>;
|
|
256
|
+
}>(join(paths.meta, "registry.json"), {
|
|
257
|
+
version: "1.0",
|
|
258
|
+
last_updated: "",
|
|
259
|
+
pages: {},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const config = readJson<Record<string, unknown>>(join(paths.root, ".wiki", "config.json"), {});
|
|
263
|
+
|
|
264
|
+
const byType: Record<string, number> = {};
|
|
265
|
+
for (const entry of Object.values(registry.pages)) {
|
|
266
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: "text" as const,
|
|
273
|
+
text: JSON.stringify(
|
|
274
|
+
{
|
|
275
|
+
topic: config.topic || "Unknown",
|
|
276
|
+
mode: config.mode || "personal",
|
|
277
|
+
totalPages: Object.keys(registry.pages).length,
|
|
278
|
+
byType,
|
|
279
|
+
lastUpdated: registry.last_updated || "Never",
|
|
280
|
+
},
|
|
281
|
+
null,
|
|
282
|
+
2,
|
|
283
|
+
),
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ---- wiki_retro ----
|
|
291
|
+
|
|
292
|
+
server.registerTool(
|
|
293
|
+
"wiki_retro",
|
|
294
|
+
{
|
|
295
|
+
description:
|
|
296
|
+
"Save an atomic insight from a completed task into the wiki. Creates a source packet and source page.",
|
|
297
|
+
inputSchema: z.object({
|
|
298
|
+
slug: z.string().describe("Unique kebab-case identifier (e.g. 'jwt-revocation-pattern')"),
|
|
299
|
+
title: z.string().describe("Short descriptive title (60 chars max)"),
|
|
300
|
+
body: z
|
|
301
|
+
.string()
|
|
302
|
+
.describe(
|
|
303
|
+
"Markdown body explaining what was learned. Include [[wikilinks]] to related pages.",
|
|
304
|
+
),
|
|
305
|
+
category: z
|
|
306
|
+
.string()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe("Category (e.g. frontend, architecture, devops, bugfix)"),
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
async ({ slug, title, body, category }) => {
|
|
312
|
+
if (!hasVault()) {
|
|
313
|
+
return {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: "text" as const,
|
|
317
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
isError: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { saveInsight } = (await import("../extensions/llm-wiki/lib/retro.js")) as {
|
|
325
|
+
saveInsight: (
|
|
326
|
+
paths: Record<string, string>,
|
|
327
|
+
slug: string,
|
|
328
|
+
title: string,
|
|
329
|
+
body: string,
|
|
330
|
+
category?: string,
|
|
331
|
+
) => { sourceId: string; packetPath: string; sourcePagePath: string };
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const vaultPaths = {
|
|
335
|
+
...getPaths(),
|
|
336
|
+
raw: join(getPaths().root, "raw"),
|
|
337
|
+
dotWiki: join(getPaths().root, ".wiki"),
|
|
338
|
+
outputs: join(getPaths().root, "outputs"),
|
|
339
|
+
discoveries: join(getPaths().root, ".discoveries"),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const result = saveInsight(vaultPaths, slug, title, body, category);
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: "text" as const,
|
|
348
|
+
text: `Insight saved: ${result.sourceId} — ${title}`,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// ---- wiki_capture_source ----
|
|
356
|
+
|
|
357
|
+
server.registerTool(
|
|
358
|
+
"wiki_capture_source",
|
|
359
|
+
{
|
|
360
|
+
description: "Capture a URL, local file, or pasted text into an immutable source packet.",
|
|
361
|
+
inputSchema: z.object({
|
|
362
|
+
text: z.string().optional().describe("Text content to capture"),
|
|
363
|
+
url: z.string().optional().describe("URL to capture"),
|
|
364
|
+
file_path: z.string().optional().describe("Local file path to capture"),
|
|
365
|
+
title: z.string().optional().describe("Title for the captured source"),
|
|
366
|
+
}),
|
|
367
|
+
},
|
|
368
|
+
async ({ text, url: urlParam, file_path, title }) => {
|
|
369
|
+
if (!hasVault()) {
|
|
370
|
+
return {
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: "text" as const,
|
|
374
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
isError: true,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const vaultPaths = {
|
|
382
|
+
...getPaths(),
|
|
383
|
+
raw: join(getPaths().root, "raw"),
|
|
384
|
+
dotWiki: join(getPaths().root, ".wiki"),
|
|
385
|
+
outputs: join(getPaths().root, "outputs"),
|
|
386
|
+
discoveries: join(getPaths().root, ".discoveries"),
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
let result: { sourceId: string };
|
|
390
|
+
|
|
391
|
+
if (urlParam) {
|
|
392
|
+
// For MCP, use simple curl-based capture
|
|
393
|
+
const { captureUrl } = (await import("../extensions/llm-wiki/lib/source-packet.js")) as {
|
|
394
|
+
captureUrl: (
|
|
395
|
+
pi: never,
|
|
396
|
+
paths: Record<string, string>,
|
|
397
|
+
url: string,
|
|
398
|
+
signal?: AbortSignal,
|
|
399
|
+
) => Promise<{ sourceId: string }>;
|
|
400
|
+
};
|
|
401
|
+
result = await captureUrl(
|
|
402
|
+
{ exec: async () => ({ stdout: "", stderr: "", code: 0 }) } as never,
|
|
403
|
+
vaultPaths,
|
|
404
|
+
urlParam,
|
|
405
|
+
);
|
|
406
|
+
} else if (file_path) {
|
|
407
|
+
const { captureFile } = (await import("../extensions/llm-wiki/lib/source-packet.js")) as {
|
|
408
|
+
captureFile: (
|
|
409
|
+
pi: never,
|
|
410
|
+
paths: Record<string, string>,
|
|
411
|
+
filePath: string,
|
|
412
|
+
signal?: AbortSignal,
|
|
413
|
+
) => Promise<{ sourceId: string }>;
|
|
414
|
+
};
|
|
415
|
+
result = await captureFile(
|
|
416
|
+
{ exec: async () => ({ stdout: "", stderr: "", code: 0 }) } as never,
|
|
417
|
+
vaultPaths,
|
|
418
|
+
file_path,
|
|
419
|
+
);
|
|
420
|
+
} else if (text) {
|
|
421
|
+
const { captureText } = (await import("../extensions/llm-wiki/lib/source-packet.js")) as {
|
|
422
|
+
captureText: (
|
|
423
|
+
paths: Record<string, string>,
|
|
424
|
+
text: string,
|
|
425
|
+
title?: string,
|
|
426
|
+
) => { sourceId: string };
|
|
427
|
+
};
|
|
428
|
+
result = captureText(vaultPaths, text, title);
|
|
429
|
+
} else {
|
|
430
|
+
return {
|
|
431
|
+
content: [
|
|
432
|
+
{
|
|
433
|
+
type: "text" as const,
|
|
434
|
+
text: "Provide one of: text, url, or file_path",
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
isError: true,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: "text" as const,
|
|
445
|
+
text: `Source captured: ${result.sourceId}`,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// ─── Main ───────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
async function main() {
|
|
455
|
+
const transport = new StdioServerTransport();
|
|
456
|
+
await server.connect(transport);
|
|
457
|
+
console.error("🧠 LLM Wiki MCP Server running on stdio");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
main().catch((err) => {
|
|
461
|
+
console.error("MCP Server error:", err);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"extensions",
|
|
38
38
|
"skills",
|
|
39
39
|
"prompts",
|
|
40
|
+
"mcp",
|
|
40
41
|
"docs",
|
|
41
42
|
"assets",
|
|
42
43
|
"README.md",
|
|
@@ -62,7 +63,10 @@
|
|
|
62
63
|
"extensions": ["./extensions"],
|
|
63
64
|
"skills": ["./skills"],
|
|
64
65
|
"prompts": ["./prompts"],
|
|
65
|
-
"image": "https://raw.githubusercontent.com/zosmaai/pi-llm-wiki/main/assets/screenshot.png"
|
|
66
|
+
"image": "https://raw.githubusercontent.com/zosmaai/pi-llm-wiki/main/assets/screenshot.png",
|
|
67
|
+
"mcpservers": {
|
|
68
|
+
"llm-wiki": "node ./mcp/index.js"
|
|
69
|
+
}
|
|
66
70
|
},
|
|
67
71
|
"peerDependencies": {
|
|
68
72
|
"@mariozechner/pi-coding-agent": "*",
|
|
@@ -71,6 +75,9 @@
|
|
|
71
75
|
"engines": {
|
|
72
76
|
"node": ">=18"
|
|
73
77
|
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@modelcontextprotocol/server": "^2.0.0-alpha.2"
|
|
80
|
+
},
|
|
74
81
|
"devDependencies": {
|
|
75
82
|
"@biomejs/biome": "^1.9.4",
|
|
76
83
|
"@mariozechner/pi-coding-agent": "^0.70.2",
|
package/skills/llm-wiki/SKILL.md
CHANGED
|
@@ -45,21 +45,48 @@ WIKI_ROOT/
|
|
|
45
45
|
|
|
46
46
|
## How the Extension Helps You
|
|
47
47
|
|
|
48
|
-
| Task
|
|
49
|
-
|
|
|
50
|
-
| Track ingestion
|
|
51
|
-
| Update INDEX
|
|
52
|
-
| Update LOG
|
|
53
|
-
| Find orphans
|
|
54
|
-
| Block raw edits
|
|
55
|
-
| Create source page
|
|
48
|
+
| Task | Before (skill-only) | Now (extension-backed) |
|
|
49
|
+
| --------------------------- | ---------------------------- | ------------------------------------- |
|
|
50
|
+
| Track ingestion | Manual `history.json` | Automatic via `meta/registry.json` |
|
|
51
|
+
| Update INDEX | Manual edit after every page | Auto-rebuilds on turn end |
|
|
52
|
+
| Update LOG | Manual append | Auto-generated from `events.jsonl` |
|
|
53
|
+
| Find orphans | Shell `grep` scans | Instant from `backlinks.json` |
|
|
54
|
+
| Block raw edits | Skill says "don't" | Extension **enforces** immutability |
|
|
55
|
+
| Create source page | 8 tool calls | `wiki_capture_source` + LLM synthesis |
|
|
56
|
+
| **Recall wiki knowledge** | Never happens | **Auto-search before every turn** |
|
|
57
|
+
| **Save task insights** | Manual capture | `wiki_retro` — one tool call |
|
|
58
|
+
|
|
59
|
+
## 🔄 Auto-Recall (New)
|
|
60
|
+
|
|
61
|
+
**The extension now automatically searches the wiki before every user turn.**
|
|
62
|
+
|
|
63
|
+
When you send a prompt, the extension:
|
|
64
|
+
1. Extracts key terms from your request
|
|
65
|
+
2. Searches the wiki registry for matching pages
|
|
66
|
+
3. Injects matching page titles + summaries into context
|
|
67
|
+
4. You see this as "Relevant Wiki Knowledge" in your system prompt
|
|
68
|
+
|
|
69
|
+
**This means the wiki works as an automatic second brain.**
|
|
70
|
+
You don't need to remember to search — relevant knowledge is surfaced automatically.
|
|
71
|
+
|
|
72
|
+
### Manual recall for deeper searches
|
|
73
|
+
|
|
74
|
+
If the auto-recall doesn't find enough context, call `wiki_recall` explicitly:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
wiki_recall(query="specific terms...", max_results=10)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This gives you more control over the search terms and returns content previews.
|
|
56
81
|
|
|
57
82
|
## Available Tools
|
|
58
83
|
|
|
59
|
-
Use these directly — they handle scaffolding and
|
|
84
|
+
Use these directly — they handle scaffolding, bookkeeping, recall, and capture:
|
|
60
85
|
|
|
61
86
|
- `wiki_bootstrap` — Initialize a new vault
|
|
62
87
|
- `wiki_capture_source` — Capture URL/file/text into immutable packet + skeleton page
|
|
88
|
+
- `wiki_recall` — **Auto-called at turn start.** Search wiki for task-relevant pages
|
|
89
|
+
- `wiki_retro` — Save an atomic insight from a completed task into the wiki
|
|
63
90
|
- `wiki_ingest` — Get batch of uningested sources with extracted text
|
|
64
91
|
- `wiki_ensure_page` — Create entity/concept/synthesis/analysis page from template
|
|
65
92
|
- `wiki_search` — Search registry for existing pages
|
|
@@ -83,12 +110,20 @@ Use these directly — they handle scaffolding and bookkeeping:
|
|
|
83
110
|
|
|
84
111
|
### Query → Answer → File
|
|
85
112
|
|
|
86
|
-
1.
|
|
113
|
+
1. **Auto-recall**: Extension surfaces relevant wiki pages automatically
|
|
87
114
|
2. Read those pages
|
|
88
115
|
3. Synthesize answer with `[[wikilink]]` citations
|
|
89
116
|
4. If novel: create analysis page via `wiki_ensure_page(type="analysis")`
|
|
90
117
|
5. Extension auto-updates metadata
|
|
91
118
|
|
|
119
|
+
### Task → Capture → Retro
|
|
120
|
+
|
|
121
|
+
1. Complete a meaningful task
|
|
122
|
+
2. Call `wiki_retro` to save key insights
|
|
123
|
+
3. The insight is captured as a source packet
|
|
124
|
+
4. Extension auto-updates metadata
|
|
125
|
+
5. Next time, auto-recall surfaces your saved insight
|
|
126
|
+
|
|
92
127
|
## Page Conventions
|
|
93
128
|
|
|
94
129
|
### Naming
|