@zosmaai/pi-llm-wiki 0.1.6 → 0.2.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.
@@ -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
- }