@zosmaai/pi-llm-wiki 0.1.2 → 0.2.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/.coderabbit.yaml +43 -0
- package/.github/workflows/ci.yml +9 -8
- package/.github/workflows/codeql.yml +39 -0
- package/.github/workflows/release.yml +6 -4
- package/AGENTS.md +57 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +43 -0
- package/LICENSE +21 -0
- package/README.md +44 -344
- package/assets/README.md +3 -3
- package/assets/architecture.md +1 -1
- package/biome.json +3 -0
- package/docs/api.md +105 -0
- package/docs/architecture.md +65 -0
- package/docs/commands.md +51 -0
- package/docs/configuration.md +38 -0
- package/docs/obsidian.md +21 -0
- package/extensions/llm-wiki/index.ts +53 -0
- package/extensions/llm-wiki/lib/guardrails.ts +69 -0
- package/extensions/llm-wiki/lib/metadata.ts +218 -0
- package/extensions/llm-wiki/lib/source-packet.ts +292 -0
- package/extensions/llm-wiki/lib/tools.ts +932 -0
- package/extensions/llm-wiki/lib/utils.ts +222 -0
- package/package.json +11 -12
- package/prompts/wiki-digest.md +1 -1
- package/prompts/wiki-discover.md +2 -2
- package/prompts/wiki-ingest.md +2 -2
- package/prompts/wiki-init.md +2 -2
- package/prompts/wiki-lint.md +1 -1
- package/prompts/wiki-query.md +2 -2
- package/prompts/wiki-run.md +6 -6
- package/prompts/wiki-status.md +1 -1
- package/scripts/release.js +72 -0
- package/skills/llm-wiki/SKILL.md +95 -369
- package/skills/llm-wiki/templates/DASHBOARD.md +2 -2
- package/skills/llm-wiki/templates/pages/analysis.md +35 -0
- package/test/llm-wiki.test.ts +18 -9
- package/extensions/llm-wiki-tools.ts +0 -705
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @zosmaai/pi-llm-wiki — Custom tools for LLM Wiki management.
|
|
6
|
-
*
|
|
7
|
-
* Registers structured tools that the LLM can call to perform wiki operations:
|
|
8
|
-
* - wiki_ingest: Process new source files and update wiki pages
|
|
9
|
-
* - wiki_query: Query the wiki with citations
|
|
10
|
-
* - wiki_lint: Health check the wiki
|
|
11
|
-
* - wiki_discover: Auto-discover new sources
|
|
12
|
-
* - wiki_status: Report wiki health overview
|
|
13
|
-
* - wiki_watch: Schedule auto-updates
|
|
14
|
-
*/
|
|
15
|
-
export default function (pi: ExtensionAPI) {
|
|
16
|
-
// ─── wiki_ingest ─────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
pi.registerTool({
|
|
19
|
-
name: "wiki_ingest",
|
|
20
|
-
label: "Wiki Ingest",
|
|
21
|
-
description:
|
|
22
|
-
"Process new source files in the raw/ directory and integrate them into the wiki. " +
|
|
23
|
-
"Creates source summaries, entity pages, concept pages, cross-references, and updates INDEX.md and LOG.md. " +
|
|
24
|
-
"Call this when new files appear in raw/ or after running wiki_discover.",
|
|
25
|
-
promptSnippet:
|
|
26
|
-
"Ingest sources from raw/ into wiki: creates summaries, entities, concepts, cross-refs",
|
|
27
|
-
promptGuidelines: [
|
|
28
|
-
"Use wiki_ingest when the user asks to process new files, add sources, or update the wiki after adding raw content.",
|
|
29
|
-
"Never modify raw/ files. Only read and synthesize from them.",
|
|
30
|
-
"Flag contradictions between new and existing wiki content explicitly.",
|
|
31
|
-
],
|
|
32
|
-
parameters: Type.Object({
|
|
33
|
-
path: Type.Optional(
|
|
34
|
-
Type.String({
|
|
35
|
-
description:
|
|
36
|
-
"Specific file path to ingest (e.g., raw/articles/my-file.md). Leave empty to process all new files.",
|
|
37
|
-
}),
|
|
38
|
-
),
|
|
39
|
-
batch_size: Type.Optional(
|
|
40
|
-
Type.Number({
|
|
41
|
-
description: "Number of files to process in this batch (default: 1). Max 5.",
|
|
42
|
-
}),
|
|
43
|
-
),
|
|
44
|
-
}),
|
|
45
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
46
|
-
try {
|
|
47
|
-
const { path, batch_size = 1 } = params;
|
|
48
|
-
const messages: string[] = [];
|
|
49
|
-
|
|
50
|
-
// Determine which files to process
|
|
51
|
-
const filesToProcess: string[] = [];
|
|
52
|
-
if (path) {
|
|
53
|
-
filesToProcess.push(path);
|
|
54
|
-
} else {
|
|
55
|
-
// Check if history file exists
|
|
56
|
-
const historyExists = await pi.exec("test", ["-f", ".discoveries/history.json"], {
|
|
57
|
-
signal,
|
|
58
|
-
});
|
|
59
|
-
const historyFiles: string[] = [];
|
|
60
|
-
if (historyExists.code === 0) {
|
|
61
|
-
const historyContent = await pi.exec("cat", [".discoveries/history.json"], { signal });
|
|
62
|
-
try {
|
|
63
|
-
const history = JSON.parse(historyContent.stdout);
|
|
64
|
-
// We'll reuse the history to skip already-processed files
|
|
65
|
-
} catch {
|
|
66
|
-
// If parsing fails, process all
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Find all files in raw/
|
|
71
|
-
const rawFiles = await pi.exec("find", ["raw/", "-type", "f"], {
|
|
72
|
-
signal,
|
|
73
|
-
});
|
|
74
|
-
const allFiles = rawFiles.stdout.trim().split("\n").filter(Boolean);
|
|
75
|
-
|
|
76
|
-
// If history exists, filter out already-processed files
|
|
77
|
-
if (historyExists.code === 0) {
|
|
78
|
-
try {
|
|
79
|
-
const historyContent = await pi.exec("cat", [".discoveries/history.json"], {
|
|
80
|
-
signal,
|
|
81
|
-
});
|
|
82
|
-
const history = JSON.parse(historyContent.stdout);
|
|
83
|
-
const processed = new Set(
|
|
84
|
-
(history.processed || []).map((f: { path: string }) => f.path),
|
|
85
|
-
);
|
|
86
|
-
for (const file of allFiles) {
|
|
87
|
-
if (!processed.has(file)) {
|
|
88
|
-
filesToProcess.push(file);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
filesToProcess.push(...allFiles);
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
filesToProcess.push(...allFiles);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const batch = filesToProcess.slice(0, Math.min(batch_size, 5));
|
|
100
|
-
|
|
101
|
-
if (batch.length === 0) {
|
|
102
|
-
return {
|
|
103
|
-
content: [
|
|
104
|
-
{
|
|
105
|
-
type: "text",
|
|
106
|
-
text: "No new source files found to ingest. Try running `/wiki:discover` to find new sources, or drop files into `raw/` first.",
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
details: {},
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
messages.push(
|
|
114
|
-
`Found ${batch.length} file(s) to process. Each file will require reading, summarizing, and updating 5-15 wiki pages.\n`,
|
|
115
|
-
);
|
|
116
|
-
messages.push(`**Files to ingest:**\n${batch.map((f) => ` - \`${f}\``).join("\n")}\n`);
|
|
117
|
-
|
|
118
|
-
// Create/update history to mark these as processed
|
|
119
|
-
const ensureDir = await pi.exec("mkdir", ["-p", ".discoveries"], {
|
|
120
|
-
signal,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
let history: { processed: Array<{ path: string; ingested: string }> } = { processed: [] };
|
|
124
|
-
const histExists = await pi.exec("test", ["-f", ".discoveries/history.json"], { signal });
|
|
125
|
-
if (histExists.code === 0) {
|
|
126
|
-
const hc = await pi.exec("cat", [".discoveries/history.json"], {
|
|
127
|
-
signal,
|
|
128
|
-
});
|
|
129
|
-
try {
|
|
130
|
-
history = JSON.parse(hc.stdout);
|
|
131
|
-
} catch {
|
|
132
|
-
history = { processed: [] };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const today = new Date().toISOString().split("T")[0];
|
|
137
|
-
for (const file of batch) {
|
|
138
|
-
history.processed.push({ path: file, ingested: today });
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
await pi.exec(
|
|
142
|
-
"sh",
|
|
143
|
-
[
|
|
144
|
-
"-c",
|
|
145
|
-
`cat > .discoveries/history.json << 'EOF'\n${JSON.stringify(history, null, 2)}\nEOF`,
|
|
146
|
-
],
|
|
147
|
-
{ signal },
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
messages.push(
|
|
151
|
-
`✅ Marked ${batch.length} file(s) as processed in \`.discoveries/history.json\`.\n\n**Next steps for each file:**\n1. Read the file content\n2. Create a source summary in \`wiki/sources/\`\n3. Create/update entity pages in \`wiki/entities/\`\n4. Create/update concept pages in \`wiki/concepts/\`\n5. Add [[wikilinks]] cross-references\n6. Flag any contradictions with existing wiki content\n7. Update \`wiki/INDEX.md\` and \`wiki/LOG.md\``,
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
content: [{ type: "text", text: messages.join("\n") }],
|
|
156
|
-
details: {
|
|
157
|
-
filesToProcess: batch,
|
|
158
|
-
count: batch.length,
|
|
159
|
-
action: "ready_for_ingest",
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
} catch (err: unknown) {
|
|
163
|
-
const error = err as Error;
|
|
164
|
-
return {
|
|
165
|
-
content: [
|
|
166
|
-
{
|
|
167
|
-
type: "text",
|
|
168
|
-
text: `❌ Error during ingest preparation: ${error.message}`,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
details: { error: error.message },
|
|
172
|
-
isError: true,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ─── wiki_status_report ─────────────────────────────────
|
|
179
|
-
|
|
180
|
-
pi.registerTool({
|
|
181
|
-
name: "wiki_status_report",
|
|
182
|
-
label: "Wiki Status Report",
|
|
183
|
-
description:
|
|
184
|
-
"Report the current health and statistics of the LLM Wiki. " +
|
|
185
|
-
"Counts sources, wiki pages, checks for orphans, and reports last activity dates. " +
|
|
186
|
-
"Call this to get a quick overview of wiki health.",
|
|
187
|
-
promptSnippet: "Report wiki health: sources count, page stats, orphans, last activity",
|
|
188
|
-
promptGuidelines: [
|
|
189
|
-
"Use wiki_status_report when the user asks for wiki health, stats, or progress.",
|
|
190
|
-
],
|
|
191
|
-
parameters: Type.Object({}),
|
|
192
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
193
|
-
try {
|
|
194
|
-
// Count raw sources
|
|
195
|
-
const rawResult = await pi.exec("find", ["raw/", "-type", "f", "|", "wc", "-l"], {
|
|
196
|
-
signal,
|
|
197
|
-
});
|
|
198
|
-
const sourceCount = Number.parseInt(rawResult.stdout.trim() || "0", 10);
|
|
199
|
-
|
|
200
|
-
// Count wiki pages by type
|
|
201
|
-
const entities = await pi.exec("sh", ["-c", "ls wiki/entities/ 2>/dev/null | wc -l"], {
|
|
202
|
-
signal,
|
|
203
|
-
});
|
|
204
|
-
const concepts = await pi.exec("sh", ["-c", "ls wiki/concepts/ 2>/dev/null | wc -l"], {
|
|
205
|
-
signal,
|
|
206
|
-
});
|
|
207
|
-
const sources = await pi.exec("sh", ["-c", "ls wiki/sources/ 2>/dev/null | wc -l"], {
|
|
208
|
-
signal,
|
|
209
|
-
});
|
|
210
|
-
const syntheses = await pi.exec("sh", ["-c", "ls wiki/syntheses/ 2>/dev/null | wc -l"], {
|
|
211
|
-
signal,
|
|
212
|
-
});
|
|
213
|
-
const changes = await pi.exec("sh", ["-c", "ls wiki/changes/ 2>/dev/null | wc -l"], {
|
|
214
|
-
signal,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const entityCount = Number.parseInt(entities.stdout.trim() || "0", 10);
|
|
218
|
-
const conceptCount = Number.parseInt(concepts.stdout.trim() || "0", 10);
|
|
219
|
-
const sourceCountW = Number.parseInt(sources.stdout.trim() || "0", 10);
|
|
220
|
-
const synthesisCount = Number.parseInt(syntheses.stdout.trim() || "0", 10);
|
|
221
|
-
const changeCount = Number.parseInt(changes.stdout.trim() || "0", 10);
|
|
222
|
-
const totalPages = entityCount + conceptCount + sourceCountW + synthesisCount;
|
|
223
|
-
|
|
224
|
-
// Check last activity dates from LOG.md
|
|
225
|
-
let lastIngest = "Never";
|
|
226
|
-
let lastLint = "Never";
|
|
227
|
-
let lastDiscover = "Never";
|
|
228
|
-
const logExists = await pi.exec("test", ["-f", "wiki/LOG.md"], {
|
|
229
|
-
signal,
|
|
230
|
-
});
|
|
231
|
-
if (logExists.code === 0) {
|
|
232
|
-
const logContent = await pi.exec("grep", ["^## ", "wiki/LOG.md"], {
|
|
233
|
-
signal,
|
|
234
|
-
});
|
|
235
|
-
const lines = logContent.stdout.trim().split("\n").filter(Boolean);
|
|
236
|
-
// Parse from bottom to find most recent of each type
|
|
237
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
238
|
-
const line = lines[i];
|
|
239
|
-
if (lastIngest === "Never" && line.includes("ingest"))
|
|
240
|
-
lastIngest = line.slice(3, line.indexOf("]") + 1);
|
|
241
|
-
if (lastLint === "Never" && line.includes("lint"))
|
|
242
|
-
lastLint = line.slice(3, line.indexOf("]") + 1);
|
|
243
|
-
if (lastDiscover === "Never" && line.includes("discover"))
|
|
244
|
-
lastDiscover = line.slice(3, line.indexOf("]") + 1);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Check config
|
|
249
|
-
let mode = "personal";
|
|
250
|
-
const topics: string[] = [];
|
|
251
|
-
const configExists = await pi.exec("test", ["-f", "config.yaml"], {
|
|
252
|
-
signal,
|
|
253
|
-
});
|
|
254
|
-
if (configExists.code === 0) {
|
|
255
|
-
const configContent = await pi.exec("grep", ["-E", "(mode:|topic:)", "config.yaml"], {
|
|
256
|
-
signal,
|
|
257
|
-
});
|
|
258
|
-
const cfgLines = configContent.stdout.trim().split("\n");
|
|
259
|
-
for (const line of cfgLines) {
|
|
260
|
-
if (line.includes("mode:")) mode = line.split(":")[1].trim();
|
|
261
|
-
if (line.includes("topic:")) {
|
|
262
|
-
const t = line.split(":")[1].trim();
|
|
263
|
-
if (t) topics.push(t);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Check gaps
|
|
269
|
-
let gapCount = 0;
|
|
270
|
-
const gapsExist = await pi.exec("test", ["-f", ".discoveries/gaps.json"], { signal });
|
|
271
|
-
if (gapsExist.code === 0) {
|
|
272
|
-
const gapsContent = await pi.exec("cat", [".discoveries/gaps.json"], {
|
|
273
|
-
signal,
|
|
274
|
-
});
|
|
275
|
-
try {
|
|
276
|
-
const gaps = JSON.parse(gapsContent.stdout);
|
|
277
|
-
gapCount = Array.isArray(gaps.gaps) ? gaps.gaps.length : 0;
|
|
278
|
-
} catch {
|
|
279
|
-
gapCount = 0;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const topicStr = topics.length > 0 ? topics.join(", ") : "Not configured";
|
|
284
|
-
|
|
285
|
-
// Determine health
|
|
286
|
-
const health =
|
|
287
|
-
totalPages === 0
|
|
288
|
-
? "🔴 Needs Attention (empty wiki)"
|
|
289
|
-
: sourceCount > 0 && lastIngest !== "Never"
|
|
290
|
-
? "✅ Good"
|
|
291
|
-
: "⚠️ Warning";
|
|
292
|
-
|
|
293
|
-
const report = `📊 LLM Wiki Status
|
|
294
|
-
══════════════════
|
|
295
|
-
Mode: ${mode === "company" ? "🏢 Company" : "👤 Personal"}
|
|
296
|
-
Topics: ${topicStr}
|
|
297
|
-
Sources: ${sourceCount} files
|
|
298
|
-
Wiki Pages: ${totalPages} total
|
|
299
|
-
- Entities: ${entityCount}
|
|
300
|
-
- Concepts: ${conceptCount}
|
|
301
|
-
- Sources: ${sourceCountW}
|
|
302
|
-
- Syntheses: ${synthesisCount}
|
|
303
|
-
- Changes: ${changeCount}
|
|
304
|
-
Last Ingest: ${lastIngest}
|
|
305
|
-
Last Lint: ${lastLint}
|
|
306
|
-
Last Discover: ${lastDiscover}
|
|
307
|
-
Knowledge Gaps: ${gapCount}
|
|
308
|
-
Health: ${health}
|
|
309
|
-
|
|
310
|
-
${
|
|
311
|
-
totalPages === 0
|
|
312
|
-
? "\n💡 **Tip:** Run `/wiki:init` to set up the wiki structure, then add sources to `raw/` and run `/wiki:ingest`."
|
|
313
|
-
: ""
|
|
314
|
-
}`;
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
content: [{ type: "text", text: report }],
|
|
318
|
-
details: {
|
|
319
|
-
mode,
|
|
320
|
-
topics,
|
|
321
|
-
sourceCount,
|
|
322
|
-
totalPages,
|
|
323
|
-
entityCount,
|
|
324
|
-
conceptCount,
|
|
325
|
-
sourceCountW,
|
|
326
|
-
synthesisCount,
|
|
327
|
-
changeCount,
|
|
328
|
-
lastIngest,
|
|
329
|
-
lastLint,
|
|
330
|
-
lastDiscover,
|
|
331
|
-
gapCount,
|
|
332
|
-
health: totalPages === 0 ? "empty" : "good",
|
|
333
|
-
},
|
|
334
|
-
};
|
|
335
|
-
} catch (err: unknown) {
|
|
336
|
-
const error = err as Error;
|
|
337
|
-
return {
|
|
338
|
-
content: [
|
|
339
|
-
{
|
|
340
|
-
type: "text",
|
|
341
|
-
text: `❌ Error checking wiki status: ${error.message}`,
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
details: { error: error.message },
|
|
345
|
-
isError: true,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// ─── wiki_lint_report ──────────────────────────────────
|
|
352
|
-
|
|
353
|
-
pi.registerTool({
|
|
354
|
-
name: "wiki_lint_report",
|
|
355
|
-
label: "Wiki Lint Report",
|
|
356
|
-
description:
|
|
357
|
-
"Run a health check on the wiki. Scans for contradictions, orphans, missing pages, stale claims, " +
|
|
358
|
-
"broken links, knowledge gaps, and quality issues. Reports findings and optionally auto-fixes simple issues.",
|
|
359
|
-
promptSnippet: "Lint the wiki: check for contradictions, orphans, missing pages, gaps",
|
|
360
|
-
promptGuidelines: [
|
|
361
|
-
"Use wiki_lint_report when the user asks to check wiki health, find issues, or clean up the wiki.",
|
|
362
|
-
],
|
|
363
|
-
parameters: Type.Object({
|
|
364
|
-
auto_fix: Type.Optional(
|
|
365
|
-
Type.Boolean({
|
|
366
|
-
description:
|
|
367
|
-
"Auto-fix simple issues (orphans, missing pages, broken links). Contradictions always need human review.",
|
|
368
|
-
default: false,
|
|
369
|
-
}),
|
|
370
|
-
),
|
|
371
|
-
}),
|
|
372
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
373
|
-
try {
|
|
374
|
-
const { auto_fix = false } = params;
|
|
375
|
-
const findings: string[] = [];
|
|
376
|
-
const issues: string[] = [];
|
|
377
|
-
let orphans = 0;
|
|
378
|
-
let missingPages = 0;
|
|
379
|
-
let contradictions = 0;
|
|
380
|
-
|
|
381
|
-
// Scan wiki directory structure
|
|
382
|
-
const wikiDirs = ["entities", "concepts", "sources", "syntheses", "changes"];
|
|
383
|
-
const allPages: string[] = [];
|
|
384
|
-
|
|
385
|
-
for (const dir of wikiDirs) {
|
|
386
|
-
const result = await pi.exec("sh", ["-c", `ls wiki/${dir}/ 2>/dev/null`], { signal });
|
|
387
|
-
const files = result.stdout.trim().split("\n").filter(Boolean);
|
|
388
|
-
for (const file of files) {
|
|
389
|
-
allPages.push(`${dir}/${file.replace(".md", "")}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Build a set of existing page names for link checking
|
|
394
|
-
const existingPages = new Set(allPages);
|
|
395
|
-
|
|
396
|
-
// Scan each page for [[wikilinks]] and check for orphans and missing pages
|
|
397
|
-
const linkCount = new Map<string, number>(); // page -> inbound link count
|
|
398
|
-
for (const page of allPages) {
|
|
399
|
-
linkCount.set(page, 0);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
for (const page of allPages) {
|
|
403
|
-
const fullPath = `wiki/${page}.md`;
|
|
404
|
-
const content = await pi.exec("cat", [fullPath], { signal });
|
|
405
|
-
|
|
406
|
-
if (content.code !== 0) continue;
|
|
407
|
-
|
|
408
|
-
// Find all [[wikilinks]]
|
|
409
|
-
const linkRegex = /\[\[([^\]]+)\]\]/g;
|
|
410
|
-
let execResult: RegExpExecArray | null;
|
|
411
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: regex exec assignment pattern
|
|
412
|
-
while ((execResult = linkRegex.exec(content.stdout)) !== null) {
|
|
413
|
-
const linkedPage = execResult[1];
|
|
414
|
-
if (!existingPages.has(linkedPage)) {
|
|
415
|
-
if (!issues.includes(linkedPage)) {
|
|
416
|
-
missingPages++;
|
|
417
|
-
issues.push(`Missing page: [[${linkedPage}]] (referenced in ${page})`);
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
linkCount.set(linkedPage, (linkCount.get(linkedPage) || 0) + 1);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Find orphans (pages with zero inbound links)
|
|
426
|
-
for (const [page, count] of linkCount) {
|
|
427
|
-
if (count === 0) {
|
|
428
|
-
orphans++;
|
|
429
|
-
findings.push(`Orphan: [[${page}]] has no inbound links`);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Check for contradictions by searching for contradiction markers
|
|
434
|
-
const contradictionResult = await pi.exec(
|
|
435
|
-
"grep",
|
|
436
|
-
["-rl", "⚠️ Contradiction", "wiki/", "2>/dev/null || true"],
|
|
437
|
-
{ signal },
|
|
438
|
-
);
|
|
439
|
-
if (contradictionResult.stdout.trim()) {
|
|
440
|
-
const contradictionFiles = contradictionResult.stdout.trim().split("\n").filter(Boolean);
|
|
441
|
-
contradictions = contradictionFiles.length;
|
|
442
|
-
for (const file of contradictionFiles) {
|
|
443
|
-
findings.push(`Contradiction flagged in: ${file}`);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Build report
|
|
448
|
-
const report = `# Wiki Lint Report
|
|
449
|
-
Generated: ${new Date().toISOString().split("T")[0]}
|
|
450
|
-
|
|
451
|
-
## Summary
|
|
452
|
-
- Total pages scanned: ${allPages.length}
|
|
453
|
-
- Orphans (no inbound links): ${orphans}
|
|
454
|
-
- Missing pages (referenced but not created): ${missingPages}
|
|
455
|
-
- Contradictions flagged: ${contradictions}
|
|
456
|
-
|
|
457
|
-
${
|
|
458
|
-
findings.length > 0
|
|
459
|
-
? `## Findings\n${findings.map((f) => `- ${f}`).join("\n")}`
|
|
460
|
-
: "## Findings\n✅ No issues found!"
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
${issues.length > 0 ? `## Issues\n${issues.map((i) => `- ${i}`).join("\n")}` : ""}
|
|
464
|
-
|
|
465
|
-
${
|
|
466
|
-
auto_fix
|
|
467
|
-
? `\n## Auto-Fix${orphans > 0 || missingPages > 0 ? "\nAuto-fix would: Add cross-references to orphans, create missing pages for top-linked concepts." : "\nNo auto-fixable issues found."}`
|
|
468
|
-
: `\n## To Fix\n${orphans > 0 ? "- Add cross-references to orphan pages" : ""}${missingPages > 0 ? "\n- Create missing pages for frequently-linked concepts" : ""}${contradictions > 0 ? "\n- Review contradictions (requires human judgment)" : ""}`
|
|
469
|
-
}`;
|
|
470
|
-
|
|
471
|
-
// Save report
|
|
472
|
-
const today = new Date().toISOString().split("T")[0];
|
|
473
|
-
await pi.exec("mkdir", ["-p", "outputs"], { signal });
|
|
474
|
-
await pi.exec("sh", ["-c", `cat > outputs/lint-${today}.md << 'EOF'\n${report}\nEOF`], {
|
|
475
|
-
signal,
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
content: [{ type: "text", text: report }],
|
|
480
|
-
details: {
|
|
481
|
-
totalPages: allPages.length,
|
|
482
|
-
orphans,
|
|
483
|
-
missingPages,
|
|
484
|
-
contradictions,
|
|
485
|
-
reportFile: `outputs/lint-${today}.md`,
|
|
486
|
-
findings,
|
|
487
|
-
issues,
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
} catch (err: unknown) {
|
|
491
|
-
const error = err as Error;
|
|
492
|
-
return {
|
|
493
|
-
content: [{ type: "text", text: `❌ Lint error: ${error.message}` }],
|
|
494
|
-
details: { error: error.message },
|
|
495
|
-
isError: true,
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
// ─── wiki_discover_sources ────────────────────────────
|
|
502
|
-
|
|
503
|
-
pi.registerTool({
|
|
504
|
-
name: "wiki_discover_sources",
|
|
505
|
-
label: "Wiki Discover Sources",
|
|
506
|
-
description:
|
|
507
|
-
"Search the web for new source material based on configured topics and known knowledge gaps. " +
|
|
508
|
-
"Saves discovered articles to raw/articles/ with metadata frontmatter. " +
|
|
509
|
-
"Call this to find new content before running wiki_ingest.",
|
|
510
|
-
promptSnippet: "Discover new sources from the web based on topics and gaps",
|
|
511
|
-
promptGuidelines: [
|
|
512
|
-
"Use wiki_discover_sources when the user wants to find new content, expand the wiki, or fill knowledge gaps.",
|
|
513
|
-
"Always save discovered sources to raw/articles/ with proper frontmatter.",
|
|
514
|
-
"Max 8 sources per discover cycle to avoid information overload.",
|
|
515
|
-
],
|
|
516
|
-
parameters: Type.Object({
|
|
517
|
-
topic: Type.Optional(
|
|
518
|
-
Type.String({
|
|
519
|
-
description:
|
|
520
|
-
"Specific topic to search for. Leave empty to use configured topics from config.yaml.",
|
|
521
|
-
}),
|
|
522
|
-
),
|
|
523
|
-
max_sources: Type.Optional(
|
|
524
|
-
Type.Number({
|
|
525
|
-
description: "Maximum sources to discover (default: 5, max: 10).",
|
|
526
|
-
}),
|
|
527
|
-
),
|
|
528
|
-
}),
|
|
529
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
530
|
-
try {
|
|
531
|
-
const { topic, max_sources = 5 } = params;
|
|
532
|
-
const limit = Math.min(max_sources, 10);
|
|
533
|
-
|
|
534
|
-
// Try to read config for topics
|
|
535
|
-
let searchTopics: string[] = [];
|
|
536
|
-
if (topic) {
|
|
537
|
-
searchTopics = [topic];
|
|
538
|
-
} else {
|
|
539
|
-
const configExists = await pi.exec("test", ["-f", "config.yaml"], {
|
|
540
|
-
signal,
|
|
541
|
-
});
|
|
542
|
-
if (configExists.code === 0) {
|
|
543
|
-
const configContent = await pi.exec("cat", ["config.yaml"], {
|
|
544
|
-
signal,
|
|
545
|
-
});
|
|
546
|
-
// Simple YAML-like parsing for topic names
|
|
547
|
-
const lines = configContent.stdout.split("\n");
|
|
548
|
-
let inTopics = false;
|
|
549
|
-
for (const line of lines) {
|
|
550
|
-
if (line.trim().startsWith("topics:")) {
|
|
551
|
-
inTopics = true;
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
if (inTopics && line.trim().startsWith("- name:")) {
|
|
555
|
-
const name = line.split(":")[1].trim().replace(/"/g, "");
|
|
556
|
-
if (name) searchTopics.push(name);
|
|
557
|
-
}
|
|
558
|
-
if (inTopics && line.trim().startsWith("discovery:")) break;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (searchTopics.length === 0) {
|
|
564
|
-
searchTopics = ["latest developments"];
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return {
|
|
568
|
-
content: [
|
|
569
|
-
{
|
|
570
|
-
type: "text",
|
|
571
|
-
text: `🔍 Ready to discover new sources for topic(s): **${searchTopics.join(", ")}**\n\nI'll search for up to ${limit} new sources. For each source found, I will:\n1. Fetch the full content\n2. Save to \`raw/articles/YYYY-MM-DD-slug.md\` with frontmatter\n3. Update \`.discoveries/history.json\`\n\n**To execute discovery, please use \`/wiki:discover\` or tell me to "find new sources on ${searchTopics[0]}"**`,
|
|
572
|
-
},
|
|
573
|
-
],
|
|
574
|
-
details: {
|
|
575
|
-
searchTopics,
|
|
576
|
-
maxSources: limit,
|
|
577
|
-
action: "ready_for_discovery",
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
} catch (err: unknown) {
|
|
581
|
-
const error = err as Error;
|
|
582
|
-
return {
|
|
583
|
-
content: [
|
|
584
|
-
{
|
|
585
|
-
type: "text",
|
|
586
|
-
text: `❌ Error preparing discovery: ${error.message}`,
|
|
587
|
-
},
|
|
588
|
-
],
|
|
589
|
-
details: { error: error.message },
|
|
590
|
-
isError: true,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// ─── wiki_watch ────────────────────────────────────────
|
|
597
|
-
|
|
598
|
-
pi.registerTool({
|
|
599
|
-
name: "wiki_watch",
|
|
600
|
-
label: "Wiki Watch",
|
|
601
|
-
description:
|
|
602
|
-
"Schedule automatic wiki updates at a specified interval. " +
|
|
603
|
-
"Uses pi's scheduling system to run discover → ingest → lint on a cron schedule. " +
|
|
604
|
-
"Supports: daily, weekly, hourly intervals.",
|
|
605
|
-
promptSnippet: "Schedule auto-updates for the wiki",
|
|
606
|
-
promptGuidelines: [
|
|
607
|
-
"Use wiki_watch when the user wants the wiki to stay current automatically.",
|
|
608
|
-
],
|
|
609
|
-
parameters: Type.Object({
|
|
610
|
-
interval: Type.String({
|
|
611
|
-
description:
|
|
612
|
-
"Update interval: 'daily', 'weekly', 'hourly', or 'stop' to cancel existing schedules.",
|
|
613
|
-
}),
|
|
614
|
-
}),
|
|
615
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
616
|
-
try {
|
|
617
|
-
const { interval } = params;
|
|
618
|
-
|
|
619
|
-
if (interval === "stop") {
|
|
620
|
-
return {
|
|
621
|
-
content: [
|
|
622
|
-
{
|
|
623
|
-
type: "text",
|
|
624
|
-
text:
|
|
625
|
-
"🛑 To stop wiki auto-updates, run:\n\n" +
|
|
626
|
-
"```\nschedule_prompt action=list\n```\n" +
|
|
627
|
-
"Find the wiki job IDs, then:\n\n" +
|
|
628
|
-
"```\nschedule_prompt action=remove jobId=<id>\n```",
|
|
629
|
-
},
|
|
630
|
-
],
|
|
631
|
-
details: { action: "stop_instructions" },
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
let cronSchedule: string;
|
|
636
|
-
let label: string;
|
|
637
|
-
|
|
638
|
-
switch (interval) {
|
|
639
|
-
case "daily":
|
|
640
|
-
cronSchedule = "0 0 8 * * *";
|
|
641
|
-
label = "Daily at 8:00 AM";
|
|
642
|
-
break;
|
|
643
|
-
case "weekly":
|
|
644
|
-
cronSchedule = "0 0 9 * * 1";
|
|
645
|
-
label = "Weekly on Monday at 9:00 AM";
|
|
646
|
-
break;
|
|
647
|
-
case "hourly":
|
|
648
|
-
cronSchedule = "0 0 * * * *";
|
|
649
|
-
label = "Every hour";
|
|
650
|
-
break;
|
|
651
|
-
default:
|
|
652
|
-
return {
|
|
653
|
-
content: [
|
|
654
|
-
{
|
|
655
|
-
type: "text",
|
|
656
|
-
text: `❌ Unknown interval: "${interval}". Use: daily, weekly, hourly, or stop.`,
|
|
657
|
-
},
|
|
658
|
-
],
|
|
659
|
-
details: {},
|
|
660
|
-
isError: true,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return {
|
|
665
|
-
content: [
|
|
666
|
-
{
|
|
667
|
-
type: "text",
|
|
668
|
-
text: `⏰ To set up ${label} wiki updates, run:\n\n\`\`\`\nschedule_prompt action=add schedule="${cronSchedule}" prompt="Run /wiki:run for the LLM Wiki" name="llm-wiki-autoupdate"\n\`\`\`\n\nThis will automatically discover new sources, ingest them, and lint the wiki at the scheduled time.`,
|
|
669
|
-
},
|
|
670
|
-
],
|
|
671
|
-
details: {
|
|
672
|
-
interval,
|
|
673
|
-
cronSchedule,
|
|
674
|
-
label,
|
|
675
|
-
scheduleCommand: `schedule_prompt action=add schedule="${cronSchedule}" prompt="Run /wiki:run for the LLM Wiki" name="llm-wiki-autoupdate"`,
|
|
676
|
-
},
|
|
677
|
-
};
|
|
678
|
-
} catch (err: unknown) {
|
|
679
|
-
const error = err as Error;
|
|
680
|
-
return {
|
|
681
|
-
content: [
|
|
682
|
-
{
|
|
683
|
-
type: "text",
|
|
684
|
-
text: `❌ Error setting up watch: ${error.message}`,
|
|
685
|
-
},
|
|
686
|
-
],
|
|
687
|
-
details: { error: error.message },
|
|
688
|
-
isError: true,
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
},
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// Notify on load
|
|
695
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
696
|
-
const tools = [
|
|
697
|
-
"wiki_ingest",
|
|
698
|
-
"wiki_status_report",
|
|
699
|
-
"wiki_lint_report",
|
|
700
|
-
"wiki_discover_sources",
|
|
701
|
-
"wiki_watch",
|
|
702
|
-
];
|
|
703
|
-
ctx.ui.setStatus("llm-wiki", `🧠 LLM Wiki loaded (${tools.length} tools)`);
|
|
704
|
-
});
|
|
705
|
-
}
|