@zosmaai/pi-llm-wiki 0.5.0 → 0.6.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 +40 -0
- package/README.md +89 -52
- package/docs/architecture.md +30 -26
- package/docs/commands.md +1 -1
- package/docs/configuration.md +1 -1
- package/docs/obsidian.md +5 -5
- package/extensions/llm-wiki/index.ts +38 -12
- package/extensions/llm-wiki/lib/guardrails.ts +8 -18
- package/extensions/llm-wiki/lib/recall.ts +207 -0
- package/extensions/llm-wiki/lib/retro.ts +196 -0
- package/extensions/llm-wiki/lib/tools.ts +11 -10
- package/extensions/llm-wiki/lib/utils.ts +48 -8
- package/mcp/index.ts +487 -0
- package/package.json +9 -2
- package/prompts/wiki-discover.md +6 -6
- package/prompts/wiki-ingest.md +8 -8
- package/prompts/wiki-init.md +11 -11
- package/prompts/wiki-lint.md +5 -5
- package/prompts/wiki-status.md +4 -4
- package/skills/llm-wiki/SKILL.md +67 -30
- package/skills/llm-wiki/templates/pages/source.md +2 -2
package/mcp/index.ts
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
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
|
+
raw: string;
|
|
26
|
+
rawSources: string;
|
|
27
|
+
wiki: string;
|
|
28
|
+
meta: string;
|
|
29
|
+
dotWiki: string;
|
|
30
|
+
outputs: string;
|
|
31
|
+
discoveries: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Detect vault format at a directory. */
|
|
35
|
+
function detectFormat(dir: string): "new" | "legacy" | "none" {
|
|
36
|
+
if (existsSync(join(dir, ".llm-wiki", "config.json"))) return "new";
|
|
37
|
+
if (existsSync(join(dir, ".wiki", "config.json"))) return "legacy";
|
|
38
|
+
return "none";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveVaultRoot(cwd: string): string | null {
|
|
42
|
+
// Check cwd first
|
|
43
|
+
if (detectFormat(cwd) !== "none") return cwd;
|
|
44
|
+
|
|
45
|
+
// Walk up
|
|
46
|
+
const parts = cwd.split("/");
|
|
47
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
48
|
+
const dir = parts.slice(0, i + 1).join("/") || "/";
|
|
49
|
+
if (detectFormat(dir) !== "none") return dir;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getPaths(): VaultPaths {
|
|
55
|
+
const detectedRoot = resolveVaultRoot(process.cwd());
|
|
56
|
+
const root = process.env.WIKI_ROOT || detectedRoot || process.cwd();
|
|
57
|
+
const format = process.env.WIKI_ROOT
|
|
58
|
+
? detectFormat(root) // Use format detection even with explicit WIKI_ROOT
|
|
59
|
+
: detectedRoot
|
|
60
|
+
? detectFormat(root)
|
|
61
|
+
: "none";
|
|
62
|
+
|
|
63
|
+
if (format === "legacy") {
|
|
64
|
+
return {
|
|
65
|
+
root,
|
|
66
|
+
raw: join(root, "raw"),
|
|
67
|
+
rawSources: join(root, "raw", "sources"),
|
|
68
|
+
wiki: join(root, "wiki"),
|
|
69
|
+
meta: join(root, "meta"),
|
|
70
|
+
dotWiki: join(root, ".wiki"),
|
|
71
|
+
outputs: join(root, "outputs"),
|
|
72
|
+
discoveries: join(root, ".discoveries"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
root,
|
|
78
|
+
raw: join(root, ".llm-wiki", "raw"),
|
|
79
|
+
rawSources: join(root, ".llm-wiki", "raw", "sources"),
|
|
80
|
+
wiki: join(root, ".llm-wiki", "wiki"),
|
|
81
|
+
meta: join(root, ".llm-wiki", "meta"),
|
|
82
|
+
dotWiki: join(root, ".llm-wiki"),
|
|
83
|
+
outputs: join(root, ".llm-wiki", "outputs"),
|
|
84
|
+
discoveries: join(root, ".llm-wiki", ".discoveries"),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasVault(): boolean {
|
|
89
|
+
const paths = getPaths();
|
|
90
|
+
return existsSync(join(paths.dotWiki, "config.json"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function readJson<T>(path: string, defaultVal: T): T {
|
|
96
|
+
try {
|
|
97
|
+
if (!existsSync(path)) return defaultVal;
|
|
98
|
+
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
99
|
+
} catch {
|
|
100
|
+
return defaultVal;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── MCP Server ─────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const server = new McpServer({
|
|
107
|
+
name: "llm-wiki",
|
|
108
|
+
version: "1.0.0",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---- wiki_recall ----
|
|
112
|
+
|
|
113
|
+
server.registerTool(
|
|
114
|
+
"wiki_recall",
|
|
115
|
+
{
|
|
116
|
+
description:
|
|
117
|
+
"Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
|
|
118
|
+
inputSchema: z.object({
|
|
119
|
+
query: z.string().describe("Search query — use the user's full request or key terms"),
|
|
120
|
+
max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
async ({ query, max_results }) => {
|
|
124
|
+
if (!hasVault()) {
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text" as const,
|
|
129
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const paths = getPaths();
|
|
137
|
+
const registry = readJson<{
|
|
138
|
+
pages: Record<string, { type: string; title: string; [key: string]: unknown }>;
|
|
139
|
+
}>(join(paths.meta, "registry.json"), { pages: {} });
|
|
140
|
+
|
|
141
|
+
const terms = query
|
|
142
|
+
.toLowerCase()
|
|
143
|
+
.split(/\s+/)
|
|
144
|
+
.filter((t) => t.length > 2)
|
|
145
|
+
.slice(0, 10);
|
|
146
|
+
|
|
147
|
+
if (terms.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text" as const, text: "Query too short." }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type Scored = { id: string; score: number };
|
|
154
|
+
const scored: Scored[] = [];
|
|
155
|
+
|
|
156
|
+
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
157
|
+
let score = 0;
|
|
158
|
+
const title = String(entry.title || "").toLowerCase();
|
|
159
|
+
const type = String(entry.type || "").toLowerCase();
|
|
160
|
+
|
|
161
|
+
for (const term of terms) {
|
|
162
|
+
if (id.toLowerCase().includes(term)) score += 3;
|
|
163
|
+
if (title.includes(term)) score += 4;
|
|
164
|
+
if (type.includes(term)) score += 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tags = String(entry.tags || entry.category || entry.domain || "").toLowerCase();
|
|
168
|
+
for (const term of terms) {
|
|
169
|
+
if (tags.includes(term)) score += 2;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (score > 0) scored.push({ id, score });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
scored.sort((a, b) => b.score - a.score);
|
|
176
|
+
const top = scored.slice(0, Math.min(max_results ?? 5, 10));
|
|
177
|
+
|
|
178
|
+
const results = top.map(({ id }) => {
|
|
179
|
+
const entry = registry.pages[id];
|
|
180
|
+
let preview = "";
|
|
181
|
+
const pagePath = join(paths.wiki, `${id}.md`);
|
|
182
|
+
if (existsSync(pagePath)) {
|
|
183
|
+
const content = readFileSync(pagePath, "utf-8");
|
|
184
|
+
preview = content
|
|
185
|
+
.replace(/^---[\s\S]*?---\n/, "")
|
|
186
|
+
.trim()
|
|
187
|
+
.slice(0, 200)
|
|
188
|
+
.replace(/\n/g, " ");
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
id,
|
|
192
|
+
title: String(entry?.title || id),
|
|
193
|
+
type: String(entry?.type || "page"),
|
|
194
|
+
preview,
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: "text" as const,
|
|
202
|
+
text: JSON.stringify(results, null, 2),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// ---- wiki_search ----
|
|
210
|
+
|
|
211
|
+
server.registerTool(
|
|
212
|
+
"wiki_search",
|
|
213
|
+
{
|
|
214
|
+
description: "Search the wiki registry for pages matching a query.",
|
|
215
|
+
inputSchema: z.object({
|
|
216
|
+
query: z.string().describe("Search term"),
|
|
217
|
+
type: z
|
|
218
|
+
.string()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe("Filter by page type (source, entity, concept, synthesis, analysis)"),
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
async ({ query, type }) => {
|
|
224
|
+
if (!hasVault()) {
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text" as const,
|
|
229
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
isError: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const paths = getPaths();
|
|
237
|
+
const registry = readJson<{
|
|
238
|
+
pages: Record<string, { type: string; title: string; [key: string]: unknown }>;
|
|
239
|
+
}>(join(paths.meta, "registry.json"), { pages: {} });
|
|
240
|
+
|
|
241
|
+
const q = query.toLowerCase();
|
|
242
|
+
const matches = Object.entries(registry.pages)
|
|
243
|
+
.filter(([id, entry]) => {
|
|
244
|
+
const matchesQuery =
|
|
245
|
+
id.toLowerCase().includes(q) ||
|
|
246
|
+
String(entry.title).toLowerCase().includes(q) ||
|
|
247
|
+
String(entry.type).toLowerCase().includes(q);
|
|
248
|
+
const matchesType = !type || String(entry.type).toLowerCase() === type.toLowerCase();
|
|
249
|
+
return matchesQuery && matchesType;
|
|
250
|
+
})
|
|
251
|
+
.map(([id, entry]) => ({
|
|
252
|
+
id,
|
|
253
|
+
title: entry.title,
|
|
254
|
+
type: entry.type,
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text" as const,
|
|
261
|
+
text:
|
|
262
|
+
matches.length > 0 ? JSON.stringify(matches, null, 2) : `No pages found for "${query}"`,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// ---- wiki_status ----
|
|
270
|
+
|
|
271
|
+
server.registerTool(
|
|
272
|
+
"wiki_status",
|
|
273
|
+
{
|
|
274
|
+
description: "Show wiki health and stats: page counts, orphans, recent activity.",
|
|
275
|
+
inputSchema: z.object({}),
|
|
276
|
+
},
|
|
277
|
+
async () => {
|
|
278
|
+
if (!hasVault()) {
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text" as const,
|
|
283
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const paths = getPaths();
|
|
291
|
+
const registry = readJson<{
|
|
292
|
+
version: string;
|
|
293
|
+
last_updated: string;
|
|
294
|
+
pages: Record<string, { type: string; title: string; [key: string]: unknown }>;
|
|
295
|
+
}>(join(paths.meta, "registry.json"), {
|
|
296
|
+
version: "1.0",
|
|
297
|
+
last_updated: "",
|
|
298
|
+
pages: {},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const config = readJson<Record<string, unknown>>(join(paths.dotWiki, "config.json"), {});
|
|
302
|
+
|
|
303
|
+
const byType: Record<string, number> = {};
|
|
304
|
+
for (const entry of Object.values(registry.pages)) {
|
|
305
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text" as const,
|
|
312
|
+
text: JSON.stringify(
|
|
313
|
+
{
|
|
314
|
+
topic: config.topic || "Unknown",
|
|
315
|
+
mode: config.mode || "personal",
|
|
316
|
+
totalPages: Object.keys(registry.pages).length,
|
|
317
|
+
byType,
|
|
318
|
+
lastUpdated: registry.last_updated || "Never",
|
|
319
|
+
},
|
|
320
|
+
null,
|
|
321
|
+
2,
|
|
322
|
+
),
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// ---- wiki_retro ----
|
|
330
|
+
|
|
331
|
+
server.registerTool(
|
|
332
|
+
"wiki_retro",
|
|
333
|
+
{
|
|
334
|
+
description:
|
|
335
|
+
"Save an atomic insight from a completed task into the wiki. Creates a source packet and source page.",
|
|
336
|
+
inputSchema: z.object({
|
|
337
|
+
slug: z.string().describe("Unique kebab-case identifier (e.g. 'jwt-revocation-pattern')"),
|
|
338
|
+
title: z.string().describe("Short descriptive title (60 chars max)"),
|
|
339
|
+
body: z
|
|
340
|
+
.string()
|
|
341
|
+
.describe(
|
|
342
|
+
"Markdown body explaining what was learned. Include [[wikilinks]] to related pages.",
|
|
343
|
+
),
|
|
344
|
+
category: z
|
|
345
|
+
.string()
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("Category (e.g. frontend, architecture, devops, bugfix)"),
|
|
348
|
+
}),
|
|
349
|
+
},
|
|
350
|
+
async ({ slug, title, body, category }) => {
|
|
351
|
+
if (!hasVault()) {
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: "text" as const,
|
|
356
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
isError: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const { saveInsight } = (await import("../extensions/llm-wiki/lib/retro.js")) as {
|
|
364
|
+
saveInsight: (
|
|
365
|
+
paths: Record<string, string>,
|
|
366
|
+
slug: string,
|
|
367
|
+
title: string,
|
|
368
|
+
body: string,
|
|
369
|
+
category?: string,
|
|
370
|
+
) => { sourceId: string; packetPath: string; sourcePagePath: string };
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const vaultPaths = getPaths();
|
|
374
|
+
const result = saveInsight(vaultPaths, slug, title, body, category);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
content: [
|
|
378
|
+
{
|
|
379
|
+
type: "text" as const,
|
|
380
|
+
text: `Insight saved: ${result.sourceId} — ${title}`,
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// ---- wiki_capture_source ----
|
|
388
|
+
|
|
389
|
+
server.registerTool(
|
|
390
|
+
"wiki_capture_source",
|
|
391
|
+
{
|
|
392
|
+
description: "Capture a URL, local file, or pasted text into an immutable source packet.",
|
|
393
|
+
inputSchema: z.object({
|
|
394
|
+
text: z.string().optional().describe("Text content to capture"),
|
|
395
|
+
url: z.string().optional().describe("URL to capture"),
|
|
396
|
+
file_path: z.string().optional().describe("Local file path to capture"),
|
|
397
|
+
title: z.string().optional().describe("Title for the captured source"),
|
|
398
|
+
}),
|
|
399
|
+
},
|
|
400
|
+
async ({ text, url: urlParam, file_path, title }) => {
|
|
401
|
+
if (!hasVault()) {
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "text" as const,
|
|
406
|
+
text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
isError: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const vaultPaths = getPaths();
|
|
414
|
+
let result: { sourceId: string };
|
|
415
|
+
|
|
416
|
+
if (urlParam) {
|
|
417
|
+
const { captureUrl } = (await import("../extensions/llm-wiki/lib/source-packet.js")) as {
|
|
418
|
+
captureUrl: (
|
|
419
|
+
pi: never,
|
|
420
|
+
paths: Record<string, string>,
|
|
421
|
+
url: string,
|
|
422
|
+
signal?: AbortSignal,
|
|
423
|
+
) => Promise<{ sourceId: string }>;
|
|
424
|
+
};
|
|
425
|
+
result = await captureUrl(
|
|
426
|
+
{ exec: async () => ({ stdout: "", stderr: "", code: 0 }) } as never,
|
|
427
|
+
vaultPaths,
|
|
428
|
+
urlParam,
|
|
429
|
+
);
|
|
430
|
+
} else if (file_path) {
|
|
431
|
+
const { captureFile } = (await import("../extensions/llm-wiki/lib/source-packet.js")) as {
|
|
432
|
+
captureFile: (
|
|
433
|
+
pi: never,
|
|
434
|
+
paths: Record<string, string>,
|
|
435
|
+
filePath: string,
|
|
436
|
+
signal?: AbortSignal,
|
|
437
|
+
) => Promise<{ sourceId: string }>;
|
|
438
|
+
};
|
|
439
|
+
result = await captureFile(
|
|
440
|
+
{ exec: async () => ({ stdout: "", stderr: "", code: 0 }) } as never,
|
|
441
|
+
vaultPaths,
|
|
442
|
+
file_path,
|
|
443
|
+
);
|
|
444
|
+
} else if (text) {
|
|
445
|
+
const { captureText } = (await import("../extensions/llm-wiki/lib/source-packet.js")) as {
|
|
446
|
+
captureText: (
|
|
447
|
+
paths: Record<string, string>,
|
|
448
|
+
text: string,
|
|
449
|
+
title?: string,
|
|
450
|
+
) => { sourceId: string };
|
|
451
|
+
};
|
|
452
|
+
result = captureText(vaultPaths, text, title);
|
|
453
|
+
} else {
|
|
454
|
+
return {
|
|
455
|
+
content: [
|
|
456
|
+
{
|
|
457
|
+
type: "text" as const,
|
|
458
|
+
text: "Provide one of: text, url, or file_path",
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
isError: true,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: "text" as const,
|
|
469
|
+
text: `Source captured: ${result.sourceId}`,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// ─── Main ───────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
async function main() {
|
|
479
|
+
const transport = new StdioServerTransport();
|
|
480
|
+
await server.connect(transport);
|
|
481
|
+
console.error("🧠 LLM Wiki MCP Server running on stdio");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
main().catch((err) => {
|
|
485
|
+
console.error("MCP Server error:", err);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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/prompts/wiki-discover.md
CHANGED
|
@@ -17,17 +17,17 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first. Also read `conf
|
|
|
17
17
|
|
|
18
18
|
## Steps
|
|
19
19
|
|
|
20
|
-
1. Read
|
|
21
|
-
2. Read `.discoveries/gaps.json` → knowledge gaps to fill
|
|
22
|
-
3. Read `.discoveries/history.json` → already-fetched URLs
|
|
20
|
+
1. Read `.llm-wiki/config.yaml` → extract topics, keywords, feeds
|
|
21
|
+
2. Read `.llm-wiki/.discoveries/gaps.json` → knowledge gaps to fill
|
|
22
|
+
3. Read `.llm-wiki/.discoveries/history.json` → already-fetched URLs
|
|
23
23
|
4. Search for new sources:
|
|
24
24
|
- Web search each topic + latest keywords
|
|
25
|
-
- Search for gaps from `.discoveries/gaps.json`
|
|
25
|
+
- Search for gaps from `.llm-wiki/.discoveries/gaps.json`
|
|
26
26
|
- If `--topic` specified, focus search on that topic
|
|
27
27
|
5. For each promising result:
|
|
28
28
|
a. Fetch full content
|
|
29
|
-
b. Save to
|
|
30
|
-
6. Update `.discoveries/history.json`
|
|
29
|
+
b. Save to `.llm-wiki/raw/articles/YYYY-MM-DD-slug.md` with frontmatter (title, url, discovered, topic)
|
|
30
|
+
6. Update `.llm-wiki/.discoveries/history.json`
|
|
31
31
|
7. Report: "Discovered [N] new sources. Run `/wiki-ingest` to process them."
|
|
32
32
|
|
|
33
33
|
**Rules:** Max 5-10 sources. Skip ads, listicles, duplicates. Prefer in-depth analysis.
|
package/prompts/wiki-ingest.md
CHANGED
|
@@ -7,7 +7,7 @@ topLevelCli: true
|
|
|
7
7
|
|
|
8
8
|
# /wiki-ingest
|
|
9
9
|
|
|
10
|
-
Process new files in
|
|
10
|
+
Process new files in `.llm-wiki/raw/` and integrate them into the wiki.
|
|
11
11
|
|
|
12
12
|
## User Arguments
|
|
13
13
|
|
|
@@ -17,18 +17,18 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
17
17
|
|
|
18
18
|
## Steps
|
|
19
19
|
|
|
20
|
-
1. Read
|
|
21
|
-
2. If a specific path is given (e.g., `/wiki-ingest raw/articles/my-file.md`), process just that file
|
|
22
|
-
3. If no path given, scan all files in
|
|
20
|
+
1. Read `.llm-wiki/config.yaml` and `.llm-wiki/.discoveries/history.json`
|
|
21
|
+
2. If a specific path is given (e.g., `/wiki-ingest .llm-wiki/raw/articles/my-file.md`), process just that file
|
|
22
|
+
3. If no path given, scan all files in `.llm-wiki/raw/` and find ones not in history
|
|
23
23
|
4. For each new source:
|
|
24
24
|
a. Read the full content
|
|
25
25
|
b. Briefly discuss with the user: "This is about [topic]. Key points: [summary]. Any specific emphasis?"
|
|
26
|
-
c. Create/update pages in
|
|
26
|
+
c. Create/update pages in `.llm-wiki/wiki/sources/`, `.llm-wiki/wiki/entities/`, `.llm-wiki/wiki/concepts/`
|
|
27
27
|
d. Add `[[wikilinks]]` cross-references between related pages
|
|
28
28
|
e. Flag any contradictions with existing wiki content
|
|
29
|
-
5. Update
|
|
30
|
-
6. Append to
|
|
31
|
-
7. Update `.discoveries/history.json`
|
|
29
|
+
5. Update `.llm-wiki/wiki/INDEX.md` with all new/updated pages
|
|
30
|
+
6. Append to `.llm-wiki/wiki/LOG.md`
|
|
31
|
+
7. Update `.llm-wiki/.discoveries/history.json`
|
|
32
32
|
8. Report: "Ingested [N] sources → [M] pages created/updated. [X] contradictions flagged."
|
|
33
33
|
|
|
34
34
|
**Rules:** Never modify raw/ files. Never fabricate information. Always cite sources.
|
package/prompts/wiki-init.md
CHANGED
|
@@ -19,16 +19,16 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` (or wherever the skill
|
|
|
19
19
|
|
|
20
20
|
1. Ask the user for the wiki topic and mode (`personal` or `company`)
|
|
21
21
|
2. Create the directory structure:
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- `.discoveries/`
|
|
26
|
-
3. Create
|
|
27
|
-
4. Create
|
|
28
|
-
5. Create
|
|
29
|
-
6. Create
|
|
30
|
-
7. Create `.gitignore` to exclude
|
|
22
|
+
- `.llm-wiki/raw/articles/`, `.llm-wiki/raw/papers/`, `.llm-wiki/raw/notes/`, `.llm-wiki/raw/assets/`
|
|
23
|
+
- `.llm-wiki/wiki/entities/`, `.llm-wiki/wiki/concepts/`, `.llm-wiki/wiki/sources/`, `.llm-wiki/wiki/syntheses/`, `.llm-wiki/wiki/changes/`
|
|
24
|
+
- `.llm-wiki/outputs/`
|
|
25
|
+
- `.llm-wiki/.discoveries/`
|
|
26
|
+
3. Create `.llm-wiki/config.yaml` with the topic, mode, and default settings
|
|
27
|
+
4. Create `.llm-wiki/wiki/INDEX.md` with section headings organized by page type
|
|
28
|
+
5. Create `.llm-wiki/wiki/LOG.md` with initial entry
|
|
29
|
+
6. Create `.llm-wiki/wiki/DASHBOARD.md` with Dataview queries for Obsidian
|
|
30
|
+
7. Create `.gitignore` to exclude `.llm-wiki/outputs/` from version control if desired
|
|
31
31
|
8. Initialize git repo if not already present
|
|
32
|
-
9. Report the structure and suggest first steps: "Drop sources into
|
|
32
|
+
9. Report the structure and suggest first steps: "Drop sources into `.llm-wiki/raw/` and run `/wiki-ingest`"
|
|
33
33
|
|
|
34
|
-
If `--mode company`, add the `change_detection: true` flag to config.yaml and add a
|
|
34
|
+
If `--mode company`, add the `change_detection: true` flag to config.yaml and add a `.llm-wiki/wiki/decisions/` folder.
|
package/prompts/wiki-lint.md
CHANGED
|
@@ -17,17 +17,17 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
17
17
|
|
|
18
18
|
## Steps
|
|
19
19
|
|
|
20
|
-
1. Scan all files in
|
|
20
|
+
1. Scan all files in `.llm-wiki/wiki/`
|
|
21
21
|
2. Check for:
|
|
22
22
|
- **Contradictions:** Conflicting claims between pages
|
|
23
23
|
- **Orphans:** Pages with zero inbound `[[wikilinks]]`
|
|
24
24
|
- **Missing pages:** `[[links]]` pointing to non-existent files
|
|
25
25
|
- **Stale claims:** Info superseded by newer sources
|
|
26
|
-
- **Broken raw links:** References to
|
|
26
|
+
- **Broken raw links:** References to `.llm-wiki/raw/` files that don't exist
|
|
27
27
|
- **Knowledge gaps:** Topics mentioned but lacking their own page
|
|
28
28
|
- **Quality:** Pages under 3 lines, pages with no sources or cross-refs
|
|
29
29
|
3. If `--fix` flag is present: auto-fix broken links, create missing pages for frequently-linked concepts, add cross-refs to orphans. Flag contradictions for human decision.
|
|
30
|
-
4. Save report →
|
|
31
|
-
5. Update `.discoveries/gaps.json`
|
|
32
|
-
6. Append to
|
|
30
|
+
4. Save report → `.llm-wiki/outputs/lint-YYYY-MM-DD.md`
|
|
31
|
+
5. Update `.llm-wiki/.discoveries/gaps.json`
|
|
32
|
+
6. Append to `.llm-wiki/wiki/LOG.md`
|
|
33
33
|
7. Report key findings
|
package/prompts/wiki-status.md
CHANGED
|
@@ -11,11 +11,11 @@ Show a quick overview of wiki health and statistics.
|
|
|
11
11
|
|
|
12
12
|
## Steps
|
|
13
13
|
|
|
14
|
-
1. Count sources in
|
|
15
|
-
2. Count pages in
|
|
16
|
-
3. Check
|
|
14
|
+
1. Count sources in `.llm-wiki/raw/` (recursive)
|
|
15
|
+
2. Count pages in `.llm-wiki/wiki/` (by type: entities, concepts, sources, syntheses)
|
|
16
|
+
3. Check `.llm-wiki/wiki/LOG.md` for last ingest, lint, and discover dates
|
|
17
17
|
4. Check for orphan pages (zero inbound links)
|
|
18
|
-
5. Read `.discoveries/gaps.json` for known gaps
|
|
18
|
+
5. Read `.llm-wiki/.discoveries/gaps.json` for known gaps
|
|
19
19
|
6. Report:
|
|
20
20
|
|
|
21
21
|
```
|